diff --git a/docs/implementation.md b/docs/implementation.md
index c8c516b..4599079 100644
--- a/docs/implementation.md
+++ b/docs/implementation.md
@@ -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
diff --git a/docs/plan.md b/docs/plan.md
index ac71ef7..ba469ea 100644
--- a/docs/plan.md
+++ b/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
diff --git a/static/app.js b/static/app.js
index 891d50f..11ce816 100644
--- a/static/app.js
+++ b/static/app.js
@@ -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');
diff --git a/static/chart.js b/static/chart.js
index 4073832..eca3400 100644
--- a/static/chart.js
+++ b/static/chart.js
@@ -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) {
diff --git a/static/index.html b/static/index.html
index 99b6401..e2baece 100644
--- a/static/index.html
+++ b/static/index.html
@@ -76,6 +76,7 @@
Config
+