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

+26
View File
@@ -2,6 +2,8 @@
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <span>
#include <utility>
#include <vector>
#include <plot/core/rect.hpp>
@@ -105,6 +107,13 @@ struct PlotLayout {
/// controller's hit test reads the very same rects, so where a region is drawn and
/// where a pointer can grab it cannot drift apart.
std::vector<RegionLayout> regions;
/// Every invalid span's shading, in `Plot::invalid_spans()` order and clipped to the axes'
/// columns. Measured here for the regions' first reason -- the frame draws exactly these
/// numbers -- and reserving nothing, so a hole cannot move the strips the labels were
/// measured into. A span entirely outside the window contributes no rect at all, so this is
/// not index-aligned with `invalid_spans()` and nothing needs it to be: there is no hit test
/// on a band and no label beside it.
std::vector<Rect> invalid_spans;
/// The crosshair's geometry, when one is active: the two lines' stroke boxes, the
/// readout panel and one row per visible trace with the value of the sample nearest the
/// pointer. Placed here rather than derived by the renderer, exactly as the legend and
@@ -525,6 +534,21 @@ class Plot {
void set_x_note(std::string text);
const std::string& x_note() const noexcept { return x_note_; }
/// Spans of the primary x axis the host has declared invalid -- in udpscope's case a reserved
/// hole: samples the stream's counter says exist and that have not been delivered. Each pair
/// is `(x_begin, x_end)` in **data** coordinates, `Region`'s convention, and is shaded behind
/// the traces and over the grid.
///
/// Deliberately *not* a `Region`: a region is interactive -- draggable bounds, hot zones, a
/// label, a cached statistic, a controller hit test -- and a plot can carry dozens of holes at
/// once, which would put a hundred-odd grab targets in the way of every pointer event for
/// marks nobody can usefully drag. This is passive shading and nothing else.
///
/// The span is copied, so the caller's storage need not outlive the call. Spans need not be
/// sorted and may overlap; an empty call clears the shading.
void set_invalid_spans(std::span<const std::pair<f64, f64>> spans);
std::span<const std::pair<f64, f64>> invalid_spans() const noexcept { return invalid_spans_; }
/// The measurement for `id`. Re-evaluated only when a cursor moved, when the
/// measure was added, or when the series underneath it changed: a different
/// series, a longer one, or a sliding window whose sample count stayed the same
@@ -852,6 +876,7 @@ class Plot {
/// region's pixel geometry is derived -- the controller hit-tests what this
/// measured instead of mapping the bounds through the transform again.
void build_regions();
void build_invalid_spans();
/// Exact statistics of `region` over `series`, or an invalid result when the
/// interval covers no sample. The one place the region arithmetic lives.
static RegionStats evaluate_region_stats(const Region& region, const data::Series& series);
@@ -899,6 +924,7 @@ class Plot {
std::vector<MeasureSlot> measures_;
std::vector<Annotation> annotations_;
std::vector<Region> regions_;
std::vector<std::pair<f64, f64>> invalid_spans_;
/// The (region, trace) statistics cache, filled on demand by `region_stats()`
/// and so mutable: a lookup on a const Plot may cache, exactly as `measures_`
/// does. Entries are created for the pairs a caller actually asks about, never
+6
View File
@@ -513,6 +513,12 @@ class Renderer {
void draw_plot(Plot& plot, const PlotLayout& layout);
void draw_grid(const Plot& plot);
/// `Plot::invalid_spans()`, as `PlotLayout::invalid_spans` measured them: one fill per span in
/// `style_.invalid_fill`. Over the grid and under everything else inside the axes, so a hole
/// reads as a property of the plot's background -- the grid shows through it and a trace that
/// does have data in those columns is drawn on top, which is what distinguishes a reserved
/// hole from a trace that has gone quiet.
void draw_invalid_spans(const PlotLayout& layout);
void draw_axis_lines(const Plot& plot);
/// The chrome helpers take the frame's layout: `Plot::layout()` recomputes the
/// ticks, the strips and the grid, so it is computed once, in `draw()`, and handed
+7
View File
@@ -55,6 +55,13 @@ struct Style {
Color axis_text = Color::hex(0xC8D0DC);
Color cursor = Color::hex(0xFFB020);
Color measure_fill = Color::hex(0xFFB020, 0.15f);
/// The shading over a span the host has declared invalid (`Plot::set_invalid_spans()`): the
/// axis furniture's own neutral grey at a low alpha, on the same model as `measure_fill`
/// above. Neutral and not a trace colour on purpose -- a band is the *absence* of data, and
/// one in a hue a trace could have would read as a signal rather than as a gap. The alpha is
/// low enough that the grid under it stays legible, which is what makes the band read as a
/// region of the plot rather than as a panel laid over it.
Color invalid_fill = Color::hex(0x8A94A6, 0.18f);
/// The crosshair's colour: its two lines and the readout panel's border (spec §15). Markers are
/// deliberately *not* this colour -- they carry each trace's own, because a marker says *which*
/// trace and the line says *where*.