From 036e3b8b62e7ab9614faca1c1eb433caf3ea8451 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 24 Jun 2015 15:08:51 +0200 Subject: [PATCH] Extract pygal french maps into a pygal_maps_fr plugin --- demo/moulinrouge/tests.py | 20 +- pygal/__init__.py | 38 +++- pygal/config.py | 6 +- pygal/graph/box.py | 9 +- pygal/graph/frenchmap.py | 325 --------------------------- pygal/graph/maps/fr.departments.svg | 328 ---------------------------- pygal/graph/maps/fr.regions.svg | 91 -------- pygal/maps/__init__.py | 0 pygal/test/test_box.py | 12 +- pygal/test/test_config.py | 24 +- pygal/test/test_frenchmap.py | 42 ---- 11 files changed, 77 insertions(+), 818 deletions(-) delete mode 100644 pygal/graph/frenchmap.py delete mode 100644 pygal/graph/maps/fr.departments.svg delete mode 100644 pygal/graph/maps/fr.regions.svg create mode 100644 pygal/maps/__init__.py delete mode 100644 pygal/test/test_frenchmap.py diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index b62c6ff..7796ec9 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -3,14 +3,18 @@ from pygal import ( Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY, CHARTS_BY_NAME, Config, Line, Worldmap, Histogram, Box, SwissMapCantons, - FrenchMapDepartments, FrenchMapRegions, Pie, Treemap, TimeLine, DateLine, + Pie, Treemap, TimeLine, DateLine, DateTimeLine, SupranationalWorldmap) +try: + from pygal.maps import fr +except ImportError: + fr = None +from flask import abort from pygal.style import styles, Style, RotateStyle from pygal.colors import rotate from pygal.graph.horizontal import HorizontalGraph -from pygal.graph.frenchmap import DEPARTMENTS, REGIONS from pygal.graph.swissmap import CANTONS from random import randint, choice from datetime import datetime @@ -444,10 +448,12 @@ def get_test_routes(app): @app.route('/test/frenchmapdepartments') def test_frenchmapdepartments(): - fmap = FrenchMapDepartments(style=choice(list(styles.values()))) + if fr is None: + abort(404) + fmap = fr.Departments(style=choice(list(styles.values()))) for i in range(10): fmap.add('s%d' % i, [ - (choice(list(DEPARTMENTS.keys())), randint(0, 100)) + (choice(list(fr.DEPARTMENTS.keys())), randint(0, 100)) for _ in range(randint(1, 5))]) fmap.add('links', [{ @@ -484,10 +490,12 @@ def get_test_routes(app): @app.route('/test/frenchmapregions') def test_frenchmapregions(): - fmap = FrenchMapRegions(style=choice(list(styles.values()))) + if fr is None: + abort(404) + fmap = fr.Regions(style=choice(list(styles.values()))) for i in range(10): fmap.add('s%d' % i, [ - (choice(list(REGIONS.keys())), randint(0, 100)) + (choice(list(fr.REGIONS.keys())), randint(0, 100)) for _ in range(randint(1, 5))]) fmap.add('links', [{ diff --git a/pygal/__init__.py b/pygal/__init__.py index b23431e..6e3188c 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -26,7 +26,6 @@ __version__ = '1.9.9' from pygal.graph.bar import Bar from pygal.graph.box import Box from pygal.graph.dot import Dot -from pygal.graph.frenchmap import FrenchMapDepartments, FrenchMapRegions from pygal.graph.funnel import Funnel from pygal.graph.gauge import Gauge from pygal.graph.histogram import Histogram @@ -46,11 +45,48 @@ from pygal.graph.worldmap import Worldmap, SupranationalWorldmap from pygal.graph.xy import XY from pygal.graph.graph import Graph from pygal.config import Config +from pygal import maps +import pkg_resources +import sys +import traceback +import warnings CHARTS_BY_NAME = dict( [(k, v) for k, v in locals().items() if isinstance(v, type) and issubclass(v, Graph) and v != Graph]) + +from pygal.graph.map import BaseMap +for entry in pkg_resources.iter_entry_points('pygal.maps'): + try: + cls = entry.load() + except Exception: + warnings.warn('Unable to load %s pygal plugin \n\n%s' % ( + entry, traceback.format_exc()), Warning) + continue + setattr(maps, entry.name, cls) + for k, v in cls.__dict__.items(): + if isinstance(v, type) and issubclass(v, BaseMap) and v != BaseMap: + CHARTS_BY_NAME[entry.name.capitalize() + k + 'Map'] = v + CHARTS_NAMES = list(CHARTS_BY_NAME.keys()) CHARTS = list(CHARTS_BY_NAME.values()) + + +class PluginImportFixer(object): + def __init__(self): + pass + + def find_module(self, fullname, path=None): + if fullname.startswith('pygal.maps.') and hasattr( + maps, fullname.split('.')[2]): + return self + return None + + def load_module(self, name): + if name not in sys.modules: + sys.modules[name] = getattr(maps, name.split('.')[2]) + return sys.modules[name] + +sys.meta_path += [PluginImportFixer()] diff --git a/pygal/config.py b/pygal/config.py index 1405d9e..4613869 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -342,10 +342,10 @@ class Config(CommonConfig): "{'type': 'cardinal', 'c': .5}", int) mode = Key( - None, str, "Value", "Sets the mode to be used. " + 'extremes', str, "Value", "Sets the mode to be used. " "(Currently only supported on box plot)", - "May be %s" % ' or '.join(["1.5IQR", "extremes", "tukey", "stdev",\ - "pstdev"])) + "May be %s" % ' or '.join([ + "1.5IQR", "extremes", "tukey", "stdev", "pstdev"])) order_min = Key( None, int, "Value", "Minimum order of scale, defaults to None") diff --git a/pygal/graph/box.py b/pygal/graph/box.py index 5cefd26..fc99434 100644 --- a/pygal/graph/box.py +++ b/pygal/graph/box.py @@ -54,7 +54,7 @@ class Box(Graph): elif self.mode in ["tukey", "stdev", "pstdev"]: return 'Min: %s Lower Whisker: %s Q1: %s Q2: %s Q3: %s '\ 'Upper Whisker: %s Max: %s' % tuple(map(sup, x)) - else: + elif self.mode == '1.5IQR': # 1.5IQR mode return 'Q1: %s Q2: %s Q3: %s' % tuple(map(sup, x[2:5])) else: @@ -70,7 +70,6 @@ class Box(Graph): serie.values, serie.outliers = \ self._box_points(serie.values, self.mode) - if self._min: self._box.ymin = min(self._min, self.zero) if self._max: @@ -227,13 +226,13 @@ class Box(Graph): m = mean(seq) l = len(seq) v = sum((n - m)**2 for n in seq) / (l - 1) # variance - return v**0.5 # sqrt + return v**0.5 # sqrt def pstdev(seq): m = mean(seq) l = len(seq) v = sum((n - m)**2 for n in seq) / l # variance - return v**0.5 # sqrt + return v**0.5 # sqrt outliers = [] # sort the copy in case the originals must stay in original order @@ -294,7 +293,7 @@ class Box(Graph): q0 = s[b0] q4 = s[b4-1] outliers = s[:b0] + s[b4:] - else: + elif mode == '1.5IQR': # 1.5IQR mode q0 = q1 - 1.5 * iqr q4 = q3 + 1.5 * iqr diff --git a/pygal/graph/frenchmap.py b/pygal/graph/frenchmap.py deleted file mode 100644 index 78125bb..0000000 --- a/pygal/graph/frenchmap.py +++ /dev/null @@ -1,325 +0,0 @@ -# -*- 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 collections import defaultdict -from pygal.graph.map import BaseMap -from pygal._compat import u -from numbers import Number - -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__), 'maps', - 'fr.departments.svg')) as file: - DPT_MAP = file.read() - - -class IntCodeMixin(object): - def adapt_code(self, area_code): - if isinstance(area_code, Number): - if area_code > 100: - return '%3d' % area_code - - return '%2d' % area_code - return super(IntCodeMixin, self).adapt_code(area_code) - - -class FrenchMapDepartments(IntCodeMixin, BaseMap): - """French department map""" - x_labels = list(DEPARTMENTS.keys()) - area_names = DEPARTMENTS - area_prefix = 'z' - kind = 'departement' - svg_map = DPT_MAP - - -with open(os.path.join( - os.path.dirname(__file__), 'maps', - 'fr.regions.svg')) as file: - REG_MAP = file.read() - - -class FrenchMapRegions(IntCodeMixin, BaseMap): - """French regions map""" - x_labels = list(REGIONS.keys()) - area_names = REGIONS - area_prefix = 'a' - svg_map = REG_MAP - kind = 'region' - - -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()) diff --git a/pygal/graph/maps/fr.departments.svg b/pygal/graph/maps/fr.departments.svg deleted file mode 100644 index 0d02fbd..0000000 --- a/pygal/graph/maps/fr.departments.svg +++ /dev/null @@ -1,328 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pygal/graph/maps/fr.regions.svg b/pygal/graph/maps/fr.regions.svg deleted file mode 100644 index 046c62d..0000000 --- a/pygal/graph/maps/fr.regions.svg +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pygal/maps/__init__.py b/pygal/maps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pygal/test/test_box.py b/pygal/test/test_box.py index 2a277a8..0d27491 100644 --- a/pygal/test/test_box.py +++ b/pygal/test/test_box.py @@ -22,7 +22,8 @@ from pygal import Box as ghostedBox def test_quartiles(): a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data - (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(a) + (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( + a, mode='1.5IQR') assert q1 == 7.0 / 4.0 assert q2 == 4.0 @@ -31,17 +32,20 @@ def test_quartiles(): assert q4 == 23 / 4.0 + 6.0 # q3 + 1.5 * iqr b = [1.0, 4.0, 6.0, 8.0] # even test data - (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(b) + (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( + b, mode='1.5IQR') assert q2 == 5.0 c = [2.0, None, 4.0, 6.0, None] # odd with None elements - (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(c) + (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( + c, mode='1.5IQR') assert q2 == 4.0 d = [4] - (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points(d) + (min_s, q0, q1, q2, q3, q4, max_s), outliers = Box._box_points( + d, mode='1.5IQR') assert q0 == 4 assert q1 == 4 diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index 9a3f935..373c279 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -20,9 +20,9 @@ from pygal import ( Line, Dot, Pie, Treemap, Radar, Config, Bar, Funnel, Worldmap, SupranationalWorldmap, Histogram, Gauge, Box, XY, - Pyramid, HorizontalBar, HorizontalStackedBar, - FrenchMapRegions, FrenchMapDepartments, SwissMapCantons, + Pyramid, HorizontalBar, HorizontalStackedBar, SwissMapCantons, DateTimeLine, TimeLine, DateLine, TimeDeltaLine) +from pygal.graph.map import BaseMap from pygal._compat import u from pygal.test.utils import texts from tempfile import NamedTemporaryFile @@ -273,9 +273,9 @@ def test_no_data(): def test_include_x_axis(Chart): chart = Chart() - if Chart in (Pie, Treemap, Radar, Funnel, Dot, Gauge, Worldmap, - SupranationalWorldmap, Histogram, Box, - FrenchMapRegions, FrenchMapDepartments, SwissMapCantons): + if Chart in ( + Pie, Treemap, Radar, Funnel, Dot, Gauge, Histogram, Box + ) or issubclass(Chart, BaseMap): return if not chart._dual: data = 100, 200, 150 @@ -360,11 +360,10 @@ def test_x_y_title(Chart): def test_x_label_major(Chart): if Chart in ( - Pie, Treemap, Funnel, Dot, Gauge, Worldmap, - SupranationalWorldmap, Histogram, Box, - FrenchMapRegions, FrenchMapDepartments, SwissMapCantons, + Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, Pyramid, DateTimeLine, TimeLine, DateLine, - TimeDeltaLine): + TimeDeltaLine + ) or issubclass(Chart, BaseMap): return chart = Chart() chart.add('test', range(12)) @@ -405,12 +404,11 @@ def test_x_label_major(Chart): def test_y_label_major(Chart): if Chart in ( - Pie, Treemap, Funnel, Dot, Gauge, Worldmap, - SupranationalWorldmap, Histogram, Box, - FrenchMapRegions, FrenchMapDepartments, SwissMapCantons, + Pie, Treemap, Funnel, Dot, Gauge, Histogram, Box, HorizontalBar, HorizontalStackedBar, Pyramid, DateTimeLine, TimeLine, DateLine, - TimeDeltaLine): + TimeDeltaLine + ) or issubclass(Chart, BaseMap): return chart = Chart() data = range(12) diff --git a/pygal/test/test_frenchmap.py b/pygal/test/test_frenchmap.py deleted file mode 100644 index 19b917f..0000000 --- a/pygal/test/test_frenchmap.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- 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 . - -from pygal import ( - FrenchMapRegions, FrenchMapDepartments) -from pygal.graph.frenchmap import REGIONS, DEPARTMENTS, aggregate_regions - - -def test_frenchmaps(): - datas = {} - for dept in DEPARTMENTS.keys(): - datas[dept] = int(''.join([x for x in dept if x.isdigit()])) * 10 - - fmap = FrenchMapDepartments() - fmap.add('departements', datas) - q = fmap.render_pyquery() - assert len( - q('#departements .departement,#dom-com .departement') - ) == len(DEPARTMENTS) - - fmap = FrenchMapRegions() - fmap.add('regions', aggregate_regions(datas)) - q = fmap.render_pyquery() - assert len(q('#regions .region,#dom-com .region')) == len(REGIONS) - - assert aggregate_regions(datas.items()) == aggregate_regions(datas)