Skip to content

feat(quadrant): native quadrant charts across every surface - #564

Merged
graphite-app[bot] merged 1 commit into
mainfrom
feat/quadrant-charts
Oct 29, 2026
Merged

graphite-app[bot] merged 1 commit into
mainfrom
feat/quadrant-charts

Conversation

@anandpant

@anandpant anandpant commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

What changes

Native quadrant charts (#555). A prompt like "effort vs impact quadrant chart of our backlog" or "Eisenhower matrix of my tasks" used to go through createCanvas with model-placed coordinates. Now the model sends axes, quadrant names and scores, and Sketchi lays the chart out.

  • Contract (quadrant). xAxis and yAxis are { title, low, high }: the end labels are free text, so an axis may run Urgent → Not urgent. quadrants is named by corner (topLeft = low x, high y; topRight, bottomLeft, bottomRight), which fixes the order. points: [{ label, x, y, group? }] (up to 30) has x and y in 0..1, so a score out of 100 is score / 100. No field takes coordinates. Validation returns repair hints for a score outside 0..1, a repeated label, more than 30 points or 6 groups, blank text, and text too long to place.
  • Layout (renderer/src/quadrant.ts):
    • A square plot of four tinted quadrants, each name in its outer corner. A name slides along the edge (then wraps, then moves inward) to stay clear of every dot and leave room for that dot's label.
    • The axes are arrows toward their high ends. x runs under the plot (low label left, high right, title between); y runs beside it (high label top, low bottom, title between).
    • Every dot sits at its exact position, drawn over the middle lines with a surface ring, so a point on a midline, edge or corner stays readable.
    • Each label takes the first clear spot beside its dot, most constrained point first, off the middle lines where it can, and only where it is nearer its own dot than any other dot.
    • A reader pairs a label with a dot level with it, or under or over it. So a corner spot that lines up with another dot within 48 px, and with neither its own dot's row nor its column, waits behind every leader: the label takes a spot that pairs clearly, else a short leader.
    • Crowded labels move out on leader lines. A leader crosses no other leader, label, name or other clump's dot, and leaders keep their far halves apart. A clump of near-identical dots stacks its labels in a column beside it. A label with no clear spot can push the one label in its way to a new spot.
    • A crowded chart grows its plot from 560 to 960 px until it reads. Past that, labels go a size smaller and the search runs longer. Only after that may a leader pass over another dot. A chart is rejected only after it has tried every plot size and the smaller labels. That fallback search runs within a fixed, deterministic budget of spot checks, so a chart that can never read fails fast.
    • Labels wrap at whole words and an authored newline is a line break. Both dividers and both axes are always drawn whole, and no label sits across a divider.
    • The plot starts large enough for every quadrant name's longest word and for the x axis's two end labels side by side.
    • Never published unreadable: a chart that still cannot place every label clear after growing, shrinking labels and searching longer is rejected (invalid_quadrant_chart, quadrant_too_large) with a repair hint: spread points that nearly coincide, shorten labels or names, or split the chart.
  • Excalidraw: a new quadrant-mark renderer role (square corners, roughness 0, no generated labels; patches keep it). Tints are grouped with their names, dots with their labels and leaders, axes with their labels, and legend keys with their names. Exports validate after a save and reload.
  • Code Mode and MCP: buildQuadrantChart, docs({ topic: "buildQuadrantChart" }), and three issue codes (quadrant_too_large, invalid_quadrant_point, invalid_quadrant_text). Docs cost, estimated at about 4 characters per token:
    • execute description head: +20 chars (the builder name), about 5 tokens. With wireframes the head would have reached 1,606 of its 1,600; shortening one helper line ("fix the spec per hint and rebuild") brings it back under;
    • code parameter: +508 chars (one summary line and one spec line), about 125 tokens (budget 12,400 → 12,920; tools/list budget 15,450 → 16,030);
    • topic, served on demand: 1,757 chars, about 440 tokens;
    • generation system prompt: about 920 chars, about 230 tokens.
  • Every other family surface:
    • generation IR rules, an example, a points count target and point/quadrant label targets;
    • generate --type quadrant (API and CLI) and sketchi create, with a changeset;
    • Studio build_quadrant_chart and POST /api/v1/quadrant-charts/build;
    • CLI PNGs skip the title banner (the chart draws its own title);
    • Storybook stories, both llms.txt, both READMEs, and docs/quadrant-charts.md.

Scenarios

From frontierclimb's draft §5:

  • effort-impact-quadrant (challenge): 15 items by score, 8 crowded into Quick wins, near-identical positions, Admin role templates on the effort midline, long labels.
  • eisenhower-matrix-quadrant (smoke): 8 tasks placed by quadrant only; the axis direction is left open.
  • competitor-positioning-quadrant (standard): 9 products by score, Budget and complex left empty, Our product in its own color.

Checks read every placement through the axis ends as drawn, so an axis that runs high to low passes when every name and point follows it. They also accept swapped axes when a prompt leaves them open.

  • axis:x|y (variable by title), axis-ends:x|y (stated end labels, either direction), quadrant:<key> (each name in the corner its conditions describe).
  • point:<key>, points-exact, position:<key> (±0.02 of score / 100, mirrored for a reversed axis), and quadrant-of:<key> for membership-only items (strictly inside, at least 0.03 from both midlines).
  • highlight:<key> (a group no other point shares) and empty:<key>.
  • quadrant-drawn (output): every dot within 0.5 px of its position with its label, each name inside its own quadrant, both axes' titles and end labels.
  • quadrant-layout (hint): no label overlaps text or a dot, every label sits in the plot nearest its own dot or on a leader to it, none lines up with another dot instead of its own, and no line crosses text.

Verification

  • Tests that fail on main: core contract (including zero scores, blank text, constructor/__proto__ names), renderer (tints and corner names, axes, exact dots, boundary points, a dot in a name's corner, crowded clusters on leaders, ten points at one spot, plot growth, groups, whole words, authored newlines), Excalidraw conversion and reload, Code Mode (documented example, patch keeps the role — red with that clause removed — repair codes, malformed points rejected with a hint rather than dropped, round-trip), generation, scenarios (each expected diagram passes; every mutation fails its own check; reversed axes pass), CLI offline failure and PNG without banner, MCP docs and budgets, route lists.
  • Fuzz (pnpm nx run diagram-excalidraw:quadrant-fuzz; CI runs 14): 700 random charts covering uniform spreads, clusters, round scores on midlines and edges, near-coinciding points, one-quadrant crowds, all points at one spot, long and non-Latin labels, long names, and groups. Results: 0 defects (exact dots, labels inside the plot and clear of every text and dot, nearest their own dot or on a leader, whole words, valid export). 10 shortfalls (1.4%, capped at 2%), all in crowded charts: in 8 a leader passes over another dot, and in 2 a label lines up with another dot because no leader in reach was clear. Render time: median 10 ms, p95 338 ms, max 1.2 s (crowded 25–30 point charts). A chart that can never read (30 points at one corner) is rejected in 0.4–0.6 s locally, after the bounded fallback. No fuzz chart is rejected; the fuzz counts a rejection as a defect, and also fails any divider or axis drawn in pieces or crossed by a label. The three scenarios take 7–50 ms.
  • Scenario CLI --fixture: effort-impact 42/42, Eisenhower 28/28, competitor 32/32.
  • Render identity: a harness hashes the scene of each of the 3 scenario specs, the 3 preview-replay specs and the 700 fuzz charts. The tier fix (5679928c) renders all 706 byte-identically to a97af669, the commit the QD564 A/B ran on. The pairing fix (421c98f6) changes effort-impact's scenario and replay specs and 210 fuzz charts. Eisenhower and competitor stay byte-identical. Across the fuzz, labels that line up with another dot drop from 253 to 2.
  • PNGs: I inspected every scenario plus stress cases (clusters at corners, duplicate corner points, 14-point corner clumps, long labels).
  • Local (at 421c98f6, on main e74b914a; the last rebase only took a version bump): pnpm nx affected -t typecheck,test,build --base=origin/main (18 projects), pnpm run check, pnpm run test:tools, pnpm nx build-storybook diagram-ui, pnpm run test:wrangler-dry-runs.
  • Preview MCP replay at ba2ecec0, and again at b9da89eb after restacking onto Venn and journey maps (pr-564-sketchi-studio MCP). For each scenario I wrote one buildQuadrantChart call from its prompt. Each was accepted on the first call in 0.8–1.4 s (8.5 s for the first call on a cold preview) and graded with --input: effort-impact 43/43, Eisenhower 29/29, competitor 33/33. The hosted PNGs match the local ones.
  • Real page (agent-browser -p browserbase, preview at ba2ecec0): the effort-impact artifact page renders with no page or console errors and survives a reload. In the editor, dragging the Dark mode dot moves its label with it as one group.

Known limits

  • A 25–30 point chart crowded into tight clusters can take up to about a second to place. In about 1.4% of fuzz charts, all of them crowded, a leader passes over another dot or a label lines up with another dot.

Shared files: additive edits to DIAGRAM_TYPES, documents.ts, catalog.ts, messages.ts, intent.ts, CLI generation types, llms.txt and canvas.ts (the new role), resolved alongside Venn, journey maps, Sankey and wireframes. scene.ts and other families' renderers are untouched; the quadrant palette reuses CHART_SERIES_COLORS.

This is one PR rather than a stack: type-structure.test.ts rejects a contract module that is not registered, and registering a family requires every pipeline stage.

Copy link
Copy Markdown
Collaborator Author

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • merge - adds this PR to the back of the merge queue
  • fast - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Web Preview

Status: deleted

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Icons Preview

Status: deleted

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Eval Harness Preview

Status: deleted

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Excalidraw Workspace Preview

Status: deleted

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Playground / Studio Preview

Status: deleted

@anandpant
anandpant force-pushed the feat/quadrant-charts branch from ba2ecec to b9da89e Compare October 29, 2026 16:25
@anandpant
anandpant force-pushed the feat/quadrant-charts branch from b9da89e to ab42725 Compare October 29, 2026 16:49
@anandpant
anandpant force-pushed the feat/quadrant-charts branch 2 times, most recently from 29b60cf to a97af66 Compare October 29, 2026 17:24
@graphite-app

graphite-app Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Merge activity

## What changes

Native quadrant charts (#555). A prompt like "effort vs impact quadrant chart of our backlog" or "Eisenhower matrix of my tasks" used to go through `createCanvas` with model-placed coordinates. Now the model sends axes, quadrant names and scores, and Sketchi lays the chart out.

- **Contract** (`quadrant`). `xAxis` and `yAxis` are `{ title, low, high }`: the end labels are free text, so an axis may run Urgent → Not urgent. `quadrants` is named by corner (`topLeft` = low x, high y; `topRight`, `bottomLeft`, `bottomRight`), which fixes the order. `points: [{ label, x, y, group? }]` (up to 30) has `x` and `y` in 0..1, so a score out of 100 is score / 100. No field takes coordinates. Validation returns repair hints for a score outside 0..1, a repeated label, more than 30 points or 6 groups, blank text, and text too long to place.
- **Layout** (`renderer/src/quadrant.ts`):
  - A square plot of four tinted quadrants, each name in its outer corner. A name slides along the edge (then wraps, then moves inward) to stay clear of every dot and leave room for that dot's label.
  - The axes are arrows toward their high ends. x runs under the plot (low label left, high right, title between); y runs beside it (high label top, low bottom, title between).
  - Every dot sits at its exact position, drawn over the middle lines with a surface ring, so a point on a midline, edge or corner stays readable.
  - Each label takes the first clear spot beside its dot, most constrained point first, off the middle lines where it can, and only where it is nearer its own dot than any other dot.
  - A reader pairs a label with a dot level with it, or under or over it. So a corner spot that lines up with another dot within 48 px, and with neither its own dot's row nor its column, waits behind every leader: the label takes a spot that pairs clearly, else a short leader.
  - Crowded labels move out on leader lines. A leader crosses no other leader, label, name or other clump's dot, and leaders keep their far halves apart. A clump of near-identical dots stacks its labels in a column beside it. A label with no clear spot can push the one label in its way to a new spot.
  - A crowded chart grows its plot from 560 to 960 px until it reads. Past that, labels go a size smaller and the search runs longer. Only after that may a leader pass over another dot. A chart is rejected only after it has tried every plot size and the smaller labels. That fallback search runs within a fixed, deterministic budget of spot checks, so a chart that can never read fails fast.
  - Labels wrap at whole words and an authored newline is a line break. Both dividers and both axes are always drawn whole, and no label sits across a divider.
  - The plot starts large enough for every quadrant name's longest word and for the x axis's two end labels side by side.
  - **Never published unreadable**: a chart that still cannot place every label clear after growing, shrinking labels and searching longer is rejected (`invalid_quadrant_chart`, `quadrant_too_large`) with a repair hint: spread points that nearly coincide, shorten labels or names, or split the chart.
- **Excalidraw**: a new `quadrant-mark` renderer role (square corners, roughness 0, no generated labels; patches keep it). Tints are grouped with their names, dots with their labels and leaders, axes with their labels, and legend keys with their names. Exports validate after a save and reload.
- **Code Mode and MCP**: `buildQuadrantChart`, `docs({ topic: "buildQuadrantChart" })`, and three issue codes (`quadrant_too_large`, `invalid_quadrant_point`, `invalid_quadrant_text`). Docs cost, estimated at about 4 characters per token:
  - execute description head: +20 chars (the builder name), about 5 tokens. With wireframes the head would have reached 1,606 of its 1,600; shortening one helper line ("fix the spec per hint and rebuild") brings it back under;
  - `code` parameter: +508 chars (one summary line and one spec line), about 125 tokens (budget 12,400 → 12,920; tools/list budget 15,450 → 16,030);
  - topic, served on demand: 1,757 chars, about 440 tokens;
  - generation system prompt: about 920 chars, about 230 tokens.
- **Every other family surface**:
  - generation IR rules, an example, a `points` count target and `point`/`quadrant` label targets;
  - `generate --type quadrant` (API and CLI) and `sketchi create`, with a changeset;
  - Studio `build_quadrant_chart` and `POST /api/v1/quadrant-charts/build`;
  - CLI PNGs skip the title banner (the chart draws its own title);
  - Storybook stories, both `llms.txt`, both READMEs, and `docs/quadrant-charts.md`.

## Scenarios

From frontierclimb's draft §5:

- `effort-impact-quadrant` (challenge): 15 items by score, 8 crowded into Quick wins, near-identical positions, Admin role templates on the effort midline, long labels.
- `eisenhower-matrix-quadrant` (smoke): 8 tasks placed by quadrant only; the axis direction is left open.
- `competitor-positioning-quadrant` (standard): 9 products by score, Budget and complex left empty, Our product in its own color.

Checks read every placement through the axis ends as drawn, so an axis that runs high to low passes when every name and point follows it. They also accept swapped axes when a prompt leaves them open.

- `axis:x|y` (variable by title), `axis-ends:x|y` (stated end labels, either direction), `quadrant:<key>` (each name in the corner its conditions describe).
- `point:<key>`, `points-exact`, `position:<key>` (±0.02 of score / 100, mirrored for a reversed axis), and `quadrant-of:<key>` for membership-only items (strictly inside, at least 0.03 from both midlines).
- `highlight:<key>` (a group no other point shares) and `empty:<key>`.
- `quadrant-drawn` (output): every dot within 0.5 px of its position with its label, each name inside its own quadrant, both axes' titles and end labels.
- `quadrant-layout` (hint): no label overlaps text or a dot, every label sits in the plot nearest its own dot or on a leader to it, none lines up with another dot instead of its own, and no line crosses text.

## Verification

- **Tests that fail on main**: core contract (including zero scores, blank text, `constructor`/`__proto__` names), renderer (tints and corner names, axes, exact dots, boundary points, a dot in a name's corner, crowded clusters on leaders, ten points at one spot, plot growth, groups, whole words, authored newlines), Excalidraw conversion and reload, Code Mode (documented example, patch keeps the role — red with that clause removed — repair codes, malformed `points` rejected with a hint rather than dropped, round-trip), generation, scenarios (each expected diagram passes; every mutation fails its own check; reversed axes pass), CLI offline failure and PNG without banner, MCP docs and budgets, route lists.
- **Fuzz** (`pnpm nx run diagram-excalidraw:quadrant-fuzz`; CI runs 14): 700 random charts covering uniform spreads, clusters, round scores on midlines and edges, near-coinciding points, one-quadrant crowds, all points at one spot, long and non-Latin labels, long names, and groups. Results: 0 defects (exact dots, labels inside the plot and clear of every text and dot, nearest their own dot or on a leader, whole words, valid export). 10 shortfalls (1.4%, capped at 2%), all in crowded charts: in 8 a leader passes over another dot, and in 2 a label lines up with another dot because no leader in reach was clear. Render time: median 10 ms, p95 338 ms, max 1.2 s (crowded 25–30 point charts). A chart that can never read (30 points at one corner) is rejected in 0.4–0.6 s locally, after the bounded fallback. No fuzz chart is rejected; the fuzz counts a rejection as a defect, and also fails any divider or axis drawn in pieces or crossed by a label. The three scenarios take 7–50 ms.
- **Scenario CLI `--fixture`**: effort-impact 42/42, Eisenhower 28/28, competitor 32/32.
- **Render identity**: a harness hashes the scene of each of the 3 scenario specs, the 3 preview-replay specs and the 700 fuzz charts. The tier fix (`5679928c`) renders all 706 byte-identically to `a97af669`, the commit the QD564 A/B ran on. The pairing fix (`421c98f6`) changes effort-impact's scenario and replay specs and 210 fuzz charts. Eisenhower and competitor stay byte-identical. Across the fuzz, labels that line up with another dot drop from 253 to 2.
- **PNGs**: I inspected every scenario plus stress cases (clusters at corners, duplicate corner points, 14-point corner clumps, long labels).
- **Local** (at `421c98f6`, on main `e74b914a`; the last rebase only took a version bump): `pnpm nx affected -t typecheck,test,build --base=origin/main` (18 projects), `pnpm run check`, `pnpm run test:tools`, `pnpm nx build-storybook diagram-ui`, `pnpm run test:wrangler-dry-runs`.
- **Preview MCP replay** at `ba2ecec0`, and again at `b9da89eb` after restacking onto Venn and journey maps (`pr-564-sketchi-studio` MCP). For each scenario I wrote one `buildQuadrantChart` call from its prompt. Each was accepted on the first call in 0.8–1.4 s (8.5 s for the first call on a cold preview) and graded with `--input`: effort-impact 43/43, Eisenhower 29/29, competitor 33/33. The hosted PNGs match the local ones.
- **Real page** (`agent-browser -p browserbase`, preview at `ba2ecec0`): the effort-impact artifact page renders with no page or console errors and survives a reload. In the editor, dragging the Dark mode dot moves its label with it as one group.

## Known limits

- A 25–30 point chart crowded into tight clusters can take up to about a second to place. In about 1.4% of fuzz charts, all of them crowded, a leader passes over another dot or a label lines up with another dot.

Shared files: additive edits to `DIAGRAM_TYPES`, `documents.ts`, `catalog.ts`, `messages.ts`, `intent.ts`, CLI generation types, `llms.txt` and `canvas.ts` (the new role), resolved alongside Venn, journey maps, Sankey and wireframes. `scene.ts` and other families' renderers are untouched; the quadrant palette reuses `CHART_SERIES_COLORS`.

This is one PR rather than a stack: `type-structure.test.ts` rejects a contract module that is not registered, and registering a family requires every pipeline stage.
@graphite-app
graphite-app Bot force-pushed the feat/quadrant-charts branch from bb17240 to 854907c Compare October 29, 2026 18:43
@graphite-app
graphite-app Bot merged commit 854907c into main Oct 29, 2026
14 checks passed
@graphite-app
graphite-app Bot deleted the feat/quadrant-charts branch October 29, 2026 18:44
@graphite-app
graphite-app Bot deployed to production October 29, 2026 18:44 Active
@graphite-app
graphite-app Bot deployed to production October 29, 2026 18:44 Active
@graphite-app
graphite-app Bot deployed to production October 29, 2026 18:44 Active
@graphite-app
graphite-app Bot deployed to production October 29, 2026 18:44 Active
@graphite-app
graphite-app Bot deployed to production October 29, 2026 18:44 Active
@github-actions github-actions Bot mentioned this pull request Oct 29, 2026

This branch was successfully deployed

1 active and 1 inactive deployments
staging — 854907c0 Deployed Oct 29, 2026 by graphite-app[bot] via cleanup-preview (eval-harness) #1063
production — 854907c0 Deployed Oct 29, 2026 by graphite-app[bot] via deploy (eval-harness) #239
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