From 4dedda50d229a16ddc53552633c0f8730cef6644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Plasse?= Date: Wed, 18 Mar 2015 12:16:35 +0100 Subject: [PATCH 1/3] Allow to inverse y axis, Fixes #24 --- demo/moulinrouge/tests.py | 11 ++++++++--- pygal/config.py | 2 ++ pygal/graph/graph.py | 8 +++++--- pygal/view.py | 7 +++++++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 15e7870..e051aaa 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -379,7 +379,6 @@ def get_test_routes(app): @app.route('/test/dateline') def test_dateline(): - from datetime import date datey = DateLine(show_dots=False) datey.add('1', [ (datetime(2013, 1, 2), 300), @@ -427,8 +426,8 @@ def get_test_routes(app): (time(21, 2, 29), 10), (time(12, 30, 59), 7) ]) - datey.add('2', - [(time(12, 12, 12), 4), (time(), 8), (time(23, 59, 59), 6)]) + datey.add( + '2', [(time(12, 12, 12), 4), (time(), 8), (time(23, 59, 59), 6)]) datey.x_label_rotation = 25 return datey.render_response() @@ -624,4 +623,10 @@ def get_test_routes(app): graph.legend_at_bottom = True return graph.render_response() + @app.route('/test/inverse_y_axis/') + def test_inverse_y_axis(chart): + graph = CHARTS_BY_NAME[chart](**dict(inverse_y_axis=True)) + graph.add('inverse', [1, 2, 3, 12, 24, 36]) + return graph.render_response() + return list(sorted(filter(lambda x: x.startswith('test'), locals()))) diff --git a/pygal/config.py b/pygal/config.py index 9e91fad..489618d 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -446,6 +446,8 @@ class Config(CommonConfig): False, bool, "Misc", "Don't prefix css") + inverse_y_axis = Key(False, bool, "Misc", "Inverse Y axis direction") + class SerieConfig(CommonConfig): """Class holding serie config values""" diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index 5a26986..c4d4215 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -24,7 +24,7 @@ Commmon graphing functions from __future__ import division from pygal.interpolate import INTERPOLATIONS from pygal.graph.base import BaseGraph -from pygal.view import View, LogView, XYLogView +from pygal.view import View, LogView, XYLogView, ReverseView from pygal.util import ( truncate, reverse_text_len, get_texts_box, cut, rad, decorate) from math import sqrt, ceil, cos @@ -58,7 +58,7 @@ class Graph(BaseGraph): else: view_class = LogView else: - view_class = View + view_class = ReverseView if self.inverse_y_axis else View self.view = view_class( self.width - self.margin.x, @@ -211,7 +211,9 @@ class Graph(BaseGraph): self.show_y_guides): self.svg.node( axis, 'path', - d='M%f %f h%f' % (0, self.view.height, self.view.width), + d='M%f %f h%f' % ( + 0, 0 if self.inverse_y_axis else self.view.height, + self.view.width), class_='line' ) diff --git a/pygal/view.py b/pygal/view.py index 0de1e48..e0a6ff5 100644 --- a/pygal/view.py +++ b/pygal/view.py @@ -155,6 +155,13 @@ class View(object): return (self.x(x), self.y(y)) +class ReverseView(View): + def y(self, y): + if y is None: + return None + return (self.height * (y - self.box.ymin) / self.box.height) + + class HorizontalView(View): def __init__(self, width, height, box): self._force_vertical = None From b72b6f1112d7848774689cc54567f2f118a94b3e Mon Sep 17 00:00:00 2001 From: Serge Droz Date: Mon, 4 May 2015 13:26:15 +0200 Subject: [PATCH 2/3] 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)) From 90a91b76b5bca68f73db09e528d5bb2a4d34c5df Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Mon, 4 May 2015 16:38:37 +0200 Subject: [PATCH 3/3] PR Fix #207 --- demo/moulinrouge/tests.py | 26 +++++- pygal/graph/ch.cantons.svg | 179 ++++++++++++++++++------------------- pygal/graph/swissmap.py | 16 ++-- pygal/test/test_config.py | 8 +- pygal/test/test_graph.py | 12 ++- pygal/util.py | 10 ++- 6 files changed, 135 insertions(+), 116 deletions(-) diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index e051aaa..0e4184b 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -3,10 +3,12 @@ from pygal import ( Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY, CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram, Box, - FrenchMap_Departments, FrenchMap_Regions, Pie, Treemap, TimeLine, DateLine) + FrenchMap_Departments, FrenchMap_Regions, Pie, Treemap, TimeLine, DateLine, + SwissMap_Cantons) from pygal.style import styles, Style, RotateStyle from pygal.colors import rotate from pygal.graph.frenchmap import DEPARTMENTS, REGIONS +from pygal.graph.swissmap import CANTONS from random import randint, choice from datetime import datetime @@ -475,6 +477,26 @@ def get_test_routes(app): fmap.title = 'French map' return fmap.render_response() + @app.route('/test/swissmap') + def test_swissmap(): + smap = SwissMap_Cantons(style=choice(list(styles.values()))) + for i in range(10): + smap.add('s%d' % i, [ + (choice(list(CANTONS.keys())), randint(0, 100)) + for _ in range(randint(1, 5))]) + + smap.add('links', [{ + 'value': ('kt-vs', 10), + 'label': '\o/', + 'xlink': 'http://google.com?q=69' + }, { + 'value': ('bt', 20), + 'label': 'Y', + }]) + smap.add('6th', [3, 5, 34, 12]) + smap.title = 'Swiss map' + return smap.render_response() + @app.route('/test/frenchmapregions') def test_frenchmapregions(): fmap = FrenchMap_Regions(style=choice(list(styles.values()))) @@ -624,7 +646,7 @@ def get_test_routes(app): return graph.render_response() @app.route('/test/inverse_y_axis/') - def test_inverse_y_axis(chart): + def test_inverse_y_axis_for(chart): graph = CHARTS_BY_NAME[chart](**dict(inverse_y_axis=True)) graph.add('inverse', [1, 2, 3, 12, 24, 36]) return graph.render_response() diff --git a/pygal/graph/ch.cantons.svg b/pygal/graph/ch.cantons.svg index b2f3ffc..62ab954 100644 --- a/pygal/graph/ch.cantons.svg +++ b/pygal/graph/ch.cantons.svg @@ -1,96 +1,91 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pygal/graph/swissmap.py b/pygal/graph/swissmap.py index e18284e..21be8bc 100644 --- a/pygal/graph/swissmap.py +++ b/pygal/graph/swissmap.py @@ -22,7 +22,6 @@ 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 @@ -36,6 +35,7 @@ CANTONS = { 'kt-zh': u("Zürich"), 'kt-be': u("Bern"), 'kt-lu': u("Luzern"), + 'kt-ju': u("Jura"), 'kt-ur': u("Uri"), 'kt-sz': u("Schwyz"), 'kt-ow': u("Obwalden"), @@ -44,7 +44,7 @@ CANTONS = { 'kt-zg': u("Zug"), 'kt-fr': u("Freiburg"), 'kt-so': u("Solothurn"), - 'kt-bl': u("Basel-Stadt "), + 'kt-bl': u("Basel-Stadt"), 'kt-bs': u("Basle-Land"), 'kt-sh': u("Schaffhausen"), 'kt-ar': u("Appenzell Ausseroden"), @@ -56,14 +56,11 @@ CANTONS = { 'kt-ti': u("Tessin"), 'kt-vd': u("Waadt"), 'kt-vs': u("Wallis"), - 'ke-ne': u("Neuenburg"), - 'ke-ge': u("Genf"), + 'kt-ne': u("Neuenburg"), + 'kt-ge': u("Genf"), } - - - with open(os.path.join( os.path.dirname(__file__), 'ch.cantons.svg')) as file: @@ -165,9 +162,6 @@ class SwissMapCantons(SwissMapCantons): svg_map = CNT_MAP kind = 'canton' + class SwissMap(ChartCollection): Cantons = SwissMapCantons - - - - diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index d773ea9..8a4d03c 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -21,7 +21,7 @@ from pygal import ( Line, Dot, Pie, Treemap, Radar, Config, Bar, Funnel, Worldmap, SupranationalWorldmap, Histogram, Gauge, Box, XY, Pyramid, DateY, HorizontalBar, HorizontalStackedBar, - FrenchMap_Regions, FrenchMap_Departments, + FrenchMap_Regions, FrenchMap_Departments, SwissMap_Cantons, DateTimeLine, TimeLine, DateLine, TimeDeltaLine) from pygal._compat import u from pygal.test.utils import texts @@ -275,7 +275,7 @@ def test_include_x_axis(Chart): chart = Chart() if Chart in (Pie, Treemap, Radar, Funnel, Dot, Gauge, Worldmap, SupranationalWorldmap, Histogram, Box, - FrenchMap_Regions, FrenchMap_Departments): + FrenchMap_Regions, FrenchMap_Departments, SwissMap_Cantons): return if not chart.cls._dual: data = 100, 200, 150 @@ -362,7 +362,7 @@ def test_x_label_major(Chart): if Chart in ( Pie, Treemap, Funnel, Dot, Gauge, Worldmap, SupranationalWorldmap, Histogram, Box, - FrenchMap_Regions, FrenchMap_Departments, + FrenchMap_Regions, FrenchMap_Departments, SwissMap_Cantons, Pyramid, DateY, DateTimeLine, TimeLine, DateLine, TimeDeltaLine): return @@ -407,7 +407,7 @@ def test_y_label_major(Chart): if Chart in ( Pie, Treemap, Funnel, Dot, Gauge, Worldmap, SupranationalWorldmap, Histogram, Box, - FrenchMap_Regions, FrenchMap_Departments, + FrenchMap_Regions, FrenchMap_Departments, SwissMap_Cantons, HorizontalBar, HorizontalStackedBar, Pyramid, DateTimeLine, TimeLine, DateLine, TimeDeltaLine, DateY): diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py index 1d223ee..3d455cf 100644 --- a/pygal/test/test_graph.py +++ b/pygal/test/test_graph.py @@ -24,6 +24,7 @@ import sys import pytest from pygal import i18n from pygal.graph.frenchmap import DEPARTMENTS, REGIONS +from pygal.graph.swissmap import CANTONS from pygal.util import cut from pygal._compat import u from pygal.test import make_data @@ -86,6 +87,8 @@ def test_metadata(Chart): 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())] + elif Chart == pygal.SwissMap_Cantons: + v = [(i, k) for k, i in enumerate(CANTONS.keys())] chart.add('Serie with metadata', [ v[0], @@ -110,7 +113,8 @@ def test_metadata(Chart): assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value')) elif Chart not in ( pygal.Worldmap, pygal.SupranationalWorldmap, - pygal.FrenchMap_Regions, pygal.FrenchMap_Departments): + pygal.FrenchMap_Regions, pygal.FrenchMap_Departments, + pygal.SwissMap_Cantons): # Tooltip are not working on maps assert len(v) == len(q('.tooltip-trigger').siblings('.value')) @@ -193,7 +197,8 @@ def test_values_by_dict(Chart): if not issubclass(Chart, ( pygal.Worldmap, pygal.FrenchMap_Departments, - pygal.FrenchMap_Regions)): + pygal.FrenchMap_Regions, + pygal.SwissMap_Cantons)): chart1.add('A', {'red': 10, 'green': 12, 'blue': 14}) chart1.add('B', {'green': 11, 'red': 7}) chart1.add('C', {'blue': 7}) @@ -378,7 +383,8 @@ def test_labels_with_links(Chart): if issubclass(chart.cls, (pygal.graph.worldmap.Worldmap, - pygal.graph.frenchmap.FrenchMapDepartments)): + pygal.graph.frenchmap.FrenchMapDepartments, + pygal.graph.swissmap.SwissMapCantons)): # No country is found in this case so: assert len(links) == 4 # 3 links and 1 tooltip else: diff --git a/pygal/util.py b/pygal/util.py index 15703c1..84f5401 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -335,14 +335,14 @@ def prepare_values(raw, config, cls, offset=0): """Prepare the values to start with sane values""" from pygal.serie import Serie from pygal.config import SerieConfig - from pygal.graph.time import DateY 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,SwissMapCantons)): + if config.zero == 0 and issubclass(cls, ( + Worldmap, FrenchMapDepartments, SwissMapCantons)): config.zero = 1 for key in ('x_labels', 'y_labels'): @@ -377,7 +377,8 @@ def prepare_values(raw, config, cls, offset=0): metadata = {} values = [] if isinstance(raw_values, dict): - if issubclass(cls, (Worldmap, FrenchMapDepartments,SwissMapCantons)): + if issubclass(cls, ( + Worldmap, FrenchMapDepartments, SwissMapCantons)): raw_values = list(raw_values.items()) else: value_list = [None] * width @@ -412,7 +413,8 @@ def prepare_values(raw, config, cls, offset=0): if x_adapter: value = (x_adapter(value[0]), adapter(value[1])) if issubclass( - cls, (Worldmap, FrenchMapDepartments,SwissMapCantons)): + cls, ( + Worldmap, FrenchMapDepartments, SwissMapCantons)): value = (adapter(value[0]), value[1]) else: value = list(map(adapter, value))