Dynamically enable/disable DataShader with Panel Switch Widget

I am making an application that involves interacting with large dataset (~750k points) and finding interesting subsets (100-200 points) to analyze. I would like to allow the user to decide whether to enable or disable datashader. I built a little toy demonstrating what I want:

import panel as pn
import holoviews as hv
import datashader
import holoviews.operation.datashader
import numpy as np
hv.extension('bokeh')

should_datashade_start = True

def plot_my_points(color_method, should_datashade):
    x = np.arange(-4, 4, 0.01)
    y = x**2
    if color_method == 'ascending':
        z = np.arange(0, len(x))
    elif color_method == 'descending':
        z = np.flip(np.arange(0, len(x)))
    elif color_method == 'random':
        z = np.random.rand(*x.shape)
    points = hv.Points((x, y, z), kdims=['x', 'y'], vdims=['height'])
    if should_datashade:
        shaded = hv.operation.datashader.datashade(points, aggregator=datashader.max('height'), cmap='plasma', dynamic=False)
    else:  
        shaded = points.opts(hv.opts.Points(color='height', cmap='plasma'))
    return shaded

drop_down = pn.widgets.Select(name='Color By', options=['ascending', 'descending', 'random'], value='ascending')
datashade_switch = pn.widgets.Switch(value=should_datashade_start)

pts = hv.DynamicMap(pn.bind(plot_my_points, color_method=drop_down, should_datashade=datashade_switch, watch=True))

col = pn.Column(pn.pane.HoloViews(pts), drop_down, datashade_switch)

Changing “should_datashade_start” to True or False before running the script works fine, but flipping the switch in the browser causes a crash:

Traceback (most recent call last):
  File "/Users/stgardner4/micromamba/envs/interactive-pyxlmadev/lib/python3.11/site-packages/holoviews/plotting/util.py", line 293, in get_plot_frame
    return map_obj[key]
           ~~~~~~~^^^^^
  File "/Users/stgardner4/micromamba/envs/interactive-pyxlmadev/lib/python3.11/site-packages/holoviews/core/spaces.py", line 1219, in __getitem__
    self._cache(tuple_key, val)
  File "/Users/stgardner4/micromamba/envs/interactive-pyxlmadev/lib/python3.11/site-packages/holoviews/core/spaces.py", line 1292, in _cache
    self[key] = val
    ~~~~^^^^^
  File "/Users/stgardner4/micromamba/envs/interactive-pyxlmadev/lib/python3.11/site-packages/holoviews/core/ndmapping.py", line 566, in __setitem__
    self._add_item(key, value, update=False)
  File "/Users/stgardner4/micromamba/envs/interactive-pyxlmadev/lib/python3.11/site-packages/holoviews/core/ndmapping.py", line 165, in _add_item
    self._item_check(dim_vals, data)
  File "/Users/stgardner4/micromamba/envs/interactive-pyxlmadev/lib/python3.11/site-packages/holoviews/core/ndmapping.py", line 986, in _item_check
    raise AssertionError(f"{self.__class__.__name__} must only contain one type of object, not both {type(data).__name__} and {self.type.__name__}.")
AssertionError: DynamicMap must only contain one type of object, not both RGB and Points.

is there a way that I can get around this ‘same types’ issue?

As I was writing the above post, I thought of a jank workaround! Using an overlay of RGB and points, and setting the unwanted one to only contain one point and alpha=0.

def plot_my_points(color_method, should_datashade):
    x = np.arange(-4, 4, 0.01)
    y = x**2
    if color_method == 'ascending':
        z = np.arange(0, len(x))
    elif color_method == 'descending':
        z = np.flip(np.arange(0, len(x)))
    elif color_method == 'random':
        z = np.random.rand(*x.shape)
    points = hv.Points((x, y, z), kdims=['x', 'y'], vdims=['height'])
    if should_datashade:
        fake_points = hv.Points(([0], [0], [0]), kdims=['x', 'y'], vdims=['height']).opts(alpha=0)
        shaded = hv.operation.datashader.datashade(points, aggregator=datashader.max('height'), cmap='plasma', dynamic=False)
        shaded = shaded * fake_points
    else:
        fake_points = hv.Points(([0], [0], [0]), kdims=['x', 'y'], vdims=['height'])
        fake_shaded = hv.operation.datashader.datashade(fake_points, aggregator=datashader.max('height'), cmap='plasma', dynamic=False).opts(alpha=0)
        shaded = points.opts(hv.opts.Points(color='height', cmap='plasma'))
        shaded = shaded * fake_shaded
    return shaded

this gets rid of the crash, but flipping the switch still doesn’t work, as doing so just removes the points from my plot silently with no error message.

pts = hv.DynamicMap(pn.bind(plot_my_points, color_method=drop_down, should_datashade=datashade_switch, watch=True))

I think you should drop the watch=True because it returns a value.

Also, you might be interested in Show individual points when zoomed, else datashade

Thanks for the suggestion, but I think I found an easier way of doing this, just have both layers set and bind the switch to set the visible keyword in .opts

import panel as pn
import holoviews as hv
import datashader
import holoviews.operation.datashader
import numpy as np

hv.extension('bokeh')
pn.extension()

def plot_my_points(color_method, should_datashade):
    x = np.linspace(-4, 4, 100)
    y = x**2
    if color_method == 'ascending':
        z = np.arange(0, len(x))
    elif color_method == 'descending':
        z = np.flip(np.arange(0, len(x)))
    elif color_method == 'random':
        z = np.random.rand(*x.shape)
    points = hv.Points((x, y, z), kdims=['x', 'y'], vdims=['height']).opts(hv.opts.Points(color='height', cmap='plasma', visible=not should_datashade))
    return points

drop_down = pn.widgets.Select(name='Color By', options=['ascending', 'descending', 'random'], value='ascending')
datashade_switch = pn.widgets.Switch(value=True)

pts = hv.DynamicMap(pn.bind(plot_my_points, color_method=drop_down, should_datashade=datashade_switch, watch=True))
pts_shaded = hv.operation.datashader.datashade(pts, aggregator=datashader.max('height'), cmap='cividis', dynamic=True)
pn.bind(pts_shaded.opts, visible=datashade_switch, watch=True)
to_plot = pts * pts_shaded
col = pn.Column(pts*pts_shaded, drop_down, datashade_switch)
pn.serve(col)

This even allows the datashader layer to be dynamic, which I actually wanted and had only set to false because I had seen another post on here suggesting it. The only issue with this is that when changing the datashader layer from visible=False to visible=True, it doesn’t show anything to the user until the user moves the axes and datashader is invoked to re–render. Is there a way to programatically invoke the “datashader do work” code without waiting on the user to move the axes slightly?

.apply.opts() maybe

that didn’t work but this is a bit outside the scope of the original thread, for future posterity’s searchability I’ve made a new one: Datashader change aggregator with panel widget