Browse Source

Merge branch 'master' into 2.0.0

2.0.0
Florian Mounier 10 years ago
parent
commit
4817b8e3a4
  1. 32
      demo/moulinrouge/tests.py
  2. 1
      pygal/__init__.py
  3. 2
      pygal/config.py
  4. 13
      pygal/graph/base.py
  5. 91
      pygal/graph/ch.cantons.svg
  6. 8
      pygal/graph/graph.py
  7. 162
      pygal/graph/swissmap.py
  8. 8
      pygal/test/test_config.py
  9. 14
      pygal/test/test_graph.py
  10. 7
      pygal/view.py

32
demo/moulinrouge/tests.py

@ -2,11 +2,14 @@
# This file is part of pygal # This file is part of pygal
from pygal import ( from pygal import (
Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY, Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY,
CHARTS_BY_NAME, Config, Line, Worldmap, Histogram, Box, CHARTS_BY_NAME, Config, Line, Worldmap, Histogram, Box, SwissMapCantons,
FrenchMapDepartments, FrenchMapRegions, Pie, Treemap, TimeLine, DateLine) FrenchMapDepartments, FrenchMapRegions, Pie, Treemap, TimeLine, DateLine)
from pygal.style import styles, Style, RotateStyle from pygal.style import styles, Style, RotateStyle
from pygal.colors import rotate from pygal.colors import rotate
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from pygal.graph.swissmap import CANTONS
from random import randint, choice from random import randint, choice
from datetime import datetime from datetime import datetime
@ -374,7 +377,6 @@ def get_test_routes(app):
@app.route('/test/dateline') @app.route('/test/dateline')
def test_dateline(): def test_dateline():
from datetime import date
datey = DateLine(show_dots=False) datey = DateLine(show_dots=False)
datey.add('1', [ datey.add('1', [
(datetime(2013, 1, 2), 300), (datetime(2013, 1, 2), 300),
@ -443,6 +445,26 @@ def get_test_routes(app):
fmap.title = 'French map' fmap.title = 'French map'
return fmap.render_response() 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') @app.route('/test/frenchmapregions')
def test_frenchmapregions(): def test_frenchmapregions():
fmap = FrenchMapRegions(style=choice(list(styles.values()))) fmap = FrenchMapRegions(style=choice(list(styles.values())))
@ -591,4 +613,10 @@ def get_test_routes(app):
graph.legend_at_bottom = True graph.legend_at_bottom = True
return graph.render_response() return graph.render_response()
@app.route('/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()
return list(sorted(filter(lambda x: x.startswith('test'), locals()))) return list(sorted(filter(lambda x: x.startswith('test'), locals())))

1
pygal/__init__.py

@ -27,6 +27,7 @@ from pygal.graph.bar import Bar
from pygal.graph.box import Box from pygal.graph.box import Box
from pygal.graph.dot import Dot from pygal.graph.dot import Dot
from pygal.graph.frenchmap import FrenchMapDepartments, FrenchMapRegions from pygal.graph.frenchmap import FrenchMapDepartments, FrenchMapRegions
from pygal.graph.swissmap import SwissMapCantons
from pygal.graph.funnel import Funnel from pygal.graph.funnel import Funnel
from pygal.graph.gauge import Gauge from pygal.graph.gauge import Gauge
from pygal.graph.histogram import Histogram from pygal.graph.histogram import Histogram

2
pygal/config.py

@ -427,6 +427,8 @@ class Config(CommonConfig):
False, bool, "Misc", False, bool, "Misc",
"Don't prefix css") "Don't prefix css")
inverse_y_axis = Key(False, bool, "Misc", "Inverse Y axis direction")
class SerieConfig(CommonConfig): class SerieConfig(CommonConfig):
"""Class holding serie config values""" """Class holding serie config values"""

13
pygal/graph/base.py

@ -86,9 +86,11 @@ class BaseGraph(object):
def prepare_values(self, raw, offset=0): def prepare_values(self, raw, offset=0):
"""Prepare the values to start with sane values""" """Prepare the values to start with sane values"""
from pygal import Worldmap, FrenchMapDepartments, Histogram from pygal import (
Worldmap, FrenchMapDepartments, Histogram, SwissMapCantons)
# TODO: Generalize these conditions
if self.zero == 0 and isinstance( if self.zero == 0 and isinstance(
self, (Worldmap, FrenchMapDepartments)): self, (Worldmap, FrenchMapDepartments, SwissMapCantons)):
self.zero = 1 self.zero = 1
for key in ('x_labels', 'y_labels'): for key in ('x_labels', 'y_labels'):
@ -124,7 +126,8 @@ class BaseGraph(object):
metadata = {} metadata = {}
values = [] values = []
if isinstance(raw_values, dict): if isinstance(raw_values, dict):
if isinstance(self, (Worldmap, FrenchMapDepartments)): if isinstance(self, (
Worldmap, FrenchMapDepartments, SwissMapCantons)):
raw_values = list(raw_values.items()) raw_values = list(raw_values.items())
else: else:
value_list = [None] * width value_list = [None] * width
@ -159,7 +162,9 @@ class BaseGraph(object):
if x_adapter: if x_adapter:
value = (x_adapter(value[0]), adapter(value[1])) value = (x_adapter(value[0]), adapter(value[1]))
if isinstance( if isinstance(
self, (Worldmap, FrenchMapDepartments)): self, (
Worldmap, FrenchMapDepartments,
SwissMapCantons)):
value = (adapter(value[0]), value[1]) value = (adapter(value[0]), value[1])
else: else:
value = list(map(adapter, value)) value = list(map(adapter, value))

91
pygal/graph/ch.cantons.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 151 KiB

8
pygal/graph/graph.py

@ -24,7 +24,7 @@ Commmon graphing functions
from __future__ import division from __future__ import division
from pygal.interpolate import INTERPOLATIONS from pygal.interpolate import INTERPOLATIONS
from pygal.graph.base import BaseGraph 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 ( from pygal.util import (
cached_property, majorize, humanize, split_title, cached_property, majorize, humanize, split_title,
truncate, reverse_text_len, get_text_box, get_texts_box, cut, rad, truncate, reverse_text_len, get_text_box, get_texts_box, cut, rad,
@ -60,7 +60,7 @@ class Graph(BaseGraph):
else: else:
view_class = LogView view_class = LogView
else: else:
view_class = View view_class = ReverseView if self.inverse_y_axis else View
self.view = view_class( self.view = view_class(
self.width - self.margin_box.x, self.width - self.margin_box.x,
@ -213,7 +213,9 @@ class Graph(BaseGraph):
self.show_y_guides): self.show_y_guides):
self.svg.node( self.svg.node(
axis, 'path', 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' class_='line'
) )

162
pygal/graph/swissmap.py

@ -0,0 +1,162 @@
# -*- 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 <http://www.gnu.org/licenses/>.
"""
Worldmap chart
"""
from __future__ import division
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-ju': u("Jura"),
'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"),
'kt-ne': u("Neuenburg"),
'kt-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'

8
pygal/test/test_config.py

@ -21,7 +21,7 @@ from pygal import (
Line, Dot, Pie, Treemap, Radar, Config, Bar, Funnel, Worldmap, Line, Dot, Pie, Treemap, Radar, Config, Bar, Funnel, Worldmap,
SupranationalWorldmap, Histogram, Gauge, Box, XY, SupranationalWorldmap, Histogram, Gauge, Box, XY,
Pyramid, HorizontalBar, HorizontalStackedBar, Pyramid, HorizontalBar, HorizontalStackedBar,
FrenchMapRegions, FrenchMapDepartments, FrenchMapRegions, FrenchMapDepartments, SwissMapCantons,
DateTimeLine, TimeLine, DateLine, TimeDeltaLine) DateTimeLine, TimeLine, DateLine, TimeDeltaLine)
from pygal._compat import u from pygal._compat import u
from pygal.test.utils import texts from pygal.test.utils import texts
@ -275,7 +275,7 @@ def test_include_x_axis(Chart):
chart = Chart() chart = Chart()
if Chart in (Pie, Treemap, Radar, Funnel, Dot, Gauge, Worldmap, if Chart in (Pie, Treemap, Radar, Funnel, Dot, Gauge, Worldmap,
SupranationalWorldmap, Histogram, Box, SupranationalWorldmap, Histogram, Box,
FrenchMapRegions, FrenchMapDepartments): FrenchMapRegions, FrenchMapDepartments, SwissMapCantons):
return return
if not chart._dual: if not chart._dual:
data = 100, 200, 150 data = 100, 200, 150
@ -362,7 +362,7 @@ def test_x_label_major(Chart):
if Chart in ( if Chart in (
Pie, Treemap, Funnel, Dot, Gauge, Worldmap, Pie, Treemap, Funnel, Dot, Gauge, Worldmap,
SupranationalWorldmap, Histogram, Box, SupranationalWorldmap, Histogram, Box,
FrenchMapRegions, FrenchMapDepartments, FrenchMapRegions, FrenchMapDepartments, SwissMapCantons,
Pyramid, DateTimeLine, TimeLine, DateLine, Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine): TimeDeltaLine):
return return
@ -407,7 +407,7 @@ def test_y_label_major(Chart):
if Chart in ( if Chart in (
Pie, Treemap, Funnel, Dot, Gauge, Worldmap, Pie, Treemap, Funnel, Dot, Gauge, Worldmap,
SupranationalWorldmap, Histogram, Box, SupranationalWorldmap, Histogram, Box,
FrenchMapRegions, FrenchMapDepartments, FrenchMapRegions, FrenchMapDepartments, SwissMapCantons,
HorizontalBar, HorizontalStackedBar, HorizontalBar, HorizontalStackedBar,
Pyramid, DateTimeLine, TimeLine, DateLine, Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine): TimeDeltaLine):

14
pygal/test/test_graph.py

@ -24,6 +24,7 @@ import sys
import pytest import pytest
from pygal import i18n from pygal import i18n
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from pygal.graph.swissmap import CANTONS
from pygal.util import cut from pygal.util import cut
from pygal._compat import u from pygal._compat import u
from pygal.test import make_data from pygal.test import make_data
@ -86,6 +87,8 @@ def test_metadata(Chart):
v = [(i, k) for k, i in enumerate(REGIONS.keys())] v = [(i, k) for k, i in enumerate(REGIONS.keys())]
elif Chart == pygal.FrenchMapDepartments: elif Chart == pygal.FrenchMapDepartments:
v = [(i, k) for k, i in enumerate(DEPARTMENTS.keys())] v = [(i, k) for k, i in enumerate(DEPARTMENTS.keys())]
elif Chart == pygal.SwissMapCantons:
v = [(i, k) for k, i in enumerate(CANTONS.keys())]
chart.add('Serie with metadata', [ chart.add('Serie with metadata', [
v[0], v[0],
@ -110,7 +113,9 @@ def test_metadata(Chart):
assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value')) assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value'))
elif Chart not in ( elif Chart not in (
pygal.Worldmap, pygal.SupranationalWorldmap, pygal.Worldmap, pygal.SupranationalWorldmap,
pygal.FrenchMapRegions, pygal.FrenchMapDepartments): pygal.FrenchMapRegions, pygal.FrenchMapDepartments,
pygal.SwissMapCantons):
# Tooltip are not working on maps # Tooltip are not working on maps
assert len(v) == len(q('.tooltip-trigger').siblings('.value')) assert len(v) == len(q('.tooltip-trigger').siblings('.value'))
@ -193,7 +198,9 @@ def test_values_by_dict(Chart):
if not issubclass(Chart, ( if not issubclass(Chart, (
pygal.Worldmap, pygal.Worldmap,
pygal.FrenchMapDepartments, pygal.FrenchMapDepartments,
pygal.FrenchMapRegions)): pygal.FrenchMapRegions,
pygal.SwissMapCantons)):
chart1.add('A', {'red': 10, 'green': 12, 'blue': 14}) chart1.add('A', {'red': 10, 'green': 12, 'blue': 14})
chart1.add('B', {'green': 11, 'red': 7}) chart1.add('B', {'green': 11, 'red': 7})
chart1.add('C', {'blue': 7}) chart1.add('C', {'blue': 7})
@ -378,7 +385,8 @@ def test_labels_with_links(Chart):
if isinstance(chart, if isinstance(chart,
(pygal.graph.worldmap.Worldmap, (pygal.graph.worldmap.Worldmap,
pygal.graph.frenchmap.FrenchMapDepartments)): pygal.graph.frenchmap.FrenchMapDepartments,
pygal.graph.swissmap.SwissMapCantons)):
# No country is found in this case so: # No country is found in this case so:
assert len(links) == 4 # 3 links and 1 tooltip assert len(links) == 4 # 3 links and 1 tooltip
else: else:

7
pygal/view.py

@ -155,6 +155,13 @@ class View(object):
return (self.x(x), self.y(y)) 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): class HorizontalView(View):
def __init__(self, width, height, box): def __init__(self, width, height, box):
self._force_vertical = None self._force_vertical = None

Loading…
Cancel
Save