drawings from higher timeframes to lower fixes
This commit is contained in:
parent
913eec34ea
commit
eaf118ef8b
7 changed files with 120 additions and 26 deletions
|
|
@ -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
|
||||
and native 30m rendering separately.
|
||||
|
||||
Sloped manual lines also stop propagating upward: a 30m line is available on
|
||||
30m and lower charts but hidden on 1h/1d. This is a display filter only; the
|
||||
drawing remains loaded and flat price levels remain visible everywhere. The
|
||||
daily-chart regression creates a real 30m drawing, confirms it is rendered on
|
||||
the lower chart, switches to 1d, and requires only its series to disappear.
|
||||
Drawing objects also stop propagating upward by default: a 30m trendline, price
|
||||
level, Fibonacci drawing, comment, or symbol is available on 30m and lower
|
||||
charts but hidden on 1h/1d. **Hide lower-TF drawings** in Config persists this
|
||||
display preference in `localStorage`, defaults on, and can restore every drawing
|
||||
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
|
||||
|
||||
|
|
|
|||
15
docs/plan.md
15
docs/plan.md
|
|
@ -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
|
||||
```
|
||||
|
||||
Derived levels and flat price levels stay visible on every base timeframe — the
|
||||
200DMA on a 1-minute chart is the point, not a side effect. Sloped manual
|
||||
trendlines are visible on their attributed timeframe and lower charts, but not
|
||||
on higher charts: 30m structure belongs on 30m/15m/5m/1m, not on 1h or 1d.
|
||||
Derived overlays stay visible on every base timeframe — the 200DMA on a 1-minute
|
||||
chart is the point, not a side effect. Drawing objects follow one configurable
|
||||
rule: visible on their attributed timeframe and lower charts, but not on higher
|
||||
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
|
||||
```
|
||||
|
||||
- **Derived and flat levels stay across base timeframes.** A 200DMA is equally
|
||||
valid on a 1m chart. Sloped manual trendlines are the exception: hide them
|
||||
above their attributed timeframe while retaining them on native/lower charts.
|
||||
- **Derived overlays stay across base timeframes.** A 200DMA is equally valid on
|
||||
a 1m chart. Drawing objects default to native/lower charts only. The persisted
|
||||
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
|
||||
once; individual periods nest under it.
|
||||
- The colour swatch beside each timeframe is that timeframe's hue, used identically on
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ createApp({
|
|||
const animateCurrentPrice = ref(localStorage.getItem('chart-animate-current-price') !== 'false');
|
||||
const autoScrollLivePrice = ref(localStorage.getItem('chart-auto-scroll-live-price') !== 'false');
|
||||
const extraDetail = ref(localStorage.getItem('chart-extra-detail') === 'true');
|
||||
const hideLowerTfDrawings = ref(localStorage.getItem('chart-hide-lower-tf-drawings') !== 'false');
|
||||
const optionPrefs = (() => {
|
||||
try { return JSON.parse(localStorage.getItem('chart-es-options')) || {}; }
|
||||
catch { return {}; }
|
||||
|
|
@ -721,14 +722,24 @@ createApp({
|
|||
comments.value = (payload.drawings || []).filter(item => ['comment', 'symbol'].includes(item.kind));
|
||||
fibs.value = (payload.drawings || []).filter(item => item.kind === 'fibonacci');
|
||||
syncComments();
|
||||
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings
|
||||
? fibs.value.filter(item => !item.hidden) : []);
|
||||
syncFibs();
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!chartApi) return;
|
||||
const visible = prefs.value.enabled.drawings
|
||||
? comments.value.filter(item => !item.hidden)
|
||||
? comments.value.filter(item => !item.hidden && drawingVisible(item))
|
||||
: [];
|
||||
chartApi.setComments(visible);
|
||||
}
|
||||
|
|
@ -829,7 +840,7 @@ createApp({
|
|||
pinned: !commentFloat.value,
|
||||
anchor_t: result.time ?? null,
|
||||
anchor_p: result.price ?? null,
|
||||
x: result.x, y: result.y,
|
||||
x: result.x, y: result.y, tf: timeframe.value,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
|
@ -850,6 +861,7 @@ createApp({
|
|||
body: JSON.stringify({
|
||||
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,
|
||||
tf: timeframe.value,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
|
@ -994,7 +1006,7 @@ createApp({
|
|||
body: JSON.stringify({
|
||||
price: alertPrice.value, note: alertNote.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; }
|
||||
|
|
@ -1339,6 +1351,9 @@ createApp({
|
|||
function selectTimeframe(tf) {
|
||||
timeframe.value = tf;
|
||||
prefs.value.base_tf = tf;
|
||||
syncVisibleLevels();
|
||||
syncComments();
|
||||
syncFibs();
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
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 === '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 === 'vwap') return prefs.value.enabled.vwap;
|
||||
|
|
@ -1408,8 +1423,7 @@ createApp({
|
|||
syncVisibleLevels();
|
||||
syncComments();
|
||||
if (chartApi) chartApi.setRthEnabled(prefs.value.enabled.rth);
|
||||
if (chartApi) chartApi.setFibs(prefs.value.enabled.drawings
|
||||
? fibs.value.filter(item => !item.hidden) : []);
|
||||
syncFibs();
|
||||
sendPrefs();
|
||||
}, { deep: true });
|
||||
|
||||
|
|
@ -1475,6 +1489,13 @@ createApp({
|
|||
localStorage.setItem('chart-extra-detail', String(value));
|
||||
});
|
||||
|
||||
watch(hideLowerTfDrawings, value => {
|
||||
localStorage.setItem('chart-hide-lower-tf-drawings', String(value));
|
||||
syncVisibleLevels();
|
||||
syncComments();
|
||||
syncFibs();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
chartApi = new ConfluenceChart();
|
||||
// Deliberate debug handle. Chart geometry bugs are invisible from the
|
||||
|
|
@ -1518,6 +1539,6 @@ createApp({
|
|||
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');
|
||||
|
|
|
|||
|
|
@ -1250,9 +1250,10 @@ class ConfluenceChart {
|
|||
return level.kind === 'ma' || level.kind === 'vwap' || level.kind === 'horizontal';
|
||||
}
|
||||
|
||||
static manualLevelVisibleOn(level, displayedTf) {
|
||||
if (level.kind !== 'manual' || ConfluenceChart.isFlat(level)) return true;
|
||||
return ConfluenceChart.tfRank[level.tf] >= ConfluenceChart.tfRank[displayedTf];
|
||||
static drawingVisibleOn(drawing, displayedTf) {
|
||||
const drawingRank = ConfluenceChart.tfRank[drawing?.tf];
|
||||
const displayedRank = ConfluenceChart.tfRank[displayedTf];
|
||||
return drawingRank == null || displayedRank == null || drawingRank >= displayedRank;
|
||||
}
|
||||
|
||||
manualLineDisplayStyle(level) {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@
|
|||
<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>
|
||||
<label><input type="checkbox" v-model="hideLowerTfDrawings"> Hide lower-TF drawings</label>
|
||||
<label><input type="checkbox" v-model="extraDetail"> Extra detail</label>
|
||||
</details>
|
||||
<details class="sidebar-section tools-section" open>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,68 @@ import {
|
|||
withChart, chartBox, at, armTool, drawingIds, assertNoPageErrors,
|
||||
} 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',
|
||||
{
|
||||
timeout: 180000,
|
||||
|
|
|
|||
|
|
@ -383,13 +383,17 @@ test('a lower-timeframe trendline is hidden from the daily chart',
|
|||
const state = await page.evaluate(async id => ({
|
||||
drawingStillStored: (await (await fetch('/api/drawings')).json()).drawings
|
||||
.some(drawing => drawing.id === id),
|
||||
flatLevelVisible: ConfluenceChart.manualLevelVisibleOn(
|
||||
flatLevelVisible: ConfluenceChart.drawingVisibleOn(
|
||||
{ kind: 'manual', slope: 0, tf: '30m' }, '1d',
|
||||
),
|
||||
}), created.id);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue