From ac2943f2482d57cbd2ebc2089c0a267ff162011e Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Wed, 19 Aug 2026 09:33:18 +0000 Subject: [PATCH] fib look and feel tweaks --- static/chart.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++- static/style.css | 2 +- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/static/chart.js b/static/chart.js index 45d1a8f..c250311 100644 --- a/static/chart.js +++ b/static/chart.js @@ -140,7 +140,7 @@ class FibLevelsPrimitive { color, width, dashed: !major, - label: `${level.label} ${level.price.toFixed(2)}`, + label: `fib ${level.label} ${level.price.toFixed(2)}`, }); } } @@ -218,6 +218,8 @@ class ConfluenceChart { this.lineHitTarget = null; this.lineFocus = null; this.lineFocusTimer = null; + this.fibFocus = null; + this.fibFocusTimer = null; this.draggingLine = null; this.lineMoveListener = null; this.lineUpListener = null; @@ -507,6 +509,10 @@ class ConfluenceChart { this.lineFocus.classList.add('chart-line-focus'); this.lineFocus.setAttribute('hidden', ''); handles.appendChild(this.lineFocus); + this.fibFocus = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + this.fibFocus.classList.add('chart-fib-focus'); + this.fibFocus.setAttribute('hidden', ''); + handles.appendChild(this.fibFocus); this.lineHitTarget = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); this.lineHitTarget.classList.add('chart-line-hit'); this.lineHitTarget.setAttribute('hidden', ''); @@ -1993,6 +1999,11 @@ class ConfluenceChart { focusLine(id) { this.setSelectedLine(id); + const fib = this.fibs.find(item => item.id === id); + if (fib) { + this.focusFib(fib); + return; + } const level = this.levels.find(value => value.id === id && value.kind === 'manual'); if (!level) return; if (ConfluenceChart.isFlat(level)) { @@ -2020,6 +2031,52 @@ class ConfluenceChart { this.lineFocusTimer = setTimeout(() => this.lineFocus.setAttribute('hidden', ''), 800); } + focusFib(fib) { + if (!this.fibFocus || !this.overlayLayer) return; + this.fibFocus.replaceChildren(); + const width = this.overlayLayer.getBoundingClientRect().width; + const originX = this.coordinateAtTime(fib.anchor_t); + const start = originX == null ? 0 : Math.max(0, originX); + const color = fib.color || '#c8992f'; + const lineWidth = Math.max(1, Number(fib.line_width) || 2); + this.fibFocus.style.setProperty('--line-focus-start-width', `${lineWidth + 10}px`); + this.fibFocus.style.setProperty('--line-focus-width', `${lineWidth}px`); + const y0 = this.candles.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.candles.priceToCoordinate(endP); + if (originX != null && y0 != null && y1 != null) { + const rail = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + rail.classList.add('chart-line-focus'); + rail.setAttribute('x1', originX); + rail.setAttribute('x2', originX); + rail.setAttribute('y1', y0); + rail.setAttribute('y2', y1); + rail.setAttribute('stroke', color); + this.fibFocus.appendChild(rail); + } + for (const level of ConfluenceChart.fibPrices(fib)) { + const y = this.candles.priceToCoordinate(level.price); + if (y == null) continue; + const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + line.classList.add('chart-line-focus'); + line.setAttribute('x1', start); + line.setAttribute('x2', width); + line.setAttribute('y1', y); + line.setAttribute('y2', y); + line.setAttribute('stroke', color); + this.fibFocus.appendChild(line); + } + this.fibFocus.removeAttribute('hidden'); + for (const line of this.fibFocus.querySelectorAll('line')) { + line.style.animation = 'none'; + void line.getBoundingClientRect(); + line.style.animation = ''; + } + clearTimeout(this.fibFocusTimer); + this.fibFocusTimer = setTimeout(() => this.fibFocus.setAttribute('hidden', ''), 800); + } + focusComment(id) { const node = this.commentNodes.get(id); if (!node) return; @@ -2906,6 +2963,7 @@ class ConfluenceChart { this.detachPointers('anchor'); this.detachPointers('line'); clearTimeout(this.lineFocusTimer); + clearTimeout(this.fibFocusTimer); for (const node of this.commentNodes.values()) clearTimeout(node.focusTimer); if (this.chartEl && this.clickListener) this.chartEl.removeEventListener('click', this.clickListener); if (this.chartEl && this.contextListener) this.chartEl.removeEventListener('contextmenu', this.contextListener); diff --git a/static/style.css b/static/style.css index 970813d..ecd3536 100644 --- a/static/style.css +++ b/static/style.css @@ -13,7 +13,7 @@ main { display:grid; grid-template-columns:minmax(0, 1fr) 300px; gap:16px; } button { border:1px solid var(--line); background:transparent; color:var(--muted); padding:6px 11px; font:inherit; cursor:pointer; }button.active { color:var(--bg); background:var(--accent); border-color:var(--accent); } .timeframes { display:flex; flex-wrap:wrap; justify-content:flex-end; }.timeframes button+button { border-left:0; } .drawing-tools { min-height:38px; padding:5px 12px; display:flex; align-items:center; gap:9px; border-bottom:1px solid var(--line); color:var(--muted); font-size:10px; }.drawing-tools button,.drawing-tools select,.drawing-tools .line-name { padding:4px 8px; font-size:10px; }.drawing-tools select,.drawing-tools .line-name { background:var(--panel); color:var(--fg); border:1px solid var(--line); }.drawing-tools .line-name { width:130px; font:inherit; }.drawing-tools label { display:flex; gap:4px; align-items:center; }.drawing-tools input { accent-color:var(--accent); } -#chart { position:relative; height:calc(100vh - 190px); min-height:420px; touch-action:pan-y; }.chart-preview,.chart-line-bridges,.chart-handles { position:absolute; inset:0; width:100%; height:100%; overflow:hidden; pointer-events:none; }.chart-preview,.chart-line-bridges { z-index:4; }.chart-line-bridges line { stroke-linecap:round; }.chart-handles { z-index:6; }.chart-preview line[hidden],.chart-anchor[hidden],.chart-line-hit[hidden],.chart-line-focus[hidden] { display:none; }.chart-line-focus { stroke-linecap:round; pointer-events:none; animation:line-focus .75s ease-out forwards; }.chart-line-hit { fill:none; stroke:transparent; stroke-width:16px; pointer-events:stroke; cursor:move; touch-action:none; }.chart-anchor { stroke:var(--panel); stroke-width:2px; cursor:grab; pointer-events:all; touch-action:none; }.chart-anchor:active { cursor:grabbing; }.chart-tooltip { position:absolute; z-index:5; padding:4px 7px; border:1px solid var(--line); background:var(--panel); color:var(--fg); font-size:10px; white-space:pre-line; pointer-events:none; }.chart-tooltip[hidden] { display:none; } +#chart { position:relative; height:calc(100vh - 190px); min-height:420px; touch-action:pan-y; }.chart-preview,.chart-line-bridges,.chart-handles { position:absolute; inset:0; width:100%; height:100%; overflow:hidden; pointer-events:none; }.chart-preview,.chart-line-bridges { z-index:4; }.chart-line-bridges line { stroke-linecap:round; }.chart-handles { z-index:6; }.chart-preview line[hidden],.chart-anchor[hidden],.chart-line-hit[hidden],.chart-line-focus[hidden],.chart-fib-focus[hidden] { display:none; }.chart-line-focus { stroke-linecap:round; pointer-events:none; animation:line-focus .75s ease-out forwards; }.chart-line-hit { fill:none; stroke:transparent; stroke-width:16px; pointer-events:stroke; cursor:move; touch-action:none; }.chart-anchor { stroke:var(--panel); stroke-width:2px; cursor:grab; pointer-events:all; touch-action:none; }.chart-anchor:active { cursor:grabbing; }.chart-tooltip { position:absolute; z-index:5; padding:4px 7px; border:1px solid var(--line); background:var(--panel); color:var(--fg); font-size:10px; white-space:pre-line; pointer-events:none; }.chart-tooltip[hidden] { display:none; } .current-price-pulse { position:absolute; left:0; right:0; z-index:5; height:3px; transform:translateY(-1.5px); overflow:hidden; opacity:0; background:repeating-linear-gradient(110deg,transparent 0 8px,color-mix(in srgb,var(--accent) 55%,white) 8px 13px,transparent 13px 19px); mask-image:linear-gradient(90deg,transparent 0,#000 8%,#000 100%); pointer-events:none; }.current-price-pulse::after { content:''; position:absolute; top:-2px; bottom:-2px; left:-24%; width:24%; background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--accent) 35%,white),transparent); opacity:.8; }.current-price-pulse.active { opacity:.9; animation:current-price-flow 1.5s linear infinite; }.current-price-pulse.active::after { animation:current-price-scan 2.8s linear infinite; }@keyframes current-price-flow { to { background-position-x:57px; } }@keyframes current-price-scan { to { transform:translateX(520%); } } .chart-ohlc { position:absolute; top:6px; left:8px; z-index:5; padding:2px 6px; border:1px solid var(--line); background:color-mix(in srgb,var(--panel) 90%,transparent); color:var(--fg); font-size:10px; font-variant-numeric:tabular-nums; pointer-events:none; white-space:nowrap; }.chart-ohlc.up { color:var(--green); }.chart-ohlc.down { color:var(--red); }.chart-ohlc[hidden] { display:none; } .projection-diagnostic { position:absolute; top:8px; left:8px; z-index:9; margin:0; padding:5px 7px; border:1px solid var(--line); background:color-mix(in srgb,var(--panel) 92%,transparent); color:var(--fg); font:9px/1.35 "IBM Plex Mono", monospace; white-space:pre; }