aboutsummaryrefslogtreecommitdiffstats
path: root/web/plot.js
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2024-03-20 18:11:42 +0000
committers-ol <s+removethis@s-ol.nu>2024-03-26 18:23:29 +0000
commit2f691d7483b500d76a0955a6311e76a226751efd (patch)
treead607ab90a4e2fa6c6fd3e61cefaabb5440f501d /web/plot.js
parentweb, simulator: support WebSocket transport for development (diff)
downloadt937-serial-2f691d7483b500d76a0955a6311e76a226751efd.tar.gz
t937-serial-2f691d7483b500d76a0955a6311e76a226751efd.zip
web: selectable, hideable plots
Diffstat (limited to 'web/plot.js')
-rw-r--r--web/plot.js118
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);
});