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:
- Describe the shape, not the chart. A consumer that has a
MetricEnvelope-shaped answer callsinferVizForEnvelope(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-sentencerationaleso an AI agent can read why a template fired rather than trust an opaque score. - Or describe the intent. The newer UI-DS2 selector
selectVisualization(req)(router.ts) takes aVizIntent(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 bydataShapeTagsoverlap. 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." - Look up the full entry. With a template id in hand,
lookupVizTemplate(id)returns theVizTemplate:name,description,inputShape(a TypeScript-style type literal — humans and AI can parse it without JSON-Schema round-tripping),whenToUse,whenNotToUse,aiRoutingTags,rendererPath, optionaldataSchema/exampleData/documentation, and merged taxonomy (chartKind,interactionPatterns,accessibilityProfile). - Render. A surface imports the renderer at
rendererPath(a real React component, Recharts-backed) and pairs it with aVizPayload({ 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. - AI consumers reach the same three discovery functions over MCP:
toolbox.viz.list-templates(optionally tag-filtered),toolbox.viz.lookup-template, andtoolbox.viz.infer-template-for-envelope, plustoolbox.viz.renderfor 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 mislead —
viz.forest-plotrenders 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 bridge —
viz.waterfallwalks a starting total through hires and exits to an ending total, the natural fit forworkforce-planningreconciliation rather than a faked stacked bar. - Before/after program evaluation —
viz.slopegraphshows each item's pre/post change across exactly two ordered points, fit forprogram-evaluationlift reads. - Comp positioning by level —
viz.compa-ratio-distributionstacks per-level histograms with median + IQR markers, the canonical view foranycompband positioning. - AI-authored Insight Cards — an agent emits a
VizPayload(templateId+ data), and the toolbox renders the chart server-side; the templates back theInsightCardSchema.visual.kindenum (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'svisual.kindresolves 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
VizPayloadshape mirrors Performix'sInsightChartcontract; consumers vendorcontracts/types.tsand 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 layer —
min-n-suppressionis the same min-N gatedata-anonymizerenforces 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.sourcefield). - 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+ theVIZ_TEMPLATE_TAXONOMY/VIZ_TEMPLATE_ROUTINGmaps, with 32 Recharts-backed React renderer components undertemplates/. The declarativeVizTemplatecontract withinputShape,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-rulePORTFOLIO_HYGIENE_RULESspine + elevenRETIRE_DEFAULTS+ per-chart-kindCHART_HYGIENE. Three MCP discovery tools (toolbox.viz.list-templates,lookup-template,infer-template-for-envelope) plustoolbox.viz.render. Satori SSR for 15 supported template ids (render/index.ts). The/viz-galleryshowcase page renders templates from theirgetSampleData(). 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.tsheader), not yet wired. The viz feedback model (learning per-userexcludeTemplateIds) is scaffolded in the selection shape but (TBD). SSR coverage is 15 of 37 templates; the remainder render client-only (TBD — server-render coverage). Thechart-palette.tsdesign-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.