Skip to content

Spec ​

Tiny versioned chart language for Markdown. Source is tabular data. Path: fence | GFM table | HTML comment → parser → Chart IR → type pack paint → deterministic SVG (table always kept). Tags chart, markvis, vis share one parser. No JSON-as-default. No JS in the fence.

Aligned with repo root SPEC.md. Agents that need a short brief should fetch /llms.txt.

Grammar ​

Order inside a fence:

  1. Optional markvis: 2
  2. Header fields, one key: value per line
  3. Blank line
  4. Data: CSV or one GFM table (header row required)
md
```chart
markvis: 2
type: bar
title: Feb led Q3 at 180
unit: USD k
x: month
y: revenue

month,revenue
Jan,120
Feb,180
Mar,150
```

Also valid under markvis or vis fences.

Progressive form — comment immediately followed by a GFM table:

md
<!-- chart: bar x=month y=revenue title="Feb led Q3 at 180" -->
| month | revenue |
| --- | --- |
| Jan | 120 |
| Feb | 180 |
| Mar | 150 |

Comment keys: type (required), x, y, title, unit, series, theme, palette, surface, plus legal encodings (layout, innerRadius, min, max, orient, role, target). Same meaning as fence headers.

Fields ​

FieldRequiredDefaultNotes
markvisno2Language version. Any other value → E_BAD_VERSION. It is not rewritten to 2.
typeyes—bar | line | area | scatter | pie | hist | heatmap | funnel | waterfall | radar | gauge | sankey | treemap | dumbbell | bullet | boxplot | calendar.
titlenoderivedConclusion title when present.
themenofolioGrammar only: folio | highcharts | shadcn | docs | ant | recharts | graphite.
palettenotheme defaultColors only: ink | porcelain | warm | cool | vivid. Omit → theme pack colors.
unitno—Display suffix for values.
xtypedfirst category / numeric colIndependent axis or labels.
ytypedfirst numeric colMeasure.
seriesno—Optional column that splits series.
surfacenolightlight | dark | export. Light, the default, paints no canvas so the host background shows through. Dark paints a dark paper and light ink. Export paints an opaque white card.
layoutnogrouped when omittedOn bar / line / area only: grouped | stacked | percent.
innerRadiusnotheme PIE_INNER_RATIOOn pie only. [0, 1]. Omit → theme hole; 0 = solid.
minnosee typeGauge omit → 0. Heatmap omit → data minimum. Bullet omit → min(0, data).
maxnogauge 100Gauge, heatmap, radar, or bullet. Omit on a gauge means a labeled 0–100 range, not the current value.
orientnoverticalOn bar only: horizontal | vertical.
rolenoevery row is a deltaOn waterfall only. Names a column of delta | total | subtotal.
targetnono targetOn bullet only. Names a column of targets; an empty cell has no target.
datayes—CSV or GFM after a blank line.

x / y / series must name real header columns. CORE keys stay separate from type-local encodings. Undeclared or illegal encodings → E_UNKNOWN_FIELD + table.

Types ​

TypexyseriesRules
barcategorynumberoptional; layoutKeep input row order. orient: horizontal puts categories on the side. Empty y is a gap unless stacked or percent.
lineordered category or numbernumberoptional; layoutKeep input row order. An empty y is a gap. The line does not connect through it.
areasame as linenumberoptional; layoutFill under the line. Missing y stays a gap.
scatternumbernumberoptionalOne mark per row. Repeated observations stay. Both axes are labeled.
pielabelnumber ≥ 0ignoredDo not normalize to 100. Optional innerRadius. An empty y is E_MISSING_VALUE.
histnumberoptional weightignoredSturges bins; table keeps raw rows. Repeated samples stay.
heatmapcategoryintensityrequiredLong form. Empty y is a missing cell, not zero. Optional min/max share the color domain.
funnelstagenumber ≥ 0ignoredCentered bands narrow from each stage to the next. The label sits to the right.
waterfallstepsigned deltaignoredDelta and the level after it are labeled. Optional role column. Totals are not inferred from the step name.
radarspokenumber ≥ 0optionalOne scale: max when it is at least the data max, otherwise the data max. A missing spoke is a gap. A grouped bar is clearer when the exact number matters.
gaugelabelnumberignoredOne row. A second row → E_DUP_KEY. Omit max → 100, omit min → 0.
sankeysourceflow ≥ 0required (target)One row = one link. Equal values share one thickness. A cycle → E_SANKEY_CYCLE.
treemaplabelnumber ≥ 0optional (parent)Flat or two levels. y≤0 omitted from paint.
dumbbellcategorynumberrequired, exactly two valuesFirst value hollow, second filled; the change prints at the right. A missing value is a gap.
bulletlabelnumber (actual)ignoredOptional target column and min/max. Past the scale → drawn to the edge, still labeled.
boxplotcategoryone observation per rowoptionalRepeated categories expected. Tukey box; under five values shows points and a median.
calendarYYYY-MM-DD datenumberignoredOne cell per day; position from the date. Bad date → E_BAD_DATE.

Encodings ​

EncodingLegal onValuesOmit
layoutbar line areagrouped | stacked | percentgrouped
innerRadiuspie[0, 1]theme PIE_INNER_RATIO (explicit 0 = solid)
mingauge heatmap bullet calendarnumbergauge 0; heatmap and calendar data min; bullet min(0, data)
maxgauge heatmap radar bullet calendarnumbergauge 100; heatmap and calendar data max; radar data max; bullet data max
orientbarhorizontal | verticalvertical
rolewaterfallcolumn of delta | total | subtotalevery row is a delta
targetbulletcolumn of numbersno target marker

Do not invent donut or stacked-bar type ids.

Fallback ​

  1. Never drop recovered rows.
  2. Emit a table of those rows plus one line with the stable error code.
  3. Valid HTML: figure → SVG + figcaption + data table.
  4. Same IR → same SVG bytes.

Error codes ​

CodeWhen
E_UNKNOWN_TYPEtype not in the allowed set.
E_TYPE_TYPONear-miss spelling of a known type.
E_JSON_DATAData body is JSON.
E_MISSING_HEADERNo CSV/GFM header row.
E_EMPTY_DATAHeader only, or zero data rows.
E_EXTRA_COLUMNRow width ≠ header width.
E_DUP_COLUMNDuplicate header names.
E_UNKNOWN_FIELDMissing column name; undeclared header; illegal encoding.
E_PIE_NEGATIVEPie value < 0.
E_NEGATIVE_VALUEFunnel, radar, sankey, or treemap value < 0, or a percent layout that includes a negative.
E_YAML_TABLE_CONFLICTHeader fields disagree with progressive table mapping.
E_EMPTY_FENCEFence body empty.
E_UNKNOWN_THEMEtheme not in the allow-list.
E_UNKNOWN_PALETTEpalette not in the allow-list.
E_BAD_VERSIONmarkvis is present and is not 2.
E_BAD_NUMBERA measure cell is not a finite number. The message names the row and column.
E_MISSING_VALUEA required measure cell is empty.
E_DUP_KEYThe same category/series key appears twice. Scatter, hist, and waterfall steps may repeat.
E_SANKEY_CYCLESankey links form a cycle. The table is kept.
E_BAD_DATEA calendar date is not YYYY-MM-DD or does not exist, or the dates span more than five calendar years.

theme is grammar. palette is colors only. Never merge them. Contribute a type: repo docs/TYPES.md.

0.0.13 lives under legacy/