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.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 { registerIntent } from "semiotic/ai" registerIntent({ id: "forecast-vs-actual", label: "Forecast vs. actual", description: "Compare a projected series against realized values.", familyHint: "time-series", }) // Capabilities can now score the "forecast-vs-actual" intent.
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