Extending the above to larger dataframes; and looking to apply filter to the dataframe to what is selected (this bit is fine), but then editing a value within that dataframe…
import holoviews as hv
import numpy as np
import pandas as pd
import panel as pn
hv.extension('bokeh')
pn.extension('tabulator')
# setup data
dim = 341
bounds=(-0.5, dim-0.5)
xx, yy = np.meshgrid(range(dim), range(dim))
# added the column 's' to use as the selection to manipulate the alpha
df = pd.DataFrame({'x': xx.flatten(),
'y': yy.flatten(),
'z': np.random.uniform(size=dim*dim),
's': np.ones(shape=dim*dim)})
selected_indices = pn.widgets.ArrayInput(name="selected_index")
# callback to update the table selection. need to transpose index
def create_heatmap(data):
if data:
df.loc[data, 's'] = 1.0
df.loc[df.index.difference(data), 's'] = 0.2
else:
df.loc[:, 's'] = 1.0
heatmap = hv.HeatMap(
df,
kdims=['x', 'y'],
vdims=['z', 's']).opts(
tools=['tap', 'box_select'],
aspect=1,
responsive=True,
xlim=bounds,
ylim=bounds,
alpha=hv.dim('s')
).redim.range(z=(0, 1.0))
return heatmap
# update callback
def update_selection(*events):
for event in events:
if event.name == 'index':
# transpose
indices = [(i % dim) * dim + i // dim for i in event.new]
tb.selection = indices
if indices:
selected_indices.value = indices
else:
selected_indices.value = []
# short circuit to avoid infinite recursion since the line
# right above will retrigger this method
return
pipe.send(event.new)
# setup table
tabulator_formatters = {
'z': {'type': 'progress', 'max': 1},
}
tabulator_editors = {
'x': None,
'y': None,
'z': {'type': 'number', 'min': 0, 'max': 1, 'step': 0.01},
's': None,
}
tb = pn.widgets.Tabulator(df,
selectable=True,
disabled=False,
layout="fit_columns",
editors=tabulator_editors,
formatters=tabulator_formatters,
initial_page_size=40,
)
tb.on_edit(
lambda e: print(
f"Updated col: {e.column}, row: {e.row}, old: {e.old}, new: {e.value}"))
def df_filter(df, indices):
if not indices:
return df
return df.loc[df.index.isin(indices)]
# create pipe, and use a stream for the heatmap dynamic map,
# and use the dmap as the source for the Selection1D
pipe = hv.streams.Pipe(data=[])
dmap_heatmap = hv.DynamicMap(create_heatmap, streams=[pipe])
selection_stream = hv.streams.Selection1D(source=dmap_heatmap)
# set up watchers to trigger the selection update
tb_watcher = tb.param.watch(
update_selection,
['selection'],
onlychanged=True,
)
heatmap_watcher = selection_stream.param.watch(
update_selection,
['index'],
onlychanged=True,
)
tb.add_filter(pn.bind(df_filter, indices=selected_indices))
pn.serve(
pn.Row(
tb,
dmap_heatmap)
)
It runs, the panel allows selection via the sidebar dataframe and highlights. Select by the image and it filters the dataframe. So far, so good.
Attempt to modify the dataframe from a dataframe selection, no problem.
Attempt to modify the filtered dataframe (from image selection), while the dataframe acknowledges the update, there are a load of warnings fired in the console:
ERROR:tornado.application:Exception in callback functools.partial(<bound method IOLoop._discard_future_result of <tornado.platform.asyncio.AsyncIOMainLoop object at 0x00000246C6656090>>, <Task finished name='Task-1836' coro=<ServerSession.with_document_locked() done, defined at ~\.conda\envs\flask\Lib\site-packages\bokeh\server\session.py:77> exception=IndexError('index 82613 is out of bounds for axis 0 with size 1')>)
Traceback (most recent call last):
File "~\.conda\envs\flask\Lib\site-packages\tornado\ioloop.py", line 750, in _run_callback
ret = callback()
^^^^^^^^^^
File "~\.conda\envs\flask\Lib\site-packages\tornado\ioloop.py", line 774, in _discard_future_result
future.result()
File "~\.conda\envs\flask\Lib\site-packages\bokeh\server\session.py", line 98, in _needs_document_lock_wrapper
result = await result
^^^^^^^^^^^^
File "~\.conda\envs\flask\Lib\site-packages\panel\reactive.py", line 508, in _change_coroutine
state._handle_exception(e)
File "~\.conda\envs\flask\Lib\site-packages\panel\io\state.py", line 468, in _handle_exception
raise exception
File "~\.conda\envs\flask\Lib\site-packages\panel\reactive.py", line 506, in _change_coroutine
self._change_event(doc)
File "~\.conda\envs\flask\Lib\site-packages\panel\reactive.py", line 524, in _change_event
self._process_events(events)
File "~\.conda\envs\flask\Lib\site-packages\panel\widgets\tables.py", line 1329, in _process_events
return super()._process_events(events)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "~\.conda\envs\flask\Lib\site-packages\panel\reactive.py", line 1422, in _process_events
self._process_data(events.pop('data'))
File "~\.conda\envs\flask\Lib\site-packages\panel\widgets\tables.py", line 1419, in _process_data
return super()._process_data(data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "~\.conda\envs\flask\Lib\site-packages\panel\reactive.py", line 1410, in _process_data
self.value = old_data
^^^^^^^^^^
File "~\.conda\envs\flask\Lib\site-packages\param\parameterized.py", line 528, in _f
instance_param.__set__(obj, val)
File "~\.conda\envs\flask\Lib\site-packages\param\parameterized.py", line 530, in _f
return f(self, obj, val)
^^^^^^^^^^^^^^^^^
File "~\.conda\envs\flask\Lib\site-packages\param\parameterized.py", line 1553, in __set__
obj.param._call_watcher(watcher, event)
File "~\.conda\envs\flask\Lib\site-packages\param\parameterized.py", line 2526, in _call_watcher
self_._execute_watcher(watcher, (event,))
File "~\.conda\envs\flask\Lib\site-packages\param\parameterized.py", line 2506, in _execute_watcher
watcher.fn(*args, **kwargs)
File "~\.conda\envs\flask\Lib\site-packages\panel\widgets\tables.py", line 141, in _reset_selection
idx = event.old.index[sel]
~~~~~~~~~~~~~~~^^^^^
File "~\AppData\Roaming\Python\Python312\site-packages\pandas\core\indexes\base.py", line 5389, in __getitem__
return getitem(key)
^^^^^^^^^^^^
IndexError: index 82613 is out of bounds for axis 0 with size 1
Updated col: z, row: 82613, old: 0.47128191914644113, new: 0.49
The .onedit callback occurs after the edit has been performed, otherwise resetting the filter then reapplying would be an option. Anyone know of a way of getting myself into the middle of the Tabulator update?
(Besides the sledgehammer approach of modifying panel.model.tabulator.TableEditEvent ?)
Thanks!