Plot

Custom

Turning quantitative values into geometric shapes.

AI assisted, human approved — novem 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.

SpecifierGlobal
d3@7d3
@observablehq/plot@0.6Plot
@observablehq/plot@0.6.16Plot
ramda@0.28.0R
ramda@0.30R
ramda@0.32R

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.