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()

3 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)
2 Likes

Thanks for sharing how to make bi direction link between tabulator and plot. Its awesome, however did you have idea if we have a lot data in the table and we can select from graph then table will be selected to those point of data in the table and put them in upper row instead we look one by one in every page in tabulator table or simply make filter to showing only selected data table later we can reset the table to showing original data. Same thing when we select multiple data point in the table then graph will correspond as well like in usually we use powerbi to cross filter between graph, table and map.
Thank you

1 Like

This is possible though I don’t have working code to show. To update the tabulator in point_selected:

  1. filter the original dataframe by the selected point indices: filtered_df = df.iloc[index]
  2. then set tabulator.value = filtered_df

You may need to save the original data to repopulate the widgets when reset.

Thanks @skytaker . It works to display only selected point however I get difficulty to put back the original data when reset the filter. do you have any idea.
def point_selected(index):
‘’’ link scatter plot selection to table row ‘’’
#print(‘point_selected’, index)
tabulator.selection = index
tabulator.remove_filter

filtered_df = df.iloc[index]
tabulator.value = filtered_df

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

sel = streams.Selection1D(source=plot)

sel.param.watch_values(point_selected, ‘index’)

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

Something like this:

def point_selected(index):
    ''' link scatter plot selection to table row '''
    print('point_selected', index)
    # tabulator.selection = index
    if len(index) == 0:
        tabulator.value = df
    else:
        filtered_df = df.iloc[index]
        tabulator.value = filtered_df
1 Like

awesome, thanks for your help dan sharing. :+1: :+1:

however there is some other problem that I realize. When we click from plot its show as expected unfortunately when we click from table expecially move to other row they will have error


did you got the same problem?

thank you

Yes, I get that error also. Not sure why that is happening.

i think related with tabulator value, but I don’t know to solve it:)