Skip to main content
```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

width
number
Set the width of this component (in percent) relative to the page width
height
number
Set a fixed height for the chart in pixels
variables
object
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.