Skip to content

Frameworks and plain pages ​

@chart-on/core and @chart-on/ui are plain TypeScript with no framework dependency. This page covers three things:

  • Plain pages: a bundler, or a single script tag with no build step.
  • Server rendering: what is safe to import and call on a server.
  • The wrapper contract: the rules every framework wrapper follows, so components behave the same whichever framework you use. Follow them too when you write your own wrapper.

Ready-made wrappers ship as @chart-on/react, see Component wrapper, @chart-on/vue, see Template component wrapper, and @chart-on/svelte, see Compiled component wrapper.

Plain pages ​

With a bundler ​

Import the packages as ES modules. Any bundler that understands package.json exports works, with no plugin or configuration.

ts
import { createChart, createMockDataSource, dataSource } from '@chart-on/core';

const chart = createChart(document.getElementById('chart')!, { plugins: [dataSource()] });
chart.setDataSource(createMockDataSource());
await chart.setSymbol('DEMO', '1h');

With the shell, also import its stylesheet once:

ts
import { createChartShell } from '@chart-on/ui';
import '@chart-on/ui/style.css';

const shell = createChartShell(document.getElementById('chart')!, { theme: 'system' });

With a script tag ​

The script-tag build bundles the engine, the shell and its icons into a single file. pnpm build writes it to packages/ui/dist/browser/:

FileUse
charton.iife.jsA classic script that defines the global charton. Works from any page, also opened straight from disk.
charton.jsThe same as one ES module, for <script type="module"> and import.
style.cssThe shell's stylesheet. The engine alone does not need it.

Each comes with a source map. Both scripts are production builds, without the development warnings; import the packages through a bundler to get them. Copy the files you use next to your page:

html
<link rel="stylesheet" href="./style.css" />
<div id="chart" style="height: 480px"></div>

<script src="./charton.iife.js"></script>
<script>
  const { createChartShell, createMockDataSource } = charton;
  const shell = createChartShell(document.getElementById('chart'), { theme: 'dark' });
  shell.chart.setDataSource(createMockDataSource());
  shell.chart.setSymbol('DEMO', '1h');
</script>

Or as a module:

html
<script type="module">
  import { createChart, createMockDataSource } from './charton.js';
  const chart = createChart(document.getElementById('chart'));
</script>

charton holds every export of @chart-on/core and @chart-on/ui, so the rest of this guide applies unchanged: createChart(...) becomes charton.createChart(...). A complete page is in examples/script-tag/index.html.

Browsers load module scripts only over http(s), so charton.js needs a local server, while charton.iife.js also works from file://.

Use the packages instead of this build when you have a bundler: they share one copy of the engine with @chart-on/indicators and plugins, and your bundler leaves out what you do not use.

Server rendering ​

Frameworks that render on a server import your components there, so importing charton must not need a browser. It doesn't:

  • Importing @chart-on/core, @chart-on/core/testing, @chart-on/core/internal, @chart-on/ui, @chart-on/indicators and @chart-on/ta reads no window, document, navigator, storage or WebGL. A test checks this for every entry.
  • Pure helpers (formatting, intervals, indicator math, parseCsv, layout migration) work on a server.
  • isWebGL2Supported() returns false on a server instead of throwing.
  • createChart and createChartShell need a DOM. Call them only in the browser, after the element exists: in the framework's mount hook, never during rendering.

Importing @chart-on/ui/style.css is a stylesheet import; let your framework's CSS handling take care of it, as for any other package stylesheet.

The wrapper contract ​

A framework wrapper is a thin component around createChart (or createChartShell). Every wrapper follows these rules, so apps can move between frameworks and get the same behaviour.

1. Create on mount, destroy on unmount ​

  • Create the chart or shell when the component's element is in the page: the mount hook, never the render function, and never on a server.
  • Call destroy() when the component unmounts. It stops the frame loop, timers, observers and listeners, releases the WebGL context and removes the chart's element.
  • destroy() is idempotent on both Chart and ChartShell. Development modes that mount, unmount and mount again leave nothing behind: tests mount and destroy a chart and a shell 100 times each and check that no WebGL context, observer, global listener, timer or animation frame is left.
  • Do not keep a destroyed chart around for a later mount. Create a new one.
ts
// The shape of any wrapper, written without a framework.
function mountChart(el: HTMLElement, props: ChartProps) {
  const chart = createChart(el, props.options);
  return {
    chart,
    update: (next: ChartProps) => updateChart(chart, props, next),
    unmount: () => chart.destroy(),
  };
}

2. Update options in place ​

Changing a prop never re-creates the chart. Re-creating would lose the view, drawings, indicators and data, and costs a new WebGL context.

  • Chart options go through chart.applyOptions(patch). It is a deep-partial update, so pass only what changed. Passing the whole options object again is also fine.
  • Shell settings go through the shell's runtime methods: setTheme, setPalette, applyFeatures.
  • Symbol and interval go through chart.setSymbol(symbol, interval); a data source through chart.setDataSource(source).
  • A few choices are fixed when the chart is created, for example gl: 'own' | 'shared'. A wrapper reads them only at mount. If an app needs to change one, it remounts the component on purpose, for example by changing a key.

3. Update data through the series ​

Data never travels through a remount.

  • Data you hold yourself: keep the series handle from chart.addCandles() (or addLine, addHistogram, …) and call series.setData(bars) for a new data set, or series.update(bar) for a live tick.
  • Data from a data source: the chart loads, pages and streams it; the wrapper only passes the source and the symbol.

4. Expose the instance ​

The component gives the app the Chart (or ChartShell) it created, through the framework's usual way of reaching a component instance. The wrapper never hides part of the imperative API: anything the app can do with a chart it created itself, it can do through a wrapper. The instance exists only between mount and unmount, so it is empty during server rendering and before mount.

5. Forward events as framework callbacks ​

  • Each event the app listens to is subscribed with chart.on(name, listener) (or shell.on), which returns an unsubscribe function. Call every unsubscribe on unmount; destroy() also drops the chart's listeners.
  • Expose events in the framework's own style (callback props or component events), named after the chart's noun:verb event names. The payload is passed on unchanged.
  • Subscribe only to events the app actually listens to: some, like crosshair:move and frame:render, fire on every pointer move or frame.
  • When the app passes a new callback for the same event, call the newest one without re-subscribing, for example by keeping the latest callback in a variable the listener reads.
ts
// Events, written without a framework: the listener always calls the newest handler.
function forwardEvents(chart: Chart, handlers: () => Partial<Record<keyof ChartEvents, (payload: never) => void>>) {
  const names = Object.keys(handlers()) as (keyof ChartEvents)[];
  const offs = names.map((name) => chart.on(name, (payload) => handlers()[name]?.(payload as never)));
  return () => offs.forEach((off) => off());
}

6. Keep the engine framework-free ​

A wrapper depends on @chart-on/core (and @chart-on/ui for the shell) as peer dependencies. The engine and the shell never depend on a framework, so they stay usable from plain pages and from every wrapper at once.

Checklist for wrapper authors ​

  • [ ] The chart is created in the mount hook and destroyed in the unmount hook.
  • [ ] Mounting, unmounting and mounting again (development double mounting) leaves one chart and one WebGL context.
  • [ ] Changing any prop except the creation-only ones keeps the same Chart instance.
  • [ ] Data changes call setData / update on the series, not a remount.
  • [ ] The instance is reachable by the app.
  • [ ] Event props subscribe through on and unsubscribe on unmount; a new callback is used without re-subscribing.
  • [ ] The component renders on a server without errors (it outputs only its container element).