From a4f8b56908ce23a1ed45d82fc73a994ab81d8a1d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 17 Aug 2016 12:28:34 +0100 Subject: [PATCH 01/11] Ensured xarray returns raw data in range Fix for datetime64 --- holoviews/core/data/xarray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index a293d4ecc3..4fe9ef3644 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -78,7 +78,7 @@ def range(cls, dataset, dimension): dim = dataset.get_dimension(dimension).name if dim in dataset.data: data = dataset.data[dim] - return data.min().item(), data.max().item() + return data.min().data, data.max().data else: return np.NaN, np.NaN From f0e33d219b9b358d319f8c524a85d756f5371982 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 17 Aug 2016 12:29:50 +0100 Subject: [PATCH 02/11] Added handling of datetime64 to matplotlib range handling --- holoviews/plotting/mpl/element.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index a481e17dd3..f39aaa63e8 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -1,4 +1,5 @@ import math +import datetime as dt from matplotlib import ticker from matplotlib import colors @@ -302,6 +303,8 @@ def _set_axis_limits(self, axis, view, subplots, ranges): extents = self.get_extents(view, ranges) if extents and not self.overlaid: coords = [coord if np.isreal(coord) else np.NaN for coord in extents] + coords = [c.astype(dt.datetime) if isinstance(c, np.datetime64) else c + for c in coords] valid_lim = lambda c: util.isnumeric(c) and not np.isnan(c) if self.projection == '3d' or len(extents) == 6: l, b, zmin, r, t, zmax = coords From a720c2a4c32fb50d14dcc253ffc9b7500cb49fac Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 17 Aug 2016 12:31:24 +0100 Subject: [PATCH 03/11] Safely handle datetime64 in matplotlib Curve plots --- holoviews/plotting/mpl/chart.py | 12 +++++++++++- holoviews/plotting/util.py | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index 2a9846cab3..673667a182 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -1,10 +1,12 @@ from __future__ import unicode_literals + from itertools import product import numpy as np from matplotlib import cm from matplotlib import pyplot as plt from matplotlib.collections import LineCollection +from matplotlib.dates import date2num import param @@ -12,7 +14,7 @@ from ...core.util import (match_spec, unique_iterator, safe_unicode, basestring, max_range, unicode) from ...element import Points, Raster, Polygons, HeatMap -from ..util import compute_sizes, get_sideplot_ranges +from ..util import compute_sizes, get_sideplot_ranges, dt64_to_dt from .element import ElementPlot, ColorbarPlot, LegendPlot from .path import PathPlot from .plot import AdjoinedPlot @@ -61,6 +63,14 @@ def get_data(self, element, ranges, style): ys = element.dimension_values(1) return (xs, ys), style, {} + def init_artists(self, ax, plot_args, plot_kwargs): + xs, ys = plot_args + if xs.dtype.kind == 'M': + xs = np.array([date2num(dt64_to_dt(x)) for x in xs]) + artist = ax.plot_date(xs, ys, '-', **plot_kwargs)[0] + else: + artist = ax.plot(xs, ys, **plot_kwargs)[0] + return {'artist': artist} def update_handles(self, key, axis, element, ranges, style): artist = self.handles['artist'] diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 727bdffdcd..f723d68514 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from datetime import datetime as dt + import numpy as np import param @@ -270,3 +272,10 @@ def dim_axis_label(dimensions, separator=', '): if not isinstance(dimensions, list): dimensions = [dimensions] return separator.join([safe_unicode(d.pprint_label) for d in dimensions]) + +def dt64_to_dt(dt64): + """ + Safely converts NumPy datetime64 to a datetime object. + """ + ts = (dt64 - np.datetime64('1970-01-01T00:00:00Z')) / np.timedelta64(1, 's') + return dt.utcfromtimestamp(ts) From eba660c8135b006dd4302fac083cabd966d715f1 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 17 Aug 2016 13:50:55 +0100 Subject: [PATCH 04/11] Further small fix for xarray datetime64 ranges --- holoviews/core/data/xarray.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 4fe9ef3644..16c15fd688 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -78,7 +78,10 @@ def range(cls, dataset, dimension): dim = dataset.get_dimension(dimension).name if dim in dataset.data: data = dataset.data[dim] - return data.min().data, data.max().data + dmin, dmax = data.min().data, data.max().data + dmin = dmin if np.isscalar(dmin) else dmin.item() + dmax = dmax if np.isscalar(dmax) else dmax.item() + return dmin, dmax else: return np.NaN, np.NaN From ae6369bb5b540a85a6a1acda041c4910908cf69c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 17 Aug 2016 13:51:26 +0100 Subject: [PATCH 05/11] Fix for Dateset datetime64 type lookup --- holoviews/core/data/__init__.py | 2 +- holoviews/core/data/grid.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index a159f297a1..f266adbe45 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -463,7 +463,7 @@ def get_dimension_type(self, dim): dim_obj = self.get_dimension(dim) if dim_obj and dim_obj.type is not None: return dim_obj.type - return self.interface.dimension_type(self, dim) + return self.interface.dimension_type(self, dim_obj) def dframe(self, dimensions=None): diff --git a/holoviews/core/data/grid.py b/holoviews/core/data/grid.py index 36c533b1b7..e4df29006d 100644 --- a/holoviews/core/data/grid.py +++ b/holoviews/core/data/grid.py @@ -84,10 +84,8 @@ def validate(cls, dataset): @classmethod def dimension_type(cls, dataset, dim): - if dim in dataset.kdims: - arr = dataset.data[dim.name] - elif dim in dataset.vdims: - arr = dataset.data[dim.name] + if dim in dataset.dimensions(): + arr = cls.values(dataset, dim, False, False) else: return None return arr.dtype.type From f801d42bc47b650f527b1f978bc42eec76c9ffd7 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 18 Aug 2016 10:55:01 +0100 Subject: [PATCH 06/11] Simplified datetime64 handling in matplotlib --- holoviews/plotting/mpl/chart.py | 1 - holoviews/plotting/mpl/element.py | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index 673667a182..d2f2da7d01 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -66,7 +66,6 @@ def get_data(self, element, ranges, style): def init_artists(self, ax, plot_args, plot_kwargs): xs, ys = plot_args if xs.dtype.kind == 'M': - xs = np.array([date2num(dt64_to_dt(x)) for x in xs]) artist = ax.plot_date(xs, ys, '-', **plot_kwargs)[0] else: artist = ax.plot(xs, ys, **plot_kwargs)[0] diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index f39aaa63e8..bd43651a6f 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -1,18 +1,18 @@ import math -import datetime as dt +import param +import numpy as np +import matplotlib.pyplot as plt from matplotlib import ticker from matplotlib import colors -import matplotlib.pyplot as plt -import numpy as np -import param +from matplotlib.dates import date2num from ...core import util from ...core import (OrderedDict, NdOverlay, DynamicMap, CompositeOverlay, Element3D, Element) from ...core.options import abbreviated_exception from ..plot import GenericElementPlot, GenericOverlayPlot -from ..util import dynamic_update +from ..util import dynamic_update, dt64_to_dt from .plot import MPLPlot from .util import wrap_formatter @@ -302,8 +302,8 @@ def _set_axis_limits(self, axis, view, subplots, ranges): scalex, scaley = True, True extents = self.get_extents(view, ranges) if extents and not self.overlaid: - coords = [coord if np.isreal(coord) else np.NaN for coord in extents] - coords = [c.astype(dt.datetime) if isinstance(c, np.datetime64) else c + coords = [coord if np.isreal(coord) or isinstance(coord, np.datetime64) else np.NaN for coord in extents] + coords = [date2num(dt64_to_dt(c)) if isinstance(c, np.datetime64) else c for c in coords] valid_lim = lambda c: util.isnumeric(c) and not np.isnan(c) if self.projection == '3d' or len(extents) == 6: From 8d4392d7f8a9f8755c1684b381d337c8f21f4674 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 18 Aug 2016 10:55:53 +0100 Subject: [PATCH 07/11] Unified tick formatting on matplotlib Grid plots --- holoviews/plotting/mpl/plot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/mpl/plot.py b/holoviews/plotting/mpl/plot.py index faae2e9580..907fed2cdf 100644 --- a/holoviews/plotting/mpl/plot.py +++ b/holoviews/plotting/mpl/plot.py @@ -262,9 +262,6 @@ class GridPlot(CompositePlot): show_legend = param.Boolean(default=False, doc=""" Legends add to much clutter in a grid and are disabled by default.""") - tick_format = param.String(default="%.2f", doc=""" - Formatting string for the GridPlot ticklabels.""") - xaxis = param.ObjectSelector(default='bottom', objects=['bottom', 'top', None], doc=""" Whether and where to display the xaxis, supported options are @@ -534,10 +531,13 @@ def _process_ticklabels(self, labels, dim): for k in labels: if dim and dim.value_format: k = dim.value_format(k) - elif not isinstance(k, (str, type(None))): - k = self.tick_format % k + elif dim.type in dim.type_formatters: + formatter = dim.type_formatters[dim.type] + k = formatter % k elif k is None: k = '' + else: + k = str(k) formatted_labels.append(k) return formatted_labels From 354086808c0b4bde83159ab650e7ca77f7269ee4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 18 Aug 2016 11:39:26 +0100 Subject: [PATCH 08/11] Allowed setting datetime formatters on dimensions --- holoviews/core/__init__.py | 1 + holoviews/core/dimension.py | 9 +++++++-- holoviews/core/util.py | 8 ++++++++ holoviews/plotting/mpl/chart.py | 12 ++++++++---- holoviews/plotting/mpl/element.py | 4 ++-- holoviews/plotting/util.py | 9 --------- 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/holoviews/core/__init__.py b/holoviews/core/__init__.py index 2146af3e90..2a4dd7deb2 100644 --- a/holoviews/core/__init__.py +++ b/holoviews/core/__init__.py @@ -17,6 +17,7 @@ Dimension.type_formatters[float] = "%.5g" Dimension.type_formatters[np.float32] = "%.5g" Dimension.type_formatters[np.float64] = "%.5g" +Dimension.type_formatters[np.datetime64] = '%Y-%m-%d %H:%M:%S' def public(obj): diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 8925e693f9..089a0475de 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -5,6 +5,7 @@ """ from __future__ import unicode_literals import re +import datetime as dt from operator import itemgetter import numpy as np @@ -13,7 +14,7 @@ from ..core.util import (basestring, sanitize_identifier, group_sanitizer, label_sanitizer, max_range, find_range, dimension_sanitizer, OrderedDict, - safe_unicode, unicode) + safe_unicode, unicode, dt64_to_dt) from .options import Store, StoreOptions from .pprint import PrettyPrinter @@ -178,7 +179,11 @@ def pprint_value(self, value): if callable(formatter): return formatter(value) elif isinstance(formatter, basestring): - if re.findall(r"\{(\w+)\}", formatter): + if isinstance(value, dt.datetime): + return value.strftime(formatter) + elif isinstance(value, np.datetime64): + return dt64_to_dt(value).strftime(formatter) + elif re.findall(r"\{(\w+)\}", formatter): return formatter.format(value) else: return formatter % value diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 7e558c268f..13c0d44324 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -3,6 +3,7 @@ import itertools import string, fnmatch import unicodedata +import datetime as dt from collections import defaultdict import numpy as np @@ -917,3 +918,10 @@ def expand_grid_coords(dataset, dim): idx = dataset.get_dimension_index(dim) return cartesian_product(arrays)[idx] + +def dt64_to_dt(dt64): + """ + Safely converts NumPy datetime64 to a datetime object. + """ + ts = (dt64 - np.datetime64('1970-01-01T00:00:00Z')) / np.timedelta64(1, 's') + return dt.datetime.utcfromtimestamp(ts) diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index d2f2da7d01..a2fd2120f1 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -6,15 +6,15 @@ from matplotlib import cm from matplotlib import pyplot as plt from matplotlib.collections import LineCollection -from matplotlib.dates import date2num +from matplotlib.dates import date2num, DateFormatter import param -from ...core import OrderedDict +from ...core import OrderedDict, Dimension from ...core.util import (match_spec, unique_iterator, safe_unicode, basestring, max_range, unicode) from ...element import Points, Raster, Polygons, HeatMap -from ..util import compute_sizes, get_sideplot_ranges, dt64_to_dt +from ..util import compute_sizes, get_sideplot_ranges from .element import ElementPlot, ColorbarPlot, LegendPlot from .path import PathPlot from .plot import AdjoinedPlot @@ -61,7 +61,11 @@ class CurvePlot(ChartPlot): def get_data(self, element, ranges, style): xs = element.dimension_values(0) ys = element.dimension_values(1) - return (xs, ys), style, {} + dims = element.dimensions() + if xs.dtype.kind == 'M': + dt_format = Dimension.type_formatters[np.datetime64] + dims[0] = dims[0](value_format=DateFormatter(dt_format)) + return (xs, ys), style, {'dimensions': dims} def init_artists(self, ax, plot_args, plot_kwargs): xs, ys = plot_args diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index bd43651a6f..d899082463 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -12,7 +12,7 @@ CompositeOverlay, Element3D, Element) from ...core.options import abbreviated_exception from ..plot import GenericElementPlot, GenericOverlayPlot -from ..util import dynamic_update, dt64_to_dt +from ..util import dynamic_update from .plot import MPLPlot from .util import wrap_formatter @@ -303,7 +303,7 @@ def _set_axis_limits(self, axis, view, subplots, ranges): extents = self.get_extents(view, ranges) if extents and not self.overlaid: coords = [coord if np.isreal(coord) or isinstance(coord, np.datetime64) else np.NaN for coord in extents] - coords = [date2num(dt64_to_dt(c)) if isinstance(c, np.datetime64) else c + coords = [date2num(util.dt64_to_dt(c)) if isinstance(c, np.datetime64) else c for c in coords] valid_lim = lambda c: util.isnumeric(c) and not np.isnan(c) if self.projection == '3d' or len(extents) == 6: diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index f723d68514..727bdffdcd 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from datetime import datetime as dt - import numpy as np import param @@ -272,10 +270,3 @@ def dim_axis_label(dimensions, separator=', '): if not isinstance(dimensions, list): dimensions = [dimensions] return separator.join([safe_unicode(d.pprint_label) for d in dimensions]) - -def dt64_to_dt(dt64): - """ - Safely converts NumPy datetime64 to a datetime object. - """ - ts = (dt64 - np.datetime64('1970-01-01T00:00:00Z')) / np.timedelta64(1, 's') - return dt.utcfromtimestamp(ts) From 97763aff8170539f8a6b2247ec6b277776779735 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 18 Aug 2016 11:48:26 +0100 Subject: [PATCH 09/11] Cleaned up mpl GridPlot tick formatting --- holoviews/plotting/mpl/plot.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/holoviews/plotting/mpl/plot.py b/holoviews/plotting/mpl/plot.py index 907fed2cdf..87c34f9df9 100644 --- a/holoviews/plotting/mpl/plot.py +++ b/holoviews/plotting/mpl/plot.py @@ -493,13 +493,15 @@ def _layout_axis(self, layout, axis): yticks = [(plot_height/2)+(r*(plot_height+border_height)) for r in range(self.rows)] layout_axis.set_xticks(xticks) - layout_axis.set_xticklabels(self._process_ticklabels(sorted(set(dim1_keys)), dims[0])) + layout_axis.set_xticklabels([dims[0].pprint_value(l) + for l in sorted(set(dim1_keys))]) for tick in layout_axis.get_xticklabels(): tick.set_rotation(self.xrotation) ydim = dims[1] if layout.ndims > 1 else None layout_axis.set_yticks(yticks) - layout_axis.set_yticklabels(self._process_ticklabels(sorted(set(dim2_keys)), ydim)) + layout_axis.set_yticklabels([ydim.pprint_value(l) if ydim else l + for l in sorted(set(dim2_keys))]) for tick in layout_axis.get_yticklabels(): tick.set_rotation(self.yrotation) @@ -526,22 +528,6 @@ def _layout_axis(self, layout, axis): return layout_axis - def _process_ticklabels(self, labels, dim): - formatted_labels = [] - for k in labels: - if dim and dim.value_format: - k = dim.value_format(k) - elif dim.type in dim.type_formatters: - formatter = dim.type_formatters[dim.type] - k = formatter % k - elif k is None: - k = '' - else: - k = str(k) - formatted_labels.append(k) - return formatted_labels - - def _adjust_subplots(self, axis, subaxes): bbox = axis.get_position() l, b, w, h = bbox.x0, bbox.y0, bbox.width, bbox.height From 4d6714f27b118cba3a0a864bd601d636f5286325 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 18 Aug 2016 13:14:59 +0100 Subject: [PATCH 10/11] Fixed RasterGridPlot tick formatting --- holoviews/plotting/mpl/raster.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index a734fecf39..4eaec6a75c 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -379,8 +379,9 @@ def update_frame(self, key, ranges=None): def _get_axis_kwargs(self): xdim = self.layout.kdims[0] ydim = self.layout.kdims[1] if self.layout.ndims > 1 else None - xticks = (self._xticks, self._process_ticklabels(self._xkeys, xdim)) - yticks = (self._yticks, self._process_ticklabels(self._ykeys, ydim)) + xticks = (self._xticks, [xdim.pprint_value(l) for l in self._xkeys]) + yticks = (self._yticks, [ydim.pprint_value(l) if ydim else l + for l in self._ykeys]) return dict(xlabel=xdim.pprint_label, ylabel=ydim.pprint_label if ydim else '', xticks=xticks, yticks=yticks) From 09bf924324ff319ac2b9adcd36194cd8771e83a8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 18 Aug 2016 14:14:31 +0100 Subject: [PATCH 11/11] Small fix for 1D grid tick formatting --- holoviews/plotting/mpl/plot.py | 2 +- holoviews/plotting/mpl/raster.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/mpl/plot.py b/holoviews/plotting/mpl/plot.py index 87c34f9df9..2d24f82ea5 100644 --- a/holoviews/plotting/mpl/plot.py +++ b/holoviews/plotting/mpl/plot.py @@ -500,7 +500,7 @@ def _layout_axis(self, layout, axis): ydim = dims[1] if layout.ndims > 1 else None layout_axis.set_yticks(yticks) - layout_axis.set_yticklabels([ydim.pprint_value(l) if ydim else l + layout_axis.set_yticklabels([ydim.pprint_value(l) if ydim else '' for l in sorted(set(dim2_keys))]) for tick in layout_axis.get_yticklabels(): tick.set_rotation(self.yrotation) diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index 4eaec6a75c..9f70fcd2bf 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -380,7 +380,7 @@ def _get_axis_kwargs(self): xdim = self.layout.kdims[0] ydim = self.layout.kdims[1] if self.layout.ndims > 1 else None xticks = (self._xticks, [xdim.pprint_value(l) for l in self._xkeys]) - yticks = (self._yticks, [ydim.pprint_value(l) if ydim else l + yticks = (self._yticks, [ydim.pprint_value(l) if ydim else '' for l in self._ykeys]) return dict(xlabel=xdim.pprint_label, ylabel=ydim.pprint_label if ydim else '', xticks=xticks, yticks=yticks)