Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Made datetime handling in plotting code more general #1098

Merged
merged 3 commits into from
Feb 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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