From 4e9a668f6113cb0fc49d2b9804ccfe397a6bb90b Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 2 Feb 2017 00:50:13 +0000
Subject: [PATCH 1/3] Made datetime handling in plotting code more general
---
holoviews/core/util.py | 7 +++-
holoviews/plotting/bokeh/element.py | 59 +++++++++++++----------------
holoviews/plotting/plot.py | 10 ++++-
3 files changed, 39 insertions(+), 37 deletions(-)
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
index 55712daa54..9223f01077 100644
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -16,8 +16,11 @@
except:
from collections import OrderedDict
+datetime_types = (np.datetime64, dt.datetime)
+
try:
import pandas as pd # noqa (optional import)
+ datetime_types = datetime_types + (pd.tslib.Timestamp,)
except ImportError:
pd = None
@@ -493,13 +496,13 @@ def max_extents(extents, zrange=False):
for lidx, uidx in inds:
lower = [v for v in arr[lidx] if v is not None]
upper = [v for v in arr[uidx] if v is not None]
- if lower and isinstance(lower[0], np.datetime64):
+ if lower and isinstance(lower[0], datetime_types):
extents[lidx] = np.min(lower)
elif any(isinstance(l, basestring) for l in lower):
extents[lidx] = np.sort(lower)[0]
elif lower:
extents[lidx] = np.nanmin(lower)
- if upper and isinstance(upper[0], np.datetime64):
+ if upper and isinstance(upper[0], datetime_types):
extents[uidx] = np.max(upper)
elif any(isinstance(u, basestring) for u in upper):
extents[uidx] = np.sort(upper)[-1]
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index 1312210319..01aa128843 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -281,12 +281,12 @@ def _axes_props(self, plots, subplots, element, ranges):
if plot.yaxis[0].axis_label == xlabel:
plot_ranges['x_range'] = plot.y_range
- if el.get_dimension_type(0) is np.datetime64:
+ if el.get_dimension_type(0) in util.datetime_types:
x_axis_type = 'datetime'
else:
x_axis_type = 'log' if self.logx else 'auto'
- if len(dims) > 1 and el.get_dimension_type(1) is np.datetime64:
+ if len(dims) > 1 and el.get_dimension_type(1) in util.datetime_types:
y_axis_type = 'datetime'
else:
y_axis_type = 'log' if self.logy else 'auto'
@@ -517,44 +517,37 @@ def _update_ranges(self, element, ranges):
x_range = self.handles['x_range']
y_range = self.handles['y_range']
+ l, b, r, t = None, None, None, None
if any(isinstance(r, Range1d) for r in [x_range, y_range]):
l, b, r, t = self.get_extents(element, ranges)
if self.invert_axes:
l, b, r, t = b, l, t, r
+ xfactors, yfactors = None, None
if any(isinstance(r, FactorRange) for r in [x_range, y_range]):
xfactors, yfactors = self._get_factors(element)
-
- if isinstance(x_range, Range1d):
- if l == r and l is not None:
- offset = abs(l*0.1 if l else 0.5)
- l -= offset
- r += offset
-
- if self.invert_xaxis: l, r = r, l
- if l is not None and (isinstance(l, np.datetime64) or np.isfinite(l)):
- plot.x_range.start = l
- if r is not None and (isinstance(r, np.datetime64) or np.isfinite(r)):
- plot.x_range.end = r
- elif isinstance(x_range, FactorRange):
- xfactors = list(xfactors)
- if self.invert_xaxis: xfactors = xfactors[::-1]
- x_range.factors = xfactors
-
- if isinstance(plot.y_range, Range1d):
- if b == t and b is not None:
- offset = abs(b*0.1 if b else 0.5)
- b -= offset
- t += offset
- if self.invert_yaxis: b, t = t, b
- if b is not None and (isinstance(l, np.datetime64) or np.isfinite(b)):
- plot.y_range.start = b
- if t is not None and (isinstance(l, np.datetime64) or np.isfinite(t)):
- plot.y_range.end = t
- elif isinstance(y_range, FactorRange):
- yfactors = list(yfactors)
- if self.invert_yaxis: yfactors = yfactors[::-1]
- y_range.factors = yfactors
+ self._update_range(x_range, l, r, xfactors, self.invert_xaxis)
+ self._update_range(y_range, b, t, yfactors, self.invert_yaxis)
+
+
+ def _update_range(self, axis_range, low, high, factors, invert):
+ if isinstance(axis_range, Range1d):
+ if (low == high and low is not None and
+ not isinstance(high, util.datetime_types)):
+ offset = abs(low*0.1 if low else 0.5)
+ low -= offset
+ high += offset
+ if self.invert_yaxis: low, high = high, low
+ if low is not None and (isinstance(low, util.datetime_types)
+ or np.isfinite(low)):
+ axis_range.start = low
+ if high is not None and (isinstance(high, util.datetime_types)
+ or np.isfinite(high)):
+ axis_range.end = high
+ elif isinstance(axis_range, FactorRange):
+ factors = list(factors)
+ if invert: factors = factors[::-1]
+ axis_range.factors = factors
def _categorize_data(self, data, cols, dims):
diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py
index 0d2eb9ac3a..338683de1a 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -709,8 +709,14 @@ def get_extents(self, view, ranges):
if getattr(self, 'shared_axes', False) and self.subplot:
return util.max_extents([range_extents, extents], self.projection == '3d')
else:
- return tuple(l1 if l2 is None or not np.isfinite(l2) else
- l2 for l1, l2 in zip(range_extents, extents))
+ max_extent = []
+ for l1, l2 in zip(range_extents, extents):
+ if (isinstance(l2, util.datetime_types)
+ or (l2 is not None and np.isfinite(l2))):
+ max_extent.append(l2)
+ else:
+ max_extent.append(l1)
+ return tuple(max_extent)
def _get_axis_labels(self, dimensions, xlabel=None, ylabel=None, zlabel=None):
From 37f860568d07f346b22d855f69d870df04257858 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 2 Feb 2017 02:58:57 +0000
Subject: [PATCH 2/3] Separated axis initialization from range setting
---
holoviews/plotting/bokeh/element.py | 75 +++++++----------------------
1 file changed, 17 insertions(+), 58 deletions(-)
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index 01aa128843..0365ea3d4c 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -294,66 +294,25 @@ def _axes_props(self, plots, subplots, element, ranges):
# Get the Element that determines the range and get_extents
range_el = el if self.batched and not isinstance(self, OverlayPlot) else element
l, b, r, t = self.get_extents(range_el, ranges)
-
- categorical = False
- if not 'x_range' in plot_ranges:
- if 'x_range' in ranges:
- plot_ranges['x_range'] = ranges['x_range']
- else:
- low, high = (b, t) if self.invert_axes else (l, r)
- if x_axis_type == 'datetime':
- low = convert_datetime(low)
- high = convert_datetime(high)
- elif any(isinstance(x, util.basestring) for x in (low, high)):
- plot_ranges['x_range'] = FactorRange()
- categorical = True
- elif low == high and low is not None:
- offset = low*0.1 if low else 0.5
- low -= offset
- high += offset
- if not categorical and all(x is not None and np.isfinite(x) for x in (low, high)):
- plot_ranges['x_range'] = [low, high]
-
- if self.invert_xaxis:
- x_range = plot_ranges['x_range']
- if isinstance(x_range, Range1d):
- plot_ranges['x_range'] = x_range.__class__(start=x_range.end,
- end=x_range.start)
- elif not isinstance(x_range, (Range, FactorRange)):
- plot_ranges['x_range'] = x_range[::-1]
-
- categorical = False
- if not 'y_range' in plot_ranges:
- if 'y_range' in ranges:
- plot_ranges['y_range'] = ranges['y_range']
- else:
- low, high = (l, r) if self.invert_axes else (b, t)
- if y_axis_type == 'datetime':
- low = convert_datetime(low)
- high = convert_datetime(high)
- elif any(isinstance(y, util.basestring) for y in (low, high)):
- plot_ranges['y_range'] = FactorRange()
- categorical = True
- elif low == high and low is not None:
- offset = low*0.1 if low else 0.5
- low -= offset
- high += offset
- if not categorical and all(y is not None and np.isfinite(y) for y in (low, high)):
- plot_ranges['y_range'] = [low, high]
-
- if self.invert_yaxis:
- yrange = plot_ranges['y_range']
- if isinstance(yrange, Range1d):
- plot_ranges['y_range'] = yrange.__class__(start=yrange.end,
- end=yrange.start)
- elif not isinstance(yrange, (Range, FactorRange)):
- plot_ranges['y_range'] = yrange[::-1]
+ if self.invert_axes:
+ l, b, r, t = b, l, t, r
categorical = any(self.traverse(lambda x: x._categorical))
- if categorical:
- x_axis_type, y_axis_type = 'auto', 'auto'
+ categorical_x = any(isinstance(x, util.basestring) for x in (l, r))
+ categorical_y = any(isinstance(y, util.basestring) for y in (b, t))
+
+ if categorical or categorical_x:
+ x_axis_type = 'auto'
plot_ranges['x_range'] = FactorRange()
+ elif 'x_range' not in plot_ranges:
+ plot_ranges['x_range'] = Range1d()
+
+ if categorical or categorical_y:
+ y_axis_type = 'auto'
plot_ranges['y_range'] = FactorRange()
+ elif 'y_range' not in plot_ranges:
+ plot_ranges['y_range'] = Range1d()
+
return (x_axis_type, y_axis_type), (xlabel, ylabel, zlabel), plot_ranges
@@ -524,7 +483,7 @@ def _update_ranges(self, element, ranges):
l, b, r, t = b, l, t, r
xfactors, yfactors = None, None
- if any(isinstance(r, FactorRange) for r in [x_range, y_range]):
+ if any(isinstance(ax_range, FactorRange) for ax_range in [x_range, y_range]):
xfactors, yfactors = self._get_factors(element)
self._update_range(x_range, l, r, xfactors, self.invert_xaxis)
self._update_range(y_range, b, t, yfactors, self.invert_yaxis)
@@ -537,7 +496,7 @@ def _update_range(self, axis_range, low, high, factors, invert):
offset = abs(low*0.1 if low else 0.5)
low -= offset
high += offset
- if self.invert_yaxis: low, high = high, low
+ if invert: low, high = high, low
if low is not None and (isinstance(low, util.datetime_types)
or np.isfinite(low)):
axis_range.start = low
From 6c4b8a8ed1a3a0e51bdbeaa377e3d72c91aab007 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Thu, 2 Feb 2017 14:17:52 +0000
Subject: [PATCH 3/3] Added unit tests for plotting different datetime types
---
tests/testplotinstantiation.py | 86 ++++++++++++++++++++++++++++++++++
1 file changed, 86 insertions(+)
diff --git a/tests/testplotinstantiation.py b/tests/testplotinstantiation.py
index f2b16c61bc..fe2a403f85 100644
--- a/tests/testplotinstantiation.py
+++ b/tests/testplotinstantiation.py
@@ -13,6 +13,7 @@
import numpy as np
from holoviews import (Dimension, Overlay, DynamicMap, Store,
NdOverlay, GridSpace, HoloMap, Layout)
+from holoviews.core.util import pd
from holoviews.element import (Curve, Scatter, Image, VLine, Points,
HeatMap, QuadMesh, Spikes, ErrorBars,
Scatter3D, Path, Polygons, Bars, Text,
@@ -142,6 +143,46 @@ def test_points_non_numeric_size_warning(self):
'cannot use to scale Points size.\n' % plot.name)
self.assertEqual(log_msg, warning)
+ def test_curve_datetime64(self):
+ dates = [np.datetime64(dt.datetime(2016,1,i)) for i in range(1, 11)]
+ curve = Curve((dates, np.random.rand(10)))
+ plot = mpl_renderer.get_plot(curve)
+ self.assertEqual(plot.handles['axis'].get_xlim(), (735964.0, 735973.0))
+
+ def test_curve_pandas_timestamps(self):
+ if not pd:
+ raise SkipError("Pandas not available")
+ dates = pd.date_range('2016-01-01', '2016-01-10', freq='D')
+ curve = Curve((dates, np.random.rand(10)))
+ plot = mpl_renderer.get_plot(curve)
+ self.assertEqual(plot.handles['axis'].get_xlim(), (735964.0, 735973.0))
+
+ def test_curve_dt_datetime(self):
+ dates = [dt.datetime(2016,1,i) for i in range(1, 11)]
+ curve = Curve((dates, np.random.rand(10)))
+ plot = mpl_renderer.get_plot(curve)
+ self.assertEqual(plot.handles['axis'].get_xlim(), (735964.0, 735973.0))
+
+ def test_curve_heterogeneous_datetime_types_overlay(self):
+ dates64 = [np.datetime64(dt.datetime(2016,1,i)) for i in range(1, 11)]
+ dates = [dt.datetime(2016,1,i) for i in range(2, 12)]
+ curve_dt64 = Curve((dates64, np.random.rand(10)))
+ curve_dt = Curve((dates, np.random.rand(10)))
+ plot = mpl_renderer.get_plot(curve_dt*curve_dt64)
+ self.assertEqual(plot.handles['axis'].get_xlim(), (735964.0, 735974.0))
+
+ def test_curve_heterogeneous_datetime_types_with_pd_overlay(self):
+ if not pd:
+ raise SkipError("Pandas not available")
+ dates_pd = pd.date_range('2016-01-04', '2016-01-13', freq='D')
+ dates64 = [np.datetime64(dt.datetime(2016,1,i)) for i in range(1, 11)]
+ dates = [dt.datetime(2016,1,i) for i in range(2, 12)]
+ curve_dt64 = Curve((dates64, np.random.rand(10)))
+ curve_dt = Curve((dates, np.random.rand(10)))
+ curve_pd = Curve((dates_pd, np.random.rand(10)))
+ plot = mpl_renderer.get_plot(curve_dt*curve_dt64*curve_pd)
+ self.assertEqual(plot.handles['axis'].get_xlim(), (735964.0, 735976.0))
+
class TestBokehPlotInstantiation(ComparisonTestCase):
@@ -557,6 +598,51 @@ def test_box_whisker_datetime(self):
self.assertTrue(cds.data['Date'][0] in formatted for cds in
plot.state.select(ColumnDataSource))
+ def test_curve_datetime64(self):
+ dates = [np.datetime64(dt.datetime(2016,1,i)) for i in range(1, 11)]
+ curve = Curve((dates, np.random.rand(10)))
+ plot = bokeh_renderer.get_plot(curve)
+ self.assertEqual(plot.handles['x_range'].start, np.datetime64(dt.datetime(2016, 1, 1)))
+ self.assertEqual(plot.handles['x_range'].end, np.datetime64(dt.datetime(2016, 1, 10)))
+
+ def test_curve_pandas_timestamps(self):
+ if not pd:
+ raise SkipError("Pandas not available")
+ dates = pd.date_range('2016-01-01', '2016-01-10', freq='D')
+ curve = Curve((dates, np.random.rand(10)))
+ plot = bokeh_renderer.get_plot(curve)
+ self.assertEqual(plot.handles['x_range'].start, np.datetime64(dt.datetime(2016, 1, 1)))
+ self.assertEqual(plot.handles['x_range'].end, np.datetime64(dt.datetime(2016, 1, 10)))
+
+ def test_curve_dt_datetime(self):
+ dates = [dt.datetime(2016,1,i) for i in range(1, 11)]
+ curve = Curve((dates, np.random.rand(10)))
+ plot = bokeh_renderer.get_plot(curve)
+ self.assertEqual(plot.handles['x_range'].start, np.datetime64(dt.datetime(2016, 1, 1)))
+ self.assertEqual(plot.handles['x_range'].end, np.datetime64(dt.datetime(2016, 1, 10)))
+
+ def test_curve_heterogeneous_datetime_types_overlay(self):
+ dates64 = [np.datetime64(dt.datetime(2016,1,i)) for i in range(1, 11)]
+ dates = [dt.datetime(2016,1,i) for i in range(2, 12)]
+ curve_dt64 = Curve((dates64, np.random.rand(10)))
+ curve_dt = Curve((dates, np.random.rand(10)))
+ plot = bokeh_renderer.get_plot(curve_dt*curve_dt64)
+ self.assertEqual(plot.handles['x_range'].start, np.datetime64(dt.datetime(2016, 1, 1)))
+ self.assertEqual(plot.handles['x_range'].end, np.datetime64(dt.datetime(2016, 1, 11)))
+
+ def test_curve_heterogeneous_datetime_types_with_pd_overlay(self):
+ if not pd:
+ raise SkipError("Pandas not available")
+ dates_pd = pd.date_range('2016-01-04', '2016-01-13', freq='D')
+ dates64 = [np.datetime64(dt.datetime(2016,1,i)) for i in range(1, 11)]
+ dates = [dt.datetime(2016,1,i) for i in range(2, 12)]
+ curve_dt64 = Curve((dates64, np.random.rand(10)))
+ curve_dt = Curve((dates, np.random.rand(10)))
+ curve_pd = Curve((dates_pd, np.random.rand(10)))
+ plot = bokeh_renderer.get_plot(curve_dt*curve_dt64*curve_pd)
+ self.assertEqual(plot.handles['x_range'].start, np.datetime64(dt.datetime(2016, 1, 1)))
+ self.assertEqual(plot.handles['x_range'].end, np.datetime64(dt.datetime(2016, 1, 13)))
+
class TestPlotlyPlotInstantiation(ComparisonTestCase):