# Premium and licensing

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

`west-coast-charts/premium` adds nine chart types for operational and analytical dashboards:

| `chart.type` | Use it for |
| --- | --- |
| `sankey` | flows between stages (traffic, budgets, funnels with branches) |
| `chord` | two-way relationships between groups (service-to-service calls, trade) |
| `gantt` | schedules with dependencies, baselines and calendars |
| `tree` | hierarchies and org charts with cards and semantic zoom |
| `stock` | financial time series with indicators and drawing tools |
| `map` | choropleths, markers and routes (world and US states maps included) |
| `marimekko` | two-dimensional share (market × segment) |
| `network` | node-link graphs |
| `radialColumn` | radial dashboards that combine several rings with linked highlighting, see the [radial column guide](https://westcoast-charts.com/docs/radial-column/index.md) |

Two more premium types come as a separate, lazily loaded type module,
`west-coast-charts/layers/quint` (`WestCoastCharts.use([premium, quint])`):

| `chart.type` | Use it for |
| --- | --- |
| `quintTimeline` | an operations day on one time axis: requests and error rate, cost heatmaps, error budgets, latency lines, region traffic, incidents and deploys, see the [Quint guide](https://westcoast-charts.com/docs/quint/index.md) |
| `quintConvergence` | the same lanes with a labelled focus lens that magnifies the time around an incident (time only, never values) |

Two further families are supplied as optional modules: `west-coast-charts/layers/vitals`
adds `vitalsClock` (Vitals Clock), and `west-coast-charts/layers/schedule` adds
`schedule` (Practice Schedule). The complete Premium offer therefore contains
thirteen families: nine in the main bundle and four in the type modules.

The paid offer covers the separately supplied Premium distribution and services described in the [business terms](https://westcoast-charts.com/terms/). It does not replace the MIT licence on MIT-marked code. See the [licensing overview](https://westcoast-charts.com/maintenance/#licensing) for the scope and links to the original notices and full terms.

Premium code is supplied in the separate complete package obtained through a trial or paid licence. The public MIT Core package does not export /premium, /layers or /maps. Install the complete package instead of the Core-only package before using the examples below. Without a licence key every chart renders and works fully,
and on public host names it shows a small "unlicensed" line. Development hosts never show it.

- [Loading premium](#loading-premium)
- [Licence keys](#licence-keys)
- [Domain binding](#domain-binding)
- [The unlicensed watermark](#the-unlicensed-watermark)
- [Development and test hosts](#development-and-test-hosts)
- [Browser requirements (ES2020)](#browser-requirements-es2020)
- [FAQ](#faq)

## Loading premium

With a bundler, install the plugin once at startup:

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

WestCoastCharts.use(premium)

const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'sankey', height: 320 },
  series: [{
    data: [
      { from: 'Visits', to: 'Pricing page', value: 4200 },
      { from: 'Visits', to: 'Docs', value: 2600 },
      { from: 'Pricing page', to: 'Trial', value: 1100 },
      { from: 'Docs', to: 'Trial', value: 700 },
      { from: 'Trial', to: 'Paid', value: 420 },
    ],
  }],
})
await chart.render()
```

With script tags, load the premium bundle after the core. It installs itself into the global
`WestCoastCharts`; its own global is `WestCoastChartsPremium`.

```html
<script src="./dist/west-coast-charts.min.js"></script>
<script src="./dist/west-coast-charts-premium.min.js"></script>
<div id="chart"></div>
<script>
  WestCoastCharts.setLicenseKey('YOUR-LICENCE-KEY') // see below; omit while evaluating
  new WestCoastCharts(document.querySelector('#chart'), {
    chart: { type: 'chord', height: 360 },
    series: [{
      data: [
        { from: 'web', to: 'api', value: 120 },
        { from: 'api', to: 'db', value: 80 },
        { from: 'api', to: 'cache', value: 60 },
        { from: 'worker', to: 'db', value: 40 },
      ],
    }],
  }).render()
</script>
```

Install the plugin before the first premium chart renders. Without it, `render()` rejects with a message
that tells you to import and `use()` the premium plugin.

The map type needs map shapes. `world` and `us-states` ship in `west-coast-charts/maps/*`
(`dist/maps/*.js` for script tags) and register themselves with `WestCoastCharts.registerMap`.

## Licence keys

A licence key is a single string you receive from West Coast Software when you buy a licence. It is
checked **offline** in the browser: there is no network request, no telemetry and no licence server.

Register it once, before you create premium charts:

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

WestCoastCharts.use(premium)

const licence = await WestCoastCharts.setLicenseKey('YOUR-LICENCE-KEY')
if (!licence) {
  // invalid, expired, for another major version or another domain: charts still work, with the watermark
  console.warn('WestCoast Charts licence not accepted')
}
```

`setLicenseKey(key)` returns a promise. It resolves to the key's contents when the key is valid for this
page, and to `null` otherwise. It never throws and never blocks rendering. The contents look like this:

| Field | Meaning |
| --- | --- |
| `l` | licensee (your company) |
| `t` | tier: `'premium'` or `'oem'` |
| `e` | expiry in milliseconds since 1970; `0` = perpetual |
| `v` | `[first, last]` major versions covered, inclusive |
| `i` | issue date (milliseconds since 1970), optional |
| `d` | host patterns the key is bound to, optional (see [Domain binding](#domain-binding)) |

The same function is available as `premium.setLicenseKey(key)`. To use a different key for one chart,
for example in a multi-tenant page, set `chart.licenseKey`; it wins over the global key for that chart:

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

WestCoastCharts.use(premium)

const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'marimekko', height: 300, licenseKey: 'TENANT-SPECIFIC-KEY' },
  series: [
    { name: 'Enterprise', data: [{ x: 'EMEA', y: 40 }, { x: 'Americas', y: 55 }, { x: 'APAC', y: 25 }] },
    { name: 'SMB', data: [{ x: 'EMEA', y: 30 }, { x: 'Americas', y: 20 }, { x: 'APAC', y: 35 }] },
  ],
})
await chart.render()
// 'TENANT-SPECIFIC-KEY' is a placeholder, so this example shows the watermark
```

How a key is checked:

- The signature is verified with ECDSA P-256 / SHA-256 in plain JavaScript. It takes roughly 15 ms once
  per key and page, and the result is cached.
- It needs no `crypto.subtle`, so it works the same on HTTPS, plain HTTP, intranet host names and bare IP
  addresses.
- A key covers the major versions in `v`. The version that counts is the major number of the
  installed package: WestCoast Charts 1.0.0 and every later 1.x release are major 1, so a key with
  `v: [1, 1]` covers all of them.
- The runtime must also trust the key's signer. Version 1.1.1-rc.4 introduces a new active signer
  and retains the previous public key for existing licences. Keys issued by the new signer need
  1.1.1-rc.4 or a later runtime that trusts it; older runtimes do not accept those new signatures
  merely because the key's major-version range includes 1. Existing licences retain their original
  expiry, major-version and host restrictions.
- An expired key is treated like a missing key: the watermark returns, nothing stops working.

### Issuing evaluation, review and gift keys

The companion `tools/license/issue-access.cjs` supports `--purpose evaluation`
and `--purpose review`, delegating signing to the unchanged approved issuer. These future
keys expire 30 days after issuance by default; an explicit `--expires` must be
after issuance. They cannot use `--perpetual` or an expiry of zero. Add
`--domains "review.example.com"` when the evaluation host is known; host binding
is optional when reviewers need several unknown environments.

Intentional gifts use explicit `--purpose grant`; the additional `--gift` flag
is rejected without that purpose. A grant may deliberately be perpetual. The
ten previously issued perpetual gift keys retain their original major-version,
host and expiry rights. They are not reclassified as trials or modified by this
issuance policy. Existing paid issuance remains backward compatible: omitted
`--purpose` means `paid`, with its previous expiry behavior. Purpose controls
issuance only and is not a new runtime permission or signed payload field.

```sh
node tools/license/issue-access.cjs --licensee "Review organisation" --purpose review --domains "review.example.com"
node tools/license/issue-access.cjs --licensee "Intentional gift" --purpose grant --gift --perpetual --versions 1
```

Verification is offline. There is no online revocation service or global
revocation switch. Changing future issuance defaults does not revoke an issued
signature. An older installed bundle continues accepting a signature it trusts
while that key satisfies the bundle's existing expiry, version and host checks.
A future runtime could change its own trust policy, but that would not update
older copies or withdraw independently granted licence rights.

## Domain binding

A key may carry host patterns in `d`. The key is then accepted only when the page's host name matches
one pattern **completely** (case-insensitive, port ignored). `*` stands for any run of characters,
including dots.

| Pattern | Matches | Does not match |
| --- | --- | --- |
| `acme.com` | `acme.com` | `www.acme.com` |
| `*.acme.com` | `app.acme.com`, `eu.app.acme.com` | `acme.com` |
| `10.20.*` | `10.20.0.5`, `10.20.31.7` | `10.2.0.5` |
| `ops-*` | `ops-grafana`, `ops-01` | `devops-01` |

A key without `d` is valid on every host. To cover both the apex domain and its subdomains, the key
needs both patterns: `acme.com` and `*.acme.com`.

## The unlicensed watermark

When a premium chart renders on a host that is not exempt (below) and no valid key applies, the chart
gets one quiet line in its bottom-right corner:

> WestCoast Charts Premium · unlicensed

- It is SVG text (`<text class="wcc-unlicensed">`) in 10 px, the theme's muted colour at reduced opacity,
  `pointer-events: none` and `aria-hidden="true"`, so it never blocks interaction or reaches screen
  readers.
- It is part of the chart SVG, so it also appears in PNG and SVG exports.
- The console shows one warning per page:
  `WestCoast Charts premium: no valid licence key for this site, see WestCoastCharts.setLicenseKey()`.
- Nothing else changes: no feature is disabled, no data is hidden, nothing is sent anywhere.
- Core and `west-coast-charts/types` charts never get a watermark.

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

WestCoastCharts.use(premium)

// a public host without a key: the chart works and carries the watermark
await new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'network', height: 300 },
  series: [{
    data: [
      ['gateway', 'auth'],
      ['gateway', 'orders'],
      ['orders', 'payments'],
    ],
  }],
}).render()
```

Set the key **before** the first premium chart renders. A watermark that is already drawn stays until the
chart redraws.

## Development and test hosts

These pages never show the watermark, with or without a key:

- `localhost`, `127.0.0.1` and `[::1]`
- host names ending in `.local` or `.localhost` (for example `myapp.localhost`, `printer.local`)
- `file://` pages
- environments without `location` or without a host name (server-side code, workers, jsdom without a URL)

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

WestCoastCharts.use(premium)

// on a dev server: no key needed, no watermark
await new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'sankey', height: 260 },
  series: [{ data: [{ from: 'A', to: 'B', value: 3 }, { from: 'A', to: 'C', value: 2 }] }],
}).render()
```

Staging and preview hosts (`staging.acme.com`, `*.vercel.app`, an intranet IP) are not exempt; include
them in the key's patterns.

`setLicenseKey()` itself has no exemption: on `localhost` it still tells you whether the key would be
accepted, so you can test a key before you deploy it.

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

WestCoastCharts.use(premium)

const result = await WestCoastCharts.setLicenseKey('not-a-real-key')
console.log(result) // null: the dev host hides the watermark, but the key itself is rejected
if (result !== null) throw new Error('expected null')
```

## Browser requirements (ES2020)

The premium bundle is built for ES2020 engines because the offline signature check uses `BigInt`:

| Browser | Minimum |
| --- | --- |
| Chrome, Edge | 80 |
| Firefox | 80 |
| Safari (macOS, iOS) | 14 |

Older engines (Internet Explorer, legacy Edge, Safari 13) cannot load the premium bundle; the core and
the chart types still run there (ES2018, see [Getting started](https://westcoast-charts.com/docs/getting-started/index.md)). The ES2020 build
also keeps the bundle smaller: it saves 2,232 B gzip, more than the 1,708 B gzip the licence check adds
(figures from docs/architecture/size-ledger.md (library repository)).

## FAQ

**Does the licence check contact a server?** No. The key carries its own signature, which is checked
against a public key built into the bundle.

**Which releases does my key cover?** Signature acceptance requires both a trusted signer and the
major-version range in the key. The commercial offer covers major version 1 and available updates
during the paid term. Later major versions are not included unless separately agreed. New RC4-signer
keys require RC4 or a later runtime trusting that signer; older RC3 runtimes do not accept them.

**What happens when a key expires?** An expired technical key brings back the watermark and console
warning; charts keep working. A paid annual entitlement and a key's technical expiry are different:
a key may remain valid to support continued use of an existing licensed deployment after the paid
term ends. This does not extend update delivery or support.

**What does the watermark mean?** It indicates that the runtime has not accepted a key for this
chart and host. It does not determine or limit independent MIT rights. Contact service@pekagi.io
for help with a purchased key or an integration issue.

**What licence applies to each part?** The MIT-marked source keeps its MIT licence. Premium distribution, updates and support have separate business terms. See the [licensing overview](https://westcoast-charts.com/maintenance/#licensing), package [LICENSE](https://westcoast-charts.com/wcc/LICENSE), and [NOTICE](https://westcoast-charts.com/wcc/NOTICE).

## Network layouts and timing diagnostics

The default `plotOptions.network.layout.type: 'force'` computes positions. If your
application already has a layout, choose `'preset'` and provide finite numeric
`x` and `y` on **every** item in `series[].nodes`:

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

WestCoastCharts.use(premium)
const chart = new WestCoastCharts(document.querySelector('#chart'), {
  chart: { type: 'network', height: 400 },
  series: [{
    nodes: [{ id: 'api', x: -60, y: 0 }, { id: 'db', x: 60, y: 0 }],
    edges: [{ source: 'api', target: 'db', weight: 3 }],
  }],
  plotOptions: { network: { layout: { type: 'preset' } } },
})
await chart.render()
const timings = premium.api(chart).performanceStats()
```

Preset coordinates are world coordinates, fitted to the chart viewport without
running a force simulation or moving overlapping nodes apart. Nodes referenced
by edges also need explicit positioned definitions. Missing or invalid positions
produce a one-time warning naming the nodes and fall back to the force layout.
Dragging changes the current view; a subsequent data/options update reads the
supplied positions again. Preset mode is independent of SVG/Canvas rendering.

Canvas changes drawing, not the force-layout calculation. Similar total SVG and
Canvas render times can occur when layout dominates. Compare the same data,
options, browser and hardware before drawing a performance conclusion.
`performanceStats()` returns a diagnostic copy containing `modelMs`, `layoutMs`,
`drawMs`, `labelsMs`, `renderMs`, `frames`, `settled`, the actual `layout`, and
`renderer`. It returns `null` for a chart without network data.

The instrumented phases accumulate for the current renderer instance. `modelMs`
covers model construction, `layoutMs` layout setup and simulation/decluttering,
`drawMs` drawing setup and frames, and `labelsMs` label planning. Updating label
anchors during a frame belongs to drawing. Phase counters are scoped diagnostic
regions rather than an exhaustive partition of all application work; their sum
is not a whole-page load time. `renderMs` measures the initial renderer call.
With animated force layouts the first render is labelled while geometry can
continue moving; listen for `layoutSettled` if you need the settled arrangement.
Timings depend on the environment and are excluded from `describe()` and the
semantic data contract.
