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. 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. 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 The manual price range resets on a timeframe change, a double click/tap on the
right axis. A separate touch tap path was required for selection: after chart right axis, or when a live price leaves the plot with **Autoscroll to live
gesture handling, mobile browsers do not reliably synthesize the DOM click the price** enabled. A separate touch tap path was
desktop selection path expects. A short stationary tap now hit-tests and selects 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 the drawing and opens its tooltip; moved touches, annotations, armed tools and
two-finger gestures are excluded. Verified with Chromium touch events: logical two-finger gestures are excluded. Verified with Chromium touch events: logical
time span and visible price span both changed while `visualViewport.scale` time span and visible price span both changed while `visualViewport.scale`

View file

@ -142,6 +142,7 @@ createApp({
const levelWidth = ref(2); const levelWidth = ref(2);
const alertEarlyPoints = ref(null); const alertEarlyPoints = ref(null);
const animateCurrentPrice = ref(localStorage.getItem('chart-animate-current-price') !== 'false'); const animateCurrentPrice = ref(localStorage.getItem('chart-animate-current-price') !== 'false');
const autoScrollLivePrice = ref(localStorage.getItem('chart-auto-scroll-live-price') !== 'false');
const optionPrefs = (() => { const optionPrefs = (() => {
try { return JSON.parse(localStorage.getItem('chart-es-options')) || {}; } try { return JSON.parse(localStorage.getItem('chart-es-options')) || {}; }
catch { return {}; } catch { return {}; }
@ -1134,6 +1135,11 @@ createApp({
if (chartApi) chartApi.setCurrentPriceAnimation(value); if (chartApi) chartApi.setCurrentPriceAnimation(value);
}); });
watch(autoScrollLivePrice, value => {
localStorage.setItem('chart-auto-scroll-live-price', String(value));
if (chartApi) chartApi.setAutoScrollLivePrice(value);
});
onMounted(async () => { onMounted(async () => {
chartApi = new ConfluenceChart(); chartApi = new ConfluenceChart();
// Deliberate debug handle. Chart geometry bugs are invisible from the // Deliberate debug handle. Chart geometry bugs are invisible from the
@ -1152,6 +1158,7 @@ createApp({
chartApi.setLineDeleteHandler(deleteLine); chartApi.setLineDeleteHandler(deleteLine);
chartApi.setSnap(snap.value); chartApi.setSnap(snap.value);
chartApi.setCurrentPriceAnimation(animateCurrentPrice.value); chartApi.setCurrentPriceAnimation(animateCurrentPrice.value);
chartApi.setAutoScrollLivePrice(autoScrollLivePrice.value);
chartApi.setCommentHandlers({ onToggle: toggleComment, onMove: moveComment, onSelect: selectComment }); chartApi.setCommentHandlers({ onToggle: toggleComment, onMove: moveComment, onSelect: selectComment });
window.addEventListener('keydown', handleKeydown); window.addEventListener('keydown', handleKeydown);
await migrateLegacyToken(); await migrateLegacyToken();
@ -1167,6 +1174,6 @@ createApp({
window.removeEventListener('keydown', handleKeydown); 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'); }).mount('#app');

View file

@ -10,6 +10,7 @@ class ConfluenceChart {
this.currentPriceLine = null; this.currentPriceLine = null;
this.currentPricePulse = null; this.currentPricePulse = null;
this.animateCurrentPrice = true; this.animateCurrentPrice = true;
this.autoScrollLivePrice = true;
this.lastCurrentPrice = null; this.lastCurrentPrice = null;
this.previewLine = null; this.previewLine = null;
this.projectionLayer = null; this.projectionLayer = null;
@ -532,6 +533,7 @@ class ConfluenceChart {
// the source: a bar behind the last one has nothing to contribute. // the source: a bar behind the last one has nothing to contribute.
const last = this.bars[this.bars.length - 1]; const last = this.bars[this.bars.length - 1];
if (last && bar.t < last.t) return; if (last && bar.t < last.t) return;
this.keepLivePriceVisible(bar.c);
this.candles.update(this.toCandle(bar)); this.candles.update(this.toCandle(bar));
this.updateCurrentPrice(bar.c); this.updateCurrentPrice(bar.c);
this.volume.update(ConfluenceChart.toVolume(bar)); this.volume.update(ConfluenceChart.toVolume(bar));
@ -555,6 +557,20 @@ class ConfluenceChart {
this.currentPricePulse.classList.toggle('active', active); 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) { updateCurrentPrice(price) {
if (price == null) return; if (price == null) return;
const options = { const options = {

View file

@ -58,6 +58,7 @@
<details class="sidebar-section config-section"> <details class="sidebar-section config-section">
<summary>Config</summary> <summary>Config</summary>
<label><input type="checkbox" v-model="animateCurrentPrice"> Animate current price</label> <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>
<details class="sidebar-section tools-section" open> <details class="sidebar-section tools-section" open>
<summary>Tools</summary> <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); assertNoPageErrors(page, assert);
}, { viewport: { width: 1200, height: 800 }, hasTouch: true, isMobile: true }); }, { 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 });
});