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);