PointDraw as Parameterized Class

I am working off of the PointDraw example. I would like to display a 3rd plot when a user selects a point or table row.
The intended workflow is:

  1. user selects a dataset
  2. user selects point or row
  3. new plot appears below based on selected point/row
  4. user may delete points/rows

My issues are these:

  1. getting ‘selected’ data from the stream (or scatter or table)
  2. updating original dataset if point/row is deleted with ‘point_draw’
  3. new plot is made from data not shown in this example - a 2D numpy array. The plot should be an hv.Path if ‘lines’ is checked, else a regrid(hv.Image). Any random 2D numpy array will work here.

The codes:

import holoviews as hv
import numpy as np
import param
from holoviews import opts, streams
from holoviews.operation.datashader import regrid
from holoviews.plotting.links import DataLink

import panel as pn

hv.extension('bokeh')

datasets = {
    'data1': np.random.randint(1, 10, size=(4, 2)),
    'data2': np.random.randint(1, 10, size=(4, 2))
}


class Explorer(param.Parameterized):

    dataset = param.ObjectSelector(default=list(datasets.keys())[0],
                                   objects=list(datasets.keys()))
    lines = param.Boolean(default=False)

    @param.depends('dataset')
    def load_file(self):
        name = self.dataset
        datum = datasets[name]
        scatter = hv.Scatter(datum).opts(size=8)
        self.stream = streams.PointDraw(data=scatter.columns(),
                                        source=scatter,
                                        drag=False,
                                        add=False)
        table = hv.Table(scatter).opts(index_position=False,
                                       editable=True,
                                       selectable=True)
        DataLink(scatter, table)

        return (scatter + table).opts(
            opts.Scatter(tools=['tap', 'hover', 'box_select']),
            opts.Table(editable=True))

    '''
    @pn.depends('stream', 'lines') # stream, scatter, table ???
    def view(self):
        1. if no point/row is selected, hide this view
        2. when a point/row is selected:
          a. access selected data (points to 2D np.array not in this example)
          b. if self.lines:
                 return hv.Path(np.array)
             else:
                 return hv.operation.datashader.regrid(hv.Image(np.array),upsample=True,interpolation='bilinear')

    @pn.depends('stream') # stream, scatter, table ???
    def update_data(self):
        1. when an point is deleted with 'point_draw':
          a. update original data

    '''


explorer = Explorer()
pn.Column(explorer.param, explorer.load_file).servable()

Here is some code that should get you closer to what you want. Without knowing exactly what you want to do with your other dataset, I did my best.

The main trick was to define the PointDraw stream and a Selection1D stream as parameterized subobjects of the parent class. This way you can write param.depends statements for the stream params.

import holoviews as hv
import numpy as np
import param
from holoviews import opts, streams
from holoviews.operation.datashader import regrid
from holoviews.plotting.links import DataLink

import panel as pn

hv.extension('bokeh')

datasets = {
    'data1': np.random.randint(1, 10, size=(4, 2)),
    'data2': np.random.randint(1, 10, size=(4, 2))
}

other_data = {i: np.random.randint(1, 10, size=(100, 100)) for i in range(4)}

class Explorer(param.Parameterized):

    dataset = param.ObjectSelector(default=list(datasets.keys())[0],
                                   objects=list(datasets.keys()))
    
    lines = param.Boolean(default=False)
    
    # streams
    stream = param.ClassSelector(
        default = streams.PointDraw(drag=False, add=False), 
        class_ = (streams.PointDraw), 
        precedence = -1
    )
    
    selection = param.ClassSelector(
        default = streams.Selection1D(), 
        class_=(streams.Selection1D), 
        precedence = -1
    )

    @param.depends('dataset')
    def load_file(self):
        name = self.dataset
        datum = datasets[name]
        scatter = hv.Scatter(datum).opts(size=8)
        
        # update the PointDraw/Selection1D sources/data
        self.stream.source = scatter
        self.selection.source = scatter
        self.stream.update(data=scatter.columns()) # reset PointDraw data
        self.selection.update(index = []) # reset selection index
        
        table = hv.Table(scatter).opts(index_position=False,
                                       editable=True,
                                       selectable=True)
        DataLink(scatter, table)

        return (scatter + table).opts(
            opts.Scatter(tools=['tap', 'hover', 'box_select']),
            opts.Table(editable=True))
    
    @param.depends('dataset', 'selection.index', 'lines')
    def view(self):
        '''update 3rd plot whenever dataset, selection.index, or lines is changed'''
        if self.selection.index == []:
            return None
        else:
            # modify with your "other" data
            if self.lines == True:
                return hv.Path(other_data[self.selection.index[0]]).opts(shared_axes=False)
            else:
                return hv.operation.datashader.regrid(hv.Image(other_data[self.selection.index[0]]),upsample=True,interpolation='bilinear').opts(shared_axes=False)
            
    @param.depends('stream.data', watch=True)
    def update_data(self):
        '''update dataset whenever a point is deleted using PointDraw'''
        datasets[self.dataset] = np.vstack((self.stream.data['x'], self.stream.data['y'])).T

explorer = Explorer()
pn.Row(pn.Column(explorer.param, explorer.view), explorer.load_file).servable()

video to show it in action:

Note that this code assumes your selection only ever includes a single point. If you want to use, for example, the box select tool to select more than one point, you’ll need to modify Explorer.view accordingly.

Hopefully this works for your needs.

3 Likes

Brilliant! I had lately incorporated a Selection1D stream but this solution is much more complete & elegant. Thank you.

2 Likes