Sync scrollbars for wide Tabulators

Nothing fancy to show. But I needed to write some custom javascript to get the scrollbars of wide tabulators to sync with each other. It took some time to figure out, so I wanted to share it:

import pandas as pd
import panel as pn

pn.extension("tabulator")

js = """
<script type="text/javascript">
function $$$(selector, rootNode=document.body) {
    const arr = []

    const traverser = node => {
        // 1. decline all nodes that are not elements
        if(node.nodeType !== Node.ELEMENT_NODE) {
            return
        }

        // 2. add the node to the array, if it matches the selector
        if(node.matches(selector)) {
            arr.push(node)
        }

        // 3. loop through the children
        const children = node.children
        if (children.length) {
            for(const child of children) {
                traverser(child)
            }
        }

        // 4. check for shadow DOM, and loop through it's children
        const shadowRoot = node.shadowRoot
        if (shadowRoot) {
            const shadowChildren = shadowRoot.children
            for(const shadowChild of shadowChildren) {
                traverser(shadowChild)
            }
        }
    }

    traverser(rootNode)
    return arr
}

async function sync_scrolling() {
  tables = $$$(".sync-table")

  const elements = []
  for (let i = 0; i < tables.length; i++) {
    var e = null

    // Wait for the table to appear
    while (e === null) {
      await new Promise((r) => setTimeout(r, 100))
      e = tables[i].shadowRoot.querySelector(
        ".pnx-tabulator .tabulator-tableholder"
      )
    }
    elements.push(e)
  }

  var ignore_scroll_events = false
  for (let i = 0; i < elements.length; i++) {
    elements[i].onscroll = (e) => {
      if (ignore_scroll_events) return
      ignore_scroll_events = true
      for (let j = 0; j < elements.length; j++) {
        if (elements[j] == elements[i]) continue
        elements[j].scrollLeft = elements[i].scrollLeft
        elements[j].scrollTop = elements[i].scrollTop
      }
      ignore_scroll_events = false
    }
  }
}

sync_scrolling()
</script>
"""

df = pd.DataFrame(range(10))
pn.Row(
    pn.Column(
        pn.widgets.Tabulator(df.T, css_classes=["sync-table"]),
        pn.widgets.Tabulator(df.T, css_classes=["sync-table"]),
        pn.widgets.Tabulator(df.T, css_classes=["sync-table"]),
        pn.widgets.Tabulator(df.T, css_classes=["sync-table"]),
        pn.pane.HTML(js),
        width=200,
    )
).servable()
6 Likes

i’ve just created login for give u respect

maybe I should have used ReactiveHTML or render_html
but with the help of your snippet I was finally able to synchronize echarts instances

def sync_charts(event=None):
    sync_code = """
    <script>
    function $$$(selector, rootNode=document.body) {
        const arr = [];
        console.log('Starting search with selector:', selector, 'and rootNode:', rootNode);
    
        const traverser = node => {
            // 1. Decline all nodes that are not elements
            if (node.nodeType !== Node.ELEMENT_NODE) {
                return;
            }
    
            // 2. Add the node to the array if it matches the selector
            if (node.matches(selector)) {
                console.log('Found matching node:', node);
                arr.push(node);
            }
    
            // 3. Loop through the children
            const children = node.children;
            if (children.length) {
                for (const child of children) {
                    traverser(child);
                }
            }
    
            // 4. Check for shadow DOM, and loop through its children
            const shadowRoot = node.shadowRoot;
            if (shadowRoot) {
                console.log('Found shadow root in node:', node);
                const shadowChildren = shadowRoot.children;
                for (const shadowChild of shadowChildren) {
                    traverser(shadowChild);
                }
            }
        };
    
        traverser(rootNode);
        console.log('Finished search. Found nodes:', arr);
        return arr;
    }

    function syncCharts() {
        console.log('Syncing charts...');
        const chartInstances = $$$('div[_echarts_instance_]').map(elem => {
            const echartsInstance = echarts.getInstanceByDom(elem);
            if (echartsInstance) {
                console.log('Found ECharts instance:', echartsInstance);
                return echartsInstance;
            }
        }).filter(instance => instance !== undefined);
    
        console.log('Total ECharts instances found:', chartInstances.length);
        if (chartInstances.length > 1) {
            echarts.connect(chartInstances);
            console.log('ECharts instances connected.');
        } else {
            console.log('Not enough ECharts instances to connect.');
        }
    }
    syncCharts();
    </script>
    """

    plot_pane.append(pn.pane.HTML(sync_code, sizing_mode='stretch_both'))