feat(plot)!: harden pipeline, XY series, dash/markers, touch, packaging
Breaking: Series::gather emits x relative to x0() and takes a GatherMode; consumers reconstruct absolute x in f64 before mapping (series.hpp documents the contract). Bugs, each pinned by a fail-before regression test: - render: f32 staging quantised epoch-time x into 128 px columns; every data-space draw now stages to_space(x) - to_space(origin) with the origin folded into the offset uniform in f64 (epoch/zero-origin twins rasterise identically) - ticks: major test was ULP-fragile and blanked epoch axes; majors are an integer-index test and are never truncated by the minor cap - gather: LOD envelope emitted buckets straddling [begin,end); ceil/floor bucket edges + raw lead/trail spans - live follow: log x axes held a linear span and zoomed every frame; span and offset now live in axis space (plot + both figure link-group paths) - input: a zero-notch wheel disarmed auto_scale on every axis; dy==0 pans x, factor == 1 / inf refused in wheel and drag zoom - render: the single text flush painted legend names over the crosshair readout panel; two batches, under flushed before the panel - data: NaN dropped from min/max unless it seeded the fold; non-finite samples are now excluded from every aggregate and from cached bounds Robustness: viewport normalisation (no clamp UB), non-finite range refusal, pyramid level cap at 57 (no shift/div UB through public gather), rounded ring capacity documented + pre-sized, GL blend func and UNPACK_ALIGNMENT save/restore, Renderer::release_gl() teardown, u64 buffer epochs, annotation captions clamped, button-aware GLFW row capture, cached figure row rects (no per-event layout), ruler-pair measurement refused, span-nonfinite guards in axis_ends. Features: XY series (sorted x, binary-search neighbours/snap, LOD over sample index); StreamSeries::freeze(); ArraySeries::push(); per-trace dash and markers (scatter at width 0) with auto or literal stride; GatherMode Envelope/Nearest/Mean via bucket_sum/sumsq; Style::line_aa_px; per-glyph text colour (readout values in trace colour); wheel-dx x pan; arrow-key pan; Escape cancels a drag and restores captured ranges; modifier-masked box zoom (Shift both, Ctrl x, Alt y); double-click reset via PointerEvent::time_s; ImGui key repeat unified; pinch gestures on Qt and GTK; pixelDelta wheel path and DevicePixelRatioChange handling; GNUInstallDirs, SOVERSION, SameMinorVersion, generated plot/version.hpp, relocatable plot.pc, licence install, FILES_MATCHING, GitHub Actions CI (debug/coverage/asan). Suite 283 -> 350 cases, 0 failed; coverage 93.9% (3730/3971) over 24 files, floor 80/90 held; ASAN preset suite green; all four demos run headless (the GTK scripted-resize check needs a window manager and fails identically on the parent commit); readme_main.cpp committed and compiles with the README command; pkg-config and find_package consumption verified from a relocated install prefix.
This commit is contained in:
1 parent
f132a5ec92
commit
38ef459897
77 files changed
+5905
-688
No files matched your search
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
|
||||
#pragma once
|
||||
#include <vector>
|
||||
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/data/minmax_pyramid.hpp>
|
||||
#include <plot/data/series.hpp>
|
||||
|
||||
namespace plot::data {
|
||||
|
||||
/// Series over its own x array: sample i lives at `x()[i]`, and the samples are
|
||||
/// sorted by ascending x. This is the store for data whose x is *not* on a
|
||||
/// uniform grid -- scattered measurements, event times, a log-swept frequency
|
||||
/// axis -- where `ArraySeries`' fixed `dx` cannot hold.
|
||||
///
|
||||
/// **Precondition: `x` is sorted ascending.** The O(log n) crosshair and
|
||||
/// nearest-sample lookups are binary searches, which are only correct on sorted
|
||||
/// input; the constructor and `reset()` assert sortedness in debug builds and
|
||||
/// the behaviour with unsorted input is undefined in release ones. Callers with
|
||||
/// a possibly-unsorted source sort it first (sorting the *pairs*, not the
|
||||
/// arrays independently, or the samples get scrambled).
|
||||
///
|
||||
/// Everything the base `Series` contracts to holds, with the sample index as
|
||||
/// the grid: the pyramid buckets samples by *index* (`kBucket` samples per
|
||||
/// level-0 bucket, exactly as for a uniform series) and `gather()` emits x
|
||||
/// relative to `x0()` -- `f32(x_at(i) - x0())`, never the absolute
|
||||
/// coordinate -- so an epoch-scale x array narrows to f32 the same safe,
|
||||
/// O(visible pixels) way a uniform one does. The queries that need a *uniform*
|
||||
/// grid are overridden with the binary-search answer instead: `x_at()` reads
|
||||
/// the array, `neighbours()` brackets with `upper_bound`/`lower_bound`, and
|
||||
/// `snap_index()` compares the two candidates the search leaves. `dx()` is
|
||||
/// meaningless here and returns 0: nothing that routes through
|
||||
/// `x_at`/`neighbours`/`snap_index` consults it.
|
||||
class XYSeries final : public Series {
|
||||
public:
|
||||
/// Takes both arrays by value. `x.size() == y.size()` and `x` sorted
|
||||
/// ascending (asserted in debug).
|
||||
XYSeries(std::vector<f64> x, std::vector<f32> y);
|
||||
/// Replaces both arrays, then rebuilds derived state (== commit()). Same
|
||||
/// preconditions as the constructor.
|
||||
void reset(std::vector<f64> x, std::vector<f32> y);
|
||||
/// Rebuilds the pyramid and the cached bounds from the arrays as they stand.
|
||||
void commit();
|
||||
|
||||
u64 size() const override;
|
||||
/// x of sample 0: the first (smallest) x in the array.
|
||||
f64 x0() const override;
|
||||
/// Always 0: the grid is the x array, not a uniform step.
|
||||
f64 dx() const override;
|
||||
/// From the first and last x; a zero span for an empty series.
|
||||
Range x_bounds() const override;
|
||||
Range y_bounds() const override;
|
||||
const MinMaxPyramid& pyramid() const override;
|
||||
/// Same contract as `Series::gather`, with x emitted relative to `x0()`.
|
||||
void gather(u32 level, u64 begin, u64 end, std::vector<f32>& out_xy,
|
||||
GatherMode mode = GatherMode::Envelope) const override;
|
||||
|
||||
/// The sample's own x.
|
||||
f64 x_at(u64 i) const override;
|
||||
/// Binary-search bracket of data-space `x`: false outside
|
||||
/// `[x_at(0), x_at(size() - 1)]`, else the last sample at or left of `x` and
|
||||
/// the first at or right of it (equal on an exact hit).
|
||||
bool neighbours(f64 x, u64& i0, u64& i1) const override;
|
||||
/// Binary-search nearest sample, clamped to `[0, size() - 1]`; ties go to the
|
||||
/// lower index, and an empty series returns 0.
|
||||
u64 snap_index(f64 x) const override;
|
||||
|
||||
/// The x array itself (sorted ascending).
|
||||
const std::vector<f64>& x() const noexcept { return x_; }
|
||||
|
||||
protected:
|
||||
void copy_raw(u64 begin, u64 end, f32* out) const override;
|
||||
|
||||
private:
|
||||
std::vector<f64> x_;
|
||||
std::vector<f32> y_;
|
||||
MinMaxPyramid pyramid_;
|
||||
Range x_bounds_{};
|
||||
Range y_bounds_{};
|
||||
/// False until a finite sample has been seen (see `ArraySeries`): the cached
|
||||
/// y bounds are then a placeholder, and the first finite sample seeds them.
|
||||
bool y_has_finite_ = false;
|
||||
};
|
||||
|
||||
} // namespace plot::data
|
||||
Reference in new issue
Block a user