fix(render): floor the axes so chrome cannot erase it, one title row per titled axis

Two findings on the pass-1 fix:

- The reserved strips were clamped only to the viewport, so a title wider than a
  narrow panel took the rest of it and the axes collapsed: draw_traces() returned
  on a zero-width rect (no trace drawn, nothing reported) and a zero-height one
  flattened every trace onto a single row. The chrome is now capped at half the
  viewport -- the label strip is served first, the title strip takes what is left
  of that allowance -- an over-long title is clipped to its strip by a walk that
  both measures and stages it, and draw_traces() tests the height as well as the
  width.
- Every titled y axis was staged at the same centred position inside one strip, so
  a Left/Right pair of titles overprinted. Each titled axis now owns a text line:
  the x strip grows one line per titled x axis (x_title_height), and the y runs
  stack down the axes-tall strip, which needs no extra width for it.

Tests: `narrow_panel_with_a_long_y_title_still_draws` and
`short_panel_with_an_x_title_still_draws` pin the floor (the axes keeps half the
panel and the trace still covers the columns, with the title clipped into its
strip), `titled_axis_pairs_get_their_own_rows` pins one text row per titled axis
in both directions, and `layout_stacks_one_title_line_per_titled_axis` pins the
strip sizing. All fail against the pass-1 sources.
This commit is contained in:
Martino Ferrari committed 2026-09-27 23:33:06 +02:00
1 parent 8f45108559
commit 1f5aa11de2
6 files changed
+330 -51

No files matched your search

+23 -10
View File
@@ -36,10 +36,13 @@ struct PlotLayout {
/// Bottom strip height. The default is used when no text backend has been set.
f32 x_axis_height = 22.0f;
/// Width of `y_title_area` and height of `x_title_area`, both 0.0f when nothing
/// is reserved. An x title is one text line in a strip as wide as the axes; a y
/// title is a horizontal run in a vertical strip, so the strip is as wide as the
/// widest y title (plus the label padding) -- one text line would cut it off.
/// With no text backend there is no title to measure and both stay 0.0f.
/// is reserved. Every titled axis of a direction gets its own text line inside
/// its strip -- which is what keeps a Left/Right (or Bottom/Top) pair from
/// drawing two titles over each other -- so `x_title_height` is one line per
/// titled x axis; `y_title_width` is the widest y title plus the padding, since
/// y titles are horizontal runs that stack down the axes-tall strip and need
/// width, not another row of it. With no text backend there is no title to
/// measure and both stay 0.0f.
f32 y_title_width = 0.0f;
f32 x_title_height = 0.0f;
};
@@ -94,6 +97,11 @@ class Plot {
u32 x_axis_count() const noexcept { return static_cast<u32>(x_axes_.size()); }
u32 y_axis_count() const noexcept { return static_cast<u32>(y_axes_.size()); }
/// Axes of that direction that carry a title, i.e. that `layout()` reserves a
/// text line for and `Renderer::draw_titles()` draws one for. Zero when no
/// backend can measure a title, whatever the labels say.
u32 titled_x_axis_count() const noexcept;
u32 titled_y_axis_count() const noexcept;
u32 trace_count() const noexcept { return static_cast<u32>(traces_.size()); }
u32 cursor_count() const noexcept { return static_cast<u32>(cursors_.size()); }
u32 measure_count() const noexcept { return static_cast<u32>(measures_.size()); }
@@ -175,8 +183,17 @@ class Plot {
/// `axes` is inset by the label strip of each occupied edge and, when that
/// direction has a titled axis, by the strip that title lives in (`y_title_width`
/// and `x_title_height` say how much that is): a title can then only be drawn
/// outside the data, whatever the title says. Call this once per frame -- it
/// rebuilds the ticks and the grid -- and pass the result down.
/// outside the data, whatever the title says.
///
/// The chrome is capped so `axes` keeps half the viewport on each dimension
/// whatever the labels and titles ask for: a plot with a title wider than a
/// narrow panel shrinks, it does not vanish -- an axes rect with no width or no
/// height selects no level, stages no segment and draws no trace, and says
/// nothing about why. The label strip is what makes the tick values readable, so
/// it is served first and the title strip takes what is left of the allowance.
///
/// Call this once per frame -- it rebuilds the ticks and the grid -- and pass
/// the result down.
const PlotLayout& layout();
/// The layout as of the last refresh. The const overload never recomputes, so
/// a lookup on a const Plot cannot move the axes out from under the caller.
@@ -219,10 +236,6 @@ class Plot {
/// Widest y tick label plus the label padding on both sides: the left strip the
/// text backend asks for. Zero with no backend or no y axis.
f32 y_label_width() const;
/// True when any axis of that direction carries a title, i.e. when layout()
/// must reserve the strip `Renderer::draw_titles()` draws it in.
bool has_x_title() const noexcept;
bool has_y_title() const noexcept;
/// Widest y axis title plus the label padding on both sides: the vertical strip
/// a horizontal y title fits in. Zero with no backend or no titled y axis.
f32 y_title_strip_width() const;