diff options
| author | s-ol <s+removethis@s-ol.nu> | 2024-03-20 18:11:42 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2024-03-26 18:23:29 +0000 |
| commit | 2f691d7483b500d76a0955a6311e76a226751efd (patch) | |
| tree | ad607ab90a4e2fa6c6fd3e61cefaabb5440f501d /web/plot.js | |
| parent | web, simulator: support WebSocket transport for development (diff) | |
| download | t937-serial-2f691d7483b500d76a0955a6311e76a226751efd.tar.gz t937-serial-2f691d7483b500d76a0955a6311e76a226751efd.zip | |
web: selectable, hideable plots
Diffstat (limited to 'web/plot.js')
| -rw-r--r-- | web/plot.js | 118 |
1 files changed, 77 insertions, 41 deletions
diff --git a/web/plot.js b/web/plot.js index 9095ec3..98d8d88 100644 --- a/web/plot.js +++ b/web/plot.js @@ -1,12 +1,11 @@ import './lib/d3.v7.min.js'; -let POINTS = []; - const x = d3.scaleLinear([0, 120]).nice(30); -const y = d3.scaleLinear([0, 350]); +const y = d3.scaleLinear([0, 350]).nice(25); const line = d3.line(d => x(d[0]), d => y(d[1])); -const margin = 2 * parseFloat(getComputedStyle(document.documentElement).fontSize); +const marginx = 3 * parseFloat(getComputedStyle(document.documentElement).fontSize); +const marginy = 2 * parseFloat(getComputedStyle(document.documentElement).fontSize); const svg = d3.select('#plot'); @@ -16,32 +15,59 @@ const xa = svg.append('g'); // Add the y-axis. const ya = svg.append('g'); -const PLOTS = {}; +export const PLOTS = {}; +export let ACTIVE = null; const sec = d3.format('02'); const min = d3.format(' '); const minsec = v => `${min(Math.floor(v / 60))}:${sec(Math.floor(v % 60))}`; const degc = v => `${Math.floor(v)}°C`; -export const addPlot = (name, color, data=[], interactive=false) => { - const path = svg.append('path') - // .attr('cursor', 'copy') +export const addPlot = (name, options={}) => { + const plot = Object.assign({ + color: 'black', + data: [], + interactive: false, + visible: true, + }, options); + + plot.root = svg.append('g') + .attr('fill', plot.color) + .attr('stroke', plot.color) + .on('click', (e) => { + if (!plot.interactive) return; + if (name === ACTIVE) return; + + e.preventDefault(); + toggleActive(name); + update(); + }); + + plot.path = plot.root.append('path') .attr('fill', 'none') - .attr('stroke', color) .attr('stroke-width', '2'); - const points = interactive && ( - svg.append('g') - .attr('cursor', 'grab') - ); - - PLOTS[name] = { - color, - data, - points, - path, - }; - return PLOTS[name]; + if (plot.interactive) { + plot.shadow = plot.root.append('path') + .attr('fill', 'none') + .attr('stroke', 'transparent') + .attr('stroke-width', '8'); + + plot.points = plot.root.append('g') + .attr('stroke', 'none') + } + + PLOTS[name] = plot; + return plot; +}; + +export const toggleActive = (name) => { + if (ACTIVE === name) { + ACTIVE = null; + } else if (PLOTS[name]) { + PLOTS[name].root.raise(); + ACTIVE = name; + } }; export const update = (transition=true) => { @@ -55,15 +81,21 @@ export const update = (transition=true) => { xa.transition().duration(dur).call(d3.axisBottom(x).tickFormat(minsec)); ya.transition().duration(dur).call(d3.axisLeft(y).tickFormat(degc)); - for (const { path, points, data, color } of Object.values(PLOTS)) { + for (const name of Object.keys(PLOTS)) { + const { root, path, shadow, points, data, color, interactive, visible } = PLOTS[name]; + root + .attr('cursor', interactive && name !== ACTIVE ? 'pointer' : 'default') + .attr('display', visible ? null : 'none'); + path.transition().duration(dur).attr('d', line(data)); + shadow && shadow.transition().duration(dur).attr('d', line(data)); points && points.selectAll('circle') .data(data) .join('circle') - .attr('fill', color) - .attr('r', 5) .attr('cx', (d) => x(d[0])) .attr('cy', (d) => y(d[1])) + .attr('r', name === ACTIVE ? 5 : 3) + .attr('cursor', name === ACTIVE ? 'grab' : null) .on('dblclick', function (e, d) { e.stopPropagation(); if (d[0] === 0) return; @@ -74,9 +106,12 @@ export const update = (transition=true) => { }) .call( d3.drag() + .filter((e) => !e.ctrlKey && !e.button && name === ACTIVE) .on('start', () => points.attr('cursor', 'grabbing')) .on('drag', function (e, d) { - const [time, temp] = [x.invert(e.x), y.invert(e.y)]; + let [time, temp] = [x.invert(e.x), y.invert(e.y)]; + time = Math.max(1, time); + temp = Math.max(0, Math.min(temp, 400)); const ci = data.indexOf(d); if (data[ci-1] && data[ci-1][0] > time) { @@ -105,8 +140,8 @@ const onresize = () => { .attr('width', width) .attr('height', height); - x.range([margin, width - margin]); - y.range([height - margin, margin]); + x.range([marginx, width - marginx]); + y.range([height - marginy, marginy]); xa.transition().call(d3.axisBottom(x).tickFormat(minsec)); ya.transition().call(d3.axisLeft(y).tickFormat(degc)); for (const { path, data, points } of Object.values(PLOTS)) { @@ -116,8 +151,8 @@ const onresize = () => { .attr('cy', (d) => y(d[1])); } - xa.attr('transform', `translate(0, ${height - margin})`); - ya.attr('transform', `translate(${margin}, 0)`); + xa.attr('transform', `translate(0, ${height - marginy})`); + ya.attr('transform', `translate(${marginx}, 0)`); }; window.addEventListener('resize', onresize); @@ -129,13 +164,13 @@ const crosshair = svg.append('g') .attr('stroke-width', 0.5); crosshair.append('text') .attr('id', 'ctextx') - .attr('y', margin) + .attr('y', marginy) .attr('dx', '0.32em') .attr('dy', '1em') .attr('fill', 'currentColor'); crosshair.append('text') .attr('id', 'ctexty') - .attr('x', margin) + .attr('x', marginx) .attr('dx', '0.32em') .attr('dy', '-0.32em') .attr('fill', 'currentColor');; @@ -153,26 +188,27 @@ svg const width = svg.node().clientWidth; const height = svg.node().clientHeight; - const inRange = margin < px && px < width - margin && - margin < py && py < height - margin; + const inRange = marginx < px && px < width - marginx && + marginy < py && py < height - marginy; crosshair.attr('display', inRange ? null : 'none').lower(); crosshair.select('#ctextx').text(minsec(time)).attr('x', px); crosshair.select('#ctexty').text(degc(temp)).attr('y', py); - crosshair.select('#clinex').attr('x1', px).attr('x2', px).attr('y1', margin).attr('y2', height - margin); - crosshair.select('#cliney').attr('y1', py).attr('y2', py).attr('x1', margin).attr('x2', width - margin); + crosshair.select('#clinex').attr('x1', px).attr('x2', px).attr('y1', marginy).attr('y2', height - marginy); + crosshair.select('#cliney').attr('y1', py).attr('y2', py).attr('x1', marginx).attr('x2', width - marginx); }) .on('dblclick', (e) => { + const plot = ACTIVE && PLOTS[ACTIVE]; + if (!plot) return; + const [px, py] = d3.pointer(e); const [time, temp] = [x.invert(px), y.invert(py)]; - const current = PLOTS.setpoint; - - const ni = current.data.findLastIndex(([tt, tp]) => tt <= time); - if (current.data[ni][0] === time) { - current.data[ni][1] = temp; + const ni = plot.data.findLastIndex(([tt, tp]) => tt <= time); + if (plot.data[ni][0] === time) { + plot.data[ni][1] = temp; } else { - current.data.splice(ni + 1, 0, [time, temp]); + plot.data.splice(ni + 1, 0, [time, temp]); } update(false); }); |
