plotdown

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.

Open the playground Find your setup

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

02468101214161820↑ HoursJanFebMarAprMayJunJulAugSepOctNovDecOsloBuenos Aires

Change it in the playground

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.

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.

Apps and chat interfaces

The developer renders a component. A block still streaming shows a placeholder, then the chart once its JSON is complete.

GitHub and GitLab

The repository owner commits an SVG next to each block, or links an image. Readers need nothing.

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.

Editors and agents

You install a plugin, or give your model the skill so that it writes valid 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]}
  ]
}
Marks areaY, ruleY; transform stackY. Edit in the playground

Contours

{
  "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"}
  ]
}
Marks contour, dot. Edit in the playground

Bollinger 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}
  ]
}
Marks bollingerY, lineY. Edit in the playground

Voronoi 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"}
  ]
}
Marks voronoi, hull, dot. Edit in the playground

Grouped 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]}
  ]
}
Marks barY, ruleY; transforms filter, groupX. Edit in the playground

Waffle

{
  "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]}
  ]
}
Marks waffleY, ruleY. Edit in the playground

See all 30 examples, one for each kind of mark

The 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.