aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2024-03-13 21:38:46 +0000
committers-ol <s+removethis@s-ol.nu>2024-03-13 21:38:46 +0000
commit49cc2223a419eb86610287e0ed9ff73deb95cd84 (patch)
tree960d876a2f6b8740d28b1ae6970ee0f7899d430a
parentweb: reset plot when entering manual mode (diff)
downloadt937-serial-49cc2223a419eb86610287e0ed9ff73deb95cd84.tar.gz
t937-serial-49cc2223a419eb86610287e0ed9ff73deb95cd84.zip
web: editable setpoint curve
-rw-r--r--web/main.js10
-rw-r--r--web/plot.js84
-rw-r--r--web/style.css1
3 files changed, 81 insertions, 14 deletions
diff --git a/web/main.js b/web/main.js
index c682567..d164f70 100644
--- a/web/main.js
+++ b/web/main.js
@@ -33,10 +33,12 @@ let END_TIME = null;
let TIMEOUT;
const PLOTS = {
- temp1: plot.addPlot('temp1'),
- temp2: plot.addPlot('temp2'),
- temp3: plot.addPlot('temp3'),
- tempavg: plot.addPlot('tempavg'),
+ paste: plot.addPlot('paste', 'slategray', [[0, 24]], true),
+ setpoint: plot.addPlot('setpoint', 'teal', [[0, 24]], true),
+ temp1: plot.addPlot('temp1', 'plum'),
+ temp2: plot.addPlot('temp2', 'salmon'),
+ temp3: plot.addPlot('temp3', 'sienna'),
+ tempavg: plot.addPlot('tempavg', 'rebeccapurple'),
};
const elements = [
diff --git a/web/plot.js b/web/plot.js
index ae826d5..4c2320f 100644
--- a/web/plot.js
+++ b/web/plot.js
@@ -22,31 +22,77 @@ const sec = d3.format('02');
const min = d3.format(' ');
const minsec = v => `${min(Math.floor(v / 60))}:${sec(v % 60)}`;
-export const addPlot = (name) => {
+export const addPlot = (name, color, data=[], interactive=false) => {
const path = svg.append('path')
+ // .attr('cursor', 'copy')
.attr('fill', 'none')
- .attr('stroke', 'black')
+ .attr('stroke', color)
.attr('stroke-width', '2');
+ const points = interactive && (
+ svg.append('g')
+ .attr('cursor', 'grab')
+ );
+
PLOTS[name] = {
- data: [],
+ color,
+ data,
+ points,
path,
};
return PLOTS[name];
};
-
-export const update = () => {
+export const update = (transition=true) => {
const times = d3.merge(Object.values(PLOTS).map(p => p.data.map(d => d[0])));
const temps = d3.merge(Object.values(PLOTS).map(p => p.data.map(d => d[1])));
x.domain([0, d3.max([...times, 120])]).nice(30);
y.domain([0, d3.max([...temps, 200])]).nice(25);
- xa.transition().call(d3.axisBottom(x).tickFormat(minsec))
- ya.transition().call(d3.axisLeft(y))
- for (const { path, data } of Object.values(PLOTS)) {
- path.transition().attr('d', line(data));
+ const dur = transition ? null : 0;
+
+ xa.transition().duration(dur).call(d3.axisBottom(x).tickFormat(minsec))
+ ya.transition().duration(dur).call(d3.axisLeft(y))
+ for (const { path, points, data, color } of Object.values(PLOTS)) {
+ path.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]))
+ .on('dblclick', function (e, d) {
+ e.stopPropagation();
+
+ const i = data.indexOf(d);
+ data.splice(i, 1);
+ update(false);
+ })
+ .call(
+ d3.drag()
+ .on('start', () => points.attr('cursor', 'grabbing'))
+ .on('drag', function (e, d) {
+ const [time, temp] = [x.invert(e.x), y.invert(e.y)];
+
+ const ci = data.indexOf(d);
+ if (data[ci-1] && data[ci-1][0] > time) {
+ data.splice(ci-1, 0, ...data.splice(ci, 1));
+ } else if (data[ci+1] && data[ci+1][0] < time) {
+ data.splice(ci+1, 0, ...data.splice(ci, 1));
+ }
+
+ d[0] = time;
+ d[1] = temp;
+
+ d3.select(this)
+ .attr('cx', (d) => x(d[0]))
+ .attr('cy', (d) => y(d[1]));
+ update(false);
+ })
+ .on('end', () => points.attr('cursor', 'grab'))
+ );
}
};
@@ -61,8 +107,11 @@ const onresize = () => {
y.range([height - margin, margin]);
xa.transition().call(d3.axisBottom(x).tickFormat(minsec))
ya.transition().call(d3.axisLeft(y))
- for (const { path, data } of Object.values(PLOTS)) {
+ for (const { path, data, points } of Object.values(PLOTS)) {
path.transition().attr('d', line(data));
+ points && points.transition().selectAll('circle')
+ .attr('cx', (d) => x(d[0]))
+ .attr('cy', (d) => y(d[1]));
}
xa.attr('transform', `translate(0, ${height - margin})`);
@@ -70,5 +119,20 @@ const onresize = () => {
};
window.addEventListener('resize', onresize);
+svg.on('dblclick', (e) => {
+ 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;
+ } else {
+ current.data.splice(ni + 1, 0, [time, temp]);
+ }
+ update(false);
+});
+
onresize();
update();
diff --git a/web/style.css b/web/style.css
index 8bd7380..a397fce 100644
--- a/web/style.css
+++ b/web/style.css
@@ -50,4 +50,5 @@ aside div label + label {
#plot {
flex: 1 0 400px;
+ user-select: none;
}