trendline selection issue

This commit is contained in:
Chris Amow 2026-08-23 23:25:55 -05:00
parent eaf118ef8b
commit 18585e42cd
5 changed files with 85 additions and 14 deletions

View file

@ -1104,6 +1104,23 @@ stored as 1d and therefore remain global because their original timeframe is
unknowable. Integration coverage creates all four non-trendline drawing types,
switches to 1d, and verifies the setting hides and restores them together.
### 2026-08-15 — checked in the list was not active on the chart
The Drawings checkboxes support bulk operations, but `selectedDrawing` was set
to null whenever more than one checkbox was checked. Every checked row retained
the same selected border while no line owned the handles, focus animation, or
drag target. Duplicated lines had a related path: duplication assigned the new
id programmatically, but the watcher called `setSelectedLine()` rather than
`focusLine()`, so the expected glow never ran.
Bulk membership and the active chart drawing are now separate. Checking a row
adds it to the bulk set and makes it the active drawing; unchecking the active
row falls back to the most recently selected remaining id. Clicking a row makes
it the sole selection. The active row has a distinct inset marker, and every
selection path, including duplication, runs the same focus method. Browser
coverage checks two rows simultaneously, requires the newest one to own the
focus and hit polyline, and starts a real pointer drag on that line.
### 2026-08-15 — 1m zoom-out died at ~1am because the socket sent 1,000 bars
Compressing the 1m time scale stopped around 01:00, then looked empty.

View file

@ -687,7 +687,6 @@ createApp({
const id = chartApi.hitTest(param);
selectedDrawing.value = id;
selectedDrawings.value = id ? [id] : [];
if (id) chartApi.focusLine(id);
}
async function handleToolComplete(result) {
@ -1115,15 +1114,17 @@ createApp({
}
function toggleDrawingSelection(id) {
selectedDrawings.value = selectedDrawings.value.includes(id)
const removing = selectedDrawings.value.includes(id);
selectedDrawings.value = removing
? selectedDrawings.value.filter(value => value !== id)
: [...selectedDrawings.value, id];
selectedDrawing.value = selectedDrawings.value.length === 1 ? selectedDrawings.value[0] : null;
if (selectedDrawing.value === id) {
const item = drawings.value.find(drawing => drawing.id === id);
if (item?.comment) chartApi?.focusComment(id);
else chartApi?.focusLine(id);
if (!removing) selectedDrawing.value = id;
else if (selectedDrawing.value === id) selectedDrawing.value = selectedDrawings.value.at(-1) || null;
}
function activateDrawing(id) {
selectedDrawings.value = [id];
selectedDrawing.value = id;
}
function toggleSelectAll() {
@ -1460,9 +1461,11 @@ createApp({
watch(selectedDrawing, id => {
if (chartApi) {
chartApi.setSelectedDrawing(id);
chartApi.setSelectedLine(
levels.value.some(level => level.id === id) || fibs.value.some(fib => fib.id === id) ? id : null,
);
const lineLike = levels.value.some(level => level.id === id)
|| fibs.value.some(fib => fib.id === id);
if (lineLike && id) chartApi.focusLine(id);
else chartApi.setSelectedLine(null);
if (comments.value.some(comment => comment.id === id)) chartApi.focusComment(id);
}
revealSelectedDrawing(id);
});
@ -1539,6 +1542,6 @@ createApp({
window.removeEventListener('keydown', handleKeydown);
});
return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, extraDetail, hideLowerTfDrawings, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, eventsMore, loadOlderEvents, diagnosticMode, captureBusy, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, maValue, maAlertOn, toggleMaAlert, deleteSelected, deleteLine, toggleDrawingSelection, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, drawingTf, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption, canUndo, undoTitle, undo };
return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, extraDetail, hideLowerTfDrawings, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, eventsMore, loadOlderEvents, diagnosticMode, captureBusy, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, maValue, maAlertOn, toggleMaAlert, deleteSelected, deleteLine, toggleDrawingSelection, activateDrawing, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, drawingTf, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption, canUndo, undoTitle, undo };
},
}).mount('#app');

View file

@ -289,7 +289,8 @@
<div v-for="item in filteredDrawings" :key="item.id" class="trendline-row"
:data-drawing-id="item.id"
:class="{selected: selectedDrawings.includes(item.id), 'hidden-drawing': item.hidden}">
:class="{selected: selectedDrawings.includes(item.id), active: selectedDrawing === item.id, 'hidden-drawing': item.hidden}"
@click="activateDrawing(item.id)">
<input class="line-select" type="checkbox"
:checked="selectedDrawings.includes(item.id)" :aria-label="`Select drawing ${item.number}`"
@click.stop @change="toggleDrawingSelection(item.id)">

View file

@ -29,7 +29,7 @@ aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px;
.drawing-list-resize::after { content:''; display:block; width:36px; height:3px; margin:3px auto 0; border-radius:1px; background:var(--muted); }
.drawing-list-resize:hover::after { background:var(--accent); }
.trendline-actions { display:grid; grid-template-columns:auto 1fr auto auto; align-items:center; gap:4px; margin-bottom:7px; }.trendline-actions button { padding:3px 5px; font-size:8px; white-space:nowrap; }.selection-count { color:var(--muted); font-size:8px; text-align:center; white-space:nowrap; }
.trendline-row { display:grid; grid-template-columns:14px minmax(0,1fr); gap:4px; padding:2px 4px; border:1px solid transparent; border-bottom-color:var(--line); }.trendline-row.selected { border-color:var(--accent); }.trendline-row.hidden-drawing .drawing-content { opacity:.48; }.trendline-row.hidden-drawing .drawing-secondary>span::before { content:'HIDDEN · '; }.trendline-row>.line-select { align-self:center; width:12px; height:12px; margin:0; accent-color:var(--accent); }.trendline-row>.drawing-icon { align-self:center; }
.trendline-row { display:grid; grid-template-columns:14px minmax(0,1fr); gap:4px; padding:2px 4px; border:1px solid transparent; border-bottom-color:var(--line); }.trendline-row.selected { border-color:var(--accent); }.trendline-row.active { background:color-mix(in srgb,var(--accent) 9%,transparent); box-shadow:inset 2px 0 var(--accent); }.trendline-row.hidden-drawing .drawing-content { opacity:.48; }.trendline-row.hidden-drawing .drawing-secondary>span::before { content:'HIDDEN · '; }.trendline-row>.line-select { align-self:center; width:12px; height:12px; margin:0; accent-color:var(--accent); }.trendline-row>.drawing-icon { align-self:center; }
.drawing-content { min-width:0; display:grid; gap:1px; }.drawing-primary,.drawing-secondary { display:flex; align-items:center; min-width:0; }.drawing-primary { gap:3px; }.drawing-primary>input { flex:1; min-width:0; height:20px; padding:1px 3px; border:0; border-bottom:1px solid var(--line); background:transparent; color:var(--fg); font:inherit; font-size:10px; }.drawing-secondary { justify-content:space-between; gap:5px; min-height:19px; }.drawing-secondary>span { overflow:hidden; color:var(--muted); font-size:8px; letter-spacing:.25px; text-transform:uppercase; white-space:nowrap; text-overflow:ellipsis; }
.drawing-state { position:relative; display:grid; place-items:center; flex:none; width:20px; height:20px; color:var(--accent); cursor:pointer; }.drawing-state.off { color:var(--muted); }.drawing-state input { position:absolute; opacity:0; pointer-events:none; }.drawing-delete,.collapse-toggle { display:grid; place-items:center; flex:none; width:20px; height:20px; padding:0; border:0; background:transparent; color:var(--muted); font-size:9px; cursor:pointer; }.drawing-delete:hover { color:var(--red); }
.drawing-controls { display:flex; align-items:center; gap:3px; flex:none; }.drawing-controls select { width:31px; height:19px; padding:0 2px; border:1px solid var(--line); border-radius:3px; background:var(--panel); color:var(--fg); font:inherit; font-size:8px; }.drawing-controls select.mark-scale { width:38px; }.color-picker { position:relative; height:19px; }.color-picker>summary { width:19px; height:19px; border:1px solid var(--line); border-radius:3px; cursor:pointer; list-style:none; }.color-picker>summary::-webkit-details-marker { display:none; }.color-picker:not([open])>.color-popover { display:none; }.color-popover { position:absolute; right:0; bottom:24px; z-index:20; display:grid; grid-template-columns:repeat(4,18px); gap:3px; width:95px; padding:6px; border:1px solid var(--line); border-radius:5px; background:var(--panel); box-shadow:0 5px 18px color-mix(in srgb,var(--fg) 18%,transparent); }.color-popover>button,.custom-color { width:18px; height:18px; padding:0; border:1px solid color-mix(in srgb,var(--fg) 20%,transparent); border-radius:2px; cursor:pointer; }.color-popover>button.selected { outline:2px solid var(--fg); outline-offset:1px; }.color-popover>.palette-close { grid-column:4; background:var(--chart-bg); color:var(--muted); }.color-popover>.palette-close:hover { color:var(--fg); }.custom-color { position:relative; display:grid; place-items:center; background:var(--chart-bg); color:var(--muted); font-size:9px; }.custom-color input { position:absolute; inset:0; width:100%; height:100%; opacity:0; cursor:pointer; }

View file

@ -66,6 +66,56 @@ test('the timeframe visibility setting governs every drawing type',
});
});
test('the newest checked trendline stays active and movable during bulk selection',
{ timeout: 180000 }, async () => {
await withChart(async page => {
const ids = await page.evaluate(async () => {
const bars = (await (await fetch('/api/bars?tf=1m&limit=30')).json()).bars;
const create = async (first, second, suffix) => (await (await fetch('/api/lines', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tf: '1m', side: 'support', anchor_t: first.t, anchor_p: first.l,
end_t: second.t, end_p: second.l + 0.25, note: `active ${suffix} ${Date.now()}`,
}),
})).json()).id;
return [
await create(bars.at(-25), bars.at(-18), 'one'),
await create(bars.at(-15), bars.at(-8), 'two'),
];
});
const row = id => page.locator(`.trendline-row[data-drawing-id="${id}"]`);
await page.waitForFunction(values => values.every(id =>
document.querySelector(`.trendline-row[data-drawing-id="${id}"]`)), ids);
await row(ids[0]).locator('.line-select').check();
await row(ids[1]).locator('.line-select').check();
await page.waitForFunction(id => window.__chart.selectedLineId === id
&& !window.__chart.lineHitTarget.hasAttribute('hidden'), ids[1]);
assert.equal(await row(ids[0]).evaluate(node => node.classList.contains('selected')), true);
assert.equal(await row(ids[1]).evaluate(node => node.classList.contains('selected')), true);
assert.equal(await row(ids[0]).evaluate(node => node.classList.contains('active')), false);
assert.equal(await row(ids[1]).evaluate(node => node.classList.contains('active')), true);
assert.equal(await page.locator('.chart-line-focus').getAttribute('hidden'), null,
'programmatic selection did not trigger the focus glow');
const target = await page.evaluate(() => {
const chart = window.__chart;
const hit = chart.lineHitTarget;
const plot = chart.plotCanvas().getBoundingClientRect();
return {
x: plot.left + (Number(hit.getAttribute('x1')) + Number(hit.getAttribute('x2'))) / 2,
y: plot.top + (Number(hit.getAttribute('y1')) + Number(hit.getAttribute('y2'))) / 2,
};
});
await page.mouse.move(target.x, target.y);
await page.mouse.down();
assert.equal(await page.evaluate(() => window.__chart.draggingLine?.id), ids[1],
'the active selected line did not own the drag target');
await page.mouse.up();
assertNoPageErrors(page, assert);
});
});
test('editing a drawing name with Backspace or Delete cannot delete the drawing',
{
timeout: 180000,