feat: a span declared invalid is shaded behind the data it is missing from

This commit is contained in:
Martino Ferrari committed 2026-10-08 20:11:16 +02:00
1 parent 1ae72d4903
commit ebc67bd318
6 files changed
+250

No files matched your search

+40
View File
@@ -344,6 +344,10 @@ void Plot::set_y_axis_origin_from_range(u32 axis, bool on) {
void Plot::set_x_note(std::string text) { x_note_ = std::move(text); }
void Plot::set_invalid_spans(std::span<const std::pair<f64, f64>> spans) {
invalid_spans_.assign(spans.begin(), spans.end());
}
f64 Plot::x_axis_origin(u32 axis) const {
return axis < x_axes_.size() ? x_axes_[axis].origin : 0.0;
}
@@ -1091,6 +1095,7 @@ const PlotLayout& Plot::layout() {
// measured from the *placed* live windows, which is what makes a region's data
// interval map to the pixel columns the frame will actually use.
build_regions();
build_invalid_spans();
build_legend();
// The crosshair last: its panel is fitted into `axes` and its lines are held inside it, and
// its rows are the sample values nearest a data x -- so it wants the transforms the
@@ -1358,6 +1363,41 @@ void Plot::build_regions() {
}
}
void Plot::build_invalid_spans() {
layout_.invalid_spans.clear();
if (invalid_spans_.empty()) return;
const Rect& axes = layout_.axes;
// The primary x axis' transform, taken here for `build_regions()`' reason: after the live
// windows have been placed and the strips have settled, so these are the columns the frame
// draws with and a band is anchored to the data by construction rather than by a second
// mapping that would have to be kept in step with this one.
const Transform tx = x_transform(0u);
// A hole can be narrower than a column -- one missing sample in a window of hundreds of
// thousands -- and a fill rounded to zero width would draw nothing, losing exactly the case
// the band is most needed for. One device pixel is the floor.
const f32 min_w = std::max(1.0f, style_.device(1.0f));
for (const std::pair<f64, f64>& span : invalid_spans_) {
const f64 a = std::min(span.first, span.second);
const f64 b = std::max(span.first, span.second);
if (!std::isfinite(a) || !std::isfinite(b)) continue;
const f32 lo = static_cast<f32>(tx.to_screen_x(a));
const f32 hi = static_cast<f32>(tx.to_screen_x(b));
// Scrolled out of the window: nothing to shade. Tested before the floor is applied, so a
// span off the left edge is dropped rather than pinned to it as a one-pixel band that
// accuses the samples at the window's start.
if (hi < axes.x || lo > axes.right()) continue;
const f32 first = std::clamp(lo, axes.x, axes.right());
const f32 last = std::clamp(hi, axes.x, axes.right());
const f32 w = std::min(std::max(last - first, min_w), axes.w);
// Widening a band at the right edge would run it over the axis line, so the floor is taken
// out of the left instead: the span's own edge is kept wherever there is room for it.
const f32 x = std::clamp(first, axes.x, axes.right() - w);
layout_.invalid_spans.push_back(Rect{x, axes.y, w, axes.h});
}
}
// --- crosshair (spec §15) --------------------------------------------------
CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept {
+9
View File
@@ -516,6 +516,9 @@ void Renderer::draw_plot(Plot& plot, const PlotLayout& layout) {
// against the line passes would not change a pixel.)
set_clip(layout.axes);
draw_grid(plot);
// The holes over the grid and under the data: a band is chrome of the *background*, not an
// overlay, so everything a frame draws inside the axes is drawn on top of it.
draw_invalid_spans(layout);
clear_clip();
draw_axis_lines(plot);
set_clip(layout.axes);
@@ -1728,6 +1731,12 @@ void Renderer::draw_cursor_label(const Plot& plot, const PlotLayout& layout, con
}
}
void Renderer::draw_invalid_spans(const PlotLayout& layout) {
for (const Rect& band : layout.invalid_spans) {
if (band.w > 0.0f) fill_pixels(band, style_.invalid_fill);
}
}
void Renderer::draw_regions(const Plot& plot, const PlotLayout& layout) {
const HotHandles hot = plot.hot_handles();
const f32 edge_w = style_.device(style_.line_width_px);