--- url: https://chartbuddy.io/embed/docs/samples.md --- # Samples Copy-paste HTML galleries with a **live ChartBuddy Insight** of the same config under each sample. Examples load the latest `@chartbuddy.io/embed` from unpkg. ## Chart types | Sample | Link | |---|---| | Clustered bar | [samples/clustered-bar](/samples/clustered-bar) | | Stacked bar | [samples/stacked-bar](/samples/stacked-bar) | | Line | [samples/line](/samples/line) | | Stacked area | [samples/stacked-area](/samples/stacked-area) | | Pie | [samples/pie](/samples/pie) | | Scatter / bubble | [samples/scatter](/samples/scatter) | | Waterfall | [samples/waterfall](/samples/waterfall) | | Marimekko | [samples/mekko](/samples/mekko) | | Bar Mekko | [samples/bar-mekko](/samples/bar-mekko) | | Combo | [samples/combo](/samples/combo) | ## Variants & patterns | Sample | Link | |---|---| | Horizontal bar | [samples/horizontal-bar](/samples/horizontal-bar) | | Currency ticks | [samples/currency-ticks](/samples/currency-ticks) | | Dual-axis line | [samples/dual-axis-line](/samples/dual-axis-line) | | Percent axis | [samples/percent-axis](/samples/percent-axis) | | Multi-chart dashboard | [samples/dashboard](/samples/dashboard) | | Editable Insight | [samples/editable](/samples/editable) | ::: tip Live previews load `@chartbuddy.io/embed` from unpkg in your browser — the same import path as the code blocks below each sample. ::: --- --- url: https://chartbuddy.io/embed/docs/samples/clustered-bar.md --- # Sample: Clustered bar ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/stacked-bar.md --- # Sample: Stacked bar ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/line.md --- # Sample: Line ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/stacked-area.md --- # Sample: Stacked Area ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/pie.md --- # Sample: Pie ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/scatter.md --- # Sample: Scatter ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/waterfall.md --- # Sample: Waterfall ## Live preview `End` carries no value. It is marked `isTotal`, so ChartBuddy sums the bridge and draws the closing bar at 115. See [Waterfall](/chart-types/waterfall) for why you should not type that number yourself. ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/mekko.md --- # Sample: Marimekko ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/bar-mekko.md --- # Sample: Bar Mekko ## Live preview ## Code ```html
``` See [Bar Mekko chart type](/chart-types/bar-mekko) for the width/height grid rules and how this differs from [Marimekko](/chart-types/mekko). --- --- url: https://chartbuddy.io/embed/docs/samples/combo.md --- # Sample: Combo ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/horizontal-bar.md --- # Sample: Horizontal bar ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/currency-ticks.md --- # Sample: Currency ticks Value axis with `$` prefix, preferred interval, solid gridlines, and an axis title. ## Live preview ## Code ```html
``` Docs: [Value axis](/axes/value-axis) · [Titles & chrome](/axes/titles-and-chrome) --- --- url: https://chartbuddy.io/embed/docs/samples/dual-axis-line.md --- # Sample: Dual-axis line Revenue on the primary (left) axis; margin % on the secondary (right). ## Live preview ## Code ```html
``` Docs: [Secondary / dual axis](/axes/secondary) --- --- url: https://chartbuddy.io/embed/docs/samples/percent-axis.md --- # Sample: Percent axis 100% stacked bar with pinned 0–100 ticks every 25 and `%` postfix. ## Live preview ## Code ```html
``` Docs: [Value axis](/axes/value-axis) · [By chart type](/axes/by-chart-type) --- --- url: https://chartbuddy.io/embed/docs/samples/dashboard.md --- # Sample: Multi-chart dashboard ## Live preview ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/samples/editable.md --- # Sample: Editable Insight ## Live preview Opens in edit mode with the formatting toolbar. Use **Done** on the ChartBuddy ball to return to view. ## Code ```html
``` --- --- url: https://chartbuddy.io/embed/docs/getting-started.md --- # Getting Started Developer documentation for embedding ChartBuddy in HTML and dashboards. Using ChartBuddy in Google Slides? See the [Help Center](https://chartbuddy.io/help) instead. ChartBuddy is a charting product for analysis deliverables: HTML pages, dashboards, and PowerPoint. The public embed surface is **`@chartbuddy.io/embed`** — you create charts with `new Insight()`. ## Paths * [Installation](/getting-started/installation) — npm, CDN, single-file vs multi-file * [Quick start](/getting-started/quick-start) — HTML file, mount, update, destroy * [Connect AI](/connect-ai/) — Cursor, Claude Code, ChatGPT, … + Embed handoff * [View vs edit](/getting-started/view-vs-edit) — default chrome and editor mode * [Single vs multi mounts](/getting-started/multi-mount) — multi-mount default; single-mount for constrained hosts * [Canvas & sizing](/concepts/canvas) — host must have a definite width/height before mount ## Also browse * [Concepts](/concepts/) — mental model and `chartData` * [Chart types](/chart-types/) — clustered bar, line, waterfall, … * [Insight reference](/api/) — `Insight` methods (under Docs → Reference) * [Visual QA](/quality-assurance/visual-qa) — observe, PNG export, fixture hygiene * [Validation](/quality-assurance/validation) — `validateChartData` before mount * [Samples](/samples/) — live charts + copy-paste HTML * [llms.txt](https://unpkg.com/@chartbuddy.io/embed/llms.txt) — curated agent index ([llmstxt.org](https://llmstxt.org/)) --- --- url: https://chartbuddy.io/embed/docs/getting-started/installation.md --- # Installation ## npm ```bash npm install @chartbuddy.io/embed ``` ```js import { Insight, version } from '@chartbuddy.io/embed'; ``` ## CDN (recommended for HTML pages and single-file embeds) Import the latest embed from unpkg or jsDelivr: ```js import { Insight } from 'https://unpkg.com/@chartbuddy.io/embed'; ``` Equivalents: * `https://unpkg.com/@chartbuddy.io/embed/chartbuddy-embed.single.mjs` — single-file build * `https://cdn.jsdelivr.net/npm/@chartbuddy.io/embed` ## Single-file vs multi-file | Build | When | |---|---| | **Single-file** (package root / `.single.mjs`) | Single HTML page, strict CSP, one script tag | | **Multi-file** (`chartbuddy-embed.mjs` + siblings) | Sites you control that can load engine assets | For single-script or strict-CSP hosts: use the **single-file** path. Do not pass `assetBase`. Do not import engine internals (`d3`, wasm, workers) yourself. ## Bundle size and many charts ChartBuddy ships a chart **editor**, not a draw-only chart library, so it is considerably heavier than Chart.js or Recharts. Plan for it. | Asset | Raw | Gzipped | |---|---|---| | `chartbuddy-embed.single.mjs` (engine, all-in-one) | 2.24 MB | ~560 KB | | `chartbuddy-embed.mjs` (multi-file loader only) | 40 KB | ~13 KB | | `webapp-entry.js` (engine, fetched by the loader) | 1.92 MB | ~449 KB | | `labelPlacementAccel.wasm` (label placement) | 1.43 MB | ~314 KB | | `react.mjs` / `vue.mjs` / `element.mjs` (framework bindings) | 6–8 KB each | ~2 KB each | What that means in practice: * **The engine loads once per page, not once per chart.** The loader is idempotent, so ten Insights on a dashboard share one engine download. Chart number ten costs you draw time and DOM, not another 560 KB. * **The wasm is a separate file, not part of the bundle.** It accelerates label placement and is fetched by a worker when that path runs, so it does not block first paint. * **The multi-file build defers the weight.** Your bundle grows by ~13 KB gzipped; the engine arrives as a separate cacheable request on first mount. Prefer it for sites you control. The single-file build trades that for one request and no `assetBase`, which is what artifact and strict-CSP hosts need. * **View-only and editable load the same engine.** `editable: false` is a lighter *mount*, not a lighter *download*. For a dashboard of many small tiles, the honest advice is to measure before you commit: mount your real chart count and watch draw time, because that — not the download — is what you will feel. If tiles are below the fold, defer mounting until they scroll into view. There is no headless or server-side render path today, so charts cannot be pre-rendered on a server. ## TypeScript Types ship as `chartbuddy-embed.d.ts` from the package (`types` field). ## Framework bindings | Framework | Import | Peer dependency | |---|---|---| | React 18/19 | `@chartbuddy.io/embed/react` | `react` (optional) | | Vue 3 | `@chartbuddy.io/embed/vue` | `vue` (optional) | | Angular, Svelte, Solid, Lit, plain HTML | `@chartbuddy.io/embed/element` | none | Each ships its own types. Angular uses the `` custom element rather than a compiled Angular library, so ChartBuddy upgrades are not tied to your Angular major — see the [Angular guide](/guides/angular). Guides: [React](/guides/react) · [Vue](/guides/vue) · [Angular & web components](/guides/angular). --- --- url: https://chartbuddy.io/embed/docs/getting-started/quick-start.md --- # Quick start Create a ChartBuddy Insight, then update, read, and destroy it. ## Host sizing (required) You own a **measured rectangle**; ChartBuddy draws into it. Before `new Insight()`, the mount element needs a definite width and height. Safe recipes: * Fixed size: `width: 800px; height: 450px` * Fluid width: `width: 100%; aspect-ratio: 16 / 9; min-height: 380px` (preferred for cards / decks) * Full-page fill: `html, body { height: 100% }` **and** every ancestor resolved, plus `#chart { height: 100% }` Do **not** rely on `height: 100%` alone inside flex layouts, Reveal/slide transforms, or other parents without an explicit height. More recipes and anti-patterns: [Canvas & sizing](/concepts/canvas). ## One HTML file ```html ChartBuddy
``` You get a chart in `#chart` in view mode (no spreadsheet). Hover the ChartBuddy ball (top-left) for **Download** (PNG), **Drag to slide**, and **Edit**. For npm / single-file installs, see [Installation](/getting-started/installation). ## Create (npm) ```js import { Insight } from '@chartbuddy.io/embed'; const insight = new Insight('#chart', { instanceId: 'main', // stable id for multi-mount chartData: { /* … */ }, }); await insight.ready; ``` `target` may be a CSS selector string or an `HTMLElement`. ## Wait for ready Always `await insight.ready` before measuring the SVG, calling `downloadPng()`, or reading `getChartData()`. You can also subscribe: ```js insight.on('ready', () => { /* … */ }); ``` ## Update data ```js // Data only — keeps chart type and formatting insight.setData([ ['', 'Q1', 'Q2'], ['Revenue', 100, 120], ]); // Any partial patch insight.update({ title: { visible: true, text: 'Updated' }, subtitle: { visible: false, text: '' }, }); // Or setChartData — chartType is optional on partials insight.setChartData({ seriesData: [/* … */], title: { visible: true, text: 'Updated' }, }); ``` Partial objects are merged over defaults (and over the current chart). Nested keys like `title`, `axes`, and `legend` are deep-merged. See [Defaults & merging](/concepts/defaults). Invalid `chartType` / field types throw; see [Insight API → Validation](/api/). ## Read config ```js const cd = insight.getChartData(); ``` Prefer this (or `exportConfig()`) when you need a full schema example. ## Destroy ```js insight.destroy(); ``` Removes the chart and chrome from the target. Call before replacing the DOM node. ## Inspect in the console After mount (when the host exposes it): ```js insight.chart.cd.chartType insight.chart.cd.orientation insight.mode // 'view' | 'edit' insight.isDirty() ``` ## Next * [Installation](/getting-started/installation) — npm and CDN * [Canvas & sizing](/concepts/canvas) — host box recipes and anti-patterns * [View vs edit](/getting-started/view-vs-edit) — chrome and editor mode * [Data model](/concepts/data-model) — `seriesData` layouts --- --- url: https://chartbuddy.io/embed/docs/getting-started/view-vs-edit.md --- # View vs edit ## Default: view mode ```js new Insight('#chart', { chartData }); ``` * Chart draws immediately * No formatting toolbar * Hover the ChartBuddy ball for **Download**, **Drag to slide**, and **Edit** ## Display-only (export chrome, no Edit) ```js new Insight('#chart', { allowEdit: false, chartData, }); ``` Same view ball and exports, but **Edit** is hidden and `enterEditMode()` is a no-op. Use for dashboards that should stay read-only. ## Open the editor immediately ```js new Insight('#chart', { editable: true, chartData, }); ``` Boots in edit with toggle chrome (Done / morph). Users can leave edit and return to view. ## Always-on editor (no mode chrome) ```js new Insight('#chart', { editSession: 'locked', chartData, }); ``` Stays in edit for the life of the mount: no Done control, no morph ball. `enterEditMode()` / `exitEditMode()` are no-ops. Use this when the host page owns the surrounding UI (marketing demos, dialogs). ## Switch at runtime ```js await insight.enterEditMode(); insight.mode; // 'edit' await insight.exitEditMode(); // or use Done on the chrome ball insight.mode; // 'view' ``` Only applies when `editSession` is `'toggle'` (the default) and `allowEdit` is not `false`. ## What edit mode is for Interactive formatting: axes, colors, annotations, orientation, and the rest of the ChartBuddy editor. The embed package has no spreadsheet UI — you edit styling, not the underlying data grid. --- --- url: https://chartbuddy.io/embed/docs/getting-started/multi-mount.md --- # Single vs multi mounts ChartBuddy Embed ships in two packaging modes: **multi-mount** and **single-mount**. Multi-mount is the normal package for pages that can load ChartBuddy’s engine assets and host one or more charts. Single-mount is an all-in-one script for hosts that cannot fetch any further JavaScript after the first file. **Multi-mount is the default.** Use it for dashboards and normal HTML/app pages. Reach for **single-mount** only when the host is constrained (for example an AI chat artifact window). ## Multi-mount (default) A normal page or dashboard: you control the host, so ChartBuddy can load its engine assets and you can put **several Insights** on one page. * Prefer the **multi-file** package (`@chartbuddy.io/embed` / `chartbuddy-embed.mjs`) on sites you control — see [Installation](/getting-started/installation#single-file-vs-multi-file). * Construct **one `Insight` per container**. * Pass a stable **`instanceId`** so each chart has a stable registry key. Duplicate ids on the same page **throw**. ```html
``` Update with `setData` / `update` / `setChartData` on the instance you own. Destroy before removing the element. Live registry: `window.__CHARTBUDDY_INSIGHTS__` / `getInsights()`. See the [dashboard sample](/samples/dashboard). ## Single-mount (constrained hosts) Use single-mount when the environment only allows **one self-contained script** and cannot fetch sibling engine files afterward. Typical cases: * AI chat / artifact sandboxes * Strict CSP or single-script embeds In those hosts: * Use the **single-file** build (`https://unpkg.com/@chartbuddy.io/embed` or `chartbuddy-embed.single.mjs`). Do not pass `assetBase`. * Usually mount **one** Insight for the deliverable. * Keep view mode unless you deliberately want in-artifact editing. See [Connect AI](/connect-ai/) for client-specific notes. ```html
``` If you are building a dashboard you control, stay on **multi-mount**. Reach for single-mount only when the host forces a one-script, no-extra-JS setup. --- --- url: https://chartbuddy.io/embed/docs/connect-ai.md --- # Connect AI Need **desktop Hub** editing over MCP (open charts in Chartbuddy Hub, `list_charts`, …)? See [Chartbuddy Hub – Connect MCP](https://chartbuddy.io/hub/docs/connect-mcp/). ## How Embed fits AI workflows 1. **Fetch** [`llms.txt`](https://unpkg.com/@chartbuddy.io/embed/llms.txt) (curated index). 2. **Fetch only the docs pages you need** (quick start, chartData schema, chart type, axes, validation) via `.md` links from the index — do **not** load `llms-full.txt` by default. 3. **Write** HTML/JS that imports `@chartbuddy.io/embed` and passes `chartData`. 4. **Size the host** – definite width/height (`aspect-ratio` or fixed px). See [Canvas & sizing](/concepts/canvas). 5. **Verify** with [Visual QA](/quality-assurance/visual-qa) – layout first, then annotations. ## Pick your client | Client | Guide | |--------|--------| | **Cursor** | [Connect Cursor](/connect-ai/cursor) | | **Claude** | [Connect Claude](/connect-ai/claude) | | **Claude Code** | [Connect Claude Code](/connect-ai/claude-code) | | **OpenCode** | [Connect OpenCode](/connect-ai/opencode) | | **ChatGPT / Codex** | [Connect ChatGPT / Codex](/connect-ai/chatgpt-codex) | ## Shared handoff Paste into a new agent chat when starting an Embed task: ```text Build ChartBuddy Embed charts with @chartbuddy.io/embed (new Insight()). 1) Fetch https://unpkg.com/@chartbuddy.io/embed/llms.txt (index) 2) Fetch linked docs .md pages you need (quick-start, chart-data, chart type, validation) — do not load llms-full.txt by default Host sizing: mount element needs definite width/height (prefer width + aspect-ratio, not height:100% in flex/slide layouts). Use validateChartData / Visual QA loop before declaring done. ``` ## Troubleshooting | Symptom | Check | |---------|--------| | Charts render tiny or letterboxed | [Host sizing](/concepts/canvas) – aspect-ratio or fixed px | | `format` changes do nothing | Set `numberFormat: { mode: 'custom', customFormat }` – see [Number formats](/configuration/number-formats) | | Agent invents wrong API | `new Insight()` only – not Chart.js / `createChart` | | Slide deck charts wrong size | Mount when slide is visible; remount or `update` after layout | | Need Hub window tools | Use [Hub Connect MCP](https://chartbuddy.io/hub/docs/connect-mcp/) instead of Embed docs alone | --- --- url: https://chartbuddy.io/embed/docs/connect-ai/cursor.md --- # Connect Cursor Use **Cursor** agents to build pages and artifacts with `@chartbuddy.io/embed`. ::: tip Paste into Cursor ```text ChartBuddy Embed guide: https://chartbuddy.io/embed/docs/connect-ai/cursor 1) https://unpkg.com/@chartbuddy.io/embed/llms.txt Docs: https://chartbuddy.io/embed/docs/getting-started/ Size chart hosts with aspect-ratio or fixed px before new Insight(). ``` ::: ## Three steps ### 1. Give the agent Embed context * Point the agent at [`llms.txt`](https://unpkg.com/@chartbuddy.io/embed/llms.txt), then fetch the linked docs `.md` pages needed for the task. * Do not load `llms-full.txt` by default. * Link [Quick start](/getting-started/quick-start) and [Visual QA](/quality-assurance/visual-qa). * For Hub desktop MCP (separate from Embed), see [Hub – Connect Cursor](https://chartbuddy.io/hub/docs/connect-mcp/cursor). ### 2. Build in the workspace Agents should: * Import from `https://unpkg.com/@chartbuddy.io/embed` (or npm in your project). * Use `new Insight('#chart', { chartData })` and `await insight.ready`. * Size the host with `aspect-ratio` or fixed px – not bare `height: 100%` in nested layouts. ### 3. Verify layout and config * Open the HTML locally or use Cursor browser tools. * Check the chart **fills the host** before judging annotations. * Use `insight.getChartData()` / `toPngBase64()` for automated checks – see [Visual QA](/quality-assurance/visual-qa). ## Optional: Hub MCP alongside Embed Cursor can also run **Chartbuddy Hub MCP** (`chartbuddy --mcp`) for desktop chart windows. That is independent of Embed npm – configure both only if you need both workflows. --- --- url: https://chartbuddy.io/embed/docs/connect-ai/claude.md --- # Connect Claude Use **Claude in the browser** (claude.ai) and the **Home** tab in the Claude desktop app for **HTML artifacts** (`create_file` + `present_files`). The **Cowork** and **Code** tabs can also use the MCP visualize tool `mcp__visualize__show_widget`. Claude Code uses a different path. See [Connect Claude Code](/connect-ai/claude-code). ## Claude Artifact Best for “show me a chart of X”: one-off, rendered in the conversation. Claude can deliver a chart in two ways: | Mechanism | What it is | Where it works | | --- | --- | --- | | `mcp__visualize__show_widget` | MCP visualize tool that mounts a ChartBuddy widget in the conversation | **Cowork** or **Code** tab only (for now). Not available in browser / Home chat. | | `create_file` + `present_files` | Claude writes an HTML file and presents it as an artifact | Browser (claude.ai) and the **Home** tab in the desktop app | The rest of this page covers the **`create_file` + `present_files`** HTML artifact path (unpkg import, host height, view mode, export). Edits the user makes inside the artifact (including via the ChartBuddy hover ball → Edit) are **not** passed back to the LLM. People often expect that loop; it does not exist here. Prefer creating the chart in **view mode** so the deliverable stays what Claude generated. ### Import ```html ``` No bundler, no `assetBase`. Claude’s artifact sandbox allows **unpkg** (also jsDelivr, cdnjs, esm.sh, Google Fonts). The standard package-root import works as-is. ### Options ```js const insight = new Insight('#chart', { persist: false, // artifacts are ephemeral; nothing to reload later chartData: { /* … */ }, }); await insight.ready; ``` Default is **view-only** (hover ball: Download PNG / Drag to slide / Edit). Leave it that way unless you deliberately want in-artifact editing that will not sync back to the chat. ### Host height: explicit, not min-height only Insight sizes from the mount element’s **measured box**. In Claude artifacts, a host with only `min-height` (and no resolved `height`) often renders shorter than expected. The chart looks squashed. Give the mount container an **explicit `height`** (or `aspect-ratio` **and** a definite height chain): ```css #chart { width: 100%; height: 420px; /* preferred in artifacts */ } ``` `aspect-ratio` alone can work on normal pages; in artifact sandboxes, **fixed `height` is the safest default**. See [Canvas & sizing](/concepts/canvas#ai-artifact-hosts). ### Export to a deck Claude cannot programmatically export a PNG from an artifact. The user can open the ChartBuddy hover ball on the chart and choose **Download** or **Drag to slide**. ## Paste into Claude ```text Create a Claude HTML artifact with ChartBuddy Embed. import { Insight } from 'https://unpkg.com/@chartbuddy.io/embed' Mount #chart with explicit height (e.g. height: 420px), not min-height only. persist: false. View mode (no editable: true). await insight.ready. Read https://unpkg.com/@chartbuddy.io/embed/llms.txt and follow linked docs .md pages ``` ## Chartbuddy Hub For **advanced visual QA** through Chartbuddy MCP, and for connecting charts to other apps, install **[Chartbuddy Hub](https://chartbuddy.io/hub/docs/getting-started/installation)** and follow [Connect Claude Code (Hub MCP)](https://chartbuddy.io/hub/docs/connect-mcp/claude-code). Browser / Home chat cannot reach local Hub MCP. --- --- url: https://chartbuddy.io/embed/docs/connect-ai/claude-code.md --- # Connect Claude Code Use **Claude Code** (CLI or desktop **Code** tab) to build Embed pages and deliverables. ::: info Also covers the Claude Desktop Code tab These steps apply to **Claude Code** in the terminal and the **Code** tab in the Claude desktop app. For inline charts in browser/Home chat, see [Connect Claude](/connect-ai/claude). ::: ## Standalone HTML (deliverable on disk) Same `new Insight()` API as artifacts – the output is a **real `.html` file** (or small project) written to the workspace, not rendered inline in chat. ### 1. Seed context Start a **new** session with Embed references: ```text Build with @chartbuddy.io/embed (new Insight()). 1) https://unpkg.com/@chartbuddy.io/embed/llms.txt 2) Fetch linked docs .md pages as needed (quick-start, chart-data, chart type) Docs: https://chartbuddy.io/embed/docs/getting-started/quick-start Host sizing: aspect-ratio or fixed height – see https://chartbuddy.io/embed/docs/concepts/canvas ``` ### 2. Implement * Single-file HTML with unpkg import, or npm in a repo. * Host sizing: `aspect-ratio` + `min-height`, or fixed `width`/`height` – normal page layout rules apply (unlike artifact sandboxes, see [Connect Claude](/connect-ai/claude#host-height-explicit-not-min-height-only)). * `await insight.ready` before PNG export or `getChartData()`. * Partial updates via `setData` / `update` / `setChartData`. * `persist: true` only if the page should reload chart state from storage. ### 3. Verify Follow [Visual QA](/quality-assurance/visual-qa): layout first, then chart config. ## Optional: Hub MCP For Chartbuddy Hub desktop tools, configure MCP separately – [Hub – Connect Claude Code](https://chartbuddy.io/hub/docs/connect-mcp/claude-code). Embed docs alone do not spawn Hub. --- --- url: https://chartbuddy.io/embed/docs/connect-ai/opencode.md --- # Connect OpenCode Use **[OpenCode](https://opencode.ai)** agents to build Embed pages and artifacts with `@chartbuddy.io/embed`. ## Three steps ### 1. Seed context In a new session, point the agent at: * [`llms.txt`](https://unpkg.com/@chartbuddy.io/embed/llms.txt) (index → follow linked `.md` pages) * [Quick start](/getting-started/quick-start) * [Canvas & sizing](/concepts/canvas) ```text ChartBuddy Embed – new Insight(), @chartbuddy.io/embed from unpkg or npm. 1) https://unpkg.com/@chartbuddy.io/embed/llms.txt 2) Fetch linked docs .md pages as needed Host must have definite size (aspect-ratio or fixed px). ``` ### 2. Build Same patterns as [Connect Cursor](/connect-ai/cursor): HTML artifact or project files, `chartData` + host CSS. ### 3. Verify [Visual QA](/quality-assurance/visual-qa) loop – confirm the chart fills the host. ## Optional: Hub MCP OpenCode can also run Chartbuddy Hub MCP – see [Hub – Connect OpenCode](https://chartbuddy.io/hub/docs/connect-mcp/opencode). Embed and Hub MCP are separate integrations. --- --- url: https://chartbuddy.io/embed/docs/connect-ai/chatgpt-codex.md --- # Connect ChatGPT / Codex ChartBuddy cannot be used with regular ChatGPT chat at this moment. Use **Codex CLI** or the **Codex** tab in the ChatGPT desktop app to build Embed charts in code. ## Embed workflow (Codex) ### 1. Paste context ```text Build ChartBuddy Embed charts (@chartbuddy.io/embed, new Insight()). 1) https://unpkg.com/@chartbuddy.io/embed/llms.txt 2) Fetch linked docs .md pages as needed https://chartbuddy.io/embed/docs/getting-started/quick-start Size hosts with aspect-ratio or fixed px – not height:100% in slide/flex layouts. ``` ### 2. Generate HTML or a small project * Import from unpkg in a single HTML file, or add `@chartbuddy.io/embed` via npm. * Follow [Quick start](/getting-started/quick-start) host sizing. * Run [Visual QA](/quality-assurance/visual-qa) before finishing. ### 3. Optional: Hub MCP in Codex Codex can also connect to **Chartbuddy Hub MCP** for desktop chart editing – [Hub – Connect ChatGPT / Codex](https://chartbuddy.io/hub/docs/connect-mcp/chatgpt-codex). Use Hub MCP when you need `list_charts` / live Hub windows; use Embed docs when you need npm/HTML deliverables. --- --- url: https://chartbuddy.io/embed/docs/concepts.md --- # Mental model ChartBuddy embed has three layers: 1. **`Insight`**: the public instance you create (`new Insight(target, options)`) 2. **Chart**: the engine drawing SVG into the target (`insight.chart`) 3. **`chartData` (`cd`)**: the serializable config + data object ``` Insight → mounts & chrome (view/edit, PNG, export) └─ chart → renders SVG └─ cd / chartData → type, seriesData, axes, annotations, … ``` ## Modes * **View**: deliverable chart + hover ball * **Edit**: full formatting UI inside the mount --- --- url: https://chartbuddy.io/embed/docs/concepts/data-model.md --- # Data model Every Insight is driven by a **`chartData`** document: a chart type, a 2D `seriesData` grid, and optional furniture / axes / annotations. Deep field reference, validation codes, waterfall rules, and merge behaviour: **[chartData schema](/api/chart-data)**. ## Required | Field | Type | Notes | |---|---|---| | `chartType` | `string` | Chart type id – see [chart types](/chart-types/) | | `seriesData` | 2D array | Layout depends on type | ## Recommended shell ```js { chartType: 'clusteredBar', isDataTransposed: true, seriesData: [/* … */], title: { visible: true, text: '…' }, subtitle: { visible: false, text: '' }, legend: { visible: true }, orientation: 'vertical', // or 'horizontal' } ``` ## chartType values `clusteredBar` · `stackedBar` · `stackedBar100` · `line` · `stackedArea` · `stackedArea100` · `pie` · `scatter` · `waterfall` · `mekko` · `barMekko` · `combo` You can also pass `donut` (pie with a hole) or `bubble` (scatter with a larger marker). `getChartData()` returns `pie` / `scatter` with those defaults applied. Details: [chartData schema – donut & bubble](/api/chart-data#donut--bubble). ## seriesData layouts (quick) **Bar / line / area / stacked / waterfall / mekko / combo** – rows = series (`isDataTransposed: true`): ```js [ ['', 'Q1', 'Q2', 'Q3'], ['Revenue', 100, 112, 125], ['Costs', 60, 66, 70], ] ``` **Pie** – category / value: ```js [ ['Category', 'Value'], ['North', 45], ['South', 30], ] ``` **Bar Mekko** – row 1 = widths (not drawn); later rows = stacked heights: ```js [ ['', 'Enterprise', 'Mid-market', 'SMB'], ['Accounts', 120, 85, 200], ['Core revenue', 48, 16, 8], ['Add-ons', 14, 12, 10], ] ``` **Scatter** – no transpose; columns are metrics: ```js [ ['', 'Metric X', 'Metric Y', 'Size', 'Group'], ['Point 1', 10, 15, 8, 'A'], ] ``` Full layout rules, minima, and ragged-grid behaviour: [chartData schema – seriesData](/api/chart-data#seriesdata-layouts). ## Next * [chartData schema](/api/chart-data) – top-level map, option bags, patches, validation * [Defaults & merging](/concepts/defaults) * [Axes](/axes/) · [Configuration](/configuration/) · [Chart types](/chart-types/) --- --- url: https://chartbuddy.io/embed/docs/concepts/defaults.md --- # Defaults & merging Partial `chartData` is merged over ChartBuddy defaults when you create or update an Insight. ## Deep-merge keys These nested objects deep-merge: * `title` * `subtitle` * `legend` * `axes` * `canvas` * `footnote` * `annotations` Other top-level keys typically replace. ## Practical tip Start minimal (`chartType` + `seriesData` + title/subtitle/legend). Add axes or annotations only when you need them, or export a full config from the editor and trim. --- --- url: https://chartbuddy.io/embed/docs/concepts/orientation.md --- # Orientation ```js { orientation: 'vertical', // default // orientation: 'horizontal', } ``` Orientation controls which physical sides carry the **domain** (categories) vs **range** (values) axes. ChartBuddy remaps axis roles automatically when orientation changes, so you keep the same `seriesData` and only flip `orientation`. | Orientation | Domain (categories) | Primary range (values) | |---|---|---| | `vertical` | bottom | left | | `horizontal` | left | bottom | ## Horizontal clustered bar ```js { chartType: 'clusteredBar', orientation: 'horizontal', isDataTransposed: true, seriesData: [/* … */], title: { visible: true, text: 'Sources' }, subtitle: { visible: false, text: '' }, legend: { visible: false }, } ``` Give horizontal bars enough **height**: category labels sit on the left and need vertical room. Format value ticks on **`axes.bottom`** (primary range), not `left`. More detail: [Axes · Orientation & roles](/axes/orientation) · [Value axis](/axes/value-axis) · [Secondary (top)](/axes/secondary#horizontal-dual-axis-secondary-on-top). --- --- url: https://chartbuddy.io/embed/docs/concepts/colors-and-fonts.md --- # Colors & fonts ## Series colors Palette lives on the legend: ```js legend: { visible: true, colors: ['#4285F4', '#DB4437', '#F4B400', '#0F9D58'], } ``` ## Text Title, subtitle, footnote, axis labels, and multilines carry their own font fields in a full `chartData` export (`fontFamily`, `fontSize`, `fontColor`, …). Face/color for title furniture are often inside the HTML `text`. Start from `getChartData()` when matching a brand. See [Title, subtitle & footnote](/configuration/title-subtitle-footnote) and [Text boxes](/configuration/text-boxes). --- --- url: https://chartbuddy.io/embed/docs/concepts/canvas.md --- # Canvas & sizing ChartBuddy fills its **target element**. The contract: **you own a measured rectangle; we draw into it.** Size the host so `getBoundingClientRect()` has a real width and height before `new Insight()`. ## Recipes **Fixed (slides / PNG exports):** ```html
``` Typical slide-friendly sizes: ~800×450–600. **Fluid width + stable aspect (cards, decks, dashboards):** ```css .chart-host { width: 100%; aspect-ratio: 16 / 9; min-height: 380px; } ``` **Full-page fill** (only when the height chain is complete): ```css html, body { margin: 0; height: 100%; } #chart { width: 100%; height: 100%; } ``` Every ancestor between `html` and `#chart` must also resolve height. Prefer aspect-ratio or fixed px when that chain is unclear. ## AI artifact hosts (Claude, ChatGPT, …) Sandboxed **inline artifacts** measure the mount box at render time. A host with **only `min-height`** and no resolved `height` often produces a squashed chart — Insight has nothing definite to size against. **Prefer explicit height in artifacts:** ```css #chart { width: 100%; height: 420px; } ``` On normal pages, `aspect-ratio` + `min-height` is usually fine. In artifact sandboxes, fixed `height` is the safest default. See [Connect Claude](/connect-ai/claude). ## Anti-patterns * `height: 100%` (or flex “fill the rest”) when parents lack an explicit height — common in Reveal, Zoom, and CSS-transformed slide shells * Mounting while the host is `display: none` or on a hidden slide with no layout box — wait until the host is visible, then mount (or remount / `update` after layout) * Measuring inside transformed parents without remasuring after the slide enters view ## Slide decks (Reveal and similar) 1. Give the chart a fixed slot: aspect-ratio or fixed height — not `%` height inside the slide transform. 2. Mount when the slide is shown (`slidechanged` / equivalent), after the host has layout. 3. If the slot size changes, call a partial `insight.update({})` or remount so the chart remeasures. ## Background Host CSS can wash out transparent charts. Set an explicit chart or page background when the surrounding page is light or unknown. ## Export bounds `downloadPng()` / `toPngBase64()` export the drawable chart for the current host size. Match container size to the slide slot you care about before exporting. --- --- url: https://chartbuddy.io/embed/docs/configuration.md --- # Configuration Most ChartBuddy styling lives on **`chartData`**, not a separate `options` tree. Field map and layouts: [chartData schema](/api/chart-data). | Concern | Where | |---|---| | Title / subtitle / footnote | `title`, `subtitle`, `footnote` | | Text boxes | `multilines` (free callouts) | | Legend & palette | `legend` (docked / free placement, `colors`, …) | | Axes, ticks, gridlines | `axes.*.labels`, `axes.*.gridlines` – see [Axes](/axes/) | | Number formats | `axes.*.labels.numberFormat`, `annotations.dataLabels.numberFormat`, `annotations.totals.numberFormat` (see [Number formats](/configuration/number-formats)) | | Type-specific look | `bar`, `line`, `area`, `pie`, … | | Arrows / level lines | `annotations.differenceArrows`, `cagrs`, `levelArrows`, `levelLines` | ## Pages * [Title, subtitle & footnote](/configuration/title-subtitle-footnote) * [Text boxes](/configuration/text-boxes) * [Legend](/configuration/legend) * [Gridlines](/configuration/gridlines) * [Number formats](/configuration/number-formats) * [Arrows](/configuration/arrows) * [Level lines](/configuration/level-lines) * [Point markers](/configuration/point-markers) Axes deep-dive (ticks, bounds, dual-axis, breaks): **[Axes](/axes/)**. --- --- url: https://chartbuddy.io/embed/docs/configuration/title-subtitle-footnote.md --- # Title, subtitle & footnote These are **special docked text boxes**: fixed keys (`title`, `subtitle`, `footnote`) that reserve plot space. Free callouts live in `multilines` — see [Text boxes](/configuration/text-boxes). They are not free-positioned like the [legend](/configuration/legend) either (legend has its own dock / free model). ```js title: { visible: true, text: 'Revenue vs Costs', fontSize: 11, }, subtitle: { visible: true, text: 'USD millions', fontSize: 11, }, footnote: { visible: true, text: 'Source: Company filings, FY2025.', fontSize: 6, }, ``` All three default to **`visible: false`** on every chart type. Defaults may still carry placeholder HTML in `text` while hidden — set `text: ''` when you want a clean hidden field. *** ## Where each sits | Element | Path | Band | Default `fontSize` | Typical use | |---|---|---|---|---| | Title | `title` | Top of the chart | `11` | Chart name / main claim | | Subtitle | `subtitle` | Top, under the title | `11` | Units, period, clarifying line | | Footnote | `footnote` | Bottom of the chart | `6` | Source, caveat, legal line | **Top stack (top → plot):** title → subtitle → (optional top legend) → axes / plot…\ **Bottom stack (plot → bottom):** … axes → (optional bottom legend) → **footnote** → canvas edge. If both title and subtitle are off, a small top padding is still reserved so the plot does not sit flush against the top. There is **no** position enum and **no** free `x` / `y` for authors. Layout percentages are recomputed every draw from the offset stack — omit them in hand-authored config. *** ## Shared fields Same multiline text shape as [text boxes](/configuration/text-boxes), with docked defaults: | Field | Type | Notes | |---|---|---| | `visible` | boolean | Hidden unless `true` **and** `text` is non-empty | | `text` | string | Plain text **or** inline HTML (see below) | | `fontSize` | number | Points (footnote defaults smaller) | | `id` | string | Stable: `multiline-title` / `multiline-subtitle` / `multiline-footnote` (usually leave default) | Optional parallel fields sometimes appear in exports (`fontFamily`, `fontColor`, `backgroundColor`). Face and color are usually embedded in the HTML `text` instead. Leave `movable`, `deletable`, `width`, `height`, and percentage coords alone — runtime owns them. *** ## Text content `text` accepts **inline HTML**, sanitized for safe tags (`font`, `b` / `i` / `u`, `br`, …). Plain string (merge-friendly): ```js title: { visible: true, text: 'Revenue vs Costs' } ``` Editor-style HTML (face + color): ```js title: { visible: true, fontSize: 11, text: 'Revenue vs Costs', }, subtitle: { visible: true, fontSize: 11, text: 'USD millions', }, footnote: { visible: true, fontSize: 6, text: 'Source: Company filings. Unaudited.', }, ``` Line breaks: use `
` inside `text`. Bold / italic: ``, ``, etc. House-style / brand tools may rewrite the `` / color inside `text` when applying a font family. *** ## Title Main headline above the plot. ```js title: { visible: true, text: 'Revenue vs Costs', fontSize: 14, } ``` Soft-delete / hide in the editor sets `title.visible = false` (does not remove the object). Showing an empty title from the UI may seed placeholder copy — prefer an explicit string in embed configs. *** ## Subtitle Secondary line under the title (units, time range, “Indexed to 100”, …). ```js subtitle: { visible: true, text: 'USD millions · FY2024–FY2025', fontSize: 11, } ``` Starter charts often leave a placeholder subtitle in defaults. **Hide it unless intentional:** ```js subtitle: { visible: false, text: '' } ``` *** ## Footnote Source / caveat line in the **bottom** band. Same shape as title/subtitle; smaller default type. There is no separate “auto sources” pipeline — content is whatever you put in `text`. ```js footnote: { visible: true, text: 'Source: Company filings, FY2025. Unaudited.', fontSize: 6, } ``` When a bottom legend is also on, the footnote sits below that legend in the bottom stack. *** ## Visibility cheat sheet ```js // Show title only title: { visible: true, text: 'Mix by quarter' }, subtitle: { visible: false, text: '' }, footnote: { visible: false, text: '' }, // Title + source line title: { visible: true, text: 'Mix by quarter' }, subtitle: { visible: false, text: '' }, footnote: { visible: true, text: 'Source: Internal sales.' }, // Hide all furniture cleanly after a deep-merge title: { visible: false, text: '' }, subtitle: { visible: false, text: '' }, footnote: { visible: false, text: '' }, ``` `title`, `subtitle`, and `footnote` are deep-merged on partial `setChartData` / `update` — see [Defaults & merging](/concepts/defaults). *** ## Tips * Prefer plain `text` for embed configs; use HTML when you need face/color/bold. * Do not hand-author `leftXPercentage` / `topYPercentage` — they are overwritten on draw. * These blocks reserve plot space (like a docked legend), so long wrapping titles shrink the plot vertically. * For free callouts on the plot, use [text boxes](/configuration/text-boxes) (`multilines`). --- --- url: https://chartbuddy.io/embed/docs/configuration/text-boxes.md --- # Text boxes Free callouts on the chart. In the editor they are **Add text box**; in `chartData` they live at top-level **`multilines`** (an array). [Title, subtitle & footnote](/configuration/title-subtitle-footnote) are the same multiline text shape, but as **fixed docked furniture** — not members of `multilines`. ```js multilines: [ { id: 'multiline-callout-1', text: 'Note: figures are unaudited.', visible: true, leftXPercentage: 0.62, topYPercentage: 0.12, positionRelativeTo: 'drawableChartArea', width: 160, fontSize: 11, backgroundColor: 'transparent', movable: true, deletable: true, }, ] ``` Defaults seed `multilines: []`. There is no `textBoxes` or `annotations.textBoxes` key on `chartData`. *** ## Free boxes vs docked furniture | | Text boxes (`multilines[]`) | Title / subtitle / footnote | |---|---|---| | Storage | Array of N boxes | Three fixed keys: `title`, `subtitle`, `footnote` | | Placement | Free — you set `%` coords | Docked — engine places them each draw | | Plot space | Overlay only (does not reserve margin) | Reserves top / bottom margin | | Movable / deletable | Usually `true` | `false` | | `positionRelativeTo` | Usually `'drawableChartArea'` | `'fullChart'` | | Docs | This page | [Title, subtitle & footnote](/configuration/title-subtitle-footnote) | Axis titles under `axes.*.axisTitle` also use multiline text, but they are axis chrome — see [Titles & chrome](/axes/titles-and-chrome). Not free text boxes. *** ## Placement | Field | Role | |---|---| | `leftXPercentage` / `topYPercentage` | Position as fractions of the reference area (roughly `0`–`1`) | | `positionRelativeTo` | `'drawableChartArea'` — plot area (default for free boxes). `'fullChart'` — full canvas | | `width` | Wrap width in px (editor ~130, a typical hand-authored width is `160`) | | `movable` | `true` so the box can be dragged in edit mode | Free boxes can overlap series and labels because they do not push the plot. Precise placement is easiest by dragging in edit mode, then exporting config. *** ## Fields | Field | Type | Notes | |---|---|---| | `id` | string | Unique; editor uses `multiline-…` | | `text` | string | Plain text or inline HTML (``, ``, `
`, …) — same rules as title furniture | | `visible` | boolean | Off unless `true` and `text` is non-empty | | `fontSize` | number | Points (default `11`) | | `backgroundColor` | string | e.g. `'transparent'` or a callout fill | | `leftXPercentage` / `topYPercentage` | number | See placement | | `positionRelativeTo` | string | `'drawableChartArea'` | `'fullChart'` | | `width` | number | Wrap width (px) | | `movable` / `deletable` | boolean | Defaults `true` for free boxes | | `selectors.resizable` | boolean | Editor may set `true` for resize handles | *** ## Examples Plain callout: ```js multilines: [ { id: 'note-1', text: 'Excludes one-offs.', visible: true, leftXPercentage: 0.7, topYPercentage: 0.15, positionRelativeTo: 'drawableChartArea', width: 140, fontSize: 10, backgroundColor: 'transparent', movable: true, deletable: true, }, ] ``` Styled HTML + fill: ```js multilines: [ { id: 'highlight-1', text: 'Peak quarter', visible: true, leftXPercentage: 0.55, topYPercentage: 0.2, positionRelativeTo: 'drawableChartArea', width: 160, fontSize: 11, backgroundColor: '#E8E8E8', movable: true, deletable: true, }, ] ``` Replace or append the whole `multilines` array on `setChartData` / `update` when managing multiple boxes (arrays typically replace rather than deep-merge item-by-item). *** ## Tips * Product name: text box. Data path: `multilines`. * For chart title / units / source line, use [title, subtitle & footnote](/configuration/title-subtitle-footnote) so layout reserves space correctly. * Prefer edit-mode placement + `getChartData()` / `exportConfig()` when coordinates matter. --- --- url: https://chartbuddy.io/embed/docs/configuration/legend.md --- # Legend The legend lives at `legend` on `chartData`. It controls **series colors** (`legend.colors`), visibility, typography, item order, and **placement** (docked slots or free-floating). ```js legend: { visible: true, position: 'rightTop', // docked slot, or 'free' orientation: 'vertical', // item flow: 'vertical' | 'horizontal' reverseOrder: false, colors: ['#4285F4', '#DB4437', '#F4B400'], itemVisibility: {}, text: { fontSize: 11, fontColor: 'black', fontFamily: 'Arial', }, // When position is 'free' only: // x: 0.55, // y: 0.08, } ``` `legend.orientation` is **item layout** (stack vs row). It is not `orientation` on the chart (axes / bars). Flipping the chart does not flip the legend. *** ## Placement model Two modes, both via `legend.position`: | Mode | `position` | Plot space | Coordinates | |---|---|---|---| | **Docked** | One of 12 slots below | Reserves margin (width on left/right, height on top/bottom) | Engine places the legend; `x` / `y` are rewritten on draw — do not treat them as source of truth | | **Free-floating** | `'free'` | Does **not** reserve margin (overlays the plot) | `x` / `y` are fractions of the drawable chart area (roughly `0`–`1`) | There is no separate `dock` / `placement` field. ### Docked positions (12) Side first, then alignment: | Side | Values | |---|---| | Top | `topLeft`, `topCenter`, `topRight` | | Bottom | `bottomLeft`, `bottomCenter`, `bottomRight` | | Left | `leftTop`, `leftCenter`, `leftBottom` | | Right | `rightTop`, `rightCenter`, `rightBottom` | Default is `rightTop`. ```js // Bottom strip legend: { visible: true, position: 'bottomCenter', orientation: 'horizontal', } // Left rail legend: { visible: true, position: 'leftTop', orientation: 'vertical', } ``` ### Free-floating ```js legend: { visible: true, position: 'free', orientation: 'horizontal', // author-controlled; not tied to a dock x: 0.55, y: 0.08, } ``` Precise `x` / `y` are easiest to set by dragging in edit mode, then exporting config. Free legends can overlap series or labels because they do not push the plot. *** ## Orientation (item flow) | Value | Layout | |---|---| | `vertical` | Items stack top → bottom (default; fits left/right docks) | | `horizontal` | Items flow sideways, may wrap (fits top/bottom docks) | When you **drag** the legend onto a dock in the editor, orientation is updated automatically: | Dock family | Forced orientation | |---|---| | `top*` / `bottom*` | `horizontal` | | `left*` / `right*` | `vertical` | | `free` | unchanged | Hand-authored / API patches that only change `position` do **not** auto-update orientation. Always set a matching `orientation` when docking by config — e.g. `bottomCenter` + leftover `vertical` yields a tall legend on the bottom strip. *** ## Visibility and items | Field | Role | |---|---| | `visible` | Whole legend on/off | | `itemVisibility` | Per-series map: `{ "0": false }` hides series `0` from the legend. Missing key or `true` = shown | | `reverseOrder` | `true` reverses series order in the legend | Defaults: legend starts **hidden** on waterfall, bar mekko, and pie/donut; **shown** on other types. Single-series charts often look cleaner with `visible: false`. Hiding the last visible legend item also turns the whole legend off. Showing the legend again when every item was hidden clears `itemVisibility`. *** ## Colors and text | Field | Role | |---|---| | `colors` | Series / category fill palette (shared with the chart) | | `text.fontSize` / `fontFamily` / `fontColor` | Legend label typography | See also [Colors & fonts](/concepts/colors-and-fonts). *** ## Field reference | Field | Type | Default | Notes | |---|---|---|---| | `visible` | boolean | type-dependent | See above | | `position` | string | `'rightTop'` | 12 docks or `'free'` | | `orientation` | `'vertical'` | `'horizontal'` | `'vertical'` | Item flow; match dock family when hand-authoring | | `x` / `y` | number | `0.13` / `0.05` | Used when `position === 'free'` | | `reverseOrder` | boolean | `false` | | | `colors` | string\[] | org palette | Series fills | | `itemVisibility` | object | `{}` | Keys are series index strings | | `text.fontSize` | number | `11` | pt | | `text.fontColor` | string | `'black'` | | | `text.fontFamily` | string | org default | | *** ## Tips * Docked legends reserve plot space; free legends overlay it. * On horizontal charts, left/right docks compete with category labels for width — bottom or free often works better, or hide the legend. * Prefer edit-mode drag + `exportConfig()` / `getChartData()` when you need exact free placement or a precise dock alignment. --- --- url: https://chartbuddy.io/embed/docs/configuration/gridlines.md --- # Gridlines Gridlines live under each axis side (`left` / `right` / `top` / `bottom`). On a typical vertical chart, value gridlines sit on the **primary range** axis (`left`). ## Defaults for an axis ```js axes: { left: { gridlines: { visible: false, // new charts default off — set true to show color: '#aaaaaa', width: 0.5, dashArray: '5,5', // dashed; use '' for solid opacity: 0.8, overrides: {}, }, }, } ``` New charts default to **`visible: false`**. When enabled, the default stroke is a thin dashed line (`0.5px`, `5,5`). Those top-level fields apply to **every** gridline on that axis unless an override says otherwise. For a solid stroke in authored `chartData`, prefer `dashArray: ''`. `'none'` also works; both clear the dash pattern. ## Per-tick overrides `overrides` is a sparse map. Keys are the **tick value as a string** (for example `"0"`, `"50"`, `"100"`). Values are partial style objects. Only list the ticks you want to change; every other tick keeps the axis defaults above. | Override field | Effect | |---|---| | `visible` | Show or hide this tick’s gridline | | `color` | Stroke color | | `width` | Stroke width (px) | | `dashArray` | Dash pattern (`'5,5'`, or `''` for solid) | | `opacity` | Stroke opacity | ### Make ticks deterministic when authoring overrides Override keys must match ticks the engine actually draws. Without a fixed interval, auto tick selection can pick a different step and your keys silently no-op. **Pair overrides with `labels.preferredTickInterval`** on the same axis so keys like `"50"` / `"75"` / `"100"` are predictable: ```js axes: { left: { labels: { preferredTickInterval: 25, }, gridlines: { visible: true, color: '#aaaaaa', width: 0.5, dashArray: '5,5', overrides: { '0': { visible: false }, '50': { color: '#0f766e', width: 1, dashArray: '' }, '100': { color: '#0f766e', width: 1.5, dashArray: '' }, }, }, }, } ``` With interval `25`, drawn ticks are multiples of 25 within the axis range (`"0"`, `"25"`, `"50"`, …). More on intervals: [Ticks & intervals](/axes/ticks). ### Smaller recipes Hide only the line at `0`: ```js overrides: { '0': { visible: false }, } ``` Hide `0` and recolor `100`: ```js overrides: { '0': { visible: false }, '100': { color: '#c45c26' }, } ``` ## Discovering keys from a live chart If you are not fixing the interval yourself: 1. Style one gridline in the editor, then call `getChartData()` / `exportConfig()` and copy `axes.*.gridlines.overrides` 2. Or read the resolved ticks after boot and stringify those values as keys ## Live patches (`setChartData`) When updating an already-mounted Insight, `gridlines` merges one level, but the **`overrides` map replaces as a whole**. Spread the previous map if you only want to add a key: ```js const cd = insight.getChartData(); const prev = cd.axes.left.gridlines.overrides || {}; insight.setChartData({ axes: { left: { gridlines: { overrides: { ...prev, '75': { color: '#c45c26' }, }, }, }, }, }); ``` For a full upfront `chartData` object, skip this: just author the sparse `overrides` map (and `preferredTickInterval`) you need. --- --- url: https://chartbuddy.io/embed/docs/configuration/number-formats.md --- # Number formats Every numeric surface uses the same **spec** shape. There is no separate sibling `format` bag. ```js { mode: 'custom', // or 'linked' source: undefined, // only when mode is 'linked' customFormat: { prefix: '', postfix: '', thousandSeparator: ',', decimalSeparator: '.', decimalPlaces: 0, forceDecimals: true, signDisplay: 'auto', // 'auto' | 'always' | 'parens' scaleDown: 0, }, } ``` | Field | Role | |---|---| | `mode` | `'custom'` — this surface owns `customFormat`. `'linked'` — inherit another surface | | `source` | When linked: `'primaryRangeAxis'` | `'dataLabels'` | `'totalLabels'` | | `customFormat` | The formatting knobs (also kept as an unlink stash when linked) | | `prefix` / `postfix` | Strings before / after the number (`postfix: '%'` on percentage charts) | | `thousandSeparator` / `decimalSeparator` | Grouping and decimal marks | | `decimalPlaces` | Digits after the decimal | | `forceDecimals` | Pad to `decimalPlaces` even with trailing zeros | | `signDisplay` | `'auto'` | `'always'` | `'parens'` | | `scaleDown` | Scale exponent (`0` = raw; higher values shorten large magnitudes) | **Not a number format:** category date patterns live at `axes..labels.dateFormat.format` (a string like `'MMM D, YYYY'`). *** ## 1. Axes Value-axis tick labels. Path is always under the side that holds that axis for the current orientation. | Surface | Path | Notes | |---|---|---| | Primary value axis (Y on vertical charts) | `axes.left.labels.numberFormat` | Usually the main “Y-axis” format hub | | Primary value axis (Y on horizontal charts) | `axes.bottom.labels.numberFormat` | Same role after orientation flip | | Secondary value axis | `axes.right` (vertical) or `axes.top` (horizontal) `.labels.numberFormat` | Dual-axis; typically `mode: 'custom'` only — see [Secondary](/axes/secondary) | | Domain / category axis | `axes..labels.numberFormat` | Rarely used for numbers; mekko-style % domain ticks are the main case | Link targets elsewhere refer to the primary value axis as `source: 'primaryRangeAxis'` (not the side name). ```js axes: { left: { labels: { numberFormat: { mode: 'custom', customFormat: { prefix: '$', postfix: '', thousandSeparator: ',', decimalSeparator: '.', decimalPlaces: 0, forceDecimals: true, signDisplay: 'auto', scaleDown: 0, }, }, }, }, } ``` *** ## 2. Chart labels Whole-chart label formats under `annotations`. ### Data labels | Surface | Path | Default / notes | |---|---|---| | Main data-label format | `annotations.dataLabels.numberFormat` | Hub used by Absolute when Absolute is linked | | Absolute part | `annotations.dataLabels.numberFormatAbsolute` | Defaults to `mode: 'linked', source: 'dataLabels'` | | Percentage part | `annotations.dataLabels.numberFormatPercentage` | Defaults to custom with `postfix: '%'` | Which part paints depends on `annotations.dataLabels.labelParts` (e.g. `['absolute']`, `['percentage']`, `['absolute','percentage']`). Pie / donut slice labels also use these Absolute / Percentage helpers. ```js annotations: { dataLabels: { labelParts: ['absolute'], numberFormat: { mode: 'custom', customFormat: { /* … */ }, }, numberFormatAbsolute: { mode: 'custom', // or keep linked to 'dataLabels' customFormat: { /* … */ }, }, numberFormatPercentage: { mode: 'custom', customFormat: { prefix: '', postfix: '%', thousandSeparator: ',', decimalSeparator: '.', decimalPlaces: 0, forceDecimals: true, signDisplay: 'auto', scaleDown: 0, }, }, }, } ``` ### Stack / column totals | Surface | Path | Charts | |---|---|---| | Totals | `annotations.totals.numberFormat` | Stacked bar / `stackedBar100`, mekko, bar mekko, waterfall | Not used on line or area (no stack totals). Changing the Y-axis does **not** change totals unless totals are linked to `primaryRangeAxis`. ```js annotations: { totals: { visible: true, numberFormat: { mode: 'custom', customFormat: { /* … */ }, }, }, } ``` *** ## 3. Annotation overlays Per-object formats. Each arrow / line has its own spec (not shared chart-wide). ### Level lines | Surface | Path | |---|---| | Level-line value label | `annotations.levelLines..labelNumberFormatSpec` | Same shape as `numberFormat`. Default when unset: follow the primary Y-axis (`linked` → `primaryRangeAxis`). Lines bound to the secondary axis follow that axis’s custom format. See [Level lines](/configuration/level-lines). ```js annotations: { levelLines: { target: { value: 70, labelNumberFormatSpec: { mode: 'custom', customFormat: { /* … */ }, }, }, }, } ``` ### Arrows (difference, CAGR, level) Each entry under these maps can carry Absolute and Percentage specs: | Kind | Map path | Absolute | Percentage | |---|---|---|---| | Difference | `annotations.differenceArrows.` | `numberFormatAbsolute` | `numberFormatPercentage` | | CAGR | `annotations.cagrs.` | `numberFormatAbsolute` | `numberFormatPercentage` | | Level | `annotations.levelArrows.` | `numberFormatAbsolute` | `numberFormatPercentage` | Which of Absolute / Percentage appears depends on that arrow’s `displayMode` / `labelParts`. See [Arrows](/configuration/arrows). ```js annotations: { differenceArrows: { q1VsQ2: { start: 0, end: 1, displayMode: 'absoluteWithPercentage', numberFormatAbsolute: { mode: 'custom', customFormat: { /* … */ }, }, numberFormatPercentage: { mode: 'custom', customFormat: { prefix: '', postfix: '%', thousandSeparator: ',', decimalSeparator: '.', decimalPlaces: 1, forceDecimals: true, signDisplay: 'auto', scaleDown: 0, }, }, }, }, } ``` *** ## 4. Linking Three **hubs** can be link sources: primary Y-axis (`primaryRangeAxis`), data labels (`dataLabels`), totals (`totalLabels`). | Target | Can link to | |---|---| | Primary Y-axis | `dataLabels`, `totalLabels` | | Data labels | `primaryRangeAxis`, `totalLabels` | | Totals | `primaryRangeAxis`, `dataLabels` | | Absolute / Percentage parts | `dataLabels`, `primaryRangeAxis`, `totalLabels` | | Arrow Absolute / Percentage | `primaryRangeAxis`, `dataLabels`, `totalLabels` | | Level-line labels | `primaryRangeAxis`, `dataLabels`, `totalLabels` | Hand-authored configs should prefer `mode: 'custom'` so the format does not depend on another surface. ```js // Totals follow the Y-axis annotations: { totals: { numberFormat: { mode: 'linked', source: 'primaryRangeAxis', customFormat: { /* stash if unlinked later */ }, }, }, } ``` *** ## 5. Quick path index | Category | Full path | |---|---| | Primary / secondary / domain axis | `axes..labels.numberFormat` | | Data labels (main) | `annotations.dataLabels.numberFormat` | | Data-label Absolute | `annotations.dataLabels.numberFormatAbsolute` | | Data-label Percentage | `annotations.dataLabels.numberFormatPercentage` | | Stack totals | `annotations.totals.numberFormat` | | Level-line label | `annotations.levelLines..labelNumberFormatSpec` | | Difference arrow Absolute / % | `annotations.differenceArrows..numberFormatAbsolute` / `numberFormatPercentage` | | CAGR arrow Absolute / % | `annotations.cagrs..numberFormatAbsolute` / `numberFormatPercentage` | | Level arrow Absolute / % | `annotations.levelArrows..numberFormatAbsolute` / `numberFormatPercentage` | Percentage chart types (`stackedBar100`, `stackedArea100`, `mekko`) often set `customFormat.postfix: '%'` on the surfaces you care about (usually the primary axis and/or totals). --- --- url: https://chartbuddy.io/embed/docs/configuration/arrows.md --- # Arrows Difference, CAGR, and level arrows live under `annotations` as **id-keyed maps**. Each entry is one arrow. | Kind | chartData path | What it shows | |---|---|---| | Difference | `annotations.differenceArrows.` | Absolute and/or % delta between two columns | | CAGR | `annotations.cagrs.` | Compound annual growth between two columns | | Level | `annotations.levelArrows.` | Horizontal compare between two anchor points | CAGR and difference use simple column indices. Level arrows need fuller `anchorPoints` geometry (`barTop` / `linePoint`, `columnIndex`, `seriesIndex`), so they are the easiest of the three to mis-wire by hand. ## Chart type support | Kind | Stacked / clustered bar | 100% stacked | Waterfall | Combo (bars) | Line / area | Pie / scatter / mekko | |---|---|---|---|---|---|---| | Difference | yes | yes\* | yes | yes | no | no | | CAGR | yes | no | yes | yes | no | no | | Level | yes | yes | yes | yes | yes | no | \*Engine can draw difference on 100% stacked; the editor add menu may not offer it on every type. Combo arrows attach to **bar** series only. ## Shared styling These fields are common across the three arrow maps: | Field | Type | Typical default | Notes | |---|---|---|---| | `lineColor` | string | `'#000000'` | Stroke color | | `lineWidth` | number | `1.5` | Stroke width (px) | | `lineStyle` | string | `'solid'` | `solid` · `dashed` · `densely-dashed` · `dotted` · `dash-dot` | | `labelShape` | string | `'pill'` | `none` · `rounded` · `pill` · `circle` | | `fontSize` | number | `8`–`10` | Label size (pt) | | `chartType` | string | current type | Snapshotted when the arrow was created | Category anchors (`start` / `end` / `columnIndex`) are **0-based column indices** in `seriesData` (after the header row / label column). ### Labels `displayMode` (and default `labelParts`) drive the **computed** absolute / % text. Optional custom copy uses `label` on level arrows, and `text` or `label` on difference / CAGR arrows. If you set custom text **and** leave a computed mode active, both can render (for example `+36` next to another `+36`, or `Expansion` plus a computed `+15%`). For custom text only: ```js label: 'Expansion', labelParts: ['label'], ``` To show custom text plus a computed part: `labelParts: ['label', 'percentage']` (or `'absolute'`). ### Layout offsets Fields like `verticalLinePositionIndex`, `horizontalYOffset`, and CAGR’s `startYOffset` / `endYOffset` are usually set by dragging in edit mode. When hand-authoring, start with `null` / omit offsets where possible, or copy them from an exported config. ## Difference arrows (`annotations.differenceArrows`) U-shaped connector between two columns. Label is usually a computed delta (`displayMode` / `labelParts`). ```js annotations: { differenceArrows: { q1VsQ2: { start: 0, end: 1, startSeries: null, // null = bar tops / stack totals endSeries: null, displayMode: 'percentage', chartType: 'stackedBar', lineColor: '#111827', lineWidth: 1.5, lineStyle: 'solid', labelShape: 'pill', }, }, } ``` | Field | Meaning | |---|---| | `start` / `end` | Column indices | | `startSeries` / `endSeries` | Series index, or `null` for tops / totals | | `displayMode` | `percentage` · `absolute` · `absoluteWithPercentage` · `percentageWithAbsolute` | | `labelParts` | Composite label parts (e.g. `['percentage']`, `['label','percentage']`) | | `label` / `text` | Optional custom text | | `numberFormatAbsolute` / `numberFormatPercentage` | Per-arrow number specs — see [Number formats](/configuration/number-formats) | | `horizontalYOffset` | Drag offset for the horizontal segment | ## CAGR arrows (`annotations.cagrs`) Diagonal growth-rate arrow. Duration comes from the category span (`|end - start|`), or the series span when comparing within a column. ```js annotations: { cagrs: { fySpan: { start: 0, end: 5, startSeries: null, endSeries: null, chartType: 'stackedBar', lineColor: '#000000', lineWidth: 1.5, lineStyle: 'solid', startYOffset: -8, endYOffset: -8, }, }, } ``` | Field | Meaning | |---|---| | `start` / `end` | Column indices | | `startSeries` / `endSeries` | Series index, or `null` for tops / totals | | `text` | Optional custom label override | | `startYOffset` / `endYOffset` | Per-endpoint vertical drag offsets | On waterfall, CAGR uses cumulative totals (not step deltas). ## Level arrows (`annotations.levelArrows`) Horizontal comparison between two explicit anchors (bar tops, segments, or line points). ```js annotations: { levelArrows: { seriesCompare: { anchorPoints: [ { columnIndex: 4, seriesIndex: 2, type: 'barTop' }, { columnIndex: 5, seriesIndex: 2, type: 'barTop' }, ], verticalLinePositionIndex: null, // or a snap index from the editor displayMode: 'percentage', chartType: 'stackedBar', lineColor: '#000000', lineWidth: 1.5, lineStyle: 'solid', label: '', labelShape: 'pill', fontSize: 8, }, }, } ``` Custom label only (no computed delta beside it): ```js label: '+36', labelParts: ['label'], ``` Line-chart variant: ```js anchorPoints: [ { columnIndex: 0, seriesIndex: 0, type: 'linePoint' }, { columnIndex: 4, seriesIndex: 0, type: 'linePoint' }, ] ``` | Field | Meaning | |---|---| | `anchorPoints` | Two anchors: `columnIndex`, `seriesIndex` (or `'total'` / `null`), optional `type` | | `verticalLinePositionIndex` | Snap index for the vertical rail / label (`null` = engine default) | | `displayMode` / `labelParts` | What the label shows | | `label` | Optional custom text | `levelArrows` is not seeded in defaults; the bag appears when the first level arrow is added. ## Authoring tip Add arrows in edit mode (or Hub MCP `manage_annotations`), then snapshot with `getChartData()` / `exportConfig()`. That picks up anchors and layout offsets without guessing snap indices. See also [Level lines](/configuration/level-lines). --- --- url: https://chartbuddy.io/embed/docs/configuration/level-lines.md --- # Level lines A level line is a reference line at a fixed value on a range axis. Config lives at `annotations.levelLines.`. Unlike arrows, simple level lines are reasonable to hand-author. For label snap / custom number formats, authoring in the editor then exporting is still easiest. ## Minimal example ```js annotations: { levelLines: { target: { id: 'target', value: 70, color: '#1A1A1A', width: 1.5, dashArray: '6,4', rangeAxis: 'primaryRange', labelFontFamily: 'Arial', labelFontSize: 10, labelFontColor: '#000000', }, }, } ``` Solid stroke: prefer `dashArray: ''` (same convention as [gridlines](/configuration/gridlines)). ## Fields | Field | Type | Notes | |---|---|---| | `id` | string | Should match the map key | | `value` | number | Position on the range axis | | `color` | string | Line color | | `width` | number | Stroke width (px) | | `dashArray` | string | Dash pattern; `''` = solid | | `rangeAxis` | `'primaryRange'` | `'secondaryRange'` | Which scale measures `value` (default primary) | | `labelPositionIndex` | number | `null` | Horizontal snap for the value label; omit/`null` for auto edge placement | | `labelFontFamily` / `labelFontSize` / `labelFontColor` | string / number / string | Per-line label chrome | | `labelNumberFormatSpec` | object | `{ mode: 'custom'\|'linked', source?, customFormat }` — see [Number formats](/configuration/number-formats) | Shared bag option: `annotations.levelLines.fontFamily` can set a default label font for all lines. ## Chart types Common on stacked / clustered / 100% bar, waterfall, line, stacked area, scatter, combo, and bar mekko. Orientation-aware: the line follows the range axis (horizontal line on a vertical chart, and the remapped equivalent when orientation flips). ## Live patches `annotations` deep-merges for nested story maps in many host paths, but the safest pattern for one-off adds is still: read `getChartData()`, merge the new id into `levelLines`, then `setChartData` with the updated map (or replace the whole `annotations.levelLines` object you want). See also [Arrows](/configuration/arrows). --- --- url: https://chartbuddy.io/embed/docs/configuration/point-markers.md --- # Point markers Line and area charts style markers under `line.pointMarker` / `area.pointMarker`. Markers render when `drawPoints` is on for that series (or forced visible on a single point). ## Defaults ```js line: { // or area: { … } drawPoints: true, pointMarker: { shape: 'circle', radius: 3, fill: null, // derive from series color stroke: '#ffffff', strokeWidth: 1, strokeDashArray: '', // '' = solid series: {}, // per-series style overrides: {}, // per-point style }, } ``` | Field | Notes | |---|---| | `shape` | `circle` · `square` · `triangle` · `diamond` · `cross` | | `radius` | Size in px | | `fill` | Color, or `null` to use the series color | | `stroke` | Border color (`null` also derives from the series) | | `strokeWidth` | Border width (px) | | `strokeDashArray` | Border dash; `''` for solid | Resolution is three-tier: **per-point → per-series → global → built-in default**. ## Per-series style (`pointMarker.series`) Keys are **0-based series indices** (string keys in JSON are fine: `"0"`, `"1"`). Only set the fields you want to change. ```js line: { drawPoints: true, pointMarker: { shape: 'circle', radius: 3, fill: null, stroke: '#ffffff', strokeWidth: 1, strokeDashArray: '', series: { '1': { shape: 'diamond', radius: 5 }, }, overrides: {}, }, } ``` Turn markers on for one series only via `line.overrides` / `area.overrides` (not under `pointMarker`): ```js line: { drawPoints: false, overrides: { '0': { drawPoints: true }, }, pointMarker: { /* … */ }, } ``` ## Per-point overrides (`pointMarker.overrides`) Sparse map: `overrides[seriesIndex][columnIndex]`. Column indices are **0-based data columns** (the category cells in that series row). Only list points you want to change. ```js line: { drawPoints: true, pointMarker: { shape: 'circle', radius: 3, fill: null, stroke: '#ffffff', strokeWidth: 1, strokeDashArray: '', series: {}, overrides: { '0': { '2': { shape: 'square', radius: 6, fill: '#c45c26' }, '5': { visible: false }, }, }, }, } ``` | Override field | Effect | |---|---| | `shape` / `radius` / `fill` / `stroke` / `strokeWidth` / `strokeDashArray` | Same as global | | `visible` | Force show (`true`) or hide (`false`) this point, regardless of `drawPoints` | Hide one point: ```js overrides: { '0': { '3': { visible: false } }, } ``` Emphasize a couple of points: ```js overrides: { '0': { '2': { radius: 6, stroke: '#0f766e', strokeWidth: 2 }, '4': { shape: 'triangle', radius: 5 }, }, } ``` ## Live patches `line` / `area` bags replace as a whole on `setChartData`. To add one point override without dropping others, merge client-side: ```js const cd = insight.getChartData(); const pm = cd.line.pointMarker; const prev = pm.overrides?.['0'] || {}; insight.setChartData({ line: { ...cd.line, pointMarker: { ...pm, overrides: { ...pm.overrides, '0': { ...prev, '4': { radius: 6 }, }, }, }, }, }); ``` For a full upfront `chartData` object, author the sparse `series` / `overrides` maps directly. --- --- url: https://chartbuddy.io/embed/docs/chart-types.md --- # Chart types Each type page follows the same pattern: identity (incl. option bag) → minimal `chartData` → `seriesData` → bag fields → axes → pitfalls → sample. The **Type** column links to the docs page. **`chartType`** is the identifier you pass in `chartData`. **Bag** is the `cd.` object that type reads. | Type | `chartType` | Option bag | `seriesData` layout | Sample | |---|---|---|---|---| | [Clustered bar](/chart-types/clustered-bar) | `clusteredBar` | `bar` | `seriesRows` | [sample](/samples/clustered-bar) | | [Stacked bar / 100%](/chart-types/stacked-bar) | `stackedBar` · `stackedBar100` | `bar` | `seriesRows` | [sample](/samples/stacked-bar) | | [Line](/chart-types/line) | `line` | `line` | `seriesRows` | [sample](/samples/line) | | [Stacked area / 100%](/chart-types/stacked-area) | `stackedArea` · `stackedArea100` | `area` | `seriesRows` | [sample](/samples/stacked-area) | | [Pie / donut](/chart-types/pie) | `pie` · `donut` | `pie` | `categoryValue` | [sample](/samples/pie) | | [Scatter / bubble](/chart-types/scatter) | `scatter` · `bubble` | `scatter` | `pointRows` | [sample](/samples/scatter) | | [Waterfall](/chart-types/waterfall) | `waterfall` | `waterfall` | `seriesRows` | [sample](/samples/waterfall) | | [Marimekko](/chart-types/mekko) | `mekko` | `mekko` | `seriesRows` | [sample](/samples/mekko) | | [Bar Mekko](/chart-types/bar-mekko) | `barMekko` | `barMekko` | `widthRowThenHeightRows` | [sample](/samples/bar-mekko) | | [Combo](/chart-types/combo) | `combo` | `combo` | `seriesRows` | [sample](/samples/combo) | Shared bags: `bar` covers clustered + stacked (+ 100%); `area` covers both stacked area variants. Mekko and Bar Mekko use different bags. Also: [horizontal bar](/samples/horizontal-bar) (clustered bar + `orientation: 'horizontal'`), [chartData schema](/api/chart-data), [Axes · by chart type](/axes/by-chart-type), and the [samples gallery](/samples/). --- --- url: https://chartbuddy.io/embed/docs/chart-types/clustered-bar.md --- # Clustered bar Side-by-side bars that compare series within each category. ## Identity | | | |---|---| | `chartType` | `clusteredBar` | | Option bag | `bar` | | `seriesData` layout | `seriesRows` – row 0 = category header; later rows = series | | Min grid | 2 rows × 2 columns (header included) | | Orientation | Yes – `vertical` (default) or `horizontal` | | Sample | [Clustered bar](/samples/clustered-bar) · [Horizontal bar](/samples/horizontal-bar) | *** ## Minimal `chartData` ```js { chartType: 'clusteredBar', isDataTransposed: true, seriesData: [ ['', 'Q1', 'Q2', 'Q3', 'Q4'], ['Revenue', 100, 112, 125, 140], ['Costs', 60, 66, 70, 78], ], title: { visible: true, text: 'Revenue vs Costs' }, subtitle: { visible: false, text: '' }, } ``` *** ## `seriesData` With `isDataTransposed: true` (usual): ```js [ ['', 'Q1', 'Q2', 'Q3'], // categories ['Revenue', 100, 112, 125], // series 0 ['Costs', 60, 66, 70], // series 1 ] ``` | Rule | Detail | |---|---| | Row 0 | Category labels (after the corner cell) | | Later rows | One series each; first cell is the series name | | Cells | `string \| number \| boolean \| null` | | Ragged rows | Warn, still load – pad when columns must align | Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts). *** ## Option bag: `bar` `clusteredBar`, `stackedBar`, and `stackedBar100` all read **`cd.bar`**. Stacking / 100% behaviour comes from `chartType`, not from a different bag. | Field | Default (new charts) | Role | |---|---|---| | `padding` | `0.3` | Gap between bars / clusters, `0–1` | | `cornerRadius` | `0` | Bar corner radius (px) | | `reverseStackOrder` | `false` | Stack order (mainly stacked types; harmless on clustered) | | `seriesStrokes` / `seriesStrokeWidths` / `seriesStrokeDashArrays` | `[]` | Per-series stroke chrome | | `series` | `[]` | Per-series / per-point override slots | | `overrides` | – | Indexed style patches | ```js { chartType: 'clusteredBar', seriesData: [/* … */], bar: { padding: 0.2, cornerRadius: 2, }, } ``` *** ## Axes & scales | Role | Vertical (default) | Horizontal | |---|---|---| | Domain (categories) | `bottom` | `left` | | Primary range (values) | `left` | `bottom` | * Keep **0 in range** for honest bar lengths (engine bias for bars). * Format / tick interval / gridlines on the **primary range** side after orientation. * Secondary axis is uncommon on pure clustered bars; use [combo](/chart-types/combo) or [dual-axis line](/samples/dual-axis-line) when units differ. Details: [Axes · by chart type](/axes/by-chart-type) · [Value axis](/axes/value-axis) · [Orientation](/concepts/orientation). ### Horizontal ```js { chartType: 'clusteredBar', orientation: 'horizontal', isDataTransposed: true, seriesData: [/* same layout */], title: { visible: true, text: 'Pageview Sources' }, subtitle: { visible: false, text: '' }, legend: { visible: false }, } ``` After the flip, put value formats and `preferredTickInterval` on `axes.bottom`, not `axes.left`. Live: [Horizontal bar sample](/samples/horizontal-bar). *** ## Type-specific behaviour * Palette lives on `legend.colors`, not on the `bar` bag. * Data labels / series emphasis are mostly annotation and override trees – polish in the editor, then `getChartData()`. * Changing to `stackedBar` / `stackedBar100` keeps the same `seriesData` shape and the same `bar` bag; only the type (and axis % defaults for 100%) change. *** ## Pitfalls * Using the `line` or `area` bag on a bar chart – rejected as `foreign-option-bag` * Formatting `axes.left` after setting `orientation: 'horizontal'` (range moved to `bottom`) * Pinning `customMin` above zero so bars look taller than the data * Expecting `reverseStackOrder` to change clustered side-by-side order in a meaningful way – it targets stacks * Wrong `seriesData` orientation (`isDataTransposed: false`) without matching the grid *** ## Related * [Stacked bar / 100%](/chart-types/stacked-bar) · [Combo](/chart-types/combo) * [chartData schema](/api/chart-data) · [Axes](/axes/) · [Legend](/configuration/legend) * Samples: [Clustered bar](/samples/clustered-bar) · [Horizontal bar](/samples/horizontal-bar) · [Currency ticks](/samples/currency-ticks) --- --- url: https://chartbuddy.io/embed/docs/chart-types/stacked-bar.md --- # Stacked bar Stacked bars show composition within each category (absolute or normalised to 100%). ## Identity | | | |---|---| | `chartType` | `stackedBar` · `stackedBar100` | | Option bag | `bar` | | `seriesData` layout | `seriesRows` – row 0 = category header; later rows = series | | Min grid | 2 rows × 2 columns (header included) | | Orientation | Yes – `vertical` (default) or `horizontal` | | Sample | [Stacked bar](/samples/stacked-bar) · [Percent axis](/samples/percent-axis) | *** ## Minimal `chartData` ```js { chartType: 'stackedBar', // or 'stackedBar100' isDataTransposed: true, seriesData: [ ['', 'Q1', 'Q2', 'Q3'], ['Product A', 40, 48, 52], ['Product B', 30, 28, 35], ], title: { visible: true, text: 'Mix by quarter' }, subtitle: { visible: false, text: '' }, } ``` *** ## `seriesData` Same `seriesRows` layout as [clustered bar](/chart-types/clustered-bar) (`isDataTransposed: true`): ```js [ ['', 'Q1', 'Q2', 'Q3'], ['Product A', 40, 48, 52], ['Product B', 30, 28, 35], ] ``` Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts). *** ## Option bag: `bar` `stackedBar` / `stackedBar100` / `clusteredBar` all read **`cd.bar`**. Stacking vs clustering comes from `chartType`, not a different bag. | Field | Default (new charts) | Role | |---|---|---| | `padding` | `0.3` | Gap between category stacks, `0–1` | | `cornerRadius` | `0` | Corner radius (px) | | `reverseStackOrder` | `false` | Reverse segment order within the stack | | `seriesStrokes` / `seriesStrokeWidths` / `seriesStrokeDashArrays` | `[]` | Per-series stroke chrome | | `series` / `overrides` | empty | Indexed style patches | ```js { chartType: 'stackedBar', seriesData: [/* … */], bar: { padding: 0.25, reverseStackOrder: true, }, } ``` *** ## Axes & scales | Role | Vertical | Horizontal | |---|---|---| | Domain | `bottom` | `left` | | Primary range | `left` | `bottom` | ### `stackedBar` Absolute stacks. Keep **0 in range**. Format and tick the primary range side after orientation. ### `stackedBar100` Stacks normalised to **0–100**. Defaults seed `%` postfix; prefer `preferredTickInterval` of `20` / `25` / `50` (divisors of 100). ```js { chartType: 'stackedBar100', isDataTransposed: true, seriesData: [/* … */], axes: { left: { labels: { preferredTickInterval: 25, // % postfix usually already seeded }, }, }, } ``` Details: [Axes · by chart type](/axes/by-chart-type) · [Value axis · percentage charts](/axes/value-axis#percentage-charts) · [Percent axis sample](/samples/percent-axis). *** ## Type-specific behaviour * Switching `stackedBar` ↔ `stackedBar100` ↔ `clusteredBar` keeps the same grid and `bar` bag. * Totals / data labels often live under `annotations` – polish in the editor, then `getChartData()`. * Palette: `legend.colors`. *** ## Pitfalls * Using the `area` bag on a stacked bar (`foreign-option-bag`) * Hand-picking a `%` tick interval that is not a divisor of 100 on `stackedBar100` * Pinning bounds that clip the stack (or that fight 0–100 on 100% charts) * Expecting `reverseStackOrder` to reorder categories – it reorders **segments**, not domain columns *** ## Related * [Clustered bar](/chart-types/clustered-bar) · [Stacked area](/chart-types/stacked-area) · [Combo](/chart-types/combo) * [chartData schema](/api/chart-data) · [Axes](/axes/) * Samples: [Stacked bar](/samples/stacked-bar) · [Percent axis](/samples/percent-axis) --- --- url: https://chartbuddy.io/embed/docs/chart-types/line.md --- # Line Trends over an ordered domain – one polyline per series. ## Identity | | | |---|---| | `chartType` | `line` | | Option bag | `line` | | `seriesData` layout | `seriesRows` – row 0 = category header; later rows = series | | Min grid | 2 rows × 2 columns (header included) | | Orientation | Yes – `vertical` (default) or `horizontal` | | Sample | [Line](/samples/line) · [Dual-axis line](/samples/dual-axis-line) | *** ## Minimal `chartData` ```js { chartType: 'line', isDataTransposed: true, seriesData: [ ['', 'Jan', 'Feb', 'Mar', 'Apr'], ['Visits', 120, 132, 128, 150], ], title: { visible: true, text: 'Weekly visits' }, subtitle: { visible: false, text: '' }, legend: { visible: false }, } ``` *** ## `seriesData` Same `seriesRows` layout as clustered bar (with `isDataTransposed: true`): ```js [ ['', 'Jan', 'Feb', 'Mar', 'Apr'], ['Visits', 120, 132, 128, 150], ['Signups', 40, 44, 41, 55], ] ``` | Rule | Detail | |---|---| | Row 0 | Ordered domain labels (categories / periods) | | Later rows | One series each | | Empty / `null` cells | Breaks in the line for that point (intentional gaps) | Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts). *** ## Option bag: `line` Only `chartType: 'line'` reads **`cd.line`**. Stacked areas use the separate `area` bag. | Field | Default (new charts) | Role | |---|---|---| | `width` | `2` | Stroke width (px) | | `strokeDashArray` | `''` | Dash pattern (SVG-style string) | | `drawPoints` | `false` | Show point markers on the line | | `curveParameter` | `0.85` | Curve tension (editor **Smoothness** control) | | `shadowIntensity` | `0` | Soft shadow under the stroke | | `pointMarker` | see below | Marker style when `drawPoints` is on | | `overrides` | `{}` | Indexed style patches | Leave `curveType` at its default (`'cardinal'`). Prefer adjusting **`curveParameter`** (editor Smoothness) rather than swapping interpolation names. ### Point markers Rendered only when `drawPoints: true`. Resolution order: per-point override → per-series → global `pointMarker`. ```js { chartType: 'line', seriesData: [/* … */], line: { width: 2.5, drawPoints: true, curveParameter: 0.85, pointMarker: { shape: 'circle', // circle | square | diamond | triangle radius: 3, fill: null, // null = series color stroke: '#ffffff', strokeWidth: 1, }, }, } ``` Full marker surface: [Point markers](/configuration/point-markers). *** ## Axes & scales | Role | Vertical (default) | Horizontal | |---|---|---| | Domain | `bottom` | `left` | | Primary range | `left` | `bottom` | | Secondary range | `right` | `top` | * Dual-axis is fully supported on line charts: enable the secondary range axis and set `boundSeries` to the series indices that use it. * Setting the range axis with `customMin` / `customMax` is normal (unlike bars, which bias toward including zero). * Pair tick formats and `preferredTickInterval` with the side that holds the range role after orientation. Details: [Axes · Secondary](/axes/secondary) · [Axes · by chart type](/axes/by-chart-type) · [Dual-axis line sample](/samples/dual-axis-line). ```js { chartType: 'line', isDataTransposed: true, seriesData: [ ['', 'Q1', 'Q2', 'Q3', 'Q4'], ['Revenue', 120, 140, 155, 170], ['Margin %', 18, 19, 21, 20], ], axes: { right: { id: 'secondaryRange', enabled: true, visible: true, boundSeries: [1], labels: { preferredTickInterval: 5, numberFormat: { mode: 'custom', customFormat: { prefix: '', postfix: '%', thousandSeparator: ',', decimalSeparator: '.', decimalPlaces: 0, forceDecimals: false, signDisplay: 'auto', scaleDown: 0, }, }, }, }, }, } ``` *** ## Type-specific behaviour * Series color still comes from `legend.colors`. * Markers stay off until `line.drawPoints: true` (or you turn them on in the editor). * For bar + line on one chart, use [`combo`](/chart-types/combo) with `combo.seriesTypes` – do not put bar options in `cd.line`. * Area fills belong on `stackedArea` / `cd.area`, not on `line`. *** ## Pitfalls * Expecting markers without `drawPoints: true` * Putting area/bar fields on `line` (`foreign-option-bag`) * Binding secondary with the wrong series index after reordering rows * Forcing bar-style “must include zero” when a tighter range-axis span is what you want – set `customMin` / `customMax` deliberately ([Value axis](/axes/value-axis)) * Using `stackedArea` defaults/`area` bag when you only wanted a stroke *** ## Related * [Stacked area](/chart-types/stacked-area) · [Combo](/chart-types/combo) · [Point markers](/configuration/point-markers) * [chartData schema](/api/chart-data) · [Axes](/axes/) * Samples: [Line](/samples/line) · [Dual-axis line](/samples/dual-axis-line) · [Percent axis](/samples/percent-axis) --- --- url: https://chartbuddy.io/embed/docs/chart-types/stacked-area.md --- # Stacked area Filled stacked series over an ordered domain (absolute or normalised to 100%). ## Identity | | | |---|---| | `chartType` | `stackedArea` · `stackedArea100` | | Option bag | `area` | | `seriesData` layout | `seriesRows` – row 0 = category header; later rows = series | | Min grid | 2 rows × 2 columns (header included) | | Orientation | Yes – `vertical` (default) or `horizontal` | | Sample | [Stacked area](/samples/stacked-area) | *** ## Minimal `chartData` ```js { chartType: 'stackedArea', // or 'stackedArea100' isDataTransposed: true, seriesData: [ ['', 'Q1', 'Q2', 'Q3', 'Q4'], ['Series A', 20, 28, 26, 32], ['Series B', 12, 14, 18, 16], ], title: { visible: true, text: 'Area mix' }, subtitle: { visible: false, text: '' }, } ``` *** ## `seriesData` Same `seriesRows` layout as line / bar (`isDataTransposed: true`): ```js [ ['', 'Q1', 'Q2', 'Q3', 'Q4'], ['Series A', 20, 28, 26, 32], ['Series B', 12, 14, 18, 16], ] ``` Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts). *** ## Option bag: `area` Both stacked area types read **`cd.area`**. | Field | Default (new charts) | Role | |---|---|---| | `width` | `2` | Stroke width (px) | | `strokeDashArray` | `''` | Dash pattern | | `drawPoints` | `false` | Point markers on the upper edge | | `curveParameter` | `1` | Curve tension (same idea as line smoothness) | | `fillOpacity` | `0.6` | Solid fill opacity | | `fillType` | `'solid'` | `'solid'` | `'gradient'` | | `fillOpacityTop` / `fillOpacityBottom` | `0.35` / `0.05` | Gradient stops when `fillType: 'gradient'` | | `gradientTarget` | `'transparent'` | Gradient fade target | | `strokeOpacity` | `1` | Stroke opacity | | `shadowIntensity` | `0` | Soft shadow | | `pointMarker` | (same shape as line) | Markers when `drawPoints` is on | | `dataLabels` / `overrides` | empty | Label / style patches | ```js { chartType: 'stackedArea', seriesData: [/* … */], area: { fillOpacity: 0.45, drawPoints: false, }, } ``` Markers: [Point markers](/configuration/point-markers). *** ## Axes & scales | Role | Vertical | Horizontal | |---|---|---| | Domain | `bottom` | `left` | | Primary range | `left` | `bottom` | * Absolute stacks (`stackedArea`) usually keep **0 in range**. * `stackedArea100` uses a **0–100** `%` value axis (same interval rules as other 100% types). * Secondary axis is uncommon; prefer [line](/chart-types/line) or [combo](/chart-types/combo) for dual units. Details: [Axes · by chart type](/axes/by-chart-type) · [Percent axis sample](/samples/percent-axis). *** ## Type-specific behaviour * Do not put area options on `cd.line` – that is a foreign bag. * Palette: `legend.colors`. *** ## Pitfalls * Using `chartType: 'line'` with `area: {…}` (or the reverse) * Expecting a single non-stacked “area chart” type – public types are the stacked variants * Fighting 0–100 bounds / tick intervals on `stackedArea100` * Turning on `drawPoints` without checking marker clutter on dense domains *** ## Related * [Line](/chart-types/line) · [Stacked bar](/chart-types/stacked-bar) * [chartData schema](/api/chart-data) · [Axes](/axes/) * Sample: [Stacked area](/samples/stacked-area) --- --- url: https://chartbuddy.io/embed/docs/chart-types/pie.md --- # Pie / donut Share of a single total across categories. A donut is a pie with a hole. ## Identity | | | |---|---| | `chartType` | `pie` · `donut` | | Option bag | `pie` | | `seriesData` layout | `categoryValue` – columns `[Category, Value]` | | Min grid | 2 rows × 2 columns | | Orientation | N/A – axes disabled | | Sample | [Pie](/samples/pie) | *** ## Minimal `chartData` ```js { chartType: 'pie', seriesData: [ ['Category', 'Value'], ['North', 45], ['South', 30], ['West', 25], ], title: { visible: true, text: 'Share' }, subtitle: { visible: false, text: '' }, } ``` *** ## `seriesData` ```js [ ['Category', 'Value'], ['North', 45], ['South', 30], ['West', 25], ] ``` | Rule | Detail | |---|---| | Layout | `categoryValue` – not series-rows | | Transpose | Do not use bar/line `isDataTransposed: true` patterns here | | One measure | One value column | Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts). *** ## Option bag: `pie` | Field | Default (new charts) | Role | |---|---|---| | `innerRadiusRatio` | `0` for `pie`; **`0.5` for `donut`** | Hole size, `0–1` (`0` = full pie) | | `scaleFactor` | `0.9` | Overall pie scale inside the plot | | `labelVisibilityThreshold` | `0` | Hide tiny-slice labels below this share | | `explode` | `[]` | Per-slice explode offsets | | `line` | white stroke defaults | Slice border color / width / dash | | `seriesStrokes` / widths / dash | `[]` | Per-slice stroke chrome | | `overrides` | `{}` | Indexed style patches | ### Donut Ask for `'donut'` – the hole is already open. You do **not** need to set `innerRadiusRatio` unless you want a different size: ```js { chartType: 'donut', seriesData: [['Category', 'Value'], ['North', 45], ['South', 30]], } // Override hole only when needed: { chartType: 'donut', pie: { innerRadiusRatio: 0.7 } } ``` `getChartData()` returns `chartType: 'pie'` with `innerRadiusRatio: 0.5`. That round-trips correctly. *** ## Axes & scales Axes are **disabled** for pie / donut. Do not author `axes` for layout. Labels live in pie / series-label chrome. Details: [Axes · by chart type](/axes/by-chart-type). *** ## Type-specific behaviour * Slice colors: `legend.colors`. * Series labels default more visible on pie/donut than on bar/line. * Prefer editor placement + `getChartData()` for label leader lines and explode. *** ## Pitfalls * Feeding a bar/line `seriesRows` grid into pie * Putting options in `bar` / `line` on a pie (`foreign-option-bag`) * Expecting `getChartData()` to keep `chartType: 'donut'` — it returns `pie` with the hole set * Setting `innerRadiusRatio: 5` (must be `0–1` – validation throws) *** ## Related * [chartData schema](/api/chart-data) · [Legend](/configuration/legend) * Sample: [Pie](/samples/pie) --- --- url: https://chartbuddy.io/embed/docs/chart-types/scatter.md --- # Scatter / bubble Points positioned by two metrics, optionally sized and grouped. A bubble is a scatter with a larger default marker. ## Identity | | | |---|---| | `chartType` | `scatter` · `bubble` | | Option bag | `scatter` | | `seriesData` layout | `pointRows` – row 0 names metrics; later rows are points | | Min grid | 2 rows × 3 columns | | Orientation | Linear X/Y – not category band remap like bars | | Sample | [Scatter](/samples/scatter) | *** ## Minimal `chartData` ```js { chartType: 'scatter', // or 'bubble' seriesData: [ ['', 'Metric X', 'Metric Y', 'Size', 'Group'], ['A', 10, 15, 8, 'G1'], ['B', 14, 11, 12, 'G1'], ['C', 8, 20, 6, 'G2'], ], title: { visible: true, text: 'Scatter' }, subtitle: { visible: false, text: '' }, } ``` *** ## `seriesData` Scatter does **not** use bar/line transpose. Layout is `pointRows`: ```js [ ['', 'Metric X', 'Metric Y', 'Size', 'Group'], ['Point 1', 10, 15, 8, 'A'], ['Point 2', 14, 11, 12, 'A'], ] ``` | Column (typical) | Role | |---|---| | 0 | Point label | | 1 | X metric | | 2 | Y metric | | 3 | Optional size | | 4 | Optional group | Minimum: header + one point, with at least X and Y columns. Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts). *** ## Option bag: `scatter` | Field | Default (new charts) | Role | |---|---|---| | `diameter` | `10` for `scatter`; **larger (~`45`) for `bubble`** | Marker diameter (px) | | `shape` | `'circle'` | Default marker shape | | `shapes` | `[]` | Optional per-series shape list | | `seriesStrokes` / widths / dash | `[]` | Per-series stroke chrome | | `overrides` | `{}` | Indexed style patches | ```js { chartType: 'bubble', seriesData: [/* … */], // diameter already seeded larger; override only if needed: scatter: { diameter: 36, shape: 'circle' }, } ``` `getChartData()` returns `chartType: 'scatter'` with the larger diameter already set. *** ## Axes & scales | Role | Behaviour | |---|---| | Domain | **Linear X** (runtime forces linear; not a category band) | | Primary range | Linear Y | Format both linear sides. Ignore category `fitMechanism` / date wrap unless you intentionally treat labels as categories (you usually do not). Details: [Axes · by chart type](/axes/by-chart-type) · [Domain axis](/axes/domain). *** ## Type-specific behaviour * Size / group columns are optional but common for bubble-style decks. * Palette / grouping still ties into legend colors. * Prefer editor + `getChartData()` for per-point overrides. *** ## Pitfalls * Using `isDataTransposed: true` series-rows layout meant for bars * Putting options in `pie` / `bar` on a scatter * Expecting category wrap/rotate behaviour from band axes * Expecting `getChartData()` to keep `chartType: 'bubble'` — it returns `scatter` with the larger diameter *** ## Related * [chartData schema](/api/chart-data) · [Axes](/axes/) * Sample: [Scatter](/samples/scatter) --- --- url: https://chartbuddy.io/embed/docs/chart-types/waterfall.md --- # Waterfall Running total that shows how contributions bridge two values. ## Identity | | | |---|---| | `chartType` | `waterfall` | | Option bag | `waterfall` | | `seriesData` layout | `seriesRows` – row 0 = category header; later rows = series | | Min grid | 2 rows × 2 columns (header included) | | Orientation | Yes – `vertical` (default) or `horizontal` | | Sample | [Waterfall](/samples/waterfall) | *** ## Minimal `chartData` ```js { chartType: 'waterfall', isDataTransposed: true, seriesData: [ ['', 'Start', 'Price', 'Volume', 'End'], ['Bridge', 100, 20, -10, null], ], waterfall: { columns: { 3: { isTotal: true } } }, title: { visible: true, text: 'Bridge' }, subtitle: { visible: false, text: '' }, } ``` *** ## `seriesData` Same `seriesRows` shape as bar/line (`isDataTransposed: true`). Leave **total** columns empty (`null`) – ChartBuddy computes them. ```js [ ['', 'Start', 'Price', 'Volume', 'End'], ['Bridge', 100, 20, -10, null], // End has no value of its own ] ``` Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts) · [Waterfall totals](/api/chart-data#waterfall-do-not-invent-totals). *** ## Option bag: `waterfall` | Field | Default (new charts) | Role | |---|---|---| | `columns` | `{}` | Sparse per-column metadata (`isTotal`, `startBar`, `showSegments`) | | `totalBarColor` | `'#808080'` | Color for total bars | | `totalLegendLabel` | `'Total'` | Legend label for totals | | `seriesStrokes` / `seriesStrokeWidths` | `[]` | Stroke chrome | | `connectorOverrides` / `overrides` | `{}` | Connector / bar style patches | ### Do not invent totals Contribution columns (default) add their value to the running total. Total columns (`isTotal: true`) **display** the running total and contribute nothing. Mark the column and leave its cell empty – a value typed there is ignored. Typing a closing figure **without** `isTotal` creates another contribution (the bridge then ends near ~2×). ### `waterfall.columns` keys Keys are 0-based **data-column** indices: positions in the header row **after** the row-label cell. ```js seriesData: [ ['', 'Start', 'Price', 'Volume', 'Mix', 'End'], // ^0 ^1 ^2 ^3 ^4 <- waterfall.columns keys ['Bridge', 100, 18, -8, 5, null], ], waterfall: { columns: { 4: { isTotal: true } } }, ``` `'End'` is header array index 5 but data column **4**. Off-by-one is silent. | Property | Default | Meaning | |---|---|---| | `isTotal` | `false` | Show running total; ignore this column's own value | | `startBar` | `false` | New sequence from zero; no connector from the previous column | | `showSegments` | `false` | Draw a total as per-series segments instead of one solid bar | ### Column 0 Always forced to `isTotal` + `startBar`. Unlike other totals it **does** use its own value (opening balance). ### Multiple sequences A mid-chart opening bar needs **both** `isTotal` and `startBar`: ```js waterfall: { columns: { 3: { isTotal: true }, // close first bridge 4: { isTotal: true, startBar: true }, // open second }, } ``` *** ## Axes & scales | Role | Vertical | Horizontal | |---|---|---| | Domain | `bottom` | `left` | | Primary range | `left` | `bottom` | Keep zero in play for readable bridges. Format the primary range side after orientation. Details: [Axes · by chart type](/axes/by-chart-type). *** ## Type-specific behaviour * Connectors and total chrome are easier to judge in the editor – then `getChartData()`. * Palette: `legend.colors` (totals use `totalBarColor` / legend label). *** ## Pitfalls * Closing value without `isTotal` (double-counts the bridge) * Wrong column index (header position vs data-column index) * `startBar` alone mid-chart (floating contribution instead of an opening total) * Seeding sample totals into every new chart – leave `columns` sparse *** ## Related * [chartData schema · waterfall](/api/chart-data#waterfall-do-not-invent-totals) · [Axes](/axes/) * Sample: [Waterfall](/samples/waterfall) --- --- url: https://chartbuddy.io/embed/docs/chart-types/mekko.md --- # Marimekko Variable-width 100% stacked columns. Every column is normalised to **100% height**; column width is proportional to the category total. Both axes read as shares. For **absolute** heights with variable widths, use [Bar Mekko](/chart-types/bar-mekko) instead. ## Identity | | | |---|---| | `chartType` | `mekko` | | Option bag | `mekko` (not `barMekko`) | | `seriesData` layout | `seriesRows` – row 0 = category header; later rows = series | | Min grid | 2 rows × 2 columns (header included) | | Orientation | Vertical composition layout (see axes notes) | | Sample | [Marimekko](/samples/mekko) | *** ## Minimal `chartData` ```js { chartType: 'mekko', isDataTransposed: true, seriesData: [ ['', 'Seg A', 'Seg B', 'Seg C'], ['Series 1', 40, 30, 20], ['Series 2', 25, 35, 15], ], title: { visible: true, text: 'Marimekko' }, subtitle: { visible: false, text: '' }, } ``` *** ## `seriesData` Same `seriesRows` shape as stacked bar (`isDataTransposed: true`). Values drive both segment shares (height) and category weight (width). ```js [ ['', 'Seg A', 'Seg B', 'Seg C'], ['Series 1', 40, 30, 20], ['Series 2', 25, 35, 15], ] ``` There is **no** separate width row (that is Bar Mekko). Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts). *** ## Option bag: `mekko` | Field | Default (new charts) | Role | |---|---|---| | `showTotals` | `true` | Totals visibility (prefer `annotations.totals.visible`) | | `overrides` | `{}` | Indexed style patches | Most authoring is axes, annotations, and legend – not a large `mekko` field set. Prefer editor + `getChartData()` for segment chrome. ```js { chartType: 'mekko', seriesData: [/* … */], mekko: { /* overrides if needed */ }, } ``` Do not put Bar Mekko `sort` on `mekko` – that belongs on `barMekko`. *** ## Axes & scales | Role | Behaviour | |---|---| | Domain | Linear **0–100** width domain (category thickness) | | Primary range | Linear **0–100** `%` stacks | Prefer `%` postfix and tick intervals that divide 100 (`20` / `25` / `50`). Details: [Axes · by chart type](/axes/by-chart-type) · [Domain axis · mekko](/axes/domain) · [Percent axis sample](/samples/percent-axis). *** ## Type-specific behaviour * Width comes from category totals of the same grid – not a dedicated width row. * Totals labels often via `annotations.totals`. *** ## Pitfalls * Using `barMekko` options / width-row layout on a classic mekko * Expecting absolute €/units on the value axis – mekko heights are shares * Hand-authoring dense domain chrome from scratch – export from the editor when possible *** ## Related * [Bar Mekko](/chart-types/bar-mekko) · [Stacked bar 100%](/chart-types/stacked-bar) * [chartData schema](/api/chart-data) · [Axes](/axes/) * Sample: [Marimekko](/samples/mekko) --- --- url: https://chartbuddy.io/embed/docs/chart-types/bar-mekko.md --- # Bar Mekko Variable-width bars with **absolute** heights: width encodes one metric, height another. * **Bar width** – first data row after the header (not drawn as a segment) * **Bar height** – later rows stack on a real value axis (not normalised to 100%) Classic [Marimekko](/chart-types/mekko) normalises every column to 100% height. Use Bar Mekko when height must stay in real units (revenue, passengers, …). ## Identity | | | |---|---| | `chartType` | `barMekko` | | Option bag | `barMekko` (not `mekko`) | | `seriesData` layout | `widthRowThenHeightRows` | | Min grid | 3 rows × 2 columns (header + width + ≥1 height series) | | Orientation | Width along domain; value axis often hidden by default | | Sample | [Bar Mekko](/samples/bar-mekko) | *** ## Minimal `chartData` ```js { chartType: 'barMekko', isDataTransposed: true, seriesData: [ ['', 'Enterprise', 'Mid-market', 'SMB', 'Startup'], ['Accounts', 120, 85, 200, 350], // width – sizes bars, not drawn ['Core revenue', 48, 16, 8, 3], // stacked height ['Add-ons', 14, 12, 10, 6], ], title: { visible: true, text: 'Revenue by segment' }, subtitle: { visible: true, text: 'Bar height = revenue (€m) · width = number of accounts', }, } ``` *** ## `seriesData` With `isDataTransposed: true` (recommended): | Row | Role | |---|---| | Row 0 | Category labels | | Row 1 | **Width** values – one per category (not drawn) | | Row 2+ | **Height** series – stacked inside each bar | ```js [ ['', 'Enterprise', 'Mid-market', 'SMB'], ['Accounts', 120, 85, 200], ['Core revenue', 48, 16, 8], ['Add-ons', 14, 12, 10], ] ``` Positive segments stack up; negative stack down. Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts). *** ## Option bag: `barMekko` | Field | Default (new charts) | Role | |---|---|---| | `sort` | `'heightDesc'` | Column order along the width axis | | `overrides` | `{}` | Per-bar patches (column-first) | `sort` values: `heightDesc` · `heightAsc` · `widthDesc` · `widthAsc` · `dataOrder`. ```js { chartType: 'barMekko', seriesData: [/* … */], barMekko: { sort: 'widthDesc' }, } ``` Do not put these fields on `mekko` – wrong bag. *** ## Axes & scales | Role | Behaviour | |---|---| | Domain | Linear width domain (category thickness) | | Primary range | Absolute value scale; often **`visible: false`** by default (totals + domain labels can be enough) | Format the value side when you show it. Details: [Axes · by chart type](/axes/by-chart-type) · [Domain axis](/axes/domain). *** ## Type-specific behaviour * Width row is structural – it is not a legend series you “hide.” * Sorting rearranges columns along the width axis; it does not change the grid math. *** ## Pitfalls * Only two rows (missing a height series) – fails min grid * Putting width in a later row or treating width as a drawn segment * Using `mekko` bag / expecting 100% heights *** ## Related * [Marimekko](/chart-types/mekko) · [chartData schema](/api/chart-data) · [Axes](/axes/) * Sample: [Bar Mekko](/samples/bar-mekko) --- --- url: https://chartbuddy.io/embed/docs/chart-types/combo.md --- # Combo Mixed series types on one chart (bars + line/area) via the `combo` bag. Bind series to the [secondary axis](/axes/secondary) when units differ. ## Identity | | | |---|---| | `chartType` | `combo` | | Option bag | `combo` | | `seriesData` layout | `seriesRows` – row 0 = category header; later rows = series | | Min grid | 2 rows × 2 columns (header included) | | Orientation | Yes – `vertical` (default) or `horizontal` (secondary moves to **top**) | | Sample | [Combo](/samples/combo) · [Dual-axis line](/samples/dual-axis-line) | *** ## Minimal `chartData` ```js { chartType: 'combo', isDataTransposed: true, seriesData: [ ['', 'Q1', 'Q2', 'Q3', 'Q4'], ['Units', 40, 48, 52, 61], ['ASP', 12, 11, 13, 14], ], combo: { seriesTypes: { 1: 'line' }, }, title: { visible: true, text: 'Units and ASP' }, subtitle: { visible: false, text: '' }, } ``` *** ## `seriesData` Same `seriesRows` layout as clustered bar / line (`isDataTransposed: true`). Series index in `combo.seriesTypes` and `boundSeries` is the data-row index among series (0 = first series row after the header). ```js [ ['', 'Q1', 'Q2', 'Q3', 'Q4'], ['Units', 40, 48, 52, 61], // series 0 – default bar ['ASP', 12, 11, 13, 14], // series 1 – map to line ] ``` Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts). *** ## Option bag: `combo` | Field | Default (new charts) | Role | |---|---|---| | `seriesTypes` | `{}` | Map **series index →** `'bar'` | `'line'` | `'area'` | | `baseChartType` | `null` | Optional `'clusteredBar'` | `'stackedBar'` for unlisted / bar series | Unlisted series default to bars. Stroke / fill detail still comes from the usual `line` / `area` / `bar` bags when those series render as that type – set `seriesTypes` first, then style. ```js combo: { seriesTypes: { 0: 'bar', 1: 'line' }, baseChartType: 'clusteredBar', } ``` *** ## Axes & scales | Role | Vertical | Horizontal | |---|---|---| | Domain | `bottom` | `left` | | Primary range | `left` | `bottom` | | Secondary range | `right` | `top` | Bars on the primary scale share one scale. Bind line/area series with different units via `boundSeries` on the secondary side: ```js { chartType: 'combo', isDataTransposed: true, seriesData: [/* … */], combo: { seriesTypes: { 1: 'line' } }, axes: { right: { id: 'secondaryRange', enabled: true, visible: true, boundSeries: [1], labels: { numberFormat: { mode: 'custom', customFormat: { prefix: '$', postfix: '', thousandSeparator: ',', decimalSeparator: '.', decimalPlaces: 0, forceDecimals: false, signDisplay: 'auto', scaleDown: 0, }, }, }, }, }, } ``` Details: [Axes · Secondary](/axes/secondary) · [Axes · by chart type](/axes/by-chart-type) · [Orientation](/concepts/orientation). *** ## Type-specific behaviour * For a dual-scale **line-only** chart, `chartType: 'line'` + secondary is enough – combo is for mixed geometries. * After inserting/reordering series rows, re-check `seriesTypes` and `boundSeries` indices. * Prefer editor + `getChartData()` when unsure of indices. *** ## Pitfalls * Wrong `seriesTypes` values (`'clusteredBar'` is not valid here – use `'bar'` | `'line'` | `'area'`) * Enabling secondary without `boundSeries` (or with a stale index) * Putting secondary on `right` after flipping to horizontal (it moves to `top`) * Two axes for the same unit (readers compare the wrong scale) *** ## Related * [Line](/chart-types/line) · [Clustered bar](/chart-types/clustered-bar) · [Axes · Secondary](/axes/secondary) * [chartData schema](/api/chart-data) * Samples: [Combo](/samples/combo) · [Dual-axis line](/samples/dual-axis-line) --- --- url: https://chartbuddy.io/embed/docs/axes.md --- # Axes Axes map categories and values onto the chart. Config covers ticks, bounds, formats, dual scales, and label fit. Axes are keyed by **physical side** (`left` | `right` | `top` | `bottom`). Each side holds a **role**: | Role | Meaning | |---|---| | `domain` | Categories (or linear X on scatter / mekko width) | | `primaryRange` | Main value scale | | `secondaryRange` | Second value scale (dual-axis) | | `unused` | Placeholder for the side not used by the current orientation | ```js axes: { bottom: { id: 'domain', /* … */ }, left: { id: 'primaryRange', /* … */ }, right: { id: 'secondaryRange', enabled: false, boundSeries: [], /* … */ }, top: { id: 'unused', enabled: false, /* … */ }, } ``` Which role sits on which side depends on [orientation](/concepts/orientation). When orientation changes, the embed moves each role to its new side. Put settings on the side that holds that role *after* the flip (for example, value formats go on `left` when vertical and on `bottom` when horizontal). | Orientation | Domain | Primary range | Secondary range | |---|---|---|---| | `vertical` (default) | bottom | left | right | | `horizontal` | left | bottom | **top** | *** ## Checklist Typical order: 1. **Format the value axis** – currency, `%`, decimals ([Number formats](/configuration/number-formats), [Value axis](/axes/value-axis)) 2. **Tick density** – leave auto, or set `preferredTickInterval` ([Value axis](/axes/value-axis#preferred-interval)) 3. **Bounds** – pin min/max only when you mean it (zero rules, zoom) ([Value axis](/axes/value-axis#bounds)) 4. **Gridlines** – usually on the primary range side ([Gridlines](/configuration/gridlines)) 5. **Category labels** – wrap / rotate / dates when crowded ([Domain axis](/axes/domain)) 6. **Titles** – units on the value axis when the format alone is not enough ([Titles & chrome](/axes/titles-and-chrome)) 7. **Dual axis / breaks** – when you need two scales or a collapsed gap ([Secondary](/axes/secondary), [Breaks](/axes/breaks)) *** ## Defaults (what you get) From `getDefaultAxesConfig` (new charts): * Domain: band scale, `fitMechanism: 'wrap'`, titles **hidden** * Primary range: linear, `preferredTickInterval: null` (auto sparse ticks), gridlines **off**, titles **hidden** * Secondary: `enabled: false`, `visible: false`, `boundSeries: []` * `%` chart types (`stackedBar100`, mekko, …) seed `postfix: '%'` on the relevant labels * Bar Mekko hides the primary range **axis line** by default (`visible: false`); labels stay available when you turn it on * Pie / donut: axes disabled `axes` **deep-merges** on `setChartData` / `update` – see [Defaults & merging](/concepts/defaults). Prefer exporting a full tree from the editor (`getChartData()`) when you need a complete starting point, then patch. *** ## Pages | Page | Use when | |---|---| | [Value axis](/axes/value-axis) | Ticks, bounds, `%` charts, zero rules, currency | | [Domain axis](/axes/domain) | Category labels, wrap/rotate, dates | | [Titles & chrome](/axes/titles-and-chrome) | Axis titles, visibility, tick marks, colors | | [Secondary / dual axis](/axes/secondary) | Second scale + `boundSeries` (incl. horizontal **top**) | | [Axis breaks](/axes/breaks) | Collapse a dead range on the value axis | | [By chart type](/axes/by-chart-type) | What each type exposes / fixes / hides | | [Orientation & roles](/axes/orientation) | Remap rules and the “0,1,2,3,4” pitfall | Also: [Gridlines](/configuration/gridlines) · [Number formats](/configuration/number-formats) · [Orientation](/concepts/orientation) *** ## Samples * [Currency ticks](/samples/currency-ticks) – format + preferred interval + title * [Dual-axis line](/samples/dual-axis-line) – secondary + `boundSeries` * [Percent axis](/samples/percent-axis) – pinned 0–100 `%` ticks * [Horizontal bar](/samples/horizontal-bar) – orientation remap (format the **bottom** range) --- --- url: https://chartbuddy.io/embed/docs/axes/value-axis.md --- # Value axis The **primary range** (and secondary range) axes are linear value scales. On a vertical chart that is usually `axes.left`; on a horizontal chart it is `axes.bottom`. Always write to the **side that holds `id: 'primaryRange'`** (or `'secondaryRange'`) after orientation remap – see [Orientation](/concepts/orientation). ```js axes: { left: { id: 'primaryRange', labels: { preferredTickInterval: null, // auto customMin: null, customMax: null, numberFormat: { mode: 'custom', customFormat: { prefix: '', postfix: '', thousandSeparator: ',', decimalSeparator: '.', decimalPlaces: 0, forceDecimals: true, signDisplay: 'auto', scaleDown: 0, }, }, }, }, } ``` Full format field reference: [Number formats](/configuration/number-formats). *** ## Two modes: interval vs bounds Ticks and bounds are **mutually exclusive drivers**. Change one; do not fight both at once. | | Interval / auto mode | Bounds mode | |---|---|---| | Trigger | Leave min/max `null`; optionally set `preferredTickInterval` | Set `customMin` and/or `customMax` | | Bounds | Auto-round to multiples of the chosen interval | Fixed to your min/max | | Interval | Prefers your `preferredTickInterval` when allowed | Auto-picked so ticks land on the bounds | If you pin bounds **and** a preferred interval that cannot divide the span, the engine adjusts the interval (or ignores the preference). Chasing both by hand causes a feedback loop – pick one mode. *** ## Preferred interval `preferredTickInterval` is a **step size** (e.g. `10`, `25`, `50`), not “number of labels.” ```js axes: { left: { labels: { preferredTickInterval: 25, }, }, } ``` Leave it `null` for new charts so auto sparse selection runs from the space budget: | Space for (max labels) | Aim for | |---|---| | 1–5 | ~2 labels | | 6–13 | ~3 | | 14–20 | ~4 | | 21+ | ~5 | Nice candidates are values like 1, 2, 2.5, 5, 10 × powers of 10. **Do not hand-author `customTickInterval`.** The engine writes that field when resolving ticks. Authors set `preferredTickInterval` (or leave null). ### Percentage charts For `stackedBar100`, `stackedArea100`, and mekko value axes, allowed intervals are filtered to **divisors of 100** (1, 2, 2.5, 4, 5, 10, 20, 25, 50, 100). Prefer `20` / `25` / `50` for clean decks. Defaults already seed `postfix: '%'`. See [Percent axis sample](/samples/percent-axis). *** ## Bounds ```js axes: { left: { labels: { customMin: 0, customMax: 200, }, }, } ``` * `null` / omit – auto from data (+ nice rounding in interval mode) * Set one or both – bounds mode ### Zero rules | Chart family | Guidance | |---|---| | Clustered / stacked bar, stacked area, waterfall | Usually keep **0 in range**. Cutting off zero exaggerates differences. | | Line, scatter | Setting the range with `customMin` / `customMax` around the data is often right – zero is not required. | | 100% / mekko value | Pin **0–100** (engine and defaults already bias this way). | If `customMin` sits **above** the lowest data point (or `customMax` below the highest), bars/lines clip. Fix the bound or widen it. ### Recipe: zoomed line ```js { chartType: 'line', isDataTransposed: true, seriesData: [/* … */], axes: { left: { labels: { customMin: 80, customMax: 120, preferredTickInterval: 10, }, }, }, } ``` ### Recipe: currency ticks ```js axes: { left: { labels: { preferredTickInterval: 50, numberFormat: { mode: 'custom', customFormat: { prefix: '$', postfix: '', thousandSeparator: ',', decimalSeparator: '.', decimalPlaces: 0, forceDecimals: false, signDisplay: 'auto', scaleDown: 0, }, }, }, axisTitle: { visible: true, text: 'USD millions', }, }, } ``` Live: [Currency ticks sample](/samples/currency-ticks). *** ## Horizontal charts After `orientation: 'horizontal'`, the **primary range is on `bottom`**. Put formats, preferred intervals, bounds, and value gridlines there – not on `left` (that side is domain / categories). ```js { orientation: 'horizontal', axes: { bottom: { labels: { preferredTickInterval: 20, numberFormat: { mode: 'custom', customFormat: { prefix: '', postfix: '%', decimalPlaces: 0, forceDecimals: false, thousandSeparator: ',', decimalSeparator: '.', signDisplay: 'auto', scaleDown: 0, }, }, }, gridlines: { visible: true }, }, }, } ``` *** ## Gridlines Default for new charts: gridlines **`visible: false`**. Turn them on on the value axis side and pair overrides with a fixed `preferredTickInterval` so tick keys stay stable – [Gridlines](/configuration/gridlines). *** ## Pitfalls * Authoring `customTickInterval` instead of `preferredTickInterval` * Formatting `left` after flipping to horizontal (range moved to `bottom`) * Pinning bounds that clip data * Using dual custom min/max **and** an incompatible preferred interval * Expecting `chartData` number format alone to change PNG/slide look – formats are on the axis labels (and linked surfaces) Next: [Domain axis](/axes/domain) · [Secondary](/axes/secondary) · [By chart type](/axes/by-chart-type) --- --- url: https://chartbuddy.io/embed/docs/axes/domain.md --- # Domain axis The **domain** axis carries categories (most charts) or a linear X domain (scatter / bubble; mekko / bar mekko use a special width domain). On a vertical chart it is usually `axes.bottom`; on a horizontal chart it is `axes.left`. ```js axes: { bottom: { id: 'domain', labels: { visible: true, fitMechanism: 'wrap', // 'wrap' | 'rotation' | 'alternating' | 'none' wrapMaxLines: 5, position: 'bottom', // often mirrors the side; 'auto' | 'bottom' | 'top' | 'chart-top' dateFormat: { treatAsDate: false, format: 'MMM D, YYYY', }, }, }, } ``` Domain axes **cannot be hidden** as a role (range axes can). You can still hide **labels** with `labels.visible: false`. *** ## Fit mechanisms When labels collide, ChartBuddy applies the configured fit strategy: | `fitMechanism` | Behavior | Best for | |---|---|---| | `'wrap'` (default) | Multi-line labels within the category column | Short phrases, vertical charts | | `'rotation'` | Tilts labels (~−25° to −60°) | Many dense categories | | `'alternating'` | Staggers labels on two (or more) rows | Medium crowding without rotation | | `'none'` | No fitting – labels stay single-line horizontal | Few short labels, or when you control spacing | If there is no overlap, labels stay horizontal even when a fit mechanism is set. ### Horizontal charts Category labels sit on the **left** and need **height**. Prefer wrap or fewer categories; rotation eats horizontal plot width. Give the mount enough height (e.g. 480px+) – see [Orientation](/concepts/orientation). ```js { orientation: 'horizontal', axes: { left: { labels: { fitMechanism: 'wrap', wrapMaxLines: 3, }, }, }, } ``` *** ## Category dates Category values that should format as dates use **`dateFormat`**, not `numberFormat`: ```js axes: { bottom: { labels: { dateFormat: { treatAsDate: true, format: 'MMM YYYY', }, }, }, } ``` `format` is a date pattern string (e.g. `'MMM D, YYYY'`). Leave `treatAsDate: false` for ordinary category strings. *** ## Mekko / bar Mekko domain Mekko and bar Mekko use a **linear 0–100 width domain** (column thickness), not a simple band of equal categories. Defaults seed mekko domain ticks with `%` postfix and `preferredTickInterval: 20`. Prefer exporting a finished mekko from the editor when authoring advanced domain chrome. *** ## Scatter / bubble Scatter and bubble use **linear** scales on both domain and range (runtime forces linear domain even if a band default was seeded). Format both sides with `numberFormat` as needed; category `fitMechanism` / `dateFormat` usually do not apply. *** ## Pitfalls * Crowding labels then blaming the value axis – fix `fitMechanism` or reduce categories * Horizontal charts with tall wrapped labels and a short container * Using `numberFormat` for calendar strings – use `dateFormat.treatAsDate` * Hand-authoring mekko domain from scratch – start from `getChartData()` after editing Next: [Value axis](/axes/value-axis) · [Titles & chrome](/axes/titles-and-chrome) --- --- url: https://chartbuddy.io/embed/docs/axes/titles-and-chrome.md --- # Titles & chrome Axis chrome is everything that is not the tick values themselves: titles, axis line, tick marks, and label visibility. *** ## Axis titles Each side has an `axisTitle` block (same multiline text shape as chart title furniture). Defaults are **`visible: false`**. ```js axes: { left: { axisTitle: { visible: true, text: 'USD millions', fontSize: 11, }, }, } ``` `text` accepts plain strings or inline HTML (``, ``, …) like [title / subtitle](/configuration/title-subtitle-footnote). ### Rotation Rotation follows the **physical side**: | Side | Default title rotation | |---|---| | `left` | −90° (reads up the axis) | | `right` | +90° | | `top` / `bottom` | 0° (upright) | Horizontal charts put the value title on **`bottom`** (upright) and the category title on **`left`** (rotated). You usually do not need to set `rotation` by hand. ### When to use a title vs a format * Prefer **`numberFormat` prefix/postfix** for `$`, `%`, `×` on tick labels * Use **`axisTitle`** for longer unit lines (“USD millions, FY25”) that would clutter every tick *** ## Visibility | Field | Role | |---|---| | `enabled` | Axis participates in layout / scales (secondary starts `false`) | | `visible` | Axis line (and chrome) drawn | | `labels.visible` | Tick / category labels | Range axes (`primaryRange`, `secondaryRange`) can be hidden as a role. The **domain** axis cannot be removed as a role – hide labels instead if needed. Bar Mekko defaults `primaryRange.visible: false` (totals and domain labels without a value axis line). Set `visible: true` to show the value axis line. *** ## Tick marks & axis line ```js axes: { left: { axisColor: '#666666', axisWidth: 1, tickMarks: { visible: true }, // primary/secondary default true; domain default false }, } ``` | Field | Notes | |---|---| | `axisColor` / `axisWidth` | Axis line stroke | | `tickMarks.visible` | Small ticks at label positions | *** ## Label fonts Under `labels`: ```js labels: { visible: true, fontSize: 12, fontFamily: 'Arial', fontColor: 'black', distanceFromAxis: 8, } ``` House-style tools may rewrite face/color inside `axisTitle.text` HTML when applying brand fonts. *** ## Recipe: titled currency axis ```js axes: { left: { labels: { preferredTickInterval: 50, numberFormat: { mode: 'custom', customFormat: { prefix: '$', postfix: '', decimalPlaces: 0, forceDecimals: false, thousandSeparator: ',', decimalSeparator: '.', signDisplay: 'auto', scaleDown: 0, }, }, }, axisTitle: { visible: true, text: 'Revenue' }, gridlines: { visible: true, dashArray: '' }, }, } ``` See [Currency ticks sample](/samples/currency-ticks). --- --- url: https://chartbuddy.io/embed/docs/axes/secondary.md --- # Secondary / dual axis Use a secondary value axis when two series need **independent scales** (e.g. revenue in dollars vs growth in %). Same units on two axes is usually harder to read; prefer one axis when you can. Secondary sits on: | Orientation | Secondary side | |---|---| | `vertical` | `right` | | `horizontal` | **`top`** | *** ## Config shape ```js axes: { right: { id: 'secondaryRange', enabled: true, visible: true, boundSeries: [1], // original series indices (0-based) labels: { preferredTickInterval: 10, numberFormat: { mode: 'custom', customFormat: { prefix: '', postfix: '%', decimalPlaces: 0, forceDecimals: false, thousandSeparator: ',', decimalSeparator: '.', signDisplay: 'auto', scaleDown: 0, }, }, }, axisTitle: { visible: true, text: 'Growth' }, }, } ``` | Field | Role | |---|---| | `enabled` / `visible` | Turn the secondary scale and chrome on | | `boundSeries` | Which **original** series indices use this scale (not filtered/display order) | | `labels.*` | Independent ticks, bounds, and formats from the primary range | | `breaks` | Supported on secondary the same way as primary – see [Breaks](/axes/breaks) | Series **not** listed in `boundSeries` stay on the primary range. Primary bounds **exclude** secondary-bound series so each scale fits its own data. *** ## Line charts Dual-axis is fully supported on **line** charts: ```js { chartType: 'line', isDataTransposed: true, seriesData: [ ['', 'Q1', 'Q2', 'Q3', 'Q4'], ['Revenue', 120, 132, 141, 155], ['Margin %', 18, 19, 17, 21], ], axes: { left: { axisTitle: { visible: true, text: 'Revenue' }, labels: { numberFormat: { mode: 'custom', customFormat: { prefix: '$', postfix: '', decimalPlaces: 0, forceDecimals: false, thousandSeparator: ',', decimalSeparator: '.', signDisplay: 'auto', scaleDown: 0, }, }, }, }, right: { id: 'secondaryRange', enabled: true, visible: true, boundSeries: [1], axisTitle: { visible: true, text: 'Margin %' }, labels: { preferredTickInterval: 5, numberFormat: { mode: 'custom', customFormat: { prefix: '', postfix: '%', decimalPlaces: 0, forceDecimals: false, thousandSeparator: ',', decimalSeparator: '.', signDisplay: 'auto', scaleDown: 0, }, }, }, }, }, } ``` Live: [Dual-axis line sample](/samples/dual-axis-line). Indices in `boundSeries` match **row order in `seriesData`** after the header row – series `0` is the first data row, series `1` the second, and so on. *** ## Horizontal dual axis (secondary on top) With `orientation: 'horizontal'`, put secondary on **`top`**: ```js { orientation: 'horizontal', chartType: 'line', axes: { bottom: { /* primaryRange – values */ }, top: { id: 'secondaryRange', enabled: true, visible: true, boundSeries: [1], }, left: { /* domain – categories */ }, }, } ``` Give the chart enough height: domain labels on the left plus a top value axis need vertical room. *** ## Combo charts Combo can bind series to the secondary axis the same way (`boundSeries` + `enabled` / `visible`). Pair with `combo.seriesTypes` so the secondary series renders as a line (or whatever type you need): ```js { chartType: 'combo', isDataTransposed: true, seriesData: [ ['', 'Q1', 'Q2', 'Q3', 'Q4'], ['Units', 40, 48, 52, 61], ['ASP', 12, 11, 13, 14], ], combo: { seriesTypes: { 1: 'line' }, }, axes: { right: { id: 'secondaryRange', enabled: true, visible: true, boundSeries: [1], labels: { numberFormat: { mode: 'custom', customFormat: { prefix: '$', postfix: '', decimalPlaces: 0, forceDecimals: false, thousandSeparator: ',', decimalSeparator: '.', signDisplay: 'auto', scaleDown: 0, }, }, }, }, }, } ``` Bars that stay on the primary axis still share one primary scale. Prefer exporting from the editor if you are unsure of series indices after reordering. *** ## Anti-patterns * Two axes for the **same** unit (readers compare the wrong scale) * Forgetting `enabled: true` **and** `visible: true` * Wrong `boundSeries` index after inserting/reordering series rows * Putting secondary on `right` after flipping to horizontal (it moves to `top`) Next: [Value axis](/axes/value-axis) · [Breaks](/axes/breaks) · [By chart type](/axes/by-chart-type) --- --- url: https://chartbuddy.io/embed/docs/axes/breaks.md --- # Axis breaks An **axis break** skips a span on a **linear range** axis (for outliers or empty bands). Breaks live on `axes..breaks` for `primaryRange` or `secondaryRange` (not on the domain axis). ```js axes: { left: { id: 'primaryRange', breaks: [ { id: 'break-1', domainStart: 40, domainEnd: 180, gapPixels: 4, // 3–12; default 4 coverageMode: 'local', // 'local' | 'fullWidth' // style: 'wave' | 'slashPair' (optional) }, ], }, } ``` | Field | Role | |---|---| | `id` | Stable id for update/remove | | `domainStart` / `domainEnd` | Skipped value range (`start < end`) | | `gapPixels` | Visual gap height/width in px (clamped ~3–12) | | `coverageMode` | `'local'` – break marks near data; `'fullWidth'` – band across the plot. Default: `'fullWidth'` on **line**, `'local'` on bar-like types | | `style` | Optional break stroke variant (`wave`, `slashPair`, …) | Defaults seed `breaks: []` (no breaks). *** ## When to use * One outlier column dwarfs the rest * A long empty band between clusters of values * You need readable ticks on both sides of a gap Avoid breaks when the full magnitude matters (for example one bar is 10× the others). A break can hide that. *** ## Recipe: collapse a gap on a bar chart ```js { chartType: 'clusteredBar', isDataTransposed: true, seriesData: [ ['', 'A', 'B', 'C', 'D'], ['Value', 12, 15, 220, 18], ], axes: { left: { breaks: [ { id: 'skip-mid', domainStart: 40, domainEnd: 200, gapPixels: 6, coverageMode: 'local', }, ], labels: { preferredTickInterval: 10 }, }, }, } ``` *** ## Authoring tips * Prefer building the break in the editor (drag handles), then `getChartData()` – geometry is easier to judge visually * Keep `domainStart` / `domainEnd` **inside** the visible scale; breaks outside the domain are ignored or clamped * Pair with a sensible `preferredTickInterval` so labels still land cleanly on both sides of the gap * Secondary axis breaks use the same shape on the secondary side (`right` or `top`) *** ## Pitfalls * Break that overlaps real data you still need to compare linearly * `gapPixels` outside 3–12 (clamped) * Expecting breaks on band/category domains – range axes only Next: [Value axis](/axes/value-axis) · [Secondary](/axes/secondary) --- --- url: https://chartbuddy.io/embed/docs/axes/by-chart-type.md --- # Axes by chart type What each chart type does with axes. Always confirm sides after [orientation](/concepts/orientation) remap. | Chart type | Domain | Primary range | Secondary | Notes | |---|---|---|---|---| | `clusteredBar` / `stackedBar` | Band categories | Linear values | Optional (rare) | Keep **0 in range** for bars | | `stackedBar100` | Band | Fixed **0–100**, `%` ticks | – | Intervals divisors of 100 | | `line` | Band | Linear | Fully supported | Set range with `customMin` / `customMax`; secondary via `boundSeries` | | `stackedArea` / `stackedArea100` | Band | Linear / 0–100 `%` | – | Area usually includes zero | | `pie` / `donut` | Disabled | Disabled | – | No axes | | `scatter` / `bubble` | **Linear** X | Linear Y | – | Runtime forces linear domain | | `waterfall` | Band | Linear | – | Keep zero in play for bridges | | `mekko` | Linear **0–100** width | Linear **0–100** `%` stacks | – | Domain + range both %-ish | | `barMekko` | Linear width domain | Often **`visible: false`** | – | Totals + domain labels are enough without the value axis line | | `combo` | Band | Linear | Supported with `boundSeries` | Pair with `combo.seriesTypes` | *** ## Detail ### Bars & waterfalls Value axis on left (vertical) or bottom (horizontal). Format + `preferredTickInterval` + optional gridlines. See [Value axis](/axes/value-axis) and [Horizontal bar sample](/samples/horizontal-bar). ### 100% stacks & mekko Prefer `preferredTickInterval: 20|25|50`, bounds 0–100, `postfix: '%'`. [Percent axis sample](/samples/percent-axis). ### Line + dual axis [Secondary](/axes/secondary) · [Dual-axis line sample](/samples/dual-axis-line). ### Scatter / bubble Format both linear sides; ignore category `fitMechanism` / `dateFormat` unless you intentionally treat labels as categories (you usually do not). ### Combo Bars share the primary scale; bind the line (or other) series to secondary when units differ. [Combo](/chart-types/combo) · [Secondary](/axes/secondary). ### Pie Do not author `axes` for layout – they are disabled. Labels live in the pie config / data labels. *** ## Hide / show Only **range** roles can be hidden as axes (`primaryRange`, `secondaryRange`). Domain cannot. Bar Mekko hiding the primary line is a product default, not a missing axis. --- --- url: https://chartbuddy.io/embed/docs/api.md --- # Insight API ```js import { Insight, version, } from '@chartbuddy.io/embed'; ``` ## Constructor ```js const insight = new Insight(target, options?); ``` ### `target` CSS selector string or `HTMLElement`. ### `options` | Option | Type | Default | Description | |---|---|---|---| | `chartData` | `object` | – | Partial or full chart config ([schema](/api/chart-data)) | | `instanceId` | `string` | random UUID | Stable id for `getInsights()`. Must be unique on the page — duplicates **throw**. | | `editable` | `boolean` | `false` | Start in editor mode | | `allowEdit` | `boolean` | `true` | When `false`, hide Edit on the view ball and no-op `enterEditMode()`. Download / Drag still work. Ignored when locked. | | `editSession` | `'toggle'` | `'locked'` | `'toggle'` | `'locked'` = always edit (no Done / morph chrome). Implies edit boot. | | `host` | `object` | — | Host chrome hooks — see [Host overrides](#host-overrides) | | `assetBase` | `string` | — | Only for multi-file loader; ignore with single-file | ## Instance | Member | Description | |---|---| | `ready` | `Promise` — resolves when mounted | | `mode` | `'view'` | `'edit'` | | `editSession` | `'toggle'` | `'locked'` | | `allowEdit` | `boolean` — whether Edit is offered | | `chart` | Engine chart instance | | `instanceId` | Stable id for this mount | | `setChartData(cd)` | Merge a full or **partial** config and redraw (`chartType` optional) | | `setData(seriesData)` | Refresh only the data grid — keeps type and formatting | | `update(patch?)` | Partial patch + redraw; no argument = redraw only | | `getChartData()` | Full `cd` snapshot | | `on(event, handler)` | Subscribe to `ready` | `mode` | `change` (returns unsubscribe) | | `off(event, handler)` | Remove a handler | | `isDirty()` | `true` when the chart changed since boot / last Done | | `getRevision()` | Monotonic edit counter | | `toPngBlob()` / `toPngBase64()` | PNG bytes/base64 without a Save dialog | | `downloadPng()` | Download PNG (human Save dialog) | | `exportConfig()` | Download current `chartData` as JSON | | `enterEditMode()` | View → edit (no-op when locked or `allowEdit: false`) | | `exitEditMode()` | Edit → view (no-op when locked) | | `focus()` | Focus the insight | | `destroy()` | Tear down | ## Host overrides Optional `options.host` hooks for embedding inside your own chrome: | Hook | Description | |---|---| | `getToolbarPlacement()` | Where the formatting toolbar lives: `'widget'` (default embed edit — morph rail), `'float'`, or `'dock'`. | | `getToolbarContainer()` | Mount point for the toolbar (selector or element). Ignored when placement is `'widget'` (modules mount in the morph). | | `positionToolbar(toolbar, target)` | Float-only XY override. Ignored for `'dock'` / `'widget'`. | | `getPopupContainer()` | Where portaled menus mount (selector or element). Default: `document.body`. Use your dialog root when the editor sits in a high-z overlay. | | `startDragging(event)` | Forward chart-background drag to host window chrome. | ```js new Insight('#chart', { editable: true, editSession: 'locked', host: { getToolbarPlacement: () => ({ mode: 'dock', side: 'right' }), getToolbarContainer: () => '#my-toolbar-rail', getPopupContainer: () => '#my-dialog', }, }); ``` ## Partial updates ```js await insight.ready; // Data only — Chart.js-shaped insight.setData([ ['', 'Q1', 'Q2'], ['Revenue', 100, 120], ]); // Any partial patch insight.update({ title: { text: 'FY26' } }); // Redraw without changing config insight.update(); // setChartData also accepts partials — chartType is not required insight.setChartData({ seriesData: [/* … */] }); ``` ## Events ```js insight.on('ready', () => { /* booted */ }); insight.on('mode', (mode) => { /* 'view' | 'edit' */ }); insight.on('change', (cd) => { /* after meaningful edits */ }); insight.isDirty(); // since boot / last Done checkpoint insight.getRevision(); // increments on every meaningful change ``` `change` fires after programmatic `setChartData` / `setData` / `update`, live edits in edit mode, and Done. ## Validation `new Insight({ chartData })`, `setChartData`, `setData`, and `update` validate input at the API boundary: | Input | Behaviour | |---|---| | Unknown `chartType` (e.g. `bubbleChart3D`) | **Throws** (with a did-you-mean hint) | | Wrong field type / enum / range | **Throws** (lists every path) | | Foreign option bag (`pie` + `bar: {…}`) | **Throws** | | Ragged `seriesData` (unequal row lengths) | **Warns**, still accepts | | Duplicate `instanceId` on the page | **Throws** | | Unknown keys | Always allowed (forward-compatible) | Throws are `ChartDataValidationError`, which carries the problems as structured data — and the same checks are callable directly, so you can validate before you mount. See [Validation](/quality-assurance/validation). ```js import { validateChartData } from '@chartbuddy.io/embed'; const { valid, errors } = validateChartData(candidate); if (!valid) console.log(errors[0].path, errors[0].code, errors[0].suggestion); ``` ## PNG / export ```js await insight.ready; const png = await insight.toPngBase64(); // default background #ffffff await insight.downloadPng(); insight.exportConfig(); ``` PNG download and drag-to-slide are also available from the view-mode hover ball. --- --- url: https://chartbuddy.io/embed/docs/api/chart-data.md --- # chartData schema `chartData` (also called `cd`) is the serializable chart document. You pass it into `new Insight()`, `setChartData()`, and `update()`. You read the resolved form with `getChartData()`. Two shapes matter: | Shape | When | Notes | |---|---|---| | **Input** (`ChartDataInput`) | What you author or patch | Partial OK. `donut` / `bubble` accepted. Merged over defaults. | | **Resolved** (`ChartData`) | What `getChartData()` returns | Resolved `chartType`. Full trees seeded. Safe to round-trip into `setChartData()`. | Machine-readable rules (same registry the runtime uses): [`chart-schema.json`](https://unpkg.com/@chartbuddy.io/embed/chart-schema.json). *** ## Minimal ```js { chartType: 'clusteredBar', isDataTransposed: true, seriesData: [ ['', 'Q1', 'Q2', 'Q3'], ['Revenue', 100, 112, 125], ], } ``` `chartType` + `seriesData` are enough to mount. Everything else deep-merges from defaults (or stays unset until you need it). *** ## Recommended shell ```js { chartType: 'clusteredBar', isDataTransposed: true, seriesData: [/* … */], orientation: 'vertical', // or 'horizontal' title: { visible: true, text: 'Revenue' }, subtitle: { visible: false, text: '' }, // hide default placeholder legend: { visible: true, colors: ['#2563eb', '#64748b'] }, backgroundColor: '#ffffff', } ``` Add `axes`, `annotations`, type bags (`bar`, `line`, …), and furniture only when you need them – or export a full tree from the editor and trim. *** ## Top-level map | Field | Role | See | |---|---|---| | `chartType` | Chart type id (`donut` / `bubble` also accepted) | [Chart types](/chart-types/) · below | | `seriesData` | 2D grid (layout depends on type) | below | | `isDataTransposed` | Rows = series when `true` (default for most types) | below | | `orientation` | `'vertical'` | `'horizontal'` | [Orientation](/concepts/orientation) · [Axes](/axes/) | | `title` / `subtitle` / `footnote` | Furniture text blocks | [Title, subtitle & footnote](/configuration/title-subtitle-footnote) | | `legend` | Visibility, placement, **`colors` palette** | [Legend](/configuration/legend) | | `backgroundColor` | Chart underlay (PNG export still needs its own `background` option) | [Visual QA](/quality-assurance/visual-qa) | | `axes` | Sides, roles, ticks, bounds, formats, breaks | [Axes](/axes/) | | `annotations` | Arrows, level lines, totals, data-label formats | [Arrows](/configuration/arrows) · [Level lines](/configuration/level-lines) · [Number formats](/configuration/number-formats) | | `multilines` | Free text boxes | [Text boxes](/configuration/text-boxes) | | `canvas` | Internal width/height snapshot | [Canvas & sizing](/concepts/canvas) | | `bar` / `line` / `area` / `pie` / `scatter` / `waterfall` / `mekko` / `barMekko` / `combo` | Type-specific option bags | [Chart types](/chart-types/) · below | | `seriesLabels` | Per-series / per-point label chrome | full export | | `chartPositionPercentages` | Plot margins inside the canvas | full export | | `id` / `version` | Document identity / version | runtime | Unknown top-level keys are **allowed** (forward-compatible). Wrong types, foreign option bags, and unknown `chartType` values are **rejected**. *** ## chartType Resolved values (`cd.chartType` from `getChartData()`): `clusteredBar` · `stackedBar` · `stackedBar100` · `line` · `stackedArea` · `stackedArea100` · `pie` · `scatter` · `waterfall` · `mekko` · `barMekko` · `combo` ### Donut & bubble `donut` and `bubble` are shortcuts that seed defaults so the chart looks like what you asked for: | You pass | Resolves to | Seeded defaults | |---|---|---| | `donut` | `pie` | `pie.innerRadiusRatio: 0.5` | | `pie` | `pie` | `pie.innerRadiusRatio: 0` | | `bubble` | `scatter` | larger point diameter | ```js { chartType: 'donut', seriesData: [['Category', 'Value'], ['North', 45]] } ``` Do not also set `innerRadiusRatio` unless you want a different hole. `getChartData()` returns `pie` with `innerRadiusRatio: 0.5`. That round-trips correctly. *** ## seriesData layouts Layout is fixed by `chartType` (see the registry / `chart-schema.json`). Cells are `string | number | boolean | null`. ### Bar / line / area / stacked / waterfall / mekko / combo (`seriesRows`) With `isDataTransposed: true` (usual): row 0 is the category header; later rows are series. ```js [ ['', 'Q1', 'Q2', 'Q3'], ['Revenue', 100, 112, 125], ['Costs', 60, 66, 70], ] ``` Minimum: 2 rows × 2 columns (header included). ### Pie / donut (`categoryValue`) ```js [ ['Category', 'Value'], ['North', 45], ['South', 30], ] ``` ### Scatter / bubble (`pointRows`) No transpose. Row 0 names metrics; later rows are points (`x`, `y`, optional `size`, optional `group`). ```js [ ['', 'Metric X', 'Metric Y', 'Size', 'Group'], ['Point 1', 10, 15, 8, 'A'], ] ``` Minimum: 2 rows × 3 columns. ### Bar Mekko (`widthRowThenHeightRows`) Row 1 after the header sets **widths** (not drawn as a series). Later rows stack as **heights** on a real value axis. See [Bar Mekko](/chart-types/bar-mekko). ```js [ ['', 'Enterprise', 'Mid-market', 'SMB'], ['Accounts', 120, 85, 200], // width ['Core revenue', 48, 16, 8], // height ['Add-ons', 14, 12, 10], ] ``` Minimum: 3 rows × 2 columns. ### Ragged grids Unequal row lengths **warn** and still load. Pad short rows when you care about clean columns. *** ## isDataTransposed | Value | Meaning | |---|---| | `true` (typical) | Rows after the header are series; columns are categories | | `false` | Opposite spreadsheet orientation | Scatter / bubble ignore transpose (`pointRows`). When unsure, keep `true` for bar/line/area/waterfall/combo and match the samples. *** ## Type-specific option bags Each chart type reads **one** bag: | Types | Bag | |---|---| | `clusteredBar`, `stackedBar`, `stackedBar100` | `bar` | | `line` | `line` | | `stackedArea`, `stackedArea100` | `area` | | `pie`, `donut` | `pie` | | `scatter`, `bubble` | `scatter` | | `waterfall` | `waterfall` | | `mekko` | `mekko` | | `barMekko` | `barMekko` | | `combo` | `combo` | Putting options in the wrong bag (e.g. `chartType: 'pie'` with `bar: {…}`) **throws** (`foreign-option-bag`). Resolved snapshots from `getChartData()` may carry every bag (defaults seed them); that is normal when round-tripping. Examples: ```js { chartType: 'donut', pie: { innerRadiusRatio: 0.7 } } { chartType: 'combo', combo: { seriesTypes: { 0: 'bar', 1: 'line' } } } { chartType: 'barMekko', barMekko: { sort: 'heightDesc' } } ``` Per-type fields and recipes: [Chart types](/chart-types/). *** ## Waterfall: do not invent totals Total columns are computed. Mark the column and leave its cell empty. A value typed into an `isTotal` column is ignored. A closing figure typed **without** `isTotal` becomes another contribution bar (the bridge then ends near ~2×). ```js { chartType: 'waterfall', isDataTransposed: true, seriesData: [ ['', 'Start', 'Price', 'Volume', 'Mix', 'End'], // ^0 ^1 ^2 ^3 ^4 <- waterfall.columns keys ['Bridge', 100, 18, -8, 5, null], // End carries NO value ], waterfall: { columns: { 4: { isTotal: true }, // closing bar = 115 }, }, } ``` | Rule | Detail | |---|---| | Column keys | 0-based **data-column** indices: header cells **after** the row-label cell. `'End'` is header index 5 but data column **4**. | | `isTotal: true` | Show running total; ignore this column's own value | | `startBar: true` | Reset to zero and drop the connector (new sequence). A mid-chart opening bar needs **both** `isTotal` and `startBar`. | | `showSegments: true` | Draw a total as per-series segments instead of a solid bar | | Column 0 | Always forced to `isTotal` + `startBar`; unlike other totals it **does** use its own value (opening balance) | Full type page: [Waterfall](/chart-types/waterfall). *** ## Partial patches and merging ```js insight.setData(seriesData); // data only insight.update({ title: { text: 'FY26' } }); // any partial insight.setChartData({ seriesData: […] }); // chartType optional insight.update(); // redraw only ``` | Behaviour | Detail | |---|---| | Omit `chartType` | Keeps the current type | | Deep-merge keys | `title`, `subtitle`, `footnote`, `legend`, `axes`, `canvas`, `annotations` | | Other top-level keys | Typically **replace** (including arrays such as `multilines`) | | Empty / useless patch | Rejected (`empty-patch`) when there is nothing to apply | See [Defaults & merging](/concepts/defaults). *** ## Validation `new Insight({ chartData })`, `setChartData`, `setData`, and `update` validate at the API boundary and **throw** `ChartDataValidationError` with structured issues. | Input | Behaviour | |---|---| | Unknown `chartType` | Throws (did-you-mean / `allowed`) | | Wrong type / enum / range | Throws (lists every path) | | Foreign option bag | Throws | | Ragged `seriesData` | Warns, still accepts | | Unknown keys | Always allowed | Prefer checking before mount: ```js import { validateChartData } from '@chartbuddy.io/embed'; const { valid, errors, warnings } = validateChartData(candidate); // issue: { path, code, message, severity, expected?, received?, allowed?, suggestion? } ``` Branch on `code`, not `message` (messages may change): | Code | Fix | |---|---| | `unknown-chart-type` / `not-in-enum` | Use `suggestion` or pick from `allowed` | | `wrong-type` | Coerce to `expected` | | `out-of-range` | Clamp to the bound in `expected` | | `series-data-shape` | Reshape; `expected` names row/column minimums | | `ragged-series-data` | Warning – pad short rows | | `foreign-option-bag` | Move options into the bag named in `suggestion` | | `empty-patch` | Include `chartType` and/or `seriesData` | Full reference: [Validation](/quality-assurance/validation). *** ## Full document workflow Hand-authoring every axis tick and annotation is painful. Prefer: 1. Mount with a minimal or recommended shell 2. Polish in edit mode (`editable: true`) 3. Snapshot: ```js await insight.ready; const full = insight.getChartData(); // or insight.exportConfig() for a JSON download ``` Expect trees such as `canvas`, `axes`, `annotations`, `multilines`, type bags, `seriesLabels`, and `chartPositionPercentages`. Round-trip that object with `setChartData(full)` when you need a complete starting point, then patch. *** ## Pitfalls * Generating a chart type that is not in the registry (`bubbleChart3D`) – validate first * Putting `bar` options on a `pie` (foreign bag) * Wrong `seriesData` layout for the type (pie as series-rows, scatter transposed) * Waterfall closing value without `isTotal` (double-counts the bridge) * Wrong waterfall column index (header position vs data-column index) * Expecting `backgroundColor` alone to appear in PNG export – pass `toPngBase64({ background })` * Expecting `getChartData()` to keep `chartType: 'donut'` / `'bubble'` — it returns `pie` / `scatter` with the seeded defaults * Deep-merging arrays by hand – `multilines` and similar usually **replace** *** ## Related * [Data model](/concepts/data-model) – layouts overview * [Defaults & merging](/concepts/defaults) * [Insight API](/api/) – mount / update / events * [Visual QA](/quality-assurance/visual-qa) – observe after mount * Package index: [llms.txt](https://unpkg.com/@chartbuddy.io/embed/llms.txt) · schema: [chart-schema.json](https://unpkg.com/@chartbuddy.io/embed/chart-schema.json) --- --- url: https://chartbuddy.io/embed/docs/api/helpers.md --- # Helpers ```js import { getInsights, snapshotInsights, version, } from '@chartbuddy.io/embed'; getInsights(); // { [instanceId]: Insight } // Also mirrored at window.__CHARTBUDDY_INSIGHTS__ await snapshotInsights(); // config-only for all mounts await snapshotInsights({ png: true }); // + pngBase64 (default white bg) await snapshotInsights({ png: true, background: '#0f172a' }); console.log(version); // e.g. '1.7.50' ``` ## Validation ```js import { validateChartData, assertValidChartData, formatValidationIssues, ChartDataValidationError, } from '@chartbuddy.io/embed'; validateChartData(candidate); // → { valid, issues, errors, warnings } validateChartData(cd, { form: 'resolved', requireSomething: false, maxIssues: 5 }); assertValidChartData(candidate, '[my-app]'); // throws ChartDataValidationError formatValidationIssues(result.issues); // human-readable multi-line string ``` Full reference: [Validation](/quality-assurance/validation). ## Framework bindings ```js import { InsightChart, useInsight } from '@chartbuddy.io/embed/react'; import { InsightChart, useInsight } from '@chartbuddy.io/embed/vue'; import '@chartbuddy.io/embed/element'; // — Angular & anything else ``` Guides: [React](/guides/react) · [Vue](/guides/vue) · [Angular & web components](/guides/angular). On each `Insight` instance: ```js await insight.toPngBase64(); // default background #ffffff await insight.toPngBase64({ background: '#0f172a' }); // custom opaque underlay await insight.toPngBase64({ background: null }); // transparent await insight.toPngBlob({ background: '#ffffff' }); insight.getChartData(); insight.setData(seriesData); // data-only refresh insight.update(patch); // partial patch insight.on('change', (cd) => { /* … */ }); insight.isDirty(); insight.downloadPng(); // human Save; transparent default insight.downloadPng({ background: '#ffffff' }); // opaque human download ``` --- --- url: https://chartbuddy.io/embed/docs/quality-assurance/visual-qa.md --- # Visual QA You cannot judge ChartBuddy charts from `chartData` alone. Mount, look, then adjust. ## Layout first Before judging labels or annotations, confirm the **host box**: * Host has a definite size (fixed px, or `width` + `aspect-ratio` – not bare `height: 100%` in nested/flex/slide layouts). See [Canvas & sizing](/concepts/canvas). * Chart fills the host (not cramped, letterboxed, or tiny in the corner). * Size stays stable when a slide/panel becomes visible (remeasure or remount after layout if needed). Feature-present PNGs are not enough if the container geometry is wrong. ## Loop 1. Mount with a realistic container size (e.g. `800×450` or `aspect-ratio: 16 / 9`) and a stable `instanceId` 2. `await insight.ready` (or `insight.on('ready', …)`) 3. Observe via return values (preferred): ```js const insight = window.__CHARTBUDDY_INSIGHTS__['revenue']; const cd = insight.getChartData(); // Default underlay is #ffffff; pass any CSS color for dark decks, etc. const png = await insight.toPngBase64({ background: '#ffffff' }); ``` 4. Fix config (orientation, subtitle, ticks, container height) with a **partial** update: ```js insight.setData([['', 'Q1'], ['Revenue', 120]]); insight.update({ subtitle: { visible: false, text: '' } }); ``` 5. Write PNG / JSON into the workspace yourself — do not rely on `downloadPng()` Save dialogs Prefer **`toPngBase64` / `getChartData`** over screenshots or `downloadPng()` for automated checks. `chartData.backgroundColor` does **not** bake into PNGs. Use the `background` export option. Unknown `chartType` values throw at the API boundary — treat that as a validation error to fix in `chartData`, not as a blank chart to debug. Pre-check with [`validateChartData`](/quality-assurance/validation) when configs are generated or assembled. ## PNG for slides ```js // Human Save dialog — transparent by default (good for Slides overlays) await insight.downloadPng(); // Opaque white (or any CSS color) when you need a solid slide asset: await insight.downloadPng({ background: '#ffffff' }); // Programmatic path (no Save dialog; defaults to #ffffff): const b64 = await insight.toPngBase64({ background: '#ffffff' }); ``` Or use **Download** on the hover ball. Paste the PNG into PowerPoint or Slides. Prefer ChartBuddy export over DevTools SVG screenshots for final assets. ## Probe ```js insight.chart.cd.chartType insight.chart.cd.orientation insight.chart.cd.axes insight.getChartData() insight.isDirty() ``` For horizontal bars, confirm domain is on the left and primary range on the bottom. ## Fixture hygiene * Unique stable `instanceId` per mount (duplicates throw) * Host CSS: definite width/height (prefer aspect-ratio or fixed px for decks) * `subtitle: { visible: false, text: '' }` unless you want a subtitle * `isDataTransposed: true` for bar / line / stackedArea layouts in the docs * Horizontal bars need `orientation: 'horizontal'` * Prefer `setData` / `update` for refreshes – `chartType` is optional on partials * Run [`validateChartData()`](/quality-assurance/validation) on generated configs before mounting ## Chartbuddy Hub For **advanced visual QA** through Chartbuddy MCP (live Hub windows, `list_charts`, desktop editing), and for connecting charts to other apps, install **[Chartbuddy Hub](https://chartbuddy.io/hub/docs/getting-started/installation)** and follow [Connect MCP](https://chartbuddy.io/hub/docs/connect-mcp/). Embed `toPngBase64` / `getChartData` cover npm and static HTML loops; Hub MCP is the desktop path when you need more than mount-and-snapshot. --- --- url: https://chartbuddy.io/embed/docs/quality-assurance/validation.md --- # Validation Pre-mount checks for `chartData`. For the look-and-export loop, see [Visual QA](/quality-assurance/visual-qa). ChartBuddy validates chart data at runtime, not just at compile time. TypeScript types are erased at build time, and chart data almost always arrives at runtime — parsed JSON, a Sheets range, untyped JSON, or an untyped `setChartData()`. Two ways in: * `new Insight()`, `setChartData()`, `setData()`, and `update()` validate their input and **throw** `ChartDataValidationError` on an error-severity problem. * `validateChartData()` **never throws** and returns every problem it found. Use it to check a config before you mount one. The second is what makes a generate → check → repair loop possible without rendering anything, which matters when configs are generated or assembled at runtime. ## Checking before you mount ```js import { validateChartData, Insight } from '@chartbuddy.io/embed'; const { valid, errors, warnings } = validateChartData(candidate); if (!valid) { // Branch on `code`, fix the value at `path`. for (const issue of errors) { console.log(issue.code, issue.path, issue.expected, issue.received); } } else { new Insight('#chart', { chartData: candidate }); } ``` ## `ValidationResult` | Field | Type | Meaning | |---|---|---| | `valid` | `boolean` | True when there are no **error**-severity issues. Warnings do not invalidate. | | `issues` | `ValidationIssue[]` | Everything found, errors and warnings, in document order. | | `errors` | `ValidationIssue[]` | The error-severity subset — what makes `valid` false. | | `warnings` | `ValidationIssue[]` | Rendered anyway, but probably a bug. | ## `ValidationIssue` | Field | Type | Always present | Meaning | |---|---|---|---| | `path` | `string` | yes | Where the problem is, e.g. `pie.innerRadiusRatio`, `seriesData[2][0]`. Empty string for whole-object problems. | | `code` | `ValidationIssueCode` | yes | Stable classification. **Branch on this.** | | `severity` | `'error' \| 'warning'` | yes | Errors reject at the API boundary; warnings are logged. | | `message` | `string` | yes | For humans. **May be reworded in any release — never parse it.** | | `expected` | `string` | no | What the schema wanted: `'number'`, `'integer'`, `'>= 0'`, `'2D array'`. | | `received` | `string` | no | What arrived: `'string'`, `'null'`, `'1.5'`. | | `allowed` | `string[]` | no | The full legal set, for `not-in-enum` and `unknown-chart-type`. | | `suggestion` | `string` | no | Nearest legal value, when one is close enough to be a likely typo. | Optional fields are **omitted**, not set to `undefined`, so issues serialize cleanly to JSON. ## Issue codes `code` is the stable contract. New codes may be added in a minor release, so treat an unrecognized code as a generic failure rather than crashing. | Code | Severity | Cause | Repair | |---|---|---|---| | `not-an-object` | error | The value is not a chart-data object at all. | Pass an object. | | `unknown-chart-type` | error | `chartType` is not a known type. | Use `suggestion`, or pick from `allowed`. | | `empty-patch` | error | Neither `chartType` nor `seriesData` supplied. | Include at least one, or set `requireSomething: false`. | | `wrong-type` | error | A known field holds the wrong JavaScript type. | Coerce to `expected`. Covers `NaN` / `Infinity`. | | `out-of-range` | error | A numeric field is outside its documented range. | Clamp to the bound in `expected`. | | `not-in-enum` | error | A string field is outside its legal set. | Use `suggestion`, or pick from `allowed`. | | `series-data-shape` | error | `seriesData` is not a usable grid for this chart type. | Reshape — `expected` names the row/column minimum. | | `ragged-series-data` | **warning** | Rows have unequal lengths. | Pad the short rows. The renderer pads for you, but the result is rarely what you meant. | | `foreign-option-bag` | error | An option bag belongs to a different chart type. | Move the options to the bag in `suggestion`. | Unknown keys are **never** an issue at any level. Extra fields are ignored. ## Repairing automatically `suggestion` and `allowed` exist so a fix does not need a second model call: ```js function repair(chartData) { const patched = structuredClone(chartData); for (const issue of validateChartData(patched).errors) { if (issue.suggestion) setAtPath(patched, issue.path, issue.suggestion); } return patched; } ``` `'clusterdBar'` → `suggestion: 'clusteredBar'`; `orientation: 'verical'` → `suggestion: 'vertical'`. ## Catching the throw When you skip the pre-check, the API boundary still stops bad data. The thrown error carries the same structured issues: ```js import { ChartDataValidationError } from '@chartbuddy.io/embed'; try { insight.setChartData(candidate); } catch (err) { if (err instanceof ChartDataValidationError) { console.log(err.errors); // error-severity issues console.log(err.warnings); // logged, did not cause the throw console.log(err.issues); // both JSON.stringify(err); // { name, message, issues } } } ``` Warnings never throw. They go to `console.warn` and the call proceeds. ## Options ```js validateChartData(chartData, { form: 'auto', // 'patch' | 'resolved' | 'auto' requireSomething: true, // demand chartType and/or seriesData maxIssues: 20, // cap the report }); ``` **`form`** decides whether a foreign option bag is a mistake. A hand-authored *patch* names one chart type, so `{ chartType: 'pie', bar: {…} }` is worth reporting. A *resolved* snapshot from `getChartData()` legitimately carries every bag, because defaults seed them all. `auto` tells them apart by whether every bag is present, which is right in practice — pass `form` explicitly when you know. **`requireSomething`** should be `false` when validating a styling-only patch: ```js validateChartData({ title: { text: 'FY26' } }, { requireSomething: false }); ``` ## Formatting for humans ```js import { formatValidationIssues } from '@chartbuddy.io/embed'; console.error(formatValidationIssues(result.issues, '[my-app]')); ``` ## Where else validation runs Inside the ChartBuddy app, validation is advisory (logged, not thrown) so charts keep opening while you edit. The embed API is strict and throws, so callers can catch and fix bad configs before retrying. ## Generating valid data in the first place The machine-readable schema ships in the package and describes every field the validator checks: ```js import schema from '@chartbuddy.io/embed/chart-schema.json'; ``` Constrain generation with it and most of this page stops mattering. See [chartData schema](/api/chart-data). --- --- url: https://chartbuddy.io/embed/docs/migrations/from-chartjs.md --- # From Chart.js ChartBuddy is **not** a Chart.js drop-in. Use this as a migration aid. | Chart.js | ChartBuddy | |---|---| | `type: 'bar'` | `chartType: 'clusteredBar'` (or stacked variants) | | `type: 'line'` | `chartType: 'line'` | | `data.labels` + `datasets[].data` | `seriesData` 2D array (+ `isDataTransposed: true`) | | `options.indexAxis: 'y'` | `orientation: 'horizontal'` | | `options.plugins.legend` | `legend: { visible, colors }` | | `options.plugins.title` | `title: { visible, text }` | | `new Chart(ctx, config)` | `new Insight(el, { chartData })` | ## Example mapping Chart.js: ```js new Chart(ctx, { type: 'bar', data: { labels: ['Q1', 'Q2'], datasets: [{ label: 'Revenue', data: [100, 112] }], }, options: { indexAxis: 'y' }, }); ``` ChartBuddy: ```js new Insight('#chart', { chartData: { chartType: 'clusteredBar', orientation: 'horizontal', isDataTransposed: true, seriesData: [ ['', 'Q1', 'Q2'], ['Revenue', 100, 112], ], title: { visible: true, text: 'Revenue' }, subtitle: { visible: false, text: '' }, legend: { visible: false }, }, }); ``` --- --- url: https://chartbuddy.io/embed/docs/guides/react.md --- # React ```bash npm install @chartbuddy.io/embed react ``` `react` is an optional peer dependency — install it only if you use these bindings. React 18 or 19. ```jsx import { InsightChart } from '@chartbuddy.io/embed/react'; export function Revenue({ grid }) { return ( ); } ``` The chart fills its container, so **give the container a height**. A container with no height renders nothing visible. ## Without a bundler (CDN / single HTML file) The bindings import `react` as a bare specifier, so a plain HTML page needs an **import map** to tell the browser where React lives. This is the whole setup — no build step: ```html
``` Two things to get right: * The import map must map **both** `react` and `@chartbuddy.io/embed/react`. The subpath needs the explicit `.mjs` file — bare-specifier subpath resolution does not work in browsers. * Use `React.createElement` (aliased to `h` above) rather than JSX, since there is no build step to compile JSX. Import maps need a modern browser. If you are generating a standalone artifact for an AI host and JSX or import maps are awkward, skip React entirely and use the plain [`Insight`](/api/) API or the [custom element](/guides/angular#other-frameworks) — both work with a single ` ``` The chart fills its container, so **give the container a height**. A container with no height renders nothing visible. ## Without a bundler (CDN / single HTML file) The bindings import `vue` as a bare specifier, so a plain HTML page needs an **import map**. Use the browser ESM build of Vue, and render without SFCs: ```html
``` The import map must map **both** `vue` and `@chartbuddy.io/embed/vue`, and the subpath needs the explicit `.mjs` file — bare-specifier subpath resolution does not work in browsers. For a standalone artifact with no framework at all, the [custom element](/guides/angular#other-frameworks) needs no import map. ## Why use the wrapper `new Insight()` owns a DOM node and lives across renders. The wrapper handles the lifecycle so you don't: * **Data changes patch instead of remount.** A new `chartData` calls `update()` on the live instance. Rebuilding the chart on every change makes an embedded editor feel broken. * **Only construction options remount.** Changing `editable` or `instanceId` rebuilds the chart; everything else is applied in place. * **Reactive proxies never reach the engine.** The wrapper passes `toRaw()` config, so Vue's proxy wrapper doesn't leak into chart internals. * **Unmount destroys.** Listeners and the instance are cleaned up on `onBeforeUnmount`. ## Editable charts with two-way data ```vue ``` `change` fires after live edits in edit mode, after programmatic updates, and on Done. Feeding it straight back into `chartData` is safe — the wrapper compares by identity and the object it hands you is the one it already applied. ## Assign, don't mutate `chartData` is compared **by identity**, not deeply. Mutating a nested property in place will not be detected: ```js // Not picked up — same object identity chartData.value.title.text = 'FY26'; // Picked up chartData.value = { ...chartData.value, title: { text: 'FY26' } }; ``` ## Events | Event | Payload | |---|---| | `ready` | The `Insight` instance | | `change` | `ChartData \| null` | | `mode` | `'view' \| 'edit'` | | `error` | `Error` — a `ChartDataValidationError` for invalid config | ## `useInsight` for imperative access When you need the instance itself — PNG export, edit mode, revision tracking — use the composable and place the container yourself. ```vue ``` `chartData` accepts a plain object, a `ref`, or a getter. `insight` is a `shallowRef` holding `null` until the mount resolves — guard imperative calls on `ready`. ## What remounts, what patches | Prop change | Effect | |---|---| | `chartData` | `update()` on the live instance | | `instanceId`, `editable`, `assetBase`, `hostOverrides` | Full remount (destroy + construct) | | Anything else in the parent | Nothing | Toggling `editable` remounting is deliberate: view mode and the full editor are different mounts. If you toggle it often, prefer one mount plus `insight.enterEditMode()` / `exitEditMode()`. ## Handling invalid data Chart data is validated at the API boundary, so bad data fails at mount. The wrapper surfaces it rather than throwing through your render: ```vue ``` See [Validation](/quality-assurance/validation). ## Dashboards One `` per chart, each with a stable `instance-id`. Duplicate ids on a page throw, so derive them from your data rather than the loop index if the list can reorder. ```vue ``` The engine loads once per page, not once per chart — but read [Installation](/getting-started/installation#bundle-size-and-many-charts) before putting a lot of charts on one screen. ## Nuxt / SSR The bindings are browser-only: the chart engine needs a real DOM. The mount happens in `onMounted`, so nothing runs during SSR, but wrap usage in `` so Nuxt does not try to render it on the server: ```vue ``` There is no headless/Node render path today, so charts cannot be pre-rendered to PNG on a server. --- --- url: https://chartbuddy.io/embed/docs/guides/angular.md --- # Angular Angular uses the `` **custom element** rather than a compiled Angular library. Angular binds properties and listens to events on custom elements natively, so you get the same lifecycle guarantees as the React and Vue bindings. ```bash npm install @chartbuddy.io/embed ``` No extra peer dependency — the element is plain DOM. ## Why an element instead of an Angular package A native Angular library has to be compiled with Angular's own toolchain and published against a specific Angular major, which would tie your ChartBuddy upgrades to your Angular upgrades. The element has no such coupling and works unchanged across Angular versions. If you would rather have typed Angular inputs, the [thin wrapper](#optional-typed-wrapper-component) below is about twenty lines and lives in your codebase, where it can follow your Angular version. ## Setup Import once — importing registers the element — and allow custom element tags in the components that use it. ```ts import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import '@chartbuddy.io/embed/element'; @Component({ selector: 'app-revenue', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` `, }) export class RevenueComponent { chartData = { chartType: 'clusteredBar', isDataTransposed: true, seriesData: [['', 'Q1', 'Q2'], ['Revenue', 100, 112]], }; onChange(event: Event) { const cd = (event as CustomEvent).detail; // … } } ``` For an NgModule app, put `CUSTOM_ELEMENTS_SCHEMA` in the module's `schemas` instead. The chart fills its container, so **give it a height**. ## Properties vs attributes `chartData` is an object, so it must be set as a **property** — `[chartData]="…"` with square brackets. Angular sets DOM properties for bracket bindings on custom elements, so this works as written. | Option | Binding | |---|---| | `chartData` | `[chartData]="cd"` — property only | | `instanceId` | `instance-id="revenue"` or `[instanceId]="id"` | | `editable` | `editable` / `[editable]="true"` | | `assetBase` | `asset-base="https://…/"` | Assigning a new object to `chartData` patches the live chart via `update()`. Changing any of the others remounts it. A burst of attribute changes in the same tick is coalesced into a single remount. ## Mutation is not detected `chartData` is compared **by identity**. With Angular's default change detection you must assign a new object: ```ts // Not picked up this.chartData.title.text = 'FY26'; // Picked up this.chartData = { ...this.chartData, title: { text: 'FY26' } }; ``` This also keeps you compatible with `OnPush`. ## Events All four are `CustomEvent`s; read `event.detail`. | Event | `detail` | |---|---| | `ready` | The `Insight` instance | | `change` | `ChartData \| null` | | `mode` | `'view' \| 'edit'` | | `error` | `Error` — a `ChartDataValidationError` for invalid config | Note that `error` is a `CustomEvent` named `error`, not an `ErrorEvent`, and it does not bubble. ## Imperative access Grab the element with a template ref and read `.insight`: ```ts @ViewChild('chart') chartRef!: ElementRef; async downloadPng() { const { insight } = this.chartRef.nativeElement; if (insight) await insight.downloadPng(); } ``` ```html ``` `insight` is `null` until the mount resolves; `ready` tells you when it has. ## Optional: typed wrapper component If you want Angular-native inputs and outputs, wrap the element once: ```ts import { Component, CUSTOM_ELEMENTS_SCHEMA, EventEmitter, Input, Output, } from '@angular/core'; import '@chartbuddy.io/embed/element'; import type { ChartData, ChartDataInput } from '@chartbuddy.io/embed'; @Component({ selector: 'cb-insight', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` `, }) export class CbInsightComponent { @Input() chartData?: ChartDataInput; @Input() instanceId?: string; @Input() editable = false; @Output() change = new EventEmitter(); @Output() error = new EventEmitter(); } ``` Then `` with no schema boilerplate at each call site. ## Handling invalid data Chart data is validated at the API boundary. Invalid config fires the `error` event instead of rendering a blank chart: ```ts import { ChartDataValidationError, validateChartData } from '@chartbuddy.io/embed'; onError(event: Event) { const err = (event as CustomEvent).detail; if (err instanceof ChartDataValidationError) { this.issues = err.errors; // [{ path, code, expected, suggestion }, …] } } ``` You can also check before binding, with no chart involved: ```ts const { valid, errors } = validateChartData(candidate); ``` See [Validation](/quality-assurance/validation). ## Dashboards One element per chart, each with a stable `instance-id`. Duplicate ids on a page throw, so derive them from your data rather than the loop index. ```html @for (panel of panels; track panel.id) { } ``` The engine loads once per page, not once per chart — but read [Installation](/getting-started/installation#bundle-size-and-many-charts) before putting a lot of charts on one screen. ## SSR The element needs a real DOM, so don't import it on the server. Under Angular Universal, import it inside a browser-only guard: ```ts if (isPlatformBrowser(this.platformId)) { await import('@chartbuddy.io/embed/element'); } ``` There is no headless/Node render path today, so charts cannot be pre-rendered to PNG on a server. ## Other frameworks The same element works in Svelte, Solid, Lit, Astro, and plain HTML: ```html ``` --- --- url: https://chartbuddy.io/embed/docs/guides/versioning.md --- # Versioning ## chartData documents Every `chartData` object can carry a top-level **`version`** field (the ChartBuddy release that wrote the JSON, for example `"1.8.3"`). **Always version JSON you store or ship.** When an older chart loads in a newer `@chartbuddy.io/embed` release, ChartBuddy runs its migration pipeline automatically and converts the document to the current schema. You do not need to rewrite saved charts by hand when you upgrade the package. That forward compatibility is why versioning matters: * A chart saved months ago with a `version` stamp still mounts in today's embed. * Missing `version` makes it harder to pick the right migration path; stamped JSON is unambiguous. Stamp on save from the running embed: ```js import { Insight, version } from '@chartbuddy.io/embed'; const insight = new Insight('#chart', { chartData: myChart }); await insight.ready; const toStore = { ...insight.getChartData(), version, // or the version field already on getChartData() }; ``` Or export from the editor (`exportConfig()` / full `getChartData()` round-trip). Both include the resolved tree; keep `version` when you persist it. Migration runs on mount (and when chart data enters the engine). If transformers apply, the in-memory document is updated to the current format for that release. See also the [`version` field](/api/chart-data#top-level-map) in the chartData reference. ## Embed package Examples and live previews load the **latest** `@chartbuddy.io/embed` from unpkg. Check the running version at runtime: ```js import { version } from '@chartbuddy.io/embed'; console.log(version); ``` ## Docs site This docs site is **single-version**. It documents the current embed package. --- --- url: https://chartbuddy.io/embed/docs/README.md --- # ChartBuddy Embed docs site VitePress documentation for **`@chartbuddy.io/embed`** (served at `/embed/docs/`). ## Develop ```bash cd embed-docs-site npm install npm run dev ``` Open the printed local URL (usually `http://localhost:5173`). ## Build ```bash npm run build ``` Static output: `embed-docs-site/.vitepress/dist/` (built with `base: '/embed/docs/'`). ## Serve on local backend (`localhost:8000`) ```bash # from 2_chart_buddy repo root npm run docs:sync ``` This rebuilds and copies into `../1_chart_backend/public/embed/docs/`. Then open `http://localhost:8000/embed/docs/` in your browser. Later, point `docs.chartbuddy.io` at the same static folder (or keep `/embed/docs` on the main site). Preview production build locally without Laravel: ```bash npm run preview ``` ## Layout ``` embed-docs-site/ .vitepress/config.ts # nav + sidebar index.md # home getting-started/ concepts/ configuration/ chart-types/ axes/ api/ quality-assurance/ migrations/ guides/ samples/ public/ ``` ## Notes * Examples load the latest **`@chartbuddy.io/embed`** from unpkg. * Agent index: [llms.txt](https://unpkg.com/@chartbuddy.io/embed/llms.txt). * Serving from Laravel (`1_chart_backend` / `:8000`) can come later by publishing the VitePress `dist` as static assets.