Charts you write in Markdown
A plot block holds the options of Observable Plot as plain JSON. plotdown draws it as a chart on GitHub and GitLab, on your docs site, in your editor and in your chat app.
The block, in your Markdown file
```plot
{
"title": "Hours of daylight",
"subtitle": "Oslo and Buenos Aires, on the 15th of each month",
"data": {"sun": [
{"month": "Jan", "Oslo": 6.9, "Buenos Aires": 14.2},
{"month": "Feb", "Oslo": 9.2, "Buenos Aires": 13.2},
{"month": "Mar", "Oslo": 11.8, "Buenos Aires": 12.1},
{"month": "Apr", "Oslo": 14.6, "Buenos Aires": 11.0},
{"month": "May", "Oslo": 17.2, "Buenos Aires": 10.1},
{"month": "Jun", "Oslo": 18.8, "Buenos Aires": 9.7},
{"month": "Jul", "Oslo": 18.1, "Buenos Aires": 9.9},
{"month": "Aug", "Oslo": 15.7, "Buenos Aires": 10.7},
{"month": "Sep", "Oslo": 12.9, "Buenos Aires": 11.8},
{"month": "Oct", "Oslo": 10.1, "Buenos Aires": 12.9},
{"month": "Nov", "Oslo": 7.5, "Buenos Aires": 13.9},
{"month": "Dec", "Oslo": 6.0, "Buenos Aires": 14.4}
]},
"x": {"domain": ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], "label": null},
"y": {"domain": [0, 20], "grid": true, "label": "Hours"},
"marks": [
{"mark": "ruleY", "data": [12], "strokeDasharray": "2,3"},
{"mark": "lineY", "data": "sun", "x": "month", "y": "Oslo",
"stroke": "#5079e0", "strokeWidth": 2.5,
"curve": "catmull-rom", "marker": "circle"},
{"mark": "lineY", "data": "sun", "x": "month", "y": "Buenos Aires",
"stroke": "#e8a10c", "strokeWidth": 2.5,
"curve": "catmull-rom", "marker": "circle"},
{"mark": "text", "data": [0], "x": ["Jun"], "y": [18.8],
"text": ["Oslo"], "dy": -14, "fill": "#5079e0",
"fontSize": 13, "fontWeight": "bold"},
{"mark": "text", "data": [0], "x": ["Jun"], "y": [9.7],
"text": ["Buenos Aires"], "dy": 16, "fill": "#e8a10c",
"fontSize": 13, "fontWeight": "bold"}
]
}
```
What the reader sees
Hours of daylight
Oslo and Buenos Aires, on the 15th of each month
One block, wherever Markdown is read
Write the block once. Each place that shows Markdown needs one small step, taken once, by the person who runs it.
Sites you publish
The site owner adds one script tag. plotdown.js finds the blocks in the HTML your tool writes and draws them in the reader's browser.
- Any HTML page One script tag
- Jekyll and GitHub Pages A head include
- Hugo A code block render hook
- MkDocs custom_fences
- mdBook additional-js
- Docsify A script tag
- Pandoc and Quarto A Lua filter
- Gitea and Forgejo A footer template
Markdown pipelines
The developer adds a parser plugin. The chart is drawn when the page is built, so the page ships the SVG and needs no script.
- VitePress markdown-it plugin
- Docusaurus rehype plugin
- Astro rehype plugin
- markdown-it md.use(plotdown)
- unified and rehype rehypePlugins
- marked marked.use(plotdown())
Apps and chat interfaces
The developer renders a component. A block still streaming shows a placeholder, then the chart once its JSON is complete.
- React and Streamdown <Plotdown> or plotRenderer
GitHub and GitLab
The repository owner commits an SVG next to each block, or links an image. Readers need nothing.
- GitHub Action, extension, image links
- GitLab A CI job
- Command line plotdown render, plotdown link
- Image links Comments, chat, email
Sites you only read
The reader installs the browser extension or the userscript. Blocks become charts on GitHub, GitLab, AI chats and any page, for that reader alone.
- Browser extension Chrome, Edge, Firefox, userscript
Editors and agents
You install a plugin, or give your model the skill so that it writes valid blocks.
- VS Code Markdown preview and notebooks
- Obsidian A community plugin
- MCP hosts An MCP App server
- Agent skill SKILL.md and llms.txt
Real charts, written as blocks
Each chart on this page is a plot block that plotdown.js found and drew in your browser. Press Source on any of them to read its block.
Stacked areas
{
"title": "Unemployed workers by industry",
"data": {
"jobs": [
{"date": "2024-01-01", "industry": "Construction", "unemployed": 612},
{"date": "2024-02-01", "industry": "Construction", "unemployed": 640},
{"date": "2024-03-01", "industry": "Construction", "unemployed": 690},
{"date": "2024-04-01", "industry": "Construction", "unemployed": 720},
{"date": "2024-05-01", "industry": "Construction", "unemployed": 700},
{"date": "2024-06-01", "industry": "Construction", "unemployed": 650},
{"date": "2024-01-01", "industry": "Manufacturing", "unemployed": 830},
{"date": "2024-02-01", "industry": "Manufacturing", "unemployed": 810},
{"date": "2024-03-01", "industry": "Manufacturing", "unemployed": 800},
{"date": "2024-04-01", "industry": "Manufacturing", "unemployed": 790},
{"date": "2024-05-01", "industry": "Manufacturing", "unemployed": 770},
{"date": "2024-06-01", "industry": "Manufacturing", "unemployed": 760},
{"date": "2024-01-01", "industry": "Retail", "unemployed": 910},
{"date": "2024-02-01", "industry": "Retail", "unemployed": 880},
{"date": "2024-03-01", "industry": "Retail", "unemployed": 860},
{"date": "2024-04-01", "industry": "Retail", "unemployed": 900},
{"date": "2024-05-01", "industry": "Retail", "unemployed": 940},
{"date": "2024-06-01", "industry": "Retail", "unemployed": 920}
]
},
"y": {"grid": true, "label": "Unemployed (thousands)"},
"color": {"legend": true},
"marks": [
{
"mark": "areaY",
"data": "jobs",
"x": "date",
"y": "unemployed",
"fill": "industry",
"transform": [{"stackY": {"order": "appearance"}}]
},
{"mark": "ruleY", "data": [0]}
]
}
areaY, ruleY; transform stackY. Edit in the playgroundContours
{
"data": {
"grid": [
{"x": 0, "y": 0, "v": 0},
{"x": 0, "y": 1, "v": 0},
{"x": 0, "y": 2, "v": 0},
{"x": 0, "y": 3, "v": 0},
{"x": 0, "y": 4, "v": 0},
{"x": 1, "y": 0, "v": 0.58},
{"x": 1, "y": 1, "v": 0.52},
{"x": 1, "y": 2, "v": 0.36},
{"x": 1, "y": 3, "v": 0.13},
{"x": 1, "y": 4, "v": -0.1},
{"x": 2, "y": 0, "v": 1.04},
{"x": 2, "y": 1, "v": 0.94},
{"x": 2, "y": 2, "v": 0.65},
{"x": 2, "y": 3, "v": 0.26},
{"x": 2, "y": 4, "v": -0.15},
{"x": 3, "y": 0, "v": 1.3},
{"x": 3, "y": 1, "v": 1.18},
{"x": 3, "y": 2, "v": 0.84},
{"x": 3, "y": 3, "v": 0.37},
{"x": 3, "y": 4, "v": -0.12},
{"x": 4, "y": 0, "v": 1.31},
{"x": 4, "y": 1, "v": 1.2},
{"x": 4, "y": 2, "v": 0.89},
{"x": 4, "y": 3, "v": 0.46},
{"x": 4, "y": 4, "v": 0.02}
]
},
"color": {"type": "linear", "scheme": "viridis"},
"marks": [
{
"mark": "contour",
"data": "grid",
"x": "x",
"y": "y",
"fill": "v",
"stroke": "white",
"thresholds": 6,
"blur": 1
},
{"mark": "dot", "data": "grid", "x": "x", "y": "y", "r": 1.5, "fill": "black"}
]
}
contour, dot. Edit in the playgroundBollinger bands
{
"data": {
"aapl": [
{"date": "2024-03-01", "close": 129.6},
{"date": "2024-03-02", "close": 130.2},
{"date": "2024-03-03", "close": 128.9},
{"date": "2024-03-04", "close": 131.5},
{"date": "2024-03-05", "close": 133.1},
{"date": "2024-03-06", "close": 132.4},
{"date": "2024-03-07", "close": 134.8},
{"date": "2024-03-08", "close": 136},
{"date": "2024-03-09", "close": 135.2},
{"date": "2024-03-10", "close": 137.9},
{"date": "2024-03-11", "close": 139.4},
{"date": "2024-03-12", "close": 138.1},
{"date": "2024-03-13", "close": 140.6},
{"date": "2024-03-14", "close": 142.3},
{"date": "2024-03-15", "close": 141},
{"date": "2024-03-16", "close": 143.8},
{"date": "2024-03-17", "close": 145.2},
{"date": "2024-03-18", "close": 144.1},
{"date": "2024-03-19", "close": 146.7},
{"date": "2024-03-20", "close": 148}
]
},
"y": {"grid": true},
"marks": [
{"mark": "bollingerY", "data": "aapl", "x": "date", "y": "close", "n": 5, "k": 2, "fill": "steelblue"},
{"mark": "lineY", "data": "aapl", "x": "date", "y": "close", "strokeWidth": 1}
]
}
bollingerY, lineY. Edit in the playgroundVoronoi and Delaunay
{
"data": {
"penguins": [
{
"species": "Adelie",
"island": "Torgersen",
"bill_length_mm": 39.1,
"flipper_length_mm": 181,
"body_mass_g": 3750,
"sex": "MALE"
},
{
"species": "Adelie",
"island": "Torgersen",
"bill_length_mm": 39.5,
"flipper_length_mm": 186,
"body_mass_g": 3800,
"sex": "FEMALE"
},
{
"species": "Adelie",
"island": "Biscoe",
"bill_length_mm": 37.8,
"flipper_length_mm": 174,
"body_mass_g": 3400,
"sex": "FEMALE"
},
{
"species": "Adelie",
"island": "Dream",
"bill_length_mm": 40.6,
"flipper_length_mm": 199,
"body_mass_g": 4250,
"sex": "MALE"
},
{
"species": "Adelie",
"island": "Dream",
"bill_length_mm": 36.5,
"flipper_length_mm": 182,
"body_mass_g": 3150,
"sex": null
},
{
"species": "Chinstrap",
"island": "Dream",
"bill_length_mm": 46.5,
"flipper_length_mm": 192,
"body_mass_g": 3500,
"sex": "FEMALE"
},
{
"species": "Chinstrap",
"island": "Dream",
"bill_length_mm": 50,
"flipper_length_mm": 196,
"body_mass_g": 3900,
"sex": "MALE"
},
{
"species": "Chinstrap",
"island": "Dream",
"bill_length_mm": 51.3,
"flipper_length_mm": 197,
"body_mass_g": 3650,
"sex": "MALE"
},
{
"species": "Chinstrap",
"island": "Dream",
"bill_length_mm": 45.4,
"flipper_length_mm": 188,
"body_mass_g": 3525,
"sex": "FEMALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 46.1,
"flipper_length_mm": 211,
"body_mass_g": 4500,
"sex": "FEMALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 50,
"flipper_length_mm": 230,
"body_mass_g": 5700,
"sex": "MALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 48.7,
"flipper_length_mm": 210,
"body_mass_g": 4450,
"sex": "FEMALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 50,
"flipper_length_mm": 218,
"body_mass_g": 5700,
"sex": "MALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 47.6,
"flipper_length_mm": 215,
"body_mass_g": 5400,
"sex": null
}
]
},
"marks": [
{
"mark": "voronoi",
"data": "penguins",
"x": "flipper_length_mm",
"y": "body_mass_g",
"fill": "species",
"fillOpacity": 0.2,
"stroke": "white"
},
{
"mark": "hull",
"data": "penguins",
"x": "flipper_length_mm",
"y": "body_mass_g",
"stroke": "species",
"z": "species"
},
{"mark": "dot", "data": "penguins", "x": "flipper_length_mm", "y": "body_mass_g", "fill": "species"}
]
}
voronoi, hull, dot. Edit in the playgroundGrouped bars
{
"data": {
"penguins": [
{
"species": "Adelie",
"island": "Torgersen",
"bill_length_mm": 39.1,
"flipper_length_mm": 181,
"body_mass_g": 3750,
"sex": "MALE"
},
{
"species": "Adelie",
"island": "Torgersen",
"bill_length_mm": 39.5,
"flipper_length_mm": 186,
"body_mass_g": 3800,
"sex": "FEMALE"
},
{
"species": "Adelie",
"island": "Biscoe",
"bill_length_mm": 37.8,
"flipper_length_mm": 174,
"body_mass_g": 3400,
"sex": "FEMALE"
},
{
"species": "Adelie",
"island": "Dream",
"bill_length_mm": 40.6,
"flipper_length_mm": 199,
"body_mass_g": 4250,
"sex": "MALE"
},
{
"species": "Adelie",
"island": "Dream",
"bill_length_mm": 36.5,
"flipper_length_mm": 182,
"body_mass_g": 3150,
"sex": null
},
{
"species": "Chinstrap",
"island": "Dream",
"bill_length_mm": 46.5,
"flipper_length_mm": 192,
"body_mass_g": 3500,
"sex": "FEMALE"
},
{
"species": "Chinstrap",
"island": "Dream",
"bill_length_mm": 50,
"flipper_length_mm": 196,
"body_mass_g": 3900,
"sex": "MALE"
},
{
"species": "Chinstrap",
"island": "Dream",
"bill_length_mm": 51.3,
"flipper_length_mm": 197,
"body_mass_g": 3650,
"sex": "MALE"
},
{
"species": "Chinstrap",
"island": "Dream",
"bill_length_mm": 45.4,
"flipper_length_mm": 188,
"body_mass_g": 3525,
"sex": "FEMALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 46.1,
"flipper_length_mm": 211,
"body_mass_g": 4500,
"sex": "FEMALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 50,
"flipper_length_mm": 230,
"body_mass_g": 5700,
"sex": "MALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 48.7,
"flipper_length_mm": 210,
"body_mass_g": 4450,
"sex": "FEMALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 50,
"flipper_length_mm": 218,
"body_mass_g": 5700,
"sex": "MALE"
},
{
"species": "Gentoo",
"island": "Biscoe",
"bill_length_mm": 47.6,
"flipper_length_mm": 215,
"body_mass_g": 5400,
"sex": null
}
]
},
"color": {"legend": true},
"marks": [
{
"mark": "barY",
"data": "penguins",
"x": "island",
"fill": "species",
"sort": {"x": "-y"},
"transform": [{"filter": "sex"}, {"groupX": {"y": "count"}}]
},
{"mark": "ruleY", "data": [0]}
]
}
barY, ruleY; transforms filter, groupX. Edit in the playgroundWaffle
{
"data": {
"fruit": [{"fruit": "apples", "count": 23}, {"fruit": "bananas", "count": 15}, {"fruit": "cherries", "count": 31}]
},
"marks": [
{"mark": "waffleY", "data": "fruit", "x": "fruit", "y": "count", "fill": "fruit", "unit": 1, "gap": 2},
{"mark": "ruleY", "data": [0]}
]
}
waffleY, ruleY. Edit in the playgroundThe format is Plot's own options
If you know Plot.plot(), you know the block. Each mark is an object named by "mark", each transform an entry of "transform", and fields are named by strings.
In Markdown
```plot
{
"title": "Penguin body mass",
"data": {"penguins": {"csv": "species,body_mass_g\nAdelie,3750\nGentoo,5000\nChinstrap,3500\n"}},
"color": {"legend": true},
"y": {"grid": true},
"marks": [
{"mark": "rectY", "data": "penguins", "x": "body_mass_g", "fill": "species",
"transform": [{"binX": {"y": "count"}}]},
{"mark": "ruleY", "data": [0]}
]
}
```
The same chart in JavaScript
Plot.plot({
title: "Penguin body mass", color: {legend: true}, y: {grid: true},
marks: [
Plot.rectY(penguins, Plot.binX({y: "count"}, {x: "body_mass_g", fill: "species"})),
Plot.ruleY([0])
]
})
Strict JSON, nothing to run. Any language can read and check a block. A renderer evaluates nothing, so blocks from strangers or from a model are safe to show.
Errors name the place. An unknown key, mark or reducer is an error with its JSON path, such as $.marks[1].transform[0].binX.y, instead of a chart that is silently wrong.
No network by default. Data sits in the block, as rows or CSV. Loading a URL is something the host turns on.
Read the spec or validate with the JSON Schema.
Models can write it too
Ask for a chart and a model answers with a block, if it knows the format. Give it the skill, or point it at the guide.
- The agent skill:
SKILL.mdfor Claude Code, Codex, Copilot and other agents that read skills. - llms.txt and llms-full.txt: the rules, the catalog of marks and 30 examples, for any system prompt.
- plot.schema.json: JSON Schema 2020-12, for structured outputs and editors, and for files named
*.plotdown.json. This URL follows the current spec; /schema/0.1/plot.schema.json stays that of this version. - The MCP App: a
render_plottool that shows the chart inside Claude, ChatGPT and VS Code.