holoviz / holoviews

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

ValueError when .opts(xticks=[]) #6112

Open msrasmussen opened 5 months ago

msrasmussen commented 5 months ago

ALL software version info

(this library, plus any other relevant software, e.g. bokeh, python, notebook, OS, browser, etc) holoviews: main branch 9ac5d350b2c711e498d9075832045f538c975757 python: 3.11.6 bokeh: 3.3.4 pandas: 2.2.0 jupyterlab: 4.1.0 OS: Arch Linux browser: Safari 17.3 @ MacOS 14.3

Description of expected behavior and the observed behavior

Specifying an empty list of xticks for a plot should be ignored instead of failing with a ValueError. The problem seems to be that an empty list is accepted by isinstance(ticker, (tuple, list)) while zip() doesn't. The below change appears to solve the problem although it might not be the correct solution.

--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -899,7 +899,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
                 axis_props['ticker'] = ticker
             elif isinstance(ticker, int):
                 axis_props['ticker'] = BasicTicker(desired_num_ticks=ticker)
-            elif isinstance(ticker, (tuple, list)):
+            elif isinstance(ticker, (tuple, list)) and ticker:
                 if all(isinstance(t, tuple) for t in ticker):
                     ticks, labels = zip(*ticker)
                     # Ensure floats which are integers are serialized as ints

Complete, minimal, self-contained example code that reproduces the issue

import pandas as pd
import holoviews as hv
hv.extension('bokeh')
df = pd.DataFrame({'a': [1, 2, 3, 4]})
hv.Curve(df).opts(xticks=[])

Stack traceback and/or browser JavaScript console output


ValueError Traceback (most recent call last) File ~/work/lisa/.lisa-venv-3.11/lib/python3.11/site-packages/IPython/core/formatters.py:974, 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 ~/work/trees/holoviews/holoviews/core/dimension.py:1286, in Dimensioned._reprmimebundle(self, include, exclude) 1279 def _reprmimebundle(self, include=None, exclude=None): 1280 """ 1281 Resolves the class hierarchy for the class rendering the 1282 object using any display hooks registered on Store.display 1283 hooks. The output of all registered display_hooks is then 1284 combined and returned. 1285 """ -> 1286 return Store.render(self)

File ~/work/trees/holoviews/holoviews/core/options.py:1428, in Store.render(cls, obj) 1426 data, metadata = {}, {} 1427 for hook in hooks: -> 1428 ret = hook(obj) 1429 if ret is None: 1430 continue

File ~/work/trees/holoviews/holoviews/ipython/display_hooks.py:287, in pprint_display(obj) 285 if not ip.display_formatter.formatters['text/plain'].pprint: 286 return None --> 287 return display(obj, raw_output=True)

File ~/work/trees/holoviews/holoviews/ipython/display_hooks.py:255, in display(obj, raw_output, **kwargs) 253 elif isinstance(obj, (CompositeOverlay, ViewableElement)): 254 with option_state(obj): --> 255 output = element_display(obj) 256 elif isinstance(obj, (Layout, NdLayout, AdjointLayout)): 257 with option_state(obj):

File ~/work/trees/holoviews/holoviews/ipython/display_hooks.py:149, 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 ~/work/trees/holoviews/holoviews/ipython/display_hooks.py:195, in element_display(element, max_frames) 192 if type(element) not in Store.registry[backend]: 193 return None --> 195 return render(element)

File ~/work/trees/holoviews/holoviews/ipython/display_hooks.py:76, in render(obj, kwargs) 73 if renderer.fig == 'pdf': 74 renderer = renderer.instance(fig='png') ---> 76 return renderer.components(obj, kwargs)

File ~/work/trees/holoviews/holoviews/plotting/renderer.py:396, in Renderer.components(self, obj, fmt, comm, **kwargs) 393 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 ~/work/trees/holoviews/holoviews/plotting/renderer.py:403, 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 ~/work/lisa/.lisa-venv-3.11/lib/python3.11/site-packages/panel/viewable.py:748, in Viewable._render_model(self, doc, comm) 746 if comm is None: 747 comm = state._comm_manager.get_server_comm() --> 748 model = self.get_root(doc, comm) 750 if self._design and self._design.theme.bokeh_theme: 751 doc.theme = self._design.theme.bokeh_theme

File ~/work/lisa/.lisa-venv-3.11/lib/python3.11/site-packages/panel/layout/base.py:311, in Panel.get_root(self, doc, comm, preprocess) 307 def get_root( 308 self, doc: Optional[Document] = None, comm: Optional[Comm] = None, 309 preprocess: bool = True 310 ) -> Model: --> 311 root = super().get_root(doc, comm, preprocess) 312 # ALERT: Find a better way to handle this 313 if hasattr(root, 'styles') and 'overflow-x' in root.styles:

File ~/work/lisa/.lisa-venv-3.11/lib/python3.11/site-packages/panel/viewable.py:666, 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 ~/work/lisa/.lisa-venv-3.11/lib/python3.11/site-packages/panel/layout/base.py:177, in Panel._get_model(self, doc, root, parent, comm) 175 root = root or model 176 self.models[root.ref['id']] = (model, parent) --> 177 objects, = self._get_objects(model, [], doc, root, comm) 178 props = self._get_properties(doc) 179 props[self._property_mapping['objects']] = objects

File ~/work/lisa/.lisa-venv-3.11/lib/python3.11/site-packages/panel/layout/base.py:159, in Panel._get_objects(self, model, old_objects, doc, root, comm) 157 else: 158 try: --> 159 child = pane._get_model(doc, root, model, comm) 160 except RerenderError as e: 161 if e.layout is not None and e.layout is not self:

File ~/work/lisa/.lisa-venv-3.11/lib/python3.11/site-packages/panel/pane/holoviews.py:423, in HoloViews._get_model(self, doc, root, parent, comm) 421 plot = self.object 422 else: --> 423 plot = self._render(doc, comm, root) 425 plot.pane = self 426 backend = plot.renderer.backend

File ~/work/lisa/.lisa-venv-3.11/lib/python3.11/site-packages/panel/pane/holoviews.py:518, in HoloViews._render(self, doc, comm, root) 515 if comm: 516 kwargs['comm'] = comm --> 518 return renderer.get_plot(self.object, **kwargs)

File ~/work/trees/holoviews/holoviews/plotting/bokeh/renderer.py:68, 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 ~/work/trees/holoviews/holoviews/plotting/renderer.py:240, in Renderer.get_plot(self_or_cls, obj, doc, renderer, comm, **kwargs) 237 defaults = [kd.default for kd in plot.dimensions] 238 init_key = tuple(v if d is None else d for v, d in 239 zip(plot.keys[0], defaults)) --> 240 plot.update(init_key) 241 else: 242 plot = obj

File ~/work/trees/holoviews/holoviews/plotting/plot.py:955, in DimensionedPlot.update(self, key) 953 def update(self, key): 954 if len(self) == 1 and key in (0, self.keys[0]) and not self.drawn: --> 955 return self.initialize_plot() 956 item = self.getitem(key) 957 self.traverse(lambda x: setattr(x, '_updated', True))

File ~/work/trees/holoviews/holoviews/plotting/bokeh/element.py:1923, in ElementPlot.initialize_plot(self, ranges, plot, plots, source) 1921 self._init_glyphs(plot, element, ranges, source) 1922 if not self.overlaid: -> 1923 self._update_plot(key, plot, style_element) 1924 self._update_ranges(style_element, ranges) 1926 for cb in self.callbacks:

File ~/work/trees/holoviews/holoviews/plotting/bokeh/element.py:982, in ElementPlot._update_plot(self, key, plot, element) 980 plot.update(**self._plot_properties(key, element)) 981 if not self.multi_y: --> 982 self._update_labels(key, plot, element) 983 self._update_title(key, plot, element) 984 self._update_grid(plot)

File ~/work/trees/holoviews/holoviews/plotting/bokeh/element.py:990, in ElementPlot._update_labels(self, key, plot, element) 988 el = el[0] if el else element 989 dimensions = self._get_axis_dims(el) --> 990 props = {axis: self._axis_properties(axis, key, plot, dim) 991 for axis, dim in zip(['x', 'y'], dimensions)} 992 xlabel, ylabel, zlabel = self._get_axis_labels(dimensions) 993 if self.invert_axes:

File ~/work/trees/holoviews/holoviews/plotting/bokeh/element.py:990, in (.0) 988 el = el[0] if el else element 989 dimensions = self._get_axis_dims(el) --> 990 props = {axis: self._axis_properties(axis, key, plot, dim) 991 for axis, dim in zip(['x', 'y'], dimensions)} 992 xlabel, ylabel, zlabel = self._get_axis_labels(dimensions) 993 if self.invert_axes:

File ~/work/trees/holoviews/holoviews/plotting/bokeh/element.py:904, in ElementPlot._axis_properties(self, axis, key, plot, dimension, ax_mapping) 902 elif isinstance(ticker, (tuple, list)): 903 if all(isinstance(t, tuple) for t in ticker): --> 904 ticks, labels = zip(*ticker) 905 # Ensure floats which are integers are serialized as ints 906 # because in JS the lookup fails otherwise 907 ticks = [int(t) if isinstance(t, float) and t.is_integer() else t 908 for t in ticks]

ValueError: not enough values to unpack (expected 2, got 0)

Screenshots or screencasts of the bug in action

hoxbro commented 5 months ago

Will hv.Curve(df).opts(xticks=0) suffice?

msrasmussen commented 5 months ago

Will hv.Curve(df).opts(xticks=0) suffice?

Thinking more about it, the expected behaviour ought to raising a more helpful error. Setting ticks to 0 or [] resulting in default ticks to be plotted is not what people would expect I think.

Alternatively, an 0 or [] could remove all ticks.

hoxbro commented 5 months ago

0 removes all ticks.

I agree that [] should raise a better exception, likely right under your suggested change.