From 4fc5621e0d362f431ac13a1913818c3d4a00acfa Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 21 Feb 2014 13:26:30 +0100 Subject: [PATCH 01/22] Add french maps --- demo/moulinrouge/tests.py | 65 ++++-- pygal/__init__.py | 8 +- pygal/css/style.css | 4 +- pygal/ghost.py | 13 +- pygal/graph/__init__.py | 3 +- pygal/graph/fr.departments.svg | 328 +++++++++++++++++++++++++++++ pygal/graph/fr.regions.svg | 91 ++++++++ pygal/graph/frenchmap.py | 270 ++++++++++++++++++++++++ pygal/graph/worldmap.svg | 368 ++++++++++++++++----------------- setup.py | 2 +- 10 files changed, 947 insertions(+), 205 deletions(-) create mode 100644 pygal/graph/fr.departments.svg create mode 100644 pygal/graph/fr.regions.svg create mode 100644 pygal/graph/frenchmap.py diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index fa5bbab..63f1ed5 100644 --- a/demo/moulinrouge/tests.py +++ b/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): @@ -329,14 +332,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 +350,47 @@ 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() return filter(lambda x: x.startswith('test'), locals()) diff --git a/pygal/__init__.py b/pygal/__init__.py index 45803ec..8184773 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -24,17 +24,17 @@ Pygal - A python svg graph plotting library __version__ = '1.3.1' 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'] diff --git a/pygal/css/style.css b/pygal/css/style.css index 35fa8e9..e5740ed 100644 --- a/pygal/css/style.css +++ b/pygal/css/style.css @@ -113,7 +113,7 @@ fill: {{ style.foreground_light }}; } -{{ id }}.country { +{{ id }}.map-element { fill: {{ style.foreground }}; stroke: {{ style.plot_background }} !important; opacity: .9; @@ -124,7 +124,7 @@ transition: 250ms; } -{{ id }}.country:hover { +{{ id }}.map-element:hover { opacity: 1; stroke-width: 10; } diff --git a/pygal/ghost.py b/pygal/ghost.py index 635a19e..4607c0c 100644 --- a/pygal/ghost.py +++ b/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): diff --git a/pygal/graph/__init__.py b/pygal/graph/__init__.py index 500e698..77ae278 100644 --- a/pygal/graph/__init__.py +++ b/pygal/graph/__init__.py @@ -40,5 +40,6 @@ CHARTS_NAMES = [ 'Worldmap', 'SupranationalWorldmap', 'Histogram', - 'Box' + 'Box', + 'FrenchMap' ] diff --git a/pygal/graph/fr.departments.svg b/pygal/graph/fr.departments.svg new file mode 100644 index 0000000..8b386f4 --- /dev/null +++ b/pygal/graph/fr.departments.svg @@ -0,0 +1,328 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pygal/graph/fr.regions.svg b/pygal/graph/fr.regions.svg new file mode 100644 index 0000000..1465de0 --- /dev/null +++ b/pygal/graph/fr.regions.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pygal/graph/frenchmap.py b/pygal/graph/frenchmap.py new file mode 100644 index 0000000..dc610af --- /dev/null +++ b/pygal/graph/frenchmap.py @@ -0,0 +1,270 @@ +# -*- 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 . +""" +Worldmap chart + +""" + +from __future__ import division +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: %d' % ( + serie.title, + self.area_names[area_code], 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 diff --git a/pygal/graph/worldmap.svg b/pygal/graph/worldmap.svg index 1b1f73f..0adfc9b 100644 --- a/pygal/graph/worldmap.svg +++ b/pygal/graph/worldmap.svg @@ -1,24 +1,24 @@ - diff --git a/setup.py b/setup.py index 1bd88ef..83ffa2d 100644 --- a/setup.py +++ b/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", From 4f22e2b89607422306cd1a4377f8e761674750fb Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 21 Feb 2014 13:49:48 +0100 Subject: [PATCH 02/22] Fix combined tests --- pygal/test/__init__.py | 10 ++++++++-- pygal/test/test_config.py | 6 ++++-- pygal/test/test_graph.py | 17 +++++++++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/pygal/test/__init__.py b/pygal/test/__init__.py index e0d7c6e..da94e4a 100644 --- a/pygal/test/__init__.py +++ b/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,8 +58,14 @@ 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 diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index bf0f29c..deb0e22 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -18,7 +18,8 @@ # along with pygal. If not, see . 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 @@ -270,7 +271,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 diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py index 9ba8da0..5b87285 100644 --- a/pygal/test/test_graph.py +++ b/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')) @@ -335,7 +342,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: From c56af71127fdd8441a1b94f681d1c0ceeedef01a Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 21 Feb 2014 14:43:43 +0100 Subject: [PATCH 03/22] Prevent concurrent test crash --- pygal/test/test_config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index deb0e22..895bd6b 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -23,6 +23,7 @@ from pygal import ( 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(): @@ -272,7 +273,7 @@ def test_include_x_axis(Chart): chart = Chart() if Chart in (Pie, Radar, Funnel, Dot, Gauge, Worldmap, SupranationalWorldmap, Histogram, Box, - FrenchMap_Regions, FrenchMap_Departments): + FrenchMap_Regions, FrenchMap_Departments): return if not chart.cls._dual: data = 100, 200, 150 @@ -294,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) From ccff1999e7059658c213c5620ab2d7db15729b76 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 21 Feb 2014 16:35:41 +0100 Subject: [PATCH 04/22] Add region aggregate + fix css --- pygal/css/style.css | 2 +- pygal/graph/frenchmap.py | 114 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/pygal/css/style.css b/pygal/css/style.css index e5740ed..bc8f31e 100644 --- a/pygal/css/style.css +++ b/pygal/css/style.css @@ -115,7 +115,7 @@ {{ id }}.map-element { fill: {{ style.foreground }}; - stroke: {{ style.plot_background }} !important; + stroke: {{ style.foreground_dark }} !important; opacity: .9; stroke-width: 3; -webkit-transition: 250ms; diff --git a/pygal/graph/frenchmap.py b/pygal/graph/frenchmap.py index dc610af..cfa3be5 100644 --- a/pygal/graph/frenchmap.py +++ b/pygal/graph/frenchmap.py @@ -22,6 +22,7 @@ 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 @@ -268,3 +269,116 @@ class FrenchMapRegions(FrenchMapDepartments): 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): + regions = defaultdict(int) + for department, value in values: + regions[DEPARTMENTS_REGIONS[department]] += value + return list(regions.items()) From c0562ffb1ca54a27640ec53b2aa56613a6f2ab49 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 21 Feb 2014 17:22:44 +0100 Subject: [PATCH 05/22] Bump --- CHANGELOG | 3 ++- pygal/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6df8408..41fa2c8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,10 @@ -TO BE RELEASED: V 1.4.0 +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 diff --git a/pygal/__init__.py b/pygal/__init__.py index 8184773..4ecc896 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -21,7 +21,7 @@ Pygal - A python svg graph plotting library """ -__version__ = '1.3.1' +__version__ = '1.4.0' import sys from pygal.config import Config from pygal.ghost import Ghost, REAL_CHARTS From 83729669e80c26c51f34f337bdf1db7f90d43ec3 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 21 Feb 2014 18:32:16 +0100 Subject: [PATCH 06/22] Minor fixes in maps --- pygal/__init__.py | 2 +- pygal/graph/frenchmap.py | 7 +++++-- pygal/graph/worldmap.py | 4 ++-- pygal/util.py | 8 +++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pygal/__init__.py b/pygal/__init__.py index 4ecc896..8dcc799 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -21,7 +21,7 @@ Pygal - A python svg graph plotting library """ -__version__ = '1.4.0' +__version__ = '1.4.1' import sys from pygal.config import Config from pygal.ghost import Ghost, REAL_CHARTS diff --git a/pygal/graph/frenchmap.py b/pygal/graph/frenchmap.py index cfa3be5..951c86d 100644 --- a/pygal/graph/frenchmap.py +++ b/pygal/graph/frenchmap.py @@ -225,6 +225,7 @@ class FrenchMapDepartments(Graph): areae = map.xpath( "//*[contains(concat(' ', normalize-space(@class), ' ')," " ' %s%s ')]" % (self.area_prefix, area_code)) + if not areae: continue for area in areae: @@ -251,9 +252,9 @@ class FrenchMapDepartments(Graph): else: title_node = self.svg.node(area, 'title') text = '' - title_node.text = text + '[%s] %s: %d' % ( + title_node.text = text + '[%s] %s: %s' % ( serie.title, - self.area_names[area_code], value) + self.area_names[area_code], self._format(value)) self.nodes['plot'].append(map) @@ -378,6 +379,8 @@ DEPARTMENTS_REGIONS = { def aggregate_regions(values): + if isinstance(values, dict): + values = values.items() regions = defaultdict(int) for department, value in values: regions[DEPARTMENTS_REGIONS[department]] += value diff --git a/pygal/graph/worldmap.py b/pygal/graph/worldmap.py index 1ba17a4..8e144d6 100644 --- a/pygal/graph/worldmap.py +++ b/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) diff --git a/pygal/util.py b/pygal/util.py index 0b4f48d..ca72e29 100644 --- a/pygal/util.py +++ b/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)) From ad62320f410d6cba2187b8d2b37e92a717e759eb Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 24 Feb 2014 13:43:07 +0100 Subject: [PATCH 07/22] 1.4.1 changelog --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 41fa2c8..2c2b4f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +V 1.4.1 + Fix value formatting in maps + V 1.4.0 Finally a changelog ! Hopefully fix weird major scale algorithm From 3914900dc35d0103311a4ad294405f84203fb90d Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 24 Feb 2014 14:11:18 +0100 Subject: [PATCH 08/22] Fix map test --- pygal/test/test_graph.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py index 5b87285..dbefd1f 100644 --- a/pygal/test/test_graph.py +++ b/pygal/test/test_graph.py @@ -159,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}) From 5dc83dafc32143e4b4fa2ab7239f316377527d0f Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 27 Feb 2014 13:19:16 +0100 Subject: [PATCH 09/22] Bump 1.4.2 --- CHANGELOG | 3 +++ pygal/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2c2b4f2..714de2d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +V 1.4.2 + Fix broken tests + V 1.4.1 Fix value formatting in maps diff --git a/pygal/__init__.py b/pygal/__init__.py index 8dcc799..14127e8 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -21,7 +21,7 @@ Pygal - A python svg graph plotting library """ -__version__ = '1.4.1' +__version__ = '1.4.2' import sys from pygal.config import Config from pygal.ghost import Ghost, REAL_CHARTS From 0ce426cfd073ffbde7e3e1e7ef8a5448f2376669 Mon Sep 17 00:00:00 2001 From: M Nasimul Haque Date: Thu, 27 Feb 2014 21:53:43 +0000 Subject: [PATCH 10/22] Allow arbitrary number of x-labels on line plot When there are thousands of points in a line plot. It is sensible to show only some of the ticks for x labels. --- pygal/graph/line.py | 9 ++++++++- pygal/test/test_line.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pygal/graph/line.py b/pygal/graph/line.py index ccf6dcc..d4cfb95 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -110,7 +110,14 @@ class Line(Graph): self._points(x_pos) 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: self._x_labels = None diff --git a/pygal/test/test_line.py b/pygal/test/test_line.py index a82e023..55cad41 100644 --- a/pygal/test/test_line.py +++ b/pygal/test/test_line.py @@ -81,3 +81,14 @@ 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'] From 4b45750b7a741c306ef280362ab1c39876d3e16b Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 28 Feb 2014 15:43:22 +0100 Subject: [PATCH 11/22] Bump 1.4.3 --- CHANGELOG | 3 +++ demo/moulinrouge/tests.py | 7 +++++++ pygal/__init__.py | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 714de2d..babfb48 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +V 1.4.3 + Allow arbitrary number of x-labels on line plot (thanks nsmgr8) + V 1.4.2 Fix broken tests diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 63f1ed5..7bf6b2b 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -393,4 +393,11 @@ def get_test_routes(app): 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() + return filter(lambda x: x.startswith('test'), locals()) diff --git a/pygal/__init__.py b/pygal/__init__.py index 14127e8..3bcc6f1 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -21,7 +21,7 @@ Pygal - A python svg graph plotting library """ -__version__ = '1.4.2' +__version__ = '1.4.3' import sys from pygal.config import Config from pygal.ghost import Ghost, REAL_CHARTS From 9cf62f0b118773796d9c60e24395ff3ed4a8b510 Mon Sep 17 00:00:00 2001 From: Josh Gibbs Date: Mon, 3 Mar 2014 07:58:42 -0800 Subject: [PATCH 12/22] Fix ZeroDivisionError in sparktext rendering --- pygal/ghost.py | 5 +++++ pygal/test/test_sparktext.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/pygal/ghost.py b/pygal/ghost.py index 4607c0c..abd9dee 100644 --- a/pygal/ghost.py +++ b/pygal/ghost.py @@ -146,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 * diff --git a/pygal/test/test_sparktext.py b/pygal/test/test_sparktext.py index 5240da7..fd18e00 100644 --- a/pygal/test/test_sparktext.py +++ b/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('▁▁▁▁▁') From 8ba23bb2149d0214197e9bfe2759aeaa180e5a47 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 4 Mar 2014 12:25:53 +0100 Subject: [PATCH 13/22] Fix #100 --- pygal/i18n.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygal/i18n.py b/pygal/i18n.py index 3b0b714..a5ae9a2 100644 --- a/pygal/i18n.py +++ b/pygal/i18n.py @@ -203,5 +203,5 @@ SUPRANATIONAL = {'europe': EUROPE, 'oecd': OECD, 'nafta': NAFTA, 'eur': EUR} def set_countries(countries): - global COUNTRIES - COUNTRIES = countries + COUNTRIES.clear() + COUNTRIES.update(countries) From ed3148b80a8e27c4dcc8cc984be5fb4f411c30c1 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 4 Mar 2014 12:27:18 +0100 Subject: [PATCH 14/22] Fix pygal_gen. Fix #99 --- CHANGELOG | 5 +++++ pygal/__init__.py | 2 +- pygal/config.py | 7 +++---- pygal/i18n.py | 5 +++-- pygal/test/test_config.py | 5 +++++ pygal_gen.py | 6 ++++-- 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index babfb48..61bcc3c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +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) diff --git a/pygal/__init__.py b/pygal/__init__.py index 3bcc6f1..4d33f78 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -21,7 +21,7 @@ Pygal - A python svg graph plotting library """ -__version__ = '1.4.3' +__version__ = '1.4.4' import sys from pygal.config import Config from pygal.ghost import Ghost, REAL_CHARTS diff --git a/pygal/config.py b/pygal/config.py index d17f337..b19472e 100644 --- a/pygal/config.py +++ b/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") @@ -230,7 +228,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( diff --git a/pygal/i18n.py b/pygal/i18n.py index a5ae9a2..181ab48 100644 --- a/pygal/i18n.py +++ b/pygal/i18n.py @@ -202,6 +202,7 @@ NAFTA = ['ca', 'mx', 'us'] SUPRANATIONAL = {'europe': EUROPE, 'oecd': OECD, 'nafta': NAFTA, 'eur': EUR} -def set_countries(countries): - COUNTRIES.clear() +def set_countries(countries, clear=False): + if clear: + COUNTRIES.clear() COUNTRIES.update(countries) diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index 895bd6b..6510cae 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -317,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) diff --git a/pygal_gen.py b/pygal_gen.py index 981cd87..9374b6f 100755 --- a/pygal_gen.py +++ b/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 From 7627fb40a24f1f76186adee5093f928a8cecd9f6 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 5 Mar 2014 16:14:47 +0100 Subject: [PATCH 15/22] Fix y_labels map iterator exhaustion in python 3 --- CHANGELOG | 3 +++ demo/moulinrouge/tests.py | 10 ++++++++++ pygal/__init__.py | 2 +- pygal/graph/box.py | 2 +- pygal/graph/funnel.py | 2 +- pygal/graph/line.py | 2 +- pygal/graph/radar.py | 2 +- pygal/graph/stackedbar.py | 2 +- 8 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 61bcc3c..6da7617 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +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 diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 7bf6b2b..69db907 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -256,6 +256,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/') def test_secondary_for(chart): chart = CHARTS_BY_NAME[chart](fill=True) diff --git a/pygal/__init__.py b/pygal/__init__.py index 4d33f78..191f5cd 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -21,7 +21,7 @@ Pygal - A python svg graph plotting library """ -__version__ = '1.4.4' +__version__ = '1.4.5' import sys from pygal.config import Config from pygal.ghost import Ghost, REAL_CHARTS diff --git a/pygal/graph/box.py b/pygal/graph/box.py index dae5152..de928e0 100644 --- a/pygal/graph/box.py +++ b/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)])) diff --git a/pygal/graph/funnel.py b/pygal/graph/funnel.py index 3f3c6ce..0fa6aba 100644 --- a/pygal/graph/funnel.py +++ b/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'), diff --git a/pygal/graph/line.py b/pygal/graph/line.py index d4cfb95..b24c746 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -131,7 +131,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)) diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index 2c7a650..a0a7e42 100644 --- a/pygal/graph/radar.py +++ b/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)) diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py index 14d20d3..8bd129c 100644 --- a/pygal/graph/stackedbar.py +++ b/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, [ From e62d6d23d27d0e1991529bf90a7e293b4270578c Mon Sep 17 00:00:00 2001 From: James Dominy Date: Thu, 6 Mar 2014 13:04:28 +0200 Subject: [PATCH 16/22] Allow user specified line splitting in titles Titles can be split at specified points with '\n' within the title string. Titles segments which are still too long will still be split over multiple lines in the normal way. --- pygal/util.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pygal/util.py b/pygal/util.py index ca72e29..a0608a9 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -408,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] - i = title_part.rfind(' ') - if i == -1: - i = len(title_part) - titles.append(title_part[:i]) - title = title[i:].strip() - titles.append(title) + 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_line = title_line[i:].strip() + titles.append(title_line) return titles From 2b11e16466efe85866206ec3b99cdad5f0706a8f Mon Sep 17 00:00:00 2001 From: James Dominy Date: Thu, 6 Mar 2014 13:12:14 +0200 Subject: [PATCH 17/22] Differentiates title style classses The main plot title is given an additional class (plot_title) to differentiate it from from the axis titles, so the it could for example be styled in boldface using CSS. --- pygal/graph/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index c3e1bb5..3bcdc0f 100644 --- a/pygal/graph/graph.py +++ b/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 From 90a38440ca0b2eab2e1d45a9984fa158204e9240 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 11 Mar 2014 10:05:41 +0100 Subject: [PATCH 18/22] Update changelog --- CHANGELOG | 3 +++ demo/moulinrouge/tests.py | 9 +++++++++ pygal/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6da7617..ebb6711 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +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 diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 69db907..7bd6084 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -92,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() diff --git a/pygal/__init__.py b/pygal/__init__.py index 191f5cd..c37b512 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -21,7 +21,7 @@ Pygal - A python svg graph plotting library """ -__version__ = '1.4.5' +__version__ = '1.4.6' import sys from pygal.config import Config from pygal.ghost import Ghost, REAL_CHARTS From b9e6efc79bccc7c89b63f1fe6867a54cfee26440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Plasse?= Date: Wed, 2 Apr 2014 15:45:26 +0200 Subject: [PATCH 19/22] Add python 3.4 for tox --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index aa1206e..20cb90c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py32,py33 +envlist = py26,py27,py32,py33,py34 [testenv] sitepackages=True From 986860cab66f577e2a4246e9aabb3137df5a8e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Plasse?= Date: Wed, 2 Apr 2014 15:48:11 +0200 Subject: [PATCH 20/22] Add new config option for hiding all dots except major labeled ones --- pygal/config.py | 4 ++++ pygal/graph/line.py | 24 ++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/pygal/config.py b/pygal/config.py index b19472e..08f23c9 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -140,6 +140,10 @@ class Config(MetaConfig('ConfigBase', (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( diff --git a/pygal/graph/line.py b/pygal/graph/line.py index b24c746..bf34ef6 100644 --- a/pygal/graph/line.py +++ b/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: From aa18bc813362ee4db7e264550e34535533e91e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Plasse?= Date: Wed, 2 Apr 2014 15:48:50 +0200 Subject: [PATCH 21/22] Test new 'show_only_major_dots' option --- pygal/test/test_line.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pygal/test/test_line.py b/pygal/test/test_line.py index 55cad41..b673f14 100644 --- a/pygal/test/test_line.py +++ b/pygal/test/test_line.py @@ -92,3 +92,11 @@ def test_not_equal_x_labels(): 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 From aa344741e3cac987b1be5501dc08df07dcdc419b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Plasse?= Date: Wed, 2 Apr 2014 15:49:20 +0200 Subject: [PATCH 22/22] simple graph with 'show_only_major_dots' option --- demo/moulinrouge/tests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 7bd6084..813a4f6 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -419,4 +419,14 @@ def get_test_routes(app): 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())