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 | |
| parent | web, simulator: support WebSocket transport for development (diff) | |
| download | t937-serial-2f691d7483b500d76a0955a6311e76a226751efd.tar.gz t937-serial-2f691d7483b500d76a0955a6311e76a226751efd.zip | |
web: selectable, hideable plots
| -rw-r--r-- | web/index.html | 41 | ||||
| -rw-r--r-- | web/main.js | 72 | ||||
| -rw-r--r-- | web/plot.js | 118 | ||||
| -rw-r--r-- | web/style.css | 20 |
4 files changed, 186 insertions, 65 deletions
diff --git a/web/index.html b/web/index.html index 73717a7..7e37ab7 100644 --- a/web/index.html +++ b/web/index.html @@ -5,6 +5,17 @@ <link rel="stylesheet" href="style.css" /> </head> <body> + <svg style="display: none"> + <defs> + <g id="i-eye" fill="currentColor"> + <path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" /> + <path fill-rule="evenodd" d="M1.323 11.447C2.811 6.976 7.028 3.75 12.001 3.75c4.97 0 9.185 3.223 10.675 7.69.12.362.12.752 0 1.113-1.487 4.471-5.705 7.697-10.677 7.697-4.97 0-9.186-3.223-10.675-7.69a1.762 1.762 0 0 1 0-1.113ZM17.25 12a5.25 5.25 0 1 1-10.5 0 5.25 5.25 0 0 1 10.5 0Z" clip-rule="evenodd" /> + </g> + <path id="i-no-eye" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" /> + <path id="i-pencil" fill="currentColor" d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-12.15 12.15a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32L19.513 8.2Z" /> + <path id="i-no-pencil" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" /> + </defs> + </svg> <aside> <section> <h1>device</h1> @@ -53,11 +64,33 @@ </div> </section> <section class="grow"> - <h1>profiles</h1> + <h1> + <span class="grow">reflow profile</span> + <span> + <button id="profile_show" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-eye" /></svg></button> + <button id="profile_edit" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-pencil" /></svg></button> + </span> + </h1> <div> - <button id="load" class="grow">load</button> - <button id="save" class="grow">save</button> - <button id="create" class="grow">create</button> + <button id="profile_load" class="grow">load</button> + <button id="profile_save" class="grow">save</button> + <button id="profile_create" class="grow">create</button> + </div> + <select id="storedprofile" multiple class="grow"> + </select> + </section> + <section class="grow"> + <h1> + <span class="grow">paste specification</span> + <span> + <button id="paste_show" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-eye" /></svg></button> + <button id="paste_edit" class="icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><use href="#i-pencil" /></svg></button> + </span> + </h1> + <div> + <button id="paste_load" class="grow">load</button> + <button id="paste_save" class="grow">save</button> + <button id="paste_create" class="grow">create</button> </div> <select id="storedprofile" multiple class="grow"> </select> diff --git a/web/main.js b/web/main.js index 37ef19e..b93b2da 100644 --- a/web/main.js +++ b/web/main.js @@ -32,18 +32,27 @@ let START_TIME = null; let END_TIME = null; let TIMEOUT; -const PLOTS = { - 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'), -}; +plot.addPlot('paste', { + color: 'slategray', + data: [[0, 24]], + interactive: true, +}); +plot.addPlot('profile', { + color: 'teal', + data: [[0, 24]], + interactive: true, +}); +plot.addPlot('temp1', { color: 'plum' }); +plot.addPlot('temp2', { color: 'salmon' }); +plot.addPlot('temp3', { color: 'sienna' }); +plot.addPlot('tempavg', { color: 'rebeccapurple' }); const elements = [ 'temps_chamber', 'temps_controller', - 'connect', 'status', 'profile', 'start', 'read', 'write', + 'connect', 'status', + 'profile', 'start', 'read', 'write', + 'profile_load', 'profile_save', 'profile_create', 'profile_show', 'profile_edit', + 'paste_load', 'paste_save', 'paste_create', 'paste_show', 'paste_edit', 'heat', 'heat_mode', 'cool', 'cool_mode', ]; const el = Object.fromEntries(elements.map(id => [id, document.getElementById(id)])); @@ -57,6 +66,11 @@ const updateUI = () => { el.read.disabled = !ready; el.write.disabled = !ready; + el.profile_show.firstElementChild.firstElementChild.href.baseVal = plot.PLOTS.profile.visible ? '#i-eye' : '#i-no-eye'; + el.paste_show.firstElementChild.firstElementChild.href.baseVal = plot.PLOTS.paste.visible ? '#i-eye' : '#i-no-eye'; + el.profile_edit.firstElementChild.firstElementChild.href.baseVal = plot.ACTIVE === 'profile' ? '#i-pencil' : '#i-no-pencil'; + el.paste_edit.firstElementChild.firstElementChild.href.baseVal = plot.ACTIVE === 'paste' ? '#i-pencil' : '#i-no-pencil'; + const end = END_TIME ?? Date.now(); const duration = START_TIME && Math.floor((end - START_TIME) / 1000); @@ -83,6 +97,8 @@ const updateUI = () => { el.status.innerText = `profile ${+el.profile.value + 1} - ${duration}s`; break; } + + plot.update(); }; updateUI(); @@ -133,12 +149,11 @@ const onMessage = async (msg) => { if (!STATUS || STATUS === 'connecting') { STATUS = 'idle'; } else if (STATUS === 'running' || STATUS == 'manual') { - const t = PLOTS.temp1.data.length; - PLOTS.temp1.data.push([t, temp1]); - PLOTS.temp2.data.push([t, temp2]); - PLOTS.temp3.data.push([t, temp3]); - PLOTS.tempavg.data.push([t, (temp1 + temp2) / 2]); - plot.update(); + const t = plot.PLOTS.temp1.data.length; + plot.PLOTS.temp1.data.push([t, temp1]); + plot.PLOTS.temp2.data.push([t, temp2]); + plot.PLOTS.temp3.data.push([t, temp3]); + plot.PLOTS.tempavg.data.push([t, (temp1 + temp2) / 2]); } break; } @@ -194,11 +209,10 @@ el.start.onclick = () => { START_TIME = Date.now(); END_TIME = null; - PLOTS.temp1.data.length = 0; - PLOTS.temp2.data.length = 0; - PLOTS.temp3.data.length = 0; - PLOTS.tempavg.data.length = 0; - plot.update(); + plot.PLOTS.temp1.data.length = 0; + plot.PLOTS.temp2.data.length = 0; + plot.PLOTS.temp3.data.length = 0; + plot.PLOTS.tempavg.data.length = 0; if (el.profile.value !== '') { const i = +el.profile.value; @@ -214,6 +228,7 @@ el.start.onclick = () => { STATUS = 'idle'; END_TIME = Date.now(); } + updateUI(); }; @@ -297,3 +312,20 @@ if (window.location.hash !== '#ws') { console.log('opened websocket'); }; } +el.profile_show.onclick = () => { + plot.PLOTS.profile.visible = !plot.PLOTS.profile.visible; + updateUI(); +}; +el.paste_show.onclick = () => { + plot.PLOTS.pase.visible = !plot.PLOTS.pase.visible; + updateUI(); +}; +el.profile_edit.onclick = () => { + plot.toggleActive('profile'); + updateUI(); +}; +el.paste_edit.onclick = () => { + plot.toggleActive('paste'); + updateUI(); +}; +}; 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); }); diff --git a/web/style.css b/web/style.css index a397fce..8bbf5cf 100644 --- a/web/style.css +++ b/web/style.css @@ -21,6 +21,14 @@ aside { overflow: hidden; resize: horizontal; } +aside h1 { + display: flex; + gap: 1rem; + font-size: 1.5em; +} +aside h1 span { + display: flex; +} aside section { display: flex; flex-direction: column; @@ -48,6 +56,18 @@ aside div label + label { flex-grow: 1; } +button.icon { + border: none; + background: none; + appearance: none; + font: inherit; + min-width: 0; + cursor: pointer; +} +button.icon svg { + height: 0.5lh; +} + #plot { flex: 1 0 400px; user-select: none; |
