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
from pygal import (
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)
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
@ -374,7 +377,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),
@ -443,6 +445,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 = FrenchMapRegions(style=choice(list(styles.values())))
@ -591,4 +613,10 @@ def get_test_routes(app):
graph.legend_at_bottom = True
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())))

1
pygal/__init__.py

@ -27,6 +27,7 @@ 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.swissmap import SwissMapCantons
from pygal.graph.funnel import Funnel
from pygal.graph.gauge import Gauge
from pygal.graph.histogram import Histogram

2
pygal/config.py

@ -427,6 +427,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"""

13
pygal/graph/base.py

@ -86,9 +86,11 @@ class BaseGraph(object):
def prepare_values(self, raw, offset=0):
"""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(
self, (Worldmap, FrenchMapDepartments)):
self, (Worldmap, FrenchMapDepartments, SwissMapCantons)):
self.zero = 1
for key in ('x_labels', 'y_labels'):
@ -124,7 +126,8 @@ class BaseGraph(object):
metadata = {}
values = []
if isinstance(raw_values, dict):
if isinstance(self, (Worldmap, FrenchMapDepartments)):
if isinstance(self, (
Worldmap, FrenchMapDepartments, SwissMapCantons)):
raw_values = list(raw_values.items())
else:
value_list = [None] * width
@ -159,7 +162,9 @@ class BaseGraph(object):
if x_adapter:
value = (x_adapter(value[0]), adapter(value[1]))
if isinstance(
self, (Worldmap, FrenchMapDepartments)):
self, (
Worldmap, FrenchMapDepartments,
SwissMapCantons)):
value = (adapter(value[0]), value[1])
else:
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 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 (
cached_property, majorize, humanize, split_title,
truncate, reverse_text_len, get_text_box, get_texts_box, cut, rad,
@ -60,7 +60,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_box.x,
@ -213,7 +213,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'
)

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,
SupranationalWorldmap, Histogram, Gauge, Box, XY,
Pyramid, HorizontalBar, HorizontalStackedBar,
FrenchMapRegions, FrenchMapDepartments,
FrenchMapRegions, FrenchMapDepartments, SwissMapCantons,
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,
FrenchMapRegions, FrenchMapDepartments):
FrenchMapRegions, FrenchMapDepartments, SwissMapCantons):
return
if not chart._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,
FrenchMapRegions, FrenchMapDepartments,
FrenchMapRegions, FrenchMapDepartments, SwissMapCantons,
Pyramid, 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,
FrenchMapRegions, FrenchMapDepartments,
FrenchMapRegions, FrenchMapDepartments, SwissMapCantons,
HorizontalBar, HorizontalStackedBar,
Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine):

14
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.FrenchMapDepartments:
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', [
v[0],
@ -110,7 +113,9 @@ def test_metadata(Chart):
assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value'))
elif Chart not in (
pygal.Worldmap, pygal.SupranationalWorldmap,
pygal.FrenchMapRegions, pygal.FrenchMapDepartments):
pygal.FrenchMapRegions, pygal.FrenchMapDepartments,
pygal.SwissMapCantons):
# Tooltip are not working on maps
assert len(v) == len(q('.tooltip-trigger').siblings('.value'))
@ -193,7 +198,9 @@ def test_values_by_dict(Chart):
if not issubclass(Chart, (
pygal.Worldmap,
pygal.FrenchMapDepartments,
pygal.FrenchMapRegions)):
pygal.FrenchMapRegions,
pygal.SwissMapCantons)):
chart1.add('A', {'red': 10, 'green': 12, 'blue': 14})
chart1.add('B', {'green': 11, 'red': 7})
chart1.add('C', {'blue': 7})
@ -378,7 +385,8 @@ def test_labels_with_links(Chart):
if isinstance(chart,
(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:

7
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

Loading…
Cancel
Save