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:
parent
bb84b6e73f
commit
16932f85ec
1 changed files with 34 additions and 6 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue