Browse Source

Remove DateY and replace it by real XY datetime, date, time and timedelta support. Introduce new XY configuration options: `xrange`, `x_value_formatter`.

pull/158/merge
Florian Mounier 10 years ago
parent
commit
a5f94ebd7d
  1. 7
      CHANGELOG
  2. 52
      demo/moulinrouge/tests.py
  3. 12
      pygal/_compat.py
  4. 13
      pygal/adapters.py
  5. 10
      pygal/config.py
  6. 13
      pygal/ghost.py
  7. 8
      pygal/graph/__init__.py
  8. 8
      pygal/graph/base.py
  9. 5
      pygal/graph/box.py
  10. 147
      pygal/graph/datey.py
  11. 108
      pygal/graph/time.py
  12. 22
      pygal/graph/xy.py
  13. 9
      pygal/test/test_config.py
  14. 118
      pygal/test/test_date.py
  15. 1
      pygal/test/test_util.py
  16. 13
      pygal/util.py
  17. 10
      pygal/view.py

7
CHANGELOG

@ -1,4 +1,9 @@
V 1.6.3 UNRELEASED V 2.0.0 UNRELEASED
Remove DateY and replace it by real XY datetime, date, time and timedelta support.
Introduce new XY configuration options: `xrange`, `x_value_formatter`
Rework the ghost mechanism to come back to a more object oriented behavior (WIP)
V 1.6.3
Add show_x_labels option to remove them and the x axis. Add show_x_labels option to remove them and the x axis.
Set print_values to False by default. Set print_values to False by default.
Fix secondary serie text values when None in data. (#192) Fix secondary serie text values when None in data. (#192)

52
demo/moulinrouge/tests.py

@ -3,7 +3,7 @@
from pygal import ( from pygal import (
Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY, Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY,
CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram, Box, CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram, Box,
FrenchMap_Departments, FrenchMap_Regions, Pie, Treemap) FrenchMap_Departments, FrenchMap_Regions, Pie, Treemap, TimeLine, DateLine)
from pygal.style import styles, Style, RotateStyle from pygal.style import styles, Style, RotateStyle
from pygal.colors import rotate from pygal.colors import rotate
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
@ -58,7 +58,7 @@ def get_test_routes(app):
@app.route('/test/xy_links') @app.route('/test/xy_links')
def test_xy_links(): def test_xy_links():
xy = XY(style=styles['neon']) xy = XY(style=styles['neon'], interpolate='cubic')
xy.add('1234', [ xy.add('1234', [
{'value': (10, 5), {'value': (10, 5),
'label': 'Ten', 'label': 'Ten',
@ -219,6 +219,12 @@ def get_test_routes(app):
graph.title = '123456789 ' * 30 graph.title = '123456789 ' * 30
return graph.render_response() return graph.render_response()
@app.route('/test/xy_single')
def test_xy_single():
graph = XY(interpolate='cubic')
graph.add('Single', [(1, 1)])
return graph.render_response()
@app.route('/test/datey_single') @app.route('/test/datey_single')
def test_datey_single(): def test_datey_single():
graph = DateY(interpolate='cubic') graph = DateY(interpolate='cubic')
@ -371,6 +377,19 @@ def get_test_routes(app):
stacked.add('2', [4, 5, 6]) stacked.add('2', [4, 5, 6])
return stacked.render_response() return stacked.render_response()
@app.route('/test/dateline')
def test_dateline():
from datetime import date
datey = DateLine(show_dots=False)
datey.add('1', [
(datetime(2013, 1, 2), 300),
(datetime(2013, 1, 12), 412),
(datetime(2013, 2, 2), 823),
(datetime(2013, 2, 22), 672)
])
datey.x_label_rotation = 25
return datey.render_response()
@app.route('/test/datey') @app.route('/test/datey')
def test_datey(): def test_datey():
from datetime import datetime from datetime import datetime
@ -384,6 +403,35 @@ def get_test_routes(app):
datey.x_label_rotation = 25 datey.x_label_rotation = 25
return datey.render_response() 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')
def test_timexy():
from datetime import time
datey = TimeLine()
datey.add('1', [
(time(1, 12, 29), 2),
(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.x_label_rotation = 25
return datey.render_response()
@app.route('/test/worldmap') @app.route('/test/worldmap')
def test_worldmap(): def test_worldmap():
wmap = Worldmap(style=choice(list(styles.values()))) wmap = Worldmap(style=choice(list(styles.values())))

12
pygal/_compat.py

@ -18,6 +18,7 @@
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
import sys import sys
from collections import Iterable from collections import Iterable
import time
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
@ -60,3 +61,14 @@ def total_seconds(td):
(td.days * 86400 + td.seconds) * 10 ** 6 + td.microseconds (td.days * 86400 + td.seconds) * 10 ** 6 + td.microseconds
) / 10 ** 6 ) / 10 ** 6
return td.total_seconds() return td.total_seconds()
def to_timestamp(x):
if hasattr(x, 'timestamp'):
return x.timestamp()
else:
if hasattr(x, 'utctimetuple'):
t = x.utctimetuple()
else:
t = x.timetuple()
return time.mktime(t)

13
pygal/adapters.py

@ -20,8 +20,6 @@
Value adapters to use when a chart doesn't accept all value types Value adapters to use when a chart doesn't accept all value types
""" """
import datetime
from numbers import Number
from decimal import Decimal from decimal import Decimal
@ -43,17 +41,6 @@ def none_to_zero(x):
return x or 0 return x or 0
def date(x):
# Make int work for date graphs by counting days number from now
if isinstance(x, Number):
try:
d = datetime.date.today() + datetime.timedelta(days=x)
return datetime.datetime.combine(d, datetime.time(0, 0, 0))
except OverflowError:
return None
return x
def decimal_to_float(x): def decimal_to_float(x):
if isinstance(x, Decimal): if isinstance(x, Decimal):
return float(x) return float(x)

10
pygal/config.py

@ -336,6 +336,11 @@ class Config(CommonConfig):
False, bool, "Value", "Display values in human readable format", False, bool, "Value", "Display values in human readable format",
"(ie: 12.4M)") "(ie: 12.4M)")
x_value_formatter = Key(
None, type(lambda: 1), "Value",
"A function to convert abscissa numeric value to strings "
"(used in XY and Date charts)")
value_formatter = Key( value_formatter = Key(
None, type(lambda: 1), "Value", None, type(lambda: 1), "Value",
"A function to convert numeric value to strings") "A function to convert numeric value to strings")
@ -367,6 +372,11 @@ class Config(CommonConfig):
None, list, "Value", "Explicitly specify min and max of values", None, list, "Value", "Explicitly specify min and max of values",
"(ie: (0, 100))", int) "(ie: (0, 100))", int)
xrange = Key(
None, list, "Value", "Explicitly specify min and max of x values "
"(used in XY and Date charts)",
"(ie: (0, 100))", int)
include_x_axis = Key( include_x_axis = Key(
False, bool, "Value", "Always include x axis") False, bool, "Value", "Always include x axis")

13
pygal/ghost.py

@ -37,9 +37,20 @@ class ChartCollection(object):
pass pass
REAL_CHARTS = {} 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: for NAME in CHARTS_NAMES:
if NAME in REAL_CHARTS:
mod_name = 'pygal.graph.time'
else:
mod_name = 'pygal.graph.%s' % NAME.lower() mod_name = 'pygal.graph.%s' % NAME.lower()
__import__(mod_name) __import__(mod_name)
mod = sys.modules[mod_name] mod = sys.modules[mod_name]
chart = getattr(mod, NAME) chart = getattr(mod, NAME)

8
pygal/graph/__init__.py

@ -36,11 +36,15 @@ CHARTS_NAMES = [
'VerticalPyramid', 'VerticalPyramid',
'Dot', 'Dot',
'Gauge', 'Gauge',
'DateY',
'Worldmap', 'Worldmap',
'SupranationalWorldmap', 'SupranationalWorldmap',
'Histogram', 'Histogram',
'Box', 'Box',
'FrenchMap', 'FrenchMap',
'Treemap' 'Treemap',
'DateY',
'DateTimeLine',
'DateLine',
'TimeLine',
'TimeDeltaLine'
] ]

8
pygal/graph/base.py

@ -69,6 +69,8 @@ class BaseGraph(object):
for serie in self.series for val in serie.safe_values])) for serie in self.series for val in serie.safe_values]))
self.zero = min(positive_values or (1,)) or 1 self.zero = min(positive_values or (1,)) or 1
if self._len < 3:
self.interpolate = None
self._draw() self._draw()
self.svg.pre_render() self.svg.pre_render()
@ -76,6 +78,12 @@ class BaseGraph(object):
def all_series(self): def all_series(self):
return self.series + self.secondary_series 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 @property
def _format(self): def _format(self):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""

5
pygal/graph/box.py

@ -89,6 +89,11 @@ class Box(Graph):
for serie in self.series: for serie in self.series:
self._boxf(serie) self._boxf(serie)
@property
def _len(self):
"""Len is always 5 here"""
return 5
def _boxf(self, serie): def _boxf(self, serie):
""" """
For a specific series, draw the box plot. For a specific series, draw the box plot.

147
pygal/graph/datey.py

@ -1,147 +0,0 @@
# -*- coding: utf-8 -*-
# This file is proposed as a part of pygal
# A python svg graph plotting library
#
# A python svg graph plotting library
# Copyright © 2012 Snarkturne (modified from Kozea XY class)
#
# 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/>.
"""
DateY graph
Example :
import pygal
from datetime import datetime,timedelta
def jour(n) :
return datetime(year=2014,month=1,day=1)+timedelta(days=n)
x=(1,20,35,54,345,898)
x=tuple(map(jour,x))
x_label=(0,100,200,300,400,500,600,700,800,900,1000)
x_label=map(jour,x_label)
y=(1,3,4,2,3,1)
graph=pygal.DateY(x_label_rotation=20)
graph.x_label_format = "%Y-%m-%d"
graph.x_labels = x_label
graph.add("graph1",list(zip(x,y))+[None,None])
graph.render_in_browser()
"""
from pygal._compat import total_seconds
from pygal.adapters import date
from pygal.util import compute_scale
from pygal.graph.xy import XY
import datetime
class DateY(XY):
""" DateY Graph """
_offset = datetime.datetime(year=2000, month=1, day=1)
_adapters = [date]
def _todate(self, d):
""" Converts a number to a date """
currDateTime = self._offset + datetime.timedelta(seconds=d or 0)
return currDateTime.strftime(self.x_label_format)
def _tonumber(self, d):
""" Converts a date to a number """
if d is None:
return None
return total_seconds(d - self._offset)
def _get_value(self, values, i):
return 'x=%s, y=%s' % (
self._todate(values[i][0]), self._format(values[i][1]))
def _compute(self):
# Approximatively the same code as in XY.
# The only difference is the transformation of dates to numbers
# (beginning) and the reversed transformation to dates (end)
self._offset = min([val[0]
for serie in self.series
for val in serie.values
if val[0] is not None]
or [datetime.datetime.fromtimestamp(0)])
for serie in self.all_series:
serie.values = [(self._tonumber(v[0]), v[1]) for v in serie.values]
if self.xvals:
xmin = min(self.xvals)
xmax = max(self.xvals)
rng = (xmax - xmin)
else:
rng = None
if self.yvals:
ymin = self._min
ymax = self._max
if self.include_x_axis:
ymin = min(self._min or 0, 0)
ymax = max(self._max or 0, 0)
for serie in self.all_series:
serie.points = serie.values
if self.interpolate and rng:
vals = list(zip(*sorted(
[t for t in serie.points if None not in t],
key=lambda x: x[0])))
serie.interpolated = self._interpolate(vals[0], vals[1])
if self.interpolate and rng:
self.xvals = [val[0]
for serie in self.all_series
for val in serie.interpolated]
self.yvals = [val[1]
for serie in self.all_series
for val in serie.interpolated]
xmin = min(self.xvals)
xmax = max(self.xvals)
rng = (xmax - xmin)
# Calculate/prcoess the x_labels
if self.x_labels and all(
map(lambda x: isinstance(
x, (datetime.datetime, datetime.date)), self.x_labels)):
# Process the given x_labels
x_labels_num = []
for label in self.x_labels:
x_labels_num.append(self._tonumber(label))
x_pos = x_labels_num
# Update the xmin/xmax to fit all of the x_labels and the data
xmin = min(xmin, min(x_pos))
xmax = max(xmax, max(x_pos))
self._box.xmin, self._box.xmax = xmin, xmax
self._box.ymin, self._box.ymax = ymin, ymax
else:
# Automatically generate the x_labels
if rng:
self._box.xmin, self._box.xmax = xmin, xmax
self._box.ymin, self._box.ymax = ymin, ymax
x_pos = compute_scale(
self._box.xmin, self._box.xmax, self.logarithmic,
self.order_min)
# Always auto-generate the y labels
y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min)
self._x_labels = list(zip(list(map(self._todate, x_pos)), x_pos))
self._y_labels = list(zip(list(map(self._format, y_pos)), y_pos))

108
pygal/graph/time.py

@ -0,0 +1,108 @@
# -*- 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/>.
"""
Various datetime line plot
"""
from pygal.graph.xy import XY
from datetime import datetime, date, time, timedelta
from pygal._compat import to_timestamp
def datetime_to_timestamp(x):
if isinstance(x, datetime):
return to_timestamp(x)
return x
def date_to_datetime(x):
if isinstance(x, date):
return datetime.combine(x, time())
return x
def time_to_datetime(x):
if isinstance(x, time):
return datetime.combine(date(1970, 1, 1), x)
return x
def timedelta_to_seconds(x):
if isinstance(x, timedelta):
return x.total_seconds()
return x
class DateTimeLine(XY):
_x_adapters = [datetime_to_timestamp, date_to_datetime]
@property
def _x_format(self):
"""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)
return dt.isoformat()
return datetime_to_str
class DateLine(DateTimeLine):
@property
def _x_format(self):
"""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)
return d.isoformat()
return date_to_str
class TimeLine(DateTimeLine):
_x_adapters = [datetime_to_timestamp, time_to_datetime]
@property
def _x_format(self):
"""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)
return t.isoformat()
return date_to_str
class TimeDeltaLine(XY):
_x_adapters = [timedelta_to_seconds]
@property
def _x_format(self):
"""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)
return str(td)
return timedelta_to_str
# Old pygal compat
DateY = DateTimeLine

22
pygal/graph/xy.py

@ -22,13 +22,15 @@ XY Line graph
""" """
from __future__ import division from __future__ import division
from pygal.util import compute_scale, cached_property from functools import reduce
from pygal.util import compute_scale, cached_property, compose
from pygal.graph.line import Line from pygal.graph.line import Line
class XY(Line): class XY(Line):
"""XY Line graph""" """XY Line graph"""
_dual = True _dual = True
_x_adapters = []
@cached_property @cached_property
def xvals(self): def xvals(self):
@ -63,6 +65,15 @@ class XY(Line):
def _compute(self): def _compute(self):
if self.xvals: if self.xvals:
if self.xrange:
x_adapter = reduce(
compose, self._x_adapters) if getattr(
self, '_x_adapters', None) else None
xmin = x_adapter(self.xrange[0])
xmax = x_adapter(self.xrange[1])
else:
xmin = min(self.xvals) xmin = min(self.xvals)
xmax = max(self.xvals) xmax = max(self.xvals)
xrng = (xmax - xmin) xrng = (xmax - xmin)
@ -83,13 +94,13 @@ class XY(Line):
for serie in self.all_series: for serie in self.all_series:
serie.points = serie.values serie.points = serie.values
if self.interpolate and xrng: if self.interpolate:
vals = list(zip(*sorted( vals = list(zip(*sorted(
filter(lambda t: None not in t, filter(lambda t: None not in t,
serie.points), key=lambda x: x[0]))) serie.points), key=lambda x: x[0])))
serie.interpolated = self._interpolate(vals[0], vals[1]) serie.interpolated = self._interpolate(vals[0], vals[1])
if self.interpolate and xrng: if self.interpolate:
self.xvals = [val[0] self.xvals = [val[0]
for serie in self.all_series for serie in self.all_series
for val in serie.interpolated] for val in serie.interpolated]
@ -109,9 +120,10 @@ class XY(Line):
self._box.ymin, self._box.ymax = ymin, ymax self._box.ymin, self._box.ymax = ymin, ymax
x_pos = compute_scale( x_pos = compute_scale(
self._box.xmin, self._box.xmax, self.logarithmic, self.order_min) self._box.xmin, self._box.xmax, self.logarithmic,
self.order_min)
y_pos = compute_scale( y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min) self._box.ymin, self._box.ymax, self.logarithmic, self.order_min)
self._x_labels = list(zip(map(self._format, x_pos), x_pos)) self._x_labels = list(zip(map(self._x_format, x_pos), x_pos))
self._y_labels = list(zip(map(self._format, y_pos), y_pos)) self._y_labels = list(zip(map(self._format, y_pos), y_pos))

9
pygal/test/test_config.py

@ -21,7 +21,8 @@ from pygal import (
Line, Dot, Pie, Treemap, Radar, Config, Bar, Funnel, Worldmap, Line, Dot, Pie, Treemap, Radar, Config, Bar, Funnel, Worldmap,
SupranationalWorldmap, Histogram, Gauge, Box, XY, SupranationalWorldmap, Histogram, Gauge, Box, XY,
Pyramid, DateY, HorizontalBar, HorizontalStackedBar, Pyramid, DateY, HorizontalBar, HorizontalStackedBar,
FrenchMap_Regions, FrenchMap_Departments) FrenchMap_Regions, FrenchMap_Departments,
DateTimeLine, TimeLine, DateLine, TimeDeltaLine)
from pygal._compat import u from pygal._compat import u
from pygal.test.utils import texts from pygal.test.utils import texts
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
@ -362,7 +363,8 @@ def test_x_label_major(Chart):
Pie, Treemap, Funnel, Dot, Gauge, Worldmap, Pie, Treemap, Funnel, Dot, Gauge, Worldmap,
SupranationalWorldmap, Histogram, Box, SupranationalWorldmap, Histogram, Box,
FrenchMap_Regions, FrenchMap_Departments, FrenchMap_Regions, FrenchMap_Departments,
Pyramid, DateY): Pyramid, DateY, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine):
return return
chart = Chart() chart = Chart()
chart.add('test', range(12)) chart.add('test', range(12))
@ -407,7 +409,8 @@ def test_y_label_major(Chart):
SupranationalWorldmap, Histogram, Box, SupranationalWorldmap, Histogram, Box,
FrenchMap_Regions, FrenchMap_Departments, FrenchMap_Regions, FrenchMap_Departments,
HorizontalBar, HorizontalStackedBar, HorizontalBar, HorizontalStackedBar,
Pyramid, DateY): Pyramid, DateTimeLine, TimeLine, DateLine,
TimeDeltaLine, DateY):
return return
chart = Chart() chart = Chart()
data = range(12) data = range(12)

118
pygal/test/test_date.py

@ -16,49 +16,117 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import DateY from pygal import DateLine, TimeLine, DateTimeLine, TimeDeltaLine
from pygal.test.utils import texts from pygal.test.utils import texts
from datetime import datetime from datetime import datetime, date, time, timedelta
def test_date(): def test_date():
datey = DateY(truncate_label=1000) date_chart = DateLine(truncate_label=1000)
datey.add('dates', [ date_chart.add('dates', [
(datetime(2013, 1, 2), 300), (date(2013, 1, 2), 300),
(datetime(2013, 1, 12), 412), (date(2013, 1, 12), 412),
(datetime(2013, 2, 2), 823), (date(2013, 2, 2), 823),
(date(2013, 2, 22), 672)
])
q = date_chart.render_pyquery()
assert list(
map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'2013-01-12',
'2013-01-24',
'2013-02-04',
'2013-02-16'
]
def test_time():
time_chart = TimeLine(truncate_label=1000)
time_chart.add('times', [
(time(1, 12, 29), 2),
(time(21, 2, 29), 10),
(time(12, 30, 59), 7)
])
q = time_chart.render_pyquery()
assert list(
map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
'03:46:40',
'06:33:20',
'09:20:00',
'12:06:40',
'14:53:20',
'17:40:00',
'20:26:40'
]
def test_datetime():
datetime_chart = DateTimeLine(truncate_label=1000)
datetime_chart.add('datetimes', [
(datetime(2013, 1, 2, 1, 12, 29), 300),
(datetime(2013, 1, 12, 21, 2, 29), 412),
(datetime(2013, 2, 2, 12, 30, 59), 823),
(datetime(2013, 2, 22), 672) (datetime(2013, 2, 22), 672)
]) ])
q = datey.render_pyquery() q = datetime_chart.render_pyquery()
assert list( assert list(
map(lambda t: t.split(' ')[0], map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [ q(".axis.x text").map(texts))) == [
'2013-01-02', '2013-01-12T15:13:20',
'2013-01-13', '2013-01-24T05:00:00',
'2013-01-25', '2013-02-04T18:46:40',
'2013-02-05', '2013-02-16T08:33:20'
'2013-02-17'
] ]
datey.x_labels = [
datetime(2013, 1, 1), def test_timedelta():
datetime(2013, 2, 1), timedelta_chart = TimeDeltaLine(truncate_label=1000)
datetime(2013, 3, 1) timedelta_chart.add('timedeltas', [
(timedelta(seconds=1), 10),
(timedelta(weeks=1), 50),
(timedelta(hours=3, seconds=30), 3),
(timedelta(microseconds=12112), .3),
])
q = timedelta_chart.render_pyquery()
assert list(
q(".axis.x text").map(texts)) == [
'1 day, 3:46:40',
'2 days, 7:33:20',
'3 days, 11:20:00',
'4 days, 15:06:40',
'5 days, 18:53:20',
'6 days, 22:40:00'
] ]
def test_date_xrange():
datey = DateLine(truncate_label=1000)
datey.add('dates', [
(date(2013, 1, 2), 300),
(date(2013, 1, 12), 412),
(date(2013, 2, 2), 823),
(date(2013, 2, 22), 672)
])
datey.xrange = (date(2013, 1, 1), date(2013, 3, 1))
q = datey.render_pyquery() q = datey.render_pyquery()
assert list( assert list(
map(lambda t: t.split(' ')[0], map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [ q(".axis.x text").map(texts))) == [
'2013-01-01', '2013-01-01',
'2013-02-01', '2013-01-12',
'2013-03-01' '2013-01-24',
'2013-02-04',
'2013-02-16',
'2013-02-27'
] ]
def test_date_overflow():
datey = DateY(truncate_label=1000)
datey.add('dates', [1, 2, -1000000, 5, 100000000])
assert datey.render_pyquery()

1
pygal/test/test_util.py

@ -16,6 +16,7 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
from datetime import time
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, humanize,

13
pygal/util.py

@ -27,7 +27,8 @@ from decimal import Decimal
from math import floor, pi, log, log10, ceil from math import floor, pi, log, log10, ceil
from itertools import cycle from itertools import cycle
from functools import reduce from functools import reduce
from pygal.adapters import not_zero, positive, decimal_to_float from pygal.adapters import (
not_zero, positive, decimal_to_float)
ORDERS = u("yzafpnµm kMGTPEZY") ORDERS = u("yzafpnµm kMGTPEZY")
@ -334,7 +335,7 @@ def prepare_values(raw, config, cls, offset=0):
"""Prepare the values to start with sane values""" """Prepare the values to start with sane values"""
from pygal.serie import Serie from pygal.serie import Serie
from pygal.config import SerieConfig from pygal.config import SerieConfig
from pygal.graph.datey import DateY from pygal.graph.time import DateY
from pygal.graph.histogram import Histogram from pygal.graph.histogram import Histogram
from pygal.graph.worldmap import Worldmap from pygal.graph.worldmap import Worldmap
from pygal.graph.frenchmap import FrenchMapDepartments from pygal.graph.frenchmap import FrenchMapDepartments
@ -357,6 +358,9 @@ def prepare_values(raw, config, cls, offset=0):
adapters = adapters + [positive, not_zero] adapters = adapters + [positive, not_zero]
adapters = adapters + [decimal_to_float] adapters = adapters + [decimal_to_float]
adapter = reduce(compose, adapters) if not config.strict else ident 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 = [] series = []
raw = [( raw = [(
@ -404,13 +408,16 @@ def prepare_values(raw, config, cls, offset=0):
value = (None, None) value = (None, None)
elif not is_list_like(value): elif not is_list_like(value):
value = (value, config.zero) value = (value, config.zero)
if issubclass(cls, DateY) or issubclass( if x_adapter:
value = (x_adapter(value[0]), adapter(value[1]))
if issubclass(
cls, (Worldmap, FrenchMapDepartments)): cls, (Worldmap, FrenchMapDepartments)):
value = (adapter(value[0]), value[1]) value = (adapter(value[0]), value[1])
else: else:
value = list(map(adapter, value)) value = list(map(adapter, value))
else: else:
value = adapter(value) value = adapter(value)
values.append(value) values.append(value)
serie_config = SerieConfig() serie_config = SerieConfig()
serie_config(**config.to_dict()) serie_config(**config.to_dict())

10
pygal/view.py

@ -201,8 +201,11 @@ class PolarLogView(View):
if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'): if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'):
raise Exception( raise Exception(
'Box must be set with set_polar_box for polar charts') 'Box must be set with set_polar_box for polar charts')
self.log10_rmax = log10(self.box._rmax) self.log10_rmax = log10(self.box._rmax)
self.log10_rmin = log10(self.box._rmin) self.log10_rmin = log10(self.box._rmin)
if self.log10_rmin == self.log10_rmax:
self.log10_rmax = self.log10_rmin + 1
def __call__(self, rhotheta): def __call__(self, rhotheta):
"""Project rho and theta""" """Project rho and theta"""
@ -257,10 +260,11 @@ class PolarThetaLogView(View):
'Box must be set with set_polar_box for polar charts') 'Box must be set with set_polar_box for polar charts')
self.log10_tmax = log10(self.box._tmax) if self.box._tmax > 0 else 0 self.log10_tmax = log10(self.box._tmax) if self.box._tmax > 0 else 0
self.log10_tmin = log10(self.box._tmin) if self.box._tmin > 0 else 0 self.log10_tmin = log10(self.box._tmin) if self.box._tmin > 0 else 0
if self.log10_tmin == self.log10_tmax:
self.log10_tmax = self.log10_tmin + 1
def __call__(self, rhotheta): def __call__(self, rhotheta):
"""Project rho and theta""" """Project rho and theta"""
if None in rhotheta: if None in rhotheta:
return None, None return None, None
rho, theta = rhotheta rho, theta = rhotheta
@ -294,6 +298,8 @@ class LogView(View):
self.box = box self.box = box
self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0 self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0
self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0 self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0
if self.log10_ymin == self.log10_ymax:
self.log10_ymax = self.log10_ymin + 1
self.box.fix(False) self.box.fix(False)
def y(self, y): def y(self, y):
@ -347,6 +353,8 @@ class HorizontalLogView(XLogView):
self.box = box self.box = box
self.log10_xmax = log10(self.box.ymax) if self.box.ymax > 0 else 0 self.log10_xmax = log10(self.box.ymax) if self.box.ymax > 0 else 0
self.log10_xmin = log10(self.box.ymin) if self.box.ymin > 0 else 0 self.log10_xmin = log10(self.box.ymin) if self.box.ymin > 0 else 0
if self.log10_xmin == self.log10_xmax:
self.log10_xmax = self.log10_xmin + 1
self.box.fix(False) self.box.fix(False)
self.box.swap() self.box.swap()

Loading…
Cancel
Save