feat(plot): Figure with stacked rows and a synchronised x axis
This commit is contained in:
1 parent
7cc06f07f5
commit
8f1e60a105
11 files changed
+1665
-24
No files matched your search
+88
-19
@@ -288,11 +288,7 @@ void Renderer::begin_frame(u32 fb_w, u32 fb_h, Rect viewport_px) noexcept {
|
||||
gl::glBlendFunc(gl::GL_SRC_ALPHA, gl::GL_ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
void Renderer::draw(Plot& plot) {
|
||||
stats_ = FrameStats{};
|
||||
if (!ready_) {
|
||||
return;
|
||||
}
|
||||
bool Renderer::begin_frame_text() noexcept {
|
||||
// The plot reserves its label strips from TextBackend metrics, so it must
|
||||
// measure with the size the text pipeline draws with, and re-read the style
|
||||
// each frame so a changed axis_font_px takes effect on the next one.
|
||||
@@ -308,9 +304,10 @@ void Renderer::draw(Plot& plot) {
|
||||
// never changes costs a comparison a frame. A driver that refuses the re-upload
|
||||
// is reported the way the stores report a refused fence: the frame draws on (it
|
||||
// has the previous atlas), and `last_frame_had_gl_error()` says so.
|
||||
if (!atlas_.sync(font_backend_)) {
|
||||
gl_error_ = true;
|
||||
}
|
||||
return atlas_.sync(font_backend_);
|
||||
}
|
||||
|
||||
void Renderer::prepare_plot(Plot& plot) {
|
||||
#ifndef NDEBUG
|
||||
// The renderer owns the text of the frames it draws: every label goes through
|
||||
// the text pipeline in `default_font_`, so the plot is handed this backend's metrics
|
||||
@@ -332,17 +329,78 @@ void Renderer::draw(Plot& plot) {
|
||||
// propagation point the contract names, and it is what keeps the strips the
|
||||
// layout reserves and the strokes drawn into them at one scale.
|
||||
plot.set_ui_scale(style_.ui_scale);
|
||||
// One layout for the whole frame: autoscale, the strip fixed-point loop with
|
||||
// its tick rebuilds, and the grid build all run in here, and every helper
|
||||
// below reads the strips, the axes rect and the transforms it pins.
|
||||
const PlotLayout& layout = plot.layout();
|
||||
}
|
||||
|
||||
void Renderer::draw(Plot& plot) {
|
||||
stats_ = FrameStats{};
|
||||
if (!ready_) {
|
||||
return;
|
||||
}
|
||||
if (!begin_frame_text()) {
|
||||
gl_error_ = true;
|
||||
}
|
||||
prepare_plot(plot);
|
||||
// The plot's one layout for the frame: autoscale, the strip fixed-point loop with
|
||||
// its tick rebuilds, and the grid build all run in here, and every helper below
|
||||
// reads the strips, the axes rect and the transforms it pins.
|
||||
draw_plot(plot, plot.layout());
|
||||
// The frame's single text flush, after every line batch of the frame: text is
|
||||
// texture-bound, so it is one draw call whatever it holds (see draw_plot).
|
||||
flush_text();
|
||||
}
|
||||
|
||||
void Renderer::draw(Figure& figure) {
|
||||
stats_ = FrameStats{};
|
||||
if (!ready_) {
|
||||
return;
|
||||
}
|
||||
if (!begin_frame_text()) {
|
||||
gl_error_ = true;
|
||||
}
|
||||
// Every row is prepared before the figure lays itself out, because a row's strips
|
||||
// are measured from the metrics this installs: a row measuring with one backend and
|
||||
// drawing with another is the defect the propagation point exists to prevent.
|
||||
const u32 rows = figure.row_count();
|
||||
for (u32 row = 0; row < rows; ++row) prepare_plot(figure.plot(row));
|
||||
|
||||
// The figure's one layout for the frame. It hands every row its rect and its
|
||||
// x-label policy, advances the link groups, and lays every row out against its own
|
||||
// rect -- which is why the rows below are drawn with the layout it produced rather
|
||||
// than laying each row out a second time. Nothing draws before this: a row's
|
||||
// viewport is the figure's to set.
|
||||
const FigureLayout& layout = figure.layout();
|
||||
for (u32 row = 0; row < rows; ++row) {
|
||||
Plot& plot = figure.plot(row);
|
||||
// The layout the figure just computed, read through the non-recomputing
|
||||
// overload -- the one layout of this frame, not a second derivation of it.
|
||||
draw_plot(plot, static_cast<const Plot&>(plot).layout());
|
||||
}
|
||||
|
||||
// The figure's one shared x axis, staged last so it joins the frame's single text
|
||||
// flush: the labels of the last row's x axes, in the strip the figure measured
|
||||
// under it. The rows reserved no strip of their own for them (see
|
||||
// `Plot::set_x_label_strip()`), which is what keeps one set of labels from being
|
||||
// drawn once per row; with the figure set to label every row, there is no shared
|
||||
// strip and each row drew its own above.
|
||||
if (rows != 0u && figure.x_labels_only_on_bottom() && layout.x_axis_strip.h > 0.0f) {
|
||||
draw_x_tick_labels(figure.plot(rows - 1u), layout.x_axis_strip);
|
||||
}
|
||||
flush_text();
|
||||
}
|
||||
|
||||
void Renderer::draw_plot(Plot& plot, const PlotLayout& layout) {
|
||||
|
||||
// Draw order: chrome, then data, then the overlays over it, and the text last (it
|
||||
// is texture-bound, so it is one draw call after every line batch). The legend
|
||||
// closes the line batches: its panel is a backplate, so it is drawn over
|
||||
// everything geometric -- a legend under a trace is a legend nobody can read --
|
||||
// while its names join the frame's single text flush with every other label, which
|
||||
// is what keeps the two-pipeline order the whole frame is built on.
|
||||
// while its names wait for the frame's single text flush with every other label,
|
||||
// which is what keeps the two-pipeline order the whole frame is built on.
|
||||
//
|
||||
// The flush itself belongs to the frame, not to one plot: `draw(Plot&)` flushes
|
||||
// after this returns, and `draw(Figure&)` after it has drawn every row and the
|
||||
// figure's shared x axis, so a figure is one text draw call rather than one per
|
||||
// row -- and the shared axis' labels join the same batch as the labels above them.
|
||||
draw_grid(plot);
|
||||
draw_axis_lines(plot);
|
||||
draw_tick_labels(plot, layout);
|
||||
@@ -355,7 +413,6 @@ void Renderer::draw(Plot& plot) {
|
||||
draw_measures(plot, layout);
|
||||
draw_annotations(plot);
|
||||
draw_legend(plot, layout);
|
||||
flush_text();
|
||||
}
|
||||
|
||||
void Renderer::end_frame() noexcept {
|
||||
@@ -597,20 +654,32 @@ void Renderer::draw_axis_lines(const Plot& plot) {
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::draw_tick_labels(const Plot& plot, const PlotLayout& layout) {
|
||||
void Renderer::draw_x_tick_labels(const Plot& plot, const Rect& strip) {
|
||||
// Centred under their tick, on the strip's single text line -- the one rule for a
|
||||
// plot's own label strip and for the strip a figure shares out under its last row.
|
||||
// A plot that reserved no strip of its own hands its labels to the figure, which
|
||||
// is why this takes the rect rather than reading the layout it came from.
|
||||
const f32 line_h = font_backend_.height();
|
||||
|
||||
// X labels: centred under their tick, on the strip's single text line.
|
||||
for (u32 i = 0; i < plot.x_axis_count(); ++i) {
|
||||
const Transform transform = plot.x_transform(i);
|
||||
for (const Tick& tick : plot.x_axis(i).ticks) {
|
||||
if (!tick.major || tick.label.empty()) continue;
|
||||
const f32 width = font_backend_.measure(tick.label.c_str());
|
||||
stage_text({static_cast<f32>(transform.to_screen_x(tick.value)) - 0.5f * width,
|
||||
layout.x_label_area.y + 0.5f * (layout.x_label_area.h - line_h)},
|
||||
strip.y + 0.5f * (strip.h - line_h)},
|
||||
tick.label.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::draw_tick_labels(const Plot& plot, const PlotLayout& layout) {
|
||||
const f32 line_h = font_backend_.height();
|
||||
|
||||
// X labels: unless the plot gave its x label strip away -- a row of a `Figure`
|
||||
// whose figure drew one shared axis instead -- in which case it has no strip to
|
||||
// put them in and their absence is the point. A plot with no strip would otherwise
|
||||
// stage its labels just above `axes`' bottom edge, i.e. over its own data.
|
||||
if (plot.x_label_strip()) draw_x_tick_labels(plot, layout.x_label_area);
|
||||
|
||||
// Y labels: right-aligned in their strip, centred on the tick and pulled back
|
||||
// inside the strip at the two extremes (a label centred on the top tick would
|
||||
|
||||
Reference in new issue
Block a user