Bi-directional Link with Tabulator and Plot

I’d like to have a bi-directional link from a scatter plot to a tabulator table. Everything works in this example, except when a Tabulator row is chosen, it is not reflected in the scatter plot.

_table_selected captures the event but how can I update the plot to select the point in the plot? Is there a better way to link these objects?

import param
import panel as pn
import pandas as pd
import holoviews as hv
from holoviews.streams import PointDraw, Selection1D

hv.extension("bokeh")
pn.extension("tabulator")


class PlotLinker(param.Parameterized):

    df = param.DataFrame(pd.DataFrame(data={"x": [0, 5, 10], "y": [0, 3, 10]}))

    stream = param.ClassSelector(default=PointDraw(drag=True, add=False),
                                 class_=(PointDraw),
                                 precedence=-1)

    selection = param.ClassSelector(default=Selection1D(),
                                    class_=(Selection1D),
                                    precedence=-1)

    def table(self):
        self.table_ = pn.widgets.Tabulator(self.df,
                                           width=500,
                                           layout="fit_columns")
        self.table_.link(self.df,
                         callbacks={"value": self._callback_tabulator})
        self.table_.param.watch(self._table_selected, 'selection')
        return self.table_

    def scatter(self):
        points = hv.Scatter(self.df)
        points.opts(tools=["hover"], active_tools=["point_draw"], size=10)

        self.stream.source = points
        self.selection.source = points

        return points

    @pn.depends("stream.data", watch=True)
    def _update_dataframe(self):
        """Function to reflect the change in the PointDraw data to the original data"""
        print('_update_dataframe')
        stream_df = pd.DataFrame(self.stream.data)
        if not stream_df.equals(self.df):
            df = stream_df.reset_index()
            if "index" in df.columns:
                df = df.drop(["index"], axis=1)
            self.df = df

    def _callback_tabulator(self, target, event):
        """Simple callback to update the values when the table is updated"""
        print('_callback_tabulator')
        df = event.new
        if "index" in df.columns:
            df = df.drop(["index"], axis=1)
        self.df = df

    @param.depends('selection.index', watch=True)
    def _point_selected(self):
        ''' link scatter plot selection to table row '''
        print('_point_selected')
        self.table_.selection = self.selection.index

    # ??? doesn't update scatter & re-triggers seloection.index
    def _table_selected(self, event):
        ''' link table row selection to scatter plot  '''
        print('_table_selected')
        print(event)
        # self.selection.update(index=event.new)

    def view(self):
        return pn.Row(self.scatter, self.table)


pl = PlotLinker()

pn.Row(pl.scatter, pl.table).servable()

What if you wrap self.scatter with hv.DynamicMap()?

I don’t understand. Where do you recommend to use hv.DynamicMap() ?

def view(self):
    return pn.Row(hv.DynamicMap(self.scatter), self.table)

No luck with the DynamicMap.

I capture the Table row index in the _table_selected function, but how do I apply that to the Scatter plot (or Selection1D or PointDraw) ?

Looks like Annotators may be what I need. Thanks to @philippjfr for this suggestion.

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

pn.extension("tabulator")

df = pd.DataFrame(data={"x": [0, 5, 10], "y": [0, 3, 10]})

def plot(index):
    return hv.Scatter(df.iloc[index], "x", "y").opts(xlim=(0, 10), ylim=(0, 10), size=10)

tabulator = pn.widgets.Tabulator(df)
dmap = hv.DynamicMap(pn.bind(plot, tabulator.param.selection))

pn.Row(tabulator, dmap)

This works for me.

1 Like

Almost there! I want it to work like an Annotator but be linked to a Tabulator.

This example is almost what I need but when I ‘Tap’ a point in the scatter plot, the table row should be selected also.

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

pn.extension("tabulator")

df = pd.DataFrame(data={"x": [0, 5, 2], "y": [0, 3, 7]})

scatter = hv.Scatter(df, "x", "y").opts(xlim=(-10, 10))

tabulator = pn.widgets.Tabulator(df)


def plot(index):
    # return hv.Scatter(df.iloc[index], "x", "y").opts(xlim=(-10, 10),
    return hv.Scatter(df, "x", "y").opts(xlim=(-10, 10),
                                         ylim=(-10, 10),
                                         size=10,
                                         selected=index)


dmap = hv.DynamicMap(pn.bind(plot,
                             tabulator.param.selection)).opts(tools=['tap'])

pn.Row(tabulator, dmap).servable()

(In a practical case, I would rather update the scatter with the chosen index rather than return a new scatter, but I don’t know how.)

We did it! Linked a Tabulator to a Scatter plot … sort of … but it feels pretty hacky … and it’s triggering a bit of recursion. Maybe someone can help clean it up a bit. Here it is:

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

pn.extension("tabulator")

df = pd.DataFrame(data={"x": [0, 5, 2], "y": [0, 3, 7]})

scatter = hv.Scatter(df, "x", "y").opts(xlim=(-10, 10),
                                        size=10,
                                        selected=[1],
                                        tools=['tap'])

tabulator = pn.widgets.Tabulator(df, selection=[])


def update_scatter(selection):
    ''' link table row selection to scatter plot '''
    print('row_selected', selection)
    return hv.Scatter(df, "x", "y").opts(xlim=(-10, 10),
                                         ylim=(-10, 10),
                                         size=10,
                                         selected=selection,
                                         tools=['tap', 'box_select'])


def point_selected(index):
    ''' link scatter plot selection to table row '''
    print('point_selected', index)
    tabulator.selection = index


plot = hv.DynamicMap(pn.bind(update_scatter, tabulator.param.selection))

sel = streams.Selection1D(source=plot)

sel.param.watch_values(point_selected, 'index')
# tabulator.param.watch_values(row_selected, 'selection')

pn.Row(tabulator, plot).servable()

2 Likes

Maybe you can use .apply.opts() rather than plain .opts()

2 Likes

Yes. Testing your suggestion shows that this works and it appears to update the original scatter plot, rather than creating a new one.

return scatter.opts(selected=selection)
1 Like