Custom
Turning quantitative values into geometric shapes.
AI-assisted, human-approvednovem uses AI to review and keep our documentation up to date.
Overview
CSS
Novem custom plots run in a sandboxed iframe so that the plot author
can use any JavaScript library they like without affecting the
surrounding document. The iframe still inherits the parent document's
theme however, with every --novem-* value propagated in two ways: as
CSS variables on the iframe's root, and as a JavaScript object available
to the plot's render function.
The frame has no network connection and receives no viewer cookies or access
to the parent document. Libraries must be supplied through the plot's declared
dependencies or inlined in custom.js; runtime fetch, XHR and arbitrary
remote script loads are blocked. Treat every value in the render payload as
visible to the plot code itself: the sandbox protects the viewer and parent
page, not plot data from its author.
Through CSS
Variables are injected into the iframe's :root before your stylesheet
runs, so you can use them directly anywhere you'd write a colour or a
font.
body {
font-family: var(--novem-font-body);
font-size: var(--novem-font-size);
color: var(--novem-text);
background: var(--novem-bg);
}
.tooltip {
background: var(--novem-tooltip-bg);
color: var(--novem-tooltip-text);
border-radius: var(--novem-tooltip-radius);
}
Through JavaScript
Every variable is also exposed on render.theme as a camelCase value,
which is useful when you build SVG or canvas content from JavaScript.
The categorical palette is an array indexed from zero.
const t = render.theme;
const svg = d3.select(node).append("svg")
.attr("width", width)
.attr("height", height)
.style("font-family", t.fontBody)
.style("background", "transparent");
const color = d3.scaleOrdinal(t.colors); // 10 categorical entries
svg.append("text")
.attr("fill", t.text)
.attr("font-weight", t.fontWeightHeading)
.text("My chart");
const arc = d3.arc()
.innerRadius(parseFloat(t.pieInnerRadius) * r)
.outerRadius(r);
render.theme is dark-mode aware. The value of t.text already
reflects whichever mode is active, so you don't need to branch on
render.dark to pick a foreground colour. The boolean is still there
if you want it for finer distinctions.
Dependencies
custom.deps lists the libraries loaded into the iframe before your
custom.js runs. It takes Novem registry specifiers, one per line — the
value is newline-separated, never comma-separated and never a URL. // starts
a line comment and /* ... */ a block comment, so you can keep notes beside
the list.
/*
* Dependencies for this plot, one per line.
*/
d3@7
@observablehq/plot@0.6
Scoped packages keep their scope (@observablehq/plot@0.6); unscoped ones are
just the name (d3@7). A specifier spelled as a full major.minor.patch is
that exact release; the shorter ones track the newest release Novem has
bundled in that line.
| Specifier | Global |
|---|---|
d3@7 | d3 |
@observablehq/plot@0.6 | Plot |
@observablehq/plot@0.6.16 | Plot |
ramda@0.28.0 | R |
ramda@0.30 | R |
ramda@0.32 | R |
Each entry is exposed as a global under the name in the second column, which is
where the d3 in the examples above comes from. They load in the order you
list them, before custom.js. A specifier that is not in the table does not
load, and its global is undefined when your code runs.
Arbitrary URLs are not supported. A line beginning with https:// is not a
URL here — // is the comment marker, so the line is truncated at it. The
iframe's Content-Security-Policy allows scripts only from the Novem data host,
so a third-party URL could not load in any case. A library that is not in the
table has to be inlined into your custom.js.
Note: A custom chart script (custom.js) is capped at 5,242,880
characters (5 MB). Custom plots run in an isolated iframe, so everything
must be inlined — minify your script and trim heavy dependencies to stay under
it. See Size limits for what happens
when you exceed a cap and the limits on other resources.