Gridded Dataset Key Dimension Datatypes

Moving here from gitter. Seems to be somewhat of a blind-spot in support for different datatypes in the various elements that support gridded datasets.

Namely:

  • hv.HeatMap does not support a datetime-like key dimension
  • hv.QuadMesh does not (seem to) support a categorical-like key dimension

This combination is unfortunate for instances like the Measles app, where the current docs treat years as categories in a heatmap, but this prevents e.g. a Layout from syncing curves that plot against a time-series to the elements of the grid above.

Imagine wanting to resample the measles curve to be weeks/months/10Y frequencies, etc, when the underlying dataset is raw occurrence timestamps of individual cases.

On the other hand, QuadMesh does accept datetime-like key dimensions (as suggested by @philippjfr on Gitter), but it seems that a categorical/str-like key dimension throws an error. From the docs:

heatmap = hv.HeatMap((
    ['A', 'B', 'C'], 
    ['a', 'b', 'c', 'd', 'e'], 
    np.random.rand(5, 3)
))

works fine, but heatmap.to(hv.QuadMesh) throws the following opaque error:

---------------------------------------------------------------------------
UFuncTypeError                            Traceback (most recent call last)
~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/IPython/core/formatters.py in __call__(self, obj, include, exclude)
    968 
    969             if method is not None:
--> 970                 return method(include=include, exclude=exclude)
    971             return None
    972         else:

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/core/dimension.py in _repr_mimebundle_(self, include, exclude)
   1292         combined and returned.
   1293         """
-> 1294         return Store.render(self)
   1295 
   1296 

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/core/options.py in render(cls, obj)
   1366         data, metadata = {}, {}
   1367         for hook in hooks:
-> 1368             ret = hook(obj)
   1369             if ret is None:
   1370                 continue

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/ipython/display_hooks.py in pprint_display(obj)
    279     if not ip.display_formatter.formatters['text/plain'].pprint:
    280         return None
--> 281     return display(obj, raw_output=True)
    282 
    283 

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/ipython/display_hooks.py in display(obj, raw_output, **kwargs)
    249     elif isinstance(obj, (CompositeOverlay, ViewableElement)):
    250         with option_state(obj):
--> 251             output = element_display(obj)
    252     elif isinstance(obj, (Layout, NdLayout, AdjointLayout)):
    253         with option_state(obj):

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/ipython/display_hooks.py in wrapped(element)
    144         try:
    145             max_frames = OutputSettings.options['max_frames']
--> 146             mimebundle = fn(element, max_frames=max_frames)
    147             if mimebundle is None:
    148                 return {}, {}

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/ipython/display_hooks.py in element_display(element, max_frames)
    190         return None
    191 
--> 192     return render(element)
    193 
    194 

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/ipython/display_hooks.py in render(obj, **kwargs)
     66         renderer = renderer.instance(fig='png')
     67 
---> 68     return renderer.components(obj, **kwargs)
     69 
     70 

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/plotting/bokeh/renderer.py in components(self, obj, fmt, comm, **kwargs)
    248         # Bokeh has to handle comms directly in <0.12.15
    249         comm = False if bokeh_version < '0.12.15' else comm
--> 250         return super(BokehRenderer, self).components(obj,fmt, comm, **kwargs)
    251 
    252 

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/plotting/renderer.py in components(self, obj, fmt, comm, **kwargs)
    319             plot = obj
    320         else:
--> 321             plot, fmt = self._validate(obj, fmt)
    322 
    323         data, metadata = {}, {}

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/plotting/renderer.py in _validate(self, obj, fmt, **kwargs)
    218         if isinstance(obj, tuple(self.widgets.values())):
    219             return obj, 'html'
--> 220         plot = self.get_plot(obj, renderer=self, **kwargs)
    221 
    222         fig_formats = self.mode_formats['fig'][self.mode]

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/plotting/bokeh/renderer.py in get_plot(self_or_cls, obj, doc, renderer, **kwargs)
    133             curdoc().theme = self_or_cls.theme
    134         doc.theme = self_or_cls.theme
--> 135         plot = super(BokehRenderer, self_or_cls).get_plot(obj, renderer, **kwargs)
    136         plot.document = doc
    137         return plot

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/plotting/renderer.py in get_plot(self_or_cls, obj, renderer, **kwargs)
    205             init_key = tuple(v if d is None else d for v, d in
    206                              zip(plot.keys[0], defaults))
--> 207             plot.update(init_key)
    208         else:
    209             plot = obj

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/plotting/plot.py in update(self, key)
    612     def update(self, key):
    613         if len(self) == 1 and ((key == 0) or (key == self.keys[0])) and not self.drawn:
--> 614             return self.initialize_plot()
    615         item = self.__getitem__(key)
    616         self.traverse(lambda x: setattr(x, '_updated', True))

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/plotting/bokeh/element.py in initialize_plot(self, ranges, plot, plots, source)
   1278         self.handles['plot'] = plot
   1279 
-> 1280         self._init_glyphs(plot, element, ranges, source)
   1281         if not self.overlaid:
   1282             self._update_plot(key, plot, style_element)

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/plotting/bokeh/element.py in _init_glyphs(self, plot, element, ranges, source)
   1225         else:
   1226             style = self.style[self.cyclic_index]
-> 1227             data, mapping, style = self.get_data(element, ranges, style)
   1228             current_id = element._plot_id
   1229 

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/plotting/bokeh/raster.py in get_data(self, element, ranges, style)
    251                 data[y] = np.array(yc)
    252         else:
--> 253             xc, yc = (element.interface.coords(element, x, edges=True, ordered=True),
    254                       element.interface.coords(element, y, edges=True, ordered=True))
    255 

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/core/data/grid.py in coords(cls, dataset, dim, ordered, expanded, edges)
    234             isedges = False
    235         if edges and not isedges:
--> 236             data = cls._infer_interval_breaks(data)
    237         elif not edges and isedges:
    238             data = data[:-1] + np.diff(data)/2.

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/holoviews/core/data/grid.py in _infer_interval_breaks(cls, coord, axis)
    196         if len(coord) == 0:
    197             return np.array([], dtype=coord.dtype)
--> 198         deltas = 0.5 * np.diff(coord, axis=axis)
    199         first = np.take(coord, [0], axis=axis) - np.take(deltas, [0], axis=axis)
    200         last = np.take(coord, [-1], axis=axis) + np.take(deltas, [-1], axis=axis)

<__array_function__ internals> in diff(*args, **kwargs)

~/miniconda3/envs/nestor-mod/lib/python3.7/site-packages/numpy/lib/function_base.py in diff(a, n, axis, prepend, append)
   1271     op = not_equal if a.dtype == np.bool_ else subtract
   1272     for _ in range(n):
-> 1273         a = op(a[slice1], a[slice2])
   1274 
   1275     return a

UFuncTypeError: ufunc 'subtract' did not contain a loop with signature matching types (dtype('<U1'), dtype('<U1')) -> dtype('<U1')

:QuadMesh   [x,y]   (z)

Yes, it would definitely be nice if HeatMap could handle this and there is this issue to address that. As a temporary workaround you can use integer values for the categorical values and then provide explicit xticks to override the tick labels, e.g. .opts(xticks=[(0, 'A'), (1, 'B'), ...]).