Everything the intelligence layer knows about a chart —suggestions, thecapability matrix,variant discovery, and repair — reads from a capability descriptor: a structured artifact that declares what a chart is for, what data it fits, what it's good and bad at, and how to instantiate it. Built-in charts ship descriptors; a custom chart registers its own so the engine can reason about it like any first-class component.
The descriptor
A ChartCapability is both data and API — readable by the recommender, an agent, a snapshot test, or a human reviewer. By convention it lives in a *.capability.ts next to the component.
TS
import type { ChartCapability } from "semiotic/ai" export const lineChartCapability: ChartCapability = { component: "LineChart", family: "time-series", // ChartFamily taxonomy importPath: "semiotic/xy", // so generators emit the right import // Rubric — 1..5 each. Familiarity = how well-known to a general // audience; accuracy = how faithfully it represents the data; // precision = how readable individual values are. rubric: { familiarity: 5, accuracy: 4, precision: 3 }, // Hard requirements gate: return null if the chart can render this // data profile, or a human-readable reason why not. fits: (profile) => profile.primary.x ? null : "needs an x field (temporal or ordered)", // Per-intent suitability, 0..5. Missing intents default to 0. // A value may be a function for profile-aware scoring. intentScores: { trend: 5, "compare-series": (p) => ((p.seriesCount ?? 1) > 1 ? 5 : 3), "change-detection": 4, distribution: 1, "part-to-whole": 0, }, // Build a runnable config for a given dataset (+ chosen variant). buildProps: (profile, variant) => ({ xAccessor: profile.primary.x, yAccessor: profile.primary.y, ...(variant?.props ?? {}), }), }
Field by field
component / family / importPath — identity and where it's imported from (drives generated import lines).rubric — { familiarity, accuracy, precision }, each 1–5. The audience layer can override familiarity per-reader (see Audience Profiles).fits(profile) — the gate. null means "can render this"; a string is a human-readable reason it can't, surfaced verbatim by repair and suggestions.intentScores — a Partial<Record<IntentId, number | (profile) => number>>. The composite suggestion score reasons over these.semanticViability?(props, evidence) — an optional post-render check for marks that paint but cannot carry the chart’s intended meaning. Return stable warning/error diagnostics; errors make the evidencedegenerate.variants? — see below.caveats?(profile) — strings describing what the chart hides, distorts, or demands; surfaced in suggestion.caveats.buildProps(profile, variant?) — produce a spreadable, runnable config (<Component {...props} />).
The intent taxonomy
intentScores keys come from the built-in intent vocabulary — 13 communicative/analytical acts:
TS
type BuiltInIntentId = | "trend" | "compare-series" | "compare-categories" | "rank" | "part-to-whole" | "distribution" | "correlation" | "flow" | "hierarchy" | "geo" | "outlier-detection" | "composition-over-time" | "change-detection" // IntentId = BuiltInIntentId | (string & {}) — open for extension.
Extend the taxonomy with registerIntent when your domain has an act the built-ins don't capture:
TS
import { inferIntent, registerIntent, suggestCharts } from "semiotic/ai" registerIntent({ id: "forecast-vs-actual", label: "Forecast vs. actual", description: "Compare a projected series against realized values.", familyHint: "time-series", // Blend scores charts already own. Explicit capability scores for this // custom intent still take precedence when a chart declares one. composes: ["compare-series", "change-detection"], weights: { "compare-series": 2, "change-detection": 1 }, // Used only by inferIntent's opt-in schema mode. signals: { fieldNames: ["forecast", "actual"], minimumFieldMatches: 2, }, }) // The intent is immediately rankable without editing every capability. suggestCharts(data, { intent: "forecast-vs-actual" })
Natural-language inference remains the default. For a dataset with no user question, opt into schema mode; whole-token matching, minimum match counts, and a confidence floor keep incidental substrings from becoming intent signals.
TS
const inferred = inferIntent("", { mode: "schema", fields: [ { name: "forecast", kind: "numeric" }, { name: "actual", kind: "numeric" }, { name: "recorded_at", kind: "datetime" }, ], minimumConfidence: 3, }) // → { intent: "forecast-vs-actual", source: "field-name", ... } // With no fields array, schema mode treats the complete query as one unknown // field boundary, so compound signals can still match compact summaries. inferIntent("cloud region phase throughput partitions", { mode: "schema" })
Post-render semantic viability
fits(profile) prevents a chart from being recommended for an unsuitable dataset.semanticViability is the final, capability-owned oracle for direct renders: it runs against the actual HOC props and scene evidence after marks paint. This is where a ranking chart can report that every trajectory stayed at rank 1, or another family can detect its own semantically collapsed encoding.
TS
semanticViability: (props, evidence) => { const result = inspectMyEncoding(props, evidence) if (result.meaningful) return [] return [{ code: "MY_CHART_DEGENERATE_ENCODING", severity: "error", message: "Marks painted, but the selected fields collapse the encoding.", fix: "Choose a field with variation across the comparison groups.", metrics: { distinctValues: result.distinctValues }, }] }
renderChartWithEvidence keeps paint and meaning separate: a degenerate chart still hasstatus: ok andempty: false, plus semanticStatus: degenerateand structured semanticDiagnostics. Capabilities without a check report not-assessed; the server never assumes they were proven meaningful.
Variants
A ChartVariant encodes that a setting changes what a chart is good for. The suggestion engine emits one suggestion per (capability × variant) pair; intentDeltas are added to the base intentScores (clamped 0–5).
TS
variants: [ { key: "smooth", label: "Smooth trend", props: { curve: "monotoneX" }, intentDeltas: { trend: 1, "outlier-detection": -2 }, rubricDeltas: { precision: -1 }, // smoothing trades precision caveats: ["smoothing can hide individual outliers"], tags: ["smoothed"], }, ]
For proposing variants beyond this hand-curated list, seeVariant Discovery & Repair.
Registering a custom chart
Authoring a custom chart? Register its descriptor at runtime so it joins suggestions, the matrix, and repair alongside the built-ins.
TS
import { registerChartCapability, unregisterChartCapability } from "semiotic/ai" registerChartCapability(myWaffleCapability) // … myWaffle now appears in suggestCharts / proposeVariant / repairChartConfig unregisterChartCapability("WaffleChart") // remove it again
What fits() and intentScores receive
Both receive a ChartDataProfile fromprofileData(data) — the structural read of the dataset: per-role field candidates (primary.x/y/size/category/series/time), distinct counts (categoryCount, seriesCount), and shape flags (hasTimeAxis, monotonicX,hasHierarchy, …). Reason against the profile, never the raw rows.
TS
import { profileData } from "semiotic/ai" const profile = profileData(data) lineChartCapability.fits(profile) // null | string lineChartCapability.intentScores.trend // 5 (or a fn of profile)
Related: Chart Suggestions·Capability Matrix ·Variant Discovery & Repair· Custom Charts