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()`.
This commit is contained in:
1 parent
59c2e45af7
commit
c2c8c596f3
15 files changed
+150
-95
No files matched your search
+21
-24
@@ -736,24 +736,16 @@ void Renderer::stage_segment(f64 origin_x, f64 x0, f64 y0, f64 x1, f64 y1, bool
|
||||
static_cast<f32>(space(y1, log_y))});
|
||||
}
|
||||
|
||||
void Renderer::stage_xy(const f32* xy, u32 n, f64 series_x0, f64 origin_x, bool log_x,
|
||||
bool log_y) {
|
||||
void Renderer::stage_xy(const f32* xy, u32 n, f64 origin_x, bool log_x, bool log_y) {
|
||||
if (!log_x && !log_y) {
|
||||
// The common case: reconstruct the absolute data x in f64 (`x0 + rel` --
|
||||
// the gather contract's own reconstruction), stage it relative to the
|
||||
// draw's origin, and the f32 is O(visible pixels), not O(epoch).
|
||||
staged_.clear();
|
||||
staged_.reserve(usize(n) * 2u);
|
||||
const f64 origin_s = space(origin_x, log_x);
|
||||
for (u32 i = 0; i < n; ++i) {
|
||||
staged_.push_back(static_cast<f32>(xy[2u * i] + series_x0 - origin_s));
|
||||
staged_.push_back(xy[2u * i + 1u]);
|
||||
}
|
||||
lines_.add_polyline_xy(staged_.data(), n);
|
||||
// The common case costs nothing at all: the gather emitted x relative to this very origin
|
||||
// (`draw_traces` asks it for `x_range.min`, which is the draw's origin too) and a linear y
|
||||
// needs no mapping, so the gathered buffer *is* the staged one.
|
||||
lines_.add_polyline_xy(xy, n);
|
||||
return;
|
||||
}
|
||||
for (u32 i = 0; i + 1u < n; ++i) {
|
||||
stage_segment(origin_x, xy[2u * i] + series_x0, xy[2u * i + 1u], xy[2u * i + 2u] + series_x0,
|
||||
stage_segment(origin_x, xy[2u * i] + origin_x, xy[2u * i + 1u], xy[2u * i + 2u] + origin_x,
|
||||
xy[2u * i + 3u], log_x, log_y);
|
||||
}
|
||||
}
|
||||
@@ -1026,7 +1018,12 @@ void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
|
||||
: 0u;
|
||||
|
||||
gather_.clear();
|
||||
series.gather(level, first, last, gather_, trace.mode);
|
||||
// The visible range's minimum is the gather origin: the emitted f32 is then O(the span on
|
||||
// screen), so its ULP is a fixed 2^-24 of the axis at every zoom. `series.x0()` -- the origin
|
||||
// before -- is the start of the *retained window*, so the quantisation was a fraction of data
|
||||
// the view need not even hold, and a deep zoom drew a sample visibly off the x a readout
|
||||
// quotes for it. See `Series::gather()`.
|
||||
series.gather(level, first, last, x_range.min, gather_, trace.mode);
|
||||
if (gather_.size() < 4u) continue; // fewer than two points is no segment
|
||||
|
||||
stats_.visible_samples += visible;
|
||||
@@ -1041,7 +1038,7 @@ void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
|
||||
const auto& yt = plot.y_transform(trace.axis_y);
|
||||
const f32 half = 0.5f * style_.device(style_.marker_px);
|
||||
for (u32 k = 0; k + 1 < gather_.size(); k += 2) {
|
||||
const f64 x_abs = series.x0() + static_cast<f64>(gather_[k]);
|
||||
const f64 x_abs = x_range.min + static_cast<f64>(gather_[k]);
|
||||
const f64 y_abs = static_cast<f64>(gather_[k + 1]);
|
||||
if (!std::isfinite(x_abs) || !std::isfinite(y_abs)) continue;
|
||||
const f32 sx = static_cast<f32>(xt.to_screen_x(x_abs));
|
||||
@@ -1059,11 +1056,11 @@ void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
|
||||
++stats_.lod_traces;
|
||||
}
|
||||
|
||||
// Gathered x is relative to `series.x0()` (the gather contract); the staging
|
||||
// reconstructs the absolute data x in f64 and emits `to_space(x_abs) -
|
||||
// to_space(origin)` -- the visible x range's minimum is the natural origin
|
||||
// for a trace -- so the f32 is O(visible pixels), not O(epoch) (B1). The
|
||||
// mapping folds the origin back into the offset uniform, in f64.
|
||||
// Gathered x is relative to `x_range.min` (the gather contract, with the origin this
|
||||
// function asked for); the staging reconstructs the absolute data x in f64 and emits
|
||||
// `to_space(x_abs) - to_space(origin)` about that same origin, so the f32 is O(visible
|
||||
// pixels), not O(epoch) (B1). The mapping folds the origin back into the offset uniform,
|
||||
// in f64.
|
||||
const bool log_x = plot.x_axis(trace.axis_x).log;
|
||||
const bool log_y = plot.y_axis(trace.axis_y).log;
|
||||
const Mapping map = mapping(plot, trace.axis_x, trace.axis_y, x_range.min);
|
||||
@@ -1087,7 +1084,7 @@ void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
|
||||
f64 next = std::numeric_limits<f64>::infinity();
|
||||
for (usize k = count; k > 0; --k) {
|
||||
const usize p = k - 1u; // the point index; `i` above is the trace's
|
||||
const f64 x = static_cast<f64>(gather_[2u * p]) + series.x0();
|
||||
const f64 x = static_cast<f64>(gather_[2u * p]) + x_range.min;
|
||||
if (!(x < next)) continue; // out of order with what follows: unreadable, and a NaN too
|
||||
next = x;
|
||||
gather_[2u * (count - 1u - kept)] = gather_[2u * p];
|
||||
@@ -1114,7 +1111,7 @@ void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
|
||||
// trace/axis hierarchy on a high-DPI panel. `width == 0` is scatter: the
|
||||
// markers below are the whole trace.
|
||||
if (trace.width > 0.0f) {
|
||||
stage_xy(gather_.data(), points, series.x0(), x_range.min, log_x, log_y);
|
||||
stage_xy(gather_.data(), points, x_range.min, log_x, log_y);
|
||||
flush_lines(map, trace.color, style_.device(trace.width), dash);
|
||||
}
|
||||
|
||||
@@ -1132,7 +1129,7 @@ void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
|
||||
const f64 origin_s = space(x_range.min, log_x);
|
||||
u32 staged = 0;
|
||||
for (u32 p = 0; p < points; p += stride) {
|
||||
const f64 x_abs = static_cast<f64>(gather_[2u * p]) + series.x0();
|
||||
const f64 x_abs = static_cast<f64>(gather_[2u * p]) + x_range.min;
|
||||
const f32 rx = static_cast<f32>(space(x_abs, log_x) - origin_s);
|
||||
const f32 ry = static_cast<f32>(space(gather_[2u * p + 1u], log_y));
|
||||
const Vec2 centre{rx * map.scale.x + map.offset.x, ry * map.scale.y + map.offset.y};
|
||||
|
||||
Reference in new issue
Block a user