Browse Source

Merge branch 'master' of github.com:Kozea/pygal

pull/109/merge
Guillaume Ayoub 11 years ago
parent
commit
fb2e6b1ef1
  1. 23
      CHANGELOG
  2. 101
      demo/moulinrouge/tests.py
  3. 10
      pygal/__init__.py
  4. 11
      pygal/config.py
  5. 6
      pygal/css/style.css
  6. 18
      pygal/ghost.py
  7. 3
      pygal/graph/__init__.py
  8. 2
      pygal/graph/box.py
  9. 328
      pygal/graph/fr.departments.svg
  10. 91
      pygal/graph/fr.regions.svg
  11. 387
      pygal/graph/frenchmap.py
  12. 2
      pygal/graph/funnel.py
  13. 2
      pygal/graph/graph.py
  14. 35
      pygal/graph/line.py
  15. 2
      pygal/graph/radar.py
  16. 2
      pygal/graph/stackedbar.py
  17. 4
      pygal/graph/worldmap.py
  18. 368
      pygal/graph/worldmap.svg
  19. 7
      pygal/i18n.py
  20. 10
      pygal/test/__init__.py
  21. 14
      pygal/test/test_config.py
  22. 22
      pygal/test/test_graph.py
  23. 19
      pygal/test/test_line.py
  24. 10
      pygal/test/test_sparktext.py
  25. 27
      pygal/util.py
  26. 6
      pygal_gen.py
  27. 2
      setup.py

23
CHANGELOG

@ -1,9 +1,30 @@
TO BE RELEASED: V 1.4.0 V 1.4.6 -- UNRELEASED
Add support for \n separated multiline titles (thanks sirlark)
V 1.4.5
Fix y_labels map iterator exhaustion in python 3
V 1.4.4
Fix division by zero in spark text (thanks laserpony)
Fix config metaclass problem in python 3
Fix --version in pygal_gen
V 1.4.3
Allow arbitrary number of x-labels on line plot (thanks nsmgr8)
V 1.4.2
Fix broken tests
V 1.4.1
Fix value formatting in maps
V 1.4.0
Finally a changelog ! Finally a changelog !
Hopefully fix weird major scale algorithm Hopefully fix weird major scale algorithm
Add options to customize major labels (y_labels_major, y_labels_major_every, y_labels_major_count) Add options to customize major labels (y_labels_major, y_labels_major_every, y_labels_major_count)
Css can now be inline with the "inline:" prefix Css can now be inline with the "inline:" prefix
Visited links bug fixed Visited links bug fixed
Add french maps by department and region (This will be externalized in an extension later)
V 1.3.x V 1.3.x
Whisker Box Plot Whisker Box Plot

101
demo/moulinrouge/tests.py

@ -2,8 +2,11 @@
# This file is part of pygal # This file is part of pygal
from pygal import ( from pygal import (
Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, XY, Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, XY,
CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram, Box) CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram, Box,
FrenchMap_Departments, FrenchMap_Regions)
from pygal.style import styles from pygal.style import styles
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from random import randint, choice
def get_test_routes(app): def get_test_routes(app):
@ -89,6 +92,15 @@ def get_test_routes(app):
'1 12 123 1234 12345 123456 1234567 12345678 123456789 1234567890') '1 12 123 1234 12345 123456 1234567 12345678 123456789 1234567890')
return bar.render_response() return bar.render_response()
@app.route('/test/multiline_title')
def test_multiline_title():
bar = Bar()
bar.add('Looooooooooooooooooooooooooooooooooong', [2, None, 12])
bar.title = (
'First line \n Second line \n Third line'
)
return bar.render_response()
@app.route('/test/long_labels') @app.route('/test/long_labels')
def test_long_labels(): def test_long_labels():
bar = Bar() bar = Bar()
@ -253,6 +265,16 @@ def get_test_routes(app):
hist.add('2', [(2, 2, 8)]) hist.add('2', [(2, 2, 8)])
return hist.render_response() return hist.render_response()
@app.route('/test/ylabels')
def test_ylabels():
chart = Line()
chart.x_labels = 'Red', 'Blue', 'Green'
chart.y_labels = .0001, .0003, .0004, .00045, .0005
chart.add('line', [.0002, .0005, .00035])
return chart.render_response()
@app.route('/test/secondary/<chart>') @app.route('/test/secondary/<chart>')
def test_secondary_for(chart): def test_secondary_for(chart):
chart = CHARTS_BY_NAME[chart](fill=True) chart = CHARTS_BY_NAME[chart](fill=True)
@ -329,14 +351,13 @@ def get_test_routes(app):
@app.route('/test/worldmap') @app.route('/test/worldmap')
def test_worldmap(): def test_worldmap():
import random wmap = Worldmap(style=choice(list(styles.values())))
map = Worldmap(style=random.choice(styles.values()))
wmap.add('1st', [('fr', 100), ('us', 10)])
map.add('1st', [('fr', 100), ('us', 10)]) wmap.add('2nd', [('jp', 1), ('ru', 7), ('uk', 0)])
map.add('2nd', [('jp', 1), ('ru', 7), ('uk', 0)]) wmap.add('3rd', ['ch', 'cz', 'ca', 'cn'])
map.add('3rd', ['ch', 'cz', 'ca', 'cn']) wmap.add('4th', {'br': 12, 'bo': 1, 'bu': 23, 'fr': 34})
map.add('4th', {'br': 12, 'bo': 1, 'bu': 23, 'fr': 34}) wmap.add('5th', [{
map.add('5th', [{
'value': ('tw', 10), 'value': ('tw', 10),
'label': 'First label', 'label': 'First label',
'xlink': 'http://google.com?q=tw' 'xlink': 'http://google.com?q=tw'
@ -348,8 +369,64 @@ def get_test_routes(app):
'value': ('mw', 40), 'value': ('mw', 40),
'label': 'Last' 'label': 'Last'
}]) }])
map.add('6th', [3, 5, 34, 12]) wmap.add('6th', [3, 5, 34, 12])
map.title = 'World Map !!' wmap.title = 'World Map !!'
return map.render_response() return wmap.render_response()
@app.route('/test/frenchmapdepartments')
def test_frenchmapdepartments():
fmap = FrenchMap_Departments(style=choice(list(styles.values())))
for i in range(10):
fmap.add('s%d' % i, [
(choice(list(DEPARTMENTS.keys())), randint(0, 100)) for _ in range(randint(1, 5))])
fmap.add('links', [{
'value': ('69', 10),
'label': '\o/',
'xlink': 'http://google.com?q=69'
}, {
'value': ('42', 20),
'label': 'Y',
}])
fmap.add('6th', [3, 5, 34, 12])
fmap.title = 'French map'
return fmap.render_response()
@app.route('/test/frenchmapregions')
def test_frenchmapregions():
fmap = FrenchMap_Regions(style=choice(list(styles.values())))
for i in range(10):
fmap.add('s%d' % i, [
(choice(list(REGIONS.keys())), randint(0, 100))
for _ in range(randint(1, 5))])
fmap.add('links', [{
'value': ('02', 10),
'label': '\o/',
'xlink': 'http://google.com?q=69'
}, {
'value': ('72', 20),
'label': 'Y',
}])
fmap.add('6th', [91, 2, 41])
fmap.title = 'French map'
return fmap.render_response()
@app.route('/test/labels')
def test_labels():
line = Line()
line.add('test1', range(100))
line.x_labels = map(str, range(11))
return line.render_response()
@app.route('/test/major_dots')
def test_major_dots():
line = Line(x_labels_major_every=3, show_only_major_dots=True)
line.add('test', range(12))
line.x_labels = [
'lol', 'lol1', 'lol2', 'lol3', 'lol4', 'lol5',
'lol6', 'lol7', 'lol8', 'lol9', 'lol10', 'lol11']
line.x_labels_major = ['lol3']
return line.render_response()
return filter(lambda x: x.startswith('test'), locals()) return filter(lambda x: x.startswith('test'), locals())

10
pygal/__init__.py

@ -21,20 +21,20 @@ Pygal - A python svg graph plotting library
""" """
__version__ = '1.3.1' __version__ = '1.4.6'
import sys import sys
from pygal.config import Config from pygal.config import Config
from pygal.ghost import Ghost from pygal.ghost import Ghost, REAL_CHARTS
from pygal.graph import CHARTS_NAMES
CHARTS = [] CHARTS = []
CHARTS_BY_NAME = {} CHARTS_BY_NAME = {}
for NAME in CHARTS_NAMES: for NAME in REAL_CHARTS.keys():
_CHART = type(NAME, (Ghost,), {}) _CHART = type(NAME, (Ghost,), {})
CHARTS.append(_CHART) CHARTS.append(_CHART)
CHARTS_BY_NAME[NAME] = _CHART CHARTS_BY_NAME[NAME] = _CHART
setattr(sys.modules[__name__], NAME, _CHART) setattr(sys.modules[__name__], NAME, _CHART)
__all__ = CHARTS_NAMES + [Config.__name__, 'CHARTS', 'CHARTS_BY_NAME'] __all__ = list(CHARTS_BY_NAME.keys()) + [
Config.__name__, 'CHARTS', 'CHARTS_BY_NAME']

11
pygal/config.py

@ -101,11 +101,9 @@ class MetaConfig(type):
return type.__new__(mcs, classname, bases, classdict) return type.__new__(mcs, classname, bases, classdict)
class Config(object): class Config(MetaConfig('ConfigBase', (object,), {})):
"""Class holding config values""" """Class holding config values"""
__metaclass__ = MetaConfig
style = Key( style = Key(
DefaultStyle, Style, "Style", "Style holding values injected in css") DefaultStyle, Style, "Style", "Style holding values injected in css")
@ -142,6 +140,10 @@ class Config(object):
show_dots = Key(True, bool, "Look", "Set to false to remove dots") show_dots = Key(True, bool, "Look", "Set to false to remove dots")
show_only_major_dots = Key(
False, bool, "Look",
"Set to true to show only major dots according to their majored label")
dots_size = Key(2.5, float, "Look", "Radius of the dots") dots_size = Key(2.5, float, "Look", "Radius of the dots")
stroke = Key( stroke = Key(
@ -230,7 +232,8 @@ class Config(object):
0, int, "Label", "Specify y labels rotation angles", "in degrees") 0, int, "Label", "Specify y labels rotation angles", "in degrees")
x_label_format = Key( x_label_format = Key(
"%Y-%m-%d %H:%M:%S.%f", str, "Label", "Date format for strftime to display the DateY X labels") "%Y-%m-%d %H:%M:%S.%f", str, "Label",
"Date format for strftime to display the DateY X labels")
############ Value ############ ############ Value ############
human_readable = Key( human_readable = Key(

6
pygal/css/style.css

@ -113,9 +113,9 @@
fill: {{ style.foreground_light }}; fill: {{ style.foreground_light }};
} }
{{ id }}.country { {{ id }}.map-element {
fill: {{ style.foreground }}; fill: {{ style.foreground }};
stroke: {{ style.plot_background }} !important; stroke: {{ style.foreground_dark }} !important;
opacity: .9; opacity: .9;
stroke-width: 3; stroke-width: 3;
-webkit-transition: 250ms; -webkit-transition: 250ms;
@ -124,7 +124,7 @@
transition: 250ms; transition: 250ms;
} }
{{ id }}.country:hover { {{ id }}.map-element:hover {
opacity: 1; opacity: 1;
stroke-width: 10; stroke-width: 10;
} }

18
pygal/ghost.py

@ -33,12 +33,23 @@ from pygal.util import prepare_values
from uuid import uuid4 from uuid import uuid4
class ChartCollection(object):
pass
REAL_CHARTS = {} REAL_CHARTS = {}
for NAME in CHARTS_NAMES: for NAME in CHARTS_NAMES:
mod_name = 'pygal.graph.%s' % NAME.lower() mod_name = 'pygal.graph.%s' % NAME.lower()
__import__(mod_name) __import__(mod_name)
mod = sys.modules[mod_name] mod = sys.modules[mod_name]
REAL_CHARTS[NAME] = getattr(mod, NAME) chart = getattr(mod, NAME)
if issubclass(chart, ChartCollection):
for name, chart in chart.__dict__.items():
if name.startswith('_'):
continue
REAL_CHARTS['%s_%s' % (NAME, name)] = chart
else:
REAL_CHARTS[NAME] = chart
class Ghost(object): class Ghost(object):
@ -135,6 +146,11 @@ class Ghost(object):
vmax = max(values) vmax = max(values)
if relative_to is None: if relative_to is None:
relative_to = min(values) relative_to = min(values)
if (vmax - relative_to) == 0:
chart = bars[0] * len(values)
return chart
divisions = len(bars) - 1 divisions = len(bars) - 1
for value in values: for value in values:
chart += bars[int(divisions * chart += bars[int(divisions *

3
pygal/graph/__init__.py

@ -40,5 +40,6 @@ CHARTS_NAMES = [
'Worldmap', 'Worldmap',
'SupranationalWorldmap', 'SupranationalWorldmap',
'Histogram', 'Histogram',
'Box' 'Box',
'FrenchMap'
] ]

2
pygal/graph/box.py

@ -74,7 +74,7 @@ class Box(Graph):
y_pos = compute_scale( y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min self._box.ymin, self._box.ymax, self.logarithmic, self.order_min
) if not self.y_labels else map(float, self.y_labels) ) if not self.y_labels else list(map(float, self.y_labels))
self._x_labels = self.x_labels and list(zip(self.x_labels, [ self._x_labels = self.x_labels and list(zip(self.x_labels, [
(i + .5) / self._order for i in range(self._order)])) (i + .5) / self._order for i in range(self._order)]))

328
pygal/graph/fr.departments.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 584 KiB

91
pygal/graph/fr.regions.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 370 KiB

387
pygal/graph/frenchmap.py

@ -0,0 +1,387 @@
# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2014 Kozea
#
# This library is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
"""
Worldmap chart
"""
from __future__ import division
from collections import defaultdict
from pygal.ghost import ChartCollection
from pygal.util import cut, cached_property, decorate
from pygal.graph.graph import Graph
from pygal._compat import u
from numbers import Number
from lxml import etree
import os
DEPARTMENTS = {
'01': u("Ain"),
'02': u("Aisne"),
'03': u("Allier"),
'04': u("Alpes-de-Haute-Provence"),
'05': u("Hautes-Alpes"),
'06': u("Alpes-Maritimes"),
'07': u("Ardèche"),
'08': u("Ardennes"),
'09': u("Ariège"),
'10': u("Aube"),
'11': u("Aude"),
'12': u("Aveyron"),
'13': u("Bouches-du-Rhône"),
'14': u("Calvados"),
'15': u("Cantal"),
'16': u("Charente"),
'17': u("Charente-Maritime"),
'18': u("Cher"),
'19': u("Corrèze"),
'2A': u("Corse-du-Sud"),
'2B': u("Haute-Corse"),
'21': u("Côte-d'Or"),
'22': u("Côtes-d'Armor"),
'23': u("Creuse"),
'24': u("Dordogne"),
'25': u("Doubs"),
'26': u("Drôme"),
'27': u("Eure"),
'28': u("Eure-et-Loir"),
'29': u("Finistère"),
'30': u("Gard"),
'31': u("Haute-Garonne"),
'32': u("Gers"),
'33': u("Gironde"),
'34': u("Hérault"),
'35': u("Ille-et-Vilaine"),
'36': u("Indre"),
'37': u("Indre-et-Loire"),
'38': u("Isère"),
'39': u("Jura"),
'40': u("Landes"),
'41': u("Loir-et-Cher"),
'42': u("Loire"),
'43': u("Haute-Loire"),
'44': u("Loire-Atlantique"),
'45': u("Loiret"),
'46': u("Lot"),
'47': u("Lot-et-Garonne"),
'48': u("Lozère"),
'49': u("Maine-et-Loire"),
'50': u("Manche"),
'51': u("Marne"),
'52': u("Haute-Marne"),
'53': u("Mayenne"),
'54': u("Meurthe-et-Moselle"),
'55': u("Meuse"),
'56': u("Morbihan"),
'57': u("Moselle"),
'58': u("Nièvre"),
'59': u("Nord"),
'60': u("Oise"),
'61': u("Orne"),
'62': u("Pas-de-Calais"),
'63': u("Puy-de-Dôme"),
'64': u("Pyrénées-Atlantiques"),
'65': u("Hautes-Pyrénées"),
'66': u("Pyrénées-Orientales"),
'67': u("Bas-Rhin"),
'68': u("Haut-Rhin"),
'69': u("Rhône"),
'70': u("Haute-Saône"),
'71': u("Saône-et-Loire"),
'72': u("Sarthe"),
'73': u("Savoie"),
'74': u("Haute-Savoie"),
'75': u("Paris"),
'76': u("Seine-Maritime"),
'77': u("Seine-et-Marne"),
'78': u("Yvelines"),
'79': u("Deux-Sèvres"),
'80': u("Somme"),
'81': u("Tarn"),
'82': u("Tarn-et-Garonne"),
'83': u("Var"),
'84': u("Vaucluse"),
'85': u("Vendée"),
'86': u("Vienne"),
'87': u("Haute-Vienne"),
'88': u("Vosges"),
'89': u("Yonne"),
'90': u("Territoire de Belfort"),
'91': u("Essonne"),
'92': u("Hauts-de-Seine"),
'93': u("Seine-Saint-Denis"),
'94': u("Val-de-Marne"),
'95': u("Val-d'Oise"),
'971': u("Guadeloupe"),
'972': u("Martinique"),
'973': u("Guyane"),
'974': u("Réunion"),
# Not a area anymore but in case of...
'975': u("Saint Pierre et Miquelon"),
'976': u("Mayotte")
}
REGIONS = {
'11': u("Île-de-France"),
'21': u("Champagne-Ardenne"),
'22': u("Picardie"),
'23': u("Haute-Normandie"),
'24': u("Centre"),
'25': u("Basse-Normandie"),
'26': u("Bourgogne"),
'31': u("Nord-Pas-de-Calais"),
'41': u("Lorraine"),
'42': u("Alsace"),
'43': u("Franche-Comté"),
'52': u("Pays-de-la-Loire"),
'53': u("Bretagne"),
'54': u("Poitou-Charentes"),
'72': u("Aquitaine"),
'73': u("Midi-Pyrénées"),
'74': u("Limousin"),
'82': u("Rhône-Alpes"),
'83': u("Auvergne"),
'91': u("Languedoc-Roussillon"),
'93': u("Provence-Alpes-Côte d'Azur"),
'94': u("Corse"),
'01': u("Guadeloupe"),
'02': u("Martinique"),
'03': u("Guyane"),
'04': u("Réunion"),
# Not a region anymore but in case of...
'05': u("Saint Pierre et Miquelon"),
'06': u("Mayotte")
}
with open(os.path.join(
os.path.dirname(__file__),
'fr.departments.svg')) as file:
DPT_MAP = file.read()
with open(os.path.join(
os.path.dirname(__file__),
'fr.regions.svg')) as file:
REG_MAP = file.read()
class FrenchMapDepartments(Graph):
"""French department map"""
_dual = True
x_labels = list(DEPARTMENTS.keys())
area_names = DEPARTMENTS
area_prefix = 'z'
svg_map = DPT_MAP
@cached_property
def _values(self):
"""Getter for series values (flattened)"""
return [val[1]
for serie in self.series
for val in serie.values
if val[1] is not None]
def _plot(self):
map = etree.fromstring(self.svg_map)
map.set('width', str(self.view.width))
map.set('height', str(self.view.height))
for i, serie in enumerate(self.series):
safe_vals = list(filter(
lambda x: x is not None, cut(serie.values, 1)))
if not safe_vals:
continue
min_ = min(safe_vals)
max_ = max(safe_vals)
for j, (area_code, value) in enumerate(serie.values):
if isinstance(area_code, Number):
area_code = '%2d' % area_code
if value is None:
continue
if max_ == min_:
ratio = 1
else:
ratio = .3 + .7 * (value - min_) / (max_ - min_)
areae = map.xpath(
"//*[contains(concat(' ', normalize-space(@class), ' '),"
" ' %s%s ')]" % (self.area_prefix, area_code))
if not areae:
continue
for area in areae:
cls = area.get('class', '').split(' ')
cls.append('color-%d' % i)
area.set('class', ' '.join(cls))
area.set('style', 'fill-opacity: %f' % (ratio))
metadata = serie.metadata.get(j)
if metadata:
parent = area.getparent()
node = decorate(self.svg, area, metadata)
if node != area:
area.remove(node)
index = parent.index(area)
parent.remove(area)
node.append(area)
parent.insert(index, node)
last_node = len(area) > 0 and area[-1]
if last_node is not None and last_node.tag == 'title':
title_node = last_node
text = title_node.text + '\n'
else:
title_node = self.svg.node(area, 'title')
text = ''
title_node.text = text + '[%s] %s: %s' % (
serie.title,
self.area_names[area_code], self._format(value))
self.nodes['plot'].append(map)
class FrenchMapRegions(FrenchMapDepartments):
"""French regions map"""
x_labels = list(REGIONS.keys())
area_names = REGIONS
area_prefix = 'a'
svg_map = REG_MAP
class FrenchMap(ChartCollection):
Regions = FrenchMapRegions
Departments = FrenchMapDepartments
DEPARTMENTS_REGIONS = {
"01": "82",
"02": "22",
"03": "83",
"04": "93",
"05": "93",
"06": "93",
"07": "82",
"08": "21",
"09": "73",
"10": "21",
"11": "91",
"12": "73",
"13": "93",
"14": "25",
"15": "83",
"16": "54",
"17": "54",
"18": "24",
"19": "74",
"21": "26",
"22": "53",
"23": "74",
"24": "72",
"25": "43",
"26": "82",
"27": "23",
"28": "24",
"29": "53",
"2A": "94",
"2B": "94",
"30": "91",
"31": "73",
"32": "73",
"33": "72",
"34": "91",
"35": "53",
"36": "24",
"37": "24",
"38": "82",
"39": "43",
"40": "72",
"41": "24",
"42": "82",
"43": "83",
"44": "52",
"45": "24",
"46": "73",
"47": "72",
"48": "91",
"49": "52",
"50": "25",
"51": "21",
"52": "21",
"53": "52",
"54": "41",
"55": "41",
"56": "53",
"57": "41",
"58": "26",
"59": "31",
"60": "22",
"61": "25",
"62": "31",
"63": "83",
"64": "72",
"65": "73",
"66": "91",
"67": "42",
"68": "42",
"69": "82",
"70": "43",
"71": "26",
"72": "52",
"73": "82",
"74": "82",
"75": "11",
"76": "23",
"77": "11",
"78": "11",
"79": "54",
"80": "22",
"81": "73",
"82": "73",
"83": "93",
"84": "93",
"85": "52",
"86": "54",
"87": "74",
"88": "41",
"89": "26",
"90": "43",
"91": "11",
"92": "11",
"93": "11",
"94": "11",
"95": "11",
"971": "01",
"972": "02",
"973": "03",
"974": "04",
"975": "05",
"976": "06"
}
def aggregate_regions(values):
if isinstance(values, dict):
values = values.items()
regions = defaultdict(int)
for department, value in values:
regions[DEPARTMENTS_REGIONS[department]] += value
return list(regions.items())

2
pygal/graph/funnel.py

@ -85,7 +85,7 @@ class Funnel(Graph):
y_pos = compute_scale( y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min self._box.ymin, self._box.ymax, self.logarithmic, self.order_min
) if not self.y_labels else map(float, self.y_labels) ) if not self.y_labels else list(map(float, self.y_labels))
self._x_labels = list( self._x_labels = list(
zip(cut(self.series, 'title'), zip(cut(self.series, 'title'),

2
pygal/graph/graph.py

@ -404,7 +404,7 @@ class Graph(BaseGraph):
if self.title: if self.title:
for i, title_line in enumerate(self.title, 1): for i, title_line in enumerate(self.title, 1):
self.svg.node( self.svg.node(
self.nodes['title'], 'text', class_='title', self.nodes['title'], 'text', class_='title plot_title',
x=self.width / 2, x=self.width / 2,
y=i * (self.title_font_size + self.spacing) y=i * (self.title_font_size + self.spacing)
).text = title_line ).text = title_line

35
pygal/graph/line.py

@ -67,10 +67,30 @@ class Line(Graph):
points = serie.points points = serie.points
view_values = list(map(self.view, points)) view_values = list(map(self.view, points))
if self.show_dots: if self.show_dots:
if self.show_only_major_dots:
major_dots_index = []
if self.x_labels:
if self.x_labels_major:
major_dots_index = []
for major in self.x_labels_major:
start = -1
while True:
try:
index = self.x_labels.index(
major, start + 1)
except ValueError:
break
else:
major_dots_index.append(index)
start = index
elif self.x_labels_major_every:
major_dots_index = range(
0, len(self.x_labels), self.x_labels_major_every)
for i, (x, y) in enumerate(view_values): for i, (x, y) in enumerate(view_values):
if None in (x, y): if None in (x, y) or (self.show_only_major_dots
and i not in major_dots_index):
continue continue
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
classes = [] classes = []
if x > self.view.width / 2: if x > self.view.width / 2:
@ -110,7 +130,14 @@ class Line(Graph):
self._points(x_pos) self._points(x_pos)
if self.x_labels: if self.x_labels:
self._x_labels = list(zip(self.x_labels, x_pos)) label_len = len(self.x_labels)
if label_len != self._len:
label_pos = [0.5] if label_len == 1 else [
x / (label_len - 1) for x in range(label_len)
]
self._x_labels = list(zip(self.x_labels, label_pos))
else:
self._x_labels = list(zip(self.x_labels, x_pos))
else: else:
self._x_labels = None self._x_labels = None
@ -124,7 +151,7 @@ class Line(Graph):
y_pos = compute_scale( y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min self._box.ymin, self._box.ymax, self.logarithmic, self.order_min
) if not self.y_labels else map(float, self.y_labels) ) if not self.y_labels else list(map(float, self.y_labels))
self._y_labels = list(zip(map(self._format, y_pos), y_pos)) self._y_labels = list(zip(map(self._format, y_pos), y_pos))

2
pygal/graph/radar.py

@ -182,7 +182,7 @@ class Radar(Line):
y_pos = compute_scale( y_pos = compute_scale(
self._rmin, self._rmax, self.logarithmic, self.order_min, self._rmin, self._rmax, self.logarithmic, self.order_min,
max_scale=8 max_scale=8
) if not self.y_labels else map(int, self.y_labels) ) if not self.y_labels else list(map(int, self.y_labels))
self._x_labels = self.x_labels and list(zip(self.x_labels, x_pos)) self._x_labels = self.x_labels and list(zip(self.x_labels, x_pos))
self._y_labels = list(zip(map(self._format, y_pos), y_pos)) self._y_labels = list(zip(map(self._format, y_pos), y_pos))

2
pygal/graph/stackedbar.py

@ -68,7 +68,7 @@ class StackedBar(Bar):
self._points(x_pos) self._points(x_pos)
y_pos = compute_scale( y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min self._box.ymin, self._box.ymax, self.logarithmic, self.order_min
) if not self.y_labels else map(float, self.y_labels) ) if not self.y_labels else list(map(float, self.y_labels))
self._x_ranges = zip(x_pos, x_pos[1:]) self._x_ranges = zip(x_pos, x_pos[1:])
self._x_labels = self.x_labels and list(zip(self.x_labels, [ self._x_labels = self.x_labels and list(zip(self.x_labels, [

4
pygal/graph/worldmap.py

@ -102,8 +102,8 @@ class Worldmap(Graph):
else: else:
title_node = self.svg.node(country, 'title') title_node = self.svg.node(country, 'title')
text = '' text = ''
title_node.text = text + '[%s] %s: %d' % ( title_node.text = text + '[%s] %s: %s' % (
serie.title, serie.title,
self.country_names[country_code], value) self.country_names[country_code], self._format(value))
self.nodes['plot'].append(map) self.nodes['plot'].append(map)

368
pygal/graph/worldmap.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 752 KiB

After

Width:  |  Height:  |  Size: 754 KiB

7
pygal/i18n.py

@ -202,6 +202,7 @@ NAFTA = ['ca', 'mx', 'us']
SUPRANATIONAL = {'europe': EUROPE, 'oecd': OECD, 'nafta': NAFTA, 'eur': EUR} SUPRANATIONAL = {'europe': EUROPE, 'oecd': OECD, 'nafta': NAFTA, 'eur': EUR}
def set_countries(countries): def set_countries(countries, clear=False):
global COUNTRIES if clear:
COUNTRIES = countries COUNTRIES.clear()
COUNTRIES.update(countries)

10
pygal/test/__init__.py

@ -21,7 +21,7 @@ import pygal
from pygal.util import cut from pygal.util import cut
from datetime import datetime from datetime import datetime
from pygal.i18n import COUNTRIES from pygal.i18n import COUNTRIES
COUNTRY_KEYS = list(COUNTRIES.keys()) from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
def get_data(i): def get_data(i):
@ -58,8 +58,14 @@ def adapt(chart, data):
data = cut(data) data = cut(data)
if isinstance(chart, pygal.Worldmap): if isinstance(chart, pygal.Worldmap):
return list(map(lambda x: COUNTRY_KEYS[x % len(COUNTRIES)] return list(map(lambda x: list(COUNTRIES.keys())[x % len(COUNTRIES)]
if x is not None else None, data)) if x is not None else None, data))
elif isinstance(chart, pygal.FrenchMap_Regions):
return list(map(lambda x: list(REGIONS.keys())[x % len(REGIONS)]
if x is not None else None, data))
elif isinstance(chart, pygal.FrenchMap_Departments):
return list(map(lambda x: list(DEPARTMENTS.keys())[x % len(DEPARTMENTS)]
if x is not None else None, data))
return data return data

14
pygal/test/test_config.py

@ -18,10 +18,12 @@
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import ( from pygal import (
Line, Dot, Pie, Radar, Config, Bar, Funnel, Worldmap, Line, Dot, Pie, Radar, Config, Bar, Funnel, Worldmap,
SupranationalWorldmap, Histogram, Gauge, Box) SupranationalWorldmap, Histogram, Gauge, Box,
FrenchMap_Regions, FrenchMap_Departments)
from pygal._compat import u from pygal._compat import u
from pygal.test.utils import texts from pygal.test.utils import texts
from pygal.test import pytest_generate_tests, make_data from pygal.test import pytest_generate_tests, make_data
from uuid import uuid4
def test_config_behaviours(): def test_config_behaviours():
@ -270,7 +272,8 @@ def test_no_data():
def test_include_x_axis(Chart): def test_include_x_axis(Chart):
chart = Chart() chart = Chart()
if Chart in (Pie, Radar, Funnel, Dot, Gauge, Worldmap, if Chart in (Pie, Radar, Funnel, Dot, Gauge, Worldmap,
SupranationalWorldmap, Histogram, Box): SupranationalWorldmap, Histogram, Box,
FrenchMap_Regions, FrenchMap_Departments):
return return
if not chart.cls._dual: if not chart.cls._dual:
data = 100, 200, 150 data = 100, 200, 150
@ -292,7 +295,7 @@ def test_include_x_axis(Chart):
def test_css(Chart): def test_css(Chart):
css = "{{ id }}text { fill: #bedead; }\n" css = "{{ id }}text { fill: #bedead; }\n"
css_file = '/tmp/pygal_custom_style.css' css_file = '/tmp/pygal_custom_style-%s.css' % uuid4()
with open(css_file, 'w') as f: with open(css_file, 'w') as f:
f.write(css) f.write(css)
@ -314,3 +317,8 @@ def test_inline_css(Chart):
chart.add('/', [10, 1, 5]) chart.add('/', [10, 1, 5])
svg = chart.render().decode('utf-8') svg = chart.render().decode('utf-8')
assert '#bedead' in svg assert '#bedead' in svg
def test_meta_config():
from pygal.config import CONFIG_ITEMS
assert all(c.name != 'Unbound' for c in CONFIG_ITEMS)

22
pygal/test/test_graph.py

@ -21,6 +21,7 @@ import pygal
import uuid import uuid
import sys import sys
from pygal import i18n from pygal import i18n
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from pygal.util import cut from pygal.util import cut
from pygal._compat import u from pygal._compat import u
from pygal.test import pytest_generate_tests, make_data from pygal.test import pytest_generate_tests, make_data
@ -73,7 +74,11 @@ def test_metadata(Chart):
elif Chart == pygal.XY: elif Chart == pygal.XY:
v = list(map(lambda x: (x, x + 1), v)) v = list(map(lambda x: (x, x + 1), v))
elif Chart == pygal.Worldmap or Chart == pygal.SupranationalWorldmap: elif Chart == pygal.Worldmap or Chart == pygal.SupranationalWorldmap:
v = list(map(lambda x: x, i18n.COUNTRIES)) v = [(i, k) for k, i in enumerate(i18n.COUNTRIES.keys())]
elif Chart == pygal.FrenchMap_Regions:
v = [(i, k) for k, i in enumerate(REGIONS.keys())]
elif Chart == pygal.FrenchMap_Departments:
v = [(i, k) for k, i in enumerate(DEPARTMENTS.keys())]
chart.add('Serie with metadata', [ chart.add('Serie with metadata', [
v[0], v[0],
@ -96,8 +101,10 @@ def test_metadata(Chart):
if Chart == pygal.Pie: if Chart == pygal.Pie:
# Slices with value 0 are not rendered # Slices with value 0 are not rendered
assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value')) assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value'))
elif Chart != pygal.Worldmap and Chart != pygal.SupranationalWorldmap: elif Chart not in (
# Tooltip are not working on worldmap pygal.Worldmap, pygal.SupranationalWorldmap,
pygal.FrenchMap_Regions, pygal.FrenchMap_Departments):
# Tooltip are not working on maps
assert len(v) == len(q('.tooltip-trigger').siblings('.value')) assert len(v) == len(q('.tooltip-trigger').siblings('.value'))
@ -152,7 +159,10 @@ def test_values_by_dict(Chart):
chart1 = Chart(no_prefix=True) chart1 = Chart(no_prefix=True)
chart2 = Chart(no_prefix=True) chart2 = Chart(no_prefix=True)
if not issubclass(Chart, pygal.Worldmap): if not issubclass(Chart, (
pygal.Worldmap,
pygal.FrenchMap_Departments,
pygal.FrenchMap_Regions)):
chart1.add('A', {'red': 10, 'green': 12, 'blue': 14}) chart1.add('A', {'red': 10, 'green': 12, 'blue': 14})
chart1.add('B', {'green': 11, 'red': 7}) chart1.add('B', {'green': 11, 'red': 7})
chart1.add('C', {'blue': 7}) chart1.add('C', {'blue': 7})
@ -335,7 +345,9 @@ def test_labels_with_links(Chart):
q = chart.render_pyquery() q = chart.render_pyquery()
links = q('a') links = q('a')
if issubclass(chart.cls, pygal.graph.worldmap.Worldmap): if issubclass(chart.cls,
(pygal.graph.worldmap.Worldmap,
pygal.graph.frenchmap.FrenchMapDepartments)):
# No country is found in this case so: # No country is found in this case so:
assert len(links) == 4 # 3 links and 1 tooltip assert len(links) == 4 # 3 links and 1 tooltip
else: else:

19
pygal/test/test_line.py

@ -81,3 +81,22 @@ def test_no_dot():
def test_no_dot_at_all(): def test_no_dot_at_all():
q = Line().render_pyquery() q = Line().render_pyquery()
assert q(".text-overlay text").text() == 'No data' assert q(".text-overlay text").text() == 'No data'
def test_not_equal_x_labels():
line = Line()
line.add('test1', range(100))
line.x_labels = map(str, range(11))
q = line.render_pyquery()
assert len(q(".dots")) == 100
assert len(q(".axis.x")) == 1
assert q(".axis.x text").map(texts) == ['0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', '10']
def test_only_major_dots():
line = Line(show_only_major_dots=True, x_labels_major_every=3)
line.add('test', range(12))
line.x_labels = map(str, range(12))
q = line.render_pyquery()
assert len(q(".dots")) == 4

10
pygal/test/test_sparktext.py

@ -60,3 +60,13 @@ def test_negative_and_float_and_no_data_sparktext():
chart3 = Line() chart3 = Line()
assert chart3.render_sparktext() == u('') assert chart3.render_sparktext() == u('')
def test_same_max_and_relative_values_sparktext():
chart = Line()
chart.add('_', [0, 0, 0, 0, 0])
assert chart.render_sparktext() == u('▁▁▁▁▁')
chart2 = Line()
chart2.add('_', [1, 1, 1, 1, 1])
assert chart2.render_sparktext(relative_to=1) == u('▁▁▁▁▁')

27
pygal/util.py

@ -326,9 +326,10 @@ def prepare_values(raw, config, cls):
from pygal.graph.datey import DateY from pygal.graph.datey import DateY
from pygal.graph.histogram import Histogram from pygal.graph.histogram import Histogram
from pygal.graph.worldmap import Worldmap from pygal.graph.worldmap import Worldmap
from pygal.graph.frenchmap import FrenchMapDepartments
if config.x_labels is None and hasattr(cls, 'x_labels'): if config.x_labels is None and hasattr(cls, 'x_labels'):
config.x_labels = cls.x_labels config.x_labels = cls.x_labels
if config.zero == 0 and issubclass(cls, Worldmap): if config.zero == 0 and issubclass(cls, (Worldmap, FrenchMapDepartments)):
config.zero = 1 config.zero = 1
for key in ('x_labels', 'y_labels'): for key in ('x_labels', 'y_labels'):
@ -358,7 +359,7 @@ def prepare_values(raw, config, cls):
metadata = {} metadata = {}
values = [] values = []
if isinstance(raw_values, dict): if isinstance(raw_values, dict):
if issubclass(cls, Worldmap): if issubclass(cls, (Worldmap, FrenchMapDepartments)):
raw_values = list(raw_values.items()) raw_values = list(raw_values.items())
else: else:
value_list = [None] * width value_list = [None] * width
@ -390,7 +391,8 @@ def prepare_values(raw, config, cls):
value = (None, None) value = (None, None)
elif not is_list_like(value): elif not is_list_like(value):
value = (value, config.zero) value = (value, config.zero)
if issubclass(cls, DateY) or issubclass(cls, Worldmap): if issubclass(cls, DateY) or issubclass(
cls, (Worldmap, FrenchMapDepartments)):
value = (adapter(value[0]), value[1]) value = (adapter(value[0]), value[1])
else: else:
value = list(map(adapter, value)) value = list(map(adapter, value))
@ -406,13 +408,14 @@ def split_title(title, width, title_fs):
if not title: if not title:
return titles return titles
size = reverse_text_len(width, title_fs * 1.1) size = reverse_text_len(width, title_fs * 1.1)
title = title.strip() title_lines = title.split("\n")
while len(title) > size: for title_line in title_lines:
title_part = title[:size] while len(title_line) > size:
i = title_part.rfind(' ') title_part = title_line[:size]
if i == -1: i = title_part.rfind(' ')
i = len(title_part) if i == -1:
titles.append(title_part[:i]) i = len(title_part)
title = title[i:].strip() titles.append(title_part[:i])
titles.append(title) title_line = title_line[i:].strip()
titles.append(title_line)
return titles return titles

6
pygal_gen.py

@ -22,8 +22,7 @@ import pygal
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Generate pygal chart in command line', description='Generate pygal chart in command line',
prog='pygal_gen', prog='pygal_gen')
version=pygal.__version__)
parser.add_argument('-t', '--type', dest='type', default='Line', parser.add_argument('-t', '--type', dest='type', default='Line',
choices=map(lambda x: x.__name__, pygal.CHARTS), choices=map(lambda x: x.__name__, pygal.CHARTS),
@ -35,6 +34,9 @@ parser.add_argument('-o', '--output', dest='filename', default='pygal_out.svg',
parser.add_argument('-s', '--serie', dest='series', nargs='+', action='append', parser.add_argument('-s', '--serie', dest='series', nargs='+', action='append',
help='Add a serie in the form (title val1 val2...)') help='Add a serie in the form (title val1 val2...)')
parser.add_argument('--version', action='version',
version='pygal %s' % pygal.__version__)
for key in pygal.config.CONFIG_ITEMS: for key in pygal.config.CONFIG_ITEMS:
opt_name = key.name opt_name = key.name
val = key.value val = key.value

2
setup.py

@ -64,7 +64,7 @@ setup(
"svg", "chart", "graph", "diagram", "plot", "histogram", "kiviat"], "svg", "chart", "graph", "diagram", "plot", "histogram", "kiviat"],
tests_require=["pytest", "pyquery", "flask", "cairosvg"], tests_require=["pytest", "pyquery", "flask", "cairosvg"],
cmdclass={'test': PyTest}, cmdclass={'test': PyTest},
package_data={'pygal': ['css/*', 'graph/worldmap.svg']}, package_data={'pygal': ['css/*', 'graph/*.svg']},
install_requires=['lxml'], install_requires=['lxml'],
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",

Loading…
Cancel
Save