CUBSTER Join the waitlist
DOCS / CHARTS

Charts

A chart is a small JSON document — kind, labels, series, a title, maybe a footnote or a couple of callouts. Cubster's charts service owns everything past that: the scales, the palette, the type sizes, where the legend goes. There is no raw-SVG path and no chart library dependency to configure — the JSON format is the whole API.

A published chart is a versioned, workspace-scoped document — private by default, archived instead of deleted, and shareable by flipping its visibility from the dashboard, same as a diagram or an artifact. The same chart JSON also draws inline inside an artifact page, written directly in a chart block or embedded by slug with chart-embed — one schema, two homes.

Install

The CLI, then the cubster-charts skill so an agent knows the format:

$ npm i -g @cubster/cli
$ cubster login
// in Claude Code:
/plugin marketplace add seancdavis/cubster
/plugin install cubster@cubster

Also needs an API key whose services include charts (see Authentication). A key created before this shipped did not gain it automatically — create a new one with the box checked.

The normal path

Unlike a diagram, an agent never places a single mark by hand — Cubster's layout owns every coordinate, and the schema caps what that layout cannot fit. A chart that validates always renders cleanly, so create alone is the normal path. Looking first with cubster chart preview is there for when you want it, not because it's required.

chart.json
{
  "kind": "bar",
  "title": "Pull requests I opened",
  "labels": ["Sep '24 – Sep '25", "Sep '25 – Sep '26"],
  "series": [{ "name": "PRs opened", "data": [20, 193] }],
  "valueLabels": true,
  "annotations": [{ "at": 1, "text": "~10x" }]
}
$ cubster chart create --file ./chart.json
Created wN4jC8pXaTsQ v1
https://app.cubster.dev/c/wN4jC8pXaTsQ
svg: https://app.cubster.dev/c/wN4jC8pXaTsQ.svg
png: https://app.cubster.dev/c/wN4jC8pXaTsQ.png
private — change visibility in the dashboard
The chart format

The chart file is the bare chart object — { kind, labels, series, ... }, no { type, props } wrapper. kind is one of bar, line, area, pie, or heatmap. labels (1–100 strings, ≤ 60 characters each) and series (1–6 entries, each { name, data } with one number | null per label) are shared by bar/line/area/pie; a pie takes exactly one series and rejects null outright. A null elsewhere means "no reading," never zero — a bar draws no mark and leaves the gap visible, a line or area breaks its path around it.

Optional on every kind: title (≤ 120 chars), subtitle (≤ 160), footnote (≤ 200), and size: "landscape" (1200×675, the default) or "square" (1080×1080) — accepted-and-ignored inside an artifact, which always draws at its own canvas. bar/line/area also take stacked (bar/area only), yLabel, yFormat: "number" | "percent", valueLabels, and up to 4 annotations ({ at, series?, text }, text ≤ 60 chars). A heatmap takes rows (1–31 labels), columns (1–60 labels, "" = no tick), and values (rows × columns of number ≥ 0 | null) instead of labels/series, and has no annotations.

Full field-by-field reference, generated from the schema itself: the cubster-charts skill's "Schema reference" section.

REST reference

Every route below accepts a session or an API key with the charts service — except visibility, which is session only. There is no DELETE anywhere in the service — archive/unarchive are the only reversible hide/restore.

POST /api/v1/charts Publish a new chart (title?, chart, tags?) → 201 Chart
GET /api/v1/charts List charts (tag, q, includeArchived) → { charts: ChartSummary[] }
GET /api/v1/charts/:slug Fetch a chart — ?version=N for an older one → Chart
PUT /api/v1/charts/:slug Publish a new version → Chart
POST /api/v1/charts/:slug/archive Reversibly hide a chart → Chart
POST /api/v1/charts/:slug/unarchive Restore an archived chart → Chart
POST /api/v1/charts/preview Validate + render without publishing → { valid, errors, warnings, png, width, height }
POST /api/v1/charts/:slug/visibility Session only — change visibility. 403 for an API key.

title on create is optional — the server falls back to chart.title, and 400s naming title if neither exists. A create/update body carrying a visibility key at all — even set to the value it already has — is 400 "error": "visibility is not accepted here; change it in the dashboard".

Chart JSON
{
  "id": "uuid", "slug": "wN4jC8pXaTsQ", "title": "Pull requests I opened",
  "kind": "bar", "tags": ["repo"], "visibility": "private",
  "archived": false, "latestVersion": 1,
  "version": { "number": 1,
    "chart": { "kind": "bar", "labels": ["...", "..."],
      "series": [{ "name": "PRs opened", "data": [20, 193] }] },
    "width": 1200, "height": 675, "createdAt": "ISO" },
  "url": "https://app.cubster.dev/c/wN4jC8pXaTsQ",
  "svgUrl": "https://app.cubster.dev/c/wN4jC8pXaTsQ.svg",
  "pngUrl": "https://app.cubster.dev/c/wN4jC8pXaTsQ.png",
  "createdAt": "ISO", "updatedAt": "ISO"
}

ChartSummary (the list endpoint) is the same shape minus version. The three URLs are always present, even while private — they 404 until visibility changes.

Serving & visibility

GET /c/:slug (an HTML page), .svg, and .png all take an optional ?v=N for an older version, gated by the same visibility matrix a diagram uses.

Visibility Owner's session Anyone else
private (default) 200 — owner only 404
unlisted 200 200
public 200 200
archived (any visibility) 200 — owner only 404

An owner-only response (private, or archived) is never cached (Cache-Control: private, no-store). An unlisted or public one caches for 5 minutes (public, max-age=300, never immutable) so flipping a chart back to private takes effect within minutes, not forever.

Unlike a diagram's stored SVG — markup an agent wrote by hand — a chart's stored data is JSON, and .svg and the page re-validate and redraw it from that JSON on every request; a stored row today's schema rejects 404s (logged) rather than serving a partial render. The PNG is the exception: it's rendered once, at write time, and read back from storage on every request rather than regenerated.

SDK

client.charts.* mirrors the REST surface one-for-one.

const check = await cubster.charts.preview({ chart })
// { valid, errors, warnings, png, width, height } — writes nothing, valid or not
const chart = await cubster.charts.create({ chart })
// chart.visibility === 'private'; chart.url / .svgUrl / .pngUrl 404 until a human shares it
const charts = await cubster.charts.list({ tag: 'repo' })
await cubster.charts.update(chart.slug, { chart: revisedChart })
await cubster.charts.archive(chart.slug)

There's no visibility parameter anywhere on the client — not on create, not on update, not as its own method. A chart always starts private; sharing it is a dashboard action. The SDK never validates a chart itself — chart is typed loosely, and the server is the one and only validator.

CLI

cubster chart preview|create|update|get|ls|archive|unarchive, all over client.charts.*. There is no --kind flag on any subcommand — the JSON carries it.

$ cubster chart preview ./chart.json -o /tmp/chart.png
$ cubster chart create --file ./chart.json --tag repo
$ cubster chart update wN4jC8pXaTsQ --file ./chart-v2.json
$ cubster chart get wN4jC8pXaTsQ -o ./chart.json
$ cubster chart ls --tag repo
$ cubster chart archive wN4jC8pXaTsQ

No cubster chart delete and no --visibility flag — same two rules as the REST API. get/ls show a chart's visibility read-only (like diagram and artifact do) — there just isn't a flag that sets it.

What you can't do — and why
  • No raw SVG in, ever — the chart JSON is the whole API. Cubster's layout owns every coordinate, which is what makes looking before publishing optional: a chart that validates always renders cleanly, because the schema is the thing bounding what layout has to handle.
  • No delete — only reversible archive/unarchive. A chart hotlinked into a post must never 404 because an agent tidied up its list.
  • No visibility from the API, SDK, or CLI — publishing something to the open web is a decision for a human, not an agent. The one route that changes it requires a signed-in session and 403s an API key.
  • At most 6 series, and at most 4 annotations — six is the length of the validated categorical palette (a seventh would have to reuse a color, so fold the tail into an "Other" series), and four is as many callouts as the layout can reserve headroom for before they start colliding.