From 9048de7eea6ab40430d6f305c2ce48cc1990a29a Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Tue, 23 Dec 2025 17:57:19 +0100 Subject: [PATCH 1/2] Debounce scroll bar requests --- src/widget.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/widget.ts b/src/widget.ts index 503483e..15d2ddf 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -8,15 +8,60 @@ import { DataGrid, TextRenderer, } from "@lumino/datagrid"; +import { ConflatableMessage, MessageLoop } from "@lumino/messaging"; +import { Debouncer } from "@lumino/polling"; import { Panel } from "@lumino/widgets"; import type { DocumentRegistry, IDocumentWidget } from "@jupyterlab/docregistry"; import type * as DataGridModule from "@lumino/datagrid"; +import type { ISignal } from "@lumino/signaling"; +import type { ScrollBar } from "@lumino/widgets"; import { FileType } from "./file-types"; import { ArrowModel } from "./model"; import { createToolbar } from "./toolbar"; import type { FileInfo, FileReadOptions } from "./file-options"; +/* grid: DataGrid instance */ + +function installDebouncedScrollBarHook(grid: DataGrid, delay = 100) { + // Access the internal vertical scrollbar and its thumbMoved signal + // biome-ignore lint/suspicious/noExplicitAny: Hacking into private property + const vScrollBar = (grid as any)._vScrollBar as ScrollBar; + // biome-ignore lint/suspicious/noExplicitAny: Hacking into private property + const thumbMoved = (vScrollBar as any).thumbMoved as ISignal; + + // Get the original handler method from the grid + // biome-ignore lint/suspicious/noExplicitAny: Hacking into private property + const originalHandler = (grid as any)._onThumbMoved as (sender: ScrollBar) => void; + + // Disconnect the original handler + thumbMoved.disconnect(originalHandler, grid); + + // Create a debouncer that posts the scroll request after the delay + // The debouncer ensures the last event is always processed + const debouncer = new Debouncer(() => { + MessageLoop.postMessage(grid.viewport, new ConflatableMessage("scroll-request")); + }, delay); + + // Handler that invokes the debouncer on each thumb move + const debouncedHandler = () => { + void debouncer.invoke(); + }; + + // Connect our debounced handler + thumbMoved.connect(debouncedHandler); + + // Return cleanup function + return () => { + // Disconnect our handler + thumbMoved.disconnect(debouncedHandler); + // Reconnect the original handler + thumbMoved.connect(originalHandler, grid); + // Dispose the debouncer + debouncer.dispose(); + }; +} + export namespace ArrowGridViewer { export interface Options { path: string; @@ -51,6 +96,9 @@ export class ArrowGridViewer extends Panel { }; this.addWidget(this._grid); + + installDebouncedScrollBarHook(this._grid, 100); + this._ready = this.initialize(); } From ab42030b6e20110a13b7644d45f5f1125819ef66 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 5 Jan 2026 15:53:38 +0100 Subject: [PATCH 2/2] Add debounce based on value --- src/model.ts | 8 +++ src/widget.ts | 131 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 91 insertions(+), 48 deletions(-) diff --git a/src/model.ts b/src/model.ts index 8f02ba2..51c2782 100644 --- a/src/model.ts +++ b/src/model.ts @@ -94,6 +94,14 @@ export class ArrowModel extends DataModel { return this._schema; } + get numRows(): number { + return this._numRows; + } + + get numCols(): number { + return this._numCols; + } + columnCount(region: DataModel.ColumnRegion): number { if (region === "body") { return this._numCols; diff --git a/src/widget.ts b/src/widget.ts index 15d2ddf..4339035 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -8,63 +8,84 @@ import { DataGrid, TextRenderer, } from "@lumino/datagrid"; -import { ConflatableMessage, MessageLoop } from "@lumino/messaging"; import { Debouncer } from "@lumino/polling"; import { Panel } from "@lumino/widgets"; import type { DocumentRegistry, IDocumentWidget } from "@jupyterlab/docregistry"; import type * as DataGridModule from "@lumino/datagrid"; -import type { ISignal } from "@lumino/signaling"; -import type { ScrollBar } from "@lumino/widgets"; +import type { IMessageHandler, Message } from "@lumino/messaging"; import { FileType } from "./file-types"; import { ArrowModel } from "./model"; import { createToolbar } from "./toolbar"; import type { FileInfo, FileReadOptions } from "./file-options"; -/* grid: DataGrid instance */ - -function installDebouncedScrollBarHook(grid: DataGrid, delay = 100) { - // Access the internal vertical scrollbar and its thumbMoved signal - // biome-ignore lint/suspicious/noExplicitAny: Hacking into private property - const vScrollBar = (grid as any)._vScrollBar as ScrollBar; - // biome-ignore lint/suspicious/noExplicitAny: Hacking into private property - const thumbMoved = (vScrollBar as any).thumbMoved as ISignal; - - // Get the original handler method from the grid - // biome-ignore lint/suspicious/noExplicitAny: Hacking into private property - const originalHandler = (grid as any)._onThumbMoved as (sender: ScrollBar) => void; - - // Disconnect the original handler - thumbMoved.disconnect(originalHandler, grid); - - // Create a debouncer that posts the scroll request after the delay - // The debouncer ensures the last event is always processed - const debouncer = new Debouncer(() => { - MessageLoop.postMessage(grid.viewport, new ConflatableMessage("scroll-request")); - }, delay); - - // Handler that invokes the debouncer on each thumb move - const debouncedHandler = () => { - void debouncer.invoke(); - }; - - // Connect our debounced handler - thumbMoved.connect(debouncedHandler); - - // Return cleanup function - return () => { - // Disconnect our handler - thumbMoved.disconnect(debouncedHandler); - // Reconnect the original handler - thumbMoved.connect(originalHandler, grid); - // Dispose the debouncer - debouncer.dispose(); - }; +/** + * DataGrid that intercepts scroll-request messages and debounce them on large changes. + */ +class DebouncedDataGrid extends DataGrid { + constructor(options: DataGrid.IOptions & { scrollThreshold: number; debounceDelay: number }) { + super(options); + + this._scrollThresholdY = options.scrollThreshold; + this._scrollThresholdX = options.scrollThreshold; + + this._scrollDebouncer = new Debouncer((handler: IMessageHandler, msg: Message) => { + super.messageHook(handler, msg); + }, options.debounceDelay); + } + + setScrollThresholds(thresholdY: number, thresholdX: number): void { + this._scrollThresholdY = thresholdY; + this._scrollThresholdX = thresholdX; + } + + messageHook(handler: IMessageHandler, msg: Message): boolean { + if (handler === this.viewport && msg.type === "scroll-request") { + // Calculate the percentage change in vertical scroll position + const scrollChangeY = Math.abs(this.scrollY - this._previousScrollY); + const scrollChangePercentY = this.maxScrollY > 0 ? scrollChangeY / this.maxScrollY : 0; + + // Calculate the percentage change in horizontal scroll position + const scrollChangeX = Math.abs(this.scrollX - this._previousScrollX); + const scrollChangePercentX = this.maxScrollX > 0 ? scrollChangeX / this.maxScrollX : 0; + + // Check if either direction exceeds its threshold + const shouldDebounceY = scrollChangePercentY > this._scrollThresholdY; + const shouldDebounceX = scrollChangePercentX > this._scrollThresholdX; + + if (shouldDebounceY || shouldDebounceX) { + // Large scroll change - debounce it + void this._scrollDebouncer.invoke(handler, msg); + this._previousScrollY = this.scrollY; + this._previousScrollX = this.scrollX; + // Don't process immediately, return false to stop propagation + return false; + } else { + // Small scroll change - process immediately + this._previousScrollY = this.scrollY; + this._previousScrollX = this.scrollX; + return super.messageHook(handler, msg); + } + } + + return super.messageHook(handler, msg); + } + + dispose(): void { + this._scrollDebouncer.dispose(); + super.dispose(); + } + + private _scrollDebouncer: Debouncer; + private _previousScrollY: number = 0; + private _previousScrollX: number = 0; + private _scrollThresholdY: number; + private _scrollThresholdX: number; } export namespace ArrowGridViewer { - export interface Options { - path: string; + export interface Options extends ArrowModel.LoadingOptions { + debounceDelay?: number; } } @@ -76,13 +97,20 @@ export class ArrowGridViewer extends Panel { this.addClass("arrow-viewer"); this._defaultStyle = DataGrid.defaultStyle; - this._grid = new DataGrid({ + + // Start with a conservative default threshold that will be updated when model is loaded + const defaultScrollThreshold = 0.01; + const debounceDelay = options.debounceDelay ?? 300; + + this._grid = new DebouncedDataGrid({ defaultSizes: { rowHeight: 24, columnWidth: 144, rowHeaderWidth: 64, columnHeaderHeight: 36, }, + scrollThreshold: defaultScrollThreshold, + debounceDelay, }); this._grid.addClass("arrow-grid-viewer"); this._grid.headerVisibility = "all"; @@ -97,8 +125,6 @@ export class ArrowGridViewer extends Panel { this.addWidget(this._grid); - installDebouncedScrollBarHook(this._grid, 100); - this._ready = this.initialize(); } @@ -164,10 +190,19 @@ export class ArrowGridViewer extends Panel { private async _updateGrid() { try { - const dataModel = await ArrowModel.fromRemoteFileInfo({ path: this.path }); + const dataModel = await ArrowModel.fromRemoteFileInfo(this._options); await dataModel.ready; this._grid.dataModel = dataModel; this._grid.selectionModel = new BasicSelectionModel({ dataModel }); + + // Calculate scroll debounce thresholds based on chunk sizes + const rowChunkSize = this._options.rowChunkSize ?? 256; + const colChunkSize = this._options.colChunkSize ?? 24; + const numRows = dataModel.numRows; + const numCols = dataModel.numCols; + const scrollThresholdY = numRows > 0 ? rowChunkSize / numRows : 0.01; + const scrollThresholdX = numCols > 0 ? colChunkSize / numCols : 0.01; + (this._grid as DebouncedDataGrid).setScrollThresholds(scrollThresholdY, scrollThresholdX); } catch (error) { const trans = Dialog.translator.load("jupyterlab"); const buttons = [