// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #pragma once #include #include #include #include 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 x, std::vector y); /// Replaces both arrays, then rebuilds derived state (== commit()). Same /// preconditions as the constructor. void reset(std::vector x, std::vector 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 `x_origin`. void gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector& 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& x() const noexcept { return x_; } protected: void copy_raw(u64 begin, u64 end, f32* out) const override; private: std::vector x_; std::vector 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