From 4fc5621e0d362f431ac13a1913818c3d4a00acfa Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 21 Feb 2014 13:26:30 +0100 Subject: [PATCH] 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",