#pragma once #include #include #include namespace plot::data { /// Aggregate of one bucket: the extremes of the samples it covers. struct MinMax { f32 min = 0.0f; f32 max = 0.0f; }; /// Samples per level-0 bucket. Fixed so the LOD geometry is identical for every /// series and the level maths is a shift. inline constexpr u32 kBucket = 64; /// Incremental min/max LOD pyramid. /// /// Level 0 buckets kBucket consecutive samples and keeps one (min, max) per /// bucket; level l buckets 2^l level-0 buckets. Memory is n/4 bytes -- about 6% /// of 4-byte raw samples. append() merges into the last bucket at each level and /// walks up the O(log n) chain above it, so building is amortised O(1) per sample /// and nothing is ever recomputed from raw samples. /// /// A pyramid stores *aggregates*, so it can only answer exactly for whole /// buckets; a range with partial edge buckets would silently pull in samples /// outside the range. Every query here is therefore bucket-grained and named /// *_buckets, and exact range queries live on Series, which owns the raw samples. /// reduce() is the one non-*_buckets query: it is the *covering* aggregate over /// the level-0 buckets that [begin, end) touches. Use it to ask which buckets a /// range spans, never to measure a range. class MinMaxPyramid { public: MinMaxPyramid() { add_level(); } /// Sizes every level (buckets and prefix sums) for `n` samples so a following /// run of appends performs no reallocation. The pyramid itself stays empty. void reserve(u64 n); void append(f32 v); void clear(); u64 size() const noexcept { return size_; } /// At least 1, even when empty, so level_count() - 1 is always valid. u32 level_count() const noexcept { return live_levels_; } /// Level `l`'s buckets; empty span when `l` is beyond level_count(). std::span level(u32 l) const; /// Min/max over the whole level-0 buckets fully contained in [begin, end). /// Partial edge buckets are excluded, never approximated; an empty result is a /// degenerate Range with span() == 0. Range reduce_buckets(u64 begin, u64 end) const; /// Sum of the samples in those same whole buckets, accumulated in double. f64 sum_buckets(u64 begin, u64 end) const; /// Sum of squares of the samples in those same whole buckets. f64 sumsq_buckets(u64 begin, u64 end) const; /// Min/max over the covering level-0 buckets: every bucket that holds a sample /// of [begin, end). Exact only when the range is bucket-aligned. Range reduce(u64 begin, u64 end) const; /// Pyramid level whose buckets hold about `samples_per_pixel` samples, for a /// window of `n` samples: 0 when a pixel covers at most one bucket. Clamped to /// the levels that exist, to levels with more than one bucket, and to levels /// that give the window at least two buckets. u32 level_for(u64 n, f64 samples_per_pixel) const; /// Index of the sample nearest x on the uniform grid (x0 + i*dx), clamped to /// [0, size() - 1]. Returns 0 for an empty pyramid or a non-positive dx. u64 snap(f64 x, f64 x0, f64 dx) const; private: void add_level(); u64 size_ = 0; u64 reserved_n_ = 0; u32 live_levels_ = 1; std::vector> levels_; // levels_[0] = kBucket granularity std::vector> sums_, sumsqs_; // inclusive prefix sums per level }; } // namespace plot::data