Skip to content
◀ Blog

Drawing the charts on this site at build time

Every chart on steamfield.io is an OpenChart spec rendered to static SVG during the build, so the page ships no charting code and the figures show with scripts turned off.

Riley Hilliard3 min read

openchartastroengineering

The charts on the steamfield.io home page ship as plain SVG. The page’s JavaScript contains no charting library, and the figures show up with scripts turned off. OpenChart renders each one during the Astro build from the same JSON spec you would hand its React component.

Rendering a spec to SVG

OpenChart’s vanilla package has a static entry point, renderStaticSVG, that takes a spec and returns an SVG string. It runs the same layout engine as the browser renderers, including label collision handling, so a static chart and an interactive one with the same spec look the same.

import { renderStaticSVG } from "@opendata-ai/openchart-vanilla/static";

const svg = renderStaticSVG(
  {
    mark: "line",
    data: weeklyCommits(),
    encoding: {
      x: { field: "week", type: "temporal" },
      y: { field: "commits", type: "quantitative" },
      color: { field: "product", type: "nominal" },
    },
    endpointLabels: true,
  },
  { width: 620, height: 320, watermark: false },
);

In Astro the string goes straight into the page with set:html. The SVG carries a viewBox, so a little CSS (width: 100%; height: auto) makes it scale with its column.

Matching the page’s palette

A static SVG has its colors written into attributes, so CSS variables can’t restyle it after the fact. OpenChart also computes some colors itself (label contrast against a bar, for instance), so it needs real values when it renders. The site’s chart helper passes a theme whose colors mirror the CSS tokens:

export function renderChart(build, { id, ...options }) {
  return renderStaticSVG(build(palette), {
    ...options,
    idPrefix: id,
    theme,
    darkMode: "force",
    watermark: false,
  });
}

build is a function of the palette rather than a fixed spec, so a figure can pick colors in its own encoding. The OpenData figure uses that to draw FRED in hot pink and every other source in a quiet violet. idPrefix keeps each chart’s gradient and clip-path IDs unique when several charts share a page.

The neon glow isn’t in the SVG. A CSS drop-shadow filter on the chart’s wrapper adds it, so the rendered markup stays plain and the glow can change without re-rendering anything:

.oc-figure svg {
  filter: drop-shadow(0 0 3px rgb(255 46 136 / 0.45))
    drop-shadow(0 0 10px rgb(160 77 255 / 0.25));
}

Problems we ran into

Bar lists ignore the theme. OpenChart 9’s barlist mark uses a fixed color list, so a themed palette never reaches it. The OpenData figure is a horizontal bar chart instead, with the rows reversed because band scales draw the first row at the bottom.

Tick labels used the theme’s axis color, which we had set to the hairline color. They were barely legible until axis pointed at the muted text color and the hairline moved to hairline.

Weekly commit counts were too spiky to chart. The first version of the OpenChart figure showed weekly commits per product, and one busy week of Stacks work flattened every other line. The figure now shows cumulative commits, which keeps every series readable.

Where the data comes from

The figures read JSON files checked into the repository. A script walks the git history of each product repository and writes daily commit counts. Another reads OpenData’s catalog snapshot. Both run by hand, so the build never depends on a network call, and each figure’s caption says how current its data is. The StatGlass and rr figures are illustrative and schematic respectively, and their captions say so.