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):