diff --git a/docs/implementation.md b/docs/implementation.md index 48bd379..25bf5de 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1069,3 +1069,8 @@ so nine elapsed minutes occupy nine logical slots and the line remains straight. Gaps over 30 minutes stay compressed, preserving the established treatment of the 65-minute settlement break and weekends. A browser regression pins both the nine-slot spacing and equal historical/future screen slope. + +Production then showed that whitespace-only points could still disappear from the +shared scale. The time-scale owner now uses transparent zero-valued points with an +`autoscaleInfoProvider` that returns null. They remain invisible and cannot affect +price range, but Lightweight Charts retains their timestamps as real series data. diff --git a/static/chart.js b/static/chart.js index 3f91356..eb9a6cb 100644 --- a/static/chart.js +++ b/static/chart.js @@ -233,6 +233,7 @@ class ConfluenceChart { // something to label and so timeToCoordinate answers out there. this.futureSpace = this.chart.addSeries(LightweightCharts.LineSeries, { priceScaleId: 'right', + color: 'rgba(0,0,0,0)', lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, @@ -531,7 +532,7 @@ class ConfluenceChart { if (step <= 0) return []; const slots = []; for (let index = 1; index <= ConfluenceChart.FUTURE_SLOTS; index += 1) { - slots.push({ time: last.t + index * step }); + slots.push({ time: last.t + index * step, value: 0 }); } return slots; } @@ -544,7 +545,7 @@ class ConfluenceChart { const gap = bars[index].t - bars[index - 1].t; if (gap <= step || gap > ConfluenceChart.MAX_INTRADAY_GAP_SECONDS) continue; for (let time = bars[index - 1].t + step; time < bars[index].t; time += step) { - slots.push({ time }); + slots.push({ time, value: 0 }); } } return [...slots, ...this.futureTimes(bars)]; @@ -585,7 +586,7 @@ class ConfluenceChart { return; } const futureTime = bar.t + ConfluenceChart.FUTURE_SLOTS * step; - this.futureSpace.update({ time: futureTime }); + this.futureSpace.update({ time: futureTime, value: 0 }); for (const level of this.levels) { if (level.kind !== 'manual' || level.hidden || ConfluenceChart.isFlat(level) || futureTime < level.anchor_t || futureTime > (level.cutoff_t ?? Infinity)) continue; diff --git a/tests/e2e/trendline.test.mjs b/tests/e2e/trendline.test.mjs index 1fe7d69..fdc75a6 100644 --- a/tests/e2e/trendline.test.mjs +++ b/tests/e2e/trendline.test.mjs @@ -201,11 +201,15 @@ test('a short final data gap does not kink a future trendline', historicalSlope: (current.y - previous.y) / (current.x - previous.x), futureSlope: (future.y - current.y) / (future.x - current.x), historicalSlots: (current.x - previous.x) / (future.x - current.x), + missingSlotOwned: c.futureSpace.data() + .some(point => Number(point.time) === previousTime + minute), }; }); assert.ok(Math.abs(result.historicalSlots - 9) < 0.01, `a nine-minute data gap occupied ${result.historicalSlots} slots`); + assert.equal(result.missingSlotOwned, true, + 'the invisible scale series discarded the missing intraday timestamp'); assert.ok(Math.abs(result.historicalSlope - result.futureSlope) < 1e-9, `line kinked from ${result.historicalSlope} to ${result.futureSlope}`); assertNoPageErrors(page, assert);