# Performance and security

> WestCoast Charts 1.1.5, release, public Core release, on npm. HTML: https://westcoast-charts.com/docs/performance-and-security/ · Index for agents: https://westcoast-charts.com/llms.txt · Source: library guide, commit 95f093ba

This page collects what you need to know to run WestCoast Charts with large data and on pages with
strict security requirements. Every number on this page comes from a measurement that is recorded in
the repository; the source is named under each table. Numbers come from one machine, so treat them as
orders of magnitude and as a regression baseline, not as a promise for every device.

- [Performance](#performance)
  - [Settings for large data](#settings-for-large-data)
  - [Measured: core charts](#measured-core-charts)
  - [Measured: premium charts](#measured-premium-charts)
  - [Bundle sizes](#bundle-sizes)
  - [Known limits](#known-limits)
- [Security](#security)
  - [HTML and XSS](#html-and-xss)
  - [Content Security Policy and Trusted Types](#content-security-policy-and-trusted-types)
  - [CSV export](#csv-export)
  - [Robustness](#robustness)
  - [Licence check and privacy](#licence-check-and-privacy)
  - [Dependencies](#dependencies)
  - [Reporting a vulnerability](#reporting-a-vulnerability)

## Performance

### Settings for large data

Line and area charts with many points, and bar or scatter charts with thousands of marks, are handled by
two techniques that are on by default:

- **Decimation** (`wcc.performance.decimate`, default `true`): plain line and area paths keep only the
  first, last, lowest and highest point of every device-pixel column. The drawn result is the same.
  The decimation code ships in the light entry `west-coast-charts/types/decimate` (also part of
  `west-coast-charts/types` and `full`). Without it the core draws every point and logs one hint.
- **Dense marks** (`wcc.performance.denseMarks`, default `'auto'`): from `denseThreshold` points per
  series (default 2,000), columns narrower than a pixel and scatter points are drawn as one combined path
  per series instead of one element per point.

```js
import WestCoastCharts from 'west-coast-charts'
import 'west-coast-charts/types/decimate' // light entry with the decimation code

const points = Array.from({ length: 50000 }, (_, i) => [Date.UTC(2026, 0, 1) + i * 60000, Math.round(50 + 30 * Math.sin(i / 500) + (i % 7))])

const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'line', height: 320, animations: { enabled: false } },
  series: [{ name: 'Sensor', data: points }],
  xaxis: { type: 'datetime' },
  wcc: {
    performance: {
      decimate: true,        // default
      denseMarks: 'auto',    // default; true forces it, false turns it off
      denseThreshold: 2000,  // default
    },
    a11y: { dataTableMaxRows: 500 }, // the hidden data table renders this many rows and states the total
  },
})
await chart.render()
```

More tips:

- **Turn animations off** for very large series or high-frequency streams: `chart.animations.enabled: false`.
- **Update instead of re-creating.** `updateSeries()` reuses the chart; destroying and re-creating costs a
  full render.
- **Canvas renderer for millions of points.** `west-coast-charts/features/renderer-canvas` draws line,
  area and scatter series into a canvas while axes, tooltips and accessibility stay as before. See
  docs/features/renderer-canvas.md (library repository).
- Dense mode changes a few details (for example, per-point hover styling on a combined path). They are
  listed in docs/architecture/performance.md (library repository).

### Measured: core charts

**Large data sets.** Longest main-thread task in ms, headless Chromium, on the Linux build box after the
2026-10-23 performance fixes:

| Case | render | longest render task | update (sync) | longest update task |
| --- | ---: | ---: | ---: | ---: |
| line, 10k points | 73 | 73 | 34 | 0 |
| line, 100k points | 167 | 167 | 111 | 126 |
| area, 10k points | 83 | 83 | 35 | 0 |
| column, 10k points | 62 | 61 | 32 | 0 |
| column, 100k points | 121 | 120 | 96 | 104 |
| scatter, 10k points | 108 | 108 | 45 | 53 |
| scatter, 100k points | 223 | 223 | 163 | 170 |

Source: docs/architecture/performance.md (library repository), section "2026-10-23". A
longest task of 0 means no single task above the long-task threshold.

**Against the 4.7.0 code base**, longest render task in ms (Chromium, perf test cases):

| Case | 4.7.0-based | now |
| --- | ---: | ---: |
| column, 10k points | 553 | 68 |
| scatter, 10k points | 558 | 69 |
| line, 10k points | 121 | 79 |
| area, 10k points | 135 | 67 |
| line, 100k points | 383–489 | 207 |

Source: docs/architecture/performance.md (library repository), section "Numbers".

**Animation and interaction**, 720 bars, Chrome 126, 3 runs:

| Measure | Result |
| --- | --- |
| first render | 83.1 ms |
| animated morph between data sets | 60 fps (p95 frame interval 21.8 ms) |
| streaming updates | 39 fps at 39 updates per second |
| hover | 60 fps (p95 frame interval 16.8 ms) |

Source: verified site facts at commit `bd511576` (2026-10-23).

**Performance pass A** (radialColumn, 100k redraw, canvas 1M), medians before → after:

| Case | Measure | Before | After |
| --- | --- | ---: | ---: |
| radialColumn, 720 bars, animated update | longest frame per run | 27–43 ms | 22–26 ms |
| radialColumn, 720 bars, animated update | synchronous update | 13.4 ms | 11.4 ms |
| radialColumn, 720 bars, animations off | updates per second | 45.2 | 61 (display limit) |
| line, 100k points | redraw frame | 105 / 95 ms | 86 / 73 ms |
| canvas renderer, line 1M points | first render | 397.8 ms | 352.9 ms |

Source: docs/perf/perf-a.md (library repository) (box, headless Chromium with software raster).

### Measured: premium charts

Medians of 3 runs, chart 1,200 × 700 px, animations off unless noted, headless Chromium with software
raster on a shared 8 vCPU Linux box (2026-10-23):

| Case | render | update | interaction |
| --- | ---: | ---: | --- |
| Gantt, 5k tasks | 77 ms | 98 ms | wheel scroll and bar drag 60 fps |
| Gantt, 20k tasks | 132 ms | 164 ms | wheel scroll and bar drag 60 fps |
| Tree, 2k nodes | 105 ms | 103 ms | pan and hover 60 fps; expand all 111 ms |
| Tree, 10k nodes | 497 ms | 466 ms | pan and hover 60 fps; expand all 442 ms (one long frame) |
| Sankey, 300 nodes / 1.5k links | 168 ms | 186 ms | hover 57 fps, node drag 59 fps |
| Network, 2k nodes / 6k edges, animated layout | 227 ms to the first frame | 250 ms | layout settles in 1,339 ms at 49 fps |
| Network, 2k nodes / 6k edges, static layout | 828 ms | 837 ms | node drag 55 fps |
| Map, all countries + 5k bubbles | 90 ms | 98 ms | pan, zoom and hover 60 fps |
| Stock, 100k candles + volume | 145 ms | 137 ms | pan 59 fps, zoom and crosshair 60 fps |

After `destroy()` the measured heap returns to within 0.2 MB of an empty page in every case.
Source: docs/perf/premium.md (library repository), which also lists DOM sizes, heap figures and the method.

### Bundle sizes

gzip level 9, minified bundles, from the WestCoast Charts release record (library commit `95f093ba8d388c579d9f94701d3322c728c77632`):

| File | gzip |
| --- | ---: |
| core `west-coast-charts.min.js` | 194,836 B |
| all chart types `west-coast-charts-types.min.js` | 83,472 B |
| premium `west-coast-charts-premium.min.js` | 165,491 B |
| everything except premium `west-coast-charts.full.min.js` | 380,285 B |

For comparison, ApexCharts 4.7.0 `apexcharts.min.js` is 152,686 B. The difference buys accessibility,
security fixes, theming and robustness; the itemised list is in
docs/architecture/size-ledger.md (library repository). Single chart types
(`west-coast-charts/types/<name>`) and features load on their own and cost nothing until imported.

### Known limits

- **Network, static layout:** with animations off the layout runs to completion before the first paint
  (about 0.8 s for 2k nodes / 6k edges). Use the default animated layout or pass positions.
- **Tree, expand all at 10k nodes** is one long frame (about 0.44 s); panning and hover stay smooth.
- **Sankey** keeps one gradient per link (about 9.5k elements at 1.5k links); there is no virtualisation.
- **Scatter, 100k points:** each update still forces one layout of the dense path for axis label
  measurement.
- Measured in headless Chromium with software raster only. GPU-backed browsers should be faster on
  paint-heavy charts, but that is not measured. 120 Hz displays are not measured.

Sources: docs/perf/premium.md (library repository) ("Known limits"), docs/perf/perf-a.md (library repository) ("Gaps").

## Security

The fixes behind this section are documented in detail, with the affected code, in
SECURITY-FIXES.md (library repository).

### HTML and XSS

- Strings from data and configuration (series names, categories, labels, titles, annotation texts,
  locale strings) are always rendered as text, never as HTML.
- HTML returned by your functions (formatters, `tooltip.custom`, `legend.markers.customHTML`) and
  markup options (toolbar icons, annotation `customSVG`) runs through a built-in allow-list sanitizer.
- `chart.sanitizeHtml`: `true` (default), a function `(html, { svg }) => string | Node` to use your own
  sanitizer, or `false` for raw HTML (only for fully trusted output).
- Escape data you mix into your own HTML with `WestCoastCharts.escapeHtml(value)`.
- The built-in sanitizer is designed for chart labels and tooltips. If you render untrusted rich HTML
  in a custom tooltip, plug in a dedicated sanitizer such as DOMPurify:

```js
import WestCoastCharts from 'west-coast-charts'

// stand-in for DOMPurify.sanitize so this example runs without the dependency
const sanitize = (html) => html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/\son\w+="[^"]*"/gi, '')

const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'bar', height: 260, sanitizeHtml: sanitize }, // e.g. DOMPurify.sanitize
  series: [{ name: 'Incidents', data: [2, 5, 1] }],
  xaxis: { categories: ['api', 'web', 'db'] },
  tooltip: {
    custom: ({ series, seriesIndex, dataPointIndex, w }) =>
      `<div class="tip"><b>${WestCoastCharts.escapeHtml(w.globals.labels[dataPointIndex])}</b>: ${series[seriesIndex][dataPointIndex]}</div>`,
  },
})
await chart.render()
```

### Content Security Policy and Trusted Types

| Need | What to allow |
| --- | --- |
| Scripts | Nothing extra: no `eval`, no `new Function`, no inline scripts. A nonce-based `script-src` with `'strict-dynamic'` works; load the bundles with your nonce. |
| Library `<style>` elements | Pass your nonce as `chart.nonce`; every `<style>` the core, premium and feature bundles inject carries it. Alternatively serve `dist/west-coast-charts.css` yourself. |
| Style attributes | The charts set inline `style` attributes (tooltips, legends, data tables, labels). Allow them with `style-src-attr 'unsafe-inline'` (or `'unsafe-inline'` in `style-src`). They cannot run code. |
| Trusted Types (`require-trusted-types-for 'script'`) | Add the policy name `west-coast-charts` to `trusted-types`. Every HTML the library writes goes through that policy: markup it builds from escaped text, and formatter or tooltip HTML after `sanitizeHtml`. With `chart.sanitizeHtml: false` or your own sanitizer function, your HTML passes through the policy unchanged, so only use that for markup you trust. |
| PNG export and `dataURI()` | `img-src` must allow `blob:` (tried first) or `data:` (fallback). Pages that allow neither cannot rasterise SVG in the browser. |

A policy that the release check renders charts under without a single violation (core, types, premium
and feature bundles; hover, menu, legend toggle, data update, drill-down, `dataURI()`):

```text
default-src 'none';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'nonce-{random}'; style-src-elem 'nonce-{random}'; style-src-attr 'unsafe-inline';
img-src 'self' blob: data:; font-src 'self'; connect-src 'self';
base-uri 'none'; object-src 'none'; form-action 'none';
require-trusted-types-for 'script'; trusted-types west-coast-charts
```

Send it as an HTTP header: `<meta>` CSP tags ignore some of these directives. `{random}` is a fresh
nonce per response. The check is `tools/docs/check-csp.cjs` (headless Chromium).

```js
import WestCoastCharts from 'west-coast-charts'

// the same nonce your server puts in the Content-Security-Policy header
const nonce = document.querySelector('meta[name="csp-nonce"]')?.content ?? 'r4nd0m'

await new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'line', height: 240, nonce },
  series: [{ name: 'Latency', data: [120, 98, 143, 110] }],
}).render()
```

### CSV export

`exportToCSV` neutralises spreadsheet formulas: text cells starting with `=`, `+`, `-`, `@`, tab or
carriage return (and their full-width forms) get a leading `'`, so a category such as
`=HYPERLINK(...)` cannot become a live formula when the file is opened in a spreadsheet. Numbers are
unchanged. Cells containing delimiters, quotes or line breaks are quoted per RFC 4180. The same rules
apply to the output of `csv.categoryFormatter` and `csv.valueFormatter`.

### Robustness

- Malformed options (`series: null`, `null` entries, `data: null`, `xaxis.categories: null`,
  `annotations: null`, `chart: null`, toolbar icons without `click`, …) are normalised to their empty
  equivalents instead of throwing, in the constructor and in all update methods.
- A randomised configuration test found 0 exceptions in 900 random configurations
  (size ledger (library repository, `architecture/size-ledger.md`), "Robustness and security").
- Premium charts guard against very large arrays (no argument spreading over unbounded data).

### Licence check and privacy

The premium licence check is offline: no network request, no telemetry, no cookies or storage. See
[Premium and licensing](https://westcoast-charts.com/docs/premium/index.md#licence-keys).

### Dependencies

The runtime dependencies are `@svgdotjs/*` and `@yr/monotone-cubic-spline`. `npm audit --omit=dev`
reports 0 vulnerabilities for them (see SECURITY-FIXES.md (library repository)).
Nothing from the build or test tool chain is shipped in `dist/`.

### Reporting a vulnerability

Please report suspected vulnerabilities privately to West Coast Software rather than in a public issue.
