Skip to content

Charts

json-to-office gives you two ways to put charts in a document: native charts (the chart component) and Highcharts-rendered images (the highcharts component). Both formats have both. Two caveats on the native one: the docx chart needs renderer: "office-open", because docx.js has no chart primitive, and bubble is drawn only by the pptx pptxgenjs renderer. This page helps you choose between them and get each one running.

Native charts vs Highcharts

chart (native)highcharts
Formatspptx (both renderers, bubble on pptxgenjs only); docx with renderer: "office-open"pptx + docx, every renderer
OutputReal PowerPoint / Word chart objectPNG image
Editable by recipientsYes — data and styling editable in the Office appNo — it's a picture
ScalingVector, crisp at any zoomRaster (use scale for sharper exports)
External dependenciesNoneRequires a running Highcharts Export Server
Works in the browserYesNo — Node-only (needs server-side fetch)
Chart catalogpptx 9 types: area, bar, bar3D, bubble, doughnut, line, pie, radar, scatter — bubble on pptxgenjs only; docx 8: the same minus bar3D and bubble, plus columnThe full Highcharts catalog: heatmaps, treemaps, gauges, combined series, annotations, …
Theme integrationPalette + text colors follow the theme automaticallyTheme palette injected when options.colors unset

Rule of thumb: reach for native chart first. It needs no infrastructure, recipients can tweak it, and it covers the common business-chart types. Reach for highcharts when you need chart types or styling an Office chart can't express — or when you're generating a Word document on the default docxjs renderer, where highcharts is the only chart component.

Why the docx chart is renderer-scoped

docx.js has no chart primitive at all, so the component is absent from that renderer's schema rather than accepted and dropped. @office-open/docx does have one, though it writes only the cached values: json-to-office splices in the embedded workbook, the series colors and the axis titles afterwards, which is what makes Edit Data work and the theme palette apply. See the component reference.

The pptx office-open renderer needs the same repair: the same pass writes the workbook there too, so both pptx backends draw an editable native chart. @office-open/pptx forwards more of the chart options than its docx sibling, so only the cell references and the series colors have to be spliced in.

One exception: the native bubble chart is pptxgenjs-only, and is absent from the docx component altogether. @office-open spells a bubble series as x/y/size triples rather than categories and values, and there is no unambiguous reading of a category label as a numeric x — so it is refused by name rather than guessed at. A Highcharts bubble chart is unaffected: highcharts keeps its full catalog on every renderer.

Theme palette

Both paths default their series colors to the theme palette, so charts recolor themselves when you switch themes. The same token list, in the same orderprimary, secondary, accent, accent4, accent5, accent6 — backs the native chart in both formats and highcharts in both formats, and both theme schemas declare all six names (accent4accent6 optional in each). A theme that fills all six with hex values therefore paints the same series colors in a deck and in a document, letter case included.

Everywhere, an explicit options.colors (or chartColors on the native chart) always wins: when you set it, nothing is injected and no fallback warning is emitted.

Slots the theme leaves unset

Both formats do the same thing: the missing slot is dropped. The injected palette is only as long as the theme has tokens defined, the chart library reuses that shorter list once the series outnumber it (Highcharts wraps back to the start), and no warning is emitted — an implicit palette shrinking to fit the theme is normal, not a mistake.

Dropping compacts, it does not leave a gap. A theme that defines accent5 but not accent4 produces primary, secondary, accent, accent5, so accent5 paints the fourth series. The token list is a preference-ordered pool of candidate colors, not fixed per-series slots, so a color you did define is kept rather than discarded.

Built-in themes differ across the two formats

All four built-in PPTX themes (consulting, default, dark, minimal) define accent4accent6, so a pptx chart gets six distinct colors out of the box; consulting additionally orders its series through palette.chart.

None of the five built-in DOCX themes define them, so a docx chart palette is primary, secondary, accent today — three colors, wrapping from the fourth series on. To get six, add accent4, accent5 and accent6 to a custom DOCX theme; the schema accepts them.

Skipping applies to the implicit palette only. Naming an unset token explicitlychartColors: ["accent4"] on the native pptx chart, or any pptx color prop pointing at an unfilled slot — still falls back to primary and emits a THEME_COLOR_FALLBACK warning. That is an authoring error and stays loud.

Tokens that point at other tokens

Both palettes resolve each token through the theme's own recursive name resolution, so "accent4": "primary" is a valid chart color in either format and picks up whatever primary holds. A value that reaches no color — a typo'd token name, or a reference cycle — is skipped like an unset slot rather than handed to the renderer verbatim. That matters because neither renderer complains: the Highcharts export server and PowerPoint both answer an unparseable color by drawing the series black.

Named explicitly, an unresolvable token stays loud in both formats, with different mechanics. On pptx it falls back to primary and emits a warning — THEME_COLOR_FALLBACK when the slot is unset, UNKNOWN_COLOR when the slot holds a value that resolves to nothing. Docx has no warning channel for colors: it throws.

Only DOCX themes can spell the reference

The two theme schemas differ on the color value. A docx theme color is #RRGGBB or the name of another color; a pptx theme color must be strict hex (^#?[0-9A-Fa-f]{6}$), so "accent4": "primary" fails jto pptx validate. A pptx reference chain therefore only reaches generation through a theme the schema never sees — a customThemes object, a --theme-path file (parsed as plain JSON, without validation), or an inline props.theme in a run with validation.enabled = false — an inline theme is part of the document, so the generation gate checks it against ThemeConfigSchema like any other prop.

Quick examples

Native pptx chart

No server needed — this renders anywhere:

json
{
  "name": "chart",
  "props": {
    "type": "line",
    "data": [
      {
        "name": "Users",
        "labels": ["Jan", "Feb", "Mar", "Apr"],
        "values": [120, 180, 260, 390]
      }
    ],
    "title": "Monthly active users",
    "showTitle": true,
    "showLegend": false,
    "lineSmooth": true,
    "grid": { "column": 0, "row": 1, "columnSpan": 12, "rowSpan": 4 }
  }
}

Every series needs both labels and values; pie and doughnut charts take a single series. The full option set (axes, legend, data labels, bar/line/pie specifics) is in the pptx charts reference.

pptx highcharts

The options object is a standard Highcharts configuration, and it must include chart.width and chart.height in pixels — the export server needs fixed dimensions to render. On the slide, position with x/y/w/h (inches or %) or grid; when omitted, size defaults to the chart's pixel dimensions at 96 px/inch.

json
{
  "name": "highcharts",
  "props": {
    "options": {
      "chart": { "type": "column", "width": 800, "height": 500 },
      "title": { "text": "Revenue by region" },
      "xAxis": { "categories": ["EMEA", "APAC", "AMER"] },
      "series": [
        { "name": "2025", "data": [1.2, 0.8, 2.1] },
        { "name": "2026", "data": [1.4, 1.1, 2.6] }
      ]
    },
    "grid": { "column": 1, "row": 1, "columnSpan": 10, "rowSpan": 4 }
  }
}

docx highcharts

The same component exists for Word documents. Instead of slide coordinates it takes optional width/height (pixels, or a "90%" string relative to content width) for the rendered image size. It lives inside a section like any other docx block:

json
{
  "name": "docx",
  "props": { "theme": "default" },
  "children": [
    {
      "name": "section",
      "children": [
        { "name": "heading", "props": { "text": "Results", "level": 2 } },
        {
          "name": "highcharts",
          "props": {
            "options": {
              "chart": { "type": "pie", "width": 600, "height": 400 },
              "series": [
                {
                  "name": "Share",
                  "data": [
                    { "name": "Product A", "y": 61 },
                    { "name": "Product B", "y": 39 }
                  ]
                }
              ]
            },
            "width": "80%"
          }
        }
      ]
    }
  ]
}

See DOCX components for the docx-side props.

Running the export server locally

The highcharts component talks to a Highcharts Export Server over HTTP. For local development, run one with pnpm dlx:

bash
pnpm dlx highcharts-export-server --enableServer true

It listens on http://localhost:7801 by default — which is exactly where json-to-office looks when no server URL is configured. Start the server, generate your document, done:

bash
pnpm dlx highcharts-export-server --enableServer true &
pnpm dlx @json-to-office/jto pptx generate report.pptx.json -o report.pptx

TIP

The export server uses Puppeteer/Chromium under the hood, so the first run downloads a browser. If a document with highcharts components fails with a connection error, the message will remind you of the command above.

What leaves the process

Nothing is drawn locally: json-to-office POSTs one JSON body per highcharts component to the export server's /export endpoint and embeds the PNG it gets back. That body is:

FieldContents
infileThe complete options object as authored — every series with every data point, category labels, titles, axis titles, tooltips — plus the theme palette written into colors and the theme typography into the text styles
resourcesYour resources prop, with the registered non-safe theme families inlined as @font-face CSS (the font bytes, base64) ahead of it
type, b64, scaleExport format (png, base64) and the raster scale

Nothing else about the document goes: not its text, its other components, its metadata or its theme file. A chart (native, office-open) or a visual sends nothing anywhere.

Because the body carries the data, where it goes is opt-in. An export server the address itself proves private — localhost, a loopback, RFC 1918 or link-local address, a unique-local IPv6 address, or a .local, .internal or .home.arpa name — needs nothing. A hostname that DNS decides (charts, charts.corp) is not guessed at: it takes the same switch as a public one. Any other URL is refused at generation time until you set services.highcharts.allowRemote: true (CLI, playground and MCP: HIGHCHARTS_ALLOW_REMOTE=1), and once allowed every generation reports W_HIGHCHARTS_REMOTE_EXPORT once per export server it reached, naming the URL that received the chart data — in the generation warnings, and as a warning diagnostic from jto_generate. A server that is down is a failed generation, not a skipped figure: the document is not produced without its chart.

Pointing at a deployed server

For CI or production you'll usually run the export server as a service — the project's own render server bundles a Highcharts Export Server behind an /export endpoint, ready to deploy.

The server URL resolves per component in this order:

  1. serverUrl prop on the individual highcharts component
  2. services.highcharts.serverUrl from generation options / environment
  3. Default http://localhost:7801

Library

Pass a services config to any generation call:

ts
import { generateAndSaveFromJson } from '@json-to-office/json-to-pptx';

await generateAndSaveFromJson(document, 'report.pptx', {
  services: {
    highcharts: {
      serverUrl: 'https://charts.example.com',
    },
  },
});

The same services option exists on @json-to-office/json-to-docx generation calls and on the plugin-system generators.

CLI and playground

The CLI and playground read the server URL from the environment:

bash
export HIGHCHARTS_SERVER_URL=https://charts.example.com
export HIGHCHARTS_ALLOW_REMOTE=1   # a public hostname: say so, or generation refuses it
pnpm dlx @json-to-office/jto pptx generate report.pptx.json -o report.pptx

Authentication headers

If your export server sits behind an API key or gateway, attach headers via services.highcharts.headers. Headers can be a static object or an async function that receives the request body (for signed requests):

ts
await generateAndSaveFromJson(document, 'report.pptx', {
  services: {
    highcharts: {
      serverUrl: 'https://charts.example.com',
      headers: { 'x-api-key': process.env.CHARTS_API_KEY! },
      // or computed per request:
      // headers: async (body) => ({ Authorization: `Bearer ${await getToken(body)}` }),
    },
  },
});

On the CLI, set HIGHCHARTS_API_KEY (and optionally HIGHCHARTS_API_KEY_HEADER, default x-api-key) and the header is attached for you:

bash
export HIGHCHARTS_SERVER_URL=https://charts.example.com
export HIGHCHARTS_API_KEY=sk-...

How hard the server is pushed

One chart is one Puppeteer render, and a self-hosted export server usually runs a single worker. A document's charts are therefore queued rather than posted all at once: at most four requests are in flight per server URL, and the cap is process-wide, so several documents generated together in one process share it instead of each opening a pool of their own. A request the server could not answer — a timeout, a refused connection, a 429 or a 5xx — is retried twice with jittered exponential backoff; a 4xx is about the chart you sent and fails at once.

OptionDefaultWhat it does
services.highcharts.concurrency4Requests in flight at once against this export server
services.highcharts.timeoutMs30000Abort one export request after this long
services.highcharts.retries2Retries after the first attempt, for a failure the server may fix
ts
await generateAndSaveFromJson(document, 'report.docx', {
  services: {
    highcharts: {
      serverUrl: 'https://charts.example.com',
      // A server with more workers can take more at once; a slow one that
      // renders large charts needs longer before an abort is honest.
      concurrency: 8,
      timeoutMs: 60000,
    },
  },
});

A chart identical to one already rendered in the same document is requested once and its PNG reused, so a figure that appears in a summary and again in its own section costs one render. Identical means the same export server and the same resolved request body — the options after the theme's palette and typography have been written into them. Where the component sits on the page is not part of that: two appearances at different positions share one render. The width it is placed at usually is, because the type in a chart is sized for it — the same series shrunk into half the measure is drawn at a different scale, so it is a different PNG.

getChartRequestStats() (from @json-to-office/core-docx or @json-to-office/core-pptx) reports the cumulative counters behind all of this — collected charts, unique requests after dedupe, retries spent, and maxInFlight, the most requests open against an export server at one time. maxInFlight sitting at the cap for a whole run says the queue, not the renderer, is what the documents are waiting on; retries climbing says the server is shedding work. resetChartRequestStats() zeroes them.

Theme typography in Highcharts output

The chart is a PNG drawn by a browser that has never seen the document, so on its own it would come out in the export server's default face at Highcharts' own sizes — visibly foreign to the prose around it. json-to-office therefore writes the document's typography into the request, beneath whatever the author set:

Highcharts optionValue
chart.style.fontFamilythe theme's body family, quoted, with the generic it belongs to ("Calibri", sans-serif; "Georgia", serif)
title.stylethe heading family at the heading3 size and weight, in the primary text colour
subtitle, axis labels and titlethe label size in the secondary text colour
legend.itemStyle, plotOptions.series.dataLabelsthe label size in the primary text colour, with the chartLabel role's weight when the theme states one
caption, credits.stylethe source size in the secondary text colour
credits.enabledfalse — the highcharts.com credit never reaches a document unless the author sets credits.enabled: true

The label size is the theme's chartLabel type role and the source size its source role when the theme declares them (shared visual layers); otherwise labels sit one point under the body size in docx and two under the body style in pptx, and the source one point under that (the caption size in pptx).

Sizes are written in chart pixels scaled by the width the image is placed at, so a label that must read as 9pt on the page is drawn larger in a 900px chart that is shrunk into a 450pt measure. In docx the placement follows the component's width (pixels, or a percentage of the content width); in pptx it follows w (inches, or a percentage of the slide). A chart placed by neither reads as 96 px per inch.

Every explicit author value wins, property by property: setting title.style.fontSize keeps the theme's family and colour on the title, and setting chart.style.fontFamily keeps the theme's sizes. The family itself has to exist where the export server runs — a local server draws with the fonts installed on your machine; for a registered non-safe family, the document's own font bytes are inlined as @font-face rules ahead of any resources.css you supply (data URIs; they travel only to the export server, which already receives every data point). Safe fonts are never inlined.

Custom fonts in Highcharts output

The resources prop is forwarded to the export server, which lets you inject @font-face CSS (plus JS or extra files) yourself — for a family that is neither installed on the server nor registered in the document, or to override what the theme injects:

json
{
  "name": "highcharts",
  "props": {
    "options": {
      "chart": { "width": 800, "height": 500 },
      "series": [{ "data": [1, 2, 3] }]
    },
    "resources": {
      "css": "@font-face { font-family: 'Inter'; src: url('https://fonts.example.com/inter.woff2'); } .highcharts-root { font-family: 'Inter', sans-serif; }"
    }
  }
}

Next steps

Released under the MIT License.