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.
The CLI, then the cubster-charts skill so an agent knows the format:
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.
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.
{
"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" }]
}
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.
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".
{
"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.
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.
client.charts.* mirrors the REST surface one-for-one.
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.
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.
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.
- 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.