Commit Graph
7 Commits
Author SHA1 Message Date
Martino Ferrari 0ac5d47fa6 perf(data): absolute-space pyramid so acquire() is O(produced), not O(window)
Frame cost was O(capacity x levels) once the ring saturated: acquire() saw the
window base move (it always moves once the ring wraps), cleared the pyramid and
re-appended all 1,048,576 retained samples, and append() walked every live level
with no early stop -- 13,647,936 bucket merges and 73.8 ms per frame at a
1M-sample window, scaling with the window, which is what capped the 1 MS/s demo
at 32-58 ms/frame while it was still filling.

Index the pyramid in absolute bucket space instead. Level l bucket k covers the
samples [k * (kBucket << l), (k + 1) * (kBucket << l)), so a bucket's span never
depends on where the window starts and the window can slide over the aggregate
instead of forcing it to be rebuilt. Each level keeps its complete buckets in a
deque plus the absolute base of its first entry; append() folds a sample into the
single in-progress level-0 bucket and, only when that bucket completes, cascades
upward, stopping at the first level that gains no new entry; a new drop_before()
slides the window by discarding leading buckets and remembers where the window
begins. acquire() is now: drop to the new base, then append exactly what was
produced -- 256 merges and 0.034 ms per frame for 8192 new samples, independent
of the window, and memory bounded by the window (32,767 entries after 50M samples
through a 1M ring, the same as after 10M). A producer that laps the consumer by
more than capacity() between two acquires restarts the window rather than
splicing a gap.

Interface: `level(l) -> std::span<const MinMax>` cannot survive a ring and is
replaced by level_base/level_size/bucket in absolute bucket coordinates, plus
window_base/window_end/drop_before. The *_buckets queries stay in window
coordinates (index 0 = oldest retained sample), so Series keeps its window
semantics for callers; its exactness split now aligns in absolute space
(window_base() + the ceil/floor) because the bucket grid is absolute. The sums
move from per-level prefix sums to per-bucket ones -- the only representation a
sliding front can keep exact -- and StreamSeries::y_bounds() becomes
reduce(0, size()), exact in O(log n + kBucket) per frame instead of a scan of the
window. The pyramid owns the published window now, so there is no second copy of
base/end to keep in step, and gather() emits only buckets that are both complete
and wholly retained.

Tests: test-only merge counters (mirroring PLOT_PYRAMID_COUNT_COVERS, absent from
every non-test build) pin an acquire proportional to production and independent
of capacity, an append cost amortised over 16 live levels, and bounded memory;
brute-force checks pin absolute bucket aggregates across slides, sliding-window
reduce/sum/sumsq over unaligned ranges, and the level-1 gather mapping. The demo
now draws 946 samples/px at 1.03 ms/frame (967 fps) where the saturated window
cost 54.96 ms/frame (18 fps).
2026-09-28 00:05:15 +02:00
Martino Ferrari e0cdd95a3b fix(input): pan and box zoom through layout().axes, not the viewport
Every pixel-to-data conversion in the controller now maps through the
same rectangle Plot::x_transform/y_transform are built from:

- pan() derives the scale from layout().axes.w/.h, so a drag moves the
  grabbed data point by exactly the drag distance (the viewport model
  stranded it behind the pointer by the label-strip fraction).
- apply_box_zoom() clamps the selection to layout().axes and sets each
  range from the transforms (x_transform().to_data_x(l/r), y_transform()
  .to_data_y(bottom/top)); the selection's bottom edge is the y minimum.
- end_drag() takes the release event and refreshes the selection from its
  coordinates before applying, so a host that reports only press and
  release still frames the box instead of degrading it to a click.
- axis_zoom() uses a per-pixel sensitivity (200 px == one decade) rather
  than reusing the wheel's per-notch constant, so a pixel drag is no
  longer treated as a wheel notch.

Tests now assert the grab-and-drag invariant through the transform (the
old pixel-fraction assertions certified the viewport defect), add a
press+release-only box zoom, and pin the axis-zoom magnitude.
2026-09-27 22:41:42 +02:00
Martino Ferrari 606e40a9f1 fix(plot): single-rounding clock labels, log-axis fits that refuse non-positive data 2026-09-27 22:28:56 +02:00
Martino Ferrari 8f438b049b docs: repoint plan call sites off the removed MinMaxPyramid::reduce 2026-09-27 21:45:19 +02:00
Martino Ferrari c93c98002b docs: exact range queries belong on Series, not on the bucket-grained pyramid 2026-09-27 21:20:22 +02:00
Martino Ferrari 8a7e9e97f8 feat: project skeleton, core types and math with transform tests 2026-09-27 20:54:19 +02:00
Martino Ferrari e4051bc6b5 docs: design spec and implementation plan for the plot library 2026-09-27 20:50:40 +02:00