ADR-013: Charts, graphs, and data visualization - #308
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
ChartDataAgentgod 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:
ChartSpeccontract (bar/line/pie/histogram/sankey), validated on both the backend (canonicalize_chart_data) and the frontend (generated runtime validator).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.ChartDataAgentpipeline in favor of the sharedrespond_jsonstructured-output path, and gave Anthropic a real native structured-output mode (forced single-tool-call) for the first time. A realthreading.Eventcancellation token now reachesrespond_json's owncancellation_event, so a cancelled generation actually stops instead of running to completion with its result discarded.add_chart_node/resize_charthaven't produced anything the frontend reads since stage 13.2 shipped, sochart_asset_id/chart_asset_versionare gone fromChartState, the wire contract, and the frontend. The one render that's still live — the export/copy endpoint — moved off the event loop viaasyncio.to_threadand 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
pytest -q— 2400 passed, 17 skipped (5 pre-existing failures intest_native_dialogs.py, an unrelated localpywebviewversion mismatch, confirmed viagit stashto pre-date this branch)npm run check(types, lint, tests, codegen-drift, build, bundle-size gate) — cleanTestClient):add_chart_nodeleavesimage_assetsempty, and both PNG (72,621 bytes) and SVG (21,540 bytes) exports return valid bytes through the realasyncio.to_threadpath