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.
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:
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/:
| File | Use |
|---|---|
charton.iife.js | A classic script that defines the global charton. Works from any page, also opened straight from disk. |
charton.js | The same as one ES module, for <script type="module"> and import. |
style.css | The 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:
<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:
<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/indicatorsand@chart-on/tareads nowindow,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()returnsfalseon a server instead of throwing.createChartandcreateChartShellneed 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 bothChartandChartShell. 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.
// 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 throughchart.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()(oraddLine,addHistogram, …) and callseries.setData(bars)for a new data set, orseries.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)(orshell.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:verbevent names. The payload is passed on unchanged. - Subscribe only to events the app actually listens to: some, like
crosshair:moveandframe: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.
// 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
Chartinstance. - [ ] Data changes call
setData/updateon the series, not a remount. - [ ] The instance is reachable by the app.
- [ ] Event props subscribe through
onand 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).