Skip to main content

Examples

Basic Usage

Responsive D3 chart from a CDN

The evidence API

Every script inside the block can reach a single evidence object. It is the only bridge to the page — data, variables, theme, filters, and lifecycle all hang off it. The reliable pattern is to wrap your draw in async function render() { … }, then evidence.subscribe(render); await render();evidence.query returns fresh rows each call, so the same function serves the first render and every reactive re-render.

Passing Variables

The {% html %} block runs verbatim in an isolated iframe: the body is never interpolated, so the way a page-level value (frontmatter, a component attribute, a repeat-scoped value, a filter value, a literal) reaches your code is the variables={…} attribute. Nothing crosses into the sandbox that you didn’t pass. Values are evaluated on the page, snapshotted into the iframe, and read as evidence.variables.
Inside a repeat: pass the iteration’s value through with {{ }}, so each iteration gets a different evidence.variables.
Notes:
  • {{ $x }} does not interpolate inside the block — the body is verbatim. For plain text, write it in markdown outside the block (where {{ $x }} works normally); inside the block, pass the value via variables= and read evidence.variables.x from a script. Validation catches both mistakes with the exact fix.
  • Reading a value you didn’t pass returns undefined — only variables= entries exist inside the sandbox. Validation flags visible reads with no matching entry.
  • Reactivity: const speed = evidence.variables.speed at the top of your script captures the value once — when the attribute or filter behind it changes, your constant does not. evidence.variables itself is always current, so either read it where you use it (e.g. inside your render/animation loop), or register evidence.onVariablesChange((vars) => { /* re-render */ }) for structural changes. A change that arrives while nothing is listening logs a console warning explaining this.
  • 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 (start=$period.start), 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.

Parameterizing Queries from JS

evidence.query() takes no parameters. To re-run a query with different inputs, create a filter in your JS and reference it from the SQL: the query re-runs server-side, and the predicate is applied on the warehouse, so only matching rows enter the iframe. Prefer this over pulling a whole table and filtering client-side. The loop has four steps:
  1. Declare the filter in your script: evidence.filters.create("region", "north") (create it before anything .sets it).
  2. Reference it from any sql fence with {{ region }} (quoted value) or {{ region.literal }} (raw, for numbers).
  3. Set it from your interaction handler: evidence.filters.set("region", picked) — the query re-runs on the warehouse.
  4. React: evidence.subscribe(render) fires when the fresh result lands; call evidence.query() again inside render to get the new rows.
When not to use this: for high-frequency interaction over a dataset that fits in memory (scrubbing an animation slider, hover highlights), fetch once with evidence.query() and filter/redraw client-side — a warehouse round-trip per frame is the wrong tool. Use the filter loop when the table is too big to pull, or when other components on the page should react too (evidence.filters.create(id, value, { column: "the_column" }) makes built-in charts with filters="id" follow your selection).

Sizing and Responsiveness

By default the block autosizes: it grows and shrinks to fit its content height and fills the page width. Give your content a real height so it has something to size to:
  • A fixed-pixel element, or a responsive SVG sized with viewBox + width:100% + height:auto (which takes its height from the aspect ratio).
  • A height:100% element has nothing to fill in autosize mode. For a chart that fills a fixed box (canvas, ECharts, and Chart.js all read the container’s size), pass height= in pixels on the tag — that pins the box and makes the mount area full-height, so height:100% works.
Width reflows with the page, but a chart only follows if you build for it: scale SVGs with viewBox + width:100% (no redraw needed), or redraw from evidence.onResize(cb) (call the library’s resize method in the callback for canvas/ECharts/Chart.js). A hardcoded pixel width won’t reflow. The block is an isolated iframe, so its own width is the viewport width — CSS @media (max-width: 480px) queries fire at the block’s width, which makes them behave like container queries. Use them to reflow at narrow widths (stack columns, shrink type). Tooltips and popovers are clipped at the block edges — CSS overflow can’t escape the frame. Position them relative to your own container and clamp into bounds (e.g. left = Math.max(0, Math.min(x, mount.clientWidth - tip.offsetWidth))), or flip them near an edge.

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

Need data from another host?

For data from your own warehouse, use evidence.query("query_name") — page queries aren’t subject to this allowlist. To reach an external host that isn’t listed, ask your Evidence admin to add a project-level allowlist entry for it.

Attributes

number
Set the width of this component (in percent) relative to the page width
number
Set a fixed height for the chart in pixels
object
Frontmatter ($var), filter or repeat values ("{{ my_filter.literal }}"), and literals to expose to the iframe as evidence.variables. Write variables={ name=$frontmatter_name region="{{ region.literal }}" limit=10 } (Markdoc object syntax: whitespace-separated key=value, no commas). Filter/repeat values must be quoted {{ }} and use a real property — .literal (raw) or .selected (quoted for SQL); there is no .value. Changing a value triggers evidence.onVariablesChange(cb) / evidence.subscribe(cb) inside the iframe.