plotdown

Any HTML page

One script tag renders every plot block of a page as a chart, including blocks the page adds later, such as a streamed chat answer or the next route of a single-page app.

The script

As a module, anywhere in the page:

<script type="module" src="https://cdn.jsdelivr.net/npm/@devlooped/plotdown/dist/plotdown.js"></script>

Or as a classic script, where a module cannot go (some CMS fields, old tools):

<script src="https://cdn.jsdelivr.net/npm/@devlooped/plotdown/dist/plotdown.iife.js" defer></script>

Both files hold plotdown, Observable Plot and d3; nothing else loads. Each chart is drawn in a shadow root just before its block, which it hides, with a Source button to switch back. A block with a mistake stays code, with the reason under it.

The markup it finds

Markup Written by
<pre><code class="language-plot"> markdown-it, marked, remark, Hugo, Jekyll, Python-Markdown, highlight.js, Prism
<pre class="plot"> Pandoc, SuperFences custom fences
<pre lang="plot"> GitHub's plain fences, some parsers
div.highlight-source-gnuplot GitHub (a JSON block only; real gnuplot stays code)
<pre data-canonical-lang="plot"> GitLab
class*="language-plot", lang-plot, data-lang, data-language Docsify, Gitea, Chroma, others

The fence names are plot, oplot and observable-plot. A plot block whose text does not start with { is left alone.

Options

On the script tag, or in globalThis.plotdownConfig set before the script runs (the way to go with a classic script, a userscript or a CMS):

Attribute plotdownConfig Default
data-auto="false" auto: false true Do not start; call plotdown.start()
data-allow-remote="true" or origins allowRemote: true or ["https://…"] off Let blocks load {"url": …} data and images
data-theme="dark" theme: "dark" "auto" Force light or dark
data-toggle="false" toggle: false true No Source button
data-copy="false" copy: false true No Copy button (its menu copies the block as JSON, or the chart as PNG or SVG)
data-settle="1000" settle: 1000 3000 Milliseconds before a block that does not parse shows its error (false: never), so a block still streaming shows none

The API

globalThis.plotdown (and @devlooped/plotdown/browser for bundlers, without side effects):

await plotdown.start({theme: "light"});     // render the page and watch it; {rendered, failed, pending}
plotdown.stop();                             // stop watching; charts stay
await plotdown.renderAll(element);           // render under element now, once
plotdown.configure({allowRemote: ["https://data.example"]});
const renderer = new plotdown.BlockRenderer(plotdown.configure({}));
await renderer.scan(shadowRoot);             // start() does not look into shadow roots
renderer.observe(shadowRoot);

Light and dark

The chart draws in the page's text color, on the background behind the block. The scheme comes from GitHub's data-color-mode, the CSS color-scheme, the background's luminance, then prefers-color-scheme, and it follows a theme toggle while the page is open.

CSP and self-hosting

The script needs no eval, no inline script and no inline style: under script-src 'self'; style-src 'self' it works when you serve the file yourself. Take dist/plotdown.js (or dist/plotdown.iife.js) from npm i @devlooped/plotdown, put it next to your pages, and load /plotdown.js. With the CDN, add https://cdn.jsdelivr.net to script-src; pin a version (@devlooped/plotdown@0.1.0/dist/plotdown.js) to keep it from changing under you.

Write blocks as the spec says, and try them in the playground.

Note

Tried with Microsoft Edge 156 on 2026-11-12: module, classic script with plotdownConfig, script-src 'self' with the file served by the page, and the API.

Edit this page on GitHub