fix: the x labels go relative on what a label can write, not on the span
A `t0 = ...` caption appeared on every sub-second window, including a triggered one whose axis is already relative to the edge -- so the ticks were re-anchored away from the one position a triggered axis exists to show, and the caption had nothing to explain. The form is now decided by the tick step against `TickFormat::Time`'s own resolution at the axis' magnitude (three significant figures below a minute, whole seconds through `m:ss` to a day, three figures of days above). The span said nothing about that and was wrong both ways: a 2 s window at twenty minutes of uptime reads `20:00` on every tick and stayed absolute. The step is read back from the last layout's majors (a tick value depends on the range and the axis width, not on the label form); with no ticks yet the form is left alone rather than guessed, since the hysteresis would latch a guess. Entering the relative form carries a ULP of slack, because `step == res` is readable and a bare comparison of two differently computed doubles flipped the form between frames. `[timelabels-probe]` and the dump gain `distinct=`, which asserts that no two major labels read the same -- the thing the relative form is for, and the only reading that holds whatever the stream's uptime is. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
b505706d92
commit
e2985aa159
4 files changed
+145
-32
No files matched your search
+57
-6
@@ -34,6 +34,21 @@ constexpr plot::u32 kMeasureColor = 0x9BE8A0u;
|
||||
constexpr plot::u32 kTimeNoteColor = 0xC8C8C8u;
|
||||
constexpr plot::u32 kTriggerColor = 0xFF5B5Bu;
|
||||
|
||||
/// The smallest difference a `TickFormat::Time` label can *write* at the magnitude `t` -- what
|
||||
/// the last digit of a label at that magnitude is worth. `format_time` writes three significant
|
||||
/// figures below a minute (of the SI-prefixed mantissa under a second, of the seconds above it),
|
||||
/// whole seconds through `m:ss`/`h:mm:ss` up to a day, and three figures of days above. So the
|
||||
/// resolution is a step function of the *magnitude* and not of the span -- which is the whole
|
||||
/// reason a relative axis is ever needed, and the reason a span threshold alone gets it wrong in
|
||||
/// both directions.
|
||||
double time_label_resolution(double t) {
|
||||
t = std::fabs(t);
|
||||
if (!std::isfinite(t) || !(t > 0.0)) return 0.0;
|
||||
if (t >= 86400.0) return 86400.0 * std::pow(10.0, std::floor(std::log10(t / 86400.0)) - 2.0);
|
||||
if (t >= 60.0) return 1.0;
|
||||
return std::pow(10.0, std::floor(std::log10(t)) - 2.0);
|
||||
}
|
||||
|
||||
constexpr double kParked = -1.0e12; ///< hides an instrument without removing it
|
||||
/// The measurement marks' stroke: thinner than the chrome that frames the plot (a trace is 1.5,
|
||||
/// the axis lines and the trigger marker `Style::line_width_px`), and broken, so a readout over
|
||||
@@ -509,12 +524,48 @@ bool RowSet::update_time_labels() {
|
||||
const double span = view.span();
|
||||
if (!(span > 0.0) || !std::isfinite(span)) return false;
|
||||
|
||||
// Sub-second windows read relative: `TickFormat::Time` writes three significant digits, so
|
||||
// `12.345671s` and `12.345672s` are the same string and a 1 ms step around a multi-second
|
||||
// time cannot be labelled at all. The band is the switch's hysteresis -- entering below a
|
||||
// second, leaving only above 1.05 -- so a span sitting on the boundary cannot flicker
|
||||
// between two forms while a zoom is nudged.
|
||||
const bool want_relative = relative_x_ ? span < 1.05 : span < 1.0;
|
||||
// A relative axis is the answer to one problem only: a tick step the label form cannot write.
|
||||
// `TickFormat::Time` carries three significant digits, so a 1 ms step around a 12 s stamp
|
||||
// writes `12.3s` on every tick -- there the ticks have to be re-anchored and the anchor named.
|
||||
// The test is therefore the *step against the resolution at the axis' own magnitude*, never the
|
||||
// span: the two part company in both directions, and both cases are real here. A 2 s window at
|
||||
// twenty minutes of uptime is labelled `20:00` throughout and needs the anchor although its
|
||||
// span is wide; a 50 ms trigger window spanning [-25ms, +25ms] is labelled `-20ms .. +20ms`
|
||||
// perfectly well and needs nothing, although its span is narrow. That second case is what made
|
||||
// a triggered sub-second view carry a `t0` caption *and* re-anchor its ticks away from the
|
||||
// edge, which is the one position a triggered axis exists to show.
|
||||
// The 1.05 band is the switch's hysteresis, so a step sitting on the boundary cannot flicker
|
||||
// between two forms while a zoom is nudged, and the `1 - 1e-9` is a slack the *entry* needs for
|
||||
// its own reason: a step read back from two tick values and a resolution from `pow(10, n)`
|
||||
// differ in their last bits even when they are the same number, and `step == res` is a readable
|
||||
// axis (`1.73s, 1.74s, 1.75s` -- one in the last digit is exactly enough), so a decision taken
|
||||
// on those bits picked a different form from one frame to the next.
|
||||
const double mag = std::max(std::fabs(view.min), std::fabs(view.max));
|
||||
const double res = time_label_resolution(mag);
|
||||
// The step the labels are actually written at: the majors the *last* layout produced. They are
|
||||
// a function of the range and the axis' pixel width, not of the label form, so reading them
|
||||
// back here is no feedback loop.
|
||||
double step = 0.0;
|
||||
{
|
||||
const plot::Axis& ax = first.plot->x_axis(first.axis_x);
|
||||
double prev = 0.0;
|
||||
bool have = false;
|
||||
for (const plot::Tick& t : ax.ticks) {
|
||||
if (!t.major) continue;
|
||||
if (have) {
|
||||
step = t.value - prev;
|
||||
break;
|
||||
}
|
||||
prev = t.value;
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
// No ticks yet -- the frames before the first layout. The form is a property of the labels, so
|
||||
// with no labels there is nothing to decide, and keeping the current one is right where
|
||||
// guessing a step from the span is not: a guess that entered the relative form would be
|
||||
// *latched* there by the hysteresis above.
|
||||
const bool want_relative =
|
||||
step > 0.0 ? step < res * (relative_x_ ? 1.05 : 1.0 - 1.0e-9) : relative_x_;
|
||||
const bool changed = want_relative != relative_x_;
|
||||
relative_x_ = want_relative;
|
||||
|
||||
|
||||
+29
-13
@@ -7246,13 +7246,23 @@ gboolean timelabels_probe_step(gpointer user) {
|
||||
const RowSet::Row& last = a->rows->row(a->rows->row_count() - 1);
|
||||
std::string ticks;
|
||||
std::string note;
|
||||
// Whether *every* major label on the axis is a different string. That, and not a span
|
||||
// threshold, is what the relative form exists to buy: two adjacent ticks reading `12.3s` are
|
||||
// not labels. It is therefore the one assertion that holds whatever the stream's uptime is --
|
||||
// which decides where the switch actually falls, since the label resolution is a function of
|
||||
// the axis' magnitude.
|
||||
bool distinct = true;
|
||||
if (last.plot != nullptr) {
|
||||
int n = 0;
|
||||
std::vector<std::string> seen;
|
||||
for (const plot::Tick& t : last.plot->x_axis(last.axis_x).ticks) {
|
||||
if (!t.major) continue;
|
||||
if (n++ != 0) ticks += ",";
|
||||
ticks += t.label;
|
||||
if (n >= 4) break;
|
||||
if (std::find(seen.begin(), seen.end(), t.label) != seen.end()) distinct = false;
|
||||
seen.emplace_back(t.label);
|
||||
if (n++ < 4) {
|
||||
if (n != 1) ticks += ",";
|
||||
ticks += t.label;
|
||||
}
|
||||
}
|
||||
if (last.time_origin_note < last.plot->annotation_count()) {
|
||||
const plot::Annotation& an = last.plot->annotation(last.time_origin_note);
|
||||
@@ -7271,9 +7281,10 @@ gboolean timelabels_probe_step(gpointer user) {
|
||||
shown = last.plot->x_range(last.axis_x);
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[timelabels-probe] %-12s relative=%d window=%.6g origin=%.6f note=[%s] "
|
||||
"ticks=[%s] first_value=%.6f first_rel=%.6f range=[%.6f..%.6f]\n",
|
||||
what, a->rows->time_labels_relative() ? 1 : 0, a->rows->time_window(),
|
||||
"[timelabels-probe] %-12s relative=%d distinct=%d window=%.6g origin=%.6f "
|
||||
"note=[%s] ticks=[%s] first_value=%.6f first_rel=%.6f range=[%.6f..%.6f]\n",
|
||||
what, a->rows->time_labels_relative() ? 1 : 0, distinct ? 1 : 0,
|
||||
a->rows->time_window(),
|
||||
a->rows->time_labels_origin(), note.c_str(), ticks.c_str(), first_value,
|
||||
first_value - (last.plot != nullptr ? last.plot->x_axis_origin(last.axis_x) : 0.0),
|
||||
shown.min, shown.max);
|
||||
@@ -8317,13 +8328,18 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
// The labels themselves, from the axis the reader looks at: the assertion is what the
|
||||
// ticks *say*, not only which mode is on.
|
||||
std::string ticks;
|
||||
bool distinct = true;
|
||||
if (rlt.plot != nullptr) {
|
||||
int shown = 0;
|
||||
std::vector<std::string> seen;
|
||||
for (const plot::Tick& t : rlt.plot->x_axis(rlt.axis_x).ticks) {
|
||||
if (!t.major) continue;
|
||||
if (shown++ != 0) ticks += ",";
|
||||
ticks += t.label;
|
||||
if (shown >= 4) break;
|
||||
if (std::find(seen.begin(), seen.end(), t.label) != seen.end()) distinct = false;
|
||||
seen.emplace_back(t.label);
|
||||
if (shown++ < 4) {
|
||||
if (shown != 1) ticks += ",";
|
||||
ticks += t.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The labels against the anchor they were built with: the dump runs after the last
|
||||
@@ -8340,10 +8356,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf("[udpscope] time labels: relative=%d origin=%.6f note=[%s] x_origin_set=%.6f "
|
||||
"ticks=[%s] first_rel=%.9f\n",
|
||||
rows.time_labels_relative() ? 1 : 0, rows.time_labels_origin(), note.c_str(),
|
||||
origin_set, ticks.c_str(), first_value - origin_set);
|
||||
std::printf("[udpscope] time labels: relative=%d distinct=%d origin=%.6f note=[%s] "
|
||||
"x_origin_set=%.6f ticks=[%s] first_rel=%.9f\n",
|
||||
rows.time_labels_relative() ? 1 : 0, distinct ? 1 : 0, rows.time_labels_origin(),
|
||||
note.c_str(), origin_set, ticks.c_str(), first_value - origin_set);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(model.mutex());
|
||||
|
||||
Reference in new issue
Block a user