holoviews icon indicating copy to clipboard operation
holoviews copied to clipboard

DynamicMap fails when dtype changes on axis

Open GitRay opened this issue 1 year ago • 4 comments

I posted this as a question on Discourse a week ago, but did not get a response. I've played around some more in the last week and am pretty sure it is a bug and not usage error, so I'm posting it here.

Windows 10 Enterprise setup: Chrome 104.0.5112.102 Python 3.9.13 Holoviews 1.15.0 Bokeh 2.4.3 jupyter_bokeh 3.0.4 jupyterlab 3.4.5

Discourse writeup

The above Discourse writeup has lots of detail, but I'll put a more concise version here.

If I create a simple DynamicMap where the only kdim is the column name of a DataFrame, I expect to be able to switch between column names and have the graph update the Y axis with the data in this column. This works fine as long as the data is of the same dtype that was originally used to construct the graph. If columns have mixed dtypes, the DynamicMap does not correctly update the axis type and so the render fails.

Here is a sample of code which exhibits the behavior. This is the worst case, where changing to the column "class" results in an Exception. Most cases just result in undesired behavior of the chart.

import holoviews as hv
#hv.extension('matplotlib')
hv.extension('bokeh')
import pandas as pd

# creating a dataframe with both an int and an obj dtype
df = pd.DataFrame({
    'count':    [ 0,    1,    2 ], 
    'distance': [ 3,    4,    7 ],
    'class':    ['A',  'B',  'C']
})

# set up a DynamicMap where the user can select the y_axis column
y_dim = hv.Dimension('y_column', values=df.columns.tolist(), default='distance')
def return_scatter(col_name):
    return hv.Scatter(df, kdims='count', vdims=[col_name])
hv.DynamicMap(return_scatter, kdims=y_dim).opts(framewise=True, axiswise=True)

The above code results in an exception when "class" is selected: Traceback (most recent call last): File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\pyviz_comms_init_.py", line 338, in _handle_msg self._on_msg(msg) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\panel\viewable.py", line 292, in _on_msg doc.unhold() File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\bokeh\document\document.py", line 799, in unhold self.callbacks.unhold() File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\bokeh\document\callbacks.py", line 396, in unhold self.trigger_on_change(event) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\bokeh\document\callbacks.py", line 373, in trigger_on_change invoke_with_curdoc(doc, event.callback_invoker) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\bokeh\document\callbacks.py", line 408, in invoke_with_curdoc return f() File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\bokeh\util\callback_manager.py", line 191, in invoke callback(attr, old, new) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\panel\reactive.py", line 392, in _comm_change self._schedule_change(doc, comm) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\panel\reactive.py", line 376, in _schedule_change self._change_event(doc) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\panel\reactive.py", line 370, in _change_event self._process_events(events) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\panel\reactive.py", line 315, in process_events self.param.update(**self_events) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\param\parameterized.py", line 1898, in update self._batch_call_watchers() File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\param\parameterized.py", line 2059, in batch_call_watchers self._execute_watcher(watcher, events) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\param\parameterized.py", line 2021, in _execute_watcher watcher.fn(*args, kwargs) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\panel\pane\holoviews.py", line 226, in _widget_callback self._update_plot(plot, pane) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\panel\pane\holoviews.py", line 208, in _update_plot plot.update(key) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\holoviews\plotting\plot.py", line 949, in update item = self.getitem(key) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\holoviews\plotting\plot.py", line 435, in getitem self.update_frame(frame) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\holoviews\plotting\bokeh\element.py", line 1520, in update_frame self._update_ranges(style_element, ranges) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\holoviews\plotting\bokeh\element.py", line 912, in _update_ranges self._update_range(y_range, b, t, yfactors, self.invert_yaxis, File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\holoviews\plotting\bokeh\element.py", line 950, in _update_range axis_range.update({k:new}) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\bokeh\core\has_props.py", line 413, in update setattr(self, k, v) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\bokeh\core\has_props.py", line 230, in setattr return super().setattr(name, value) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\bokeh\core\property\descriptors.py", line 283, in set value = self.property.prepare_value(obj, self.name, value) File "C:\Users\rcathcart.conda\envs\py39\lib\site-packages\bokeh\core\property\bases.py", line 365, in prepare_value raise ValueError(f"failed to validate {obj_repr}.{name}: {error}") ValueError: failed to validate Range1d(id='1005', ...).start: expected an element of either Float, Datetime or TimeDelta, got 'A'

https://user-images.githubusercontent.com/1892276/186446544-45fa38e9-e587-4f4e-a43c-03631a976659.mp4

GitRay avatar Aug 24 '22 14:08 GitRay

DynamicMap reuses the existing plot layout, which is not possible going from integer -> string with the Bokeh backend, which is why you see the error.

A way to change this can be done with panel like this

import holoviews as hv
import pandas as pd
import panel as pn

hv.extension("bokeh")

# creating a dataframe with both an int and an obj dtype
df = pd.DataFrame({"count": [0, 1, 2], "distance": [3, 4, 7], "class": ["A", "B", "C"]})

y_dims = pn.widgets.Select(options=df.columns.tolist(), value="distance")

@pn.depends(y_dims)
def return_scatter(col_name):
    return hv.Scatter(df, kdims="count", vdims=[col_name])

pn.Column(y_dims, return_scatter)

https://user-images.githubusercontent.com/19758978/186451649-55e6d24d-3706-4c7b-b0a9-db1540b89bfe.mp4

It is properly also to archive something similar by using hv.Dimension,

hoxbro avatar Aug 24 '22 14:08 hoxbro

Thank you for that Panel example.

I should mention that changing the backend from bokeh to matplotlib: hv.extension('matplotlib')

Also results in broken behavior, as the axis type does not update. In a way I prefer the explicit failure of the bokeh backend, though even that does not always result in an exception - the exception is only present when switching from numeric to categorical and not the other way around.

GitRay avatar Aug 24 '22 15:08 GitRay

I agree that the matplotlib example should raise an error.

It makes sense to me that it works if the starting option is categorial, as integers can also be put into categories.

hoxbro avatar Aug 24 '22 15:08 hoxbro

Yes, I agree. Integers and categories are compatible. Other combinations don't work gracefully.

GitRay avatar Aug 24 '22 18:08 GitRay