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
<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
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.
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
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 callingdatasets.toggleand colour swatches callingdatasets.setColor. - A settings panel per chart kind, calling
charts.update. Column pickers readstate.numericColumns. - Worksheet tabs from
state.worksheets. - A formula editor using
mathChannels.previewandFUNCTION_REFERENCE.