feat(plot): vertical y-axis label via a rotated text run
This commit is contained in:
1 parent
1cb5970757
commit
daeb31b1ea
6 files changed
+453
-80
No files matched your search
+12
-9
@@ -336,9 +336,10 @@ const PlotLayout& Plot::layout() {
|
||||
// tick label can change that. Nothing at all is reserved without a text backend
|
||||
// -- there is then no title to measure, which is what keeps the two defaults
|
||||
// above meaningful for the no-text-backend case. The x strip is one text line
|
||||
// per titled x axis tall (they are stacked in it); the y strip is as wide as the
|
||||
// widest y title, since a y title is a horizontal run that stacks down the strip
|
||||
// and so needs width rather than another row of it (see y_title_strip_width).
|
||||
// per titled x axis tall (they are stacked in it); the y strip is one text line
|
||||
// per titled y axis *wide*, because the y title is drawn rotated and so spends
|
||||
// its length along the axes' height and takes a column, not a row, per titled
|
||||
// axis (see y_title_strip_width).
|
||||
const u32 x_titles = titled_x_axis_count();
|
||||
const f32 y_title_w = text_ != nullptr ? y_title_strip_width() : 0.0f;
|
||||
const f32 x_title_h = text_ != nullptr && x_titles != 0u
|
||||
@@ -437,13 +438,15 @@ u32 Plot::titled_y_axis_count() const noexcept {
|
||||
}
|
||||
|
||||
f32 Plot::y_title_strip_width() const {
|
||||
// One text line per titled y axis, not the widest title's own width: the title
|
||||
// is drawn rotated to read bottom-to-top, so it spends the axes' height and
|
||||
// each titled axis needs a column of its own -- which is what keeps a Left/Right
|
||||
// pair of titles from being drawn over each other now that they no longer stack
|
||||
// down a taller strip.
|
||||
if (text_ == nullptr) return 0.0f;
|
||||
f32 widest = 0.0f;
|
||||
for (const Axis& a : y_axes_) {
|
||||
if (a.label.empty()) continue;
|
||||
widest = std::max(widest, text_->measure(a.label.c_str()));
|
||||
}
|
||||
return widest > 0.0f ? widest + 2.0f * style_.grid_label_pad : 0.0f;
|
||||
const u32 titles = titled_y_axis_count();
|
||||
if (titles == 0u) return 0.0f;
|
||||
return static_cast<f32>(titles) * text_->height() + 2.0f * style_.grid_label_pad;
|
||||
}
|
||||
|
||||
f32 Plot::y_label_width() const {
|
||||
|
||||
+62
-37
@@ -51,19 +51,35 @@ u64 clamp_index(f64 v, u64 n) noexcept {
|
||||
constexpr usize kNumberChars = 24;
|
||||
constexpr usize kReadoutChars = 160;
|
||||
|
||||
/// Walks the longest prefix of `utf8` that fits `budget_px`, calling `emit(pen,
|
||||
/// glyph)` for every glyph it keeps -- `pen` in viewport pixels, `glyph` the
|
||||
/// backend's own entry for that codepoint -- and returning the width that prefix
|
||||
/// occupies. Both the measuring and the staging of a clipped run walk this, so the
|
||||
/// cut lands on the same pixel either way; the codepoints, the advances and the
|
||||
/// scale are the ones `TextBatch::add_text()` uses, so a clipped run still ends
|
||||
/// where the plot layer measured its title to end.
|
||||
/// The run bases the axis titles are drawn with. A title reads left to right in
|
||||
/// the x strip (`kTextRight`), and bottom-to-top in the y strip (`kTextUp`: a
|
||||
/// quarter turn anticlockwise on screen, where y grows downward). Both are unit
|
||||
/// vectors, which is what `walk_text_clipped()` spends its budget along and what
|
||||
/// `TextBatch` builds the glyph cell from (see `TextInstance`).
|
||||
constexpr Vec2 kTextRight{1.0f, 0.0f};
|
||||
constexpr Vec2 kTextUp{0.0f, -1.0f};
|
||||
|
||||
/// Walks the longest prefix of `utf8` that fits `budget_px` along `axis`, calling
|
||||
/// `emit(pen, glyph)` for every glyph it keeps -- `pen` in viewport pixels,
|
||||
/// `glyph` the backend's own entry for that codepoint -- and returning the length
|
||||
/// of that prefix in pixels. Both the measuring and the staging of a clipped run
|
||||
/// walk this, so the cut lands on the same pixel either way; the codepoints, the
|
||||
/// advances and the scale are the ones `TextBatch::add_text()` uses, so a clipped
|
||||
/// run still ends where the plot layer measured its title to end.
|
||||
///
|
||||
/// A codepoint the backend has no glyph for is skipped, exactly as `add_text`
|
||||
/// skips it, so what is measured here is what is drawn.
|
||||
///
|
||||
/// `axis` is the run's own direction in viewport pixels -- a unit vector, the same
|
||||
/// basis the cells are staged with (see `TextInstance`): the pen advances along it
|
||||
/// and the budget is spent along it too, so a run is clipped to the strip's extent
|
||||
/// *in its own direction* rather than to the strip's width. A horizontal x title
|
||||
/// asks for (1, 0) and spends the strip's width; the y title asks for (0,-1) and
|
||||
/// spends the strip's height, which is the only reason a rotated run can be cut at
|
||||
/// the strip's ends at all.
|
||||
template <class Emit>
|
||||
f32 walk_text_clipped(Vec2 pen, const char* utf8, f32 budget_px, const render::TextBackend& font,
|
||||
f32 scale, Emit emit) {
|
||||
f32 scale, Vec2 axis, Emit emit) {
|
||||
if (utf8 == nullptr || !(budget_px > 0.0f)) {
|
||||
return 0.0f; // no room at all: a strip clipped away stages nothing
|
||||
}
|
||||
@@ -74,7 +90,7 @@ f32 walk_text_clipped(Vec2 pen, const char* utf8, f32 budget_px, const render::T
|
||||
const f32 advance = glyph->advance * scale;
|
||||
if (used + advance > budget_px) break; // the next glyph does not fit: cut here
|
||||
emit(pen, *glyph);
|
||||
pen.x += advance;
|
||||
pen = pen + axis * advance;
|
||||
used += advance;
|
||||
}
|
||||
return used;
|
||||
@@ -468,16 +484,16 @@ Vec2 Renderer::stage_text(Vec2 pos_px, const char* utf8) {
|
||||
return glyphs_.add_text(utf8, to_viewport(pos_px), font_backend_.scale(), default_font_);
|
||||
}
|
||||
|
||||
f32 Renderer::clipped_text_width(const char* utf8, f32 budget_px) const {
|
||||
return walk_text_clipped(Vec2{}, utf8, budget_px, default_font_, font_backend_.scale(),
|
||||
f32 Renderer::clipped_text_width(const char* utf8, f32 budget_px, Vec2 axis) const {
|
||||
return walk_text_clipped(Vec2{}, utf8, budget_px, default_font_, font_backend_.scale(), axis,
|
||||
[](Vec2, const render::Glyph&) {});
|
||||
}
|
||||
|
||||
f32 Renderer::stage_text_clipped(Vec2 pos_px, const char* utf8, f32 budget_px) {
|
||||
f32 Renderer::stage_text_clipped(Vec2 pos_px, const char* utf8, f32 budget_px, Vec2 axis) {
|
||||
const f32 scale = font_backend_.scale();
|
||||
return walk_text_clipped(to_viewport(pos_px), utf8, budget_px, default_font_, scale,
|
||||
[this, scale](Vec2 pen, const render::Glyph& glyph) {
|
||||
glyphs_.add_glyph(pen, glyph, scale);
|
||||
return walk_text_clipped(to_viewport(pos_px), utf8, budget_px, default_font_, scale, axis,
|
||||
[this, scale, axis](Vec2 pen, const render::Glyph& glyph) {
|
||||
glyphs_.add_glyph(pen, glyph, scale, axis);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -563,46 +579,55 @@ void Renderer::draw_tick_labels(const Plot& plot, const PlotLayout& layout) {
|
||||
|
||||
void Renderer::draw_titles(const Plot& plot, const PlotLayout& layout) {
|
||||
// Every title goes in the strip its direction reserved for it, and every titled
|
||||
// axis gets its own text line inside that strip: one row per axis is what keeps
|
||||
// a Left/Right (or Bottom/Top) pair of titles from being drawn over each other,
|
||||
// where the pass before this one put them all on one centred run. Each run is
|
||||
// also cut to its strip, so a title longer than the strip -- which the layout
|
||||
// caps, because a title may shrink the plot but never erase it -- stops at the
|
||||
// strip's padding instead of spilling over the labels or the data.
|
||||
// axis gets a text line of its own inside that strip -- a row for an x axis, a
|
||||
// column for a y one -- which is what keeps a Left/Right (or Bottom/Top) pair of
|
||||
// titles from being drawn over each other, where the pass before that put them
|
||||
// all on one centred run. Each run is also cut to its strip, so a title longer
|
||||
// than the strip -- which the layout caps, because a title may shrink the plot
|
||||
// but never erase it -- stops at the strip's padding instead of spilling over the
|
||||
// labels or the data.
|
||||
const f32 pad = style_.grid_label_pad;
|
||||
const f32 line_h = font_backend_.height();
|
||||
|
||||
// X titles: the strip's rows top down, one per titled axis.
|
||||
// X titles: the strip's rows top down, one per titled axis, each read left to
|
||||
// right on the row it owns.
|
||||
for (u32 i = 0, row = 0; i < plot.x_axis_count(); ++i) {
|
||||
const Axis& axis = plot.x_axis(i);
|
||||
if (axis.label.empty()) continue;
|
||||
const Rect& strip = layout.x_title_area;
|
||||
const f32 budget = strip.w - 2.0f * pad;
|
||||
const f32 width = clipped_text_width(axis.label.c_str(), budget);
|
||||
const f32 width = clipped_text_width(axis.label.c_str(), budget, kTextRight);
|
||||
// Centred on the strip: the tick labels have their own rows above it, so the
|
||||
// title needs no alignment of its own.
|
||||
stage_text_clipped({strip.x + 0.5f * (strip.w - width),
|
||||
strip.y + pad + static_cast<f32>(row) * line_h},
|
||||
axis.label.c_str(), budget);
|
||||
axis.label.c_str(), budget, kTextRight);
|
||||
++row;
|
||||
}
|
||||
|
||||
// Y titles: the same, stacked down the axes-tall strip and centred on it as a
|
||||
// group, so the single titled axis an ordinary plot has keeps the row it always
|
||||
// had. A y title reads horizontally like the x ones, so its width is what the
|
||||
// strip had to reserve; the extra lines cost height, which the axes already
|
||||
// provides.
|
||||
const f32 stack = static_cast<f32>(plot.titled_y_axis_count()) * line_h;
|
||||
for (u32 i = 0, row = 0; i < plot.y_axis_count(); ++i) {
|
||||
// Y titles: the same strips, cut the same way, but each run is rotated a quarter
|
||||
// turn so it reads bottom-to-top -- axis (0,-1), the direction the run advances
|
||||
// in -- which is what lets the strip be one line *wide* instead of as wide as the
|
||||
// title: a rotation, not a reflection, so the text is not mirrored (the shader
|
||||
// builds the cell's other axis as the basis' perpendicular).
|
||||
//
|
||||
// Each titled axis gets its own column, the mirror of the x rows above, so a
|
||||
// Left/Right pair of titled y axes is drawn side by side rather than over each
|
||||
// other. The pen starts at the column's bottom-left: with this basis the first
|
||||
// cell occupies the pen's own corner and grows up and to the right, so the run
|
||||
// rises out of the strip's bottom edge and is centred along the height -- and cut
|
||||
// to it, which is the budget this direction needs. A title longer than the axes
|
||||
// is therefore clipped at the strip's ends exactly like an over-long x title.
|
||||
for (u32 i = 0, column = 0; i < plot.y_axis_count(); ++i) {
|
||||
const Axis& axis = plot.y_axis(i);
|
||||
if (axis.label.empty()) continue;
|
||||
const Rect& strip = layout.y_title_area;
|
||||
const f32 budget = strip.w - 2.0f * pad;
|
||||
const f32 width = clipped_text_width(axis.label.c_str(), budget);
|
||||
const f32 top = strip.y + std::max(0.0f, 0.5f * (strip.h - stack)) +
|
||||
static_cast<f32>(row) * line_h;
|
||||
stage_text_clipped({strip.x + 0.5f * (strip.w - width), top}, axis.label.c_str(), budget);
|
||||
++row;
|
||||
const f32 budget = strip.h - 2.0f * pad;
|
||||
const f32 length = clipped_text_width(axis.label.c_str(), budget, kTextUp);
|
||||
stage_text_clipped({strip.x + pad + static_cast<f32>(column) * line_h,
|
||||
strip.bottom() - 0.5f * (strip.h - length)},
|
||||
axis.label.c_str(), budget, kTextUp);
|
||||
++column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user