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:
Martino FerrariandClaude Opus 4.6 committed 2026-10-05 18:21:28 +02:00
1 parent e2985aa159
commit 39f30d913f
6 files changed
+47 -45

No files matched your search

+17 -26
View File
@@ -30,8 +30,6 @@ constexpr plot::u32 kRulerBColor = 0x8FA8C8u;
/// The measurements' own colour, distinct from both cursor rulers so a readout mark cannot be
/// mistaken for something the user placed.
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;
/// 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,
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));
// 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.
@@ -581,27 +574,25 @@ bool RowSet::update_time_labels() {
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);
// 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;
if (r.time_origin_note >= r.plot->annotation_count()) continue;
plot::Annotation note = r.plot->annotation(r.time_origin_note);
note.visible = relative_x_ && bottom;
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];
// 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
// makes the reader's addition exact -- label plus caption is that tick's absolute time.
const double shown = r.plot->x_axis_origin(r.axis_x);
x_origin_ = shown;
std::snprintf(text, sizeof(text), "t0 = %.6g s", shown);
note.text = text;
if (!relative_x_ || !bottom) {
r.plot->set_x_note("");
continue;
}
r.plot->set_annotation(r.time_origin_note, note);
char text[64];
// 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
// makes the reader's addition exact -- label plus caption is that tick's absolute time.
const double shown = r.plot->x_axis_origin(r.axis_x);
x_origin_ = shown;
std::snprintf(text, sizeof(text), "t0 = %.6g s", shown);
// 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
// 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;
}