Move daily context to the left price scale

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 <noreply@anthropic.com>
This commit is contained in:
Chris Amow 2026-08-10 12:07:52 -05:00
parent bb84b6e73f
commit 16932f85ec

View file

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