fix(plot): single-rounding clock labels, log-axis fits that refuse non-positive data
This commit is contained in:
1 parent
93fc06f050
commit
606e40a9f1
7 files changed
+258
-73
No files matched your search
+49
-48
@@ -32,28 +32,48 @@ Axis axis_from(const AxisDesc& desc) {
|
||||
return axis;
|
||||
}
|
||||
|
||||
/// Adds the margin an auto-fitted range needs. A zero-span union (a single-sample
|
||||
/// series) is widened instead of padded, because a degenerate range has no span to
|
||||
/// take 2% of and would leave the transform's inverse ambiguous.
|
||||
Range fit_range(Range r, bool log) {
|
||||
if (!std::isfinite(r.min) || !std::isfinite(r.max)) return Range{0.0, 1.0};
|
||||
f64 lo = std::min(r.min, r.max);
|
||||
f64 hi = std::max(r.min, r.max);
|
||||
/// Fits `axis`' range to the union of its traces' bounds, returning false when the
|
||||
/// union cannot be fitted and the range is then left untouched.
|
||||
///
|
||||
/// A linear axis takes 2% of the span on both ends; a zero-span union (a
|
||||
/// single-sample series) is widened instead of padded, because a degenerate range
|
||||
/// has no span to take 2% of and would leave the transform's inverse ambiguous.
|
||||
///
|
||||
/// A log axis needs strictly positive data, and this is where a log axis could
|
||||
/// otherwise be left broken: an all-negative union has no positive value to show,
|
||||
/// and padding it would produce `min > max` once the lower edge was clamped to a
|
||||
/// positive bound. Instead the positive part of the union is used -- `[hi*1e-3,
|
||||
/// hi]` when the union straddles or starts below zero, since zero has no place on
|
||||
/// a log axis -- the pad is multiplicative (2% of the span in log space), and a
|
||||
/// union with no positive value at all is refused outright. The caller keeps its
|
||||
/// range and is free to retry, which is what lets a stream that starts at zero
|
||||
/// begin fitting by itself once it goes positive.
|
||||
bool fit_axis_range(Axis& axis, Range bounds) {
|
||||
const f64 lo = std::min(bounds.min, bounds.max);
|
||||
const f64 hi = std::max(bounds.min, bounds.max);
|
||||
if (!std::isfinite(lo) || !std::isfinite(hi)) return false;
|
||||
|
||||
if (log && lo > 0.0) {
|
||||
if (hi > lo) {
|
||||
const f64 factor = std::pow(hi / lo, 0.02); // 2% of the span, in log space
|
||||
return Range{lo / factor, hi * factor};
|
||||
if (axis.log) {
|
||||
if (!(hi > 0.0)) return false; // no positive sample: not auto-fittable
|
||||
const f64 lower = lo > 0.0 ? lo : hi * 1e-3;
|
||||
if (!(lower > 0.0) || lower > hi) return false;
|
||||
if (lower == hi) {
|
||||
axis.range = Range{lower / 10.0, hi * 10.0}; // one decade either side
|
||||
return true;
|
||||
}
|
||||
return Range{lo / 10.0, hi * 10.0}; // one decade either side
|
||||
const f64 factor = std::pow(hi / lower, 0.02); // 2% of the span, in log space
|
||||
axis.range = Range{lower / factor, hi * factor};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hi - lo > 0.0) {
|
||||
if (hi > lo) {
|
||||
const f64 pad = (hi - lo) * 0.02;
|
||||
return Range{lo - pad, hi + pad};
|
||||
axis.range = Range{lo - pad, hi + pad};
|
||||
return true;
|
||||
}
|
||||
const f64 pad = std::fabs(lo) * 1e-3 + 0.5;
|
||||
return Range{lo - pad, hi + pad};
|
||||
axis.range = Range{lo - pad, hi + pad};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -170,52 +190,33 @@ Transform Plot::y_transform(u32 axis) const {
|
||||
static_cast<f64>(r.y), false, a.log);
|
||||
}
|
||||
|
||||
bool Plot::fit_x_axis(u32 axis) {
|
||||
Range union_range{};
|
||||
bool Plot::trace_bounds_union(bool x_side, u32 axis, Range& out) const {
|
||||
bool any = false;
|
||||
for (const Trace& t : traces_) {
|
||||
if (t.axis_x != axis || !t.visible()) continue; // null or empty series: skip
|
||||
const Range bounds = t.series->x_bounds();
|
||||
if ((x_side ? t.axis_x : t.axis_y) != axis || !t.visible()) continue; // null series: skip
|
||||
const Range bounds = x_side ? t.series->x_bounds() : t.series->y_bounds();
|
||||
if (!std::isfinite(bounds.min) || !std::isfinite(bounds.max)) continue;
|
||||
if (!any) {
|
||||
union_range = bounds;
|
||||
out = bounds;
|
||||
any = true;
|
||||
} else {
|
||||
union_range.min = std::min(union_range.min, bounds.min);
|
||||
union_range.max = std::max(union_range.max, bounds.max);
|
||||
out.min = std::min(out.min, bounds.min);
|
||||
out.max = std::max(out.max, bounds.max);
|
||||
}
|
||||
}
|
||||
if (!any) return false; // nothing bound to this axis: leave the caller's range
|
||||
return any;
|
||||
}
|
||||
|
||||
Axis& a = x_axes_[axis];
|
||||
Range fitted = fit_range(union_range, a.log);
|
||||
if (a.log && fitted.min <= 0.0) fitted.min = fitted.max > 0.0 ? fitted.max * 1e-3 : 1.0;
|
||||
a.range = fitted;
|
||||
return true;
|
||||
bool Plot::fit_x_axis(u32 axis) {
|
||||
Range union_range{};
|
||||
if (!trace_bounds_union(/*x_side=*/true, axis, union_range)) return false;
|
||||
return fit_axis_range(x_axes_[axis], union_range);
|
||||
}
|
||||
|
||||
bool Plot::fit_y_axis(u32 axis) {
|
||||
Range union_range{};
|
||||
bool any = false;
|
||||
for (const Trace& t : traces_) {
|
||||
if (t.axis_y != axis || !t.visible()) continue;
|
||||
const Range bounds = t.series->y_bounds();
|
||||
if (!std::isfinite(bounds.min) || !std::isfinite(bounds.max)) continue;
|
||||
if (!any) {
|
||||
union_range = bounds;
|
||||
any = true;
|
||||
} else {
|
||||
union_range.min = std::min(union_range.min, bounds.min);
|
||||
union_range.max = std::max(union_range.max, bounds.max);
|
||||
}
|
||||
}
|
||||
if (!any) return false;
|
||||
|
||||
Axis& a = y_axes_[axis];
|
||||
Range fitted = fit_range(union_range, a.log);
|
||||
if (a.log && fitted.min <= 0.0) fitted.min = fitted.max > 0.0 ? fitted.max * 1e-3 : 1.0;
|
||||
a.range = fitted;
|
||||
return true;
|
||||
if (!trace_bounds_union(/*x_side=*/false, axis, union_range)) return false;
|
||||
return fit_axis_range(y_axes_[axis], union_range);
|
||||
}
|
||||
|
||||
void Plot::auto_fit_x() {
|
||||
|
||||
Reference in new issue
Block a user