holoviz / holoviews

With Holoviews, your data visualizes itself.
https://holoviews.org
BSD 3-Clause "New" or "Revised" License
2.69k stars 403 forks source link

Categorical datashaded paths can raise an error #6326

Open maximlt opened 2 months ago

maximlt commented 2 months ago

The code below:

With some pretty old versions of HoloViews, Datashader and Pandas, it works.

image

But it breaks with newer versions with this error ValueError: Dimension name cannot be empty.


import datashader as ds
import numpy as np
import pandas as pd
import holoviews as hv

from holoviews.operation import datashader as hd

hv.extension('bokeh')

spaths = [
    np.array([[1, 2], [3, 4], [5, 6]]),
    np.array([[7, 8], [9, 10]]),
    np.array([[11, 12], [13, 14], [15, 16], [17, 18]]),
    np.array([[19, 20], [21, 22]]),
    np.array([[23, 24], [25, 26]]),
]

nan_row = np.array([[np.nan, np.nan]])
paths = np.concatenate([x for spath in spaths for x in [spath, nan_row]])

df = pd.DataFrame(paths, columns=['x', 'y'])

# CAT_SEP = np.nan
CAT_SEP = ''

origin = []
for s, spath in zip('abcdefgh', spaths):
    for _ in range(len(spath)):
        origin.append(s)
    origin.append(CAT_SEP)
df['origin'] = origin

df['origin'] = df['origin'].astype('category')

path = hv.Path(df, ['x', 'y'])

aggregator = ds.count_cat('origin')
datashaded = hd.datashade(
    path,
    aggregator=aggregator,
)
datashaded.opts(bgcolor='black')

Before:

image
Traceback now

```python > / Name Modified File Size spaths = [ np.array([[1, 2], [3, 4], [5, 6]]), np.array([[7, 8], [9, 10]]), np.array([[11, 12], [13, 14], [15, 16], [17, 18]]), np.array([[19, 20], [21, 22]]), np.array([[23, 24], [25, 26]]), ] nan_row = np.array([[np.nan, np.nan]]) paths = np.concatenate([x for spath in spaths for x in [spath, nan_row]]) df = pd.DataFrame(paths, columns=['x', 'y']) # CAT_SEP = np.nan CAT_SEP = '' origin = [] for s, spath in zip('abcdefgh', spaths): for _ in range(len(spath)): origin.append(s) origin.append(CAT_SEP) df['origin'] = origin df['origin'] = df['origin'].astype('category') path = hv.Path(df, ['x', 'y']) path aggregator = ds.count_cat('origin') datashaded = hd.datashade( path, aggregator=aggregator, ) datashaded.opts(bgcolor='black') WARNING:param.dynamic_operation: Callable raised "ValueError('Dimension name cannot be empty')". Invoked as dynamic_operation(height=400, scale=1.0, width=400, x_range=None, y_range=None) WARNING:param.dynamic_operation: Callable raised "ValueError('Dimension name cannot be empty')". Invoked as dynamic_operation(height=400, scale=1.0, width=400, x_range=None, y_range=None) --------------------------------------------------------------------------- ValueError Traceback (most recent call last) File [~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/IPython/core/formatters.py:974](http://localhost:8890/lab/tree/~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/IPython/core/formatters.py#line=973), in MimeBundleFormatter.__call__(self, obj, include, exclude) 971 method = get_real_method(obj, self.print_method) 973 if method is not None: --> 974 return method(include=include, exclude=exclude) 975 return None 976 else: File [~/dev/holoviews/holoviews/core/dimension.py:1275](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/dimension.py#line=1274), in Dimensioned._repr_mimebundle_(self, include, exclude) 1268 def _repr_mimebundle_(self, include=None, exclude=None): 1269 """ 1270 Resolves the class hierarchy for the class rendering the 1271 object using any display hooks registered on Store.display 1272 hooks. The output of all registered display_hooks is then 1273 combined and returned. 1274 """ -> 1275 return Store.render(self) File [~/dev/holoviews/holoviews/core/options.py:1423](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/options.py#line=1422), in Store.render(cls, obj) 1421 data, metadata = {}, {} 1422 for hook in hooks: -> 1423 ret = hook(obj) 1424 if ret is None: 1425 continue File [~/dev/holoviews/holoviews/ipython/display_hooks.py:287](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/ipython/display_hooks.py#line=286), in pprint_display(obj) 285 if not ip.display_formatter.formatters['text[/plain](http://localhost:8890/plain)'].pprint: 286 return None --> 287 return display(obj, raw_output=True) File [~/dev/holoviews/holoviews/ipython/display_hooks.py:261](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/ipython/display_hooks.py#line=260), in display(obj, raw_output, **kwargs) 259 elif isinstance(obj, (HoloMap, DynamicMap)): 260 with option_state(obj): --> 261 output = map_display(obj) 262 elif isinstance(obj, Plot): 263 output = render(obj) File [~/dev/holoviews/holoviews/ipython/display_hooks.py:149](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/ipython/display_hooks.py#line=148), in display_hook..wrapped(element) 147 try: 148 max_frames = OutputSettings.options['max_frames'] --> 149 mimebundle = fn(element, max_frames=max_frames) 150 if mimebundle is None: 151 return {}, {} File [~/dev/holoviews/holoviews/ipython/display_hooks.py:209](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/ipython/display_hooks.py#line=208), in map_display(vmap, max_frames) 206 max_frame_warning(max_frames) 207 return None --> 209 return render(vmap) File [~/dev/holoviews/holoviews/ipython/display_hooks.py:76](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/ipython/display_hooks.py#line=75), in render(obj, **kwargs) 73 if renderer.fig == 'pdf': 74 renderer = renderer.instance(fig='png') ---> 76 return renderer.components(obj, **kwargs) File [~/dev/holoviews/holoviews/plotting/renderer.py:396](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/plotting/renderer.py#line=395), in Renderer.components(self, obj, fmt, comm, **kwargs) 394 embed = (not (dynamic or streams or self.widget_mode == 'live') or config.embed) 395 if embed or config.comms == 'default': --> 396 return self._render_panel(plot, embed, comm) 397 return self._render_ipywidget(plot) File [~/dev/holoviews/holoviews/plotting/renderer.py:403](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/plotting/renderer.py#line=402), in Renderer._render_panel(self, plot, embed, comm) 401 doc = Document() 402 with config.set(embed=embed): --> 403 model = plot.layout._render_model(doc, comm) 404 if embed: 405 return render_model(model, comm) File [~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/viewable.py:735](http://localhost:8890/lab/tree/~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/viewable.py#line=734), in Viewable._render_model(self, doc, comm) 733 if comm is None: 734 comm = state._comm_manager.get_server_comm() --> 735 model = self.get_root(doc, comm) 737 if self._design and self._design.theme.bokeh_theme: 738 doc.theme = self._design.theme.bokeh_theme File [~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/layout/base.py:319](http://localhost:8890/lab/tree/~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/layout/base.py#line=318), in Panel.get_root(self, doc, comm, preprocess) 315 def get_root( 316 self, doc: Optional[Document] = None, comm: Optional[Comm] = None, 317 preprocess: bool = True 318 ) -> Model: --> 319 root = super().get_root(doc, comm, preprocess) 320 # ALERT: Find a better way to handle this 321 if hasattr(root, 'styles') and 'overflow-x' in root.styles: File [~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/viewable.py:666](http://localhost:8890/lab/tree/~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/viewable.py#line=665), in Renderable.get_root(self, doc, comm, preprocess) 664 wrapper = self._design._wrapper(self) 665 if wrapper is self: --> 666 root = self._get_model(doc, comm=comm) 667 if preprocess: 668 self._preprocess(root) File [~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/layout/base.py:185](http://localhost:8890/lab/tree/~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/layout/base.py#line=184), in Panel._get_model(self, doc, root, parent, comm) 183 root = root or model 184 self._models[root.ref['id']] = (model, parent) --> 185 objects, _ = self._get_objects(model, [], doc, root, comm) 186 props = self._get_properties(doc) 187 props[self._property_mapping['objects']] = objects File [~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/layout/base.py:167](http://localhost:8890/lab/tree/~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/layout/base.py#line=166), in Panel._get_objects(self, model, old_objects, doc, root, comm) 165 else: 166 try: --> 167 child = pane._get_model(doc, root, model, comm) 168 except RerenderError as e: 169 if e.layout is not None and e.layout is not self: File [~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/pane/holoviews.py:429](http://localhost:8890/lab/tree/~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/pane/holoviews.py#line=428), in HoloViews._get_model(self, doc, root, parent, comm) 427 plot = self.object 428 else: --> 429 plot = self._render(doc, comm, root) 431 plot.pane = self 432 backend = plot.renderer.backend File [~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/pane/holoviews.py:525](http://localhost:8890/lab/tree/~/dev/holoviews/.pixi/envs/test-39/lib/python3.9/site-packages/panel/pane/holoviews.py#line=524), in HoloViews._render(self, doc, comm, root) 522 if comm: 523 kwargs['comm'] = comm --> 525 return renderer.get_plot(self.object, **kwargs) File [~/dev/holoviews/holoviews/plotting/bokeh/renderer.py:68](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/plotting/bokeh/renderer.py#line=67), in BokehRenderer.get_plot(self_or_cls, obj, doc, renderer, **kwargs) 61 @bothmethod 62 def get_plot(self_or_cls, obj, doc=None, renderer=None, **kwargs): 63 """ 64 Given a HoloViews Viewable return a corresponding plot instance. 65 Allows supplying a document attach the plot to, useful when 66 combining the bokeh model with another plot. 67 """ ---> 68 plot = super().get_plot(obj, doc, renderer, **kwargs) 69 if plot.document is None: 70 plot.document = Document() if self_or_cls.notebook_context else curdoc() File [~/dev/holoviews/holoviews/plotting/renderer.py:216](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/plotting/renderer.py#line=215), in Renderer.get_plot(self_or_cls, obj, doc, renderer, comm, **kwargs) 213 raise SkipRendering(msg.format(dims=dims)) 215 # Initialize DynamicMaps with first data item --> 216 initialize_dynamic(obj) 218 if not renderer: 219 renderer = self_or_cls File [~/dev/holoviews/holoviews/plotting/util.py:270](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/plotting/util.py#line=269), in initialize_dynamic(obj) 268 continue 269 if not len(dmap): --> 270 dmap[dmap._initial_key()] File [~/dev/holoviews/holoviews/core/spaces.py:1216](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/spaces.py#line=1215), in DynamicMap.__getitem__(self, key) 1214 # Not a cross product and nothing cached so compute element. 1215 if cache is not None: return cache -> 1216 val = self._execute_callback(*tuple_key) 1217 if data_slice: 1218 val = self._dataslice(val, data_slice) File [~/dev/holoviews/holoviews/core/spaces.py:983](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/spaces.py#line=982), in DynamicMap._execute_callback(self, *args) 980 kwargs['_memoization_hash_'] = hash_items 982 with dynamicmap_memoization(self.callback, self.streams): --> 983 retval = self.callback(*args, **kwargs) 984 return self._style(retval) File [~/dev/holoviews/holoviews/core/spaces.py:581](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/spaces.py#line=580), in Callable.__call__(self, *args, **kwargs) 578 args, kwargs = (), dict(pos_kwargs, **kwargs) 580 try: --> 581 ret = self.callable(*args, **kwargs) 582 except KeyError: 583 # KeyError is caught separately because it is used to signal 584 # invalid keys on DynamicMap and should not warn 585 raise File [~/dev/holoviews/holoviews/util/__init__.py:1038](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/util/__init__.py#line=1037), in Dynamic._dynamic_operation..dynamic_operation(*key, **kwargs) 1037 def dynamic_operation(*key, **kwargs): -> 1038 key, obj = resolve(key, kwargs) 1039 return apply(obj, *key, **kwargs) File [~/dev/holoviews/holoviews/util/__init__.py:1027](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/util/__init__.py#line=1026), in Dynamic._dynamic_operation..resolve(key, kwargs) 1025 elif isinstance(map_obj, DynamicMap) and map_obj._posarg_keys and not key: 1026 key = tuple(kwargs[k] for k in map_obj._posarg_keys) -> 1027 return key, map_obj[key] File [~/dev/holoviews/holoviews/core/spaces.py:1216](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/spaces.py#line=1215), in DynamicMap.__getitem__(self, key) 1214 # Not a cross product and nothing cached so compute element. 1215 if cache is not None: return cache -> 1216 val = self._execute_callback(*tuple_key) 1217 if data_slice: 1218 val = self._dataslice(val, data_slice) File [~/dev/holoviews/holoviews/core/spaces.py:983](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/spaces.py#line=982), in DynamicMap._execute_callback(self, *args) 980 kwargs['_memoization_hash_'] = hash_items 982 with dynamicmap_memoization(self.callback, self.streams): --> 983 retval = self.callback(*args, **kwargs) 984 return self._style(retval) File [~/dev/holoviews/holoviews/core/spaces.py:581](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/spaces.py#line=580), in Callable.__call__(self, *args, **kwargs) 578 args, kwargs = (), dict(pos_kwargs, **kwargs) 580 try: --> 581 ret = self.callable(*args, **kwargs) 582 except KeyError: 583 # KeyError is caught separately because it is used to signal 584 # invalid keys on DynamicMap and should not warn 585 raise File [~/dev/holoviews/holoviews/util/__init__.py:1039](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/util/__init__.py#line=1038), in Dynamic._dynamic_operation..dynamic_operation(*key, **kwargs) 1037 def dynamic_operation(*key, **kwargs): 1038 key, obj = resolve(key, kwargs) -> 1039 return apply(obj, *key, **kwargs) File [~/dev/holoviews/holoviews/util/__init__.py:1031](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/util/__init__.py#line=1030), in Dynamic._dynamic_operation..apply(element, *key, **kwargs) 1029 def apply(element, *key, **kwargs): 1030 kwargs = dict(util.resolve_dependent_kwargs(self.p.kwargs), **kwargs) -> 1031 processed = self._process(element, key, kwargs) 1032 if (self.p.link_dataset and isinstance(element, Dataset) and 1033 isinstance(processed, Dataset) and processed._dataset is None): 1034 processed._dataset = element.dataset File [~/dev/holoviews/holoviews/util/__init__.py:1013](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/util/__init__.py#line=1012), in Dynamic._process(self, element, key, kwargs) 1011 elif isinstance(self.p.operation, Operation): 1012 kwargs = {k: v for k, v in kwargs.items() if k in self.p.operation.param} -> 1013 return self.p.operation.process_element(element, key, **kwargs) 1014 else: 1015 return self.p.operation(element, **kwargs) File [~/dev/holoviews/holoviews/core/operation.py:194](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/operation.py#line=193), in Operation.process_element(self, element, key, **params) 191 else: 192 self.p = param.ParamOverrides(self, params, 193 allow_extra_keywords=self._allow_extra_keywords) --> 194 return self._apply(element, key) File [~/dev/holoviews/holoviews/core/operation.py:141](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/operation.py#line=140), in Operation._apply(self, element, key) 139 if not in_method: 140 element._in_method = True --> 141 ret = self._process(element, key) 142 if hasattr(element, '_in_method') and not in_method: 143 element._in_method = in_method File [~/dev/holoviews/holoviews/operation/datashader.py:1542](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/operation/datashader.py#line=1541), in datashade._process(self, element, key) 1541 def _process(self, element, key=None): -> 1542 agg = rasterize._process(self, element, key) 1543 shaded = shade._process(self, agg, key) 1544 return shaded File [~/dev/holoviews/holoviews/operation/datashader.py:1522](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/operation/datashader.py#line=1521), in rasterize._process(self, element, key) 1519 op = transform.instance(**{k:v for k,v in extended_kws.items() 1520 if k in transform.param}) 1521 op._precomputed = self._precomputed -> 1522 element = element.map(op, predicate) 1523 self._precomputed = op._precomputed 1525 unused_params = list(all_supplied_kws - all_allowed_kws) File [~/dev/holoviews/holoviews/core/data/__init__.py:196](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/data/__init__.py#line=195), in PipelineMeta.pipelined..pipelined_fn(*args, **kwargs) 193 inst._in_method = True 195 try: --> 196 result = method_fn(*args, **kwargs) 197 if PipelineMeta.disable: 198 return result File [~/dev/holoviews/holoviews/core/data/__init__.py:1213](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/data/__init__.py#line=1212), in Dataset.map(self, *args, **kwargs) 1211 @wraps(LabelledData.map) 1212 def map(self, *args, **kwargs): -> 1213 return super().map(*args, **kwargs) File [~/dev/holoviews/holoviews/core/dimension.py:695](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/dimension.py#line=694), in LabelledData.map(self, map_fn, specs, clone) 693 return deep_mapped 694 else: --> 695 return map_fn(self) if applies else self File [~/dev/holoviews/holoviews/core/operation.py:214](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/operation.py#line=213), in Operation.__call__(self, element, **kwargs) 210 return element.clone([(k, self._apply(el, key=k)) 211 for k, el in element.items()]) 212 elif ((self._per_element and isinstance(element, Element)) or 213 (not self._per_element and isinstance(element, ViewableElement))): --> 214 return self._apply(element) 215 elif 'streams' not in kwargs: 216 kwargs['streams'] = self.p.streams File [~/dev/holoviews/holoviews/core/operation.py:141](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/operation.py#line=140), in Operation._apply(self, element, key) 139 if not in_method: 140 element._in_method = True --> 141 ret = self._process(element, key) 142 if hasattr(element, '_in_method') and not in_method: 143 element._in_method = in_method File [~/dev/holoviews/holoviews/operation/datashader.py:412](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/operation/datashader.py#line=411), in aggregate._process(self, element, key) 410 else: 411 params['vdims'] = list(map(str, agg.coords[agg_fn.column].data)) --> 412 return ImageStack(agg, **params) File [~/dev/holoviews/holoviews/element/raster.py:574](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/element/raster.py#line=573), in ImageStack.__init__(self, data, kdims, vdims, **params) 572 elif isinstance(data, dict): 573 vdims = [Dimension(key) for key in data.keys() if key not in _kdims] --> 574 super().__init__(data, kdims=kdims, vdims=vdims, **params) File [~/dev/holoviews/holoviews/element/raster.py:279](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/element/raster.py#line=278), in Image.__init__(self, data, kdims, vdims, bounds, extents, xdensity, ydensity, rtol, **params) 276 else: 277 params['rtol'] = config.image_rtol --> 279 Dataset.__init__(self, data, kdims=kdims, vdims=vdims, extents=extents, **params) 280 if not self.interface.gridded: 281 raise DataError( 282 f"{type(self).__name__} type expects gridded data, " 283 f"{self.interface.__name__} is columnar. " 284 "To display columnar data as gridded use the HeatMap " 285 "element or aggregate the data (e.g. using np.histogram2d)." 286 ) File [~/dev/holoviews/holoviews/core/data/__init__.py:325](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/data/__init__.py#line=324), in Dataset.__init__(self, data, kdims, vdims, **kwargs) 322 if input_transforms is None: 323 input_transforms = data._transforms --> 325 kwargs.update(process_dimensions(kdims, vdims)) 326 kdims, vdims = kwargs.get('kdims'), kwargs.get('vdims') 328 validate_vdims = kwargs.pop('_validate_vdims', True) File [~/dev/holoviews/holoviews/core/dimension.py:117](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/dimension.py#line=116), in process_dimensions(kdims, vdims) 110 elif not isinstance(dims, list): 111 raise ValueError( 112 f"{group} argument expects a Dimension or list of dimensions, " 113 "specified as tuples, strings, dictionaries or Dimension " 114 f"instances, not a {type(dims).__name__} type. " 115 "Ensure you passed the data as the first argument." 116 ) --> 117 dimensions[group] = [asdim(d) for d in dims] 118 return dimensions File [~/dev/holoviews/holoviews/core/dimension.py:117](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/dimension.py#line=116), in (.0) 110 elif not isinstance(dims, list): 111 raise ValueError( 112 f"{group} argument expects a Dimension or list of dimensions, " 113 "specified as tuples, strings, dictionaries or Dimension " 114 f"instances, not a {type(dims).__name__} type. " 115 "Ensure you passed the data as the first argument." 116 ) --> 117 dimensions[group] = [asdim(d) for d in dims] 118 return dimensions File [~/dev/holoviews/holoviews/core/dimension.py:60](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/dimension.py#line=59), in asdim(dimension) 50 def asdim(dimension): 51 """Convert the input to a Dimension. 52 53 Args: (...) 58 copy is performed if the input is already a Dimension. 59 """ ---> 60 return dimension if isinstance(dimension, Dimension) else Dimension(dimension) File [~/dev/holoviews/holoviews/core/dimension.py:276](http://localhost:8890/lab/tree/~/dev/holoviews/holoviews/core/dimension.py#line=275), in Dimension.__init__(self, spec, **params) 273 all_params.update(params) 275 if not all_params['name']: --> 276 raise ValueError('Dimension name cannot be empty') 277 if not all_params['label']: 278 raise ValueError('Dimension label cannot be empty') ValueError: Dimension name cannot be empty ```

philippjfr commented 1 month ago

So the proximate cause here is that while this used to work I actually suspect it didn't work as desired. The problem now is that the individual categories now become the dimensions of the ImageStack while previously they were merely the values of the kdim of the NdOverlay that would hold the individual layers. The real problem/question here is how the empty strings should be treated, specifically HoloViews upgrades all unique non-NaN values along the count_cat dimension to categories and the resulting DataArray will then hold all these categories INCLUDING the empty string category. This doesn't actually cause any rendering artifacts because all the empty string category coordinates are NaNs but apparently we have always created this bogus category in this case.

I suspect we should either automatically convert the empty strings to NaNs so they are not treated as a valid category OR we should issue an exception indicating that '' is not a valid category value.

philippjfr commented 1 month ago

The easiest fix might be to simply filter out the empty string category from the ImageStack dimensions.