fix: the t0 caption is chrome anchored to the axis, not an annotation in the data
The caption that names the relative x labels' anchor was an `Annotation`, whose anchor is a data coordinate -- so it crawled across the plot under a pan and drifted under a zoom, which is how it was reported. It is now `Plot::set_x_note()`: the bottom row's caption, drawn by the renderer in the axes' bottom-left corner in pixels, on the legend's own backplate. The text stays the host's, because only the host knows that label + caption has to be the exact absolute time (`%.6g`, where `format_time()` would round to three figures). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2985aa159
commit
39f30d913f
6 files changed
+41
-39
No files matched your search
@@ -1437,6 +1437,21 @@ far the largest file.
|
|||||||
is exactly enough), so the bare comparison picked a different form from one frame to the next.
|
is exactly enough), so the bare comparison picked a different form from one frame to the next.
|
||||||
The guard is `[timelabels-probe]`'s `distinct=1`, which asserts what the form is *for* rather
|
The guard is `[timelabels-probe]`'s `distinct=1`, which asserts what the form is *for* rather
|
||||||
than which form a given window gets.
|
than which form a given window gets.
|
||||||
|
- **The `t0 = …` caption is chrome, never an annotation.** It is `Plot::set_x_note()` (added to
|
||||||
|
`deps/plotter` for this), drawn by `Renderer::draw_x_note()` on the legend's own
|
||||||
|
`draw_readout_panel()` backplate in the bottom-left corner of the axes, in **pixels**. It was an
|
||||||
|
`Annotation` first, and an annotation's anchor is a *data* coordinate: the caption therefore
|
||||||
|
crawled across the plot under a pan and drifted under a zoom, which is what "[t0=...s] move
|
||||||
|
slightly around when user zoom/pan" was. Three things follow. The **text is the host's**, because
|
||||||
|
the caption has to name the anchor precisely enough that label + caption is the exact absolute
|
||||||
|
time and `format_time()` would round it to three figures (`"t0 = %.6g s"`); only the **bottom
|
||||||
|
row** of the figure carries one, every other row being set to `""`; and the renderer measures it
|
||||||
|
rather than `Plot::layout()` — it reserves no space and nothing can be picked on it, so there is
|
||||||
|
no rect to read back, and a caption explaining the x labels must not be able to move the layout
|
||||||
|
those labels were measured into. The guards are the library's
|
||||||
|
`the_x_note_is_chrome_that_a_pan_or_a_zoom_does_not_move` (the footprint is the pixels a frame
|
||||||
|
with the caption differs from one without, and a pan plus a 64× zoom must give the same box to
|
||||||
|
the pixel) and `[timelabels-probe]`'s `note=`, which reads `Plot::x_note()` back.
|
||||||
- **`fit_x()` restores the *whole of what the view may show*, which is not always `window_s_`.**
|
- **`fit_x()` restores the *whole of what the view may show*, which is not always `window_s_`.**
|
||||||
Live, that is the newest `window_s_` at the live edge (`set_time_window(window_s_)`, the same
|
Live, that is the newest `window_s_` at the live edge (`set_time_window(window_s_)`, the same
|
||||||
path a resume takes). Pinned — a held event, a filling capture or a paused view — it is the
|
path a resume takes). Pinned — a held event, a filling capture or a paused view — it is the
|
||||||
|
|||||||
@@ -95,9 +95,15 @@ feedback loop), the magnitude is the larger of the two endpoints, and the compar
|
|||||||
1.05 band leaving the relative form plus a ULP of slack entering it — a step *equal* to the
|
1.05 band leaving the relative form plus a ULP of slack entering it — a step *equal* to the
|
||||||
resolution is readable (`1.73s`, `1.74s`, `1.75s`: one in the last digit is exactly enough), so a
|
resolution is readable (`1.73s`, `1.74s`, `1.75s`: one in the last digit is exactly enough), so a
|
||||||
decision taken on the last bits of two differently computed doubles picked a different form from
|
decision taken on the last bits of two differently computed doubles picked a different form from
|
||||||
one frame to the next. The caption is an annotation
|
one frame to the next. The caption is **chrome** — `Plot::set_x_note()`, added to the plotter for
|
||||||
inside the axes rather than the axis title on purpose: its text changes with the window, and a
|
it, drawn on the legend's own backplate in the bottom-left corner of the axes — rather than the
|
||||||
title of a changing width would relayout the plot every frame. Everything a user copies out —
|
axis title or an annotation. Not the title, because its text changes with the window and a title
|
||||||
|
of a changing width would relayout the plot every frame; not an annotation, because an
|
||||||
|
annotation's anchor is a *data* coordinate, so the caption crawled across the plot under a pan and
|
||||||
|
drifted under a zoom, which is what it was reported as. The library test is
|
||||||
|
`the_x_note_is_chrome_that_a_pan_or_a_zoom_does_not_move`: the footprint is measured as the pixels
|
||||||
|
a frame with the caption differs from the same frame without one, and a pan to another decade plus
|
||||||
|
a 64× zoom must produce the same box to the pixel. Everything a user copies out —
|
||||||
the cursors' `T0`/`T1`/`dT`, a measurement, an export — stays in absolute stream seconds.
|
the cursors' `T0`/`T1`/`dT`, a measurement, an export — stays in absolute stream seconds.
|
||||||
|
|
||||||
The anchor is taken by the *axis* on every layout (`Axis::origin_follows_range`), not written
|
The anchor is taken by the *axis* on every layout (`Axis::origin_follows_range`), not written
|
||||||
|
|||||||
Vendored
+1
-1
Submodule deps/plotter updated: 03c41adbec...59c2e45af7.
+11
-20
@@ -30,8 +30,6 @@ constexpr plot::u32 kRulerBColor = 0x8FA8C8u;
|
|||||||
/// The measurements' own colour, distinct from both cursor rulers so a readout mark cannot be
|
/// The measurements' own colour, distinct from both cursor rulers so a readout mark cannot be
|
||||||
/// mistaken for something the user placed.
|
/// mistaken for something the user placed.
|
||||||
constexpr plot::u32 kMeasureColor = 0x9BE8A0u;
|
constexpr plot::u32 kMeasureColor = 0x9BE8A0u;
|
||||||
/// The window-start caption's colour: quiet chrome, brighter than a grid line.
|
|
||||||
constexpr plot::u32 kTimeNoteColor = 0xC8C8C8u;
|
|
||||||
constexpr plot::u32 kTriggerColor = 0xFF5B5Bu;
|
constexpr plot::u32 kTriggerColor = 0xFF5B5Bu;
|
||||||
|
|
||||||
/// The smallest difference a `TickFormat::Time` label can *write* at the magnitude `t` -- what
|
/// The smallest difference a `TickFormat::Time` label can *write* at the magnitude `t` -- what
|
||||||
@@ -170,11 +168,6 @@ void RowSet::install(Row& r, int /*row_index*/, plot::Plot& p) {
|
|||||||
r.plot->set_cursor_stroke(r.measure_v[mi], kMeasureStrokeWidth,
|
r.plot->set_cursor_stroke(r.measure_v[mi], kMeasureStrokeWidth,
|
||||||
plot::Vec2{kMeasureDashOn, kMeasureDashOff});
|
plot::Vec2{kMeasureDashOn, kMeasureDashOff});
|
||||||
}
|
}
|
||||||
// The window-start caption for the relative x labels: an annotation, hidden until the
|
|
||||||
// labels are relative. Inside the axes on purpose -- its text changes with the window, and
|
|
||||||
// an axis title of a changing width would relayout the plot every frame.
|
|
||||||
r.time_origin_note = r.plot->add_annotation(
|
|
||||||
plot::Annotation{0.0, 0.0, "", plot::Color::hex(kTimeNoteColor), r.axis_x, r.axis_y, false});
|
|
||||||
r.trigger_marker = r.plot->add_cursor(kParked, plot::Color::hex(kTriggerColor));
|
r.trigger_marker = r.plot->add_cursor(kParked, plot::Color::hex(kTriggerColor));
|
||||||
// A marker, not an instrument: it must sit exactly on the edge's time, so its
|
// A marker, not an instrument: it must sit exactly on the edge's time, so its
|
||||||
// snap is Free and the controller never rounds it onto a sample.
|
// snap is Free and the controller never rounds it onto a sample.
|
||||||
@@ -581,17 +574,12 @@ bool RowSet::update_time_labels() {
|
|||||||
r.plot->set_x_axis_origin_from_range(r.axis_x, relative_x_);
|
r.plot->set_x_axis_origin_from_range(r.axis_x, relative_x_);
|
||||||
if (!relative_x_) r.plot->set_x_axis_origin(r.axis_x, 0.0);
|
if (!relative_x_) r.plot->set_x_axis_origin(r.axis_x, 0.0);
|
||||||
// The caption belongs with the labels, which a figure draws once, on the bottom row (the
|
// The caption belongs with the labels, which a figure draws once, on the bottom row (the
|
||||||
// one whose x axis is the figure's). Every row gets it parked.
|
// one whose x axis is the figure's). Every other row gets an empty one, which draws nothing.
|
||||||
const bool bottom = i == rows_ - 1;
|
const bool bottom = i == rows_ - 1;
|
||||||
if (r.time_origin_note >= r.plot->annotation_count()) continue;
|
if (!relative_x_ || !bottom) {
|
||||||
plot::Annotation note = r.plot->annotation(r.time_origin_note);
|
r.plot->set_x_note("");
|
||||||
note.visible = relative_x_ && bottom;
|
continue;
|
||||||
if (note.visible) {
|
}
|
||||||
// Inside the axes, just above the axis line, at their left edge: the reader adds this
|
|
||||||
// to a tick, so it sits beside the ticks.
|
|
||||||
const plot::Range yr = r.plot->y_range(r.axis_y);
|
|
||||||
note.x = view.min;
|
|
||||||
note.y = yr.min + 0.04 * (yr.span() > 0.0 ? yr.span() : 1.0);
|
|
||||||
char text[64];
|
char text[64];
|
||||||
// The anchor the *drawn* labels were built with: the axis' own origin, which the last
|
// The anchor the *drawn* labels were built with: the axis' own origin, which the last
|
||||||
// layout set from its range. Reading it back (rather than keeping our own copy) is what
|
// layout set from its range. Reading it back (rather than keeping our own copy) is what
|
||||||
@@ -599,9 +587,12 @@ bool RowSet::update_time_labels() {
|
|||||||
const double shown = r.plot->x_axis_origin(r.axis_x);
|
const double shown = r.plot->x_axis_origin(r.axis_x);
|
||||||
x_origin_ = shown;
|
x_origin_ = shown;
|
||||||
std::snprintf(text, sizeof(text), "t0 = %.6g s", shown);
|
std::snprintf(text, sizeof(text), "t0 = %.6g s", shown);
|
||||||
note.text = text;
|
// Chrome, not an annotation: an annotation is anchored to a *data* coordinate, so the caption
|
||||||
}
|
// crawled across the plot as the view panned and drifted as it zoomed -- and it came with the
|
||||||
r.plot->set_annotation(r.time_origin_note, note);
|
// annotation's own pin cross, which pointed at nothing. `set_x_note()` places it in the axes'
|
||||||
|
// bottom-left corner in pixels, on the legend's backplate, which is where the labels it
|
||||||
|
// qualifies are.
|
||||||
|
r.plot->set_x_note(text);
|
||||||
}
|
}
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,11 +106,6 @@ class RowSet {
|
|||||||
/// it keeps its position whatever the cursor toggle says.
|
/// it keeps its position whatever the cursor toggle says.
|
||||||
plot::u32 trigger_marker = 0;
|
plot::u32 trigger_marker = 0;
|
||||||
|
|
||||||
/// The caption naming the window's start while the x labels are relative: an annotation
|
|
||||||
/// inside the axes, so its text can change every frame without moving the layout (an axis
|
|
||||||
/// *title* would, and it would be a different width each time).
|
|
||||||
plot::u32 time_origin_note = 0;
|
|
||||||
|
|
||||||
/// The measurements' readout marks: a pool created with the plot (a `Plot` cannot add or
|
/// The measurements' readout marks: a pool created with the plot (a `Plot` cannot add or
|
||||||
/// drop a cursor after the fact) and parked unless a measurement is using the slot.
|
/// drop a cursor after the fact) and parked unless a measurement is using the slot.
|
||||||
std::array<plot::u32, kMeasureMarks> measure_h{};
|
std::array<plot::u32, kMeasureMarks> measure_h{};
|
||||||
|
|||||||
+5
-10
@@ -7264,10 +7264,7 @@ gboolean timelabels_probe_step(gpointer user) {
|
|||||||
ticks += t.label;
|
ticks += t.label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (last.time_origin_note < last.plot->annotation_count()) {
|
note = last.plot->x_note();
|
||||||
const plot::Annotation& an = last.plot->annotation(last.time_origin_note);
|
|
||||||
if (an.visible) note = an.text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
double first_value = 0.0;
|
double first_value = 0.0;
|
||||||
plot::Range shown{};
|
plot::Range shown{};
|
||||||
@@ -8317,14 +8314,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
// The x label form: relative to the window's own start (with the caption that names
|
// The x label form: relative to the window's own start (with the caption that names
|
||||||
// that start) while the span is sub-second, absolute otherwise. `x_origin_set` is read
|
// that start) when the labels could not carry the tick step absolutely, absolute
|
||||||
// back out of the plot, so this is what the axis is actually formatting with.
|
// otherwise. `x_origin_set` is read back out of the plot, so this is what the axis is
|
||||||
|
// actually formatting with, and `note` is the plot's own caption string.
|
||||||
const RowSet::Row& rlt = rows.row(rows.row_count() - 1);
|
const RowSet::Row& rlt = rows.row(rows.row_count() - 1);
|
||||||
std::string note;
|
std::string note;
|
||||||
if (rlt.plot != nullptr && rlt.time_origin_note < rlt.plot->annotation_count()) {
|
if (rlt.plot != nullptr) note = rlt.plot->x_note();
|
||||||
const plot::Annotation& a = rlt.plot->annotation(rlt.time_origin_note);
|
|
||||||
if (a.visible) note = a.text;
|
|
||||||
}
|
|
||||||
// The labels themselves, from the axis the reader looks at: the assertion is what the
|
// The labels themselves, from the axis the reader looks at: the assertion is what the
|
||||||
// ticks *say*, not only which mode is on.
|
// ticks *say*, not only which mode is on.
|
||||||
std::string ticks;
|
std::string ticks;
|
||||||
|
|||||||
Reference in new issue
Block a user