`Series::gather()` stages x as f32 relative to `x0()` -- the start of the *retained window* -- so the value's ULP was a fraction of data the view need not even hold: 1.9 us over a 20 s ring. A readout quotes `x_at()` in exact f64, so at a 10 us view across 700 px that put more than a hundred pixels between where a sample is drawn and the x reported for it. The origin is now a parameter, placed before `out_xy` so every call site is a type error rather than a silent change of behaviour. `Renderer::draw_traces` passes the visible range's minimum, which makes the error a fixed 2^-24 of the span on screen -- 4e-5 of a pixel over a 700 px axis -- at every zoom. `stage_xy()` loses its second origin as a consequence: the gather origin and the draw origin are now always the same value, so the linear case hands the gathered buffer straight to `add_polyline_xy()` with no per-point copy. `Plot::crosshair_point()` names the query itself, the one origin that cannot lose precision for a caller that might. Guarded by `a_gather_origin_at_the_view_keeps_a_deep_zoom_on_the_sample_it_quotes`, which measures the error in pixels both ways: under 0.05 px with the view's minimum, over 10 px with `x0()`.
57 lines
2.0 KiB
C++
57 lines
2.0 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#pragma once
|
|
#include <vector>
|
|
|
|
#include <plot/core/types.hpp>
|
|
#include <plot/data/series.hpp>
|
|
|
|
namespace plot::data {
|
|
|
|
/// Series over a uniform-dx sample vector.
|
|
///
|
|
/// The vector is taken by value and owned from then on, so the caller cannot
|
|
/// mutate the samples behind the series' back; the store changes only through
|
|
/// reset() (a whole new vector) or commit(), which rebuilds the pyramid and the
|
|
/// cached bounds for whatever y_ currently holds.
|
|
class ArraySeries final : public Series {
|
|
public:
|
|
ArraySeries(std::vector<f32> y, f64 x0, f64 dx);
|
|
/// Replaces samples and grid, then rebuilds derived state (== commit()).
|
|
void reset(std::vector<f32> y, f64 x0, f64 dx);
|
|
/// Rebuilds the pyramid and cached bounds from y_ as it stands now.
|
|
void commit();
|
|
/// Appends one sample incrementally: the pyramid folds it (`append`, amortised
|
|
/// O(1)) and the cached bounds extend in O(1), NaN-aware, instead of commit()
|
|
/// rebuilding the whole store. The wholesale path stays `commit()`.
|
|
void push(f32 y);
|
|
|
|
u64 size() const override;
|
|
f64 x0() const override;
|
|
f64 dx() const override;
|
|
Range x_bounds() const override;
|
|
Range y_bounds() const override;
|
|
const MinMaxPyramid& pyramid() const override;
|
|
void gather(u32 level, u64 begin, u64 end, f64 x_origin, std::vector<f32>& out_xy,
|
|
GatherMode mode = GatherMode::Envelope) const override;
|
|
|
|
protected:
|
|
void copy_raw(u64 begin, u64 end, f32* out) const override;
|
|
|
|
private:
|
|
std::vector<f32> y_;
|
|
f64 x0_ = 0.0;
|
|
f64 dx_ = 1.0;
|
|
MinMaxPyramid pyramid_;
|
|
Range x_bounds_{};
|
|
Range y_bounds_{};
|
|
/// False until a finite sample has been seen: `y_bounds_` is then a
|
|
/// placeholder (zero-span), not an aggregate, and the next finite sample
|
|
/// seeds it. This is what makes push()'s incremental extension and commit()'s
|
|
/// rebuild agree on a window that holds non-finite samples.
|
|
bool y_has_finite_ = false;
|
|
};
|
|
|
|
} // namespace plot::data
|