From b72b6f1112d7848774689cc54567f2f118a94b3e Mon Sep 17 00:00:00 2001 From: Serge Droz Date: Mon, 4 May 2015 13:26:15 +0200 Subject: [PATCH] Support for Swiss cantons. Keys are of the form kt-xx where xx ist the short for the respective canton --- pygal/graph/__init__.py | 1 + pygal/graph/ch.cantons.svg | 96 ++++++++++++++++++++ pygal/graph/swissmap.py | 173 +++++++++++++++++++++++++++++++++++++ pygal/util.py | 7 +- 4 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 pygal/graph/ch.cantons.svg create mode 100644 pygal/graph/swissmap.py diff --git a/pygal/graph/__init__.py b/pygal/graph/__init__.py index 0a05b2a..3216696 100644 --- a/pygal/graph/__init__.py +++ b/pygal/graph/__init__.py @@ -41,6 +41,7 @@ CHARTS_NAMES = [ 'Histogram', 'Box', 'FrenchMap', + 'SwissMap', 'Treemap', 'DateY', 'DateTimeLine', diff --git a/pygal/graph/ch.cantons.svg b/pygal/graph/ch.cantons.svg new file mode 100644 index 0000000..b2f3ffc --- /dev/null +++ b/pygal/graph/ch.cantons.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pygal/graph/swissmap.py b/pygal/graph/swissmap.py new file mode 100644 index 0000000..e18284e --- /dev/null +++ b/pygal/graph/swissmap.py @@ -0,0 +1,173 @@ +# -*- 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.ghost import ChartCollection +from pygal.util import cut, cached_property, decorate +from pygal.graph.graph import Graph +from pygal._compat import u +from pygal.etree import etree +from numbers import Number +import os + + +CANTONS = { + 'kt-zh': u("Zürich"), + 'kt-be': u("Bern"), + 'kt-lu': u("Luzern"), + 'kt-ur': u("Uri"), + 'kt-sz': u("Schwyz"), + 'kt-ow': u("Obwalden"), + 'kt-nw': u("Nidwalden"), + 'kt-gl': u("Glarus"), + 'kt-zg': u("Zug"), + 'kt-fr': u("Freiburg"), + 'kt-so': u("Solothurn"), + 'kt-bl': u("Basel-Stadt "), + 'kt-bs': u("Basle-Land"), + 'kt-sh': u("Schaffhausen"), + 'kt-ar': u("Appenzell Ausseroden"), + 'kt-ai': u("Appenzell Innerroden"), + 'kt-sg': u("St. Gallen"), + 'kt-gr': u("Graubünden"), + 'kt-ag': u("Aargau"), + 'kt-tg': u("Thurgau"), + 'kt-ti': u("Tessin"), + 'kt-vd': u("Waadt"), + 'kt-vs': u("Wallis"), + 'ke-ne': u("Neuenburg"), + 'ke-ge': u("Genf"), +} + + + + + +with open(os.path.join( + os.path.dirname(__file__), + 'ch.cantons.svg')) as file: + CNT_MAP = file.read() + + +class SwissMapCantons(Graph): + """Swiss Cantons map""" + _dual = True + x_labels = list(CANTONS.keys()) + area_names = CANTONS + area_prefix = 'z' + kind = 'canton' + svg_map = CNT_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_) + try: + areae = map.findall( + ".//*[@class='%s%s %s map-element']" % ( + self.area_prefix, area_code, + self.kind)) + except SyntaxError: + # Python 2.6 (you'd better install lxml) + areae = [] + for g in map: + for e in g: + if '%s%s' % ( + self.area_prefix, area_code + ) in e.attrib.get('class', ''): + areae.append(e) + + 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: + node = decorate(self.svg, area, metadata) + if node != area: + area.remove(node) + for g in map: + if area not in g: + continue + index = list(g).index(area) + g.remove(area) + node.append(area) + g.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 SwissMapCantons(SwissMapCantons): + """French regions map""" + x_labels = list(CANTONS.keys()) + area_names = CANTONS + area_prefix = 'z' + svg_map = CNT_MAP + kind = 'canton' + +class SwissMap(ChartCollection): + Cantons = SwissMapCantons + + + + diff --git a/pygal/util.py b/pygal/util.py index 2e9521f..15703c1 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -339,9 +339,10 @@ def prepare_values(raw, config, cls, offset=0): from pygal.graph.histogram import Histogram from pygal.graph.worldmap import Worldmap from pygal.graph.frenchmap import FrenchMapDepartments + from pygal.graph.swissmap import SwissMapCantons if config.x_labels is None and hasattr(cls, 'x_labels'): config.x_labels = list(map(to_unicode, cls.x_labels)) - if config.zero == 0 and issubclass(cls, (Worldmap, FrenchMapDepartments)): + if config.zero == 0 and issubclass(cls, (Worldmap, FrenchMapDepartments,SwissMapCantons)): config.zero = 1 for key in ('x_labels', 'y_labels'): @@ -376,7 +377,7 @@ def prepare_values(raw, config, cls, offset=0): metadata = {} values = [] if isinstance(raw_values, dict): - if issubclass(cls, (Worldmap, FrenchMapDepartments)): + if issubclass(cls, (Worldmap, FrenchMapDepartments,SwissMapCantons)): raw_values = list(raw_values.items()) else: value_list = [None] * width @@ -411,7 +412,7 @@ def prepare_values(raw, config, cls, offset=0): if x_adapter: value = (x_adapter(value[0]), adapter(value[1])) if issubclass( - cls, (Worldmap, FrenchMapDepartments)): + cls, (Worldmap, FrenchMapDepartments,SwissMapCantons)): value = (adapter(value[0]), value[1]) else: value = list(map(adapter, value))