Pond graph: size the canvas to the viewport, not the width ratio (#131)

The SVG scaled to container width with height following the fixed
800×560 viewBox ratio — on wide windows the graph grew taller than the
viewport, the page got a scrollbar, and wheel-zoom scrolled along.
The graph page is now a flex column filling the main column; the canvas
takes the remaining height (flex: 1, min-height: 0), a ResizeObserver
feeds its measured size to ForceGraph as width/height, and the SVG
fills it exactly. LocalGraphPanel keeps its fixed defaults. Scope
deliberately layout-only (issue comment 1187): with no scrollbar there
is nothing for the wheel to scroll, so no non-passive listener needed.
The graph pack now asserts the main column does not overflow
vertically on the graph route.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
This commit is contained in:
Claude Fable 5 2026-07-16 12:02:56 +02:00
parent 31557f09e5
commit d23e5dc10c
3 changed files with 56 additions and 3 deletions

View File

@ -72,6 +72,15 @@ test('pond graph renders the link structure and creates phantom pages', async ({
expect(await page.locator('.force-graph__edge').count()).toBeGreaterThanOrEqual(2); expect(await page.locator('.force-graph__edge').count()).toBeGreaterThanOrEqual(2);
await expect(page.locator('.graph-legend')).toBeVisible(); await expect(page.locator('.graph-legend')).toBeVisible();
// #131: the page fits the viewport — wheel-zoom must never scroll along,
// so the main column may not grow a vertical scrollbar on this route.
expect(
await page.evaluate(() => {
const main = document.querySelector('.main')!;
return main.scrollHeight - main.clientHeight;
}),
).toBeLessThanOrEqual(0);
// #123: the physics/rendering sliders are there and take effect — the // #123: the physics/rendering sliders are there and take effect — the
// font-size slider writes straight into the SVG labels. // font-size slider writes straight into the SVG labels.
const controls = page.locator('.graph-controls'); const controls = page.locator('.graph-controls');

View File

@ -1,7 +1,7 @@
import type { PageView, PondGraphView, PondView } from '@dorfteich/shared'; import type { PageView, PondGraphView, PondView } from '@dorfteich/shared';
import { DEFAULT_LABEL_COLOR } from '@dorfteich/shared'; import { DEFAULT_LABEL_COLOR } from '@dorfteich/shared';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useMemo } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
@ -125,11 +125,37 @@ function GraphView({
graphSettingsKey(pondId), graphSettingsKey(pondId),
GRAPH_SETTINGS_DEFAULTS, GRAPH_SETTINGS_DEFAULTS,
); );
// #131: the canvas flexes to the remaining viewport height; its measured
// size becomes the SVG viewBox, so the graph fills it exactly instead of
// deriving its height from the width (which used to overflow the page).
const canvasRef = useRef<HTMLDivElement>(null);
const [size, setSize] = useState<{ width: number; height: number } | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const observer = new ResizeObserver((entries) => {
const rect = entries[0]?.contentRect;
if (rect && rect.width > 0 && rect.height > 0) {
setSize({ width: Math.round(rect.width), height: Math.round(rect.height) });
}
});
observer.observe(canvas);
return () => observer.disconnect();
}, []);
return ( return (
<> <>
<GraphControls settings={settings} setSettings={setSettings} /> <GraphControls settings={settings} setSettings={setSettings} />
<div className="graph-page__canvas"> <div className="graph-page__canvas" ref={canvasRef}>
<ForceGraph nodes={nodes} edges={edges} settings={settings} onNodeClick={onNodeClick} /> {size !== null && (
<ForceGraph
nodes={nodes}
edges={edges}
width={size.width}
height={size.height}
settings={settings}
onNodeClick={onNodeClick}
/>
)}
</div> </div>
</> </>
); );

View File

@ -1776,6 +1776,16 @@ button {
} }
/* Knowledge graph (issue #112) + local graph panel (#113). */ /* Knowledge graph (issue #112) + local graph panel (#113). */
/* #131: the page fills the main column as a flex column, so the canvas can
take exactly the remaining viewport height the page itself must never
grow a scrollbar (wheel-zoom would scroll along with it). */
.graph-page {
height: 100%;
display: flex;
flex-direction: column;
}
.graph-page__header { .graph-page__header {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
@ -1799,6 +1809,14 @@ button {
border-radius: var(--radius); border-radius: var(--radius);
background: var(--color-surface); background: var(--color-surface);
overflow: hidden; overflow: hidden;
flex: 1;
min-height: 0;
}
/* The pond graph SVG gets its measured container size as viewBox (#131), so
it may fill the canvas exactly; the local panel keeps width-scaled sizing. */
.graph-page__canvas .force-graph {
height: 100%;
} }
/* Physics/rendering sliders (#123). */ /* Physics/rendering sliders (#123). */