Card Composition

Reference for root-level card fields validated in lib/validation/chart-spec.ts. See also Chart Specifications for chart types, datasets, and Chart.js options.

1. What is card composition?

Chart-Output renders a complete designed card — not just a chart. The card system layers a header, optional legend row, the Chart.js plot, a KPI strip, and a footer into a single PNG (or other raster format). Every element is configurable via JSON on the same root object as type, data, and options.

2. Card anatomy

text
┌─────────────────────────────────────────────┐ │ HEADER (eyebrow · title · subtitle · badge) │ ├─────────────────────────────────────────────┤ │ LEGEND (if position: top) │ ├─────────────────────────────────────────────┤ │ │ │ CHART AREA (Chart.js) │ │ (fills remaining height) │ │ │ ├─────────────────────────────────────────────┤ │ KPI STRIP value · value · value · value │ ├─────────────────────────────────────────────┤ │ FOOTER left text right text │ └─────────────────────────────────────────────┘

3. Full field reference

Types below match ChartSpecSchema in the repo. theme uses .passthrough() so extra keys may be honored by the theme pipeline when supported.

backgroundColor

Type: string — Outer card fill when card composition is active (e.g. "#0d1117").

borderRadius

Type: number (0–64) — Corner radius of the card shell in px.

border

Type: { color: string; width: number }width is 0–8 px. Example: { "color": "rgba(255,255,255,0.08)", "width": 1 }.

padding

Type: "email" | "slack" | "pdf" — Merges preset padding into options.layout.padding. For numeric or per-side padding, set options.layout.padding in Chart.js form instead.

scale / devicePixelRatio

Type: number (1–4) — scale is an alias for devicePixelRatio. Example: 2 for 2× retina output.

theme

Type: object — Built-in keys include palette (string array, max 12), backgroundColor, fontFamily, fontSize, gridColor, mode ("light" | "dark"), textPrimary, textSecondary. Additional keys may be passed through for renderer-specific tokens (e.g. mono numerals) where supported.

header

Type: object — eyebrow, title, subtitle (strings); badge (text, optional backgroundColor, color, borderRadius); align: "left" | "center".

legend

Type: object — display (boolean), position ("top" | "bottom"), style ("square" | "line" | "circle"), fontSize (8–20), gap (0–48).

kpiStrip

Type: array (max 12) of { label: string; value: string; color?: string }.

footer

Type: object — left, right (strings); borderTop (boolean).

dataLabels

Type: boolean — When true, merges default datalabels plugin options with display: true.

annotations

Type: array (max 20) of discriminated objects — line: type: "line", axis? ("x" | "y"), value (number), label?, color?. box: type: "box", xMin, xMax (string | number), label?, color? (use rgba in color for translucent fills).

brandKitId

Type: UUID string or preset enum — obsidian, linen, polar, studio, ember, harbor. See Brand Kits.

4. Complete working example (Obsidian portfolio)

Full JSON (same as Quick Start). Produces a 1200×840 PNG at retina (scale: 2 → bitmap is 2400×1680 px; display at 1200×840 logical in HTML).

json
{ "type": "line", "brandKitId": "obsidian", "width": 1200, "height": 840, "format": "png", "scale": 2, "padding": "email", "backgroundColor": "#0d1117", "borderRadius": 16, "border": { "color": "rgba(255,255,255,0.08)", "width": 1 }, "header": { "eyebrow": "MONTHLY RECURRING REVENUE", "title": "$29,840", "subtitle": "↑ 16% from last month", "badge": { "text": "On track", "backgroundColor": "rgba(110,231,183,0.12)", "color": "#6ee7b7", "borderRadius": 6 } }, "legend": { "display": true, "position": "top", "style": "line", "fontSize": 11, "gap": 24 }, "theme": { "textPrimary": "#f0f6fc", "textSecondary": "#6e7681" }, "data": { "labels": ["Jan","Feb","Mar","Apr","May","Jun"], "datasets": [ { "label": "MRR", "data": [14200,17800,20100,22900,25720,29840], "borderColor": "#6ee7b7", "backgroundColor": "rgba(110,231,183,0.07)", "fill": true, "tension": 0.45, "borderWidth": 2.5, "pointRadius": [0,0,0,0,0,5], "pointBackgroundColor": "#ffffff", "pointBorderColor": "#6ee7b7", "pointBorderWidth": 2.5 }, { "label": "Target", "data": [15000,18500,21000,24000,27000,30000], "borderColor": "rgba(255,255,255,0.2)", "fill": false, "tension": 0.45, "borderWidth": 1.5, "borderDash": [5,4], "pointRadius": 0 } ] }, "options": { "scales": { "x": { "grid": { "display": false }, "border": { "display": false }, "ticks": { "color": "#6e7681" } }, "y": { "grid": { "color": "rgba(255,255,255,0.05)" }, "border": { "display": false }, "ticks": { "color": "#6e7681" } } } }, "kpiStrip": [ { "label": "Customers", "value": "312", "color": "#f0f6fc" }, { "label": "ARPU", "value": "$95.70", "color": "#f0f6fc" }, { "label": "Churn", "value": "3.2%", "color": "#f59e0b" }, { "label": "LTV", "value": "$2,990", "color": "#f0f6fc" } ], "footer": { "left": "chart-output.com", "right": "Generated Apr 2025", "borderTop": true } }

curl

bash
curl -X POST https://chart-output.com/api/v1/render \ -H "Authorization: Bearer pk_test_YOUR_KEY" \ -H "Content-Type: application/json" \ --output portfolio.png \ -d @- <<'SPEC' { "type": "line", "brandKitId": "obsidian", "width": 1200, "height": 840, "format": "png", "scale": 2, "padding": "email", "backgroundColor": "#0d1117", "borderRadius": 16, "border": { "color": "rgba(255,255,255,0.08)", "width": 1 }, "header": { "eyebrow": "MONTHLY RECURRING REVENUE", "title": "$29,840", "subtitle": "↑ 16% from last month", "badge": { "text": "On track", "backgroundColor": "rgba(110,231,183,0.12)", "color": "#6ee7b7", "borderRadius": 6 } }, "legend": { "display": true, "position": "top", "style": "line", "fontSize": 11, "gap": 24 }, "theme": { "textPrimary": "#f0f6fc", "textSecondary": "#6e7681" }, "data": { "labels": ["Jan","Feb","Mar","Apr","May","Jun"], "datasets": [ { "label": "MRR", "data": [14200,17800,20100,22900,25720,29840], "borderColor": "#6ee7b7", "backgroundColor": "rgba(110,231,183,0.07)", "fill": true, "tension": 0.45, "borderWidth": 2.5, "pointRadius": [0,0,0,0,0,5], "pointBackgroundColor": "#ffffff", "pointBorderColor": "#6ee7b7", "pointBorderWidth": 2.5 }, { "label": "Target", "data": [15000,18500,21000,24000,27000,30000], "borderColor": "rgba(255,255,255,0.2)", "fill": false, "tension": 0.45, "borderWidth": 1.5, "borderDash": [5,4], "pointRadius": 0 } ] }, "options": { "scales": { "x": { "grid": { "display": false }, "border": { "display": false }, "ticks": { "color": "#6e7681" } }, "y": { "grid": { "color": "rgba(255,255,255,0.05)" }, "border": { "display": false }, "ticks": { "color": "#6e7681" } } } }, "kpiStrip": [ { "label": "Customers", "value": "312", "color": "#f0f6fc" }, { "label": "ARPU", "value": "$95.70", "color": "#f0f6fc" }, { "label": "Churn", "value": "3.2%", "color": "#f59e0b" }, { "label": "LTV", "value": "$2,990", "color": "#f0f6fc" } ], "footer": { "left": "chart-output.com", "right": "Generated Apr 2025", "borderTop": true } } SPEC

5. Minimal card

Smallest useful card: set backgroundColor and header.title next to a normal data block. Presence of header activates the card shell.

json
{ "type": "bar", "backgroundColor": "#0f172a", "header": { "title": "Q1 Revenue" }, "data": { "labels": ["Jan", "Feb", "Mar"], "datasets": [{ "label": "Sales", "data": [12000, 15000, 18000] }] } }

6. Chart.js compatibility

Everything under type, data, and options passes through to Chart.js unchanged. You can copy any example from the Chart.js documentation and it will render correctly. Card fields sit alongside — not inside — the Chart.js config.