Visualization Catalog — explainer

The Visualization Catalog is the toolbox's shared chart library: a developer or AI agent describes the shape of an answer, and the catalog hands back the right honest chart — already vetted against a portfolio-wide set of chart-hygiene rules.

A People Analytics Toolbox cross-cutting primitive (PAT-43), not a spoke. It lives at src/lib/visualizations/; the catalog contract is at semver 0.4.0 (src/lib/visualizations/contracts/types.ts). Every claim below is grounded in that code; anything not yet built is marked (TBD). This earns the fuller, multi-movement treatment because it is a surface a developer actually meets — directly, via import or MCP — across every spoke.


Lead

Give the catalog the shape of your data and the intent of your question; it gives you back a named, hygiene-checked chart template — the same one whether the caller is a spoke, a consumer surface, or an AI agent.

Manifesto — why it exists

Across nineteen live spokes and the surfaces that consume them, the same chart problem recurs: a number with a confidence interval, a before/after pair, a headcount reconciliation that should be a bridge, a pay-gap estimate that should never be a mirrored bar. Left to per-spoke discretion, each one re-decides — and re-decides badly, because the default chart is usually the wrong one. A sampled estimate gets drawn as a mean-only bar with no uncertainty. A Likert distribution gets a diverging stacked bar that no one can read. A forecast gets a single confident line.

The Visualization Catalog removes that decision from each spoke and centralizes it once, declaratively. It is two things at once:

  • A registry of named templates, each with a stable id (viz.forest-plot, viz.waterfall, …), a declared input shape, AI-routing tags, and a real React renderer.
  • A discipline — a portfolio-wide hygiene rule set plus a list of misleading defaults to retire, attached to the templates so the wrong chart is hard to reach and the right one comes with its honesty guidance built in.

The differentiation beat, stated as a contrast:

  • vs. doing it by hand in each spoke — every spoke would re-pick a chart per question and re-litigate zero-baselines, uncertainty display, and min-N suppression every time. The catalog decides once; spokes inherit.
  • vs. a generic BI tool — generic BI optimizes for "any chart you ask for," including the misleading ones (gauges, radar, dual-axis, pie-as-default). The catalog optimizes for the honest chart for a given analytical intent, and actively lists eleven misleading defaults with their honest replacements (RETIRE_DEFAULTS).
  • vs. a hand-rolled chart component grab-bag — a folder of one-off React charts has no contract, no routing, no AI-discoverability, and no shared hygiene. The catalog is a versioned contract (0.4.0) that consumers vendor and AI agents query.

Visual — Tier B (in-repo rule reference). The portfolio hygiene spine, verbatim from PORTFOLIO_HYGIENE_RULES (hygiene.ts), severity in brackets:

  • [error] zero-baseline-bars — length encodings start at zero; narrow the scale and you must switch bar → dot.
  • [error] no-dual-axis — use indexed-to-100, small multiples, or separate aligned panels.
  • [warn] series-cap — default ≤4 emphasized series; 7 distinct hues is a hard cap.
  • [error] uncertainty-first — sampled / modeled / forecast values must show their uncertainty, labelled.
  • [error] min-n-suppression — segment charts route the min-N privacy gate; below threshold, suppress or roll up visibly, never silently drop.
  • [error] null-not-zero — never render missing as zero; break lines at gaps.
  • [warn] color-semantics — qualitative hues for categories, sequential for magnitude, diverging only around a real midpoint.
  • [error] log-positive-only — log scale only on strictly-signed domains; disclose the transform.
  • [error] no-decoration-as-data — no 3D, bevels, gradient fills, pictograms-as-measure.
  • [warn] direct-label — direct labels over legends; the title states the finding.
  • [error] accessibility — contrast minimums, never color as the only signal, alt text + data-table access.
  • [warn] rates-not-counts — cross-group comparisons use rates with a stated denominator.

Walkthrough — how it actually works

There is no HTTP API surface for the catalog — it is a src/lib/ primitive consumed two ways: imported directly by toolbox surfaces, or queried over MCP by AI-native consumers. A developer's path through it:

  1. Describe the shape, not the chart. A consumer that has a MetricEnvelope-shaped answer calls inferVizForEnvelope(req) (registry.ts) with shape facts only: { envelopeKind, hasCi, cohortDimension, periodCount, segmentCount }. The router is deliberately rule-based, not statistical — every branch is explicit and auditable, and it returns a one-sentence rationale so an AI agent can read why a template fired rather than trust an opaque score.
  2. Or describe the intent. The newer UI-DS2 selector selectVisualization(req) (router.ts) takes a VizIntent (change-over-time, distribution, deviation, flow, part-to-whole, ranking, uncertainty, single-value-vs-target, magnitude, correlation, spatial) as a hard filter, then ranks survivors by dataShapeTags overlap. It also returns graceful-degradation advisories from category/sample-size thresholds — e.g. ">12 categories: switch bars → ranked dot plot"; "n < 5: show raw points, apply the min-N gate."
  3. Look up the full entry. With a template id in hand, lookupVizTemplate(id) returns the VizTemplate: name, description, inputShape (a TypeScript-style type literal — humans and AI can parse it without JSON-Schema round-tripping), whenToUse, whenNotToUse, aiRoutingTags, rendererPath, optional dataSchema/exampleData/documentation, and merged taxonomy (chartKind, interactionPatterns, accessibilityProfile).
  4. Render. A surface imports the renderer at rendererPath (a real React component, Recharts-backed) and pairs it with a VizPayload ({ templateId, data, config?, source? }). For image output (Insight Cards, share images, OG previews), the satori SSR path (render/index.ts) renders a supported subset server-side.
  5. AI consumers reach the same three discovery functions over MCP: toolbox.viz.list-templates (optionally tag-filtered), toolbox.viz.lookup-template, and toolbox.viz.infer-template-for-envelope, plus toolbox.viz.render for the SSR path.

Differentiation beat: the developer's real question isn't "what charts do you have" — it's "which chart won't lie about my data, and why." Every template carries an explicit whenNotToUse that names the misleading alternative it replaces (forest plot: "do not use mirrored/butterfly bars… they force mental subtraction and hide uncertainty"). The catalog answers the question and shows its work.

Visual — Tier B (the routing path, FROM→TO). A trend envelope with confidence intervals, traced through inferVizForEnvelope:

  • INPUT (shape facts only): envelopeKind: "trend" · hasCi: true · segmentCount: 1 · periodCount: 12
  • ROUTER branch fired: single-series trend WITH confidence bands
  • OUTPUT: recommended: "viz.trend-with-ci-bands" · alternatives ["viz.sparkline-grid"]
  • RATIONALE (returned verbatim): "Single-series trend with confidence bands — the CI band foregrounds methodological honesty about period-to-period noise."

What it enables

Concrete uses a practitioner or developer would recognize:

  • A pay-fairness gap chart that can't misleadviz.forest-plot renders each segment's adjusted gap as a dot with its own confidence interval against a zero reference line, the canonical honest alternative to mirrored bars.
  • Headcount reconciliation as a bridgeviz.waterfall walks a starting total through hires and exits to an ending total, the natural fit for workforce-planning reconciliation rather than a faked stacked bar.
  • Before/after program evaluationviz.slopegraph shows each item's pre/post change across exactly two ordered points, fit for program-evaluation lift reads.
  • Comp positioning by levelviz.compa-ratio-distribution stacks per-level histograms with median + IQR markers, the canonical view for anycomp band positioning.
  • AI-authored Insight Cards — an agent emits a VizPayload (templateId + data), and the toolbox renders the chart server-side; the templates back the InsightCardSchema.visual.kind enum (PAT-34), so the same catalog drives both the live React surfaces and the rendered card images.
  • Graceful degradation at scale — a 200-category breakdown returns an advisory to use top-N + other plus a searchable table, instead of silently producing an unreadable bar chart.

How it fits in the toolbox

The catalog is cross-cutting — every spoke is a candidate emitter and every consumer surface is a candidate renderer. It composes by contract only:

  • Backs the src/lib/insight-player/ Insight Card contract (PAT-34): a card's visual.kind resolves to a catalog template, so the same library serves the analytics-player, metric-market, and decision-wizard surfaces.
  • Consumed by surfaces — the metric-market card workbench and decision-wizard import templates directly (HTTP + contracts discipline does not apply here because it is a src/lib/ primitive, imported, not a spoke API).
  • Consumed by AI agents — over MCP via toolbox.viz.*, registered as a virtual spoke in the gateway discovery router (src/lib/visualizations/mcp/register.ts).
  • Vendored externally — the VizPayload shape mirrors Performix's InsightChart contract; consumers vendor contracts/types.ts and wire AI-emitted charts to toolbox-rendered images. The catalog is the source of truth; bump-via-toolbox is the only path to evolve it.
  • Shares hygiene with the data layermin-n-suppression is the same min-N gate data-anonymizer enforces on rollups; the catalog refuses to render below threshold rather than re-deciding privacy per chart.

Visual — Tier B (typographic data-flow). Spoke MetricEnvelope / AI agent → inferVizForEnvelope · selectVisualization → VizTemplate (id + inputShape + hygiene) → renderer (React live | satori SSR) → { analytics-player · metric-market · decision-wizard · Insight Card image }.

One load-bearing worked example

A real, end-to-end pass through the catalog for a pay-fairness question — every value below is the actual getSampleData() fixture shipped on viz.forest-plot (templates/ForestPlot.tsx), not invented.

1. The question. "Is there an adjusted pay gap by department, and is it real or noise?" Five departments, each with a model-adjusted gap estimate in log points (focal − reference), its confidence interval, and a headcount n.

2. Routing. The answer is an estimate-with-uncertainty across groups, so the intent is deviation/uncertainty and the data shape is categorical-x + has-benchmark. The router (VIZ_TEMPLATE_ROUTING) tags viz.forest-plot with exactly intents: ["deviation","ranking","uncertainty","magnitude"], dataShapeTags: ["categorical-x","has-benchmark","paired-comparison"] — it wins.

3. The template entry (lookupVizTemplate("viz.forest-plot")).

  • inputShape: { rows: Array<{ label: string; estimate: number; ciLower: number; ciUpper: number; n?: number }>; reference?: number }
  • whenNotToUse (verbatim): "Do not use mirrored/butterfly bars or two-bar comparisons when the output is a gap estimate — they force mental subtraction and hide uncertainty… Do not infer significance from two overlapping group CIs; this chart shows the difference's own interval."

4. The real data rendered. metricLabel: "Adjusted pay gap (log points, focal − reference)", reference: 0:

  • Engineering — estimate +0.004, CI [-0.011, +0.019], n = 1840 → interval straddles zero: no detectable gap.
  • Sales — estimate -0.028, CI [-0.047, -0.009], n = 1120 → interval entirely below zero: a real negative gap.
  • Marketing — estimate +0.031, CI [+0.012, +0.050], n = 540 → interval entirely above zero: a real positive gap.
  • Customer Success — estimate -0.006, CI [-0.022, +0.010], n = 760 → straddles zero: no detectable gap.
  • Finance — estimate -0.041, CI [-0.068, -0.014], n = 320 → entirely below zero: the largest real negative gap.

5. What a practitioner does with it. Reading dot = estimate, line = CI, vertical rule = zero, they act only on Sales, Marketing, and Finance (intervals clear of zero) and correctly do not act on Engineering or Customer Success. The chart enforces the honest read the uncertainty-first hygiene rule demands; a mirrored bar would have hidden which gaps are noise.

Commercialization / packaging

The Visualization Catalog is infrastructure, not a sold product. It sits behind the surfaces and the Insight Card contract a buyer meets, and it is the design-system spine the portfolio's other properties vendor from.

  • Data-license posture: the catalog ships templates and rules, not data — there are no licensing constraints on the library itself. The data a template renders carries whatever provenance its source spoke attaches (the VizPayload.source field).
  • Packaging / tiers: any pricing or packaged-offering placement is (TBD) — not earned, so not stated.

Visual — (TBD — product-tier placement diagram.)

Current status

Grounded in the real code (catalog contract 0.4.0; primitive at src/lib/visualizations/):

  • Shipped: 37 named templates registered across VIZ_TEMPLATES + the VIZ_TEMPLATE_TAXONOMY/VIZ_TEMPLATE_ROUTING maps, with 32 Recharts-backed React renderer components under templates/. The declarative VizTemplate contract with inputShape, whenToUse/whenNotToUse, AI-routing tags, and merged chart-kind/interaction/accessibility taxonomy. The rule-based router (inferVizForEnvelope) and the intent-based selector (selectVisualization) with graceful-degradation advisories. The twelve-rule PORTFOLIO_HYGIENE_RULES spine + eleven RETIRE_DEFAULTS + per-chart-kind CHART_HYGIENE. Three MCP discovery tools (toolbox.viz.list-templates, lookup-template, infer-template-for-envelope) plus toolbox.viz.render. Satori SSR for 15 supported template ids (render/index.ts). The /viz-gallery showcase page renders templates from their getSampleData(). Registered in the contract registry (toolbox.viz).
  • In flight / planned: the router is explicitly a baseline — the refined weighted prefer/avoid rules + hygiene gating from the viz deep-research pass slot into the same in/out shape (per router.ts header), not yet wired. The viz feedback model (learning per-user excludeTemplateIds) is scaffolded in the selection shape but (TBD). SSR coverage is 15 of 37 templates; the remainder render client-only (TBD — server-render coverage). The chart-palette.ts design-kit helper is noted elsewhere as hardcoded hex pending tokenization (TBD — token-driven palette).

Visual — Tier B (catalog inventory). 37 templates · 32 React renderers · 15 SSR-supported · 3 MCP discovery tools + 1 render tool · 12 portfolio hygiene rules · 11 retired defaults · contract 0.4.0.

The vision

A portfolio where no spoke ever picks a misleading chart again — because the honest one for any analytical intent is one declarative call away, learns which charts each reader actually wants, and renders identically whether a human, a surface, or an AI agent asked for it.

The trajectory is from a rule-based router toward a learned-yet-auditable selector: the deep-research weighting replaces the flat scoring without changing the contract, and the viz feedback loop turns "this reader never wants a pie/treemap" into a concrete excludeTemplateIds filter — the same many-signals-make-content-better loop the rest of the portfolio runs, applied to chart choice. The hygiene rules graduate from guidance attached to templates to a lint pass that blocks the retired defaults at author time.