From aae47b66813ce3ffde4c0b454af60d0d0a4341b6 Mon Sep 17 00:00:00 2001 From: Chris Amow Date: Fri, 14 Aug 2026 13:58:44 -0500 Subject: [PATCH] autoscroll optional --- docs/implementation.md | 10 ++++++---- static/app.js | 9 ++++++++- static/chart.js | 16 ++++++++++++++++ static/index.html | 1 + tests/e2e/touch.test.mjs | 40 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/docs/implementation.md b/docs/implementation.md index e761ee8..df30c90 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -867,10 +867,12 @@ gesture, prevents its browser default, leaves the library's horizontal scaling intact, and applies the same pinch ratio around the gesture's center price. Single-finger page scrolling and every mouse path remain native and untouched. -The manual price range resets on a timeframe change or a double click/tap on the -right axis. A separate touch tap path was required for selection: after chart -gesture handling, mobile browsers do not reliably synthesize the DOM click the -desktop selection path expects. A short stationary tap now hit-tests and selects +The manual price range resets on a timeframe change, a double click/tap on the +right axis, or when a live price leaves the plot with **Autoscroll to live +price** enabled. A separate touch tap path was +required for selection: after chart gesture handling, mobile browsers do not +reliably synthesize the DOM click the desktop selection path expects. A short +stationary tap now hit-tests and selects the drawing and opens its tooltip; moved touches, annotations, armed tools and two-finger gestures are excluded. Verified with Chromium touch events: logical time span and visible price span both changed while `visualViewport.scale` diff --git a/static/app.js b/static/app.js index b64f741..fbcf9bb 100644 --- a/static/app.js +++ b/static/app.js @@ -142,6 +142,7 @@ createApp({ const levelWidth = ref(2); const alertEarlyPoints = ref(null); const animateCurrentPrice = ref(localStorage.getItem('chart-animate-current-price') !== 'false'); + const autoScrollLivePrice = ref(localStorage.getItem('chart-auto-scroll-live-price') !== 'false'); const optionPrefs = (() => { try { return JSON.parse(localStorage.getItem('chart-es-options')) || {}; } catch { return {}; } @@ -1134,6 +1135,11 @@ createApp({ if (chartApi) chartApi.setCurrentPriceAnimation(value); }); + watch(autoScrollLivePrice, value => { + localStorage.setItem('chart-auto-scroll-live-price', String(value)); + if (chartApi) chartApi.setAutoScrollLivePrice(value); + }); + onMounted(async () => { chartApi = new ConfluenceChart(); // Deliberate debug handle. Chart geometry bugs are invisible from the @@ -1152,6 +1158,7 @@ createApp({ chartApi.setLineDeleteHandler(deleteLine); chartApi.setSnap(snap.value); chartApi.setCurrentPriceAnimation(animateCurrentPrice.value); + chartApi.setAutoScrollLivePrice(autoScrollLivePrice.value); chartApi.setCommentHandlers({ onToggle: toggleComment, onMove: moveComment, onSelect: selectComment }); window.addEventListener('keydown', handleKeydown); await migrateLegacyToken(); @@ -1167,6 +1174,6 @@ createApp({ window.removeEventListener('keydown', handleKeydown); }); - return { status, price, sessionOpen, quoteChange, animateCurrentPrice, barAge, dataUpdatedAt, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, diagnosticMode, captureBusy, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, deleteSelected, deleteLine, toggleDrawingSelection, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption }; + return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, barAge, dataUpdatedAt, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, diagnosticMode, captureBusy, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, deleteSelected, deleteLine, toggleDrawingSelection, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption }; }, }).mount('#app'); diff --git a/static/chart.js b/static/chart.js index b5a889b..f72be88 100644 --- a/static/chart.js +++ b/static/chart.js @@ -10,6 +10,7 @@ class ConfluenceChart { this.currentPriceLine = null; this.currentPricePulse = null; this.animateCurrentPrice = true; + this.autoScrollLivePrice = true; this.lastCurrentPrice = null; this.previewLine = null; this.projectionLayer = null; @@ -532,6 +533,7 @@ class ConfluenceChart { // the source: a bar behind the last one has nothing to contribute. const last = this.bars[this.bars.length - 1]; if (last && bar.t < last.t) return; + this.keepLivePriceVisible(bar.c); this.candles.update(this.toCandle(bar)); this.updateCurrentPrice(bar.c); this.volume.update(ConfluenceChart.toVolume(bar)); @@ -555,6 +557,20 @@ class ConfluenceChart { this.currentPricePulse.classList.toggle('active', active); } + setAutoScrollLivePrice(enabled) { + this.autoScrollLivePrice = Boolean(enabled); + this.keepLivePriceVisible(this.lastCurrentPrice); + } + + keepLivePriceVisible(price) { + if (!this.autoScrollLivePrice || !this.manualPriceRange || price == null) return; + const plot = this.plotCanvas(); + const y = this.candles.priceToCoordinate(price); + if (!plot || (y !== null && y >= 0 && y <= plot.clientHeight)) return; + this.manualPriceRange = null; + this.candles.applyOptions({ autoscaleInfoProvider: this.candleAutoscaleProvider }); + } + updateCurrentPrice(price) { if (price == null) return; const options = { diff --git a/static/index.html b/static/index.html index 4894a33..cd20dae 100644 --- a/static/index.html +++ b/static/index.html @@ -58,6 +58,7 @@