Skip to content

Commit

Permalink
Merge pull request #1098 from ioam/datetime_handling
Browse files Browse the repository at this point in the history
Made datetime handling in plotting code more general
  • Loading branch information
jlstevens authored Feb 2, 2017
2 parents 82a8d13 + 6c4b8a8 commit 68c6af8
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 94 deletions.
7 changes: 5 additions & 2 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down
132 changes: 42 additions & 90 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,79 +281,38 @@ 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'

# 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


Expand Down Expand Up @@ -517,44 +476,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

if any(isinstance(r, FactorRange) for r in [x_range, y_range]):
xfactors, yfactors = None, None
if any(isinstance(ax_range, FactorRange) for ax_range 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 invert: 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):
Expand Down
10 changes: 8 additions & 2 deletions holoviews/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
86 changes: 86 additions & 0 deletions tests/testplotinstantiation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):

Expand Down

0 comments on commit 68c6af8

Please sign in to comment.