feat: a repaired invalid span is shaded faintly, not solidly

This commit is contained in:
Martino Ferrari committed 2026-10-08 22:59:07 +02:00
1 parent ebc67bd318
commit 1fc97d818f
7 files changed
+152 -25

No files matched your search

+38 -8
View File
@@ -3,7 +3,6 @@
#pragma once
#include <span>
#include <utility>
#include <vector>
#include <plot/core/rect.hpp>
@@ -51,6 +50,28 @@ constexpr bool x_strip_has_title(XStrip mode) noexcept {
return mode == XStrip::Both || mode == XStrip::TitleOnly;
}
/// One span of the primary x axis the host has declared invalid, in **data** coordinates --
/// `Region`'s convention. In udpscope's case a reserved hole: samples the stream's counter says
/// exist and that have not been delivered.
///
/// `repaired` says the host has since filled the span's samples in. Such a span is **still
/// reported**, because the shading is a record of what the window was missing, but it is drawn in
/// the fainter of the two tints -- so a reader can distinguish a gap that is still empty from one
/// that has since been repaired, and from a span that has left the list entirely.
struct InvalidSpan {
f64 x0 = 0.0;
f64 x1 = 0.0;
bool repaired = false;
};
/// One invalid span's measured shading: the rect, clipped to the axes' columns, and the state that
/// chooses its tint. A struct and not a bare `Rect` for `RegionLayout`'s reason -- the renderer
/// draws exactly what the layout measured and derives nothing of its own.
struct InvalidSpanLayout {
Rect fill;
bool repaired = false;
};
/// Pixel geometry of a plot's chrome, all of it in the same pixel space as
/// `Rect` (origin top-left) and therefore in **device** pixels: everything here is
/// what the renderer draws with, already converted from the logical metrics in
@@ -112,8 +133,10 @@ struct PlotLayout {
/// 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;
/// on a band and no label beside it. Each entry carries the span's `repaired` state alongside
/// its rect, so the tint is chosen where the geometry is measured rather than looked up again
/// at draw time.
std::vector<InvalidSpanLayout> 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
@@ -535,8 +558,8 @@ class Plot {
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
/// hole: samples the stream's counter says exist and that have not been delivered. Each span's
/// `x0`/`x1` are 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
@@ -546,8 +569,12 @@ class Plot {
///
/// 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_; }
///
/// A span whose `repaired` flag is set is shaded more faintly (`Style::invalid_repaired_fill`
/// against `Style::invalid_fill`). Clearing the shading is the host's decision and nothing
/// here retires a span on its own: a repaired span is drawn until the host stops sending it.
void set_invalid_spans(std::span<const InvalidSpan> spans);
std::span<const InvalidSpan> 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
@@ -876,6 +903,9 @@ 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();
/// Measures every invalid span into `layout_.invalid_spans`: its interval clipped to the axes'
/// columns, with a one-device-pixel floor, and the state that picks its tint. Here and not in
/// the renderer for `build_regions()`' reason -- one x mapping, the frame's own.
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.
@@ -924,7 +954,7 @@ class Plot {
std::vector<MeasureSlot> measures_;
std::vector<Annotation> annotations_;
std::vector<Region> regions_;
std::vector<std::pair<f64, f64>> invalid_spans_;
std::vector<InvalidSpan> 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
+5 -4
View File
@@ -513,10 +513,11 @@ 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
/// `Plot::invalid_spans()`, as `PlotLayout::invalid_spans` measured them: one fill per span,
/// in `style_.invalid_fill` or — for a span the host has marked repaired —
/// `style_.invalid_repaired_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);
+7
View File
@@ -62,6 +62,13 @@ struct Style {
/// 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 same shading for a span whose samples have since arrived and been written in, at well
/// under half the alpha. The gap is still reported -- the window was drawn without that data --
/// but a reader can tell a hole that is still empty from one that has been filled. Fainter by
/// alpha alone and not by hue, so the two read as two depths of one mark rather than as two
/// different kinds of thing; and still above the threshold where the difference from *no* band
/// is visible, which is the floor this alpha is chosen against.
Color invalid_repaired_fill = Color::hex(0x8A94A6, 0.08f);
/// 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*.