Skip to content

Fix DataFrame Scroll Divergence #11349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/flat-rooms-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gradio/dataframe": patch
"gradio": patch
---

fix:Fix DataFrame Scroll Divergence
169 changes: 49 additions & 120 deletions js/dataframe/shared/VirtualTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -40,43 +40,53 @@
? window.requestAnimationFrame
: (cb: (...args: any[]) => void) => cb();

$: mounted && raf(() => refresh_height_map(sortedItems));

let content_height = 0;
async function refresh_height_map(_items: typeof items): Promise<void> {
if (viewport_height === 0) {
return;
$: {
if (mounted && viewport_height && viewport.offsetParent) {
sortedItems, raf(refresh_height_map);
}
}

// force header height calculation first
head_height =
viewport.querySelector(".thead")?.getBoundingClientRect().height || 0;
await tick();
async function refresh_height_map(): Promise<void> {
if (sortedItems.length < start) {
await scroll_to_index(sortedItems.length - 1, { behavior: "auto" });
}

const { scrollTop } = viewport;
const scrollTop = Math.max(0, viewport.scrollTop);
show_scroll_button = scrollTop > 100;
table_scrollbar_width = viewport.offsetWidth - viewport.clientWidth;

content_height = top - (scrollTop - head_height);
let i = start;

while (content_height < max_height && i < _items.length) {
let row = rows[i - start];
if (!row) {
end = i + 1;
await tick(); // render the newly visible row
row = rows[i - start];
}
let _h = row?.getBoundingClientRect().height;
if (!_h) {
_h = average_height;
// acquire height map for currently visible rows
for (let v = 0; v < rows.length; v += 1) {
height_map[start + v] = rows[v].getBoundingClientRect().height;
}
let i = 0;
let y = head_height;
// loop items to find new start
while (i < sortedItems.length) {
const row_height = height_map[i] || average_height;
// keep a page of rows buffered above
if (y + row_height > scrollTop - max_height) {
start = i;
top = y - head_height;
break;
}
const row_height = (height_map[i] = _h);
y += row_height;
i += 1;
}

let content_height = head_height;
while (i < sortedItems.length) {
const row_height = height_map[i] || average_height;
content_height += row_height;
i += 1;
// keep a page of rows buffered below
if (content_height - head_height > 3 * max_height) {
break;
}
}

end = i;
const remaining = _items.length - end;
const remaining = sortedItems.length - end;

const scrollbar_height = viewport.offsetHeight - viewport.clientHeight;
if (scrollbar_height > 0) {
Expand All @@ -86,20 +96,22 @@
let filtered_height_map = height_map.filter((v) => typeof v === "number");
average_height =
filtered_height_map.reduce((a, b) => a + b, 0) /
filtered_height_map.length;
filtered_height_map.length || 30;

bottom = remaining * average_height;
height_map.length = _items.length;
await tick();
if (!max_height) {
actual_height = content_height + 1;
} else if (content_height < max_height) {
actual_height = content_height + 2;
} else {
if (!isFinite(bottom)) {
bottom = 200000;
}
height_map.length = sortedItems.length;
while (i < sortedItems.length) {
i += 1;
height_map[i] = average_height;
}
if (max_height && content_height > max_height) {
actual_height = max_height;
} else {
actual_height = content_height;
}

await tick();
}

$: scroll_and_render(selected);
Expand Down Expand Up @@ -144,88 +156,6 @@
return true;
}

function get_computed_px_amount(elem: HTMLElement, property: string): number {
if (!elem) {
return 0;
}
const compStyle = getComputedStyle(elem);

let x = parseInt(compStyle.getPropertyValue(property));
return x;
}

async function handle_scroll(e: Event): Promise<void> {
const scroll_top = viewport.scrollTop;

show_scroll_button = scroll_top > 100;

if (show_scroll_button) {
dispatch("scroll_top", scroll_top);
}

rows = contents.children as HTMLCollectionOf<HTMLTableRowElement>;
const is_start_overflow = sortedItems.length < start;

const row_top_border = get_computed_px_amount(rows[1], "border-top-width");

const actual_border_collapsed_width = 0;

if (is_start_overflow) {
await scroll_to_index(sortedItems.length - 1, { behavior: "auto" });
}

let new_start = 0;
// acquire height map for currently visible rows
for (let v = 0; v < rows.length; v += 1) {
height_map[start + v] = rows[v].getBoundingClientRect().height;
}
let i = 0;
// start from top: thead, with its borders, plus the first border to afterwards neglect
let y = head_height + row_top_border / 2;
let row_heights = [];
// loop items to find new start
while (i < sortedItems.length) {
const row_height = height_map[i] || average_height;
row_heights[i] = row_height;
// we only want to jump if the full (incl. border) row is away
if (y + row_height + actual_border_collapsed_width > scroll_top) {
// this is the last index still inside the viewport
new_start = i;
top = y - (head_height + row_top_border / 2);
break;
}
y += row_height;
i += 1;
}

new_start = Math.max(0, new_start);
while (i < sortedItems.length) {
const row_height = height_map[i] || average_height;
y += row_height;
i += 1;
if (y > scroll_top + viewport_height) {
break;
}
}
start = new_start;
end = i;
const remaining = sortedItems.length - end;
if (end === 0) {
end = 10;
}
average_height = (y - head_height) / end;
let remaining_height = remaining * average_height; // 0
// compute height map for remaining items
while (i < sortedItems.length) {
i += 1;
height_map[i] = average_height;
}
bottom = remaining_height;
if (!isFinite(bottom)) {
bottom = 200000;
}
}

export async function scroll_to_index(
index: number,
opts: ScrollToOptions,
Expand Down Expand Up @@ -269,7 +199,6 @@
onMount(() => {
rows = contents.children as HTMLCollectionOf<HTMLTableRowElement>;
mounted = true;
refresh_height_map(items);
});
</script>

Expand All @@ -280,7 +209,7 @@
class:disable-scroll={disable_scroll}
bind:this={viewport}
bind:contentRect={viewport_box}
on:scroll={handle_scroll}
on:scroll={refresh_height_map}
style="height: {height}; --bw-svt-p-top: {top}px; --bw-svt-p-bottom: {bottom}px; --bw-svt-head-height: {head_height}px; --bw-svt-foot-height: {foot_height}px; --bw-svt-avg-row-height: {average_height}px; --max-height: {max_height}px"
>
<thead class="thead" bind:offsetHeight={head_height}>
Expand Down
Loading