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