autoscroll optional

This commit is contained in:
Chris Amow 2026-08-14 13:58:44 -05:00
parent 6e781ed163
commit aae47b6681
5 changed files with 71 additions and 5 deletions

View file

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

View file

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

View file

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

View file

@ -58,6 +58,7 @@
<details class="sidebar-section config-section">
<summary>Config</summary>
<label><input type="checkbox" v-model="animateCurrentPrice"> Animate current price</label>
<label><input type="checkbox" v-model="autoScrollLivePrice"> Autoscroll to live price</label>
</details>
<details class="sidebar-section tools-section" open>
<summary>Tools</summary>

View file

@ -117,3 +117,43 @@ test('touch tap selects a drawing and horizontal and vertical pinches scale only
assertNoPageErrors(page, assert);
}, { viewport: { width: 1200, height: 800 }, hasTouch: true, isMobile: true });
});
test('live-price autoscroll restores autoscaling only when enabled',
{ timeout: 180000 }, async () => {
await withChart(async page => {
await page.locator('.config-section summary').click();
const autoscroll = page.locator('.config-section label:has-text("Autoscroll to live price") input');
await autoscroll.uncheck();
await page.waitForFunction(() => window.__chart.autoScrollLivePrice === false);
const offscreenPrice = await page.evaluate(() => {
const c = window.__chart;
const last = c.bars.at(-1);
// This is the same manual range created by a vertical or horizontal pinch.
c.manualPriceRange = { minValue: last.c - 1, maxValue: last.c + 1 };
c.candles.applyOptions({ autoscaleInfoProvider: c.candleAutoscaleProvider });
const price = last.c - 10;
c.updateBar({ ...last, o: price, h: price, l: price, c: price });
return price;
});
const disabledState = await page.evaluate(() => Boolean(window.__chart.manualPriceRange));
assert.equal(disabledState, true, 'disabled autoscroll reset the manual range');
await autoscroll.check();
await page.waitForFunction(() => window.__chart.autoScrollLivePrice === true);
await page.waitForTimeout(100);
const state = await page.evaluate(price => {
const c = window.__chart;
return {
manualPriceRange: c.manualPriceRange,
y: c.candles.priceToCoordinate(price),
plotHeight: c.plotCanvas().clientHeight,
};
}, offscreenPrice);
assert.equal(state.manualPriceRange, null, 'enabled autoscroll kept the manual range');
assert.ok(state.y >= 0 && state.y <= state.plotHeight,
`live price remained off-screen at y=${state.y}, plot=${state.plotHeight}`);
assertNoPageErrors(page, assert);
}, { viewport: { width: 1200, height: 800 }, hasTouch: true, isMobile: true });
});