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. 33
      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. 19
      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 !
Hopefully fix weird major scale algorithm
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
Visited links bug fixed
Add french maps by department and region (This will be externalized in an extension later)
V 1.3.x
Whisker Box Plot

101
demo/moulinrouge/tests.py

@ -2,8 +2,11 @@
# This file is part of pygal
from pygal import (
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.graph.frenchmap import DEPARTMENTS, REGIONS
from random import randint, choice
def get_test_routes(app):
@ -89,6 +92,15 @@ def get_test_routes(app):
'1 12 123 1234 12345 123456 1234567 12345678 123456789 1234567890')
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')
def test_long_labels():
bar = Bar()
@ -253,6 +265,16 @@ def get_test_routes(app):
hist.add('2', [(2, 2, 8)])
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>')
def test_secondary_for(chart):
chart = CHARTS_BY_NAME[chart](fill=True)
@ -329,14 +351,13 @@ def get_test_routes(app):
@app.route('/test/worldmap')
def test_worldmap():
import random
map = Worldmap(style=random.choice(styles.values()))
map.add('1st', [('fr', 100), ('us', 10)])
map.add('2nd', [('jp', 1), ('ru', 7), ('uk', 0)])
map.add('3rd', ['ch', 'cz', 'ca', 'cn'])
map.add('4th', {'br': 12, 'bo': 1, 'bu': 23, 'fr': 34})
map.add('5th', [{
wmap = Worldmap(style=choice(list(styles.values())))
wmap.add('1st', [('fr', 100), ('us', 10)])
wmap.add('2nd', [('jp', 1), ('ru', 7), ('uk', 0)])
wmap.add('3rd', ['ch', 'cz', 'ca', 'cn'])
wmap.add('4th', {'br': 12, 'bo': 1, 'bu': 23, 'fr': 34})
wmap.add('5th', [{
'value': ('tw', 10),
'label': 'First label',
'xlink': 'http://google.com?q=tw'
@ -348,8 +369,64 @@ def get_test_routes(app):
'value': ('mw', 40),
'label': 'Last'
}])
map.add('6th', [3, 5, 34, 12])
map.title = 'World Map !!'
return map.render_response()
wmap.add('6th', [3, 5, 34, 12])
wmap.title = 'World Map !!'
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())

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
from pygal.config import Config
from pygal.ghost import Ghost
from pygal.graph import CHARTS_NAMES
from pygal.ghost import Ghost, REAL_CHARTS
CHARTS = []
CHARTS_BY_NAME = {}
for NAME in CHARTS_NAMES:
for NAME in REAL_CHARTS.keys():
_CHART = type(NAME, (Ghost,), {})
CHARTS.append(_CHART)
CHARTS_BY_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)
class Config(object):
class Config(MetaConfig('ConfigBase', (object,), {})):
"""Class holding config values"""
__metaclass__ = MetaConfig
style = Key(
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_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")
stroke = Key(
@ -230,7 +232,8 @@ class Config(object):
0, int, "Label", "Specify y labels rotation angles", "in degrees")
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 ############
human_readable = Key(

6
pygal/css/style.css

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

18
pygal/ghost.py

@ -33,12 +33,23 @@ from pygal.util import prepare_values
from uuid import uuid4
class ChartCollection(object):
pass
REAL_CHARTS = {}
for NAME in CHARTS_NAMES:
mod_name = 'pygal.graph.%s' % NAME.lower()
__import__(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):
@ -135,6 +146,11 @@ class Ghost(object):
vmax = max(values)
if relative_to is None:
relative_to = min(values)
if (vmax - relative_to) == 0:
chart = bars[0] * len(values)
return chart
divisions = len(bars) - 1
for value in values:
chart += bars[int(divisions *

3
pygal/graph/__init__.py

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

2
pygal/graph/box.py

@ -74,7 +74,7 @@ class Box(Graph):
y_pos = compute_scale(
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, [
(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(
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(
zip(cut(self.series, 'title'),

2
pygal/graph/graph.py

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

33
pygal/graph/line.py

@ -67,10 +67,30 @@ class Line(Graph):
points = serie.points
view_values = list(map(self.view, points))
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):
if None in (x, y):
if None in (x, y) or (self.show_only_major_dots
and i not in major_dots_index):
continue
metadata = serie.metadata.get(i)
classes = []
if x > self.view.width / 2:
@ -110,6 +130,13 @@ class Line(Graph):
self._points(x_pos)
if self.x_labels:
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:
self._x_labels = None
@ -124,7 +151,7 @@ class Line(Graph):
y_pos = compute_scale(
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))

2
pygal/graph/radar.py

@ -182,7 +182,7 @@ class Radar(Line):
y_pos = compute_scale(
self._rmin, self._rmax, self.logarithmic, self.order_min,
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._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)
y_pos = compute_scale(
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_labels = self.x_labels and list(zip(self.x_labels, [

4
pygal/graph/worldmap.py

@ -102,8 +102,8 @@ class Worldmap(Graph):
else:
title_node = self.svg.node(country, 'title')
text = ''
title_node.text = text + '[%s] %s: %d' % (
title_node.text = text + '[%s] %s: %s' % (
serie.title,
self.country_names[country_code], value)
self.country_names[country_code], self._format(value))
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}
def set_countries(countries):
global COUNTRIES
COUNTRIES = countries
def set_countries(countries, clear=False):
if clear:
COUNTRIES.clear()
COUNTRIES.update(countries)

10
pygal/test/__init__.py

@ -21,7 +21,7 @@ import pygal
from pygal.util import cut
from datetime import datetime
from pygal.i18n import COUNTRIES
COUNTRY_KEYS = list(COUNTRIES.keys())
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
def get_data(i):
@ -58,7 +58,13 @@ def adapt(chart, data):
data = cut(data)
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))
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

14
pygal/test/test_config.py

@ -18,10 +18,12 @@
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import (
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.test.utils import texts
from pygal.test import pytest_generate_tests, make_data
from uuid import uuid4
def test_config_behaviours():
@ -270,7 +272,8 @@ def test_no_data():
def test_include_x_axis(Chart):
chart = Chart()
if Chart in (Pie, Radar, Funnel, Dot, Gauge, Worldmap,
SupranationalWorldmap, Histogram, Box):
SupranationalWorldmap, Histogram, Box,
FrenchMap_Regions, FrenchMap_Departments):
return
if not chart.cls._dual:
data = 100, 200, 150
@ -292,7 +295,7 @@ def test_include_x_axis(Chart):
def test_css(Chart):
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:
f.write(css)
@ -314,3 +317,8 @@ def test_inline_css(Chart):
chart.add('/', [10, 1, 5])
svg = chart.render().decode('utf-8')
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 sys
from pygal import i18n
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from pygal.util import cut
from pygal._compat import u
from pygal.test import pytest_generate_tests, make_data
@ -73,7 +74,11 @@ def test_metadata(Chart):
elif Chart == pygal.XY:
v = list(map(lambda x: (x, x + 1), v))
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', [
v[0],
@ -96,8 +101,10 @@ def test_metadata(Chart):
if Chart == pygal.Pie:
# Slices with value 0 are not rendered
assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value'))
elif Chart != pygal.Worldmap and Chart != pygal.SupranationalWorldmap:
# Tooltip are not working on worldmap
elif Chart not in (
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'))
@ -152,7 +159,10 @@ def test_values_by_dict(Chart):
chart1 = 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('B', {'green': 11, 'red': 7})
chart1.add('C', {'blue': 7})
@ -335,7 +345,9 @@ def test_labels_with_links(Chart):
q = chart.render_pyquery()
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:
assert len(links) == 4 # 3 links and 1 tooltip
else:

19
pygal/test/test_line.py

@ -81,3 +81,22 @@ def test_no_dot():
def test_no_dot_at_all():
q = Line().render_pyquery()
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()
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('▁▁▁▁▁')

19
pygal/util.py

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

6
pygal_gen.py

@ -22,8 +22,7 @@ import pygal
parser = argparse.ArgumentParser(
description='Generate pygal chart in command line',
prog='pygal_gen',
version=pygal.__version__)
prog='pygal_gen')
parser.add_argument('-t', '--type', dest='type', default='Line',
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',
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:
opt_name = key.name
val = key.value

2
setup.py

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

Loading…
Cancel
Save