Skip to content

ADR-013: Charts, graphs, and data visualization - #308

Merged
dovvnloading merged 4 commits into
mainfrom
adr013-charts-and-data-viz
Aug 10, 2026
Merged

ADR-013: Charts, graphs, and data visualization#308
dovvnloading merged 4 commits into
mainfrom
adr013-charts-and-data-viz

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Problem

The chart node pipeline was dated in three ways: matplotlib rendered a PNG on the event loop at three call sites (generate, resize, export — 50-108ms each, measured), generation ran a bespoke 3-call extract/repair chain through a 693-line ChartDataAgent god object that swallowed cancellation into an error string and gave Anthropic no structured-output mode, and charts were static, non-interactive images with no theme awareness.

Change

Four stages, one branch:

  • 13.1 — a typed, versioned ChartSpec contract (bar/line/pie/histogram/sankey), validated on both the backend (canonicalize_chart_data) and the frontend (generated runtime validator).
  • 13.2 — a hand-rolled client-side interactive SVG renderer (web_ui/src/app/canvas/charts/), zero new npm dependencies, lazy-loaded into its own code-split chunk. Charts hover/zoom, follow light/dark theme tokens, and re-render on data edit with no backend round-trip.
  • 13.3 — chart generation retired the legacy ChartDataAgent pipeline in favor of the shared respond_json structured-output path, and gave Anthropic a real native structured-output mode (forced single-tool-call) for the first time. A real threading.Event cancellation token now reaches respond_json's own cancellation_event, so a cancelled generation actually stops instead of running to completion with its result discarded.
  • 13.4 — retired the backend's own display-PNG render outright rather than just moving it off-thread: add_chart_node/resize_chart haven't produced anything the frontend reads since stage 13.2 shipped, so chart_asset_id/chart_asset_version are gone from ChartState, the wire contract, and the frontend. The one render that's still live — the export/copy endpoint — moved off the event loop via asyncio.to_thread and gained a real SVG export option alongside PNG. The event-loop watchdog's render-call-site freeze (backend/tests/perf/test_loop_watchdog.py) is now permanently empty, a ratchet against any future synchronous chart render landing back on the loop.

Test plan

  • Full backend suite: pytest -q — 2400 passed, 17 skipped (5 pre-existing failures in test_native_dialogs.py, an unrelated local pywebview version mismatch, confirmed via git stash to pre-date this branch)
  • Full frontend suite: npm run check (types, lint, tests, codegen-drift, build, bundle-size gate) — clean
  • Live verification against a real running uvicorn process (not just TestClient): add_chart_node leaves image_assets empty, and both PNG (72,621 bytes) and SVG (21,540 bytes) exports return valid bytes through the real asyncio.to_thread path
  • Browser smoke check: app loads and connects with no console errors

dovvnloading and others added 4 commits August 10, 2026 10:13
Stamp an explicit version field onto the canonical chart-data shape
(CHART_SPEC_VERSION = 1) so a future migration can read which shape
it's looking at instead of sniffing field presence. Regenerated the
scene-state contract/codegen so the frontend ChartDataRow type picks
up the field.

Port canonicalize_chart_data's validation rules to a pure TypeScript
twin (web_ui/src/app/canvas/chartSpec.ts) so the client-side renderer
coming in stage 13.2 can defensively re-validate a spec before drawing
it, satisfying the ADR's "spec validates on both ends; a hand-authored
spec renders" exit criterion.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace ChartNodeView's backend-rendered <img> with a live SVG
renderer (web_ui/src/app/canvas/charts/) covering all five chart
types - bar, line, pie, histogram, sankey - with hover tooltips and
wheel-zoom/drag-pan, drawn straight from chartData with zero backend
round-trip. No new charting dependency: the renderer is hand-rolled
SVG/React (scale/tick/wrap math in chartScales.ts, a sankey layout
ported from graphlink_chart_rendering.py's own proven algorithm in
sankeyLayout.ts) and lazy-loaded into its own code-split chunk, same
pattern as ChatLibraryDialog/HelpDialog/SettingsDialog.

Theme-aware by construction: chartTheme.ts reads the real --gl-*
custom properties already injected into the page rather than
hardcoding hex, so the renderer is automatically correct for
whichever theme is active with no dependency on ADR-012's toggle.
The categorical series palette reuses GroupColorPicker's own
established "visually distinct hues" set rather than --gl-frame-*,
which is documented elsewhere as near-monochrome by design.

Re-validates chartData through stage 13.1's chartSpec.ts before
drawing anything, so a shape ChartRenderer can't render shows the
same kind of inline placeholder the backend's own never-blank
contract guarantees, never a crash. ChartNodeView's memo comparator
now compares chartData by reference (the renderer reads the whole
shape, not just .title) rather than chartAssetId/chartAssetVersion,
which the display path no longer consumes.

Widens vitest.setup.ts's own comment on why the shared ResizeObserver
stub stays a true no-op (an earlier attempt at making it fire broke
@xyflow/react's internal ResizeObserver usage under jsdom); the new
ChartRenderer.test.tsx installs its own local, firing stub instead.

Co-Authored-By: Claude <noreply@anthropic.com>
…structured output

Retires the legacy ChartDataAgent pipeline (727 lines) in favor of
backend/structured_output.py's respond_json, and gives Anthropic a real
native structured-output mode for the first time (forced single-tool-call,
matching the JSON-schema-response mode OpenAI/Gemini already had) instead
of the prior always-fallback path. Threads a real threading.Event
cancellation token from run_lifecycle.py's run_single_shot through
_call_chart_agent to api_provider.chat()'s cancellation_event kwarg, so a
cancelled chart generation now actually stops instead of running to
completion with its result discarded.

Also fixes 4 pre-existing failures in the local evals harness (goldens
missing the ChartSpec version field stamped since stage 13.1), found while
verifying this stage's own test coverage.

Co-Authored-By: Claude <noreply@anthropic.com>
…rt, add SVG

The client-side interactive renderer shipped in stage 13.2 already made
add_chart_node/resize_chart's own matplotlib render dead weight - nothing
has fetched chart_asset_id's bytes since. Rather than move that render
off-thread, this retires it outright: chart_asset_id/chart_asset_version
are gone from ChartState, the wire contract, and the frontend, and the
domain layer no longer touches image_assets for charts at all.

The one render that's still genuinely live - the export/copy endpoint -
moves off the event loop via asyncio.to_thread and gains a real SVG
export option alongside PNG. backend/tests/perf/test_loop_watchdog.py's
render-call-site freeze is now permanently empty, a ratchet against any
future synchronous chart render landing back on the loop.

Verified end-to-end against a live uvicorn process (not just TestClient):
add_chart_node leaves image_assets empty, and both PNG/SVG exports return
valid bytes through the real asyncio.to_thread path.

Co-Authored-By: Claude <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit ab6bed2 into main Aug 10, 2026
3 checks passed
@dovvnloading
dovvnloading deleted the adr013-charts-and-data-viz branch August 10, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant