How to link bokeh active tool (PointDraw) to param.DataFrame in Panel dashboard

I’m struggling to link a bokeh tool and action button to modify a param data source. I’ve tried various approaches (@pn.depends api) but thought using the Class-based approach would be cleanest since all visualizations and actions tie back to an editable dataframe.

My goal is to have a Panel app where param.DataFrame values can be updated in several ways (1. edit table values, 2. drag points on plot, 3. programmatically change via a function) and both the table and plots should reflect these changes. Current halfway working code below:

import hvplot.pandas
import pandas as pd
from holoviews.streams import PointDraw

import param 
import panel as pn

df = pd.DataFrame(dict(x=[1, 1.5], y=[-1, 2], label=['A','B']))

class PointModifier(param.Parameterized):
    
    dataframe = param.DataFrame(df)
    
    # button to set points to specific value
    action = param.Action(lambda x: x.param.trigger('action'), label='set points')
    
    # allow app user to visually move points
    point_stream = PointDraw(num_objects=2)
    
    @param.depends('dataframe')
    def view(self):
        points =  self.dataframe.hvplot.points(color='cyan', size=200)
        self.point_stream.source = points
        plot = points.opts(active_tools=['point_draw'])
        return plot
    
    @param.depends('action')
    def _update_dataframe(self):
        newdf = pd.DataFrame(dict(x=[3, 4], y=[-3, 6], label=['A','B']))
        self.dataframe = newdf
    
pm = PointModifier(name='Points')
pn.Column(pm.param, pm.view)

What works is editing values in the dataframe widget or programmatically changing points (pm.dataframe=new). But changing point positions with the bokeh point_draw built-in tool is not reflected in the underlaying param.DataFrame (but updated values are in point_stream.contents). Also the action button callback as implements is not overwriting the dataframe values…

screenshot for visual of this little app:

Does this work for you?

class PointModifier(param.Parameterized):
    
    action = param.Action(lambda x: x.param.trigger('action'), label='set points')
    
    dataframe = param.DataFrame()
    
    stream = param.ClassSelector(default=PointDraw(num_objects=2), class_=(PointDraw,), precedence=-1)

    def view(self):
        points = self.dataframe.hvplot.points(color='cyan', size=200)
        self.stream.source = points
        return points.opts(active_tools=['point_draw'])
    
    @param.depends('action', watch=True)
    def _update_dataframe(self):
        self.dataframe = pd.DataFrame(self.stream.data)
    
df = pd.DataFrame(dict(x=[1, 1.5], y=[-1, 2], label=['A','B']))
pm = PointModifier(name='Points', dataframe=df)
pn.Column(pm.param, pm.view())

Thanks so much for looking into this @philippjfr, unfortunately your suggestion does not seem to work. edits to the table are not reflected in the plot and similarly pm.dataframe = new_dataframe does not update the plots. dragging the points updates the stream but not the connected dataframe (so I suspect the link is not accomplished) Is that approach working for you? if so, perhaps there have been recent changes to the various libraries in play? here is what i’m using. if it’s helpful i could put together a binder-repo.

bokeh=		2.2.3
holoviews=	1.14.1
panel=		0.10.3
param=		1.10.1
hvplot=		0.7.0

Sorry, I missed one thing, specifically the watch=True in @param.depends('action', watch=True)

okay, i see how that now sets things up so that after dragging points, you can click the button to have the dataframe update with self.stream.data, but things are still not working quite as i was originally intending. I’ll try to explain again, maybe forgetting the action button for a minute. What i’m really trying to do is link self.dataframe with self.stream.data. My goal is that when widget changes are made (point dragging in the plot, or direct edits to point position values in the table), each view is automatically updated to be in sync. basically i’m having to have any event tie back to a change in self.dataframe. Based on your suggestion I tried this bit below, but to no effect, so i think i’m struggling to understand how/where the bokeh tool linking and callbacks are happening:

    @param.depends('stream', watch=True)
    def _update_dataframe(self):
        self.dataframe = pd.DataFrame(self.stream.data)

Bi-directional syncing requires a bit of setup. Have you looked at the annotators which are set up to do exactly this:

from holoviews.annotators import PointAnnotator

df = pd.DataFrame(dict(x=[1, 1.5], y=[-1, 2], label=['A','B']))

PointAnnotator(df)
2 Likes

You would need to modify the code as follow:

@param.depends('stream.data', watch=True)
    def _update_dataframe(self):
        self.dataframe = pd.DataFrame(self.stream.data)

It works in my case.

1 Like