From 9b6cff853b9c48058513d2abf0f90cde48197662 Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Mon, 10 Aug 2026 16:10:51 -0500 Subject: [PATCH] Keep a pinned comment in place across a timeframe switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A comment placed on a 30m bar slid to the far left the moment the chart switched to 15m. timeToCoordinate answers only for times that are data points on the current series, so a 30m bucket start returns null on another timeframe — and the render was reading null as "off the left edge", which parked every such comment against the left of the pane. Anchors resolve to the bar that contains them instead, found by binary search over the current bars, which is timeframe-independent: an 09:30 note sits on the 09:30 bar at 15m and on the 09:00 bar at 1h. Times genuinely before the first bar or after the last are reported separately, so real off-screen comments still park on the edge they left rather than being confused with unresolved ones. setBars re-renders comments too, since a timeframe switch replaces the bar grid underneath every pinned one. Verified in a browser across 30m to 15m to 1h and back: the anchor stays 08:30 throughout, resolving to the 08:30 bar at 15m and the 08:00 bar at 1h, never edge-parked, and returning to its original coordinate. Edge-parking still fires where it should — scrolled 400 bars away a comment parks right, and comes back when the view returns to live. Co-Authored-By: Claude Opus 5 --- docs/IMPLEMENTATION_PLAN.md | 15 ++++++++++++++ static/chart.js | 39 ++++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 98b17a5..eedb9f1 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1474,3 +1474,18 @@ deleting by type or by string a single button. One CSS trap worth recording: `.trendline-row span { grid-column:2 }` captured the comment row's icon span and dragged it into the text column. Scoped to `span:not(.drawing-icon)`. + +**A comment lost its place when the timeframe changed.** Placed on a 30m bar, +then switched to 15m, it slid to the far left. `timeToCoordinate` answers only +for times that are data points on the current series, so a 30m bucket start +returned `null` on another timeframe — and `null` was being read as "off the +left edge". Anchors are now resolved to the bar that *contains* them, which is +timeframe-independent: an 09:30 note sits on the 09:30 bar at 15m and on the +09:00 bar at 1h. `setBars` also re-renders comments, since a timeframe switch +replaces the grid underneath every pinned one. + +Verified across 30m → 15m → 1h → 30m: the anchor stays 08:30 throughout, +resolving to the 08:30 bar on 15m and the 08:00 bar on 1h, never edge-parked, +and returning to its original x on the way back. Edge-parking still works where +it should — a comment scrolled 400 bars out parks right and comes back on +return to live. diff --git a/static/chart.js b/static/chart.js index 42268b2..4963b23 100644 --- a/static/chart.js +++ b/static/chart.js @@ -229,6 +229,8 @@ class ConfluenceChart { this.chart.timeScale().setVisibleRange(this.pendingView); } requestAnimationFrame(() => this.renderAnchorHandles()); + // The bar grid just changed underneath every pinned comment. + this.renderComments(); } updateBar(bar) { @@ -285,12 +287,13 @@ class ConfluenceChart { node.root.classList.remove('off-left', 'off-right'); continue; } - const x = this.chart.timeScale().timeToCoordinate(comment.anchor_t); + const placed = this.anchorCoordinate(comment.anchor_t); + const x = placed.x; const y = this.candles.priceToCoordinate(comment.anchor_p); // Scrolled past: park it on the edge it went off, pointing that way, // rather than letting it vanish and be hard to find again. - const offLeft = x === null || x < 0; - const offRight = x !== null && x > width; + const offLeft = placed.before || (x !== null && x < 0); + const offRight = placed.after || (x !== null && x > width); node.root.classList.toggle('off-left', offLeft && !offRight); node.root.classList.toggle('off-right', offRight); const clampedX = offLeft ? 4 : offRight ? width - node.root.offsetWidth - 4 : x; @@ -305,6 +308,36 @@ class ConfluenceChart { } } + /** + * Where an anchor time sits on the current timeframe. + * + * timeToCoordinate answers only for times that are data points on the series, + * so a comment placed on a 30m bar returned null the moment the chart + * switched to 15m or 1h — and null was being read as "off the left edge", + * which slid every such comment to the far left. The anchor is resolved to + * the bar that *contains* it instead, which is timeframe-independent: a 09:30 + * note lands on the 09:30 bar at 15m and on the 09:00 bar at 1h. + */ + anchorCoordinate(anchorT) { + if (!this.bars.length) return { x: null, before: false, after: false }; + if (anchorT < this.bars[0].t) return { x: null, before: true, after: false }; + const lastBar = this.bars[this.bars.length - 1]; + if (anchorT > lastBar.t) { + // Past the newest bar is not off the chart — the right edge carries + // whitespace, so ask the scale directly and only park it if it answers. + const beyond = this.chart.timeScale().timeToCoordinate(lastBar.t); + return { x: beyond, before: false, after: beyond === null }; + } + let lo = 0; + let hi = this.bars.length - 1; + while (lo < hi) { + const mid = (lo + hi + 1) >> 1; + if (this.bars[mid].t <= anchorT) lo = mid; + else hi = mid - 1; + } + return { x: this.chart.timeScale().timeToCoordinate(this.bars[lo].t), before: false, after: false }; + } + buildCommentNode(comment) { const root = document.createElement('div'); root.className = 'chart-comment';