Keep level overlays on the candle grid so bars keep their width
The hourly chart drew 160 candles as unreadable slivers. Level points are sampled on their own timeframe — VWAP every minute, the daily averages once a session — and every distinct timestamp claims a slot on the chart's shared scale. A minute-resolution VWAP on an hourly chart therefore spread 160 candles across 908 slots, five to six times wider than the candles they belonged to: before 1h inView 160 slots 908 ratio 5.46 after 1h inView 160 slots 161 ratio 1.01 Snapping ma and vwap points onto the candle grid fixes the density without changing the line: points collapse onto the candle at or after them, and the newest wins. Trendlines keep the raw path — lineData interpolates between two anchors, and snapping those would move the geometry the user drew. Two details worth keeping. Points past the final candle are clamped onto it rather than passed through: on a daily chart every one of VWAP's ~760 minute points falls after the last candle's session open, and letting them keep their own times put all 760 straight back on the scale. And the viewport is re-applied once after the level series have loaded, because setBars runs before them and the chart holds the width it derived from the previous timeframe's density — consumed rather than reapplied, so the minutely VWAP resync cannot yank the view back from wherever it has been panned. The method is snapPointsToBars, not snapToBars: this.snapToBars already exists as the "Snap to highs/lows" boolean, and the assignment silently replaced the prototype method with true. Verified in a browser across all six timeframes — 1m, 5m, 15m, 30m, 1h and 1d each show 160 candles at ratio 0.99–1.01 with no console errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9d63e6482e
commit
a15ea00c03
1 changed files with 51 additions and 3 deletions
|
|
@ -32,6 +32,7 @@ class ConfluenceChart {
|
|||
this.toolDownListener = null;
|
||||
this.toolMoveListener = null;
|
||||
this.toolUpListener = null;
|
||||
this.pendingView = null;
|
||||
}
|
||||
|
||||
static TICK = 0.25;
|
||||
|
|
@ -154,10 +155,11 @@ class ConfluenceChart {
|
|||
const last = bars[bars.length - 1];
|
||||
// Keep the old five bars of right-hand breathing room, in seconds.
|
||||
const step = bars.length > 1 ? last.t - bars[bars.length - 2].t : 60;
|
||||
this.chart.timeScale().setVisibleRange({
|
||||
this.pendingView = {
|
||||
from: bars[Math.max(0, bars.length - 160)].t,
|
||||
to: last.t + step * 5,
|
||||
});
|
||||
};
|
||||
this.chart.timeScale().setVisibleRange(this.pendingView);
|
||||
}
|
||||
requestAnimationFrame(() => this.renderAnchorHandles());
|
||||
}
|
||||
|
|
@ -201,6 +203,42 @@ class ConfluenceChart {
|
|||
}
|
||||
}
|
||||
|
||||
// Level points are sampled on their own timeframe — VWAP every minute, the
|
||||
// daily averages once a session — and every distinct timestamp claims its own
|
||||
// slot on the chart's shared scale. Left raw, a minute-resolution VWAP spread
|
||||
// 160 hourly candles across 908 slots and drew them as unreadable slivers.
|
||||
// Snapping onto the candle grid preserves the line's shape while keeping the
|
||||
// scale one slot per candle, which is what makes the bars their proper width.
|
||||
snapPointsToBars(points) {
|
||||
if (!this.bars.length || !points.length) return points;
|
||||
const times = this.bars.map(bar => bar.t);
|
||||
const last = times[times.length - 1];
|
||||
const byTime = new Map();
|
||||
for (const point of points) {
|
||||
// Clamped, not passed through: on a daily chart every one of VWAP's ~760
|
||||
// minute points falls after the last candle's session open, and letting
|
||||
// them keep their own times put all 760 back on the scale. Projection to
|
||||
// the right of the last bar is handled by the caller instead.
|
||||
if (point.time >= last) {
|
||||
byTime.set(last, point.value);
|
||||
continue;
|
||||
}
|
||||
let lo = 0;
|
||||
let hi = times.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (times[mid] < point.time) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
// Points older than the window collapse onto the first candle; the newest
|
||||
// of them wins, which is the value in force when the window opens.
|
||||
byTime.set(times[lo], point.value);
|
||||
}
|
||||
return [...byTime.entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([time, value]) => ({ time, value }));
|
||||
}
|
||||
|
||||
syncLevels(levels) {
|
||||
this.levels = levels;
|
||||
this.syncPriceLines(levels);
|
||||
|
|
@ -238,7 +276,7 @@ class ConfluenceChart {
|
|||
}
|
||||
let data;
|
||||
if (hasPoints) {
|
||||
data = (level.points || []).map(([time, value]) => ({ time, value }));
|
||||
data = this.snapPointsToBars((level.points || []).map(([time, value]) => ({ time, value })));
|
||||
const latestTime = this.bars[this.bars.length - 1]?.t;
|
||||
const latestValue = data[data.length - 1]?.value;
|
||||
if (latestTime != null && latestValue != null && latestTime > data[data.length - 1].time) {
|
||||
|
|
@ -249,6 +287,16 @@ class ConfluenceChart {
|
|||
}
|
||||
entry.series.setData(data);
|
||||
}
|
||||
// Re-anchor once, after the level series have reshaped the scale. setBars
|
||||
// runs before them, so the width it asked for was derived from the previous
|
||||
// timeframe's point density and the chart holds that width as new series
|
||||
// arrive. Consumed rather than reapplied every time: VWAP resyncs a level
|
||||
// every minute, and re-anchoring on each would yank the view back from
|
||||
// wherever the user had panned it.
|
||||
if (this.pendingView) {
|
||||
this.chart.timeScale().setVisibleRange(this.pendingView);
|
||||
this.pendingView = null;
|
||||
}
|
||||
this.renderAnchorHandles();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue