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, on every renderer. One caveat on the native one: 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 (both renderers)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.

How each renderer draws a docx chart

Both docx renderers write a real Word chart with the embedded workbook Edit Data opens, in the theme's palette and fonts. The default docxjs renderer uses docx's own chart run (docx 9.8 or later), which writes the whole chart. @office-open/docx writes only the cached values, so json-to-office splices in the workbook, the series colors and the axis titles afterwards. The data, colors, titles, legend and text sizes come out the same on both, and so does the plot: both draw what Word's Insert Chart draws — light gridlines and axis lines in a tint of the theme's text color, no tick marks, Word's bar gaps, straight lines with round markers. The renderer notes list what still differs, none of it visible. 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. A bubble needs a size for every point, and a docx series is labels and values with nowhere to put one — 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 order — primary, 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 (accent4–accent6 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 five built-in PPTX themes (consulting, dark, minimal, vermilion, devportal) define accent4–accent6, 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 explicitly — chartColors: ["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.

Native docx chart ​

The docx chart takes the same type, data, title and chartColors, and flows like an image: width/height in inches, alignment, caption. It draws on the default renderer:

json
{
  "name": "chart",
  "props": {
    "type": "doughnut",
    "data": [
      {
        "name": "2025",
        "labels": ["Retail", "Wholesale", "Online"],
        "values": [60, 30, 10]
      },
      {
        "name": "2026",
        "labels": ["Retail", "Wholesale", "Online"],
        "values": [55, 30, 15]
      }
    ],
    "title": "Revenue mix",
    "height": 3,
    "caption": "Revenue mix, 2025 and 2026."
  }
}

A pie draws one series; a doughnut draws one ring per series, as above. Pie and doughnut values cannot be negative, and neither has axes, so axis titles on one are not drawn (with a warning). On a scatter chart each label is the point's x and should be a number: a label that is not one is placed at its position, 1, 2, 3…, with a warning. A series with no points, a value that is not a number and a chart with no room to draw in are refused, naming the chart.

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": "consulting" },
  "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 native chart 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.

Theme chart options ​

Palette and type reach a chart from the theme without the author asking. The rest of a chart's form — a transparent background, the legend on the left, bars without borders, pies with a given label format — would otherwise be repeated on every highcharts node, and a theme pinned to a customer could not change it. componentDefaults.highcharts.options in the theme carries that form, in both formats. It is the theme's Highcharts.setOptions: any Highcharts option, written beneath every chart's own options, key by key. Per series type, use plotOptions.<type>, as Highcharts itself does:

json
{
  "componentDefaults": {
    "highcharts": {
      "options": {
        "chart": { "backgroundColor": "transparent" },
        "legend": { "align": "left" },
        "plotOptions": {
          "bar": { "borderWidth": 0, "pointPadding": 0.06 },
          "pie": { "dataLabels": { "format": "{point.percentage:.1f}%" } }
        }
      }
    }
  }
}

The precedence is the one the palette and typography already follow, highest first: the chart's own options, the theme's options, then the theme's palette and type for whatever is still empty. An authored value always wins, at every level it is set. A document's props.componentDefaults.highcharts merges over the theme's block, as for any other component. What a theme puts in options is posted to the export server as it is; the library does not judge the content. A theme without the key posts exactly the request it posted before.

To read what a chart will be asked to draw — the merged presets, the palette and the type — without rendering it, call resolveHighchartsOptions({ options, theme, width, height, documentProps, page, fonts }) from @json-to-office/json-to-docx. It resolves the theme with the functions generation uses and builds the request with the render's own, so it returns exactly the infile the export server receives. See the API reference.

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.