feat(render): mark the samples of a window sparse enough to see them

`Style::marker_below_samples` (50 by default, `0` for never) draws a `marker_px` dot on every
*raw* sample when the visible window holds at most that many, in the trace's own colour. Zoomed
in far enough to see individual samples, the polyline between them stops being the shape a reader
is looking at -- a handful of points across a whole panel ARE the picture -- and a dot makes each
one a place rather than a bend in a line. Level 0 only: above it the geometry is pyramid buckets,
which are not points anyone can find.

Test: a_sparse_window_marks_its_samples -- the same flat series at ten samples and at a hundred.
Ten gives exactly ten dots, evenly spaced (their centres are checked against the panel's own
step, so "they are the samples" is asserted rather than "something was painted"); a hundred is
past the threshold and gives none. The dots *group* into runs of columns (a marker is `marker_px`
wide), which is how the test reads one marker per sample -- counting columns read three.
This commit is contained in:
Martino Ferrari committed 2026-10-03 15:39:49 +02:00
1 parent fba8d985cf
commit 7dbe4e7183
5 files changed
+121

No files matched your search

+23
View File
@@ -1028,6 +1028,29 @@ void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
if (gather_.size() < 4u) continue; // fewer than two points is no segment
stats_.visible_samples += visible;
// Markers for a window sparse enough that its samples are the shape: one dot per raw sample,
// in the trace's own colour. Level 0 only -- above it the geometry is *buckets*, which are not
// points a reader can find. Drawn after the polyline they sit on, so a dot is never half
// covered by its own line, and inside the axes only (a marker is a place, and a place outside
// the frame is not one).
if (level == 0u && style_.marker_below_samples > 0.0f && visible > 0u &&
visible <= static_cast<u64>(style_.marker_below_samples)) {
const auto& xt = plot.x_transform(trace.axis_x);
const auto& yt = plot.y_transform(trace.axis_y);
const f32 half = 0.5f * style_.device(style_.marker_px);
for (u32 k = 0; k + 1 < gather_.size(); k += 2) {
const f64 x_abs = series.x0() + static_cast<f64>(gather_[k]);
const f64 y_abs = static_cast<f64>(gather_[k + 1]);
if (!std::isfinite(x_abs) || !std::isfinite(y_abs)) continue;
const f32 sx = xt.to_screen_x(x_abs);
const f32 sy = yt.to_screen_y(y_abs);
if (!(sx >= layout.axes.x && sx <= layout.axes.right() && sy >= layout.axes.y &&
sy <= layout.axes.bottom())) {
continue;
}
fill_pixels(Rect{sx - half, sy - half, 2.0f * half, 2.0f * half}, trace.color);
}
}
if (level == 0u) {
++stats_.raw_traces;
} else {