Guides

Building your own UI

A worked example: a toolbar with Import, an Add chart menu, a chart context menu and PNG export.

This is the core of what the Kymoscope app does around its charts, in plain DOM code. The same pattern works in any framework: read state, render, call the engine on user actions.

The page

HTML
<link rel="stylesheet" href="/vendor/kymoscope/styles/charts.css" />
<div class="toolbar">
  <label class="button">Import <input id="files" type="file" multiple hidden accept=".csv,.tsv,.dat,.txt,.mf4,.mdf,.dbc,.edf,.bdf,.xdf,.tdms,.tdms_index,.h5,.hdf5,.mat,.cfg,.cff" /></label>
  <select id="add-chart"><option value="">Add chart…</option></select>
</div>
<div id="grid"></div>
<menu id="chart-menu" hidden></menu>
<script src="/vendor/kymoscope/kymoscope-embed.js"></script>

Import and Add chart

TypeScript
const engine = Kymoscope.createEngine({ licenseKey: LICENSE_KEY })

document.getElementById('files').addEventListener('change', async (e) => {
  await engine.datasets.importFiles([...e.target.files])
  e.target.value = ''
})

const addChart = document.getElementById('add-chart')
for (const { kind, label } of Kymoscope.CHART_KINDS) addChart.add(new Option(label, kind))
addChart.addEventListener('change', async () => {
  const kind = addChart.value
  addChart.value = ''
  const id = engine.charts.add(kind)
  if (kind === 'multiband') await engine.charts.addSeries(id)
})

Render the charts

Mount a view per chart model of the active worksheet, and keep the views in sync as models come and go.

TypeScript
const grid = document.getElementById('grid')
const views = new Map()   // chartId -> { el, view }

function sync() {
  const s = engine.getState()
  const charts = s.worksheets.find((w) => w.id === s.activeWorksheetId).charts
  const ids = new Set(charts.map((c) => c.id))
  for (const [id, v] of views) if (!ids.has(id)) { v.view.destroy(); v.el.remove(); views.delete(id) }
  for (const c of charts) {
    if (views.has(c.id)) continue
    const el = document.createElement('section')
    el.className = 'chart-card'
    el.innerHTML = '<h3></h3><div class="plot" style="height: 280px"></div><p class="hint"></p>'
    grid.append(el)
    const view = engine.charts.mount(c.id, el.querySelector('.plot'))
    view.on('status', (st) => {
      el.querySelector('.hint').textContent = st === 'ready' ? '' : st === 'no-data' ? 'No data loaded' : 'Nothing to draw yet'
    })
    view.on('contextmenu', (e) => openMenu(c.id, view, e))
    views.set(c.id, { el, view })
  }
  for (const c of charts) views.get(c.id).el.querySelector('h3').textContent = c.title ?? engine.charts.label(c.kind)
}
engine.subscribe(sync)
sync()

Context menu and export

TypeScript
const menu = document.getElementById('chart-menu')

function openMenu(chartId, view, e) {
  const items = [
    ['Export PNG', async () => download(await view.exportPng(), 'chart.png')],
    ['Reset zoom', () => engine.view.resetZoom()],
    ['Remove chart', () => engine.charts.remove(chartId)],
  ]
  if (e.zoomXRange) items.unshift(['Zoom to selection', () => engine.view.setXRange(e.zoomXRange)])
  menu.replaceChildren(...items.map(([label, run]) => {
    const b = document.createElement('button')
    b.textContent = label
    b.onclick = () => { menu.hidden = true; run() }
    return b
  }))
  Object.assign(menu.style, { position: 'fixed', left: e.clientX + 'px', top: e.clientY + 'px' })
  menu.hidden = false
}
document.addEventListener('pointerdown', (e) => { if (!menu.contains(e.target)) menu.hidden = true })

function download(blob, filename) {
  const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: filename })
  a.click()
  URL.revokeObjectURL(a.href)
}

What to add next

  • A dataset list from state.datasets, with checkboxes calling datasets.toggle and colour swatches calling datasets.setColor.
  • A settings panel per chart kind, calling charts.update. Column pickers read state.numericColumns.
  • Worksheet tabs from state.worksheets.
  • A formula editor using mathChannels.preview and FUNCTION_REFERENCE.