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 @@