Add a Drawings layer toggle that hides lines and comments

This commit is contained in:
Chris Amow 2026-08-18 09:51:53 +00:00
parent 1ee2a96fc0
commit d9a272760b
7 changed files with 59 additions and 5 deletions

View file

@ -23,7 +23,7 @@ def level_enabled(level, enabled: dict) -> bool:
if kind == "ma":
return level.period in enabled.get("ma", {}).get(level.tf.value, [])
if kind == "manual":
return enabled.get("manual", True)
return enabled.get("drawings", True) and enabled.get("manual", True)
if kind == "trendline":
return enabled.get("auto", False)
if kind == "horizontal":

View file

@ -1153,3 +1153,11 @@ Written because they were skipped the first time and then asked for.
- Daily MA bells in Layers, independent of visibility, cooldown not
one-shot. Watches in `data/user_prefs.json`. See
`docs/plan_dma_alerts.md`.
### 2026-08-18 — Drawings layer
Layers now has a **Drawings** checkbox. Off hides every user drawing on the
chart — trendlines, price levels, and comments — and (unless "Hidden levels
still count toward confluence" is on) keeps those lines out of clusters too.
Manual lines stays as the finer control for lines only. Old stored prefs
without the key keep drawings on.

View file

@ -69,7 +69,7 @@ async function apiFetch(url, options = {}) {
const defaultPrefs = {
base_tf: '1m',
enabled: { ma: { '1d': [10, 20, 50, 100, 200] }, manual: true, auto: false, horizontal: true, vwap: true },
enabled: { ma: { '1d': [10, 20, 50, 100, 200] }, drawings: true, manual: true, auto: false, horizontal: true, vwap: true },
hidden_levels_score: false,
};
@ -667,7 +667,11 @@ createApp({
}
function syncComments() {
if (chartApi) chartApi.setComments(comments.value.filter(item => !item.hidden));
if (!chartApi) return;
const visible = prefs.value.enabled.drawings
? comments.value.filter(item => !item.hidden)
: [];
chartApi.setComments(visible);
}
async function createComment(result) {
@ -1097,7 +1101,9 @@ createApp({
function enabled(level) {
if (level.kind === 'ma') return (prefs.value.enabled.ma[level.tf] || []).includes(level.period);
if (level.kind === 'manual') return prefs.value.enabled.manual;
if (level.kind === 'manual') {
return prefs.value.enabled.drawings && prefs.value.enabled.manual;
}
if (level.kind === 'horizontal') return prefs.value.enabled.horizontal;
if (level.kind === 'vwap') return prefs.value.enabled.vwap;
return prefs.value.enabled.auto;
@ -1148,6 +1154,7 @@ createApp({
watch(prefs, () => {
localStorage.setItem('chart-layer-prefs', JSON.stringify(prefs.value));
syncVisibleLevels();
syncComments();
sendPrefs();
}, { deep: true });

View file

@ -49,6 +49,9 @@
<label><input type="checkbox" v-model="prefs.enabled.horizontal"><span class="swatch horizontal"></span>Prior day H/L/C</label>
<label><input type="checkbox" v-model="prefs.enabled.vwap"><span class="swatch vwap"></span>Session VWAP</label>
</div>
<div class="layer-group">
<label><input type="checkbox" v-model="prefs.enabled.drawings"><span class="swatch drawings"></span>Drawings</label>
</div>
<div class="layer-group layer-inline">
<label><input type="checkbox" v-model="prefs.enabled.manual"><span class="swatch manual"></span>Manual lines</label>
<label class="disabled" title="Automatic trendline detection is not built yet">(<input type="checkbox" disabled>auto)</label>

View file

@ -36,7 +36,7 @@ aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px;
.color-popover { grid-template-columns:1fr; width:178px; gap:3px; }
.color-family-row { display:grid; grid-template-columns:70px 1fr; align-items:center; gap:5px; min-width:0; }.color-family-row:nth-child(3),.color-family-row:nth-child(5) { margin-top:4px; padding-top:4px; border-top:1px solid var(--line); }.color-family-input { width:70px; height:18px; padding:1px 3px; border:1px solid transparent; border-radius:2px; background:transparent; color:var(--muted); font:inherit; font-size:8px; }.color-family-input:hover,.color-family-input:focus { border-color:var(--line); background:var(--chart-bg); color:var(--fg); outline:0; }.color-family-swatches { display:grid; grid-template-columns:repeat(4,18px); gap:3px; }.color-family-swatches>button,.color-popover-footer button { width:18px; height:18px; padding:0; border:1px solid color-mix(in srgb,var(--fg) 20%,transparent); border-radius:2px; cursor:pointer; }.color-family-swatches>button.selected { outline:2px solid var(--fg); outline-offset:1px; }.color-popover-footer { display:grid; grid-template-columns:18px 1fr 18px; align-items:center; gap:3px; margin-top:4px; padding-top:5px; border-top:1px solid var(--line); }.color-popover-footer .palette-close { grid-column:3; color:var(--muted); background:var(--chart-bg); }
.level-editors { display:flex; gap:4px; overflow:visible; text-transform:none; }.level-editors label { display:flex; align-items:center; gap:2px; }.level-editors input { width:54px; height:19px; padding:1px 3px; border:1px solid var(--line); border-radius:3px; background:var(--panel); color:var(--fg); font:inherit; font-size:8px; }
.layer-group { padding:9px 0; border-bottom:1px solid var(--line); display:grid; gap:7px; }.layer-group label,.score-hidden { display:flex; align-items:center; gap:7px; font-size:11px; cursor:pointer; }.layer-group input,.score-hidden input { accent-color:var(--accent); }.periods { display:grid; grid-template-columns:repeat(3, max-content); gap:7px 10px; padding-left:20px; }.periods .period { display:flex; align-items:center; gap:2px; }.periods label { color:var(--muted); gap:4px; }.periods input { width:12px; height:12px; margin:0; flex:none; }.ma-alert { display:grid; place-items:center; width:16px; height:16px; padding:0; border:0; background:transparent; color:var(--muted); font-size:9px; }.ma-alert.on { color:var(--accent); }.layer-inline { display:flex; align-items:center; gap:12px; }.layer-inline .disabled { gap:2px; }.swatch { width:13px; height:3px; display:inline-block; background:var(--muted); }.tf-1d { background:#d96073; }.tf-1h { background:#efb643; }.manual { background:#65b7cf; }.vwap { background:#b07ad6; }.horizontal { background:#9fb0c4; }
.layer-group { padding:9px 0; border-bottom:1px solid var(--line); display:grid; gap:7px; }.layer-group label,.score-hidden { display:flex; align-items:center; gap:7px; font-size:11px; cursor:pointer; }.layer-group input,.score-hidden input { accent-color:var(--accent); }.periods { display:grid; grid-template-columns:repeat(3, max-content); gap:7px 10px; padding-left:20px; }.periods .period { display:flex; align-items:center; gap:2px; }.periods label { color:var(--muted); gap:4px; }.periods input { width:12px; height:12px; margin:0; flex:none; }.ma-alert { display:grid; place-items:center; width:16px; height:16px; padding:0; border:0; background:transparent; color:var(--muted); font-size:9px; }.ma-alert.on { color:var(--accent); }.layer-inline { display:flex; align-items:center; gap:12px; }.layer-inline .disabled { gap:2px; }.swatch { width:13px; height:3px; display:inline-block; background:var(--muted); }.tf-1d { background:#d96073; }.tf-1h { background:#efb643; }.manual { background:#65b7cf; }.drawings { background:#c4a36a; }.vwap { background:#b07ad6; }.horizontal { background:#9fb0c4; }
.hint { margin:6px 0 2px; font-size:10px; color:var(--muted); line-height:1.35; }
/* Tool palette: the head arms the tool, the body configures what it creates. */

View file

@ -132,6 +132,28 @@ test('a floating comment holds its position while the chart scrolls', { timeout:
});
});
test('unchecking Drawings hides comments on the chart', { timeout: 180000 }, async () => {
await withChart(async page => {
const box = await chartBox(page);
await placeComment(page, box, 0.5, 0.4);
assert.ok(await commentNode(page), 'the comment was not created');
await page.locator('details:has(summary:text-is("Layers"))').evaluate(node => { node.open = true; });
const toggle = page.getByText('Drawings', { exact: true }).locator('input');
assert.equal(await toggle.isChecked(), true);
await toggle.click();
await page.waitForFunction(text =>
![...document.querySelectorAll('.chart-comment')].some(n => n.title === text), TEXT);
assert.equal(await commentNode(page), null, 'the comment stayed visible with Drawings off');
await toggle.click();
await page.waitForFunction(text =>
[...document.querySelectorAll('.chart-comment')].some(n => n.title === text), TEXT);
assert.ok(await commentNode(page), 'the comment did not return with Drawings on');
assertNoPageErrors(page, assert);
});
});
test('a comment is never a level', { timeout: 180000 }, async () => {
await withChart(async page => {
const box = await chartBox(page);

View file

@ -18,6 +18,20 @@ def ma(id_: str, period: int, price: float) -> Level:
)
def test_drawings_off_hides_manual_lines_from_confluence():
from app.api.ws import level_enabled
line = Level(
"ml1", LevelKind.MANUAL, Timeframe.D1, Side.RESISTANCE, 12, 1,
"line", 1000, 100.0, 0, None, 0, 1000, 1000, False, False,
)
assert level_enabled(line, {"manual": True, "drawings": True})
assert not level_enabled(line, {"manual": True, "drawings": False})
assert not level_enabled(line, {"manual": False, "drawings": True})
# Prefs written before the drawings key still show manuals.
assert level_enabled(line, {"manual": True})
def test_websocket_layer_preferences_are_isolated_per_connection(tmp_path):
app = FastAPI()
app.include_router(ws_router)