```sql daily_orders
select 'Mon' as day, 120 as orders union all
select 'Tue', 180 union all
select 'Wed', 90
```
{% html %}
<div id="bars" style="display:flex; gap:8px; align-items:flex-end; height:120px;"></div>
<script>
// Wrap the draw in a function and register it with evidence.subscribe so
// it re-runs whenever a filter, theme, or variable changes. evidence.query
// always returns the latest interpolated rows, so the same function works
// for the first render AND every subsequent reactive re-render.
async function render() {
const rows = await evidence.query("daily_orders");
const max = Math.max(...rows.map((r) => r.orders));
document.getElementById("bars").innerHTML = rows
.map((r) => `<div style="flex:1; background:${evidence.theme.palette[0]}; height:${(r.orders / max) * 100}%"></div>`)
.join("");
}
evidence.subscribe(render);
await render();
evidence.ready();
</script>
{% /html %}
Examples
Basic Usage
```sql daily_orders
select 'Mon' as day, 120 as orders union all
select 'Tue', 180 union all
select 'Wed', 90
```
{% html %}
<div id="bars" style="display:flex; gap:8px; align-items:flex-end; height:120px;"></div>
<script>
// Wrap the draw in a function and register it with evidence.subscribe so
// it re-runs whenever a filter, theme, or variable changes. evidence.query
// always returns the latest interpolated rows, so the same function works
// for the first render AND every subsequent reactive re-render.
async function render() {
const rows = await evidence.query("daily_orders");
const max = Math.max(...rows.map((r) => r.orders));
document.getElementById("bars").innerHTML = rows
.map((r) => `<div style="flex:1; background:${evidence.theme.palette[0]}; height:${(r.orders / max) * 100}%"></div>`)
.join("");
}
evidence.subscribe(render);
await render();
evidence.ready();
</script>
{% /html %}
Responsive D3 chart from a CDN
{% html %}
<div id="chart"></div>
<script type="module">
import * as d3 from "https://esm.sh/d3@7";
const mount = document.getElementById("chart");
// viewBox + width:100% lets the SVG scale with the container — responsive
// with no redraw. Draw in a fixed coordinate space, then let CSS stretch it.
const W = 400, H = 220;
// Same pattern as the basic example: wrap the draw in a function so a
// filter / theme change can re-run it. d3.select(...).html("") clears any
// previous SVG before re-rendering.
async function render() {
const rows = await evidence.query("daily_orders");
mount.innerHTML = "";
const svg = d3.select(mount).append("svg")
.attr("viewBox", `0 0 ${W} ${H}`)
.attr("width", "100%")
.attr("height", "auto");
const x = d3.scaleBand().domain(rows.map((d) => d.day)).range([0, W]).padding(0.2);
const y = d3.scaleLinear().domain([0, d3.max(rows, (d) => d.orders)]).range([H, 0]);
svg.selectAll("rect").data(rows).join("rect")
.attr("x", (d) => x(d.day)).attr("y", (d) => y(d.orders))
.attr("width", x.bandwidth()).attr("height", (d) => H - y(d.orders))
.attr("fill", evidence.theme.palette[0]);
}
evidence.subscribe(render);
await render();
evidence.ready();
</script>
{% /html %}
Passing Variables
The {% html %} block runs in an isolated iframe, so the only way to get page-level values (frontmatter, repeat-scoped values, filter values, literals) into your author code is through the variables={…} attribute. The values are evaluated on the page, snapshotted into the iframe, and exposed as evidence.variables.
---
selected_country: France
---
{% dropdown name=region values="north,south,east,west" defaultValue="north" /%}
{% html variables={
greeting=$selected_country
region=region.value
limit=10
} %}
<p id="msg"></p>
<script>
const { greeting, region, limit } = evidence.variables;
document.getElementById('msg').textContent =
`Hello ${greeting}! Showing top ${limit} from the ${region} region.`;
// React to filter / repeat-scope / frontmatter changes:
evidence.onVariablesChange((next) => {
document.getElementById('msg').textContent =
`Hello ${next.greeting}! Showing top ${next.limit} from the ${next.region} region.`;
});
evidence.ready();
</script>
{% /html %}
Inside a repeat: the loop variable resolves at the call site, so each iteration gets a different evidence.variables.
{% repeat for=country in=country_list %}
{% html variables={ name=country.name population=country.population } %}
<p>{evidence.variables.name} — pop. {evidence.variables.population}</p>
{% /html %}
{% /repeat %}
Notes:
- Values must be serializable primitives (string / number / boolean / null). Objects, arrays, and functions are dropped before the snapshot reaches the iframe — flatten them at the call site, or query them through
evidence.query() instead.
evidence.variables is a snapshot; mutating the returned object doesn’t change anything (each read returns a fresh shallow copy).
- For row data, prefer
evidence.query("query_name") over packing rows into variables= — query results stream lazily and aren’t limited to primitives.
Network Allowlist
Author code inside an {% html %} block runs in a sandboxed iframe with a content-security-policy that blocks all network traffic except to the curated hosts below. fetch, XHR, d3.csv, and d3.json work against these hosts; everything else is blocked at the browser level.
For data from the user’s own report, always use evidence.query("query_name") instead — page rows live in the parent context and have no URL to fetch.
Script CDNs
Used for loading JS libraries via <script src> or import:
https://cdn.jsdelivr.net
https://esm.sh
https://esm.run
https://unpkg.com
https://cdnjs.cloudflare.com
https://d3js.org
Map tiles
Available to both <img> tag-based map libraries (Leaflet raster) and modern fetch/WebGL libraries (deck.gl, MapLibre):
https://tile.openstreetmap.org
https://a.tile.openstreetmap.org
https://b.tile.openstreetmap.org
https://c.tile.openstreetmap.org
https://a.basemaps.cartocdn.com
https://b.basemaps.cartocdn.com
https://c.basemaps.cartocdn.com
https://d.basemaps.cartocdn.com
https://tiles.stadiamaps.com
https://server.arcgisonline.com
https://services.arcgisonline.com
https://maps.wikimedia.org
Images
Image-only hosts (loadable in <img> tags but not via fetch):
https://upload.wikimedia.org
https://commons.wikimedia.org
https://flagcdn.com
Data and public APIs
Reachable from fetch, XHR, d3.csv, d3.json. Includes GeoJSON / TopoJSON / Atlas files on the data CDNs (e.g. unpkg.com/world-atlas@2/countries-110m.json) plus keyless public-data APIs:
https://cdn.jsdelivr.net
https://unpkg.com
https://raw.githubusercontent.com
https://api.frankfurter.app
https://restcountries.com
https://api.worldbank.org
https://api.open-meteo.com
https://www150.statcan.gc.ca
What’s NOT on the list
- Private corporate APIs (e.g. your internal data services)
- Key-required public APIs (OpenWeatherMap, FRED, NASA, exchangerate.host) — keys handed to iframe code leak the moment the report is shared
- Arbitrary third-party endpoints
If your visualization needs a host that isn’t on the list, ask your Evidence admin to add a project-level allowlist entry for it (a per-project allowlist surface is in the works). For broadly-useful keyless public hosts, open an issue and we’ll consider adding it to the default.
Attributes
Set the width of this component (in percent) relative to the page width
Set a fixed height for the chart in pixels
Frontmatter, repeat-scoped, or literal values to expose to the iframe as evidence.variables. Mirrors the variables= attribute on {% partial %} — write variables={ name=$frontmatter_name label="static" } (Markdoc object syntax: whitespace-separated key=value, no commas). Reactive: changing a value triggers evidence.onVariablesChange(cb) / evidence.subscribe(cb) inside the iframe.