flint-chart: authoring and using a chart spec
What you produce (and what you do NOT)
Your output is the spec: the chart_spec and semantic_types of a
ChartAssemblyInput. You reference data columns by name. The host
passes the resulting input to assembleVegaLite, assembleECharts,
or assembleChartjs to get a backend spec.
You write the input spec, not the output spec. And critically:
- DO emit
chart_spec (chart type, channel→field mapping, properties)
and semantic_types (field → semantic type).
- Reference columns by name. How
data itself gets bound depends on
the situation — a URL, a host-side variable, or embedded rows (see "How
data gets bound"). Embedding is fine for small tables; just don't
re-serialize a large dataset by hand, since that risks truncation and
silent value corruption and wastes tokens.
- Transform data before Flint. If the requested chart needs aggregation,
filtering, joins, pivots, derived columns, or long/wide reshaping beyond
Flint's built-in static-series fold, use a coding, notebook, SQL, or data tool
first. Then author the Flint spec against the transformed table.
- Style after Flint, only when needed. Author structure in Flint. For a
presentation tweak Flint does not express (a reference line, annotation, or
shaded band), use the Vega-Lite escape hatch — see "Post-Flint style
customization". Never feed edited Vega-Lite JSON back to
render_chart.
When the user wants more than a spec
First decide which workflow the user is asking for:
- Spec authoring only: return a
ChartAssemblyInput or its
semantic_types + chart_spec pieces. Do not install packages or write
renderer code unless asked.
- MCP chart output: if Flint MCP tools are available, default to
create_chart_view whenever the user asks to see a chart — it opens an
interactive, live-rendered view with a customization panel, and it validates
the spec for you. Only fall back to render_chart (PNG/SVG) when the host has
no App UI support or the user explicitly wants a static image. Use
validate_chart to check a spec without rendering, compile_chart when the
user wants backend-native JSON, and list_chart_types when you need the
supported chart catalog.
- Project integration, only when the user asks for code: add Flint to an
app, notebook, script, or agentic product, install/import the library, and
call an assembler in code. Keep the same
ChartAssemblyInput contract, then
let the host render the backend result.
For MCP clients, the server can run with npx:
npx -y flint-chart-mcp
For JavaScript or TypeScript projects, install Flint first and add only the
renderer peer dependencies needed by the backend you will render:
npm install flint-chart
npm install vega vega-lite vega-embed # browser Vega-Lite rendering
npm install echarts # ECharts rendering
npm install chart.js # Chart.js rendering
Then compile with the requested backend:
import { assembleChartjs, assembleECharts, assembleVegaLite } from 'flint-chart';
const vegaLiteSpec = assembleVegaLite(input);
const echartsOption = assembleECharts(input);
const chartjsConfig = assembleChartjs(input);
Python support is planned for a later release. Until the PyPI package is
published, use the npm package or MCP server for released workflows.
interface ChartAssemblyInput {
// Bound by the HOST or by you, depending on the situation (see below).
data: { values: any[] } | { url: string };
semantic_types?: Record<string, string | SemanticAnnotation>; // field → type ( ← you write this)
chart_spec: { // ← you write this
chartType: string; // e.g. "Scatter Plot"
title?: string; // the headline — write one
subtitle?: string; // what is measured, of whom, when, in what units
encodings: Record<string, EncodingValue>; // channel → { field, ... } (or array)
baseSize?: { width: number; height: number }; // target layout size, default 400×320
canvasSize?: { width: number; height: number }; // optional hard ceiling on stretch
chartProperties?: Record<string, any>; // per-chart tuning (optional)
};
options?: Record<string, any>; // global layout options (rarely needed)
field_display_names?: Record<string, string>; // field → readable axis/legend title
theme_spec?: string | { extends: string; [key: string]: any }; // preset or preset override (Vega-Lite only)
}
How data gets bound
Use the binding mode that matches the runtime. Do not mix them.
- Direct MCP rendering: embed rows. When calling
render_chart,
compile_chart, or validate_chart, the tool arguments are JSON. If the
data is small or already transformed by another tool, pass it as
data: { values: [...] }. Do not pass runtime variable names in
MCP tool calls — the MCP server cannot see your local variables.
- Direct MCP rendering: reference a local file.
The
flint-chart-mcp server can load data: { url: "..." } from local
.json, .csv, or .tsv files. By default any local file the agent can
name is readable (relative paths resolve against the working directory); a
hardened deployment may reject local file references entirely via
--disable-file-reference (or FLINT_MCP_DISABLE_FILE_REFERENCE), in which
case pass rows inline with data.values. Remote URL
fetching is disabled. If the data must be transformed first, use a
coding/data tool to write a small prepared file, then reference that file.
- Generated application or notebook code: bind runtime variables. If the
user asks you to add Flint to code, write normal data-loading code first and
pass a real runtime value, e.g.
data: { values: rows }, to
assembleVegaLite, assembleECharts, or assembleChartjs. This variable
pattern is for generated code, not for MCP tool calls.
For spec-only answers, return the semantic_types and chart_spec pieces and
state how the host should bind data. In the worked examples below, data is
shown as { values: [] } to signal "host binds this" — focus on chart_spec
and semantic_types.
Data transformation before charting
Flint is a chart compiler, not a data-wrangling layer. If the chart needs grouped
totals, time buckets, filters, joins, pivots, derived ratios, or a long-form
table, transform the data first with a host tool, then bind the prepared table
(see "How data gets bound"). Pick semantic types and channels for the transformed
columns, not for columns that no longer exist.
Sanity-read the values first — don't chart blind. Inspect the actual data
with your data tool (distinct values per category column, min/max per measure),
not just the column names, and watch for:
- Embedded totals. A category column may mix an aggregate level with its
parts (e.g.
all alongside cage-free/caged, or a Total region). Charting
the total with its parts double-counts and flattens the parts — keep one or the
other on a stacked/grouped/colored channel, not both.
- Units. Check whether a rate is a fraction (0–1) or already a percent
(0–100) before tagging it
Percentage; don't scale twice.
- One real entity. If your breakdown column has a single distinct value, the
per-group chart collapses to one mark — the intended breakdown is likely a
different column.
Post-Flint style customization
Stay at the Flint level for structure (data, chart type, channels, transforms,
sizing, properties) — Flint specs stay portable and regenerate safely. Drop to
backend JSON only after a valid Flint chart exists, and only for a narrow
presentation change Flint does not expose (exact axis/legend/mark styling,
titles, annotations, reference lines, layout polish). Never use it to change the
data, chart type, field mappings, or transforms — fix those upstream.
For a Vega-Lite-specific style tweak:
- Author and validate the Flint
ChartAssemblyInput.
- Render or inspect the Flint chart first, when possible.
- Call
compile_chart with backend: "vegalite".
- Make the smallest necessary style/presentation edit to the returned
Vega-Lite spec.
- Render the edited spec in the host environment with a Vega-Lite renderer.
This edited Vega-Lite spec is no longer a portable Flint spec. Do not send it to
render_chart; use render_chart only for Flint ChartAssemblyInput.
Write a headline
Set chart_spec.title to the finding, in a sentence, and chart_spec.subtitle
to the reading of it — what is measured, of whom, when, in what units:
title: "A pyramid that is no longer a pyramid"
subtitle: "United States population by age and sex, 2020, millions"
Jan, Cairo, Chrome name their own kind; 26, 5,300, 0.42 do not, and
the headline is where they get named. Leave it out only where the chart is not
read on its own — a sparkline in a cell, a tile under its own caption. Nothing
breaks: with no headline to lean on, the compiler keeps the axis titles instead.
Visual themes (theme_spec)
Use one of two forms. Prefer a preset unless the user asks for a specific
brand adjustment.
1. Use a preset
Call list_themes to choose an id, then place it beside chart_spec:
{ "chart_spec": { ... }, "theme_spec": "economist" }
| id |
what it is for |
nyt |
Newsroom graphics: headline states the finding, values on the marks, series named at their ends. |
economist |
Print weekly: compact, flat headline over a deck, units repeated down the ruler. |
swiss |
International Typographic Style: strong grid structure, black typography, and a focused red accent. |
nature |
Journal figure: small panel, axis titles with units, statistics beside the fit. |
mckinsey |
Consulting deck: wide bands, every value printed, headline states the takeaway. |
datawrapper |
Embedded web chart: narrow column, plain headline and deck, rule under the footer. |
powerbi |
Dashboard tile: compact, legend to the right, latest point emphasised. |
powerbi-light |
Light dashboard tile: white canvas, fine gridlines, and bright categorical color. |
cartoon |
Playful illustration: warm paper, rounded type, bold outlines, and bright color. |
2. Override a preset
Keep overrides narrow and state only what the user wants to change:
{
"theme_spec": {
"extends": "economist",
"id": "our-brand",
"ink": {
"series": {
"single": "#6b3fa0"
}
}
}
}
Common simple overrides are ink.surface.canvas, ink.series.single,
ink.series.categorical, type.headline.family, and layout.density
("compact", "normal", or "airy"). If replacing
ink.series.categorical, also replace categoricalExtended so charts with
many series keep the requested brand palette.
Do not copy an entire preset or invent theme keys. A theme controls
presentation; fields, aggregation, filtering, and sorting still belong in the
chart input. ThemeSpec currently affects Vega-Lite only.
Full reference:
https://microsoft.github.io/flint-chart/#/documentation/theme-spec
Step 1 — pick chartType
Use one of the registered names exactly. Vega-Lite is the default and
broadest backend; the table below lists each Vega-Lite chart type, the
channels it accepts, and its tuning properties (see "Chart-level
properties"). Required channels are noted.
| chartType |
Channels |
Notes / required |
"Scatter Plot" |
x, y, color, size, opacity, column, row |
x + y required |
"Regression" |
x, y, size, color, column, row |
scatter + fit line; props regressionMethod, polyOrder |
"Connected Scatter Plot" |
x, y, order, color, detail, column, row |
x + y required; order = connection sequence (time/index), so the line traces a trajectory and may self-cross |
"Ranged Dot Plot" |
x, y, color |
dumbbell of two x per category |
"Strip Plot" |
x, y, color, size, column, row |
jittered points; props stepWidth, pointSize, opacity |
"Bar Chart" |
x, y, color, opacity, column, row |
one discrete + one measure; prop cornerRadius |
"Grouped Bar Chart" |
x, y, group, column, row |
group = the clustering category; prop dodge |
"Stacked Bar Chart" |
x, y, color, column, row |
prop stackMode |
"Pyramid Chart" |
x, y, color |
diverging horizontal bars |
"Lollipop Chart" |
x, y, color, column, row |
prop dotSize |
"Waterfall Chart" |
x, y, color, column, row |
color = Type column, values start/delta/end only; omit it for auto sign coloring; props cornerRadius, totals |
"Gantt Chart" |
y, x, x2, color, detail, column, row |
x = start, x2 = end |
"Bullet Chart" |
y, x, goal, color, column, row |
goal required (target) |
"Histogram" |
x, color, column, row |
x = measure to bin; prop binCount |
"Boxplot" |
x, y, color, opacity, column, row |
category + measure; props whiskerMethod, showOutliers, dodge |
"ECDF Plot" |
x, color, detail, column, row |
x = measure; cumulative distribution (step line); prop showPoints |
"Heatmap" |
x, y, color, column, row |
color = the measure |
"Calendar Heatmap" |
x, color |
x = date; color = daily value (summed per day); GitHub-style week × weekday grid |
"Line Chart" |
x, y, color, strokeDash, detail, opacity, column, row |
props interpolate, showPoints |
"Sparkline" |
x, y, color, detail, row, column |
x + y required; small-multiple mini trend lines, one per series (series from color or detail); props interpolate, baseline, trendWidth |
"Bump Chart" |
x, y, color, detail, column, row |
rank-over-time lines |
"Slope Chart" |
x, y, color, detail, column, row |
two-period value change; straight segments + end points, one line per category |
"Area Chart" |
x, y, color, opacity, column, row |
props interpolate, opacity, stackMode |
"Range Area Chart" |
x, y, y2, color, column, row |
x + y + y2 required; translucent band from y (low) to y2 (high), value axis fits the band (not zero) |
"Violin Plot" |
x, y, color, row |
x (category) + y (measure) required; mirrored KDE density per category, prop bandwidth; Vega-Lite only; a genuine color subgroup splits two groups or grids 3+ groups |
"Streamgraph" |
x, y, color, column, row |
centre-stacked areas |
"Density Plot" |
x, color, column, row |
prop bandwidth |
"Pie Chart" |
size, color, column, row |
size = slice value (→ angle), color = category; props innerRadius, sortSlices |
"Rose Chart" |
x, y, color, column, row |
polar bars; props alignment, padAngle, sortSlices |
"Radar Chart" |
x, y, color, column, row |
props filled, fillOpacity, strokeWidth |
"Candlestick Chart" |
x, open, high, low, close, column, row |
OHLC all required |
"Bar Table" |
y, x, color, column, row |
compact bars + value labels |
"KPI Card" |
metric, value, goal |
big-number tile; prop behindThreshold |
"Map" |
longitude, latitude, color, size, opacity |
bubble map; props region, projection |
"Choropleth" |
id, color, detail |
id = geographic key |
Donut chart: use "Pie Chart" with chartProperties.innerRadius > 0.
Choosing a bar chart (most common mix-up). All three take one discrete
category on x (or y) plus one measure. They differ in how a second
category is shown — and each reads that second category from a different
channel:
"Bar Chart" — use for a single series. When multiple rows share an x, a
second category on color produces stacked segments. For side-by-side bars,
use "Grouped Bar Chart" with the second category on group.
"Stacked Bar Chart" — second category on color, drawn as stacked
segments within each bar (totals matter). Tune with stackMode
(stacked / normalize / layered).
"Grouped Bar Chart" — second category on the group channel, drawn as
side-by-side (dodged) bars within each x cluster (compare values
directly). Put the clustering category on group, not color.
Rule of thumb: comparing parts-to-whole → Stacked; comparing values
side-by-side → Grouped (use group); single series → Bar.
Waterfall color is a special "Type" column, not a free category. On a
"Waterfall Chart" the color channel is reserved for a type field whose
values are literally start, delta, and end — it drives which bars anchor
to zero, not an arbitrary grouping. Do not bind color to an
Increase/Decrease (or up/down, gain/loss) category: the up/down direction is
already derived from the sign of the y value and colored automatically
(green up / red down). For the common case, omit color entirely and let
Flint infer the start/delta/end and per-bar sign coloring. To force which bars
are anchored totals, use the totals property (first/last/both), not a
color field. Only bind color when you genuinely have a start/delta/end
type column.
Backend coverage. Vega-Lite supports all of the above. Other backends
support a subset (verify if targeting a non-VL backend):
- ECharts adds:
"Gauge",
"Funnel", "Treemap", "Sunburst", "Sankey",
"Parallel Coordinates", "Graph", "Tree".
- Chart.js supports: Scatter, Bubble, Bar, Grouped Bar, Stacked Bar,
Lollipop, Bump, Combo, Line, Area, Range Area, Pie, Doughnut, Histogram,
Radar, Rose, Slope, Connected Scatter.
You do not need to call the library or inspect its source to author the
input — pick from this table.
Step 2 — map fields to channels
Each channel maps to an encoding object { field, ... } (or a bare
string shorthand, expanded to { field: "<string>" }):
"encodings": {
"x": { "field": "weight" },
"y": "mpg",
"color": { "field": "origin" }
}
Encoding object fields (all optional except field):
| Field |
Values |
Purpose |
field |
column name |
Bind the channel to a data column |
type |
quantitative, nominal, ordinal, temporal |
Override the inferred encoding type (rarely needed) |
aggregate |
count, sum, average, mean |
Force an aggregation on a measure channel |
sortOrder |
ascending, descending |
Sort direction for a discrete/sorted axis |
sortBy |
channel name (e.g. "y") or field |
Sort a category axis by another channel's measure |
scheme |
Vega scheme name (e.g. viridis, redblue) |
Color scheme for the color channel |
You usually don't need type, aggregate, or sortOrder — they're
inferred from the semantic type. Set them only with specific intent.
Multi-series (wide → long). To plot several measure columns as series,
pass an array on x or y (only those two channels). The library
folds them into long form and synthesizes a series/legend field:
"encodings": { "x": { "field": "month" }, "y": ["sales", "profit"] }
All array fields must be quantitative, and you cannot also bind color
when using the array form (the fold owns the color/legend). This is the
only built-in reshape — there is no transforms/fold property. For any
other shape (long↔wide, an aggregate the encodings can't express, a derived
column, a pivot, a join), reshape the data first with a host tool — pandas/polars,
Arquero/Array.map/SQL, or a data/MCP tool — and pass the result as
data.values. If you have no way to transform, surface the gap to the developer
rather than inventing a transform property that does not exist.
Step 3 — annotate with semantic types
This is the most important step. Semantic types drive all downstream
decisions — formatting, zero baseline, color scheme, scale direction, and
more. Pick the most specific type for each field. Full registered set:
| Family |
Semantic types |
| Temporal (point) |
DateTime, Date, Time, Timestamp |
| Temporal (granule) |
Year, Quarter, Month, Week, Day, Hour, YearMonth, YearQuarter, YearWeek, Decade |
| Temporal (span) |
Duration |
| Measure (amount) |
Amount, Price, Quantity, Count, Number |
| Measure (proportion) |
Percentage |
| Measure (signed/diverging) |
Profit, PercentageChange, Sentiment, Correlation |
| Measure (physical) |
Temperature |
| Discrete / rank |
Rank, Score, ID |
| Geographic (coord) |
Latitude, Longitude |
| Geographic (place) |
Country, State, City, Region, Address, ZipCode |
| Categorical |
Category, Name, Status, Boolean, Direction, Range |
| Fallback |
Unknown |
What choosing well gets you (automatically):
Price / Amount → currency formatting, zero baseline, sequential color
Temperature → diverging color scheme, no forced zero baseline
Correlation → fixed [-1, 1] diverging domain
Rank → reversed axis (1 on top), discrete color
Date / DateTime → temporal axis with auto-granularity formatting
Percentage → percent formatting, 0–100 domain awareness
If you don't know, use Quantity for numbers, Category for strings,
Date/DateTime for date-shaped values. Do not invent type names.
Saying more than the type name
A field's entry can be an object instead of a string when the type alone
understates what you know:
"semantic_types": {
"anomaly": { "semanticType": "Quantity", "unit": "°C", "divergingMidpoint": 0 },
"rating": { "semanticType": "Score", "intrinsicDomain": [1, 5] }
}
unit — the unit or currency code: "USD", "°C", "kg".
intrinsicDomain — the field's own bounds, for bounded scales only: [1, 5]
for a five-star rating, [0, 100] for a percentage score. Not for
open-ended measures.
divergingMidpoint — where the middle colour of a diverging scale sits.
Set it if you can tell what the reader is comparing against; leave it out if
you can't.
sortOrder — the order the categories should appear in, when the order in
the data is not the one you want and it isn't alphabetical either:
["Low", "Medium", "High"]. For a handful of categories, not a long list.
Chart-level properties (chartProperties)
chartProperties is an optional per-chart tuning map. Set a property only
when the user asks for that behavior — defaults are sensible. These are
design choices, not styling overrides (colors/fonts/ticks are still
derived). Values are clamped to the ranges shown.
| Chart type |
Property |
Type / range (default) |
Effect |
| Bar Chart |
cornerRadius |
0–15 (0) |
Round bar corners (px) |
| Area / Stacked Bar |
stackMode |
stacked | normalize | center | layered (unset) |
Stacking behavior; normalize = 100%, center = streamgraph |
| Grouped Bar / Boxplot |
dodge |
auto | local | global (auto) |
local compacts sparse groups per category; global preserves aligned group lanes; leave auto unless the user requests one |
| Line / Area / Sparkline |
interpolate |
linear | monotone | step | step-before | step-after | basis | cardinal | catmull-rom (linear) |
Curve shape |
| Line / ECDF Plot |
showPoints |
boolean (false) |
Draw point markers on the line |
| Sparkline |
baseline |
mean | zero | median | none (mean) |
Reference line per spark row |
| Sparkline |
trendWidth |
80–600 (240) |
Mini line-plot width (px) |
| Boxplot |
whiskerMethod |
iqr | minmax (iqr) |
Whisker rule (Tukey 1.5×IQR vs min–max) |
| Boxplot |
showOutliers |
boolean (true) |
Show outlier points (Tukey only) |
| Area |
opacity |
0.1–1 (0.7) |
Fill opacity |
| Scatter |
opacity |
0.1–1 (1) |
Point opacity |
| Strip Plot |
stepWidth |
10–100 (20) |
Jitter spread |
| Strip Plot |
pointSize |
0–150 (0=auto) |
Point size |
| Strip Plot |
opacity |
0–1 (0=auto) |
Point opacity |
| Histogram |
binCount |
5–50 (10) |
Number of bins |
| Density Plot |
bandwidth |
0.05–2 (0=auto) |
Kernel bandwidth |
| Pie Chart |
innerRadius |
0–100 (0) |
Donut hole size (>0 → donut) |
| Pie / Rose |
sortSlices |
none | descending | ascending (none) |
Order wedges and their legend by slice value |
| Rose Chart |
alignment |
left | center (left) |
Wedge alignment |
| Rose Chart |
padAngle |
0–0.1 (0) |
Gap between slices |
| Lollipop |
dotSize |
20–300 (80) |
Circle size (px) |
| Waterfall |
cornerRadius |
0–8 (0) |
Round bar corners |
| Waterfall |
totals |
auto | none | first | last | both (auto) |
Which bars anchor to zero as totals (only when no Type column) |
| Waterfall |
showTextLabels |
boolean (false) |
Legacy spelling of showValueLabels; still accepted |
| Bar / Grouped Bar / Stacked Bar / Lollipop / Pyramid / Pie / Donut / Heatmap / Waterfall |
showValueLabels |
boolean |
Print the numbers on the marks. Works with or without a theme: unset, it follows the house's own habit at this density (and with no house named, stays off), so the default the compiler reports is always the honest one. Set it to overrule that for one chart. Reported inapplicable (and ignored) where the marks are too dense to carry readable numbers, or where the template already writes its own text, so it is never a control that does nothing. On a stacked bar each segment prints its own value in the middle of the segment (at the edge it would read as the running total); segments too thin to hold a line of text go unlabelled, and a normalized stack prints each segment's share rather than its raw value, since the share is what the length shows. The printed number is rounded to roughly three significant figures — with a k/M suffix once the values get long, and enough decimals that the smallest value in the series still says something — so a raw 3.14159265 lands as 3.14 and a series of 0.001 to 5000 reads at both ends. Rounding never goes so far that two marks of different size print the same number, or that a non-zero value prints as 0; where a house asked for a coarser precision than that, the digits are raised until the labels agree with the marks. |
| Regression |
regressionMethod |
linear | log | exp | pow | quad | poly (linear) |
Fit method |
| Regression |
polyOrder |
1–5 (3) |
Polynomial order (when poly) |
| Radar |
filled |
boolean (true) |
Fill the polygon |
| Radar |
fillOpacity |
0–0.5 (0.15) |
Polygon fill opacity |
| Radar |
strokeWidth |
0.5–4 (1.5) |
Line width |
| KPI Card |
behindThreshold |
0–1 (0.5) |
Value/goal ratio cutoff for color |
| Map |
region |
us | world | auto (auto) |
Geographic scope |
| Map |
projection |
mercator | equalEarth | orthographic | stereographic | conic | mollweide |
Map projection |
Cross-cutting properties (apply to position/faceted charts when
relevant; set only to force non-default behavior):
independentYAxis (boolean) — faceted charts: give each panel its own
y-scale.
logScale_x / logScale_y (boolean) — force a logarithmic axis.
includeZero_x / includeZero_y (boolean) — force the axis to include 0.
xAxisType / yAxisType (temporal | nominal) — force a temporal
field to render as discrete bands (or vice-versa).
Parameter overrides — when to reach for them
Overrides exist, but prefer letting semantic types drive decisions. Reach
for an override only when the user's intent genuinely conflicts with the
default:
- Force an aggregation:
encodings.y = { field: "sales", aggregate: "sum" }.
- Sort a category axis by its measure:
encodings.x = { field: "name", sortBy: "y", sortOrder: "descending" }.
- Pick a color scheme:
encodings.color = { field: "region", scheme: "tableau10" }.
- Override an inferred type:
encodings.x = { field: "year", type: "ordinal" } (e.g. treat a year as discrete bands).
- Use readable field titles:
field_display_names = { percentageOfCountries: "Percentage of countries" }.
Keep encodings bound to the real column name; Flint uses the display name for axis titles and legend headers.
- Resize the chart: Flint sizes from two numbers —
baseSize (the target
it aims for, default 400×320) and canvasSize (a hard ceiling it may never
exceed). With dense data the chart stretches from base toward the ceiling.
- Want a comfortable size that may grow for dense data → set
chart_spec.baseSize = { width, height }.
- Want a fixed slot it must fit inside → set
chart_spec.canvasSize = { width, height } alone; the chart fills it and shrinks to fit, never overflowing. What you ask for is what you get.
- Both → aims for
baseSize, grows toward canvasSize, never beyond.
- Force log / zero baseline: the
logScale_* / includeZero_* chart
properties above.
Global layout tuning lives in the top-level options object (e.g.
addTooltips, band padding, facet sizing). It is rarely needed for
authoring — omit it unless asked.
Worked examples
In each example data is a placeholder — the host binds real rows or a
URL. You author only chart_spec and semantic_types.
Scatter plot
User: "Plot car weight vs fuel economy, colored by origin."
{
"data": { "values": [] },
"semantic_types": {
"weight": "Quantity",
"mpg": "Quantity",
"origin": "Country"
},
"chart_spec": {
"chartType": "Scatter Plot",
"encodings": {
"x": { "field": "weight" },
"y": { "field": "mpg" },
"color": { "field": "origin" }
},
"baseSize": { "width": 400, "height": 300 }
}
}
Revenue bar chart with facets, sorted by value
User: "Show revenue by product line, biggest first, one panel per region."
{
"data": { "values": [] },
"semantic_types": {
"product_line": "Category",
"revenue": "Amount",
"region": "Region"
},
"chart_spec": {
"chartType": "Bar Chart",
"encodings": {
"x": { "field": "product_line", "sortBy": "y", "sortOrder": "descending" },
"y": { "field": "revenue" },
"column": { "field": "region" }
}
}
}
Time series, multiple series (wide → long via array)
User: "Line chart of monthly sales and profit."
{
"data": { "values": [] },
"semantic_types": {
"month": "YearMonth",
"sales": "Amount",
"profit": "Profit"
},
"chart_spec": {
"chartType": "Line Chart",
"encodings": {
"x": { "field": "month" },
"y": ["sales", "profit"]
},
"chartProperties": { "interpolate": "monotone", "showPoints": true }
}
}
Donut chart (Pie + innerRadius), value on size
User: "Show market share by vendor as a donut."
Pie/donut maps the slice value to size (rendered as angle) and the
category to color. Data is already long (one row per vendor).
{
"data": { "values": [] },
"semantic_types": {
"vendor": "Category",
"share": "Percentage"
},
"chart_spec": {
"chartType": "Pie Chart",
"encodings": {
"size": { "field": "share" },
"color": { "field": "vendor" }
},
"chartProperties": { "innerRadius": 60 }
}
}
Bullet chart (KPI vs target)
User: "Show each rep's sales against their quota."
{
"data": { "values": [] },
"semantic_types": {
"rep": "Name",
"sales": "Amount",
"quota": "Amount"
},
"chart_spec": {
"chartType": "Bullet Chart",
"encodings": {
"y": { "field": "rep" },
"x": { "field": "sales" },
"goal": { "field": "quota" }
}
}
}
What you should NOT do
- Don't re-emit the data. Reference columns by name; let the host bind
data (url, variable, or small literal). Never paste large datasets.
- Don't write backend specs directly — write the
ChartAssemblyInput,
then call the assembler. That's the whole point.
- Don't invent transforms. The only built-in reshape is the array form
on
x/y. If the data shape is wrong for the chart, say so and ask the
host to reshape it.
- Don't invent field names. Reference only columns that exist, spelled
exactly. If the data is the wrong shape for the chart, reshape it upstream
rather than guessing column names that aren't there.
- Don't set
type/aggregate/sortOrder unless intent conflicts
with the default.
- Don't pass colors, font sizes, axis tick counts — the compiler
derives these. Users fine-tune the output spec.
- Don't invent semantic type names. If none fit, use the family
default (
Quantity, Category, Date).
- Don't call the library to discover channels/types — this document is
the authoring reference.
Validation checklist
Before returning, verify:
chartType is an exact registered name supported by the target backend.
- Every
field referenced in encodings is a real column name.
- Every encoded field has an entry in
semantic_types (specific type).
- Required channels for the chart type are present (e.g. Bullet→
goal,
Candlestick→open/high/low/close, Pie→size+color).
- Any
chartProperties keys are valid for that chart type and in range.
- You did not inline large data or hand-tune derived styling.
- The data carries no embedded total/subtotal level (e.g. an
all / total
row) mixed with its components on a stacked, grouped, or colored channel.
1---2name: flint-chart-author3description: Use when: the user asks to make or render charts with flint-chart, visualize tabular data, generate a ChartAssemblyInput, validate/render through MCP, or add Flint to a JS/TS project. Author the semantic spec, transform data before Flint when needed, install/import Flint only when executable code is needed, and reserve backend-specific style tweaks for after compiling from Flint.4---56# flint-chart: authoring and using a chart spec78## What you produce (and what you do NOT)910Your output is the **spec**: the `chart_spec` and `semantic_types` of a11`ChartAssemblyInput`. You reference data columns **by name**. The host12passes the resulting input to `assembleVegaLite`, `assembleECharts`,13or `assembleChartjs` to get a backend spec.1415**You write the input spec, not the output spec.** And critically:1617- **DO** emit `chart_spec` (chart type, channel→field mapping, properties)18 and `semantic_types` (field → semantic type).19- **Reference columns by name.** How `data` itself gets bound depends on20 the situation — a URL, a host-side variable, or embedded rows (see "How21 data gets bound"). Embedding is fine for small tables; just don't22 re-serialize a *large* dataset by hand, since that risks truncation and23 silent value corruption and wastes tokens.24- **Transform data before Flint.** If the requested chart needs aggregation,25 filtering, joins, pivots, derived columns, or long/wide reshaping beyond26 Flint's built-in static-series fold, use a coding, notebook, SQL, or data tool27 first. Then author the Flint spec against the transformed table.28- **Style after Flint, only when needed.** Author structure in Flint. For a29 presentation tweak Flint does not express (a reference line, annotation, or30 shaded band), use the Vega-Lite escape hatch — see "Post-Flint style31 customization". Never feed edited Vega-Lite JSON back to `render_chart`.3233## When the user wants more than a spec3435First decide which workflow the user is asking for:3637- **Spec authoring only:** return a `ChartAssemblyInput` or its38 `semantic_types` + `chart_spec` pieces. Do not install packages or write39 renderer code unless asked.40- **MCP chart output:** if Flint MCP tools are available, **default to41 `create_chart_view`** whenever the user asks to see a chart — it opens an42 interactive, live-rendered view with a customization panel, and it validates43 the spec for you. Only fall back to `render_chart` (PNG/SVG) when the host has44 no App UI support or the user explicitly wants a static image. Use45 `validate_chart` to check a spec without rendering, `compile_chart` when the46 user wants backend-native JSON, and `list_chart_types` when you need the47 supported chart catalog.48- **Project integration, only when the user asks for code:** add Flint to an49 app, notebook, script, or agentic product, install/import the library, and50 call an assembler in code. Keep the same `ChartAssemblyInput` contract, then51 let the host render the backend result.5253For MCP clients, the server can run with `npx`:5455```bash56npx -y flint-chart-mcp57```5859For JavaScript or TypeScript projects, install Flint first and add only the60renderer peer dependencies needed by the backend you will render:6162```bash63npm install flint-chart64npm install vega vega-lite vega-embed # browser Vega-Lite rendering65npm install echarts # ECharts rendering66npm install chart.js # Chart.js rendering67```6869Then compile with the requested backend:7071```ts72import { assembleChartjs, assembleECharts, assembleVegaLite } from 'flint-chart';7374const vegaLiteSpec = assembleVegaLite(input);75const echartsOption = assembleECharts(input);76const chartjsConfig = assembleChartjs(input);77```7879Python support is planned for a later release. Until the PyPI package is80published, use the npm package or MCP server for released workflows.8182```ts83interface ChartAssemblyInput {84 // Bound by the HOST or by you, depending on the situation (see below).85 data: { values: any[] } | { url: string };86 semantic_types?: Record<string, string | SemanticAnnotation>; // field → type ( ← you write this)87 chart_spec: { // ← you write this88 chartType: string; // e.g. "Scatter Plot"89 title?: string; // the headline — write one90 subtitle?: string; // what is measured, of whom, when, in what units91 encodings: Record<string, EncodingValue>; // channel → { field, ... } (or array)92 baseSize?: { width: number; height: number }; // target layout size, default 400×32093 canvasSize?: { width: number; height: number }; // optional hard ceiling on stretch94 chartProperties?: Record<string, any>; // per-chart tuning (optional)95 };96 options?: Record<string, any>; // global layout options (rarely needed)97 field_display_names?: Record<string, string>; // field → readable axis/legend title98 theme_spec?: string | { extends: string; [key: string]: any }; // preset or preset override (Vega-Lite only)99}100```101102## How data gets bound103104Use the binding mode that matches the runtime. Do not mix them.1051061. **Direct MCP rendering: embed rows.** When calling `render_chart`,107 `compile_chart`, or `validate_chart`, the tool arguments are JSON. If the108 data is small or already transformed by another tool, pass it as109 `data: { values: [...] }`. Do not pass runtime variable names in110 MCP tool calls — the MCP server cannot see your local variables.1112. **Direct MCP rendering: reference a local file.**112 The `flint-chart-mcp` server can load `data: { url: "..." }` from local113 `.json`, `.csv`, or `.tsv` files. By default any local file the agent can114 name is readable (relative paths resolve against the working directory); a115 hardened deployment may reject local file references entirely via116 `--disable-file-reference` (or `FLINT_MCP_DISABLE_FILE_REFERENCE`), in which117 case pass rows inline with `data.values`. Remote URL118 fetching is disabled. If the data must be transformed first, use a119 coding/data tool to write a small prepared file, then reference that file.1203. **Generated application or notebook code: bind runtime variables.** If the121 user asks you to add Flint to code, write normal data-loading code first and122 pass a real runtime value, e.g. `data: { values: rows }`, to123 `assembleVegaLite`, `assembleECharts`, or `assembleChartjs`. This variable124 pattern is for generated code, not for MCP tool calls.125126For spec-only answers, return the `semantic_types` and `chart_spec` pieces and127state how the host should bind data. In the worked examples below, `data` is128shown as `{ values: [] }` to signal "host binds this" — focus on `chart_spec`129and `semantic_types`.130131## Data transformation before charting132133Flint is a chart compiler, not a data-wrangling layer. If the chart needs grouped134totals, time buckets, filters, joins, pivots, derived ratios, or a long-form135table, transform the data first with a host tool, then bind the prepared table136(see "How data gets bound"). Pick semantic types and channels for the transformed137columns, not for columns that no longer exist.138139**Sanity-read the values first — don't chart blind.** Inspect the actual data140with your data tool (distinct values per category column, min/max per measure),141not just the column names, and watch for:142143- **Embedded totals.** A category column may mix an aggregate level with its144 parts (e.g. `all` alongside `cage-free`/`caged`, or a `Total` region). Charting145 the total with its parts double-counts and flattens the parts — keep one or the146 other on a stacked/grouped/colored channel, not both.147- **Units.** Check whether a rate is a fraction (0–1) or already a percent148 (0–100) before tagging it `Percentage`; don't scale twice.149- **One real entity.** If your breakdown column has a single distinct value, the150 per-group chart collapses to one mark — the intended breakdown is likely a151 different column.152153## Post-Flint style customization154155Stay at the Flint level for structure (data, chart type, channels, transforms,156sizing, properties) — Flint specs stay portable and regenerate safely. Drop to157backend JSON only after a valid Flint chart exists, and only for a narrow158presentation change Flint does not expose (exact axis/legend/mark styling,159titles, annotations, reference lines, layout polish). Never use it to change the160data, chart type, field mappings, or transforms — fix those upstream.161162For a Vega-Lite-specific style tweak:1631641. Author and validate the Flint `ChartAssemblyInput`.1652. Render or inspect the Flint chart first, when possible.1663. Call `compile_chart` with `backend: "vegalite"`.1674. Make the smallest necessary style/presentation edit to the returned168 Vega-Lite spec.1695. Render the edited spec in the host environment with a Vega-Lite renderer.170171This edited Vega-Lite spec is no longer a portable Flint spec. Do not send it to172`render_chart`; use `render_chart` only for Flint `ChartAssemblyInput`.173174## Write a headline175176Set `chart_spec.title` to the finding, in a sentence, and `chart_spec.subtitle`177to the reading of it — what is measured, of whom, when, in what units:178179```180title: "A pyramid that is no longer a pyramid"181subtitle: "United States population by age and sex, 2020, millions"182```183184`Jan`, `Cairo`, `Chrome` name their own kind; `26`, `5,300`, `0.42` do not, and185the headline is where they get named. Leave it out only where the chart is not186read on its own — a sparkline in a cell, a tile under its own caption. Nothing187breaks: with no headline to lean on, the compiler keeps the axis titles instead.188189## Visual themes (`theme_spec`)190191Use one of two forms. Prefer a preset unless the user asks for a specific192brand adjustment.193194### 1. Use a preset195196Call `list_themes` to choose an id, then place it beside `chart_spec`:197198```json199{ "chart_spec": { ... }, "theme_spec": "economist" }200```201202| id | what it is for |203| --- | --- |204| `nyt` | Newsroom graphics: headline states the finding, values on the marks, series named at their ends. |205| `economist` | Print weekly: compact, flat headline over a deck, units repeated down the ruler. |206| `swiss` | International Typographic Style: strong grid structure, black typography, and a focused red accent. |207| `nature` | Journal figure: small panel, axis titles with units, statistics beside the fit. |208| `mckinsey` | Consulting deck: wide bands, every value printed, headline states the takeaway. |209| `datawrapper` | Embedded web chart: narrow column, plain headline and deck, rule under the footer. |210| `powerbi` | Dashboard tile: compact, legend to the right, latest point emphasised. |211| `powerbi-light` | Light dashboard tile: white canvas, fine gridlines, and bright categorical color. |212| `cartoon` | Playful illustration: warm paper, rounded type, bold outlines, and bright color. |213214### 2. Override a preset215216Keep overrides narrow and state only what the user wants to change:217218```json219{220 "theme_spec": {221 "extends": "economist",222 "id": "our-brand",223 "ink": {224 "series": {225 "single": "#6b3fa0"226 }227 }228 }229}230```231232Common simple overrides are `ink.surface.canvas`, `ink.series.single`,233`ink.series.categorical`, `type.headline.family`, and `layout.density`234(`"compact"`, `"normal"`, or `"airy"`). If replacing235`ink.series.categorical`, also replace `categoricalExtended` so charts with236many series keep the requested brand palette.237238Do not copy an entire preset or invent theme keys. A theme controls239presentation; fields, aggregation, filtering, and sorting still belong in the240chart input. ThemeSpec currently affects Vega-Lite only.241242Full reference:243https://microsoft.github.io/flint-chart/#/documentation/theme-spec244245## Step 1 — pick `chartType`246247Use one of the registered names **exactly**. Vega-Lite is the default and248broadest backend; the table below lists each Vega-Lite chart type, the249channels it accepts, and its tuning properties (see "Chart-level250properties"). Required channels are noted.251252| chartType | Channels | Notes / required |253|---|---|---|254| `"Scatter Plot"` | x, y, color, size, opacity, column, row | x + y required |255| `"Regression"` | x, y, size, color, column, row | scatter + fit line; props `regressionMethod`, `polyOrder` |256| `"Connected Scatter Plot"` | x, y, order, color, detail, column, row | x + y required; `order` = connection sequence (time/index), so the line traces a trajectory and may self-cross |257| `"Ranged Dot Plot"` | x, y, color | dumbbell of two x per category |258| `"Strip Plot"` | x, y, color, size, column, row | jittered points; props `stepWidth`, `pointSize`, `opacity` |259| `"Bar Chart"` | x, y, color, opacity, column, row | one discrete + one measure; prop `cornerRadius` |260| `"Grouped Bar Chart"` | x, y, group, column, row | `group` = the clustering category; prop `dodge` |261| `"Stacked Bar Chart"` | x, y, color, column, row | prop `stackMode` |262| `"Pyramid Chart"` | x, y, color | diverging horizontal bars |263| `"Lollipop Chart"` | x, y, color, column, row | prop `dotSize` |264| `"Waterfall Chart"` | x, y, color, column, row | `color` = Type column, values `start`/`delta`/`end` only; omit it for auto sign coloring; props `cornerRadius`, `totals` |265| `"Gantt Chart"` | y, x, x2, color, detail, column, row | x = start, x2 = end |266| `"Bullet Chart"` | y, x, goal, color, column, row | `goal` required (target) |267| `"Histogram"` | x, color, column, row | x = measure to bin; prop `binCount` |268| `"Boxplot"` | x, y, color, opacity, column, row | category + measure; props `whiskerMethod`, `showOutliers`, `dodge` |269| `"ECDF Plot"` | x, color, detail, column, row | x = measure; cumulative distribution (step line); prop `showPoints` |270| `"Heatmap"` | x, y, color, column, row | color = the measure |271| `"Calendar Heatmap"` | x, color | x = date; color = daily value (summed per day); GitHub-style week × weekday grid |272| `"Line Chart"` | x, y, color, strokeDash, detail, opacity, column, row | props `interpolate`, `showPoints` |273| `"Sparkline"` | x, y, color, detail, row, column | x + y required; small-multiple mini trend lines, one per series (series from `color` or `detail`); props `interpolate`, `baseline`, `trendWidth` |274| `"Bump Chart"` | x, y, color, detail, column, row | rank-over-time lines |275| `"Slope Chart"` | x, y, color, detail, column, row | two-period value change; straight segments + end points, one line per category |276| `"Area Chart"` | x, y, color, opacity, column, row | props `interpolate`, `opacity`, `stackMode` |277| `"Range Area Chart"` | x, y, y2, color, column, row | x + y + y2 required; translucent band from `y` (low) to `y2` (high), value axis fits the band (not zero) |278| `"Violin Plot"` | x, y, color, row | x (category) + y (measure) required; mirrored KDE density per category, prop `bandwidth`; **Vega-Lite only**; a genuine `color` subgroup splits two groups or grids 3+ groups |279| `"Streamgraph"` | x, y, color, column, row | centre-stacked areas |280| `"Density Plot"` | x, color, column, row | prop `bandwidth` |281| `"Pie Chart"` | size, color, column, row | `size` = slice value (→ angle), `color` = category; props `innerRadius`, `sortSlices` |282| `"Rose Chart"` | x, y, color, column, row | polar bars; props `alignment`, `padAngle`, `sortSlices` |283| `"Radar Chart"` | x, y, color, column, row | props `filled`, `fillOpacity`, `strokeWidth` |284| `"Candlestick Chart"` | x, open, high, low, close, column, row | OHLC all required |285| `"Bar Table"` | y, x, color, column, row | compact bars + value labels |286| `"KPI Card"` | metric, value, goal | big-number tile; prop `behindThreshold` |287| `"Map"` | longitude, latitude, color, size, opacity | bubble map; props `region`, `projection` |288| `"Choropleth"` | id, color, detail | `id` = geographic key |289290**Donut chart:** use `"Pie Chart"` with `chartProperties.innerRadius > 0`.291292**Choosing a bar chart (most common mix-up).** All three take one discrete293category on `x` (or `y`) plus one measure. They differ in how a **second**294category is shown — and each reads that second category from a **different295channel**:296297- `"Bar Chart"` — use for a single series. When multiple rows share an `x`, a298 second category on `color` produces stacked segments. For side-by-side bars,299 use `"Grouped Bar Chart"` with the second category on `group`.300- `"Stacked Bar Chart"` — second category on `color`, drawn as **stacked**301 segments within each bar (totals matter). Tune with `stackMode`302 (`stacked` / `normalize` / `layered`).303- `"Grouped Bar Chart"` — second category on the **`group`** channel, drawn as304 **side-by-side (dodged)** bars within each `x` cluster (compare values305 directly). Put the clustering category on `group`, *not* `color`.306307Rule of thumb: comparing parts-to-whole → Stacked; comparing values308side-by-side → Grouped (use `group`); single series → Bar.309310**Waterfall color is a special "Type" column, not a free category.** On a311`"Waterfall Chart"` the `color` channel is reserved for a *type* field whose312values are literally `start`, `delta`, and `end` — it drives which bars anchor313to zero, not an arbitrary grouping. Do **not** bind `color` to an314`Increase`/`Decrease` (or up/down, gain/loss) category: the up/down direction is315already derived from the **sign** of the `y` value and colored automatically316(green up / red down). For the common case, **omit `color` entirely** and let317Flint infer the start/delta/end and per-bar sign coloring. To force which bars318are anchored totals, use the `totals` property (`first`/`last`/`both`), not a319color field. Only bind `color` when you genuinely have a `start`/`delta`/`end`320type column.321322**Backend coverage.** Vega-Lite supports all of the above. Other backends323support a subset (verify if targeting a non-VL backend):324325- **ECharts** adds: `"Gauge"`,326 `"Funnel"`, `"Treemap"`, `"Sunburst"`, `"Sankey"`,327 `"Parallel Coordinates"`, `"Graph"`, `"Tree"`.328- **Chart.js** supports: Scatter, Bubble, Bar, Grouped Bar, Stacked Bar,329 Lollipop, Bump, Combo, Line, Area, Range Area, Pie, Doughnut, Histogram,330 Radar, Rose, Slope, Connected Scatter.331332You do not need to call the library or inspect its source to author the333input — pick from this table.334335## Step 2 — map fields to channels336337Each channel maps to an **encoding object** `{ field, ... }` (or a bare338string shorthand, expanded to `{ field: "<string>" }`):339340```json341"encodings": {342 "x": { "field": "weight" },343 "y": "mpg",344 "color": { "field": "origin" }345}346```347348**Encoding object fields** (all optional except `field`):349350| Field | Values | Purpose |351|---|---|---|352| `field` | column name | Bind the channel to a data column |353| `type` | `quantitative`, `nominal`, `ordinal`, `temporal` | Override the inferred encoding type (rarely needed) |354| `aggregate` | `count`, `sum`, `average`, `mean` | Force an aggregation on a measure channel |355| `sortOrder` | `ascending`, `descending` | Sort direction for a discrete/sorted axis |356| `sortBy` | channel name (e.g. `"y"`) or field | Sort a category axis by another channel's measure |357| `scheme` | Vega scheme name (e.g. `viridis`, `redblue`) | Color scheme for the `color` channel |358359You usually don't need `type`, `aggregate`, or `sortOrder` — they're360inferred from the semantic type. Set them only with specific intent.361362**Multi-series (wide → long).** To plot several measure columns as series,363pass an **array** on `x` or `y` (only those two channels). The library364folds them into long form and synthesizes a series/legend field:365366```json367"encodings": { "x": { "field": "month" }, "y": ["sales", "profit"] }368```369370All array fields must be quantitative, and you cannot also bind `color`371when using the array form (the fold owns the color/legend). This is the372**only** built-in reshape — there is no `transforms`/`fold` property. For any373other shape (long↔wide, an aggregate the encodings can't express, a derived374column, a pivot, a join), reshape the data first with a host tool — pandas/polars,375Arquero/`Array.map`/SQL, or a data/MCP tool — and pass the result as376`data.values`. If you have no way to transform, surface the gap to the developer377rather than inventing a transform property that does not exist.378379## Step 3 — annotate with semantic types380381**This is the most important step.** Semantic types drive all downstream382decisions — formatting, zero baseline, color scheme, scale direction, and383more. Pick the most specific type for each field. Full registered set:384385| Family | Semantic types |386|---|---|387| Temporal (point) | `DateTime`, `Date`, `Time`, `Timestamp` |388| Temporal (granule) | `Year`, `Quarter`, `Month`, `Week`, `Day`, `Hour`, `YearMonth`, `YearQuarter`, `YearWeek`, `Decade` |389| Temporal (span) | `Duration` |390| Measure (amount) | `Amount`, `Price`, `Quantity`, `Count`, `Number` |391| Measure (proportion) | `Percentage` |392| Measure (signed/diverging) | `Profit`, `PercentageChange`, `Sentiment`, `Correlation` |393| Measure (physical) | `Temperature` |394| Discrete / rank | `Rank`, `Score`, `ID` |395| Geographic (coord) | `Latitude`, `Longitude` |396| Geographic (place) | `Country`, `State`, `City`, `Region`, `Address`, `ZipCode` |397| Categorical | `Category`, `Name`, `Status`, `Boolean`, `Direction`, `Range` |398| Fallback | `Unknown` |399400What choosing well gets you (automatically):401402- `Price` / `Amount` → currency formatting, zero baseline, sequential color403- `Temperature` → diverging color scheme, no forced zero baseline404- `Correlation` → fixed `[-1, 1]` diverging domain405- `Rank` → reversed axis (1 on top), discrete color406- `Date` / `DateTime` → temporal axis with auto-granularity formatting407- `Percentage` → percent formatting, 0–100 domain awareness408409If you don't know, use `Quantity` for numbers, `Category` for strings,410`Date`/`DateTime` for date-shaped values. Do **not** invent type names.411412### Saying more than the type name413414A field's entry can be an object instead of a string when the type alone415understates what you know:416417```json418"semantic_types": {419 "anomaly": { "semanticType": "Quantity", "unit": "°C", "divergingMidpoint": 0 },420 "rating": { "semanticType": "Score", "intrinsicDomain": [1, 5] }421}422```423424- `unit` — the unit or currency code: `"USD"`, `"°C"`, `"kg"`.425- `intrinsicDomain` — the field's own bounds, for bounded scales only: `[1, 5]`426 for a five-star rating, `[0, 100]` for a percentage score. Not for427 open-ended measures.428- `divergingMidpoint` — where the middle colour of a diverging scale sits.429 Set it if you can tell what the reader is comparing against; leave it out if430 you can't.431- `sortOrder` — the order the categories should appear in, when the order in432 the data is not the one you want and it isn't alphabetical either:433 `["Low", "Medium", "High"]`. For a handful of categories, not a long list.434435## Chart-level properties (`chartProperties`)436437`chartProperties` is an optional per-chart tuning map. Set a property only438when the user asks for that behavior — defaults are sensible. These are439**design choices**, not styling overrides (colors/fonts/ticks are still440derived). Values are clamped to the ranges shown.441442| Chart type | Property | Type / range (default) | Effect |443|---|---|---|---|444| Bar Chart | `cornerRadius` | 0–15 (0) | Round bar corners (px) |445| Area / Stacked Bar | `stackMode` | `stacked` \| `normalize` \| `center` \| `layered` (unset) | Stacking behavior; `normalize` = 100%, `center` = streamgraph |446| Grouped Bar / Boxplot | `dodge` | `auto` \| `local` \| `global` (`auto`) | `local` compacts sparse groups per category; `global` preserves aligned group lanes; leave `auto` unless the user requests one |447| Line / Area / Sparkline | `interpolate` | `linear` \| `monotone` \| `step` \| `step-before` \| `step-after` \| `basis` \| `cardinal` \| `catmull-rom` (`linear`) | Curve shape |448| Line / ECDF Plot | `showPoints` | boolean (false) | Draw point markers on the line |449| Sparkline | `baseline` | `mean` \| `zero` \| `median` \| `none` (`mean`) | Reference line per spark row |450| Sparkline | `trendWidth` | 80–600 (240) | Mini line-plot width (px) |451| Boxplot | `whiskerMethod` | `iqr` \| `minmax` (`iqr`) | Whisker rule (Tukey 1.5×IQR vs min–max) |452| Boxplot | `showOutliers` | boolean (true) | Show outlier points (Tukey only) |453| Area | `opacity` | 0.1–1 (0.7) | Fill opacity |454| Scatter | `opacity` | 0.1–1 (1) | Point opacity |455| Strip Plot | `stepWidth` | 10–100 (20) | Jitter spread |456| Strip Plot | `pointSize` | 0–150 (0=auto) | Point size |457| Strip Plot | `opacity` | 0–1 (0=auto) | Point opacity |458| Histogram | `binCount` | 5–50 (10) | Number of bins |459| Density Plot | `bandwidth` | 0.05–2 (0=auto) | Kernel bandwidth |460| Pie Chart | `innerRadius` | 0–100 (0) | Donut hole size (>0 → donut) |461| Pie / Rose | `sortSlices` | `none` \| `descending` \| `ascending` (`none`) | Order wedges and their legend by slice value |462| Rose Chart | `alignment` | `left` \| `center` (`left`) | Wedge alignment |463| Rose Chart | `padAngle` | 0–0.1 (0) | Gap between slices |464| Lollipop | `dotSize` | 20–300 (80) | Circle size (px) |465| Waterfall | `cornerRadius` | 0–8 (0) | Round bar corners |466| Waterfall | `totals` | `auto` \| `none` \| `first` \| `last` \| `both` (`auto`) | Which bars anchor to zero as totals (only when no Type column) |467| Waterfall | `showTextLabels` | boolean (false) | Legacy spelling of `showValueLabels`; still accepted |468| Bar / Grouped Bar / Stacked Bar / Lollipop / Pyramid / Pie / Donut / Heatmap / Waterfall | `showValueLabels` | boolean | Print the numbers on the marks. Works with or without a theme: unset, it follows the house's own habit at this density (and with no house named, stays off), so the default the compiler reports is always the honest one. Set it to overrule that for one chart. Reported inapplicable (and ignored) where the marks are too dense to carry readable numbers, or where the template already writes its own text, so it is never a control that does nothing. On a stacked bar each segment prints its own value in the middle of the segment (at the edge it would read as the running total); segments too thin to hold a line of text go unlabelled, and a normalized stack prints each segment's share rather than its raw value, since the share is what the length shows. The printed number is rounded to roughly three significant figures — with a k/M suffix once the values get long, and enough decimals that the smallest value in the series still says something — so a raw `3.14159265` lands as `3.14` and a series of `0.001` to `5000` reads at both ends. Rounding never goes so far that two marks of different size print the same number, or that a non-zero value prints as `0`; where a house asked for a coarser precision than that, the digits are raised until the labels agree with the marks. |469| Regression | `regressionMethod` | `linear` \| `log` \| `exp` \| `pow` \| `quad` \| `poly` (`linear`) | Fit method |470| Regression | `polyOrder` | 1–5 (3) | Polynomial order (when `poly`) |471| Radar | `filled` | boolean (true) | Fill the polygon |472| Radar | `fillOpacity` | 0–0.5 (0.15) | Polygon fill opacity |473| Radar | `strokeWidth` | 0.5–4 (1.5) | Line width |474| KPI Card | `behindThreshold` | 0–1 (0.5) | Value/goal ratio cutoff for color |475| Map | `region` | `us` \| `world` \| `auto` (`auto`) | Geographic scope |476| Map | `projection` | `mercator` \| `equalEarth` \| `orthographic` \| `stereographic` \| `conic` \| `mollweide` | Map projection |477478**Cross-cutting properties** (apply to position/faceted charts when479relevant; set only to force non-default behavior):480481- `independentYAxis` (boolean) — faceted charts: give each panel its own482 y-scale.483- `logScale_x` / `logScale_y` (boolean) — force a logarithmic axis.484- `includeZero_x` / `includeZero_y` (boolean) — force the axis to include 0.485- `xAxisType` / `yAxisType` (`temporal` | `nominal`) — force a temporal486 field to render as discrete bands (or vice-versa).487488## Parameter overrides — when to reach for them489490Overrides exist, but prefer letting semantic types drive decisions. Reach491for an override only when the user's intent genuinely conflicts with the492default:493494- **Force an aggregation:** `encodings.y = { field: "sales", aggregate: "sum" }`.495- **Sort a category axis by its measure:** `encodings.x = { field: "name", sortBy: "y", sortOrder: "descending" }`.496- **Pick a color scheme:** `encodings.color = { field: "region", scheme: "tableau10" }`.497- **Override an inferred type:** `encodings.x = { field: "year", type: "ordinal" }` (e.g. treat a year as discrete bands).498- **Use readable field titles:** `field_display_names = { percentageOfCountries: "Percentage of countries" }`.499 Keep encodings bound to the real column name; Flint uses the display name for axis titles and legend headers.500- **Resize the chart:** Flint sizes from two numbers — `baseSize` (the *target*501 it aims for, default 400×320) and `canvasSize` (a *hard ceiling* it may never502 exceed). With dense data the chart stretches from base toward the ceiling.503 - Want a comfortable size that may grow for dense data → set `chart_spec.baseSize = { width, height }`.504 - Want a fixed slot it must fit inside → set `chart_spec.canvasSize = { width, height }` alone; the chart fills it and shrinks to fit, never overflowing. *What you ask for is what you get.*505 - Both → aims for `baseSize`, grows toward `canvasSize`, never beyond.506- **Force log / zero baseline:** the `logScale_*` / `includeZero_*` chart507 properties above.508509Global layout tuning lives in the top-level `options` object (e.g.510`addTooltips`, band padding, facet sizing). It is rarely needed for511authoring — omit it unless asked.512513## Worked examples514515In each example `data` is a placeholder — the host binds real rows or a516URL. You author only `chart_spec` and `semantic_types`.517518### Scatter plot519520User: "Plot car weight vs fuel economy, colored by origin."521522```json523{524 "data": { "values": [] },525 "semantic_types": {526 "weight": "Quantity",527 "mpg": "Quantity",528 "origin": "Country"529 },530 "chart_spec": {531 "chartType": "Scatter Plot",532 "encodings": {533 "x": { "field": "weight" },534 "y": { "field": "mpg" },535 "color": { "field": "origin" }536 },537 "baseSize": { "width": 400, "height": 300 }538 }539}540```541542### Revenue bar chart with facets, sorted by value543544User: "Show revenue by product line, biggest first, one panel per region."545546```json547{548 "data": { "values": [] },549 "semantic_types": {550 "product_line": "Category",551 "revenue": "Amount",552 "region": "Region"553 },554 "chart_spec": {555 "chartType": "Bar Chart",556 "encodings": {557 "x": { "field": "product_line", "sortBy": "y", "sortOrder": "descending" },558 "y": { "field": "revenue" },559 "column": { "field": "region" }560 }561 }562}563```564565### Time series, multiple series (wide → long via array)566567User: "Line chart of monthly sales and profit."568569```json570{571 "data": { "values": [] },572 "semantic_types": {573 "month": "YearMonth",574 "sales": "Amount",575 "profit": "Profit"576 },577 "chart_spec": {578 "chartType": "Line Chart",579 "encodings": {580 "x": { "field": "month" },581 "y": ["sales", "profit"]582 },583 "chartProperties": { "interpolate": "monotone", "showPoints": true }584 }585}586```587588### Donut chart (Pie + innerRadius), value on `size`589590User: "Show market share by vendor as a donut."591592Pie/donut maps the slice value to `size` (rendered as angle) and the593category to `color`. Data is already long (one row per vendor).594595```json596{597 "data": { "values": [] },598 "semantic_types": {599 "vendor": "Category",600 "share": "Percentage"601 },602 "chart_spec": {603 "chartType": "Pie Chart",604 "encodings": {605 "size": { "field": "share" },606 "color": { "field": "vendor" }607 },608 "chartProperties": { "innerRadius": 60 }609 }610}611```612613### Bullet chart (KPI vs target)614615User: "Show each rep's sales against their quota."616617```json618{619 "data": { "values": [] },620 "semantic_types": {621 "rep": "Name",622 "sales": "Amount",623 "quota": "Amount"624 },625 "chart_spec": {626 "chartType": "Bullet Chart",627 "encodings": {628 "y": { "field": "rep" },629 "x": { "field": "sales" },630 "goal": { "field": "quota" }631 }632 }633}634```635636## What you should NOT do637638- **Don't re-emit the data.** Reference columns by name; let the host bind639 `data` (url, variable, or small literal). Never paste large datasets.640- **Don't write backend specs directly** — write the `ChartAssemblyInput`,641 then call the assembler. That's the whole point.642- **Don't invent transforms.** The only built-in reshape is the array form643 on `x`/`y`. If the data shape is wrong for the chart, say so and ask the644 host to reshape it.645- **Don't invent field names.** Reference only columns that exist, spelled646 exactly. If the data is the wrong shape for the chart, reshape it upstream647 rather than guessing column names that aren't there.648- **Don't set `type`/`aggregate`/`sortOrder`** unless intent conflicts649 with the default.650- **Don't pass colors, font sizes, axis tick counts** — the compiler651 derives these. Users fine-tune the *output* spec.652- **Don't invent semantic type names.** If none fit, use the family653 default (`Quantity`, `Category`, `Date`).654- **Don't call the library to discover channels/types** — this document is655 the authoring reference.656657## Validation checklist658659Before returning, verify:6606611. `chartType` is an exact registered name supported by the target backend.6622. Every `field` referenced in `encodings` is a real column name.6633. Every encoded field has an entry in `semantic_types` (specific type).6644. Required channels for the chart type are present (e.g. Bullet→`goal`,665 Candlestick→`open/high/low/close`, Pie→`size`+`color`).6665. Any `chartProperties` keys are valid for that chart type and in range.6676. You did **not** inline large data or hand-tune derived styling.6687. The data carries no embedded total/subtotal level (e.g. an `all` / `total`669 row) mixed with its components on a stacked, grouped, or colored channel.