Guides

Worksheets and charts

Create worksheets and chart models, change their settings, and manage multi-band series.

Worksheets

TypeScript
const wsId = engine.worksheets.add('Suspension')   // added and activated
engine.worksheets.rename(wsId, 'Dampers')
engine.worksheets.activate(wsId)
engine.worksheets.reorder(0, 1)                    // by index
engine.worksheets.setXKey(wsId, 'Distance')        // shared X column
engine.worksheets.remove(wsId)                     // the last worksheet cannot be removed

Add a chart

engine.charts.add(kind, options) creates a chart model in the active worksheet (or options.worksheetId) and returns its id. Multi-band, X-Y and X-Y-Z charts pick columns from the plotted data (X-Y also fits its scales); the other kinds draw nothing until you pass the columns they should use.

TypeScript
const histId = engine.charts.add('histogram', {
  title: 'Speed distribution',
  settings: { histColumn: 'speed', histBins: 40 },
})

const xyId = engine.charts.add('xyscatter', {
  settings: { xyXCol: 'speed', xyYCol: 'rpm', xyColorCol: 'throttle' },
})

Every kind and its setting names are listed in Chart kinds. engine.charts.kinds() (or CHART_KINDS) gives the kinds with labels, in menu order.

Change settings

TypeScript
engine.charts.update(histId, { histLogY: true })           // merge a patch; the kind cannot change
engine.charts.replaceSettings(histId, fullSettingsObject)  // replace (same kind)
engine.charts.rename(histId, 'Speed')                      // null clears the title
engine.charts.setBackground(histId, '#0b1020')             // null follows the theme
engine.charts.remove(histId)

engine.charts.get(histId)                                  // ChartModel
engine.charts.list()                                       // models of the active worksheet

Changing an X-Y scatter column through update re-fits that axis to the new column unless the same patch sets the axis scale.

Annotations

Any chart can carry annotations drawn over it: text, a freehand brush stroke, an arrow and a box. They are part of the chart's settings (settings.annotations, also on ChartModel.annotations) and travel with workspace, worksheet and chart exports. Threshold lines are not annotations: use the charts' alarm lines.

A point is [x, y]. y is a fraction of the chart's height (0 top, 1 bottom). x is a fraction of its width, or, with anchor: 'x', a value on the chart's X axis: the annotation then follows the data when the chart zooms, pans or scrolls. Annotations drawn by the user on a chart with a numeric X axis (multi-band, X-Y, histogram, spectrum, area, CDF and others) are pinned this way.

TypeScript
// Pinned to X = 12.5 s on a multi-band chart, a third of the way down.
const note = engine.charts.addAnnotation(chartId, {
  kind: 'text', anchor: 'x', points: [[12.5, 0.33]], color: '#f5a524', width: 14, text: 'Late braking',
})
engine.charts.addAnnotation(chartId, { kind: 'box', anchor: 'x', points: [[30, 0.2], [34, 0.7]], color: '#3b8beb', width: 2, text: 'Sector 2' })
engine.charts.updateAnnotation(chartId, note, { color: '#f05252', width: 18 })
engine.charts.removeAnnotation(chartId, note)
engine.charts.setAnnotations(chartId, [])          // replace all; [] clears

// Let the user draw: each gesture on the chart makes one annotation of this kind.
view.setAnnotationTool({ kind: 'brush', color: '#22c55e', width: 3 })
view.on('annotationtool', (tool) => { /* null: the user pressed Escape */ })
view.on('annotationselect', (id) => { /* show style controls for it; null when deselected */ })
view.setAnnotationTool(null)                       // the pointer goes back to the chart
KindPoints
textOne point: the top-left of the text. width is the font size in px.
brushThe stroke, as its points in order. width is the stroke width.
arrowTwo points: from, to.
boxTwo opposite corners. Optional text is its label.

On a mounted chart the user selects an annotation by clicking it, drags it to move it, reshapes a box by its corners and an arrow by its ends, double-clicks text or a box to edit its text, and presses Delete (or its × button) to remove it. Style controls of your own can stay next to the chart: give their container the data-kymo-annotation-controls attribute (ANNOTATION_CONTROLS_ATTR) and pressing them keeps the selection, then apply the change with charts.updateAnnotation. view.exportPng() includes the annotations.

Multi-band series

A multi-band chart stacks series in horizontal bands, each with its own Y scale. Series have their own operations so ids and auto-scaling are handled for you.

TypeScript
const mb = engine.charts.add('multiband', { title: 'Engine' })
const s1 = await engine.charts.addSeries(mb, 'rpm')     // Y scale fitted to the data; null if no column is available
const s2 = await engine.charts.addSeries(mb)            // next numeric column not yet plotted

await engine.charts.updateSeries(mb, s1!, { color: '#f5a524', bandFromPct: 50, bandToPct: 100 })
await engine.charts.updateSeries(mb, s2!, { column: 'speed' })   // re-fits the scale
await engine.charts.autoScaleSeries(mb, s1!)
engine.charts.reorderSeries(mb, [s2!, s1!])
engine.charts.removeSeries(mb, s2!)
engine.charts.update(mb, { layout: 'parallel' })        // equal bands in series order

A series is a PlotSeries: column, color, band position in percent, scaleMin/scaleMax, tick steps and an optional alarm line.

Layout metadata

Each model carries a layout rectangle ({ x, y, w, h }) on a 96 × 96 grid. The engine does not position anything with it: it stores it, exports it and re-flows it, so a dashboard grid in your UI can persist its arrangement. Ignore it if your layout is fixed.

TypeScript
engine.charts.add('spectrum', { layout: { x: 48, y: 0, w: 48, h: 48 } })
engine.charts.setLayout(chartId, { x: 0, y: 48, w: 96, h: 48 })
engine.charts.setLayouts(wsId, { [a]: rectA, [b]: rectB })   // after a grid drag
engine.charts.autoLayout(wsId)                               // re-flow onto the automatic grid

Each worksheet keeps its own charts and layouts. To place them, turn each rectangle into percentages of a relatively positioned container:

TypeScript
const pit = engine.worksheets.add('Pit wall')
engine.charts.add('multiband', { worksheetId: pit, layout: { x: 0, y: 0, w: 96, h: 64 } })
engine.charts.add('histogram', { worksheetId: pit, layout: { x: 0, y: 64, w: 48, h: 32 } })
engine.charts.add('spectrum', { worksheetId: pit, layout: { x: 48, y: 64, w: 48, h: 32 } })

const pct = (n: number) => `${(n / 96) * 100}%`
for (const c of engine.charts.list(pit)) {
  const el = document.createElement('div')
  Object.assign(el.style, { position: 'absolute', left: pct(c.layout.x), top: pct(c.layout.y), width: pct(c.layout.w), height: pct(c.layout.h) })
  board.append(el)
  engine.charts.mount(c.id, el)
}

To let people move and resize charts on that board, as the Kymoscope app does, pass the pointer movement to dragLayout and store what it returns. It works in grid steps, keeps the chart on the grid and never shrinks it below its kind's minimum size:

TypeScript
import { dragLayout } from '@kymoscope/sdk'

handle.addEventListener('pointerdown', (down) => {
  const { kind, layout } = engine.charts.get(chartId)
  const onMove = (e: PointerEvent) => engine.charts.setLayout(chartId, dragLayout(
    kind, layout, 'resize',                    // or 'move', from a title bar
    e.clientX - down.clientX, e.clientY - down.clientY,
    board.getBoundingClientRect(),
  ))
  window.addEventListener('pointermove', onMove)
  window.addEventListener('pointerup', () => window.removeEventListener('pointermove', onMove), { once: true })
})

io.exportWorkspace() and io.snapshot() save every worksheet with its charts and layouts; importWorkspace and restore bring them back.

Charts in their own window

charts.openWindow() moves a chart into a separate browser window, for example to put it on a second screen. It keeps streaming, zooming and following settings changes. Call it from a click handler, because browsers block pop-ups opened any other way. The page's styles and theme are copied into the window.

TypeScript
popOut.onclick = () => {
  const win = engine.charts.openWindow(chartId, { width: 900, height: 560 })
  hideCell(chartId)                                  // your grid, your rules

  const back = document.createElement('button')      // your controls go in win.controls
  back.textContent = 'Return to dashboard'
  back.onclick = () => win.close()
  win.controls.append(back)

  win.on('close', () => showCell(chartId))           // fires however the window closes
}

Opening a chart that already has a window focuses that window. The window closes on its own when the chart is removed or the engine is disposed. win.view is a normal ChartView, so exportPng() and events work there too. The window uses the main page's timers, so if the main window is minimised the browser may slow its updates.

Zoom and live window

TypeScript
engine.view.setXRange([120, 180])   // linked X zoom of the active worksheet
engine.view.resetZoom()
engine.view.setLiveWindow(60)       // seconds shown by live charts on a time axis; null = everything

Undo and redo

engine.history steps back and forward through changes to the analysis: worksheets, charts, their settings and annotations, the math channels a workbook carries, and the live window. It does not record loading or closing data, the selection, zoom, switching worksheets, laps (they follow the data) or library math channels (they are saved to your store at once). Changes within 500 ms of each other are one step (dragging a slider, typing a title). Up to 100 steps are kept, in memory only. io.openWorkbook, io.importWorkspace, io.restore and io.reset clear the history; call engine.history.clear() after replacing the analysis yourself.

React
import { useEngine, useEngineState } from '@kymoscope/sdk/react'

function UndoRedo() {
  const engine = useEngine()
  const { canUndo, canRedo } = useEngineState((s) => s.history)
  return (
    <>
      <button disabled={!canUndo} onClick={() => engine.history.undo()}>Undo</button>
      <button disabled={!canRedo} onClick={() => engine.history.redo()}>Redo</button>
    </>
  )
}

// Keyboard: skip when typing in a field
window.addEventListener('keydown', (e) => {
  const t = e.target as HTMLElement
  if (t.closest('input, textarea, select, [contenteditable="true"]')) return
  const mod = e.ctrlKey || e.metaKey
  const k = e.key.toLowerCase()
  // Ctrl+Y is redo on Windows/Linux only: on macOS Cmd+Y opens the browser's history.
  const isMac = /Mac|iPhone|iPad/.test(navigator.platform)
  if (mod && k === 'z' && !e.shiftKey) { e.preventDefault(); engine.history.undo() }
  else if (mod && ((k === 'z' && e.shiftKey) || (k === 'y' && !isMac))) { e.preventDefault(); engine.history.redo() }
})

Without React (the script tag):

HTML
<button id="undo" disabled>Undo</button>
<button id="redo" disabled>Redo</button>
<script>
  const undo = document.getElementById('undo'), redo = document.getElementById('redo')
  undo.onclick = () => engine.history.undo()
  redo.onclick = () => engine.history.redo()
  engine.on('change', (s) => { undo.disabled = !s.history.canUndo; redo.disabled = !s.history.canRedo })
</script>