Browse Source

Try to make it work without ghost. Not really possible as is.

2.0.0
Florian Mounier 10 years ago
parent
commit
26abcc6662
  1. 3
      demo/moulinrouge/__init__.py
  2. 49
      demo/moulinrouge/tests.py
  3. 42
      pygal/__init__.py
  4. 19
      pygal/config.py
  5. 220
      pygal/ghost.py
  6. 28
      pygal/graph/__init__.py
  7. 506
      pygal/graph/base.py
  8. 6
      pygal/graph/frenchmap.py
  9. 260
      pygal/graph/graph.py
  10. 19
      pygal/graph/time.py
  11. 2
      pygal/serie.py
  12. 28
      pygal/state.py
  13. 10
      pygal/style.py
  14. 43
      pygal/svg.py
  15. 5
      pygal/table.py
  16. 17
      pygal/test/__init__.py
  17. 20
      pygal/test/test_config.py
  18. 6
      pygal/test/test_frenchmap.py
  19. 12
      pygal/test/test_graph.py
  20. 129
      pygal/util.py

3
demo/moulinrouge/__init__.py

@ -20,7 +20,6 @@ from flask import Flask, render_template, Response, request
import pygal
from pygal.config import Config
from pygal.util import cut
from pygal.graph import CHARTS_NAMES
from pygal.etree import etree
from pygal.style import styles, parametric_styles
from base64 import (
@ -95,7 +94,7 @@ def create_app():
'index.jinja2', styles=styles, parametric_styles=parametric_styles,
parametric_colors=(
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe'),
links=links, charts_name=CHARTS_NAMES)
links=links, charts_name=pygal.CHARTS_NAMES)
@app.route("/svg/<type>/<series>/<config>")
def svg(type, series, config):

49
demo/moulinrouge/tests.py

@ -2,8 +2,8 @@
# This file is part of pygal
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)
CHARTS_BY_NAME, Config, Line, Worldmap, Histogram, Box,
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
@ -225,12 +225,6 @@ def get_test_routes(app):
graph.add('Single', [(1, 1)])
return graph.render_response()
@app.route('/test/datey_single')
def test_datey_single():
graph = DateY(interpolate='cubic')
graph.add('Single', [(datetime.now(), 1)])
return graph.render_response()
@app.route('/test/no_data/at_all/<chart>')
def test_no_data_at_all_for(chart):
graph = CHARTS_BY_NAME[chart]()
@ -287,6 +281,7 @@ def get_test_routes(app):
bar = Bar()
bar.add('1', [1, 2, 3])
bar.add('2', [4, 5, 6])
bar.x_labels = ['a']
return bar.render_response()
@app.route('/test/histogram')
@ -390,35 +385,7 @@ def get_test_routes(app):
datey.x_label_rotation = 25
return datey.render_response()
@app.route('/test/datey')
def test_datey():
from datetime import datetime
datey = DateY(show_dots=False)
datey.add('1', [
(datetime(2011, 12, 21), 10),
(datetime(2014, 4, 8), 12),
(datetime(2010, 2, 28), 2)
])
datey.add('2', [(12, 4), (219, 8), (928, 6)])
datey.x_label_rotation = 25
return datey.render_response()
@app.route('/test/datexy')
def test_datexy():
from datetime import datetime, date, timedelta
datey = DateY()
datey.add('1', [
(datetime(2011, 12, 21), 10),
(datetime(2014, 4, 8), 12),
(datetime(2010, 2, 28), 2)
])
datey.add('2', map(
lambda t: (date.today() + timedelta(days=t[0]), t[1]),
[(12, 4), (219, 8), (928, 6)]))
datey.x_label_rotation = 25
return datey.render_response()
@app.route('/test/timexy')
@app.route('/test/timeline')
def test_timexy():
from datetime import time
datey = TimeLine()
@ -427,8 +394,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()
@ -458,7 +425,7 @@ def get_test_routes(app):
@app.route('/test/frenchmapdepartments')
def test_frenchmapdepartments():
fmap = FrenchMap_Departments(style=choice(list(styles.values())))
fmap = FrenchMapDepartments(style=choice(list(styles.values())))
for i in range(10):
fmap.add('s%d' % i, [
(choice(list(DEPARTMENTS.keys())), randint(0, 100))
@ -478,7 +445,7 @@ def get_test_routes(app):
@app.route('/test/frenchmapregions')
def test_frenchmapregions():
fmap = FrenchMap_Regions(style=choice(list(styles.values())))
fmap = FrenchMapRegions(style=choice(list(styles.values())))
for i in range(10):
fmap.add('s%d' % i, [
(choice(list(REGIONS.keys())), randint(0, 100))

42
pygal/__init__.py

@ -21,20 +21,36 @@ Pygal - A python svg graph plotting library
"""
__version__ = '1.7.0'
import sys
from pygal.config import Config
from pygal.ghost import Ghost, REAL_CHARTS
__version__ = '1.9.9'
CHARTS = []
CHARTS_BY_NAME = {}
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.funnel import Funnel
from pygal.graph.gauge import Gauge
from pygal.graph.histogram import Histogram
from pygal.graph.horizontalbar import HorizontalBar
from pygal.graph.horizontalstackedbar import HorizontalStackedBar
from pygal.graph.line import Line
from pygal.graph.pie import Pie
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.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.xy import XY
from pygal.graph.graph import Graph
from pygal.config import Config
for NAME in REAL_CHARTS.keys():
_CHART = type(NAME, (Ghost,), {})
CHARTS.append(_CHART)
CHARTS_BY_NAME[NAME] = _CHART
setattr(sys.modules[__name__], NAME, _CHART)
CHARTS_BY_NAME = dict(
[(k, v) for k, v in locals().items()
if isinstance(v, type) and issubclass(v, Graph) and v != Graph])
__all__ = list(CHARTS_BY_NAME.keys()) + [
Config.__name__, 'CHARTS', 'CHARTS_BY_NAME']
CHARTS_NAMES = list(CHARTS_BY_NAME.keys())
CHARTS = list(CHARTS_BY_NAME.values())

19
pygal/config.py

@ -25,9 +25,6 @@ from pygal.style import Style, DefaultStyle
from pygal.interpolate import INTERPOLATIONS
class FontSizes(object):
"""Container for font sizes"""
CONFIG_ITEMS = []
@ -127,18 +124,6 @@ class BaseConfig(MetaConfig('ConfigBase', (object,), {})):
dict([(k, v) for (k, v) in kwargs.items()
if not k.startswith('_') and k in dir(self)]))
def font_sizes(self, with_unit=True):
"""Getter for all font size configs"""
fs = FontSizes()
for name in dir(self):
if name.endswith('_font_size'):
setattr(
fs,
name.replace('_font_size', ''),
('%dpx' % getattr(self, name))
if with_unit else getattr(self, name))
return fs
def to_dict(self):
config = {}
for attr in dir(self):
@ -321,10 +306,6 @@ class Config(CommonConfig):
y_label_rotation = Key(
0, int, "Label", "Specify y labels rotation angles", "in degrees")
x_label_format = Key(
"%Y-%m-%d %H:%M:%S.%f", str, "Label",
"Date format for strftime to display the DateY X labels")
missing_value_fill_truncation = Key(
"x", str, "Look",
"Filled series with missing x and/or y values at the end of a series "

220
pygal/ghost.py

@ -1,220 +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/>.
"""
Ghost container
It is used to delegate rendering to real objects but keeping config in place
"""
from __future__ import division
import io
import sys
from pygal._compat import u, is_list_like
from pygal.graph import CHARTS_NAMES
from pygal.config import Config, CONFIG_ITEMS
from pygal.util import prepare_values
from uuid import uuid4
class ChartCollection(object):
pass
REAL_CHARTS = {
'DateY': 'pygal.graph.time.DateY',
'DateTimeLine': 'pygal.graph.time.DateTimeLine',
'DateLine': 'pygal.graph.time.DateLine',
'TimeLine': 'pygal.graph.time.TimeLine',
'TimeDeltaLine': 'pygal.graph.time.TimeDeltaLine'
}
for NAME in CHARTS_NAMES:
if NAME in REAL_CHARTS:
mod_name = 'pygal.graph.time'
else:
mod_name = 'pygal.graph.%s' % NAME.lower()
__import__(mod_name)
mod = sys.modules[mod_name]
chart = getattr(mod, NAME)
if issubclass(chart, ChartCollection):
for name, chart in chart.__dict__.items():
if name.startswith('_'):
continue
REAL_CHARTS['%s_%s' % (NAME, name)] = chart
else:
REAL_CHARTS[NAME] = chart
class Ghost(object):
def __init__(self, config=None, **kwargs):
"""Init config"""
name = self.__class__.__name__
self.cls = REAL_CHARTS[name]
self.uuid = str(uuid4())
if config and isinstance(config, type):
config = config()
if config:
config = config.copy()
else:
config = Config()
config(**kwargs)
self.config = config
self.raw_series = []
self.raw_series2 = []
self.xml_filters = []
def add(self, title, values, **kwargs):
"""Add a serie to this graph"""
if not is_list_like(values) and not isinstance(values, dict):
values = [values]
if kwargs.get('secondary', False):
self.raw_series2.append((title, values, kwargs))
else:
self.raw_series.append((title, values, kwargs))
def add_xml_filter(self, callback):
self.xml_filters.append(callback)
def make_series(self, series, offset=0):
return prepare_values(series, self.config, self.cls, offset)
def make_instance(self, overrides=None):
for conf_key in CONFIG_ITEMS:
if conf_key.is_list:
if getattr(self, conf_key.name, None):
setattr(self, conf_key.name,
list(getattr(self, conf_key.name)))
self.config(**self.__dict__)
self.config.__dict__.update(overrides or {})
series = self.make_series(self.raw_series)
secondary_series = self.make_series(
self.raw_series2, len(series or []))
self._last__inst = self.cls(
self.config, series, secondary_series, self.uuid,
self.xml_filters)
return self._last__inst
# Rendering
def render(self, is_unicode=False, **kwargs):
return (self
.make_instance(overrides=kwargs)
.render(is_unicode=is_unicode))
def render_tree(self, **kwargs):
return self.make_instance(overrides=kwargs).render_tree()
def render_table(self, **kwargs):
# Import here to avoid lxml import
try:
from pygal.table import Table
except ImportError:
raise ImportError('You must install lxml to use render table')
real_cls, self.cls = self.cls, Table
rv = self.make_instance().render(**kwargs)
self.cls = real_cls
return rv
def render_pyquery(self):
"""Render the graph, and return a pyquery wrapped tree"""
from pyquery import PyQuery as pq
return pq(self.render(), parser='html')
def render_in_browser(self, **kwargs):
"""Render the graph, open it in your browser with black magic"""
try:
from lxml.html import open_in_browser
except ImportError:
raise ImportError('You must install lxml to use render in browser')
open_in_browser(self.render_tree(**kwargs), encoding='utf-8')
def render_response(self, **kwargs):
"""Render the graph, and return a Flask response"""
from flask import Response
return Response(self.render(**kwargs), mimetype='image/svg+xml')
def render_django_response(self, **kwargs):
"""Render the graph, and return a Django response"""
from django.http import HttpResponse
return HttpResponse(self.render(**kwargs), content_type='image/svg+xml')
def render_to_file(self, filename, **kwargs):
"""Render the graph, and write it to filename"""
with io.open(filename, 'w', encoding='utf-8') as f:
f.write(self.render(is_unicode=True, **kwargs))
def render_to_png(self, filename=None, dpi=72, **kwargs):
"""Render the graph, convert it to png and write it to filename"""
import cairosvg
return cairosvg.svg2png(
bytestring=self.render(**kwargs), write_to=filename, dpi=dpi)
def render_sparktext(self, relative_to=None):
"""Make a mini text sparkline from chart"""
bars = u('▁▂▃▄▅▆▇█')
if len(self.raw_series) == 0:
return u('')
values = list(self.raw_series[0][1])
if len(values) == 0:
return u('')
chart = u('')
values = list(map(lambda x: max(x, 0), values))
vmax = max(values)
if relative_to is None:
relative_to = min(values)
if (vmax - relative_to) == 0:
chart = bars[0] * len(values)
return chart
divisions = len(bars) - 1
for value in values:
chart += bars[int(divisions *
(value - relative_to) / (vmax - relative_to))]
return chart
def render_sparkline(self, **kwargs):
spark_options = dict(
width=200,
height=50,
show_dots=False,
show_legend=False,
show_x_labels=False,
show_y_labels=False,
spacing=0,
margin=5,
explicit_size=True
)
spark_options.update(kwargs)
return self.make_instance(spark_options).render()
def _repr_svg_(self):
"""Display svg in IPython notebook"""
return self.render(disable_xml_declaration=True)
def _repr_png_(self):
"""Display png in IPython notebook"""
return self.render_to_png()

28
pygal/graph/__init__.py

@ -20,31 +20,3 @@
Graph modules
"""
CHARTS_NAMES = [
'Line',
'StackedLine',
'XY',
'Bar',
'HorizontalBar',
'StackedBar',
'HorizontalStackedBar',
'Pie',
'Radar',
'Funnel',
'Pyramid',
'VerticalPyramid',
'Dot',
'Gauge',
'Worldmap',
'SupranationalWorldmap',
'Histogram',
'Box',
'FrenchMap',
'Treemap',
'DateY',
'DateTimeLine',
'DateLine',
'TimeLine',
'TimeDeltaLine'
]

506
pygal/graph/base.py

@ -22,12 +22,18 @@ Base for pygal charts
"""
from __future__ import division
from pygal._compat import u, is_list_like, to_unicode
from pygal.view import Margin, Box
from pygal.util import (
get_text_box, get_texts_box, cut, rad, humanize, truncate, split_title)
from pygal.config import Config
from pygal.state import State
from pygal.util import compose, ident
from pygal.svg import Svg
from pygal.util import cached_property, majorize
from math import sin, cos, sqrt, ceil
from pygal.serie import Serie
from pygal.config import SerieConfig
from pygal.adapters import (
not_zero, positive, decimal_to_float)
from functools import reduce
from uuid import uuid4
class BaseGraph(object):
@ -35,14 +41,149 @@ class BaseGraph(object):
_adapters = []
def __init__(self, config, series, secondary_series, uuid, xml_filters):
"""Init the graph"""
self.uuid = uuid
self.__dict__.update(config.to_dict())
def __init__(self, config=None, **kwargs):
if config:
if isinstance(config, type):
config = config()
else:
config = config.copy()
else:
config = Config()
config(**kwargs)
self.config = config
self.series = series or []
self.secondary_series = secondary_series or []
self.xml_filters = xml_filters or []
self.state = None
self.uuid = str(uuid4())
self.raw_series = []
self.raw_series2 = []
self.xml_filters = []
def __setattr__(self, name, value):
if name.startswith('__') or getattr(self, 'state', None) is None:
super(BaseGraph, self).__setattr__(name, value)
else:
setattr(self.state, name, value)
def __getattribute__(self, name):
if name.startswith('__') or name == 'state' or getattr(
self, 'state', None
) is None or name not in self.state.__dict__:
return super(BaseGraph, self).__getattribute__(name)
return getattr(self.state, name)
def add(self, title, values, **kwargs):
"""Add a serie to this graph"""
if not is_list_like(values) and not isinstance(values, dict):
values = [values]
if kwargs.get('secondary', False):
self.raw_series2.append((title, values, kwargs))
else:
self.raw_series.append((title, values, kwargs))
def add_xml_filter(self, callback):
self.xml_filters.append(callback)
def prepare_values(self, raw, offset=0):
"""Prepare the values to start with sane values"""
from pygal import Worldmap, FrenchMapDepartments, Histogram
if self.x_labels is not None:
self.x_labels = list(map(to_unicode, self.x_labels))
if self.zero == 0 and isinstance(
self, (Worldmap, FrenchMapDepartments)):
self.zero = 1
for key in ('x_labels', 'y_labels'):
if getattr(self, key):
setattr(self, key, list(getattr(self, key)))
if not raw:
return
adapters = list(self._adapters) or [lambda x:x]
if self.logarithmic:
for fun in not_zero, positive:
if fun in adapters:
adapters.remove(fun)
adapters = adapters + [positive, not_zero]
adapters = adapters + [decimal_to_float]
adapter = reduce(compose, adapters) if not self.strict else ident
x_adapter = reduce(
compose, self._x_adapters) if getattr(
self, '_x_adapters', None) else None
series = []
raw = [(
title,
list(raw_values) if not isinstance(
raw_values, dict) else raw_values,
serie_config_kwargs
) for title, raw_values, serie_config_kwargs in raw]
width = max([len(values) for _, values, _ in raw] +
[len(self.x_labels or [])])
for title, raw_values, serie_config_kwargs in raw:
metadata = {}
values = []
if isinstance(raw_values, dict):
if isinstance(self, (Worldmap, FrenchMapDepartments)):
raw_values = list(raw_values.items())
else:
value_list = [None] * width
for k, v in raw_values.items():
if k in self.x_labels:
value_list[self.x_labels.index(k)] = v
raw_values = value_list
for index, raw_value in enumerate(
raw_values + (
(width - len(raw_values)) * [None] # aligning values
if len(raw_values) < width else [])):
if isinstance(raw_value, dict):
raw_value = dict(raw_value)
value = raw_value.pop('value', None)
metadata[index] = raw_value
else:
value = raw_value
# Fix this by doing this in charts class methods
if isinstance(self, Histogram):
if value is None:
value = (None, None, None)
elif not is_list_like(value):
value = (value, self.zero, self.zero)
value = list(map(adapter, value))
elif self._dual:
if value is None:
value = (None, None)
elif not is_list_like(value):
value = (value, self.zero)
if x_adapter:
value = (x_adapter(value[0]), adapter(value[1]))
if isinstance(
self, (Worldmap, FrenchMapDepartments)):
value = (adapter(value[0]), value[1])
else:
value = list(map(adapter, value))
else:
value = adapter(value)
values.append(value)
serie_config = SerieConfig()
serie_config(**{k: v for k, v in self.state.__dict__.items()
if k in dir(serie_config)})
serie_config(**serie_config_kwargs)
series.append(
Serie(offset + len(series),
title, values, serie_config, metadata))
return series
def setup(self):
"""Init the graph"""
self.state = State(self)
self.series = self.prepare_values(
self.raw_series) or []
self.secondary_series = self.prepare_values(
self.raw_series2, len(self.series)) or []
self.horizontal = getattr(self, 'horizontal', False)
self.svg = Svg(self)
self._x_labels = None
@ -50,22 +191,18 @@ class BaseGraph(object):
self._x_2nd_labels = None
self._y_2nd_labels = None
self.nodes = {}
self.margin = Margin(self.margin_top or self.margin,
self.margin_right or self.margin,
self.margin_bottom or self.margin,
self.margin_left or self.margin)
self.margin = Margin(
self.margin_top or self.margin,
self.margin_right or self.margin,
self.margin_bottom or self.margin,
self.margin_left or self.margin)
self._box = Box()
self.view = None
if self.logarithmic and self.zero == 0:
# Explicit min to avoid interpolation dependency
if self._dual:
get = lambda x: x[1] or 1
else:
get = lambda x: x
positive_values = list(filter(
lambda x: x > 0,
[get(val)
[val[1] or 1 if self._dual else val
for serie in self.series for val in serie.safe_values]))
self.zero = min(positive_values or (1,)) or 1
@ -74,237 +211,17 @@ class BaseGraph(object):
self._draw()
self.svg.pre_render()
@property
def all_series(self):
return self.series + self.secondary_series
@property
def _x_format(self):
"""Return the value formatter for this graph"""
return self.config.x_value_formatter or (
humanize if self.human_readable else str)
@property
def _format(self):
"""Return the value formatter for this graph"""
return self.config.value_formatter or (
humanize if self.human_readable else str)
def _compute(self):
"""Initial computations to draw the graph"""
def _compute_margin(self):
"""Compute graph margins from set texts"""
self._legend_at_left_width = 0
for series_group in (self.series, self.secondary_series):
if self.show_legend and series_group:
h, w = get_texts_box(
map(lambda x: truncate(x, self.truncate_legend or 15),
cut(series_group, 'title')),
self.legend_font_size)
if self.legend_at_bottom:
h_max = max(h, self.legend_box_size)
cols = (self._order // self.legend_at_bottom_columns
if self.legend_at_bottom_columns
else ceil(sqrt(self._order)) or 1)
self.margin.bottom += self.spacing + h_max * round(
cols - 1) * 1.5 + h_max
else:
if series_group is self.series:
legend_width = self.spacing + w + self.legend_box_size
self.margin.left += legend_width
self._legend_at_left_width += legend_width
else:
self.margin.right += (
self.spacing + w + self.legend_box_size)
self._x_labels_height = 0
if (self._x_labels or self._x_2nd_labels) and self.show_x_labels:
for xlabels in (self._x_labels, self._x_2nd_labels):
if xlabels:
h, w = get_texts_box(
map(lambda x: truncate(x, self.truncate_label or 25),
cut(xlabels)),
self.label_font_size)
self._x_labels_height = self.spacing + max(
w * sin(rad(self.x_label_rotation)), h)
if xlabels is self._x_labels:
self.margin.bottom += self._x_labels_height
else:
self.margin.top += self._x_labels_height
if self.x_label_rotation:
self.margin.right = max(
w * cos(rad(self.x_label_rotation)),
self.margin.right)
if self.show_y_labels:
for ylabels in (self._y_labels, self._y_2nd_labels):
if ylabels:
h, w = get_texts_box(
cut(ylabels), self.label_font_size)
if ylabels is self._y_labels:
self.margin.left += self.spacing + max(
w * cos(rad(self.y_label_rotation)), h)
else:
self.margin.right += self.spacing + max(
w * cos(rad(self.y_label_rotation)), h)
self.title = split_title(
self.title, self.width, self.title_font_size)
if self.title:
h, _ = get_text_box(self.title[0], self.title_font_size)
self.margin.top += len(self.title) * (self.spacing + h)
self.x_title = split_title(
self.x_title, self.width - self.margin.x, self.title_font_size)
self._x_title_height = 0
if self.x_title:
h, _ = get_text_box(self.x_title[0], self.title_font_size)
height = len(self.x_title) * (self.spacing + h)
self.margin.bottom += height
self._x_title_height = height + self.spacing
self.y_title = split_title(
self.y_title, self.height - self.margin.y, self.title_font_size)
self._y_title_height = 0
if self.y_title:
h, _ = get_text_box(self.y_title[0], self.title_font_size)
height = len(self.y_title) * (self.spacing + h)
self.margin.left += height
self._y_title_height = height + self.spacing
@cached_property
def _legends(self):
"""Getter for series title"""
return [serie.title for serie in self.series]
@cached_property
def _secondary_legends(self):
"""Getter for series title on secondary y axis"""
return [serie.title for serie in self.secondary_series]
@cached_property
def _values(self):
"""Getter for series values (flattened)"""
return [val
for serie in self.series
for val in serie.values
if val is not None]
@cached_property
def _secondary_values(self):
"""Getter for secondary series values (flattened)"""
return [val
for serie in self.secondary_series
for val in serie.values
if val is not None]
@cached_property
def _len(self):
"""Getter for the maximum series size"""
return max([
len(serie.values)
for serie in self.all_series] or [0])
@cached_property
def _secondary_min(self):
"""Getter for the minimum series value"""
return (self.range[0] if (self.range and self.range[0] is not None)
else (min(self._secondary_values)
if self._secondary_values else None))
@cached_property
def _min(self):
"""Getter for the minimum series value"""
return (self.range[0] if (self.range and self.range[0] is not None)
else (min(self._values)
if self._values else None))
@cached_property
def _max(self):
"""Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None)
else (max(self._values) if self._values else None))
@cached_property
def _secondary_max(self):
"""Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None)
else (max(self._secondary_values)
if self._secondary_values else None))
@cached_property
def _order(self):
"""Getter for the number of series"""
return len(self.all_series)
@cached_property
def _x_major_labels(self):
"""Getter for the x major label"""
if self.x_labels_major:
return self.x_labels_major
if self.x_labels_major_every:
return [self._x_labels[i][0] for i in range(
0, len(self._x_labels), self.x_labels_major_every)]
if self.x_labels_major_count:
label_count = len(self._x_labels)
major_count = self.x_labels_major_count
if (major_count >= label_count):
return [label[0] for label in self._x_labels]
return [self._x_labels[
int(i * (label_count - 1) / (major_count - 1))][0]
for i in range(major_count)]
return []
@cached_property
def _y_major_labels(self):
"""Getter for the y major label"""
if self.y_labels_major:
return self.y_labels_major
if self.y_labels_major_every:
return [self._y_labels[i][1] for i in range(
0, len(self._y_labels), self.y_labels_major_every)]
if self.y_labels_major_count:
label_count = len(self._y_labels)
major_count = self.y_labels_major_count
if (major_count >= label_count):
return [label[1] for label in self._y_labels]
return [self._y_labels[
int(i * (label_count - 1) / (major_count - 1))][1]
for i in range(major_count)]
return majorize(
cut(self._y_labels, 1)
)
def _draw(self):
"""Draw all the things"""
self._compute()
self._compute_secondary()
self._post_compute()
self._compute_margin()
self._decorate()
if self.series and self._has_data():
self._plot()
else:
self.svg.draw_no_data()
def teardown(self):
del self.state
self.state = None
def _has_data(self):
"""Check if there is any data"""
return sum(
map(len, map(lambda s: s.safe_values, self.series))) != 0 and (
sum(map(abs, self._values)) != 0)
def render(self, is_unicode=False):
def render(self, is_unicode=False, **kwargs):
"""Render the graph, and return the svg string"""
return self.svg.render(
self.setup()
svg = self.svg.render(
is_unicode=is_unicode, pretty_print=self.pretty_print)
self.teardown()
return svg
def render_tree(self):
"""Render the graph, and return (l)xml etree"""
@ -312,3 +229,94 @@ class BaseGraph(object):
for f in self.xml_filters:
svg = f(svg)
return svg
def render_table(self, **kwargs):
# Import here to avoid lxml import
try:
from pygal.table import Table
except ImportError:
raise ImportError('You must install lxml to use render table')
return Table(self).render(**kwargs)
def render_pyquery(self):
"""Render the graph, and return a pyquery wrapped tree"""
from pyquery import PyQuery as pq
return pq(self.render(), parser='html')
def render_in_browser(self, **kwargs):
"""Render the graph, open it in your browser with black magic"""
try:
from lxml.html import open_in_browser
except ImportError:
raise ImportError('You must install lxml to use render in browser')
open_in_browser(self.render_tree(**kwargs), encoding='utf-8')
def render_response(self, **kwargs):
"""Render the graph, and return a Flask response"""
from flask import Response
return Response(self.render(**kwargs), mimetype='image/svg+xml')
def render_django_response(self, **kwargs):
"""Render the graph, and return a Django response"""
from django.http import HttpResponse
return HttpResponse(
self.render(**kwargs), content_type='image/svg+xml')
def render_to_file(self, filename, **kwargs):
"""Render the graph, and write it to filename"""
with open(filename, 'w', encoding='utf-8') as f:
f.write(self.render(is_unicode=True, **kwargs))
def render_to_png(self, filename=None, dpi=72, **kwargs):
"""Render the graph, convert it to png and write it to filename"""
import cairosvg
return cairosvg.svg2png(
bytestring=self.render(**kwargs), write_to=filename, dpi=dpi)
def render_sparktext(self, relative_to=None):
"""Make a mini text sparkline from chart"""
bars = u('▁▂▃▄▅▆▇█')
if len(self.raw_series) == 0:
return u('')
values = list(self.raw_series[0][1])
if len(values) == 0:
return u('')
chart = u('')
values = list(map(lambda x: max(x, 0), values))
vmax = max(values)
if relative_to is None:
relative_to = min(values)
if (vmax - relative_to) == 0:
chart = bars[0] * len(values)
return chart
divisions = len(bars) - 1
for value in values:
chart += bars[int(divisions *
(value - relative_to) / (vmax - relative_to))]
return chart
def render_sparkline(self, **kwargs):
spark_options = dict(
width=200,
height=50,
show_dots=False,
show_legend=False,
show_x_labels=False,
show_y_labels=False,
spacing=0,
margin=5,
explicit_size=True
)
spark_options.update(kwargs)
return self.make_instance(spark_options).render()
def _repr_svg_(self):
"""Display svg in IPython notebook"""
return self.render(disable_xml_declaration=True)
def _repr_png_(self):
"""Display png in IPython notebook"""

6
pygal/graph/frenchmap.py

@ -23,7 +23,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
from pygal._compat import u
@ -282,11 +281,6 @@ class FrenchMapRegions(FrenchMapDepartments):
kind = 'region'
class FrenchMap(ChartCollection):
Regions = FrenchMapRegions
Departments = FrenchMapDepartments
DEPARTMENTS_REGIONS = {
"01": "82",
"02": "22",

260
pygal/graph/graph.py

@ -26,8 +26,10 @@ from pygal.interpolate import INTERPOLATIONS
from pygal.graph.base import BaseGraph
from pygal.view import View, LogView, XYLogView
from pygal.util import (
truncate, reverse_text_len, get_texts_box, cut, rad, decorate)
from math import sqrt, ceil, cos
cached_property, majorize, humanize, split_title,
truncate, reverse_text_len, get_text_box, get_texts_box, cut, rad,
decorate)
from math import sqrt, ceil, cos, sin
from itertools import repeat, chain
@ -41,9 +43,9 @@ class Graph(BaseGraph):
self._make_graph()
self._axes()
self._legend()
self._title()
self._x_title()
self._y_title()
self._make_title()
self._make_x_title()
self._make_y_title()
def _axes(self):
"""Draw axes"""
@ -350,7 +352,7 @@ class Graph(BaseGraph):
width=self.legend_box_size,
height=self.legend_box_size,
class_="color-%d reactive" % (
global_serie_number % len(self.style['colors']))
global_serie_number % len(self.style.colors))
)
if isinstance(title, dict):
@ -369,22 +371,22 @@ class Graph(BaseGraph):
if truncated != title:
self.svg.node(legend, 'title').text = title
def _title(self):
def _make_title(self):
"""Make the title"""
if self.title:
for i, title_line in enumerate(self.title, 1):
if self._title:
for i, title_line in enumerate(self._title, 1):
self.svg.node(
self.nodes['title'], 'text', class_='title plot_title',
x=self.width / 2,
y=i * (self.title_font_size + self.spacing)
).text = title_line
def _x_title(self):
def _make_x_title(self):
"""Make the X-Axis title"""
y = (self.height - self.margin.bottom +
self._x_labels_height)
if self.x_title:
for i, title_line in enumerate(self.x_title, 1):
if self._x_title:
for i, title_line in enumerate(self._x_title, 1):
text = self.svg.node(
self.nodes['title'], 'text', class_='title',
x=self.margin.left + self.view.width / 2,
@ -392,11 +394,11 @@ class Graph(BaseGraph):
)
text.text = title_line
def _y_title(self):
def _make_y_title(self):
"""Make the Y-Axis title"""
if self.y_title:
if self._y_title:
yc = self.margin.top + self.view.height / 2
for i, title_line in enumerate(self.y_title, 1):
for i, title_line in enumerate(self._y_title, 1):
text = self.svg.node(
self.nodes['title'], 'text', class_='title',
x=self._legend_at_left_width,
@ -488,3 +490,231 @@ class Graph(BaseGraph):
def _post_compute(self):
pass
@property
def all_series(self):
return self.series + self.secondary_series
@property
def _x_format(self):
"""Return the value formatter for this graph"""
return self.x_value_formatter or (
humanize if self.human_readable else str)
@property
def _format(self):
"""Return the value formatter for this graph"""
return self.value_formatter or (
humanize if self.human_readable else str)
def _compute(self):
"""Initial computations to draw the graph"""
def _compute_margin(self):
"""Compute graph margins from set texts"""
self._legend_at_left_width = 0
for series_group in (self.series, self.secondary_series):
if self.show_legend and series_group:
h, w = get_texts_box(
map(lambda x: truncate(x, self.truncate_legend or 15),
cut(series_group, 'title')),
self.legend_font_size)
if self.legend_at_bottom:
h_max = max(h, self.legend_box_size)
cols = (self._order // self.legend_at_bottom_columns
if self.legend_at_bottom_columns
else ceil(sqrt(self._order)) or 1)
self.margin.bottom += self.spacing + h_max * round(
cols - 1) * 1.5 + h_max
else:
if series_group is self.series:
legend_width = self.spacing + w + self.legend_box_size
self.margin.left += legend_width
self._legend_at_left_width += legend_width
else:
self.margin.right += (
self.spacing + w + self.legend_box_size)
self._x_labels_height = 0
if (self._x_labels or self._x_2nd_labels) and self.show_x_labels:
for xlabels in (self._x_labels, self._x_2nd_labels):
if xlabels:
h, w = get_texts_box(
map(lambda x: truncate(x, self.truncate_label or 25),
cut(xlabels)),
self.label_font_size)
self._x_labels_height = self.spacing + max(
w * sin(rad(self.x_label_rotation)), h)
if xlabels is self._x_labels:
self.margin.bottom += self._x_labels_height
else:
self.margin.top += self._x_labels_height
if self.x_label_rotation:
self.margin.right = max(
w * cos(rad(self.x_label_rotation)),
self.margin.right)
if self.show_y_labels:
for ylabels in (self._y_labels, self._y_2nd_labels):
if ylabels:
h, w = get_texts_box(
cut(ylabels), self.label_font_size)
if ylabels is self._y_labels:
self.margin.left += self.spacing + max(
w * cos(rad(self.y_label_rotation)), h)
else:
self.margin.right += self.spacing + max(
w * cos(rad(self.y_label_rotation)), h)
self._title = split_title(
self.title, self.width, self.title_font_size)
if self.title:
h, _ = get_text_box(self._title[0], self.title_font_size)
self.margin.top += len(self._title) * (self.spacing + h)
self._x_title = split_title(
self.x_title, self.width - self.margin.x, self.title_font_size)
self._x_title_height = 0
if self._x_title:
h, _ = get_text_box(self._x_title[0], self.title_font_size)
height = len(self._x_title) * (self.spacing + h)
self.margin.bottom += height
self._x_title_height = height + self.spacing
self._y_title = split_title(
self.y_title, self.height - self.margin.y,
self.title_font_size)
self._y_title_height = 0
if self._y_title:
h, _ = get_text_box(self._y_title[0], self.title_font_size)
height = len(self._y_title) * (self.spacing + h)
self.margin.left += height
self._y_title_height = height + self.spacing
@cached_property
def _legends(self):
"""Getter for series title"""
return [serie.title for serie in self.series]
@cached_property
def _secondary_legends(self):
"""Getter for series title on secondary y axis"""
return [serie.title for serie in self.secondary_series]
@cached_property
def _values(self):
"""Getter for series values (flattened)"""
return [val
for serie in self.series
for val in serie.values
if val is not None]
@cached_property
def _secondary_values(self):
"""Getter for secondary series values (flattened)"""
return [val
for serie in self.secondary_series
for val in serie.values
if val is not None]
@cached_property
def _len(self):
"""Getter for the maximum series size"""
return max([
len(serie.values)
for serie in self.all_series] or [0])
@cached_property
def _secondary_min(self):
"""Getter for the minimum series value"""
return (self.range[0] if (self.range and self.range[0] is not None)
else (min(self._secondary_values)
if self._secondary_values else None))
@cached_property
def _min(self):
"""Getter for the minimum series value"""
return (self.range[0] if (self.range and self.range[0] is not None)
else (min(self._values)
if self._values else None))
@cached_property
def _max(self):
"""Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None)
else (max(self._values) if self._values else None))
@cached_property
def _secondary_max(self):
"""Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None)
else (max(self._secondary_values)
if self._secondary_values else None))
@cached_property
def _order(self):
"""Getter for the number of series"""
return len(self.all_series)
@cached_property
def _x_major_labels(self):
"""Getter for the x major label"""
if self.x_labels_major:
return self.x_labels_major
if self.x_labels_major_every:
return [self._x_labels[i][0] for i in range(
0, len(self._x_labels), self.x_labels_major_every)]
if self.x_labels_major_count:
label_count = len(self._x_labels)
major_count = self.x_labels_major_count
if (major_count >= label_count):
return [label[0] for label in self._x_labels]
return [self._x_labels[
int(i * (label_count - 1) / (major_count - 1))][0]
for i in range(major_count)]
return []
@cached_property
def _y_major_labels(self):
"""Getter for the y major label"""
if self.y_labels_major:
return self.y_labels_major
if self.y_labels_major_every:
return [self._y_labels[i][1] for i in range(
0, len(self._y_labels), self.y_labels_major_every)]
if self.y_labels_major_count:
label_count = len(self._y_labels)
major_count = self.y_labels_major_count
if (major_count >= label_count):
return [label[1] for label in self._y_labels]
return [self._y_labels[
int(i * (label_count - 1) / (major_count - 1))][1]
for i in range(major_count)]
return majorize(
cut(self._y_labels, 1)
)
def _draw(self):
"""Draw all the things"""
self._compute()
self._compute_secondary()
self._post_compute()
self._compute_margin()
self._decorate()
if self.series and self._has_data():
self._plot()
else:
self.svg.draw_no_data()
def _has_data(self):
"""Check if there is any data"""
return sum(
map(len, map(lambda s: s.safe_values, self.series))) != 0 and (
sum(map(abs, self._values)) != 0)

19
pygal/graph/time.py

@ -57,8 +57,8 @@ class DateTimeLine(XY):
"""Return the value formatter for this graph"""
def datetime_to_str(x):
dt = datetime.fromtimestamp(x)
if self.config.x_value_formatter:
return self.config.x_value_formatter(dt)
if self.x_value_formatter:
return self.x_value_formatter(dt)
return dt.isoformat()
return datetime_to_str
@ -70,8 +70,8 @@ class DateLine(DateTimeLine):
"""Return the value formatter for this graph"""
def date_to_str(x):
d = date.fromtimestamp(x)
if self.config.x_value_formatter:
return self.config.x_value_formatter(d)
if self.x_value_formatter:
return self.x_value_formatter(d)
return d.isoformat()
return date_to_str
@ -84,8 +84,8 @@ class TimeLine(DateTimeLine):
"""Return the value formatter for this graph"""
def date_to_str(x):
t = datetime.fromtimestamp(x).time()
if self.config.x_value_formatter:
return self.config.x_value_formatter(t)
if self.x_value_formatter:
return self.x_value_formatter(t)
return t.isoformat()
return date_to_str
@ -98,11 +98,8 @@ class TimeDeltaLine(XY):
"""Return the value formatter for this graph"""
def timedelta_to_str(x):
td = timedelta(seconds=x)
if self.config.x_value_formatter:
return self.config.x_value_formatter(td)
if self.x_value_formatter:
return self.x_value_formatter(td)
return str(td)
return timedelta_to_str
# Old pygal compat
DateY = DateTimeLine

2
pygal/serie.py

@ -30,7 +30,7 @@ class Serie(object):
self.title = title
self.values = values
self.config = config
self.__dict__.update(config.to_dict())
self.__dict__.update(config.__dict__)
self.metadata = metadata or {}
@cached_property

28
pygal/state.py

@ -0,0 +1,28 @@
# -*- 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/>.
"""
Class holding state during render
"""
class State(object):
def __init__(self, graph):
self.__dict__.update(**graph.config.__dict__)
self.__dict__.update(**graph.__dict__)

10
pygal/style.py

@ -28,6 +28,7 @@ import re
re_dasharray_delimiters = re.compile(r'[\.|,|x|\||\- ]+', re.I)
class Style(object):
"""Styling class containing colors for the css generation"""
def __init__(
@ -42,7 +43,7 @@ class Style(object):
opacity_hover='.9',
stroke_width='1',
stroke_style='round',
stroke_dasharray=(0,0),
stroke_dasharray=(0, 0),
transition='250ms',
colors=(
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe',
@ -79,11 +80,12 @@ class Style(object):
self.stroke_dasharray = '%d,%d' % self.stroke_dasharray
if isinstance(self.stroke_dasharray, str):
self.stroke_dasharray = re.sub(re_dasharray_delimiters, ',', self.stroke_dasharray)
self.stroke_dasharray = re.sub(
re_dasharray_delimiters, ',', self.stroke_dasharray)
if not isinstance(self.stroke_dasharray, str):
raise ValueError('stroke_dasharray not in proper form: tuple(int,int)')
raise ValueError(
'stroke_dasharray not in proper form: tuple(int, int)')
def get_colors(self, prefix):
"""Get the css color list"""

43
pygal/svg.py

@ -81,7 +81,7 @@ class Svg(object):
def add_styles(self):
"""Add the css to the svg"""
colors = self.graph.config.style.get_colors(self.id)
colors = self.graph.style.get_colors(self.id)
all_css = []
for css in ['base.css'] + list(self.graph.css):
if '://' in css:
@ -95,12 +95,23 @@ class Svg(object):
if not os.path.exists(css):
css = os.path.join(
os.path.dirname(__file__), 'css', css)
class FontSizes(object):
"""Container for font sizes"""
fs = FontSizes()
for name in dir(self.graph.state):
if name.endswith('_font_size'):
setattr(
fs,
name.replace('_font_size', ''),
('%dpx' % getattr(self.graph, name)))
with io.open(css, encoding='utf-8') as f:
css_text = template(
f.read(),
style=self.graph.config.style,
style=self.graph.style,
colors=colors,
font_sizes=self.graph.config.font_sizes(),
font_sizes=fs,
id=self.id)
if not self.graph.pretty_print:
css_text = minify_css(css_text)
@ -111,13 +122,23 @@ class Svg(object):
def add_scripts(self):
"""Add the js to the svg"""
common_script = self.node(self.defs, 'script', type='text/javascript')
def get_js_dict():
return dict((k, getattr(self.graph, k)) for k in dir(self)
if not k.startswith('_') and
k in dir(self.graph.config))
def json_default(o):
if isinstance(o, (datetime, date)):
return o.isoformat()
if hasattr(o, 'to_dict'):
o = o.to_dict()
print(o)
return json.JSONEncoder().default(o)
common_script.text = " = ".join(
("window.config", json.dumps(
self.graph.config.to_dict(),
default=lambda o: (
o.isoformat() if isinstance(o, (datetime, date))
else json.JSONEncoder().default(o))
)))
get_js_dict(), default=json_default)))
for js in self.graph.js:
if '://' in js:
@ -174,17 +195,17 @@ class Svg(object):
self.graph.nodes['plot'],
class_='series serie-%d color-%d' % (
serie.index, serie.index % len(
self.graph.style['colors']))),
self.graph.style.colors))),
overlay=self.node(
self.graph.nodes['overlay'],
class_='series serie-%d color-%d' % (
serie.index, serie.index % len(
self.graph.style['colors']))),
self.graph.style.colors))),
text_overlay=self.node(
self.graph.nodes['text_overlay'],
class_='series serie-%d color-%d' % (
serie.index, serie.index % len(
self.graph.style['colors']))))
self.graph.style.colors))))
def line(self, node, coords, close=False, **kwargs):
"""Draw a svg line"""

5
pygal/table.py

@ -35,14 +35,13 @@ class HTML(object):
class Table(BaseGraph):
_dual = None
def __init__(self, config, series, secondary_series, uuid, xml_filters):
def __init__(self, chart, series, secondary_series, uuid, xml_filters):
"Init the table"
self.uuid = uuid
self.series = series or []
self.secondary_series = secondary_series or []
self.xml_filters = xml_filters or []
self.__dict__.update(config.to_dict())
self.config = config
self.__dict__.update(chart.state)
def render(self, total=False, transpose=False, style=False):
html = HTML()

17
pygal/test/__init__.py

@ -19,7 +19,6 @@
import pygal
from pygal.util import cut
from datetime import datetime
from pygal.i18n import COUNTRIES
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from decimal import Decimal
@ -34,12 +33,12 @@ def get_data(i):
def adapt(chart, data):
if isinstance(chart, pygal.DateY):
# Convert to a credible datetime
return list(map(
lambda t:
(datetime.fromtimestamp(1360000000 + t[0] * 987654)
if t[0] is not None else None, t[1]), data))
# if isinstance(chart, pygal.DateY):
# # Convert to a credible datetime
# return list(map(
# lambda t:
# (datetime.fromtimestamp(1360000000 + t[0] * 987654)
# if t[0] is not None else None, t[1]), data))
if isinstance(chart, pygal.XY):
return data
@ -51,13 +50,13 @@ def adapt(chart, data):
COUNTRIES.keys())[
int(x) % len(COUNTRIES)]
if x is not None else None, data))
elif isinstance(chart, pygal.FrenchMap_Regions):
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.FrenchMap_Departments):
elif isinstance(chart, pygal.FrenchMapDepartments):
return list(
map(lambda x: list(
DEPARTMENTS.keys())[

20
pygal/test/test_config.py

@ -20,8 +20,8 @@
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,
Pyramid, HorizontalBar, HorizontalStackedBar,
FrenchMapRegions, FrenchMapDepartments,
DateTimeLine, TimeLine, DateLine, TimeDeltaLine)
from pygal._compat import u
from pygal.test.utils import texts
@ -275,9 +275,9 @@ 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):
FrenchMapRegions, FrenchMapDepartments):
return
if not chart.cls._dual:
if not chart._dual:
data = 100, 200, 150
else:
data = (1, 100), (3, 200), (2, 150)
@ -285,8 +285,8 @@ def test_include_x_axis(Chart):
q = chart.render_pyquery()
# Ghost thing
yaxis = ".axis.%s .guides text" % (
'y' if not chart._last__inst.horizontal else 'x')
if not issubclass(chart.cls, Bar().cls):
'y' if not getattr(chart, 'horizontal', False) else 'x')
if not isinstance(chart, Bar):
assert '0.0' not in q(yaxis).map(texts)
else:
assert '0.0' in q(yaxis).map(texts)
@ -362,8 +362,8 @@ def test_x_label_major(Chart):
if Chart in (
Pie, Treemap, Funnel, Dot, Gauge, Worldmap,
SupranationalWorldmap, Histogram, Box,
FrenchMap_Regions, FrenchMap_Departments,
Pyramid, DateY, DateTimeLine, TimeLine, DateLine,
FrenchMapRegions, FrenchMapDepartments,
Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine):
return
chart = Chart()
@ -407,10 +407,10 @@ def test_y_label_major(Chart):
if Chart in (
Pie, Treemap, Funnel, Dot, Gauge, Worldmap,
SupranationalWorldmap, Histogram, Box,
FrenchMap_Regions, FrenchMap_Departments,
FrenchMapRegions, FrenchMapDepartments,
HorizontalBar, HorizontalStackedBar,
Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine, DateY):
TimeDeltaLine):
return
chart = Chart()
data = range(12)

6
pygal/test/test_frenchmap.py

@ -18,7 +18,7 @@
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import (
FrenchMap_Regions, FrenchMap_Departments)
FrenchMapRegions, FrenchMapDepartments)
from pygal.graph.frenchmap import REGIONS, DEPARTMENTS, aggregate_regions
@ -27,14 +27,14 @@ def test_frenchmaps():
for dept in DEPARTMENTS.keys():
datas[dept] = int(''.join([x for x in dept if x.isdigit()])) * 10
fmap = FrenchMap_Departments()
fmap = FrenchMapDepartments()
fmap.add('departements', datas)
q = fmap.render_pyquery()
assert len(
q('#departements .departement,#dom-com .departement')
) == len(DEPARTMENTS)
fmap = FrenchMap_Regions()
fmap = FrenchMapRegions()
fmap.add('regions', aggregate_regions(datas))
q = fmap.render_pyquery()
assert len(q('#regions .region,#dom-com .region')) == len(REGIONS)

12
pygal/test/test_graph.py

@ -82,9 +82,9 @@ def test_metadata(Chart):
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.FrenchMap_Regions:
elif Chart == pygal.FrenchMapRegions:
v = [(i, k) for k, i in enumerate(REGIONS.keys())]
elif Chart == pygal.FrenchMap_Departments:
elif Chart == pygal.FrenchMapDepartments:
v = [(i, k) for k, i in enumerate(DEPARTMENTS.keys())]
chart.add('Serie with metadata', [
@ -110,7 +110,7 @@ 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.FrenchMapRegions, pygal.FrenchMapDepartments):
# Tooltip are not working on maps
assert len(v) == len(q('.tooltip-trigger').siblings('.value'))
@ -192,8 +192,8 @@ def test_values_by_dict(Chart):
if not issubclass(Chart, (
pygal.Worldmap,
pygal.FrenchMap_Departments,
pygal.FrenchMap_Regions)):
pygal.FrenchMapDepartments,
pygal.FrenchMapRegions)):
chart1.add('A', {'red': 10, 'green': 12, 'blue': 14})
chart1.add('B', {'green': 11, 'red': 7})
chart1.add('C', {'blue': 7})
@ -376,7 +376,7 @@ def test_labels_with_links(Chart):
q = chart.render_pyquery()
links = q('a')
if issubclass(chart.cls,
if isinstance(chart,
(pygal.graph.worldmap.Worldmap,
pygal.graph.frenchmap.FrenchMapDepartments)):
# No country is found in this case so:

129
pygal/util.py

@ -26,9 +26,6 @@ import re
from decimal import Decimal
from math import floor, pi, log, log10, ceil
from itertools import cycle
from functools import reduce
from pygal.adapters import (
not_zero, positive, decimal_to_float)
ORDERS = u("yzafpnµm kMGTPEZY")
@ -272,21 +269,21 @@ def truncate(string, index):
string = string[:index - 1] + u('')
return string
# Stolen from brownie http://packages.python.org/Brownie/
class cached_property(object):
"""Optimize a static property"""
def __init__(self, getter, doc=None):
self.getter = getter
self.__module__ = getter.__module__
self.__name__ = getter.__name__
self.__doc__ = doc or getter.__doc__
def __get__(self, obj, type_=None):
if obj is None:
return self
value = obj.__dict__[self.__name__] = self.getter(obj)
return value
cached_property = property
# # Stolen from brownie http://packages.python.org/Brownie/
# class cached_property(object):
# """Optimize a static property"""
# def __init__(self, getter, doc=None):
# self.getter = getter
# self.__module__ = getter.__module__
# self.__name__ = getter.__name__
# self.__doc__ = doc or getter.__doc__
# def __get__(self, obj, type_=None):
# if obj is None:
# return self
# value = obj.__dict__[self.__name__] = self.getter(obj)
# return value
css_comments = re.compile(r'/\*.*?\*/', re.MULTILINE | re.DOTALL)
@ -331,102 +328,6 @@ def safe_enumerate(iterable):
yield i, v
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
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)):
config.zero = 1
for key in ('x_labels', 'y_labels'):
if getattr(config, key):
setattr(config, key, list(getattr(config, key)))
if not raw:
return
adapters = list(cls._adapters) or [lambda x:x]
if config.logarithmic:
for fun in not_zero, positive:
if fun in adapters:
adapters.remove(fun)
adapters = adapters + [positive, not_zero]
adapters = adapters + [decimal_to_float]
adapter = reduce(compose, adapters) if not config.strict else ident
x_adapter = reduce(
compose, cls._x_adapters) if getattr(
cls, '_x_adapters', None) else None
series = []
raw = [(
title,
list(raw_values) if not isinstance(raw_values, dict) else raw_values,
serie_config_kwargs
) for title, raw_values, serie_config_kwargs in raw]
width = max([len(values) for _, values, _ in raw] +
[len(config.x_labels or [])])
for title, raw_values, serie_config_kwargs in raw:
metadata = {}
values = []
if isinstance(raw_values, dict):
if issubclass(cls, (Worldmap, FrenchMapDepartments)):
raw_values = list(raw_values.items())
else:
value_list = [None] * width
for k, v in raw_values.items():
if k in config.x_labels:
value_list[config.x_labels.index(k)] = v
raw_values = value_list
for index, raw_value in enumerate(
raw_values + (
(width - len(raw_values)) * [None] # aligning values
if len(raw_values) < width else [])):
if isinstance(raw_value, dict):
raw_value = dict(raw_value)
value = raw_value.pop('value', None)
metadata[index] = raw_value
else:
value = raw_value
# Fix this by doing this in charts class methods
if issubclass(cls, Histogram):
if value is None:
value = (None, None, None)
elif not is_list_like(value):
value = (value, config.zero, config.zero)
value = list(map(adapter, value))
elif cls._dual:
if value is None:
value = (None, None)
elif not is_list_like(value):
value = (value, config.zero)
if x_adapter:
value = (x_adapter(value[0]), adapter(value[1]))
if issubclass(
cls, (Worldmap, FrenchMapDepartments)):
value = (adapter(value[0]), value[1])
else:
value = list(map(adapter, value))
else:
value = adapter(value)
values.append(value)
serie_config = SerieConfig()
serie_config(**config.to_dict())
serie_config(**serie_config_kwargs)
series.append(
Serie(offset + len(series), title, values, serie_config, metadata))
return series
def split_title(title, width, title_fs):
titles = []
if not title:

Loading…
Cancel
Save