Files
plotter/include/plot/data/array_series.hpp
T
Martino Ferrari c2c8c596f3
CI / debug (tests + all adapters) (push) Canceled after 0s
CI / coverage (floor + measured table) (push) Canceled after 0s
CI / asan (AddressSanitizer) (push) Canceled after 0s
fix(data): the gather origin is the caller's, so a deep zoom draws on the sample
`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()`.
2026-10-05 18:43:50 +02:00

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