Semiotic charts ship capability descriptors alongside their components. Each chart declares what data shapes it serves, which intents it answers, what variants change those answers, and which props to use for a given dataset. TheuseChartSuggestions hook walks the registry and returns a ranked, ready-to-render list. Heuristic only — no LLM call. Pair withuseChartInterrogation to let an LLM re-rank or narrate.
Interactive demo
Pick a dataset and (optionally) an intent. The same profile is evaluated against every registered capability and its variants. The top suggestion's props drop straight into the matching chart.
Two regions, six months of revenue. Time x-axis, categorical series.
Top renderable suggestion: AreaChart · Stepped
Engine's actual top pick was semiotic.recipe.waffle.v0 — not included in this demo's render map. See the all-suggestions sidebar for the full ranking.
All suggestions (ranked)
semiotic.recipe.waffle.v04.6/5
fam 4 · acc 4 · prec 2
x = month, y = revenue; 2 series detected on field "region"; Repeated units directly expose how categories compose a total.; Unit counts support broad comparisons, though bars remain more precise.; Why leave the catalog: A pie chart would show share but provide less unit-level evidence and less interaction surface.; Designed for visual reception; Portable registered recipe
precise comparison may be harder than a bar chart; individual cells can become navigation noise; too many categories; too many units; false precision; treating each cell as a separate semantic datum; Rounded cell allocation communicates only the precision supported by the unit count.; Use a ranked bar when exact pairwise comparison is the primary task.
ParallelCoordinatesRecipe4.6/5
fam 3 · acc 4 · prec 2
x = month, y = revenue; 2 series detected on field "region"; Crossing and parallel trajectories reveal multivariate relationships.; Unusual profiles remain visible across several measures at once.; Why leave the catalog: A scatterplot exposes only two dimensions at a time; a table hides profile shape.; Designed for visual reception; Portable registered recipe
Overplotting can hide density and individual paths as row count grows.; Independent axis scales make line slopes unsuitable for direct magnitude comparison.; Do not imply that segment slope compares like units across adjacent axes.; Do not use an unreadable wall of paths without filtering, sampling, or aggregation.
AreaChart · Stepped4.1/5
fam 4 · acc 3 · prec 3
x = month, y = revenue; 2 series detected on field "region"; 12 rows is in the sweet spot for this chart
showing only the leading "region" series — for multi-series comparison use LineChart or DifferenceChart
DifferenceChart4.0/5
fam 3 · acc 4 · prec 4
x = month, y = revenue; 2 series detected on field "region"
BumpChart · Magnitude ribbons3.6/5
fam 3 · acc 4 · prec 4
x = month, y = revenue; 2 series detected on field "region"; 12 rows is in the sweet spot for this chart
AreaChart · Smooth gradient3.6/5
fam 4 · acc 3 · prec 3
x = month, y = revenue; 2 series detected on field "region"; 12 rows is in the sweet spot for this chart
showing only the leading "region" series — for multi-series comparison use LineChart or DifferenceChart
Shape profile
{ "rowCount": 12, "primary": { "x": "month", "y": "revenue", "series": "region" }, "seriesCount": 2, "uniqueXCount": 6, "hasRepeatedX": true, "monotonicX": false, "hasTimeAxis": false }How it composes
profileData(data) infers candidate x/y/series/category fields, distinct counts, monotonicity, and structure (hierarchy/network/geo). Identifier and field-role hints are applied before inference.- For each capability:
fits(profile) is a hard gate (returns null to pass). intentScores are evaluated (numbers or profile-aware functions).- Variants apply additive
intentDeltas and rubricDeltas. - Suggestions are sorted by the requested intent (or mean across intents).
buildProps(profile, variant) returns spreadable props for the chart. The capability's field policy rejects an identifier emitted as a measure.- Each result carries a machine-readable
propContract so a generic renderer knows whether chart-HOC defaults are safe.
Identifiers and field-role hints
Numeric IDs and unique string keys are structurally plausible but poor visual measures or categories. Declare them as identifiers before profiling. Use semantic roles for unusual field names, or exact encoding roles when the normal type/name heuristic is not enough. The same options work with suggestCharts, useChartSuggestions, and the MCP suggestCharts tool.
TS
import { profileData, rederiveProfile, suggestCharts } from "semiotic/ai" const options = { identifiers: ["id", "accountId"], fieldRoles: { recordedAt: "temporal", throughput: "measure", regionCode: "category", }, } const profile = profileData(rows, options) // id/accountId cannot appear in any encoding candidate or primary role. const revised = rederiveProfile(profile, { primary: { category: "regionCode", y: "throughput" }, }) // primary, categoryCount, seriesCount, x cardinality, repetition, and // stackability are recomputed together; identifier assignments throw. const suggestions = suggestCharts(rows, options)
Implementation
JSX
import { useChartSuggestions, LineChart, BarChart, /* ... */ } from "semiotic/ai" const COMPONENT_MAP = { LineChart, BarChart, /* ... */ } function SuggestedChart({ data, intent }) { const { suggestions } = useChartSuggestions(data, { intent }) const top = suggestions[0] if (!top) return <p>No fitting chart for this data.</p> const Component = COMPONENT_MAP[top.component] return <Component {...top.props} /> }
Rendering mixed chart and value suggestions
A value component such as BigNumber has its own prop envelope and mode vocabulary. Read propContract instead of maintaining a component-name set. The same declarations are published for every component inai/surface-manifest.json atcomponents.suggestionPropContracts.
TSX
function DynamicSuggestion({ suggestion }) { const Component = COMPONENT_MAP[suggestion.component] const contract = suggestion.propContract const chartDefaults = contract.commonChartProps === "supported" ? { responsiveWidth: true } : {} // BigNumber reports value-component / component-specific / headingProp=label // and modes tile|presentation|inline|thumbnail. Chart HOCs report their own // common contract and primary|context|sparkline|mobile modes. return <Component {...chartDefaults} {...suggestion.props} /> }
Charts know what they're good for
Each chart's capability lives next to its TSX file (e.g.LineChart.capability.ts). It declares fits,intentScores, variants, caveats, andbuildProps. Variants encode the idea thatsettings change what a chart is good for — a stacked area with thestreamgraph variant boosts trend readability but penalizespart-to-whole (because totals become unreadable). Those tradeoffs surface in the suggestion's intentScores, caveats, andreasons.
Tying in interrogation
Set includeSuggestions: true on useChartInterrogation and the same ranked list lands in the LLM's context.suggestions. Use it to answer questions like "would another chart show this better?" without re-deriving rules.
Receivability — can the audience receive this chart?
Familiarity asks whether an audience knows a chart. Receivability asks whether they can perceive it in their channel — a different axis. An 8-slice pie is familiar to everyone and illegible to a screen reader. Declare the channel with receptionModality and the recommender audits each candidate, folding theaccessibility audit's verdict into the score:
TS
import { suggestCharts } from "semiotic/ai" const ranked = suggestCharts(data, { intent: "part-to-whole", audience: { receptionModality: "screen-reader" }, // visual | screen-reader | sonified | agent }) // A many-slice pie drops below a bar chart for this audience. The reason and // the receivability caveats ride the same fields as everything else: ranked.find(s => s.component === "PieChart") // .reasons → ["…", "Harder to receive via a screen reader: data density is inappropriate"] // .caveats → ["…", "8 slices in a part-to-whole chart. Chartability suggests ≤ 5 categories…"]
The visual path is unchanged and pays no audit cost — receivability only engages for a non-visual channel. applyAudienceBias folds areceivabilityBias penalty derived from the audit, and the receivability findings are distilled by accessibilityCaveats into the suggestion's caveats[] — so an agent reads perceptual and reception caveats from one array, not two. See thedescription layer and thereader-grounding payload for the rest of the reception surface.
Adding a custom capability
TS
import { registerChartCapability } from "semiotic/ai" registerChartCapability({ component: "MyDomainChart", family: "categorical", importPath: "semiotic", rubric: { familiarity: 2, accuracy: 4, precision: 4 }, fits: (profile) => profile.primary.category ? null : "needs a category field", intentScores: { "compare-categories": 5, "rank": 4 }, buildProps: (profile) => ({ data: profile.data, categoryAccessor: profile.primary.category, valueAccessor: profile.primary.y, }), })