Skip to content

fix(dashboard): step the CPR levels; whitespace never broke the line - #178

Merged
DoRmAmMu1997 merged 1 commit into
mainfrom
fix/cpr-stepped-bands
Sep 21, 2026
Merged

DoRmAmMu1997 merged 1 commit into
mainfrom
fix/cpr-stepped-bands

Conversation

@DoRmAmMu1997

Copy link
Copy Markdown
Owner

What

Per-day CPR (#174) drew a shallow diagonal from one day's levels to the next on the 1m and 5m timeframes instead of separate horizontal bands. This fixes it.

Root cause

Proven from the vendored bundle, not inferred. A whitespace point cannot break a line series in lightweight-charts 5.2.1 — the data layer strips it before the series ever sees it:

uM(t,i){ ... this.hM.set(t, i.filter(_s)) ... }      // _s(t){return void 0!==t.Wt}

A whitespace row carries no value tuple (Wt), so filter(_s) removes it. The renderer then strokes one continuous path. The whitespace contributed exactly one thing: a spare time-scale column.

Two things stretched the artifact from a short step into a long diagonal:

  • 5m band ends are phantom times. A band's to is 15:29 and 5-minute buckets end at 15:25, so every session injected columns no candle occupies.
  • Bands were admitted on interval overlap alone, without checking a candle exists. When the history CSV is older than the live window, the merged bars splice across the hole while the ladder still yields a band per calendar day inside it — a run of candle-less bands, each worth three columns.

The fix

Each level now carries one point per band, placed on the first loaded candle at or after the band's start, drawn with LineType.WithSteps. The renderer's step branch is lineTo(x, previousY) then lineTo(x, y) — horizontal, then vertical — so a slope is unreachable in it. A band with no loaded candle is skipped, which leaves the previous level running rather than drawing it into empty space, and every point sits on a time the candle series already owns, so the CPR series adds zero columns to the shared scale.

Rejected: a custom ISeriesPrimitive (true gaps, ~100 lines of new rendering code and a new failure surface) and one series per band (rebuilds hundreds of series on every scroll).

Verification

Scratch server on port 8799 against the real five-year CSV, in the shape that reproduces it — history ending days before the live window, so the chart splices across sessions with candles and no band.

Summed vertical travel of each level line across the view, before → after:

level before after
pivot 43.4 px over 458 columns 0.0
BC/TC 47.9 0.0
PDH/PDL 50.8 0.2
R1/R2 22.5 0.0
S1/S2 66.3 0.0

1m, 5m and Daily all step cleanly; Daily still shows monthly bands and its caption still matches the drawn level; the group toggles still follow immediately.

Gates: both unittest suites (606 + 28), pytest (1,580), coverage policy + 74.2 % total locally, ruff, mypy ×2, compileall, bandit as CI runs it, pre-commit, node --check.

Notes worth reading

  • test_the_cpr_levels_are_drawn_as_steps_and_never_rely_on_whitespace is a source assertion — there is no JS runtime here. It fails on the pre-fix script (verified) and exists so a future edit that "restores the gap" with a whitespace point fails here instead of shipping the diagonal back.
  • ADR-0017 asserted the whitespace mechanism, so it carries a correction rather than a quiet edit.
  • The feat(dashboard): per-day CPR, five years of chart history, and a Daily timeframe #174 pixel check could not have caught this: it asserted a maximum column height of one pixel to rule out vertical connectors, which a shallow diagonal satisfies by construction. The LLD now says to measure the slope instead, to match the colour against the background blend (a sloped 1px line is anti-aliased across two rows), and to avoid #ef5f5f/#35c46b, which are R1/R2 and S1/S2 and the candle colours.

🤖 Generated with Claude Code

Per-day CPR (#174) drew a shallow DIAGONAL from one day's levels to the
next on 1m and 5m instead of separate horizontal bands.

The cause is in the library, not the data. lightweight-charts 5.2.1 runs
`rows.filter(hasValue)` in its data layer, so the whitespace point the
bands carried -- a time with no value, meant to break the stroke -- never
reached the series. The line was one continuous path and the gap never
existed. The whitespace contributed exactly one empty column.

Two things stretched that artifact from a step into a long diagonal: a
band's `to` of 15:29 is not a 5-minute bucket boundary, and bands were
admitted on interval overlap alone, so a CSV older than the live window
produced a run of bands with no candles under them.

Each level now carries ONE point per band, placed on the first loaded
candle at or after the band's start, drawn with `LineType.WithSteps` --
the renderer's step branch is `lineTo(x, previousY)` then `lineTo(x, y)`,
so a slope is unreachable. A band with no loaded candle is skipped, which
leaves the previous level running rather than drawing into empty space,
and every point sits on a real bar time so the CPR series adds no columns
to the shared scale.

Measured on a scratch server against the five-year CSV, in the shape that
reproduces it -- history ending days before the live window. Summed
vertical travel of each level line across the view, before -> after:
pivot 43.4px -> 0.0, BC/TC 47.9 -> 0.0, PDH/PDL 50.8 -> 0.2,
R1/R2 22.5 -> 0.0, S1/S2 66.3 -> 0.0. 1m, 5m and Daily all step cleanly
and the group toggles still follow immediately.

ADR-0017 asserted the whitespace mechanism, so it carries a correction
rather than a quiet edit. The LLD gains both traps, including why the
#174 pixel check passed: it measured column HEIGHT, which a shallow
diagonal satisfies by construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@DoRmAmMu1997
DoRmAmMu1997 merged commit 02aba07 into main Sep 21, 2026
7 of 8 checks passed
@DoRmAmMu1997
DoRmAmMu1997 deleted the fix/cpr-stepped-bands branch September 21, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant