feat(plot): vertical y-axis label via a rotated text run

This commit is contained in:
Martino Ferrari committed 2026-09-28 11:14:36 +02:00
1 parent 1cb5970757
commit daeb31b1ea
6 files changed
+453 -80

No files matched your search

+71 -10
View File
@@ -82,10 +82,12 @@ PLOT_TEST(layout_derives_label_areas_from_font_metrics) {
const f32 pad = p.style().grid_label_pad;
PLOT_CHECK_NEAR(l.y_axis_width, widest_major_label(p.y_axis(ay), font) + 2.0f * pad, 1e-4);
PLOT_CHECK_NEAR(l.x_axis_height, font.height() + 2.0f * pad, 1e-4);
// The y axis carries the title "volts": it is drawn horizontally, so the strip
// left of the label strip is as wide as the measured title. There is no x axis,
// so the x direction reserves nothing beyond its label strip.
PLOT_CHECK_NEAR(l.y_title_width, font.measure("volts") + 2.0f * pad, 1e-4);
// The y axis carries the title "volts", drawn rotated to read bottom-to-top, so
// the strip left of the label strip is one text line *wide* -- the column the
// run advances up -- and not as wide as the title is when horizontal. There is
// no x axis, so the x direction reserves nothing beyond its label strip.
PLOT_CHECK_NEAR(l.y_title_width, font.height() + 2.0f * pad, 1e-4);
PLOT_CHECK(l.y_title_width < font.measure("volts") + 2.0f * pad);
PLOT_CHECK_NEAR(l.x_title_height, 0.0f, 1e-6);
PLOT_CHECK_NEAR(l.axes.x, l.y_title_width + l.y_axis_width, 1e-4);
PLOT_CHECK_NEAR(l.axes.w, 800.0f - (l.y_title_width + l.y_axis_width), 1e-4);
@@ -100,8 +102,9 @@ PLOT_TEST(layout_derives_label_areas_from_font_metrics) {
PLOT_CHECK_NEAR(l.x_label_area.y, l.axes.bottom(), 1e-4);
}
/// Every titled axis of a direction gets its own text line inside its strip;
/// without that, the second titled axis of an edge is drawn on top of the first.
/// Every titled axis of a direction gets its own text line inside its strip; without
/// that, the second titled axis of an edge is drawn on top of the first. An x line is
/// a row of the strip, a y one a column of it, because the y title is drawn rotated.
PLOT_TEST(layout_stacks_one_title_line_per_titled_axis) {
plot::render::EmbeddedMonoFont font;
plot::Plot p;
@@ -117,10 +120,13 @@ PLOT_TEST(layout_stacks_one_title_line_per_titled_axis) {
PLOT_CHECK(p.titled_y_axis_count() == 2u);
// The x titles stack in rows, so the strip grows by a line per titled axis...
PLOT_CHECK_NEAR(l.x_title_height, 2.0f * font.height() + 2.0f * pad, 1e-4);
// ... while the y titles stack down the axes-tall strip, so the second one costs
// height the axes already has rather than a second column of width.
PLOT_CHECK_NEAR(l.y_title_width,
std::max(font.measure("volts"), font.measure("amps")) + 2.0f * pad, 1e-4);
// ... and the y titles, rotated, take a *column* each: the second titled y axis
// costs another text line of width rather than a row of the strip's height, and
// the pair still cannot be drawn over each other. The widest title's own width
// plays no part in it.
PLOT_CHECK_NEAR(l.y_title_width, 2.0f * font.height() + 2.0f * pad, 1e-4);
PLOT_CHECK(l.y_title_width <
std::max(font.measure("volts"), font.measure("amps")) + 2.0f * pad);
PLOT_CHECK_NEAR(l.axes.h, 400.0f - (l.x_axis_height + l.x_title_height), 1e-4);
}
@@ -143,6 +149,61 @@ PLOT_TEST(layout_reserves_no_title_strip_without_a_title) {
PLOT_CHECK_NEAR(l.axes.h, 400.0f - l.x_axis_height, 1e-4);
}
/// The y title is rotated, so its strip is a column: one text line wide where a
/// horizontal run needed the title's own width, and as tall as the axes, which is
/// where the run spends its length. The title here is wide enough to make the two
/// models plainly different -- it measures more than ten of the lines it now costs.
PLOT_TEST(y_title_strip_is_narrow_when_a_y_title_is_set) {
plot::render::EmbeddedMonoFont font;
plot::Plot p;
p.set_text_backend(&font);
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
p.add_x_axis({"seconds", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
p.add_y_axis({"millivolts per division", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
const plot::PlotLayout& l = p.layout();
const plot::f32 pad = p.style().grid_label_pad;
// One text line wide -- a column, not the run's own width...
PLOT_CHECK_NEAR(l.y_title_area.w, l.y_title_width, 1e-6);
PLOT_CHECK_NEAR(l.y_title_width, font.height() + 2.0f * pad, 1e-4);
PLOT_CHECK(l.y_title_width < font.measure("millivolts per division") + 2.0f * pad);
// ... spanning the axes' whole vertical extent, so the run has the room to
// advance in and a title longer than the axes is cut where the axes ends.
PLOT_CHECK_NEAR(l.y_title_area.x, 0.0f, 1e-6);
PLOT_CHECK_NEAR(l.y_title_area.y, l.axes.y, 1e-6);
PLOT_CHECK_NEAR(l.y_title_area.h, l.axes.h, 1e-4);
PLOT_CHECK(l.y_title_area.h > l.y_title_area.w);
// The strips tile the viewport outside `axes`: title, then labels, then data.
PLOT_CHECK_NEAR(l.y_title_area.right(), l.y_label_area.x, 1e-4);
PLOT_CHECK_NEAR(l.y_label_area.right(), l.axes.x, 1e-4);
PLOT_CHECK_NEAR(l.axes.x, l.y_title_width + l.y_axis_width, 1e-4);
PLOT_CHECK_NEAR(l.axes.w, 800.0f - (l.y_title_width + l.y_axis_width), 1e-4);
}
/// A Left and a Right titled y axis both draw in the one strip the layout reserves,
/// so the strip has to hold two runs. They no longer stack down its height -- the
/// runs are vertical, they would overlap from top to bottom -- so each gets a
/// column, and the strip is two text lines wide. What the model must not do is go
/// back to being as wide as the widest title: the columns are lines, not titles.
PLOT_TEST(two_titled_y_axes_reserve_a_column_each) {
plot::render::EmbeddedMonoFont font;
plot::Plot p;
p.set_text_backend(&font);
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
p.add_y_axis({"volts", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
p.add_y_axis({"amps", {-1.0, 1.0}, false, plot::AxisPlace::Right, false});
const plot::PlotLayout& l = p.layout();
const plot::f32 pad = p.style().grid_label_pad;
PLOT_CHECK(p.titled_y_axis_count() == 2u);
PLOT_CHECK_NEAR(l.y_title_width, 2.0f * font.height() + 2.0f * pad, 1e-4);
// Two columns fit side by side inside the strip, and the axes is inset by it once.
PLOT_CHECK(l.y_title_width >= 2.0f * font.height());
PLOT_CHECK_NEAR(l.y_title_area.w, l.y_title_width, 1e-6);
PLOT_CHECK_NEAR(l.y_title_area.right(), l.y_label_area.x, 1e-4);
PLOT_CHECK_NEAR(l.axes.x, l.y_title_width + l.y_axis_width, 1e-4);
}
PLOT_TEST(time_axis_tick_labels_use_format_time) {
plot::Plot p;
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});