Tabulator Moveable rows and columns

Hi everyone,
I’m new to Panel.
What attracted me was that panel is a fine way to integrate the powerful Tabulator with python.

Not all Tabulator features are available directly from Panel but I presume that there should be a way to implement all (?) Tabulator features through the static configuration

What I’m especially interested in is the ability to move rows and columns within or between Tabulators (even elements!).

Would it be possible to post a simple boilerplate example on how to do this with Panel
Thanks in advance

import pandas as pd
import datetime as dt
import panel as pn
pn.extension('tabulator')
df = pd.DataFrame({
      'int': [1, 2, 3],
      'float': [3.14, 6.28, 9.42],
      'str': ['A', 'B', 'C'],
      'bool': [True, False, True],
      'date': [dt.date(2019, 1, 1), dt.date(2020, 1, 1), dt.date(2020, 1, 10)]
  }, index=[1, 2, 3])

df_widget = pn.widgets.Tabulator(df, configuration  = {'movableRows':True, 'movableColumns':True}  )
df_widget.show()

setting ‘movableRows’ to True in the configuration seems to work.
‘movableColumns’ doesn’t

Any ideas on this?

Also Moving between Tabulator Tables is a great mystery to me…

    df1 = pd.DataFrame({
        'int': [1, 2, 3],
        'float': [3.14, 6.28, 9.42],
        'str': ['A', 'B', 'C'],
        'bool': [True, False, True],
        'date': [dt.date(2019, 1, 1), dt.date(2020, 1, 1), dt.date(2020, 1, 10)]
    }, index=[1, 2, 3])

    df2 = pd.DataFrame({
        'int': [4, 5, 6],
        'float': [18.84, 37.68, 75.36],
        'str': ['D', 'E', 'F'],
        'bool': [False, True, False],
        'date': [dt.date(2021, 1, 1), dt.date(2022, 1, 1), dt.date(2022, 1, 10)]
    }, index=[4, 5, 6])
 
    table1 = pn.widgets.Tabulator(df1, configuration  = {'movableRows':True})
    table2 = pn.widgets.Tabulator(df2, configuration  = {'movableRows':True, 'movableRowsConnectedTables' : [table1]})
    widget = pn.Row(table1, table2)
    widget.show()

It would be nice to have some info on how to achieve the functionality explained in the Tabulator docs

Is this achievable with Panel’s Tabulator at all?

Hi @ljilekor1,

As an introduction on the Tabulator widget, it’s indeed a very powerful component that has many features. The combinations of features (filtering, sorting, pagination, selections, …) is large and there are some areas where it doesn’t quite behave as you’d expect, my current goal is to make the current features more robust.

Not all Tabulator features are available directly from Panel but I presume that there should be a way to implement all (?) Tabulator features through the static configuration

No, not really. One thing to note first is that the static configuration is an entry-point to provide some extra configurations to the Tabulator JS library, when you use that feature you end-up in some unknown territory, as there’s no way we test all the different configurations :slight_smile: So you’re on your own, and you’ll have to make sure things work as expected. Second thing to note is that the Tabulator Panel widget is not only an integration of the Tabulator JS widget, it is constantly syncing the two when things change. E.g. when you change a value in a table in your browser, this is reflected on the Python side. Same the other way around. What you want to achieve, i.e. moving rows around, will require more than just setting the configuration, as I guess you will expect that this has an effect on the Python side. So you’d have to think about what it means to move rows up or down in a Pandas DataFrame (e.g. should the index be updated?), and how that should behave when sorters and filters are applied. Moving rows between tables gets more complicated, as there’s currently nothing that allows two Tabulator widgets to be connected.

I’m pretty sure however that you could implement something that would allow you to move rows around, based on the selections, Panel Python callbacks and a few buttons for the UI. It won’t be as nice as what Tabulator JS can offer, but it’s also fair to say that Panel cannot re-implement all of Tabulator JS :slight_smile:

from maximlt
… i.e. moving rows around, will require more than just setting the configuration, as I guess you will expect that this has an effect on the Python side. So you’d have to think about what it means to move rows up or down in a Pandas DataFrame (e.g. should the index be updated?),

I see no problems handling the DataFrame side python reorder logic, but I need to get proper feedback from tabulator.
eg. from_row_id - to_row_id … or something

Where I get stuck is that I have no idea on how to implement proper callbacks, sender receiver JS frontend stuff, … Well above my paygrade JS<->Python intricacies. How Panel handles the events, etc, …
The tabulator docs state the following on the JS rowMoved callback
sender / receiver functions
I have no experience on that side.

I presume that updating the result dataFrame ‘Tabulator.current_view’ with the recalculated DataFrame shouldn’t be too hard from there on.

Could someone give me a headstart on where to begin, please?

Below a basic example to showcase a possible solution .

But… yeah, it feels kind of ancient windows98-ish…
Dragging dropping of rows and columns, would render all buttons obsolete and kick this example into the 2020’s, where panel belongs.
Maybe, adding dragging the layout sizes with the mouse, etc. would be very useful when working on data (preparing it for Panel’s nice visualizers.)

Performance also seems to lack behind, but this is probably mainly due to the fact that this is my 1st attempt at taming Panel.
Feel free anyone to add/remove stuff that enhance performance. That would be much appreciated.

import numpy as np
import pandas as pd
import panel as pn
from panel.viewable import Viewer
import param

pn.extension(sizing_mode = 'stretch_width')
pn.extension('tabulator')

class Table_ColumnEditor(Viewer):
    df = param.DataFrame(default = pd.DataFrame())
    value = param.DataFrame(default = pd.DataFrame())

    def __init__(self, df, **params):
        super().__init__(**params)      
        
        self.df = df
        cols_df = pd.DataFrame({'cols':self.df.columns}, index = range(len(self.df.columns)))
        
        self.table          = pn.widgets.Tabulator(pd.DataFrame())
        self.src_col_table  = pn.widgets.Tabulator(cols_df, 
                                                   hidden_columns = ['index'],
                                                   editors = {'cols':None},
                                                   configuration  = {'headerVisible': False,
                                                                     #'movableRows':self.moveable_rows
                                                                     }
                                                   )
        self.trg_col_table  = pn.widgets.Tabulator(pd.DataFrame({'cols':pd.Series(dtype='str')}),
                                                   hidden_columns = ['index'],
                                                   buttons={'remove': '<i class="fa fa-times"></i>'},
                                                   editors = {'cols':None},
                                                   configuration  = {'headerVisible': False,
                                                                     #'movableRows':self.moveable_rows
                                                                     }
                                                   )
        self.add_column_btn = pn.widgets.Button(name = '>')
        self.add_order_up   = pn.widgets.Button(name = 'up')
        self.add_order_down = pn.widgets.Button(name = 'down')

        self.trg_col_table.on_click(self.trg_col_table_click) 
        self.add_column_btn.on_click(self.add_rows)
        self.add_order_up.on_click(self.move_rows)
        self.add_order_down.on_click(self.move_rows)   

        self.trg_col_table.link(self.table , callbacks={'value':self.columns_changed})
        self.table.link(self.value , callbacks={'value':self.value_changed})

        self._layout = pn.Column(pn.Row(pn.Card(self.src_col_table,
                                                title='SOURCE COLUMNS'),
                                        self.add_column_btn,
                                        pn.Card(pn.Column(pn.Row(self.add_order_up, 
                                                                 self.add_order_down
                                                                 ),
                                                          self.trg_col_table
                                                          ),
                                                title='TARGET COLUMNS'
                                                )
                                        ),
                                 pn.Card(self.table,
                                         title='RESULT'
                                         )
                                 )

    def __panel__(self):
        return self._layout


    def columns_changed(self, target, event):
        self.table.value = self.df[self.trg_col_table.value['cols'].tolist()]


    def value_changed(self, target, event):
        self.value = self.table.value


    def trg_col_table_click(self, o):        
        if o.column == 'remove':
            self.remove_rows([o.row])


    def add_rows(self, event):
        src_selected_rows = self.src_col_table.value.loc[self.src_col_table.selection].values.tolist()

        df = self.trg_col_table.value 
        df_items = df.values.tolist()
        for src_selected_row in src_selected_rows:
            if src_selected_row in df_items:
                continue
            df.loc[df.shape[0]] = src_selected_row
        df.reset_index(drop=True, inplace=True)

        self.trg_col_table.value = df 


    def remove_rows(self, remove_id_lst):
        if not remove_id_lst:
            return
        df = self.trg_col_table.value
        df.drop(remove_id_lst, axis=0, inplace=True)
        df.reset_index(drop=True, inplace=True)

        self.trg_col_table.value = df


    def move_rows(self, event):
        df = self.trg_col_table.value

        ids = df.index.tolist()
        ids_count = len(ids)
        sel_ids = sorted(self.trg_col_table.selection)
        sel_count = len(sel_ids)

        new_sel_ids = []
        if event.obj == self.add_order_down:
            new_sel_ids = [x+1 if x+1<ids_count-(sel_count-1-i) else x for i,x in enumerate(sel_ids)]
        elif event.obj == self.add_order_up:
            new_sel_ids = [x-1 if x-1>=i else x for i,x in enumerate(sel_ids)]
        popped = [ids.pop(x-i) for i,x in enumerate(sel_ids)]
        [ids.insert(x, popped[i]) for i,x in enumerate(new_sel_ids)]
        df = df.loc[ids]
        df.reset_index(drop=True, inplace=True)

        self.trg_col_table.value = df 
        self.trg_col_table.selection = new_sel_ids 



if __name__=='__main__':
    from pandas import util
    view = Table_ColumnEditor(util.testing.makeMixedDataFrame(), name='Test')
    view.show()