feat(plot): an x axis caption drawn as chrome, not as an annotation

A host that re-anchors its x labels on a grid has to name the anchor somewhere, and
an `Annotation` cannot serve: its anchor is a data coordinate, so the caption crawls
across the plot under a pan and drifts under a zoom.

`Plot::set_x_note()` takes the host's own string -- the host is the only one that
knows how much precision the sum of a label and its caption needs -- and
`Renderer::draw_x_note()` places it in the axes' bottom-left corner in pixels, on the
legend's own backplate. It is measured in the renderer rather than in `Plot::layout()`
because it reserves no space and nothing can be picked on it, and a caption explaining
the x labels must not be able to move the layout those labels were measured into.

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:16 +02:00
1 parent 03c41adbec
commit 59c2e45af7
5 files changed
+150

No files matched your search

+14
View File
@@ -508,6 +508,18 @@ class Plot {
void set_x_axis_origin_from_range(u32 axis, bool on);
void set_y_axis_origin_from_range(u32 axis, bool on);
/// A one-line caption for the x axis, drawn as **chrome** in the bottom-left corner of the
/// axes with the legend's own backplate behind it. It exists for the thing a shifted origin
/// leaves unsaid: labels counted from a window's left edge are only readable beside the value
/// that edge has, and the reader adds the two. An `Annotation` cannot serve -- its anchor is a
/// data coordinate, so the caption crawls across the plot under a pan and drifts under a zoom,
/// which is the one thing a legend-like panel must not do. The text is the host's, because only
/// the host knows how much precision the sum needs: the axis' own label form carries three
/// significant figures, which is exactly what made the shifted origin necessary.
/// Empty (the default) draws nothing.
void set_x_note(std::string text);
const std::string& x_note() const noexcept { return x_note_; }
/// The measurement for `id`. Re-evaluated only when a cursor moved, when the
/// measure was added, or when the series underneath it changed: a different
/// series, a longer one, or a sliding window whose sample count stayed the same
@@ -848,6 +860,8 @@ class Plot {
render::Style style_{};
const render::TextBackend* text_ = nullptr;
Legend legend_{};
/// The x axis' corner caption; see set_x_note(). Empty draws nothing.
std::string x_note_;
/// Which parts of the bottom x chrome are reserved at all; see set_x_strip_mode().
XStrip x_strip_ = XStrip::Both;
/// The floor a figure imposes on the left gutter; see set_min_y_gutter(). 0.0 means