From 1c3a852a034c72a9e362e97b80783951a3ba6bdf Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Wed, 19 Aug 2026 09:29:48 +0000 Subject: [PATCH] better fib left start --- docs/implementation.md | 3 + static/chart.js | 161 +++++++++++++++++++++++++++++++++-------- 2 files changed, 135 insertions(+), 29 deletions(-) diff --git a/docs/implementation.md b/docs/implementation.md index c883ec1..b1f03f5 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1216,3 +1216,6 @@ shares numbering, hide, delete and colour with other drawings, but it is not a confluence level. The chart draws native price lines at 0, 23.6, 38.2, 50, 61.8, 78.6 and 100 between the two prices. 0% is the first click. + +Levels no longer span the full chart: they start at the first click and run +to the right, with a vertical rail at the origin so the swing is obvious. diff --git a/static/chart.js b/static/chart.js index cf4f382..45d1a8f 100644 --- a/static/chart.js +++ b/static/chart.js @@ -72,6 +72,120 @@ class RthLinesPrimitive { } } +class FibLevelsPrimitive { + constructor(locateTime) { + this._locate = locateTime; + this._fibs = []; + this._series = null; + this._requestUpdate = null; + this._items = []; + this._view = { + update: () => this._sync(), + renderer: () => this._renderer, + }; + this._renderer = { draw: target => this._draw(target) }; + } + + attached(param) { + this._series = param.series; + this._requestUpdate = param.requestUpdate; + } + + detached() { + this._series = null; + this._requestUpdate = null; + } + + setFibs(fibs) { + this._fibs = fibs || []; + if (this._requestUpdate) this._requestUpdate(); + } + + updateAllViews() { + this._view.update(); + } + + paneViews() { + return [this._view]; + } + + _sync() { + const items = []; + if (!this._series) { + this._items = []; + return; + } + for (const fib of this._fibs) { + if (fib.hidden) continue; + const x0 = this._locate(fib.anchor_t); + const y0 = this._series.priceToCoordinate(Number(fib.anchor_p)); + const endP = Number(fib.anchor_p) + Number(fib.slope || 0) + * (Number(fib.last_t) - Number(fib.anchor_t)); + const y1 = this._series.priceToCoordinate(endP); + const width = Math.max(1, Number(fib.line_width) || 2); + const color = fib.color || '#c8992f'; + if (x0 != null && y0 != null && y1 != null) { + items.push({ + kind: 'rail', x: x0, y0, y1, color, width, + }); + } + for (const level of ConfluenceChart.fibPrices(fib)) { + const y = this._series.priceToCoordinate(level.price); + if (y == null) continue; + const major = level.r === 0 || level.r === 0.5 || level.r === 0.618 || level.r === 1; + items.push({ + kind: 'level', + x0, + y, + color, + width, + dashed: !major, + label: `${level.label} ${level.price.toFixed(2)}`, + }); + } + } + this._items = items; + } + + _draw(target) { + if (!this._items.length) return; + target.useMediaCoordinateSpace(scope => { + const ctx = scope.context; + const right = scope.mediaSize.width; + ctx.save(); + ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace'; + for (const item of this._items) { + if (item.kind === 'rail') { + ctx.strokeStyle = item.color; + ctx.lineWidth = item.width; + ctx.setLineDash([]); + ctx.globalAlpha = 0.85; + ctx.beginPath(); + ctx.moveTo(item.x + 0.5, item.y0); + ctx.lineTo(item.x + 0.5, item.y1); + ctx.stroke(); + continue; + } + const start = item.x0 == null ? 0 : Math.max(0, item.x0); + if (start > right) continue; + ctx.strokeStyle = item.color; + ctx.fillStyle = item.color; + ctx.lineWidth = item.width; + ctx.setLineDash(item.dashed ? [5, 4] : []); + ctx.globalAlpha = 0.9; + ctx.beginPath(); + ctx.moveTo(start, item.y + 0.5); + ctx.lineTo(right, item.y + 0.5); + ctx.stroke(); + ctx.setLineDash([]); + ctx.globalAlpha = 1; + ctx.fillText(item.label, right - ctx.measureText(item.label).width - 6, item.y - 4); + } + ctx.restore(); + }); + } +} + class ConfluenceChart { constructor() { this.chart = null; @@ -135,7 +249,7 @@ class ConfluenceChart { this.rthPrimitive = null; this.rthEnabled = true; this.fibs = []; - this.fibLines = new Map(); + this.fibPrimitive = null; this.fibPreview = null; this.futureSpace = null; this.snapDot = null; @@ -327,6 +441,8 @@ class ConfluenceChart { // something to label and so timeToCoordinate answers out there. this.rthPrimitive = new RthLinesPrimitive(); this.candles.attachPrimitive(this.rthPrimitive); + this.fibPrimitive = new FibLevelsPrimitive(time => this.coordinateAtTime(time)); + this.candles.attachPrimitive(this.fibPrimitive); this.futureSpace = this.chart.addSeries(LightweightCharts.LineSeries, { priceScaleId: 'right', color: 'rgba(0,0,0,0)', @@ -706,33 +822,7 @@ class ConfluenceChart { } syncFibLines() { - if (!this.candles) return; - const wanted = new Set(); - for (const fib of this.fibs) { - if (fib.hidden) continue; - for (const level of ConfluenceChart.fibPrices(fib)) { - const id = `${fib.id}:${level.r}`; - wanted.add(id); - const width = Math.max(1, Number(fib.line_width) || 2); - const major = level.r === 0 || level.r === 0.5 || level.r === 0.618 || level.r === 1; - const options = { - price: level.price, - color: fib.color || '#c8992f', - lineWidth: width, - lineStyle: major ? LightweightCharts.LineStyle.Solid : LightweightCharts.LineStyle.Dashed, - axisLabelVisible: true, - title: `${level.label} ${level.price.toFixed(2)}`, - }; - const existing = this.fibLines.get(id); - if (existing) existing.applyOptions(options); - else this.fibLines.set(id, this.candles.createPriceLine(options)); - } - } - for (const [id, line] of this.fibLines) { - if (wanted.has(id)) continue; - this.candles.removePriceLine(line); - this.fibLines.delete(id); - } + if (this.fibPrimitive) this.fibPrimitive.setFibs(this.fibs.filter(fib => !fib.hidden)); } setComments(comments) { @@ -1946,11 +2036,24 @@ class ConfluenceChart { this.fibPreview.removeAttribute('hidden'); this.fibPreview.replaceChildren(); const width = this.overlayLayer?.clientWidth || this.chartEl.clientWidth; + const originX = start.t == null ? 0 : (this.coordinateAtTime(start.t) ?? 0); + const yStart = this.candles.priceToCoordinate(start.p); + const yEnd = this.candles.priceToCoordinate(end.p); + if (yStart != null && yEnd != null) { + const rail = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + rail.setAttribute('x1', originX); + rail.setAttribute('x2', originX); + rail.setAttribute('y1', yStart); + rail.setAttribute('y2', yEnd); + rail.setAttribute('stroke', '#c8992f'); + rail.setAttribute('stroke-width', 2); + this.fibPreview.appendChild(rail); + } for (const item of ConfluenceChart.FIB_RATIOS) { const y = this.candles.priceToCoordinate(start.p + (end.p - start.p) * item.r); if (y == null) continue; const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); - line.setAttribute('x1', 0); + line.setAttribute('x1', originX); line.setAttribute('x2', width); line.setAttribute('y1', y); line.setAttribute('y2', y);