Browse Source

Rework formatters. Add a formatter option for chart / serie / value.

pull/307/head
Florian Mounier 9 years ago
parent
commit
7f77027392
  1. 27
      demo/moulinrouge/__init__.py
  2. 49
      demo/moulinrouge/tests.py
  3. 3
      docs/changelog.rst
  4. 14
      docs/documentation/configuration/serie.rst
  5. 13
      docs/documentation/configuration/value.rst
  6. 18
      pygal/config.py
  7. 109
      pygal/formatters.py
  8. 2
      pygal/graph/bar.py
  9. 48
      pygal/graph/box.py
  10. 6
      pygal/graph/dot.py
  11. 8
      pygal/graph/dual.py
  12. 13
      pygal/graph/funnel.py
  13. 14
      pygal/graph/gauge.py
  14. 77
      pygal/graph/graph.py
  15. 2
      pygal/graph/histogram.py
  16. 2
      pygal/graph/line.py
  17. 12
      pygal/graph/map.py
  18. 15
      pygal/graph/pie.py
  19. 7
      pygal/graph/pyramid.py
  20. 12
      pygal/graph/radar.py
  21. 16
      pygal/graph/stackedline.py
  22. 16
      pygal/graph/time.py
  23. 7
      pygal/graph/treemap.py
  24. 5
      pygal/graph/xy.py
  25. 7
      pygal/table.py
  26. 29
      pygal/test/test_config.py
  27. 88
      pygal/test/test_formatters.py
  28. 6
      pygal/test/test_graph.py
  29. 4
      pygal/test/test_line.py
  30. 16
      pygal/test/test_stacked.py
  31. 32
      pygal/test/test_util.py
  32. 29
      pygal/util.py

27
demo/moulinrouge/__init__.py

@ -70,13 +70,13 @@ def create_app():
random_value((-max, min)[random.randrange(0, 2)], max), random_value((-max, min)[random.randrange(0, 2)], max),
random_value((-max, min)[random.randrange(0, 2)], max) random_value((-max, min)[random.randrange(0, 2)], max)
) for i in range(data)] ) for i in range(data)]
series.append((random_label(), values)) series.append((random_label(), values, {}))
return series return series
def _random_series(type, data, order): def _random_series(type, data, order):
max = 10 ** order max = 10 ** order
min = 10 ** random.randrange(0, order) min = 10 ** random.randrange(0, order)
with_2nd = bool(random.randint(0, 1)) with_secondary = bool(random.randint(0, 1))
series = [] series = []
for i in range(random.randrange(1, 10)): for i in range(random.randrange(1, 10)):
if type == 'Pie': if type == 'Pie':
@ -89,10 +89,10 @@ def create_app():
else: else:
values = [random_value((-max, min)[random.randrange(1, 2)], values = [random_value((-max, min)[random.randrange(1, 2)],
max) for i in range(data)] max) for i in range(data)]
is_2nd = False config = {
if with_2nd: 'secondary': with_secondary and bool(random.randint(0, 1))
is_2nd = bool(random.randint(0, 1)) }
series.append((random_label(), values, is_2nd)) series.append((random_label(), values, config))
return series return series
from .tests import get_test_routes from .tests import get_test_routes
@ -110,15 +110,17 @@ def create_app():
def svg(type, series, config): def svg(type, series, config):
graph = get(type)( graph = get(type)(
pickle.loads(b64decode(str(config)))) pickle.loads(b64decode(str(config))))
for title, values, is_2nd in pickle.loads(b64decode(str(series))): for title, values, serie_config in pickle.loads(
graph.add(title, values, secondary=is_2nd) b64decode(str(series))):
graph.add(title, values, **serie_config)
return graph.render_response() return graph.render_response()
@app.route("/table/<type>/<series>/<config>") @app.route("/table/<type>/<series>/<config>")
def table(type, series, config): def table(type, series, config):
graph = get(type)(pickle.loads(b64decode(str(config)))) graph = get(type)(pickle.loads(b64decode(str(config))))
for title, values, is_2nd in pickle.loads(b64decode(str(series))): for title, values, serie_config in pickle.loads(
graph.add(title, values, secondary=is_2nd) b64decode(str(series))):
graph.add(title, values, **serie_config)
return graph.render_table() return graph.render_table()
@app.route("/sparkline/<style>") @app.route("/sparkline/<style>")
@ -196,16 +198,15 @@ def create_app():
xy_series = _random(data, order) xy_series = _random(data, order)
other_series = [] other_series = []
for title, values in xy_series: for title, values, config in xy_series:
other_series.append( other_series.append(
(title, cut(values, 1))) (title, cut(values, 1), config))
xy_series = b64encode(pickle.dumps(xy_series)) xy_series = b64encode(pickle.dumps(xy_series))
other_series = b64encode(pickle.dumps(other_series)) other_series = b64encode(pickle.dumps(other_series))
config = Config() config = Config()
config.width = width config.width = width
config.height = height config.height = height
config.fill = bool(random.randrange(0, 2)) config.fill = bool(random.randrange(0, 2))
config.human_readable = True
config.interpolate = interpolate config.interpolate = interpolate
config.style = style config.style = style
svgs = [] svgs = []

49
demo/moulinrouge/tests.py

@ -24,7 +24,7 @@ except ImportError:
from flask import abort from flask import abort
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 import stats from pygal import stats, formatters
from pygal.graph.horizontal import HorizontalGraph from pygal.graph.horizontal import HorizontalGraph
from random import randint, choice from random import randint, choice
from datetime import datetime, date from datetime import datetime, date
@ -38,7 +38,7 @@ def get_test_routes(app):
@app.route('/test/unsorted') @app.route('/test/unsorted')
def test_unsorted(): def test_unsorted():
bar = Bar(style=styles['neon'], human_readable=True) bar = Bar(style=styles['neon'], value_formatter=formatters.human_readable)
bar.add('A', {'red': 10, 'green': 12, 'blue': 14}) bar.add('A', {'red': 10, 'green': 12, 'blue': 14})
bar.add('B', {'green': 11, 'blue': 7}) bar.add('B', {'green': 11, 'blue': 7})
bar.add('C', {'blue': 7}) bar.add('C', {'blue': 7})
@ -352,12 +352,24 @@ def get_test_routes(app):
bar.x_labels_major = [4] bar.x_labels_major = [4]
return bar.render_response() return bar.render_response()
@app.route('/test/formatters/<chart>')
def test_formatters_for(chart):
chart = CHARTS_BY_NAME[chart](
print_values=True, formatter=lambda x, chart, serie: '%s%s$' % (
x, serie.title))
chart.add('_a', [1, 2, {'value': 3, 'formatter': lambda x: '%s¥' % x}])
chart.add('_b', [4, 5, 6], formatter=lambda x: '%s' % x)
chart.x_labels = [2, 4, 6]
chart.x_labels_major = [4]
return chart.render_response()
@app.route('/test/bar/position') @app.route('/test/bar/position')
def test_bar_print_values_position(): def test_bar_print_values_position():
bar = StackedBar(print_values=True, print_values_position='top', zero=2, bar = StackedBar(
style=styles['default']( print_values=True, print_values_position='top', zero=2,
value_font_family='googlefont:Raleway', style=styles['default'](
value_font_size=46)) value_font_family='googlefont:Raleway',
value_font_size=46))
bar.add('1', [1, -2, 3]) bar.add('1', [1, -2, 3])
bar.add('2', [4, -5, 6]) bar.add('2', [4, -5, 6])
bar.x_labels = [2, 4, 6] bar.x_labels = [2, 4, 6]
@ -366,7 +378,10 @@ def get_test_routes(app):
@app.route('/test/histogram') @app.route('/test/histogram')
def test_histogram(): def test_histogram():
hist = Histogram(print_values=True, print_values_position='top', style=styles['neon']) hist = Histogram(
print_values=True,
print_values_position='top',
style=styles['neon'])
hist.add('1', [ hist.add('1', [
(2, 0, 1), (2, 0, 1),
(4, 1, 3), (4, 1, 3),
@ -422,7 +437,7 @@ def get_test_routes(app):
@app.route('/test/box') @app.route('/test/box')
def test_box(): def test_box():
chart = Box() chart = Box()
chart.js = ('http://l:2343/2.0.x/pygal-tooltips.js',) # chart.js = ('http://l:2343/2.0.x/pygal-tooltips.js',)
chart.box_mode = '1.5IQR' chart.box_mode = '1.5IQR'
chart.add('One', [15, 8, 2, -12, 9, 23]) chart.add('One', [15, 8, 2, -12, 9, 23])
chart.add('Two', [5, 8, 2, -9, 23, 12]) chart.add('Two', [5, 8, 2, -9, 23, 12])
@ -576,21 +591,9 @@ def get_test_routes(app):
if fr is None: if fr is None:
abort(404) abort(404)
fmap = fr.Departments(style=choice(list(styles.values()))) fmap = fr.Departments(style=choice(list(styles.values())))
fmap.add('', [(69, 2), (42, 7), (38, 3), (26, 0)]) fmap.add('', [(i, i) for i in range(1, 100)])
# for i in range(10): fmap.add('', [(970 + i, i) for i in range(1, 7)])
# fmap.add('s%d' % i, [ fmap.add('', [('2A', 1), ('2B', 2)])
# (choice(list(fr.DEPARTMENTS.keys())), randint(0, 100))
# for _ in range(randint(1, 5))])
# fmap.add('links', [{
# 'value': (69, 10),
# 'label': '\o/',
# 'xlink': 'http://google.com?q=69'
# }, {
# 'value': ('42', 20),
# 'label': 'Y',
# }])
# fmap.add('6th', [3, 5, 34, 12])
fmap.title = 'French map' fmap.title = 'French map'
return fmap.render_response() return fmap.render_response()

3
docs/changelog.rst

@ -8,7 +8,8 @@ Changelog
* Support interruptions in line charts (thanks @piotrmaslanka #300) * Support interruptions in line charts (thanks @piotrmaslanka #300)
* Fix confidence interval reactiveness (thanks @chartique #296) * Fix confidence interval reactiveness (thanks @chartique #296)
* Add horizontal line charts (thanks @chartique #301) * Add horizontal line charts (thanks @chartique #301)
* There is now a `formatter` config option to format values as specified. The formatter callable may or may not take `chart`, `serie` and `index` as argument. The default value formatting is now chart dependent and is value_formatter for most graph but could be a combination of value_formatter and x_value_formatter for dual charts.
* The `human_readable` option has been removed. Now you have to use the pygal.formatters.human_readable formatter (value_formatter=human_readable instead of human_readable=True)
2.1.1 2.1.1
===== =====

14
docs/documentation/configuration/serie.rst

@ -138,3 +138,17 @@ You can set `allow_interruptions` to True in order to break lines on None values
allow_interruptions=True) allow_interruptions=True)
interrupted_chart.add( interrupted_chart.add(
'Temperature', [11, 17, 21.5, 6, None, 6, 27.5, None, 28]) 'Temperature', [11, 17, 21.5, 6, None, 6, 27.5, None, 28])
formatter
~~~~~~~~~
You can add a `formatter` function for this serie values.
It will be used for value printing and tooltip. (Not for axis.)
.. pygal-code::
chart = pygal.Bar(print_values=True, value_formatter=lambda x: '{}$'.format(x))
chart.add('bar', [.0002, .0005, .00035], formatter=lambda x: '<%s>' % x)
chart.add('bar', [.0004, .0009, .001])

13
docs/documentation/configuration/value.rst

@ -55,6 +55,19 @@ The color key set the fill and the stroke style. You can also set the css style
]) ])
Value formatting
~~~~~~~~~~~~~~~~
You can add a `formatter` metadata for a specific value.
.. pygal-code::
chart = pygal.Bar(print_values=True, value_formatter=lambda x: '{}$'.format(x))
chart.add('bar', [.0002, .0005, .00035], formatter=lambda x: '<%s>' % x)
chart.add('bar', [.0004, {'value': .0009, 'formatter': lambda x: '«%s»' % x}, .001])
Node attributes Node attributes
--------------- ---------------

18
pygal/config.py

@ -22,9 +22,11 @@ from copy import deepcopy
from pygal.interpolate import INTERPOLATIONS from pygal.interpolate import INTERPOLATIONS
from pygal.style import DefaultStyle, Style from pygal.style import DefaultStyle, Style
from pygal import formatters
CONFIG_ITEMS = [] CONFIG_ITEMS = []
callable = type(lambda: 1)
class Key(object): class Key(object):
@ -221,6 +223,12 @@ class CommonConfig(BaseConfig):
allow_interruptions = Key( allow_interruptions = Key(
False, bool, "Look", "Break lines on None values") False, bool, "Look", "Break lines on None values")
formatter = Key(
None, callable, "Value",
"A function to convert raw value to strings for this chart or serie",
"Default to value_formatter in most charts, it depends on dual charts."
"(Can be overriden by value with the formatter metadata.)")
class Config(CommonConfig): class Config(CommonConfig):
@ -383,18 +391,14 @@ class Config(CommonConfig):
"'x' (default), 'y' or 'either'") "'x' (default), 'y' or 'either'")
# Value # # Value #
human_readable = Key(
False, bool, "Value", "Display values in human readable format",
"(ie: 12.4M)")
x_value_formatter = Key( x_value_formatter = Key(
None, type(lambda: 1), "Value", formatters.default, callable, "Value",
"A function to convert abscissa numeric value to strings " "A function to convert abscissa numeric value to strings "
"(used in XY and Date charts)") "(used in XY and Date charts)")
value_formatter = Key( value_formatter = Key(
None, type(lambda: 1), "Value", formatters.default, callable, "Value",
"A function to convert numeric value to strings") "A function to convert ordinate numeric value to strings")
logarithmic = Key( logarithmic = Key(
False, bool, "Value", "Display values in logarithmic scale") False, bool, "Value", "Display values in logarithmic scale")

109
pygal/formatters.py

@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2015 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/>.
"""
Formatters to use with `value_formatter` and `x_value_formatter` configs
"""
from datetime import datetime, date, time
from math import floor, log
from pygal._compat import u, to_str
from pygal.util import float_format
class Formatter(object):
pass
class HumanReadable(Formatter):
"""Format a number to engineer scale"""
ORDERS = u("yzafpnµm kMGTPEZY")
def __init__(self, none_char=u('')):
self.none_char = none_char
def __call__(self, val):
if val is None:
return self.none_char
order = val and int(floor(log(abs(val)) / log(1000)))
orders = self.ORDERS.split(" ")[int(order > 0)]
if order == 0 or order > len(orders):
return float_format(val / (1000 ** int(order)))
return (
float_format(val / (1000 ** int(order))) +
orders[int(order) - int(order > 0)])
class Significant(Formatter):
"""Show precision significant digit of float"""
def __init__(self, precision=10):
self.format = '%%.%dg' % precision
def __call__(self, val):
if val is None:
return ''
return self.format % val
class Integer(Formatter):
"""Cast number to integer"""
def __call__(self, val):
if val is None:
return ''
return '%d' % val
class Raw(Formatter):
"""Cast everything to string"""
def __call__(self, val):
if val is None:
return ''
return to_str(val)
class IsoDateTime(Formatter):
"""Iso format datetimes"""
def __call__(self, val):
if val is None:
return ''
return val.isoformat()
class Default(Significant, IsoDateTime, Raw):
"""Try to guess best format from type"""
def __call__(self, val):
if val is None:
return ''
if isinstance(val, (int, float)):
return Significant.__call__(self, val)
if isinstance(val, (date, time, datetime)):
return IsoDateTime.__call__(self, val)
return Raw.__call__(self, val)
# Formatters with default options
human_readable = HumanReadable()
significant = Significant()
integer = Integer()
raw = Raw()
# Default config formatter
default = Default()

2
pygal/graph/bar.py

@ -109,7 +109,7 @@ class Bar(Graph):
if None in (x, y) or (self.logarithmic and y <= 0): if None in (x, y) or (self.logarithmic and y <= 0):
continue continue
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
val = self._format(serie.values[i]) val = self._format(serie, i)
bar = decorate( bar = decorate(
self.svg, self.svg,

48
pygal/graph/box.py

@ -26,7 +26,6 @@ from __future__ import division
from bisect import bisect_left, bisect_right from bisect import bisect_left, bisect_right
from pygal._compat import is_list_like
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from pygal.util import alter, decorate from pygal.util import alter, decorate
@ -44,27 +43,25 @@ class Box(Graph):
_series_margin = .06 _series_margin = .06
@property def _value_format(self, value, serie):
def _format(self): """
"""Return the value formatter for this graph""" Format value for dual value display.
sup = super(Box, self)._format """
if self.box_mode == "extremes":
def format_maybe_quartile(x): return (
if is_list_like(x): 'Min: %s\nQ1 : %s\nQ2 : %s\nQ3 : %s\nMax: %s' %
if self.box_mode == "extremes": tuple(map(self._y_format, serie.points[1:6])))
return ( elif self.box_mode in ["tukey", "stdev", "pstdev"]:
'Min: %s\nQ1 : %s\nQ2 : %s\nQ3 : %s\nMax: %s' % return (
tuple(map(sup, x[1:6]))) 'Min: %s\nLower Whisker: %s\nQ1: %s\nQ2: %s\nQ3: %s\n'
elif self.box_mode in ["tukey", "stdev", "pstdev"]: 'Upper Whisker: %s\nMax: %s' % tuple(map(
return ( self._y_format, serie.points)))
'Min: %s\nLower Whisker: %s\nQ1: %s\nQ2: %s\nQ3: %s\n' elif self.box_mode == '1.5IQR':
'Upper Whisker: %s\nMax: %s' % tuple(map(sup, x))) # 1.5IQR mode
elif self.box_mode == '1.5IQR': return 'Q1: %s\nQ2: %s\nQ3: %s' % tuple(map(
# 1.5IQR mode self._y_format, serie.points[2:5]))
return 'Q1: %s\nQ2: %s\nQ3: %s' % tuple(map(sup, x[2:5])) else:
else: return self._y_format(serie.points)
return sup(x)
return format_maybe_quartile
def _compute(self): def _compute(self):
""" """
@ -72,7 +69,7 @@ class Box(Graph):
within the rendering process within the rendering process
""" """
for serie in self.series: for serie in self.series:
serie.values, serie.outliers = \ serie.points, serie.outliers = \
self._box_points(serie.values, self.box_mode) self._box_points(serie.values, self.box_mode)
self._x_pos = [ self._x_pos = [
@ -107,10 +104,11 @@ class Box(Graph):
self.svg, self.svg,
self.svg.node(boxes, class_='box'), self.svg.node(boxes, class_='box'),
metadata) metadata)
val = self._format(serie.values)
val = self._format(serie, 0)
x_center, y_center = self._draw_box( x_center, y_center = self._draw_box(
box, serie.values[1:6], serie.outliers, serie.index, metadata) box, serie.points[1:6], serie.outliers, serie.index, metadata)
self._tooltip_data(box, val, x_center, y_center, "centered", self._tooltip_data(box, val, x_center, y_center, "centered",
self._get_x_label(serie.index)) self._get_x_label(serie.index))
self._static_value(serie_node, val, x_center, y_center, metadata) self._static_value(serie_node, val, x_center, y_center, metadata)

6
pygal/graph/dot.py

@ -68,11 +68,11 @@ class Dot(Graph):
class_='dot reactive tooltip-trigger' + ( class_='dot reactive tooltip-trigger' + (
' negative' if value < 0 else '')), metadata) ' negative' if value < 0 else '')), metadata)
value = self._format(value) val = self._format(serie, i)
self._tooltip_data( self._tooltip_data(
dots, value, x, y, 'centered', dots, val, x, y, 'centered',
self._get_x_label(i)) self._get_x_label(i))
self._static_value(serie_node, value, x, y, metadata) self._static_value(serie_node, val, x, y, metadata)
def _compute(self): def _compute(self):
"""Compute y min and max and y scale and set labels""" """Compute y min and max and y scale and set labels"""

8
pygal/graph/dual.py

@ -27,6 +27,14 @@ from pygal.util import compute_scale, cut
class Dual(Graph): class Dual(Graph):
_dual = True _dual = True
def _value_format(self, value):
"""
Format value for dual value display.
"""
return '%s: %s' % (
self._x_format(value[0]),
self._y_format(value[1]))
def _compute_x_labels(self): def _compute_x_labels(self):
x_pos = compute_scale( x_pos = compute_scale(
self._box.xmin, self._box.xmax, self.logarithmic, self._box.xmin, self._box.xmax, self.logarithmic,

13
pygal/graph/funnel.py

@ -31,10 +31,9 @@ class Funnel(Graph):
_adapters = [positive, none_to_zero] _adapters = [positive, none_to_zero]
def _format(self, value): def _value_format(self, value):
"""Return the value formatter for this graph here its absolute value""" """Format value for dual value display."""
value = value and abs(value) return super(Funnel, self)._value_format(value and abs(value))
return super(Funnel, self)._format(value)
def funnel(self, serie): def funnel(self, serie):
"""Draw a funnel slice""" """Draw a funnel slice"""
@ -42,7 +41,7 @@ class Funnel(Graph):
fmt = lambda x: '%f %f' % x fmt = lambda x: '%f %f' % x
for i, poly in enumerate(serie.points): for i, poly in enumerate(serie.points):
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
value = self._format(serie.values[i]) val = self._format(serie, i)
funnels = decorate( funnels = decorate(
self.svg, self.svg,
@ -59,9 +58,9 @@ class Funnel(Graph):
self._center(self._x_pos[serie.index]), self._center(self._x_pos[serie.index]),
sum([point[1] for point in poly]) / len(poly))) sum([point[1] for point in poly]) / len(poly)))
self._tooltip_data( self._tooltip_data(
funnels, value, x, y, 'centered', funnels, val, x, y, 'centered',
self._get_x_label(serie.index)) self._get_x_label(serie.index))
self._static_value(serie_node, value, x, y, metadata) self._static_value(serie_node, val, x, y, metadata)
def _center(self, x): def _center(self, x):
return x - 1 / (2 * self._order) return x - 1 / (2 * self._order)

14
pygal/graph/gauge.py

@ -55,7 +55,7 @@ class Gauge(Graph):
def point(x, y): def point(x, y):
return '%f %f' % self.view((x, y)) return '%f %f' % self.view((x, y))
value = self._format(serie.values[i]) val = self._format(serie, i)
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
gauges = decorate( gauges = decorate(
self.svg, self.svg,
@ -88,9 +88,9 @@ class Gauge(Graph):
x, y = self.view((.75, theta)) x, y = self.view((.75, theta))
self._tooltip_data( self._tooltip_data(
gauges, value, x, y, gauges, val, x, y,
xlabel=self._get_x_label(i)) xlabel=self._get_x_label(i))
self._static_value(serie_node, value, x, y, metadata) self._static_value(serie_node, val, x, y, metadata)
def _y_axis(self, draw_axes=True): def _y_axis(self, draw_axes=True):
"""Override y axis to plot a polar axis""" """Override y axis to plot a polar axis"""
@ -120,7 +120,7 @@ class Gauge(Graph):
self.svg.node( self.svg.node(
guides, 'title', guides, 'title',
).text = self._format(theta) ).text = self._y_format(theta)
def _x_axis(self, draw_axes=True): def _x_axis(self, draw_axes=True):
"""Override x axis to put a center circle in center""" """Override x axis to put a center circle in center"""
@ -154,13 +154,13 @@ class Gauge(Graph):
for i, y_label in enumerate(self.y_labels): for i, y_label in enumerate(self.y_labels):
if isinstance(y_label, dict): if isinstance(y_label, dict):
pos = self._adapt(y_label.get('value')) pos = self._adapt(y_label.get('value'))
title = y_label.get('label', self._format(pos)) title = y_label.get('label', self._y_format(pos))
elif is_str(y_label): elif is_str(y_label):
pos = self._adapt(y_pos[i]) pos = self._adapt(y_pos[i])
title = y_label title = y_label
else: else:
pos = self._adapt(y_label) pos = self._adapt(y_label)
title = self._format(pos) title = self._y_format(pos)
self._y_labels.append((title, pos)) self._y_labels.append((title, pos))
self.min_ = min(self.min_, min(cut(self._y_labels, 1))) self.min_ = min(self.min_, min(cut(self._y_labels, 1)))
self.max_ = max(self.max_, max(cut(self._y_labels, 1))) self.max_ = max(self.max_, max(cut(self._y_labels, 1)))
@ -169,7 +169,7 @@ class Gauge(Graph):
self.min_, self.min_,
self.max_) self.max_)
else: else:
self._y_labels = list(zip(map(self._format, y_pos), y_pos)) self._y_labels = list(zip(map(self._y_format, y_pos), y_pos))
def _plot(self): def _plot(self):
"""Plot all needles""" """Plot all needles"""

77
pygal/graph/graph.py

@ -30,8 +30,8 @@ from pygal.interpolate import INTERPOLATIONS
from pygal import stats from pygal import stats
from pygal.util import ( from pygal.util import (
cached_property, compute_scale, cut, decorate, cached_property, compute_scale, cut, decorate,
get_text_box, get_texts_box, humanize, majorize, rad, reverse_text_len, get_text_box, get_texts_box, majorize, rad, reverse_text_len,
split_title, truncate) split_title, truncate, filter_kwargs)
from pygal.view import LogView, ReverseView, View, XYLogView from pygal.view import LogView, ReverseView, View, XYLogView
@ -285,7 +285,7 @@ class Graph(PublicApi):
' ') or []) + ['backwards']) ' ') or []) + ['backwards'])
self.svg.node( self.svg.node(
guides, 'title', guides, 'title',
).text = self._format(position) ).text = self._y_format(position)
if self._y_2nd_labels: if self._y_2nd_labels:
secondary_ax = self.svg.node( secondary_ax = self.svg.node(
@ -521,10 +521,6 @@ class Graph(PublicApi):
y=y + self.style.value_font_size / 3 y=y + self.style.value_font_size / 3
).text = value if self.print_zeroes or value != '0' else '' ).text = value if self.print_zeroes or value != '0' else ''
def _get_value(self, values, i):
"""Get the value formatted for tooltip"""
return self._format(values[i][1])
def _points(self, x_pos): def _points(self, x_pos):
""" """
Convert given data values into drawable points (x, y) Convert given data values into drawable points (x, y)
@ -554,7 +550,7 @@ class Graph(PublicApi):
left_range = abs(y_pos[-1] - y_pos[0]) left_range = abs(y_pos[-1] - y_pos[0])
right_range = abs(ymax - ymin) or 1 right_range = abs(ymax - ymin) or 1
scale = right_range / ((steps - 1) or 1) scale = right_range / ((steps - 1) or 1)
self._y_2nd_labels = [(self._format(ymin + i * scale), pos) self._y_2nd_labels = [(self._y_format(ymin + i * scale), pos)
for i, pos in enumerate(y_pos)] for i, pos in enumerate(y_pos)]
self._scale = left_range / right_range self._scale = left_range / right_range
@ -578,15 +574,60 @@ class Graph(PublicApi):
@property @property
def _x_format(self): def _x_format(self):
"""Return the value formatter for this graph""" """Return the abscissa value formatter (always unary)"""
return self.x_value_formatter or ( return self.x_value_formatter
humanize if self.human_readable else to_str)
@property
def _default_formatter(self):
return to_str
@property @property
def _format(self): def _y_format(self):
"""Return the value formatter for this graph""" """Return the ordinate value formatter (always unary)"""
return self.value_formatter or ( return self.value_formatter
humanize if self.human_readable else to_str)
def _value_format(self, value):
"""
Format value for value display.
(Varies in type between chart types)
"""
return self._y_format(value)
def _format(self, serie, i):
"""Format the nth value for the serie"""
value = serie.values[i]
metadata = serie.metadata.get(i)
kwargs = {
'chart': self,
'serie': serie,
'index': i
}
formatter = (
(metadata and metadata.get('formatter')) or
serie.formatter or
self.formatter or
self._value_format
)
kwargs = filter_kwargs(formatter, kwargs)
return formatter(value, **kwargs)
def _serie_format(self, serie, value):
"""Format an independent value for the serie"""
kwargs = {
'chart': self,
'serie': serie,
'index': None
}
formatter = (
serie.formatter or
self.formatter or
self._value_format
)
kwargs = filter_kwargs(formatter, kwargs)
return formatter(value, **kwargs)
def _compute(self): def _compute(self):
"""Initial computations to draw the graph""" """Initial computations to draw the graph"""
@ -815,18 +856,18 @@ class Graph(PublicApi):
for i, y_label in enumerate(self.y_labels): for i, y_label in enumerate(self.y_labels):
if isinstance(y_label, dict): if isinstance(y_label, dict):
pos = self._adapt(y_label.get('value')) pos = self._adapt(y_label.get('value'))
title = y_label.get('label', self._format(pos)) title = y_label.get('label', self._y_format(pos))
elif is_str(y_label): elif is_str(y_label):
pos = self._adapt(y_pos[i % len(y_pos)]) pos = self._adapt(y_pos[i % len(y_pos)])
title = y_label title = y_label
else: else:
pos = self._adapt(y_label) pos = self._adapt(y_label)
title = self._format(pos) title = self._y_format(pos)
self._y_labels.append((title, pos)) self._y_labels.append((title, pos))
self._box.ymin = min(self._box.ymin, min(cut(self._y_labels, 1))) self._box.ymin = min(self._box.ymin, min(cut(self._y_labels, 1)))
self._box.ymax = max(self._box.ymax, max(cut(self._y_labels, 1))) self._box.ymax = max(self._box.ymax, max(cut(self._y_labels, 1)))
else: else:
self._y_labels = list(zip(map(self._format, y_pos), y_pos)) self._y_labels = list(zip(map(self._y_format, y_pos), y_pos))
def _compute_y_labels_major(self): def _compute_y_labels_major(self):
if self.y_labels_major_every: if self.y_labels_major_every:

2
pygal/graph/histogram.py

@ -95,7 +95,7 @@ class Histogram(Dual, Bar):
self.svg, self.svg,
self.svg.node(bars, class_='histbar'), self.svg.node(bars, class_='histbar'),
metadata) metadata)
val = self._format(serie.values[i][0]) val = self._format(serie, i)
bounds = self._bar( bounds = self._bar(
serie, bar, x0, x1, y, i, self.zero, secondary=rescale) serie, bar, x0, x1, y, i, self.zero, secondary=rescale)

2
pygal/graph/line.py

@ -120,7 +120,7 @@ class Line(Graph):
self.svg.node(serie_node['overlay'], class_="dots"), self.svg.node(serie_node['overlay'], class_="dots"),
metadata) metadata)
val = self._get_value(serie.points, i) val = self._format(serie, i)
alter(self.svg.transposable_node( alter(self.svg.transposable_node(
dots, 'circle', cx=x, cy=y, r=serie.dots_size, dots, 'circle', cx=x, cy=y, r=serie.dots_size,
class_='dot reactive tooltip-trigger'), metadata) class_='dot reactive tooltip-trigger'), metadata)

12
pygal/graph/map.py

@ -52,9 +52,13 @@ class BaseMap(Graph):
"""Hook to change the area code""" """Hook to change the area code"""
return area_code return area_code
def _get_value(self, value): def _value_format(self, value):
"""Get the value formatted for tooltip""" """
return '%s: %s' % (self.area_names[value[0]], self._format(value[1])) Format value for map value display.
"""
return '%s: %s' % (
self.area_names.get(self.adapt_code(value[0]), '?'),
self._y_format(value[1]))
def _plot(self): def _plot(self):
"""Insert a map in the chart and apply data on it""" """Insert a map in the chart and apply data on it"""
@ -120,7 +124,7 @@ class BaseMap(Graph):
node.set('class', ' '.join(cls)) node.set('class', ' '.join(cls))
alter(node, metadata) alter(node, metadata)
val = self._get_value((area_code, value)) val = self._format(serie, j)
self._tooltip_data(area, val, 0, 0, 'auto') self._tooltip_data(area, val, 0, 0, 'auto')
self.nodes['plot'].append(map) self.nodes['plot'].append(map)

15
pygal/graph/pie.py

@ -36,15 +36,6 @@ class Pie(Graph):
_adapters = [positive, none_to_zero] _adapters = [positive, none_to_zero]
@property
def _format(self):
"""Return the value formatter for this graph"""
def percentage_formatter(y, self=self):
total = sum(map(sum, map(lambda x: x.values, self.series)))
perc = y/total
return '{0:.2%}'.format(perc)
return self.value_formatter or percentage_formatter
def slice(self, serie, start_angle, total): def slice(self, serie, start_angle, total):
"""Make a serie slice""" """Make a serie slice"""
serie_node = self.svg.serie(serie) serie_node = self.svg.serie(serie)
@ -52,7 +43,6 @@ class Pie(Graph):
slices = self.svg.node(serie_node['plot'], class_="slices") slices = self.svg.node(serie_node['plot'], class_="slices")
serie_angle = 0 serie_angle = 0
total_perc = 0
original_start_angle = start_angle original_start_angle = start_angle
if self.half_pie: if self.half_pie:
center = ((self.width - self.margin_box.x) / 2., center = ((self.width - self.margin_box.x) / 2.,
@ -69,7 +59,7 @@ class Pie(Graph):
else: else:
angle = 2 * pi * perc angle = 2 * pi * perc
serie_angle += angle serie_angle += angle
val = self._format(val) val = self._format(serie, i)
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
slice_ = decorate( slice_ = decorate(
self.svg, self.svg,
@ -86,10 +76,9 @@ class Pie(Graph):
serie_node, slice_, big_radius, small_radius, serie_node, slice_, big_radius, small_radius,
angle, start_angle, center, val, i, metadata), metadata) angle, start_angle, center, val, i, metadata), metadata)
start_angle += angle start_angle += angle
total_perc += perc
if dual: if dual:
val = self._format(total_perc*total) val = self._serie_format(serie, sum(serie.values))
self.svg.slice(serie_node, self.svg.slice(serie_node,
self.svg.node(slices, class_="big_slice"), self.svg.node(slices, class_="big_slice"),
radius * .9, 0, serie_angle, radius * .9, 0, serie_angle,

7
pygal/graph/pyramid.py

@ -35,10 +35,9 @@ class VerticalPyramid(StackedBar):
_adapters = [positive] _adapters = [positive]
def _format(self, value): def _value_format(self, value):
"""Return the value formatter for this graph here its absolute value""" """Format value for dual value display."""
value = value and abs(value) return super(VerticalPyramid, self)._value_format(value and abs(value))
return super(VerticalPyramid, self)._format(value)
def _get_separated_values(self, secondary=False): def _get_separated_values(self, secondary=False):
"""Separate values between odd and even series stacked""" """Separate values between odd and even series stacked"""

12
pygal/graph/radar.py

@ -49,10 +49,6 @@ class Radar(Line):
"""Add extra values to fill the line""" """Add extra values to fill the line"""
return values return values
def _get_value(self, values, i):
"""Get the value formatted for tooltip"""
return self._format(values[i][0])
@cached_property @cached_property
def _values(self): def _values(self):
"""Getter for series values (flattened)""" """Getter for series values (flattened)"""
@ -160,7 +156,7 @@ class Radar(Line):
self.svg.node( self.svg.node(
guides, 'title', guides, 'title',
).text = self._format(r) ).text = self._y_format(r)
def _compute(self): def _compute(self):
"""Compute r min max and labels position""" """Compute r min max and labels position"""
@ -198,17 +194,17 @@ class Radar(Line):
for i, y_label in enumerate(self.y_labels): for i, y_label in enumerate(self.y_labels):
if isinstance(y_label, dict): if isinstance(y_label, dict):
pos = self._adapt(y_label.get('value')) pos = self._adapt(y_label.get('value'))
title = y_label.get('label', self._format(pos)) title = y_label.get('label', self._y_format(pos))
elif is_str(y_label): elif is_str(y_label):
pos = self._adapt(y_pos[i]) pos = self._adapt(y_pos[i])
title = y_label title = y_label
else: else:
pos = self._adapt(y_label) pos = self._adapt(y_label)
title = self._format(pos) title = self._y_format(pos)
self._y_labels.append((title, pos)) self._y_labels.append((title, pos))
self._rmin = min(self._rmin, min(cut(self._y_labels, 1))) self._rmin = min(self._rmin, min(cut(self._y_labels, 1)))
self._rmax = max(self._rmax, max(cut(self._y_labels, 1))) self._rmax = max(self._rmax, max(cut(self._y_labels, 1)))
self._box.set_polar_box(self._rmin, self._rmax) self._box.set_polar_box(self._rmin, self._rmax)
else: else:
self._y_labels = list(zip(map(self._format, y_pos), y_pos)) self._y_labels = list(zip(map(self._y_format, y_pos), y_pos))

16
pygal/graph/stackedline.py

@ -39,6 +39,22 @@ class StackedLine(Line):
self._previous_line = None self._previous_line = None
super(StackedLine, self).__init__(*args, **kwargs) super(StackedLine, self).__init__(*args, **kwargs)
def _value_format(self, value, serie, index):
"""
Display value and cumulation
"""
sum_ = serie.points[index][1]
if serie in self.series and (
self.stack_from_top and
self.series.index(serie) == self._order - 1 or
not self.stack_from_top and
self.series.index(serie) == 0):
return super(StackedLine, self)._value_format(value)
return '%s (+%s)' % (
self._y_format(sum_),
self._y_format(value)
)
def _fill(self, values): def _fill(self, values):
"""Add extra values to fill the line""" """Add extra values to fill the line"""
if not self._previous_line: if not self._previous_line:

16
pygal/graph/time.py

@ -101,9 +101,7 @@ class DateTimeLine(XY):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""
def datetime_to_str(x): def datetime_to_str(x):
dt = datetime.utcfromtimestamp(x) dt = datetime.utcfromtimestamp(x)
if self.x_value_formatter: return self.x_value_formatter(dt)
return self.x_value_formatter(dt)
return dt.isoformat()
return datetime_to_str return datetime_to_str
@ -116,9 +114,7 @@ class DateLine(DateTimeLine):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""
def date_to_str(x): def date_to_str(x):
d = date.fromtimestamp(x) d = date.fromtimestamp(x)
if self.x_value_formatter: return self.x_value_formatter(d)
return self.x_value_formatter(d)
return d.isoformat()
return date_to_str return date_to_str
@ -133,9 +129,7 @@ class TimeLine(DateTimeLine):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""
def date_to_str(x): def date_to_str(x):
t = seconds_to_time(x) t = seconds_to_time(x)
if self.x_value_formatter: return self.x_value_formatter(t)
return self.x_value_formatter(t)
return t.isoformat()
return date_to_str return date_to_str
@ -150,8 +144,6 @@ class TimeDeltaLine(XY):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""
def timedelta_to_str(x): def timedelta_to_str(x):
td = timedelta(seconds=x) td = timedelta(seconds=x)
if self.x_value_formatter: return self.x_value_formatter(td)
return self.x_value_formatter(td)
return str(td)
return timedelta_to_str return timedelta_to_str

7
pygal/graph/treemap.py

@ -39,7 +39,8 @@ class Treemap(Graph):
rh -= ry rh -= ry
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
value = self._format(val)
val = self._format(serie, i)
rect = decorate( rect = decorate(
self.svg, self.svg,
@ -57,13 +58,13 @@ class Treemap(Graph):
metadata) metadata)
self._tooltip_data( self._tooltip_data(
rect, value, rect, val,
rx + rw / 2, rx + rw / 2,
ry + rh / 2, ry + rh / 2,
'centered', 'centered',
self._get_x_label(i)) self._get_x_label(i))
self._static_value( self._static_value(
serie_node, value, serie_node, val,
rx + rw / 2, rx + rw / 2,
ry + rh / 2, ry + rh / 2,
metadata) metadata)

5
pygal/graph/xy.py

@ -37,11 +37,6 @@ class XY(Line, Dual):
_x_adapters = [] _x_adapters = []
def _get_value(self, values, i):
"""Get the value formatted for tooltip"""
vals = values[i]
return '%s: %s' % (self._x_format(vals[0]), self._format(vals[1]))
@cached_property @cached_property
def xvals(self): def xvals(self):
"""All x values""" """All x values"""

7
pygal/table.py

@ -57,7 +57,6 @@ class Table(object):
""" """
self.chart.setup() self.chart.setup()
ln = self.chart._len ln = self.chart._len
fmt = self.chart._format
html = HTML() html = HTML()
attrs = {} attrs = {}
@ -92,10 +91,10 @@ class Table(object):
v = value or 0 v = value or 0
acc[j] += v acc[j] += v
sum_ += v sum_ += v
row.append(fmt(value)) row.append(self.chart._format(serie, j))
if total: if total:
acc[-1] += sum_ acc[-1] += sum_
row.append(fmt(sum_)) row.append(self.chart._value_format(serie, sum_))
table.append(row) table.append(row)
width = ln + 1 width = ln + 1
@ -103,7 +102,7 @@ class Table(object):
width += 1 width += 1
table.append(['Total']) table.append(['Total'])
for val in acc: for val in acc:
table[-1].append(fmt(val)) table[-1].append(self.chart._value_format(serie, val))
# Align values # Align values
len_ = max([len(r) for r in table] or [0]) len_ = max([len(r) for r in table] or [0])

29
pygal/test/test_config.py

@ -28,6 +28,7 @@ from pygal import (
from pygal.graph.map import BaseMap from pygal.graph.map import BaseMap
from pygal.graph.horizontal import HorizontalGraph from pygal.graph.horizontal import HorizontalGraph
from pygal.graph.dual import Dual from pygal.graph.dual import Dual
from pygal import formatters
from pygal._compat import u from pygal._compat import u
from pygal.test.utils import texts from pygal.test.utils import texts
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
@ -253,8 +254,10 @@ def test_human_readable():
line.add('_', [10 ** 4, 10 ** 5, 23 * 10 ** 4]) line.add('_', [10 ** 4, 10 ** 5, 23 * 10 ** 4])
q = line.render_pyquery() q = line.render_pyquery()
assert q(".axis.y text").map(texts) == list(map( assert q(".axis.y text").map(texts) == list(map(
str, map(float, range(20000, 240000, 20000)))) str, range(20000, 240000, 20000)))
line.human_readable = True
line.value_formatter = formatters.human_readable
q = line.render_pyquery() q = line.render_pyquery()
assert q(".axis.y text").map(texts) == list(map( assert q(".axis.y text").map(texts) == list(map(
lambda x: '%dk' % x, range(20, 240, 20))) lambda x: '%dk' % x, range(20, 240, 20)))
@ -309,12 +312,12 @@ def test_include_x_axis(Chart):
yaxis = ".axis.%s .guides text" % ( yaxis = ".axis.%s .guides text" % (
'y' if not getattr(chart, 'horizontal', False) else 'x') 'y' if not getattr(chart, 'horizontal', False) else 'x')
if not isinstance(chart, Bar): if not isinstance(chart, Bar):
assert '0.0' not in q(yaxis).map(texts) assert '0' not in q(yaxis).map(texts)
else: else:
assert '0.0' in q(yaxis).map(texts) assert '0' in q(yaxis).map(texts)
chart.include_x_axis = True chart.include_x_axis = True
q = chart.render_pyquery() q = chart.render_pyquery()
assert '0.0' in q(yaxis).map(texts) assert '0' in q(yaxis).map(texts)
def test_css(Chart): def test_css(Chart):
@ -504,3 +507,19 @@ def test_render_data_uri(Chart):
chart.add(u('èèè'), [10, 21, 5]) chart.add(u('èèè'), [10, 21, 5])
assert chart.render_data_uri().startswith( assert chart.render_data_uri().startswith(
'data:image/svg+xml;charset=utf-8;base64,') 'data:image/svg+xml;charset=utf-8;base64,')
def test_formatters(Chart):
"""Test custom formatters"""
if Chart._dual or Chart == Box:
return
chart = Chart(formatter=lambda x, chart, serie: '%s%s$' % (
x, serie.title))
chart.add('_a', [1, 2, {'value': 3, 'formatter': lambda x: '%s¥' % x}])
chart.add('_b', [4, 5, 6], formatter=lambda x: '%s' % x)
chart.x_labels = [2, 4, 6]
chart.x_labels_major = [4]
q = chart.render_pyquery()
assert {v.text for v in q(".value")} == set((
'4€', '5€', '6€', '1_a$', '2_a$', '') + (
('6_a$', '15€') if Chart == Pie else ()))

88
pygal/test/test_formatters.py

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2015 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/>.
"""Test formatters"""
from pygal import formatters
from pygal._compat import u
def test_human_readable():
"""Test human_readable formatter"""
f = formatters.human_readable
assert f(1) == '1'
assert f(1.) == '1'
assert f(10) == '10'
assert f(12.5) == '12.5'
assert f(1000) == '1k'
assert f(5000) == '5k'
assert f(100000) == '100k'
assert f(1253) == '1.253k'
assert f(1250) == '1.25k'
assert f(0.1) == '100m'
assert f(0.01) == '10m'
assert f(0.001) == '1m'
assert f(0.002) == '2m'
assert f(0.0025) == '2.5m'
assert f(0.0001) == u('100µ')
assert f(0.000123) == u('123µ')
assert f(0.00001) == u('10µ')
assert f(0.000001) == u('')
assert f(0.0000001) == u('100n')
assert f(0.0000000001) == u('100p')
assert f(0) == '0'
assert f(0.) == '0'
assert f(-1337) == '-1.337k'
assert f(-.000000042) == '-42n'
def test_human_readable_custom():
"""Test human_readable formatter option"""
f = formatters.HumanReadable()
assert f(None) == ''
f = formatters.HumanReadable(none_char='/')
assert f(None) == '/'
def test_significant():
"""Test significant formatter"""
f = formatters.significant
assert f(1) == '1'
assert f(1.) == '1'
assert f(-1.) == '-1'
assert f(10) == '10'
assert f(10000000000) == '1e+10'
assert f(100000000000) == '1e+11'
assert f(120000000000) == '1.2e+11'
assert f(.1) == '0.1'
assert f(.01) == '0.01'
assert f(.0000000001) == '1e-10'
assert f(-.0000000001) == '-1e-10'
assert f(.0000000001002) == '1.002e-10'
assert f(.0000000001002) == '1.002e-10'
assert f(.12345678912345) == '0.1234567891'
assert f(.012345678912345) == '0.01234567891'
assert f(12345678912345) == '1.234567891e+13'

6
pygal/test/test_graph.py

@ -399,11 +399,7 @@ def test_labels_with_links(Chart):
q = chart.render_pyquery() q = chart.render_pyquery()
links = q('a') links = q('a')
if isinstance(chart, BaseMap): assert len(links) == 7 or isinstance(chart, BaseMap) and len(links) == 3
# No country is found in this case so:
assert len(links) == 3
else:
assert len(links) == 7
def test_sparkline(Chart, datas): def test_sparkline(Chart, datas):

4
pygal/test/test_line.py

@ -46,8 +46,8 @@ def test_simple_line():
'-30', '-25', '-20', '-15', '-10', '-5', '-30', '-25', '-20', '-15', '-10', '-5',
'0', '5', '10', '15', '20', '25', '30'] '0', '5', '10', '15', '20', '25', '30']
assert q(".axis.y text").map(texts) == [ assert q(".axis.y text").map(texts) == [
'-1.2', '-1.0', '-0.8', '-0.6', '-0.4', '-0.2', '-1.2', '-1', '-0.8', '-0.6', '-0.4', '-0.2',
'0.0', '0.2', '0.4', '0.6', '0.8', '1.0', '1.2'] '0', '0.2', '0.4', '0.6', '0.8', '1', '1.2']
assert q(".title").text() == 'cos sin and cos - sin' assert q(".title").text() == 'cos sin and cos - sin'
assert q(".legend text").map(texts) == ['test1', 'test2', 'test3'] assert q(".legend text").map(texts) == ['test1', 'test2', 'test3']

16
pygal/test/test_stacked.py

@ -28,8 +28,8 @@ def test_stacked_line():
stacked.add('one_two', [1, 2]) stacked.add('one_two', [1, 2])
stacked.add('ten_twelve', [10, 12]) stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery() q = stacked.render_pyquery()
assert set(q("desc.value").text().split(' ')) == set( assert set([v.text for v in q("desc.value")]) == set(
('1', '2', '11', '14')) ('1', '2', '11 (+10)', '14 (+12)'))
def test_stacked_line_reverse(): def test_stacked_line_reverse():
@ -38,8 +38,8 @@ def test_stacked_line_reverse():
stacked.add('one_two', [1, 2]) stacked.add('one_two', [1, 2])
stacked.add('ten_twelve', [10, 12]) stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery() q = stacked.render_pyquery()
assert set(q("desc.value").text().split(' ')) == set( assert set([v.text for v in q("desc.value")]) == set(
('11', '14', '10', '12')) ('11 (+1)', '14 (+2)', '10', '12'))
def test_stacked_line_log(): def test_stacked_line_log():
@ -48,8 +48,8 @@ def test_stacked_line_log():
stacked.add('one_two', [1, 2]) stacked.add('one_two', [1, 2])
stacked.add('ten_twelve', [10, 12]) stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery() q = stacked.render_pyquery()
assert set(q("desc.value").text().split(' ')) == set( assert set([v.text for v in q("desc.value")]) == set(
('1', '2', '11', '14')) ('1', '2', '11 (+10)', '14 (+12)'))
def test_stacked_line_interpolate(): def test_stacked_line_interpolate():
@ -58,5 +58,5 @@ def test_stacked_line_interpolate():
stacked.add('one_two', [1, 2]) stacked.add('one_two', [1, 2])
stacked.add('ten_twelve', [10, 12]) stacked.add('ten_twelve', [10, 12])
q = stacked.render_pyquery() q = stacked.render_pyquery()
assert set(q("desc.value").text().split(' ')) == set( assert set([v.text for v in q("desc.value")]) == set(
('1', '2', '11', '14')) ('1', '2', '11 (+10)', '14 (+12)'))

32
pygal/test/test_util.py

@ -21,7 +21,7 @@
from pygal._compat import u from pygal._compat import u
from pygal.util import ( from pygal.util import (
round_to_int, round_to_float, _swap_curly, template, humanize, round_to_int, round_to_float, _swap_curly, template,
truncate, minify_css, majorize) truncate, minify_css, majorize)
from pytest import raises from pytest import raises
@ -86,36 +86,6 @@ def test_format():
o=obj) == 'foo 1 True-3' o=obj) == 'foo 1 True-3'
def test_humanize():
"""Test humanize function"""
assert humanize(1) == '1'
assert humanize(1.) == '1'
assert humanize(10) == '10'
assert humanize(12.5) == '12.5'
assert humanize(1000) == '1k'
assert humanize(5000) == '5k'
assert humanize(100000) == '100k'
assert humanize(1253) == '1.253k'
assert humanize(1250) == '1.25k'
assert humanize(0.1) == '100m'
assert humanize(0.01) == '10m'
assert humanize(0.001) == '1m'
assert humanize(0.002) == '2m'
assert humanize(0.0025) == '2.5m'
assert humanize(0.0001) == u('100µ')
assert humanize(0.000123) == u('123µ')
assert humanize(0.00001) == u('10µ')
assert humanize(0.000001) == u('')
assert humanize(0.0000001) == u('100n')
assert humanize(0.0000000001) == u('100p')
assert humanize(0) == '0'
assert humanize(0.) == '0'
assert humanize(-1337) == '-1.337k'
assert humanize(-.000000042) == '-42n'
def test_truncate(): def test_truncate():
"""Test truncate function""" """Test truncate function"""
assert truncate('1234567890', 50) == '1234567890' assert truncate('1234567890', 50) == '1234567890'

29
pygal/util.py

@ -24,12 +24,9 @@ from __future__ import division
import re import re
from decimal import Decimal from decimal import Decimal
from math import ceil, floor, log, log10, pi from math import ceil, floor, log10, pi
from pygal._compat import is_list_like, to_unicode, u from pygal._compat import to_unicode, u
ORDERS = u("yzafpnµm kMGTPEZY")
def float_format(number): def float_format(number):
@ -37,21 +34,6 @@ def float_format(number):
return ("%.3f" % number).rstrip('0').rstrip('.') return ("%.3f" % number).rstrip('0').rstrip('.')
def humanize(number):
"""Format a number to engineer scale"""
if is_list_like(number):
return', '.join(map(humanize, number))
if number is None:
return u('')
order = number and int(floor(log(abs(number)) / log(1000)))
human_readable = ORDERS.split(" ")[int(order > 0)]
if order == 0 or order > len(human_readable):
return float_format(number / (1000 ** int(order)))
return (
float_format(number / (1000 ** int(order))) +
human_readable[int(order) - int(order > 0)])
def majorize(values): def majorize(values):
"""Filter sequence to return only major considered numbers""" """Filter sequence to return only major considered numbers"""
sorted_values = sorted(values) sorted_values = sorted(values)
@ -355,3 +337,10 @@ def split_title(title, width, title_fs):
title_line = title_line[i:].strip() title_line = title_line[i:].strip()
titles.append(title_line) titles.append(title_line)
return titles return titles
def filter_kwargs(fun, kwargs):
if not hasattr(fun, '__code__'):
return {}
args = fun.__code__.co_varnames[1:]
return dict((k, v) for k, v in kwargs.items() if k in args)

Loading…
Cancel
Save