From 16932f85ec149cb78340978978a6739f0228b983 Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Mon, 10 Aug 2026 12:07:52 -0500 Subject: [PATCH] Move daily context to the left price scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The right scale was crowding: prior-day levels, session VWAP and five daily moving averages competing with the live price and hand-drawn intraday levels. Daily and session context moves left, and the right is left for intraday. A price scale takes its range from the series on it, so moving levels across would have drawn them against a different range and put them at the wrong height — the failure this codebase has already paid for once. A transparent candlestick mirror on the left scale gives it exactly the same input as the right. Verified in a browser: the same price maps to the identical y coordinate through both scales, a delta of zero pixels. Prior-day levels move; hand-drawn price levels stay on the right, since those are the intraday markers the space is being cleared for. priceScaleId is fixed when a series is created, so it is passed at construction and left out of the options reapplied afterwards. Co-Authored-By: Claude Opus 5 --- static/chart.js | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/static/chart.js b/static/chart.js index 5f0cf6d..7869438 100644 --- a/static/chart.js +++ b/static/chart.js @@ -82,12 +82,24 @@ class ConfluenceChart { }), }, rightPriceScale: { borderVisible: false }, + // Daily context lives on the left, intraday on the right. + leftPriceScale: { visible: true, borderVisible: false }, }); this.candles = this.chart.addSeries(LightweightCharts.CandlestickSeries, { upColor: '#27825c', downColor: '#bd4545', borderVisible: true, borderUpColor: '#1d6849', borderDownColor: '#963737', wickUpColor: '#1d6849', wickDownColor: '#963737', }); + // A price scale derives its range from the series on it, so levels moved to + // the left would be drawn against a different range and sit at the wrong + // height. This transparent copy of the candles gives the left scale exactly + // the same input as the right, which keeps one price at one y. + this.leftMirror = this.chart.addSeries(LightweightCharts.CandlestickSeries, { + priceScaleId: 'left', + upColor: 'transparent', downColor: 'transparent', borderVisible: false, + wickUpColor: 'transparent', wickDownColor: 'transparent', + lastValueVisible: false, priceLineVisible: false, + }); this.resizeObserver = new ResizeObserver(() => { this.chart.applyOptions({ width: el.clientWidth, height: el.clientHeight }); requestAnimationFrame(() => this.renderAnchorHandles()); @@ -171,7 +183,9 @@ class ConfluenceChart { setBars(bars) { this.bars = bars; - this.candles.setData(bars.map(this.toCandle)); + const candleData = bars.map(this.toCandle); + this.candles.setData(candleData); + this.leftMirror.setData(candleData); // Anchored by time, not by logical index. A logical index addresses the // chart's *shared* scale — the union of every series' time points — not // this array. The daily MAs land straight after with hundreds of points @@ -199,6 +213,7 @@ class ConfluenceChart { const last = this.bars[this.bars.length - 1]; if (last && bar.t < last.t) return; this.candles.update(this.toCandle(bar)); + this.leftMirror.update(this.toCandle(bar)); if (this.bars.length && this.bars[this.bars.length - 1].t === bar.t) this.bars[this.bars.length - 1] = bar; else this.bars.push(bar); this.renderAnchorHandles(); @@ -215,9 +230,9 @@ class ConfluenceChart { syncPriceLines(levels) { const flat = levels.filter(level => ConfluenceChart.isFlat(level) && !level.hidden); const wanted = new Set(flat.map(level => level.id)); - for (const [id, line] of this.priceLines) { + for (const [id, entry] of this.priceLines) { if (!wanted.has(id)) { - this.candles.removePriceLine(line); + entry.host.removePriceLine(entry.line); this.priceLines.delete(id); } } @@ -231,8 +246,14 @@ class ConfluenceChart { title: level.label, }; const existing = this.priceLines.get(level.id); - if (existing) existing.applyOptions(options); - else this.priceLines.set(level.id, this.candles.createPriceLine(options)); + if (existing) existing.line.applyOptions(options); + else { + // Prior-day levels are daily context and move to the left scale; a + // hand-drawn price level is intraday and keeps the right, which is the + // side being kept clear for it. + const host = level.kind === 'horizontal' ? this.leftMirror : this.candles; + this.priceLines.set(level.id, { host, line: host.createPriceLine(options) }); + } } } @@ -302,7 +323,14 @@ class ConfluenceChart { autoscaleInfoProvider: () => null, }; if (!entry) { - entry = { series: this.chart.addSeries(LightweightCharts.LineSeries, options) }; + // Daily averages and session VWAP are daily/session context, so their + // last-value labels belong on the left. priceScaleId is fixed at + // creation, which is why it is not in the options reapplied below. + const scale = (isMa || level.kind === 'vwap') ? 'left' : 'right'; + entry = { + series: this.chart.addSeries(LightweightCharts.LineSeries, + { ...options, priceScaleId: scale }), + }; this.levelSeries.set(level.id, entry); } else { entry.series.applyOptions(options);