autoscroll optional
This commit is contained in:
parent
6e781ed163
commit
aae47b6681
5 changed files with 71 additions and 5 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue