Chart Template Documentation
Everything you need to write a custom chart template — the contract, the complete params reference, and the rules render(ctx) must follow.
How templates work
A template is a single JavaScript file that evaluates to an object. Files live in the repo's templates/ directory and are managed on the /templatespage (with a live preview fed by sample CSVs). To use one: add a plot to a project, pick the template as its type, then configure its params via the plot's settings (gear) dialog. The values are stored per-plot, so the same template can power many plots with different settings.
Template code runs only in the browser, inside try/catch and a React error boundary: a broken template shows an error card on its own chart and never affects the rest of the app. The backend stores the file but never executes it.
({
name: "Human readable chart name", // required-ish: falls back to "Unnamed Template"
description: "One-line description", // optional
params: [ /* see the params reference below */ ],
render(ctx) {
// ctx.versions: Array<{ id, label, columns, rows }> — every uploaded CSV version
// ctx.params: current values of the params declared above
// ctx.dark: true when dark mode is active
return { data: [ /* Plotly traces */ ], layout: { /* optional */ } };
},
})Params reference
Each entry in paramsdeclares one control in the plot's settings dialog. Every param needs a unique string key; the current values arrive in render(ctx) as ctx.params[key].
| type | UI control | value in ctx.params | extra fields |
|---|---|---|---|
string (default) | text input | string | — |
number | numeric input | number (or undefined while empty) | — |
boolean | checkbox | boolean | — |
column | dropdown of CSV column names (union across all versions) | string (column name) | — |
version | dropdown of uploaded versions | number (version id) | — |
select | dropdown of fixed choices | string | options: [...] (required) |
list | repeatable rows with add / remove buttons | array of objects keyed by field key | fields: [...] (required, non-empty) |
Common optional fields on every param: label (control caption; falls back to the key) and default (initial value before the user touches the control).
List params
A list param declares a row schema via fields — each field is itself a full param definition of anytype above, including nested lists. The dialog renders it as repeatable rows; new rows are pre-filled from the fields' defaults. If no default is set on the list itself, the value is [] (never undefined), so render() can always iterate it.
{
key: "lines",
label: "Lines",
type: "list",
fields: [
{ key: "version", label: "Version", type: "version" },
{ key: "column", label: "Y Column", type: "column" },
{ key: "scalar", label: "Scalar", type: "number", default: 1 },
{ key: "dashed", label: "Dashed", type: "boolean", default: false },
],
}
// The value arriving in ctx.params.lines is an array of row objects:
// [
// { version: 3, column: "price", scalar: 2, dashed: false },
// { version: 5, column: "price", scalar: 1, dashed: true },
// ]This is expressive enough to rebuild a multi-line chart — see templates/multi-line.js for a complete working example:
render(ctx) {
const lines = Array.isArray(ctx.params.lines) ? ctx.params.lines : [];
const data = lines.flatMap((line) => {
// Version ids in params are plain JSON — the version may have been
// deleted since the row was configured. Always look it up and skip.
const v = ctx.versions.find((ver) => ver.id === line.version);
if (!v || !line.column) return [];
return [{
type: "scatter",
mode: "lines",
name: `${v.label} · ${line.column}`,
x: v.rows.map((r, i) => i),
y: v.rows.map((r) => (typeof r[line.column] === "number" ? r[line.column] : null)),
}];
});
return { data };
}Defaults & persistence
- Values are stored in the plot's
metadata_json.paramswhen you hit Save in the settings dialog. - On load, saved values are overlaid on the declared
defaults — params you add to a template later automatically pick up their defaults on existing plots. - Values are plain JSON with no referential integrity: a saved
versionid may point at a deleted version.render()must tolerate that (skip the row / show an empty state) — never assume the id resolves.
render(ctx) rules
ctx.versions— one entry per uploaded CSV version:{ id: number, label: string, columns: string[], rows: Array<Record<string, number|string|null>> }.- Cell values can be
null(missing / non-finite data). Filter them or pass them through — Plotly rendersnullas a gap. Never coerce to 0. - Must be pure, synchronous and fast — it re-runs on every param change. Plain ES2020 only: no
import/require, no network calls, noasync, no DOM access. - Return
{ data, layout? }. Keep trace counts reasonable (<50). - Use
ctx.darkonly for extra theme-specific colors.
return {
data: [ /* Plotly traces */ ],
layout: {
// Plotly v3: titles MUST be objects — plain strings are silently ignored
title: { text: "My Chart" },
xaxis: { title: { text: "time" } },
yaxis: { title: { text: "value" } },
// Do NOT set paper_bgcolor / plot_bgcolor / font:
// the app injects the same theme-aware defaults used by the
// built-in Line/Diff charts, so fonts and colors always match.
},
}Every chart in the app — including the built-in Line and Diff charts — renders through the same pipeline, which injects one shared theme (font family/size and transparent backgrounds). Anything your layout sets explicitly wins over those defaults, but leaving fonts and backgrounds alone keeps custom charts visually identical to the built-ins.
Errors & debugging
- Compile errors (syntax, missing
render, malformedparams) and render errors (exceptions, wrong return shape) show an error card on the affected chart only. - The editor at /templates validates on every keystroke and previews against sample CSVs parsed entirely in the browser — nothing is uploaded.
- Prefer generating templates with AI: the /templates page has a copyable prompt that encodes this whole contract.