drawings from higher timeframes to lower fixes

This commit is contained in:
Chris Amow 2026-08-23 21:37:19 -05:00
parent 913eec34ea
commit eaf118ef8b
7 changed files with 120 additions and 26 deletions

View file

@ -1094,11 +1094,15 @@ same effective style is applied to rare SVG endpoint bridges so an off-grid
anchor cannot introduce a visible style seam. Browser coverage checks 30m-on-5m anchor cannot introduce a visible style seam. Browser coverage checks 30m-on-5m
and native 30m rendering separately. and native 30m rendering separately.
Sloped manual lines also stop propagating upward: a 30m line is available on Drawing objects also stop propagating upward by default: a 30m trendline, price
30m and lower charts but hidden on 1h/1d. This is a display filter only; the level, Fibonacci drawing, comment, or symbol is available on 30m and lower
drawing remains loaded and flat price levels remain visible everywhere. The charts but hidden on 1h/1d. **Hide lower-TF drawings** in Config persists this
daily-chart regression creates a real 30m drawing, confirms it is rendered on display preference in `localStorage`, defaults on, and can restore every drawing
the lower chart, switches to 1d, and requires only its series to disappear. without changing backend data or alert behavior. New price levels, comments,
and symbols now save the creation timeframe; older price levels were historically
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 — 1m zoom-out died at ~1am because the socket sent 1,000 bars ### 2026-08-15 — 1m zoom-out died at ~1am because the socket sent 1,000 bars

View file

@ -122,10 +122,11 @@ if your parser doesn't filter those, that fixture will catch it.
1d base chart + THE DAILY MAs weight 16 ← a switchable base timeframe 1d base chart + THE DAILY MAs weight 16 ← a switchable base timeframe
``` ```
Derived levels and flat price levels stay visible on every base timeframe — the Derived overlays stay visible on every base timeframe — the 200DMA on a 1-minute
200DMA on a 1-minute chart is the point, not a side effect. Sloped manual chart is the point, not a side effect. Drawing objects follow one configurable
trendlines are visible on their attributed timeframe and lower charts, but not rule: visible on their attributed timeframe and lower charts, but not on higher
on higher charts: 30m structure belongs on 30m/15m/5m/1m, not on 1h or 1d. charts. Thus 30m trendlines, levels, Fibonacci drawings, comments, and symbols
belong on 30m/15m/5m/1m, not on 1h or 1d.
--- ---
@ -919,9 +920,9 @@ LAYERS
☐ Hidden levels still count toward confluence ☐ Hidden levels still count toward confluence
``` ```
- **Derived and flat levels stay across base timeframes.** A 200DMA is equally - **Derived overlays stay across base timeframes.** A 200DMA is equally valid on
valid on a 1m chart. Sloped manual trendlines are the exception: hide them a 1m chart. Drawing objects default to native/lower charts only. The persisted
above their attributed timeframe while retaining them on native/lower charts. Config setting **Hide lower-TF drawings** can restore all drawings everywhere.
- **The group checkbox is a master toggle** — unchecking "Daily MAs" hides all five at - **The group checkbox is a master toggle** — unchecking "Daily MAs" hides all five at
once; individual periods nest under it. once; individual periods nest under it.
- The colour swatch beside each timeframe is that timeframe's hue, used identically on - The colour swatch beside each timeframe is that timeframe's hue, used identically on

View file

@ -151,6 +151,7 @@ createApp({
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 autoScrollLivePrice = ref(localStorage.getItem('chart-auto-scroll-live-price') !== 'false');
const extraDetail = ref(localStorage.getItem('chart-extra-detail') === 'true'); const extraDetail = ref(localStorage.getItem('chart-extra-detail') === 'true');
const hideLowerTfDrawings = ref(localStorage.getItem('chart-hide-lower-tf-drawings') !== '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 {}; }
@ -721,14 +722,24 @@ createApp({
comments.value = (payload.drawings || []).filter(item => ['comment', 'symbol'].includes(item.kind)); comments.value = (payload.drawings || []).filter(item => ['comment', 'symbol'].includes(item.kind));
fibs.value = (payload.drawings || []).filter(item => item.kind === 'fibonacci'); fibs.value = (payload.drawings || []).filter(item => item.kind === 'fibonacci');
syncComments(); syncComments();
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings syncFibs();
? fibs.value.filter(item => !item.hidden) : []); }
function drawingVisible(item) {
return !hideLowerTfDrawings.value
|| ConfluenceChart.drawingVisibleOn(item, timeframe.value);
}
function syncFibs() {
if (!chartApi) return;
chartApi.setFibs(prefs.value.enabled.drawings
? fibs.value.filter(item => !item.hidden && drawingVisible(item)) : []);
} }
function syncComments() { function syncComments() {
if (!chartApi) return; if (!chartApi) return;
const visible = prefs.value.enabled.drawings const visible = prefs.value.enabled.drawings
? comments.value.filter(item => !item.hidden) ? comments.value.filter(item => !item.hidden && drawingVisible(item))
: []; : [];
chartApi.setComments(visible); chartApi.setComments(visible);
} }
@ -829,7 +840,7 @@ createApp({
pinned: !commentFloat.value, pinned: !commentFloat.value,
anchor_t: result.time ?? null, anchor_t: result.time ?? null,
anchor_p: result.price ?? null, anchor_p: result.price ?? null,
x: result.x, y: result.y, x: result.x, y: result.y, tf: timeframe.value,
}), }),
}); });
if (!response.ok) { if (!response.ok) {
@ -850,6 +861,7 @@ createApp({
body: JSON.stringify({ body: JSON.stringify({
text: choice.name, icon, color: symbolColor.value, scale: symbolScale.value, pinned: true, text: choice.name, icon, color: symbolColor.value, scale: symbolScale.value, pinned: true,
anchor_t: result.time, anchor_p: result.price, x: result.x, y: result.y, anchor_t: result.time, anchor_p: result.price, x: result.x, y: result.y,
tf: timeframe.value,
}), }),
}); });
if (!response.ok) { if (!response.ok) {
@ -994,7 +1006,7 @@ createApp({
body: JSON.stringify({ body: JSON.stringify({
price: alertPrice.value, note: alertNote.value, price: alertPrice.value, note: alertNote.value,
color: levelColor.value, line_width: levelWidth.value, color: levelColor.value, line_width: levelWidth.value,
alert_early_points: alertEarlyPoints.value, alert_early_points: alertEarlyPoints.value, tf: timeframe.value,
}), }),
}); });
if (!response.ok) { console.error(`Unable to add price alert: HTTP ${response.status}`); return; } if (!response.ok) { console.error(`Unable to add price alert: HTTP ${response.status}`); return; }
@ -1339,6 +1351,9 @@ createApp({
function selectTimeframe(tf) { function selectTimeframe(tf) {
timeframe.value = tf; timeframe.value = tf;
prefs.value.base_tf = tf; prefs.value.base_tf = tf;
syncVisibleLevels();
syncComments();
syncFibs();
if (socket?.readyState === WebSocket.OPEN) { if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'subscribe', tf })); socket.send(JSON.stringify({ type: 'subscribe', tf }));
} }
@ -1348,7 +1363,7 @@ createApp({
if (level.kind === 'ma') return (prefs.value.enabled.ma[level.tf] || []).includes(level.period); if (level.kind === 'ma') return (prefs.value.enabled.ma[level.tf] || []).includes(level.period);
if (level.kind === 'manual') { if (level.kind === 'manual') {
return prefs.value.enabled.drawings && prefs.value.enabled.manual return prefs.value.enabled.drawings && prefs.value.enabled.manual
&& ConfluenceChart.manualLevelVisibleOn(level, timeframe.value); && drawingVisible(level);
} }
if (level.kind === 'horizontal') return prefs.value.enabled.horizontal; if (level.kind === 'horizontal') return prefs.value.enabled.horizontal;
if (level.kind === 'vwap') return prefs.value.enabled.vwap; if (level.kind === 'vwap') return prefs.value.enabled.vwap;
@ -1408,8 +1423,7 @@ createApp({
syncVisibleLevels(); syncVisibleLevels();
syncComments(); syncComments();
if (chartApi) chartApi.setRthEnabled(prefs.value.enabled.rth); if (chartApi) chartApi.setRthEnabled(prefs.value.enabled.rth);
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings syncFibs();
? fibs.value.filter(item => !item.hidden) : []);
sendPrefs(); sendPrefs();
}, { deep: true }); }, { deep: true });
@ -1475,6 +1489,13 @@ createApp({
localStorage.setItem('chart-extra-detail', String(value)); localStorage.setItem('chart-extra-detail', String(value));
}); });
watch(hideLowerTfDrawings, value => {
localStorage.setItem('chart-hide-lower-tf-drawings', String(value));
syncVisibleLevels();
syncComments();
syncFibs();
});
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
@ -1518,6 +1539,6 @@ createApp({
window.removeEventListener('keydown', handleKeydown); window.removeEventListener('keydown', handleKeydown);
}); });
return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, extraDetail, 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, 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'); }).mount('#app');

View file

@ -1250,9 +1250,10 @@ class ConfluenceChart {
return level.kind === 'ma' || level.kind === 'vwap' || level.kind === 'horizontal'; return level.kind === 'ma' || level.kind === 'vwap' || level.kind === 'horizontal';
} }
static manualLevelVisibleOn(level, displayedTf) { static drawingVisibleOn(drawing, displayedTf) {
if (level.kind !== 'manual' || ConfluenceChart.isFlat(level)) return true; const drawingRank = ConfluenceChart.tfRank[drawing?.tf];
return ConfluenceChart.tfRank[level.tf] >= ConfluenceChart.tfRank[displayedTf]; const displayedRank = ConfluenceChart.tfRank[displayedTf];
return drawingRank == null || displayedRank == null || drawingRank >= displayedRank;
} }
manualLineDisplayStyle(level) { manualLineDisplayStyle(level) {

View file

@ -76,6 +76,7 @@
<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> <label><input type="checkbox" v-model="autoScrollLivePrice"> Autoscroll to live price</label>
<label><input type="checkbox" v-model="hideLowerTfDrawings"> Hide lower-TF drawings</label>
<label><input type="checkbox" v-model="extraDetail"> Extra detail</label> <label><input type="checkbox" v-model="extraDetail"> Extra detail</label>
</details> </details>
<details class="sidebar-section tools-section" open> <details class="sidebar-section tools-section" open>

View file

@ -4,6 +4,68 @@ import {
withChart, chartBox, at, armTool, drawingIds, assertNoPageErrors, withChart, chartBox, at, armTool, drawingIds, assertNoPageErrors,
} from './helpers.mjs'; } from './helpers.mjs';
test('the timeframe visibility setting governs every drawing type',
{ timeout: 180000 }, async () => {
await withChart(async page => {
const ids = await page.evaluate(async () => {
const bars = (await (await fetch('/api/bars?tf=30m&limit=2')).json()).bars;
const [first, second] = bars;
const post = async (url, body) => (await fetch(url, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})).json();
const comment = await post('/api/comments', {
text: `tf comment ${Date.now()}`, tf: '30m', pinned: true,
anchor_t: first.t, anchor_p: first.c,
});
const symbol = await post('/api/comments', {
text: 'TF symbol', icon: 'skull', tf: '30m', pinned: true,
anchor_t: first.t, anchor_p: first.c,
});
const fib = await post('/api/lines', {
kind: 'fibonacci', tf: '30m', side: 'support',
anchor_t: first.t, anchor_p: first.l, end_t: second.t, end_p: second.h,
});
const level = await post('/api/lines/price', {
tf: '30m', price: first.c, note: `tf level ${Date.now()}`,
});
return { comment: comment.id, symbol: symbol.id, fib: fib.id, level: level.id };
});
await page.reload({ waitUntil: 'networkidle' });
await page.waitForFunction(() => window.__chart?.bars?.length > 0);
const allVisible = expected => page.waitForFunction(([values, visible]) => {
const chart = window.__chart;
const comments = chart.comments.some(item => item.id === values.comment)
&& chart.comments.some(item => item.id === values.symbol);
const fib = chart.fibs.some(item => item.id === values.fib);
const level = chart.priceLines.has(values.level);
return visible ? comments && fib && level : !comments && !fib && !level;
}, [ids, expected]);
await allVisible(true);
await page.click('.timeframes button:text-is("1d")');
await page.waitForFunction(() => window.__chart.bars[0]?.tf === '1d');
await allVisible(false);
await page.locator('.config-section summary').click();
const setting = page.locator('.config-section label:has-text("Hide lower-TF drawings") input');
assert.equal(await setting.isChecked(), true, 'timeframe filtering does not default on');
await setting.uncheck();
await allVisible(true);
assert.equal(await page.evaluate(() => localStorage.getItem('chart-hide-lower-tf-drawings')),
'false', 'the Config choice was not persisted');
await page.reload({ waitUntil: 'networkidle' });
await page.waitForFunction(() => window.__chart?.bars?.length > 0);
await page.locator('.config-section summary').click();
assert.equal(await page.locator(
'.config-section label:has-text("Hide lower-TF drawings") input').isChecked(), false,
'the Config choice reset on reload');
await allVisible(true);
assertNoPageErrors(page, assert);
});
});
test('editing a drawing name with Backspace or Delete cannot delete the drawing', test('editing a drawing name with Backspace or Delete cannot delete the drawing',
{ {
timeout: 180000, timeout: 180000,

View file

@ -383,13 +383,17 @@ test('a lower-timeframe trendline is hidden from the daily chart',
const state = await page.evaluate(async id => ({ const state = await page.evaluate(async id => ({
drawingStillStored: (await (await fetch('/api/drawings')).json()).drawings drawingStillStored: (await (await fetch('/api/drawings')).json()).drawings
.some(drawing => drawing.id === id), .some(drawing => drawing.id === id),
flatLevelVisible: ConfluenceChart.manualLevelVisibleOn( flatLevelVisible: ConfluenceChart.drawingVisibleOn(
{ kind: 'manual', slope: 0, tf: '30m' }, '1d', { kind: 'manual', slope: 0, tf: '30m' }, '1d',
), ),
}), created.id); }), created.id);
assert.equal(state.drawingStillStored, true, 'daily filtering deleted the drawing state'); assert.equal(state.drawingStillStored, true, 'daily filtering deleted the drawing state');
assert.equal(state.flatLevelVisible, true, 'daily filtering hid a flat price level'); assert.equal(state.flatLevelVisible, false, 'a lower-TF flat level bypassed the general rule');
await page.locator('.config-section summary').click();
await page.locator('.config-section label:has-text("Hide lower-TF drawings") input').uncheck();
await page.waitForFunction(id => window.__chart.levelSeries.has(id), created.id);
assertNoPageErrors(page, assert); assertNoPageErrors(page, assert);
}); });
}); });