diff --git a/CHANGELOG b/CHANGELOG
index c4e1780..c7f0258 100644
--- a/CHANGELOG
+++ b/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.
Set print_values to False by default.
Fix secondary serie text values when None in data. (#192)
diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py
index 5149d20..15e7870 100644
--- a/demo/moulinrouge/tests.py
+++ b/demo/moulinrouge/tests.py
@@ -3,7 +3,7 @@
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)
+ FrenchMap_Departments, FrenchMap_Regions, Pie, Treemap, TimeLine, DateLine)
from pygal.style import styles, Style, RotateStyle
from pygal.colors import rotate
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
@@ -58,7 +58,7 @@ def get_test_routes(app):
@app.route('/test/xy_links')
def test_xy_links():
- xy = XY(style=styles['neon'])
+ xy = XY(style=styles['neon'], interpolate='cubic')
xy.add('1234', [
{'value': (10, 5),
'label': 'Ten',
@@ -219,6 +219,12 @@ def get_test_routes(app):
graph.title = '123456789 ' * 30
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')
def test_datey_single():
graph = DateY(interpolate='cubic')
@@ -371,6 +377,19 @@ def get_test_routes(app):
stacked.add('2', [4, 5, 6])
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')
def test_datey():
from datetime import datetime
@@ -384,6 +403,35 @@ def get_test_routes(app):
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')
+ 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')
def test_worldmap():
wmap = Worldmap(style=choice(list(styles.values())))
diff --git a/pygal/_compat.py b/pygal/_compat.py
index d16d706..138ea12 100644
--- a/pygal/_compat.py
+++ b/pygal/_compat.py
@@ -18,6 +18,7 @@
# along with pygal. If not, see .
import sys
from collections import Iterable
+import time
if sys.version_info[0] == 3:
@@ -60,3 +61,14 @@ def total_seconds(td):
(td.days * 86400 + td.seconds) * 10 ** 6 + td.microseconds
) / 10 ** 6
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)
diff --git a/pygal/adapters.py b/pygal/adapters.py
index 11b97eb..5b650f1 100644
--- a/pygal/adapters.py
+++ b/pygal/adapters.py
@@ -20,8 +20,6 @@
Value adapters to use when a chart doesn't accept all value types
"""
-import datetime
-from numbers import Number
from decimal import Decimal
@@ -43,17 +41,6 @@ def none_to_zero(x):
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):
if isinstance(x, Decimal):
return float(x)
diff --git a/pygal/config.py b/pygal/config.py
index f85bf3a..9e91fad 100644
--- a/pygal/config.py
+++ b/pygal/config.py
@@ -232,7 +232,7 @@ class Config(CommonConfig):
stack_from_top = Key(
False, bool, "Look", "Stack from top to zero, this makes the stacked "
- "data match the legend order")
+ "data match the legend order")
spacing = Key(
10, int, "Look",
@@ -336,6 +336,11 @@ class Config(CommonConfig):
False, bool, "Value", "Display values in human readable format",
"(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(
None, type(lambda: 1), "Value",
"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",
"(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(
False, bool, "Value", "Always include x axis")
diff --git a/pygal/ghost.py b/pygal/ghost.py
index 27019d8..9d43e64 100644
--- a/pygal/ghost.py
+++ b/pygal/ghost.py
@@ -37,9 +37,20 @@ class ChartCollection(object):
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:
- mod_name = 'pygal.graph.%s' % NAME.lower()
+ 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)
diff --git a/pygal/graph/__init__.py b/pygal/graph/__init__.py
index 250ee08..0a05b2a 100644
--- a/pygal/graph/__init__.py
+++ b/pygal/graph/__init__.py
@@ -36,11 +36,15 @@ CHARTS_NAMES = [
'VerticalPyramid',
'Dot',
'Gauge',
- 'DateY',
'Worldmap',
'SupranationalWorldmap',
'Histogram',
'Box',
'FrenchMap',
- 'Treemap'
+ 'Treemap',
+ 'DateY',
+ 'DateTimeLine',
+ 'DateLine',
+ 'TimeLine',
+ 'TimeDeltaLine'
]
diff --git a/pygal/graph/base.py b/pygal/graph/base.py
index 8d6d68c..fd4b131 100644
--- a/pygal/graph/base.py
+++ b/pygal/graph/base.py
@@ -69,6 +69,8 @@ class BaseGraph(object):
for serie in self.series for val in serie.safe_values]))
self.zero = min(positive_values or (1,)) or 1
+ if self._len < 3:
+ self.interpolate = None
self._draw()
self.svg.pre_render()
@@ -76,6 +78,12 @@ class BaseGraph(object):
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"""
diff --git a/pygal/graph/box.py b/pygal/graph/box.py
index 5e41f13..fa29bdc 100644
--- a/pygal/graph/box.py
+++ b/pygal/graph/box.py
@@ -89,6 +89,11 @@ class Box(Graph):
for serie in self.series:
self._boxf(serie)
+ @property
+ def _len(self):
+ """Len is always 5 here"""
+ return 5
+
def _boxf(self, serie):
"""
For a specific series, draw the box plot.
diff --git a/pygal/graph/datey.py b/pygal/graph/datey.py
deleted file mode 100644
index 3476332..0000000
--- a/pygal/graph/datey.py
+++ /dev/null
@@ -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 .
-
-"""
-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))
diff --git a/pygal/graph/time.py b/pygal/graph/time.py
new file mode 100644
index 0000000..d9a8963
--- /dev/null
+++ b/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 .
+"""
+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
diff --git a/pygal/graph/xy.py b/pygal/graph/xy.py
index 292c3a5..028a95e 100644
--- a/pygal/graph/xy.py
+++ b/pygal/graph/xy.py
@@ -22,13 +22,15 @@ XY Line graph
"""
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
class XY(Line):
"""XY Line graph"""
_dual = True
+ _x_adapters = []
@cached_property
def xvals(self):
@@ -63,8 +65,17 @@ class XY(Line):
def _compute(self):
if self.xvals:
- xmin = min(self.xvals)
- xmax = max(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)
+ xmax = max(self.xvals)
xrng = (xmax - xmin)
else:
xrng = None
@@ -83,13 +94,13 @@ class XY(Line):
for serie in self.all_series:
serie.points = serie.values
- if self.interpolate and xrng:
+ if self.interpolate:
vals = list(zip(*sorted(
filter(lambda t: None not in t,
serie.points), key=lambda x: x[0])))
serie.interpolated = self._interpolate(vals[0], vals[1])
- if self.interpolate and xrng:
+ if self.interpolate:
self.xvals = [val[0]
for serie in self.all_series
for val in serie.interpolated]
@@ -109,9 +120,10 @@ class XY(Line):
self._box.ymin, self._box.ymax = ymin, ymax
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(
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))
diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py
index 9c281d1..d773ea9 100644
--- a/pygal/test/test_config.py
+++ b/pygal/test/test_config.py
@@ -21,7 +21,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)
+ FrenchMap_Regions, FrenchMap_Departments,
+ DateTimeLine, TimeLine, DateLine, TimeDeltaLine)
from pygal._compat import u
from pygal.test.utils import texts
from tempfile import NamedTemporaryFile
@@ -362,7 +363,8 @@ def test_x_label_major(Chart):
Pie, Treemap, Funnel, Dot, Gauge, Worldmap,
SupranationalWorldmap, Histogram, Box,
FrenchMap_Regions, FrenchMap_Departments,
- Pyramid, DateY):
+ Pyramid, DateY, DateTimeLine, TimeLine, DateLine,
+ TimeDeltaLine):
return
chart = Chart()
chart.add('test', range(12))
@@ -407,7 +409,8 @@ def test_y_label_major(Chart):
SupranationalWorldmap, Histogram, Box,
FrenchMap_Regions, FrenchMap_Departments,
HorizontalBar, HorizontalStackedBar,
- Pyramid, DateY):
+ Pyramid, DateTimeLine, TimeLine, DateLine,
+ TimeDeltaLine, DateY):
return
chart = Chart()
data = range(12)
diff --git a/pygal/test/test_date.py b/pygal/test/test_date.py
index 73016a4..875284d 100644
--- a/pygal/test/test_date.py
+++ b/pygal/test/test_date.py
@@ -16,49 +16,117 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see .
-from pygal import DateY
+from pygal import DateLine, TimeLine, DateTimeLine, TimeDeltaLine
from pygal.test.utils import texts
-from datetime import datetime
+from datetime import datetime, date, time, timedelta
def test_date():
- datey = DateY(truncate_label=1000)
- datey.add('dates', [
- (datetime(2013, 1, 2), 300),
- (datetime(2013, 1, 12), 412),
- (datetime(2013, 2, 2), 823),
- (datetime(2013, 2, 22), 672)
+ date_chart = DateLine(truncate_label=1000)
+ date_chart.add('dates', [
+ (date(2013, 1, 2), 300),
+ (date(2013, 1, 12), 412),
+ (date(2013, 2, 2), 823),
+ (date(2013, 2, 22), 672)
])
- q = datey.render_pyquery()
+ q = date_chart.render_pyquery()
assert list(
map(lambda t: t.split(' ')[0],
q(".axis.x text").map(texts))) == [
- '2013-01-02',
- '2013-01-13',
- '2013-01-25',
- '2013-02-05',
- '2013-02-17'
+ '2013-01-12',
+ '2013-01-24',
+ '2013-02-04',
+ '2013-02-16'
]
- datey.x_labels = [
- datetime(2013, 1, 1),
- datetime(2013, 2, 1),
- datetime(2013, 3, 1)
- ]
- q = datey.render_pyquery()
+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))) == [
- '2013-01-01',
- '2013-02-01',
- '2013-03-01'
- ]
+ '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)
+ ])
+
+ q = datetime_chart.render_pyquery()
+
+ assert list(
+ map(lambda t: t.split(' ')[0],
+ q(".axis.x text").map(texts))) == [
+ '2013-01-12T15:13:20',
+ '2013-01-24T05:00:00',
+ '2013-02-04T18:46:40',
+ '2013-02-16T08:33:20'
+ ]
+
+
+def test_timedelta():
+ timedelta_chart = TimeDeltaLine(truncate_label=1000)
+ 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_overflow():
- datey = DateY(truncate_label=1000)
- datey.add('dates', [1, 2, -1000000, 5, 100000000])
- assert datey.render_pyquery()
+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()
+ assert list(
+ map(lambda t: t.split(' ')[0],
+ q(".axis.x text").map(texts))) == [
+ '2013-01-01',
+ '2013-01-12',
+ '2013-01-24',
+ '2013-02-04',
+ '2013-02-16',
+ '2013-02-27'
+ ]
diff --git a/pygal/test/test_util.py b/pygal/test/test_util.py
index 787611e..b68e683 100644
--- a/pygal/test/test_util.py
+++ b/pygal/test/test_util.py
@@ -16,6 +16,7 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see .
+from datetime import time
from pygal._compat import u
from pygal.util import (
round_to_int, round_to_float, _swap_curly, template, humanize,
diff --git a/pygal/util.py b/pygal/util.py
index f08cec1..2e9521f 100644
--- a/pygal/util.py
+++ b/pygal/util.py
@@ -27,7 +27,8 @@ 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
+from pygal.adapters import (
+ not_zero, positive, decimal_to_float)
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"""
from pygal.serie import Serie
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.worldmap import Worldmap
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 + [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 = [(
@@ -404,13 +408,16 @@ def prepare_values(raw, config, cls, offset=0):
value = (None, None)
elif not is_list_like(value):
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)):
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())
diff --git a/pygal/view.py b/pygal/view.py
index 45b96b0..0de1e48 100644
--- a/pygal/view.py
+++ b/pygal/view.py
@@ -201,8 +201,11 @@ class PolarLogView(View):
if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'):
raise Exception(
'Box must be set with set_polar_box for polar charts')
+
self.log10_rmax = log10(self.box._rmax)
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):
"""Project rho and theta"""
@@ -257,10 +260,11 @@ class PolarThetaLogView(View):
'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_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):
"""Project rho and theta"""
-
if None in rhotheta:
return None, None
rho, theta = rhotheta
@@ -294,6 +298,8 @@ class LogView(View):
self.box = box
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
+ if self.log10_ymin == self.log10_ymax:
+ self.log10_ymax = self.log10_ymin + 1
self.box.fix(False)
def y(self, y):
@@ -347,6 +353,8 @@ class HorizontalLogView(XLogView):
self.box = box
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
+ if self.log10_xmin == self.log10_xmax:
+ self.log10_xmax = self.log10_xmin + 1
self.box.fix(False)
self.box.swap()