Skip to content

Chart grid ​

createChartGrid from @chart-on/ui puts one to eight shells in a grid of resizable cells. The charts are in a link group, so the crosshair, pan and zoom follow across them, and one toolbar above the grid acts on the active chart.

ts
import { createChartGrid } from '@chart-on/ui';
import '@chart-on/ui/style.css';
import type { ExampleContext } from './types';
import { sampleBars } from './sample-data';

export default function mount(el: HTMLElement, { dark }: ExampleContext) {
  // Four linked charts: pan, zoom or move the crosshair on one and the others follow.
  // Drag the bars between the cells, or focus one and use the arrow keys, to resize.
  const grid = createChartGrid(el, {
    layout: '2x2',
    theme: dark ? 'dark' : 'light',
    cell: { rail: false, bottomBar: false },
    setup(shell, index) {
      shell.chart.addCandles({ title: `SAMPLE ${index + 1}` }).setData(sampleBars(300, { seed: index + 1 }));
      shell.chart.fitContent();
    },
  });

  return () => grid.destroy();
}
ts
import { createChartGrid } from '@chart-on/ui';
import '@chart-on/ui/style.css';

const grid = createChartGrid(el, {
  layout: '2x2',                       // preset, see below; default '1x1'
  cell: { rail: false },               // shell options of every cell, or (index) => options
  setup(shell, index) {                // each new cell, also those a later setLayout adds
    shell.chart.setDataSource(source);
    void shell.chart.setSymbol(symbols[index], '1h');
    return () => {};                   // optional: runs when the cell goes
  },
  toolbar: [...],                      // acts on the active chart; omit for none
  link: { symbol: false },             // link group options; false links nothing
  theme: 'dark',                       // grid and cells; 'dark' | 'light' | 'system'
});

grid.cells;      // ChartShell per cell, by row
grid.charts;     // their charts
grid.links;      // the LinkGroup: grid.links.applyOptions({ interval: true })
grid.active;     // the active cell's shell; grid.activeIndex is its index
grid.destroy();

Layouts ​

Presets are named columns by rows:

PresetCells
1x1One chart
2x1, 3x1Side by side
1x2, 1x3Stacked
2x2Four
3x2Six
4x2Eight

grid.setLayout('3x2') switches preset. Cells keep their place by index: a smaller preset destroys cells from the end, a larger one creates new cells with the cell options and runs setup for them. New cells join the link group and take the first chart's linked state. If a new chart cannot be created (no WebGL2, or setup throws), setLayout destroys the cells it made, keeps the previous layout and rethrows the error. chartGridLayouts lists the columns and rows of each preset, for a layout menu.

Resizing ​

A bar between each pair of columns and rows resizes them. Drag it, or focus it with Tab and use the arrow keys (Shift for bigger steps, Home and End for the limits). Each bar is a separator with its position in aria-valuenow, in percent. A column or row never gets smaller than a tenth of the grid. Only the two cells beside the bar change.

grid.sizes holds the column widths and row heights as fractions that sum to 1; grid.setSizes({ columns: [2, 1] }) sets them (scaled to sum to 1). A layout change resets them to equal sizes.

Each shell sizes its narrow layout by its own cell, not by the window: a cell's compact layout (features.railSheet) follows the cell's width.

The active chart ​

The active cell is the one the grid toolbar acts on: toolbar callbacks get its chart, and isActive and isChecked are read again when it changes. A click or focus in a cell makes it active; grid.setActive(index, { focus: true }) does so from code and moves keyboard focus to its chart. Tab moves through the charts and the resize bars. With more than one cell, the active one has an outline (--pc-grid-active, the accent colour by default) and aria-current="true".

A segmented or menu item shows a value that may differ per chart. Update it when the active chart changes:

ts
grid.on('active:change', ({ shell }) => {
  grid.toolbar?.setValue('interval', shell.chart.data?.requested?.interval ?? '1h');
});

The gridLayout() preset is a layout menu that switches grid.setLayout and follows layout:change by itself:

ts
import { createChartGrid, gridLayout } from '@chart-on/ui';

const grid = createChartGrid(el, { layout: '2x2', toolbar: [gridLayout(['1x1', '2x1', '2x2'])] });

Custom toolbar items render once, with the chart active at that moment; read grid.active when they act.

Events ​

EventPayloadWhen
active:change{ index, shell }Another cell became active
layout:change{ layout }setLayout or loadLayout changed the preset
sizes:change{ columns, rows }A bar moved, or setSizes changed the sizes

grid.on(event, listener) returns an unsubscribe function.

Save and restore ​

grid.serializeLayout() returns the preset, the sizes, the active cell, the link options and each chart's layout. Store it as JSON and pass it back to grid.loadLayout(saved):

ts
localStorage.setItem('grid', JSON.stringify(grid.serializeLayout({ visibleRange: true })));

const { errors, ready } = grid.loadLayout(JSON.parse(localStorage.getItem('grid')!));
await ready; // each saved symbol loaded

Options go to each chart's serializeLayout and loadLayout. Parts that are not valid are skipped and listed in errors, with the chart's index in the part (charts[1].panes[0]). A missing sizes.columns or sizes.rows list means equal sizes. loadLayout throws a ChartError with code invalid-layout, changing nothing, only when the data is not a saved grid at all; when a new chart of the saved preset cannot be created, it rethrows that error, also changing nothing.

Performance ​

Cells draw through the shared WebGL context unless their chart options set gl, so eight charts take one context of the page's budget. Each shell has its own rail, bottom bar and dialogs; leave out the parts a small cell does not need (cell: { rail: false, bottomBar: false }).

Theming ​

The grid root is also a .pc-shell, so it takes the same tokens and data-theme. Two more tokens:

TokenDefaultUse
--pc-grid-guttervar(--pc-gap)Space between cells, where the resize bars sit
--pc-grid-activevar(--pc-accent)Outline of the active cell