From 0e4525aa1fe29ded56362e9c5b955651ca65a5d4 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Mon, 12 Oct 2026 20:24:06 +0200 Subject: [PATCH] fix(web): keep live markers off old history segments Signed-off-by: Fredrik Ahlgren --- .changeset/live-history-gap-tip.md | 5 +++++ web/app.js | 6 +++--- web/history-quality.test.mjs | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 .changeset/live-history-gap-tip.md diff --git a/.changeset/live-history-gap-tip.md b/.changeset/live-history-gap-tip.md new file mode 100644 index 000000000..3911eb18b --- /dev/null +++ b/.changeset/live-history-gap-tip.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Pulse only the latest known sample in the Live chart. Earlier points before a missing measurement stay still, and an unknown latest sample has no live marker. diff --git a/web/app.js b/web/app.js index b0fb9ed64..40ae1a193 100644 --- a/web/app.js +++ b/web/app.js @@ -1349,11 +1349,11 @@ ctx.stroke(); ctx.setLineDash([]); - // Cache the right-most point for un-clipped pulse drawing - if (sr.width >= 2) { + }); + // Only the current sample may pulse. Earlier segments remain still. + if (sr.width >= 2 && pts.length && sr.data.length === chartHistory.timestamps.length) { liveTips.push({ x: pts[pts.length - 1].x, y: pts[pts.length - 1].y, color: sr.color }); } - }); }); // ---- Forward-looking forecast (dashed lines from plan) ---- diff --git a/web/history-quality.test.mjs b/web/history-quality.test.mjs index 67b8776a4..fd874f660 100644 --- a/web/history-quality.test.mjs +++ b/web/history-quality.test.mjs @@ -31,3 +31,25 @@ test('daily view renders unknown home use and a measured grid total',()=>{ assert.match(els['energy-history-insight'].innerHTML,/Incomplete measurements/); assert.doesNotMatch(els['energy-history-insight'].innerHTML,/%/); }); + +function liveTipsFor(data, timestamps=data.map((_,i)=>i)) { + const noop=()=>{}; + const ctx={createLinearGradient:()=>({addColorStop:noop}),beginPath:noop,moveTo:noop,lineTo:noop,closePath:noop,fill:noop,stroke:noop,setLineDash:noop}; + const c={ctx,series:[{data,color:'#ef4444',width:2}],chartHistory:{timestamps},tsToX:x=>x,valToY:y=>y,pad:{top:0},plotH:100,hexAlpha:()=>''}; + vm.createContext(c); + const start=app.indexOf(' var liveTips = [];'); + const end=app.indexOf(' // ---- Forward-looking forecast',start); + assert.ok(start>=0 && end>start); + vm.runInContext(app.slice(start,end),c); + return Array.from(c.liveTips,tip=>({x:tip.x,y:tip.y})); +} + +test('live chart pulses only the current segment after a measurement gap',()=>{ + assert.deepEqual(liveTipsFor([100,200,null,300,400]),[{x:4,y:400}]); + assert.deepEqual(liveTipsFor([100,200,null,300]),[{x:3,y:300}]); +}); + +test('live chart leaves old endpoints quiet when the latest measurement is unknown',()=>{ + assert.deepEqual(liveTipsFor([100,200,null,300,400,null]),[]); + assert.deepEqual(liveTipsFor([100,200],[0,1,2]),[]); +});