Browse Source

Refactor maps

2.0.0
Florian Mounier 10 years ago
parent
commit
a61384fe99
  1. 5
      pygal/__init__.py
  2. 17
      pygal/graph/base.py
  3. 91
      pygal/graph/ch.cantons.svg
  4. 328
      pygal/graph/fr.departments.svg
  5. 91
      pygal/graph/fr.regions.svg
  6. 97
      pygal/graph/frenchmap.py
  7. 113
      pygal/graph/map.py
  8. 109
      pygal/graph/supranationalworldmap.py
  9. 94
      pygal/graph/swissmap.py
  10. 84
      pygal/graph/worldmap.py
  11. 2408
      pygal/graph/worldmap.svg
  12. 24
      pygal/test/__init__.py
  13. 30
      pygal/test/test_graph.py
  14. 6
      pygal/test/test_map.py
  15. 2
      setup.py

5
pygal/__init__.py

@ -27,7 +27,6 @@ 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
@ -39,11 +38,11 @@ from pygal.graph.pyramid import Pyramid
from pygal.graph.radar import Radar
from pygal.graph.stackedbar import StackedBar
from pygal.graph.stackedline import StackedLine
from pygal.graph.supranationalworldmap import SupranationalWorldmap
from pygal.graph.swissmap import SwissMapCantons
from pygal.graph.time import DateLine, DateTimeLine, TimeLine, TimeDeltaLine
from pygal.graph.treemap import Treemap
from pygal.graph.verticalpyramid import VerticalPyramid
from pygal.graph.worldmap import Worldmap
from pygal.graph.worldmap import Worldmap, SupranationalWorldmap
from pygal.graph.xy import XY
from pygal.graph.graph import Graph
from pygal.config import Config

17
pygal/graph/base.py

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

Before

Width:  |  Height:  |  Size: 151 KiB

328
pygal/graph/fr.departments.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 584 KiB

91
pygal/graph/fr.regions.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 370 KiB

97
pygal/graph/frenchmap.py

@ -23,11 +23,8 @@ Worldmap chart
from __future__ import division
from collections import defaultdict
from pygal.util import cut, cached_property, decorate
from pygal.graph.graph import Graph
from pygal.graph.map import BaseMap
from pygal._compat import u
from pygal.etree import etree
from numbers import Number
import os
@ -177,102 +174,22 @@ with open(os.path.join(
DPT_MAP = file.read()
with open(os.path.join(
os.path.dirname(__file__),
'fr.regions.svg')) as file:
REG_MAP = file.read()
class FrenchMapDepartments(Graph):
class FrenchMapDepartments(BaseMap):
"""French department map"""
_dual = True
x_labels = list(DEPARTMENTS.keys())
area_names = DEPARTMENTS
area_prefix = 'z'
kind = 'departement'
svg_map = DPT_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)
with open(os.path.join(
os.path.dirname(__file__),
'fr.regions.svg')) as file:
REG_MAP = file.read()
class FrenchMapRegions(FrenchMapDepartments):
class FrenchMapRegions(BaseMap):
"""French regions map"""
x_labels = list(REGIONS.keys())
area_names = REGIONS

113
pygal/graph/map.py

@ -0,0 +1,113 @@
# -*- 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/>.
from __future__ import division
from pygal.graph.graph import Graph
from pygal.util import cut, cached_property, decorate
from pygal.etree import etree
from numbers import Number
class BaseMap(Graph):
"""Base map."""
_dual = True
@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 get_values(self, serie):
return serie.values
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(self.get_values(serie)):
# TODO: Generalize
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)

109
pygal/graph/supranationalworldmap.py

@ -1,109 +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 <http://www.gnu.org/licenses/>.
"""
Supranational Worldmap chart
"""
from __future__ import division
from pygal.graph.worldmap import Worldmap
from pygal.i18n import SUPRANATIONAL
from pygal.util import cut, decorate
from pygal.etree import etree
import os
with open(os.path.join(
os.path.dirname(__file__),
'worldmap.svg')) as file:
MAP = file.read()
class SupranationalWorldmap(Worldmap):
"""SupranationalWorldmap graph"""
def _plot(self):
map = etree.fromstring(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)
serie.values = self.replace_supranationals(serie.values)
for j, (country_code, value) in enumerate(serie.values):
if value is None:
continue
if max_ == min_:
ratio = 1
else:
ratio = .3 + .7 * (value - min_) / (max_ - min_)
try:
country = map.find('.//*[@id="%s"]' % country_code)
except SyntaxError:
# Python 2.6 (you'd better install lxml)
country = None
for e in map:
if e.attrib.get('id', '') == country_code:
country = e
if country is None:
continue
cls = country.get('class', '').split(' ')
cls.append('color-%d' % i)
country.set('class', ' '.join(cls))
country.set(
'style', 'fill-opacity: %f' % (
ratio))
metadata = serie.metadata.get(j)
if metadata:
node = decorate(self.svg, country, metadata)
if node != country:
country.remove(node)
index = list(map).index(country)
map.remove(country)
node.append(country)
map.insert(index, node)
last_node = len(country) > 0 and country[-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(country, 'title')
text = ''
title_node.text = text + '[%s] %s: %d' % (
serie.title,
self.country_names[country_code], value)
self.nodes['plot'].append(map)
def replace_supranationals(self, values):
"""Replaces the values if it contains a supranational code."""
for i, (code, value) in enumerate(values[:]):
for suprakey in SUPRANATIONAL.keys():
if suprakey == code:
values.extend(
[(country, value) for country in SUPRANATIONAL[code]])
values.remove((code, value))
return values

94
pygal/graph/swissmap.py

@ -22,11 +22,8 @@ Worldmap chart
"""
from __future__ import division
from pygal.util import cut, cached_property, decorate
from pygal.graph.graph import Graph
from pygal.graph.map import BaseMap
from pygal._compat import u
from pygal.etree import etree
from numbers import Number
import os
@ -66,97 +63,10 @@ with open(os.path.join(
CNT_MAP = file.read()
class SwissMapCantons(Graph):
class SwissMapCantons(BaseMap):
"""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'

84
pygal/graph/worldmap.py

@ -23,22 +23,24 @@ Worldmap chart
from __future__ import division
from pygal.util import cut, cached_property, decorate
from pygal.graph.graph import Graph
from pygal.i18n import COUNTRIES
from pygal.graph.map import BaseMap
from pygal.i18n import COUNTRIES, SUPRANATIONAL
from pygal.etree import etree
import os
with open(os.path.join(
os.path.dirname(__file__),
'worldmap.svg')) as file:
MAP = file.read()
WORLD_MAP = file.read()
class Worldmap(Graph):
class Worldmap(BaseMap):
"""Worldmap graph"""
_dual = True
x_labels = list(COUNTRIES.keys())
country_names = COUNTRIES
area_names = COUNTRIES
area_prefix = ''
svg_map = WORLD_MAP
kind = 'country'
@cached_property
def countries(self):
@ -55,63 +57,19 @@ class Worldmap(Graph):
for val in serie.values
if val[1] is not None]
def _plot(self):
map = etree.fromstring(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, (country_code, value) in enumerate(serie.values):
if value is None:
continue
if max_ == min_:
ratio = 1
else:
ratio = .3 + .7 * (value - min_) / (max_ - min_)
class SupranationalWorldmap(Worldmap):
"""SupranationalWorldmap graph"""
try:
country = map.find('.//*[@id="%s"]' % country_code)
except SyntaxError:
# Python 2.6 (you'd better install lxml)
country = None
for e in map:
if e.attrib.get('id', '') == country_code:
country = e
def get_values(self, serie):
return self.replace_supranationals(serie.values)
if country is None:
continue
cls = country.get('class', '').split(' ')
cls.append('color-%d' % i)
country.set('class', ' '.join(cls))
country.set(
'style', 'fill-opacity: %f' % (
ratio))
metadata = serie.metadata.get(j)
if metadata:
node = decorate(self.svg, country, metadata)
if node != country:
country.remove(node)
index = list(map).index(country)
map.remove(country)
node.append(country)
map.insert(index, node)
last_node = len(country) > 0 and country[-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(country, 'title')
text = ''
title_node.text = text + '[%s] %s: %s' % (
serie.title,
self.country_names[country_code], self._format(value))
self.nodes['plot'].append(map)
def replace_supranationals(self, values):
"""Replaces the values if it contains a supranational code."""
for i, (code, value) in enumerate(values[:]):
for suprakey in SUPRANATIONAL.keys():
if suprakey == code:
values.extend(
[(country, value) for country in SUPRANATIONAL[code]])
values.remove((code, value))
return values

2408
pygal/graph/worldmap.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 754 KiB

24
pygal/test/__init__.py

@ -20,9 +20,10 @@
import pygal
from pygal.util import cut
from pygal.i18n import COUNTRIES
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from pygal.graph.map import BaseMap
from decimal import Decimal
def get_data(i):
return [
[(-1, 1), (2, 0), (0, 4)],
@ -38,29 +39,16 @@ def adapt(chart, data):
# return list(map(
# lambda t:
# (datetime.fromtimestamp(1360000000 + t[0] * 987654)
# if t[0] is not None else None, t[1]), data))
# if t[0] is not None else None, t[1]), data))
if isinstance(chart, pygal.XY):
return data
data = cut(data)
if isinstance(chart, pygal.Worldmap):
return list(
map(lambda x: list(
COUNTRIES.keys())[
int(x) % len(COUNTRIES)]
if x is not None else None, data))
elif isinstance(chart, pygal.FrenchMapRegions):
return list(
map(lambda x: list(
REGIONS.keys())[
int(x) % len(REGIONS)]
if x is not None else None, data))
elif isinstance(chart, pygal.FrenchMapDepartments):
if isinstance(chart, BaseMap):
return list(
map(lambda x: list(
DEPARTMENTS.keys())[
int(x) % len(DEPARTMENTS)]
map(lambda x: chart.__class__.x_labels[
int(x) % len(chart.__class__.x_labels)]
if x is not None else None, data))
return data

30
pygal/test/test_graph.py

@ -23,8 +23,7 @@ import uuid
import sys
import pytest
from pygal import i18n
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from pygal.graph.swissmap import CANTONS
from pygal.graph.map import BaseMap
from pygal.util import cut
from pygal._compat import u
from pygal.test import make_data
@ -81,14 +80,8 @@ def test_metadata(Chart):
return # summary charts cannot display per-value metadata
elif Chart == pygal.XY:
v = list(map(lambda x: (x, x + 1), v))
elif Chart == pygal.Worldmap or Chart == pygal.SupranationalWorldmap:
v = [(i, k) for k, i in enumerate(i18n.COUNTRIES.keys())]
elif Chart == pygal.FrenchMapRegions:
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())]
elif issubclass(Chart, BaseMap):
v = [(i, k) for k, i in enumerate(Chart.x_labels)]
chart.add('Serie with metadata', [
v[0],
@ -111,10 +104,7 @@ def test_metadata(Chart):
if Chart in (pygal.Pie, pygal.Treemap):
# Slices with value 0 are not rendered
assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value'))
elif Chart not in (
pygal.Worldmap, pygal.SupranationalWorldmap,
pygal.FrenchMapRegions, pygal.FrenchMapDepartments,
pygal.SwissMapCantons):
elif not issubclass(Chart, BaseMap):
# Tooltip are not working on maps
assert len(v) == len(q('.tooltip-trigger').siblings('.value'))
@ -195,12 +185,7 @@ def test_values_by_dict(Chart):
chart1 = Chart(no_prefix=True)
chart2 = Chart(no_prefix=True)
if not issubclass(Chart, (
pygal.Worldmap,
pygal.FrenchMapDepartments,
pygal.FrenchMapRegions,
pygal.SwissMapCantons)):
if not issubclass(Chart, BaseMap):
chart1.add('A', {'red': 10, 'green': 12, 'blue': 14})
chart1.add('B', {'green': 11, 'red': 7})
chart1.add('C', {'blue': 7})
@ -383,10 +368,7 @@ def test_labels_with_links(Chart):
q = chart.render_pyquery()
links = q('a')
if isinstance(chart,
(pygal.graph.worldmap.Worldmap,
pygal.graph.frenchmap.FrenchMapDepartments,
pygal.graph.swissmap.SwissMapCantons)):
if isinstance(chart, BaseMap):
# No country is found in this case so:
assert len(links) == 4 # 3 links and 1 tooltip
else:

6
pygal/test/test_map.py

@ -41,7 +41,7 @@ def test_worldmap():
assert len(
q('.country.color-0')
) == len(COUNTRIES)
assert 'France' in q('#fr').text()
assert 'France' in q('.country.fr').text()
def test_worldmap_i18n():
@ -57,7 +57,7 @@ def test_worldmap_i18n():
assert len(
q('.country.color-0')
) == len(COUNTRIES)
assert 'Francia' in q('#fr').text()
assert 'Francia' in q('.country.fr').text()
def test_worldmap_i18n_clear():
@ -69,7 +69,7 @@ def test_worldmap_i18n_clear():
assert len(
q('.country.color-0')
) == 1
assert 'Frankreich' in q('#fr').text()
assert 'Frankreich' in q('.country.fr').text()
def test_supranationalworldmap():

2
setup.py

@ -64,7 +64,7 @@ setup(
"svg", "chart", "graph", "diagram", "plot", "histogram", "kiviat"],
tests_require=["pytest", "pyquery", "flask", "cairosvg"],
cmdclass={'test': PyTest},
package_data={'pygal': ['css/*', 'graph/*.svg']},
package_data={'pygal': ['css/*', 'graph/maps/*.svg']},
extras_require={
'lxml': ['lxml'],
'png': ['cairosvg']

Loading…
Cancel
Save