feat(plot): crosshair with an interpolated per-signal hover readout
This commit is contained in:
1 parent
a153b73ead
commit
fd0c471430
11 files changed
+809
-22
No files matched your search
@@ -494,6 +494,10 @@ void Renderer::draw_plot(Plot& plot, const PlotLayout& layout) {
|
||||
draw_measures(plot, layout);
|
||||
draw_annotations(plot);
|
||||
draw_legend(plot, layout);
|
||||
// The crosshair last of the interior passes: it is what the pointer is on, so its panel is
|
||||
// drawn over the legend and every other overlay rather than under them, and its text joins
|
||||
// the frame's single flush with everything else.
|
||||
draw_crosshair(plot, layout);
|
||||
clear_clip();
|
||||
draw_tick_labels(plot, layout);
|
||||
draw_titles(plot, layout);
|
||||
@@ -1170,6 +1174,83 @@ void Renderer::draw_legend(const Plot& plot, const PlotLayout& layout) {
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::draw_crosshair(const Plot& plot, const PlotLayout& layout) {
|
||||
const CrosshairLayout& crosshair = layout.crosshair;
|
||||
if (!crosshair.active) return;
|
||||
|
||||
const f32 stroke = style_.device(style_.line_width_px);
|
||||
// The two lines. Each is staged as one data-space segment at the *centre* of the stroke box
|
||||
// the layout placed, spanning that box's own extent, so the pixels this draws are the box
|
||||
// rather than a second placement of the same rule: the layout held the box inside `axes`,
|
||||
// and the clip keeps the half pixel the rasteriser puts outside the box's fractional edges.
|
||||
const Mapping data = mapping(plot, 0u, 0u);
|
||||
if (crosshair.vertical.w > 0.0f) {
|
||||
const f64 x = plot.x_transform(0u).to_data_x(
|
||||
static_cast<f64>(crosshair.vertical.x + 0.5f * crosshair.vertical.w));
|
||||
const Range y_range = plot.y_range(0u);
|
||||
stage_segment(x, y_range.min, x, y_range.max, plot.x_axis(0u).log, plot.y_axis(0u).log);
|
||||
flush_lines(data, style_.crosshair, stroke);
|
||||
}
|
||||
if (crosshair.horizontal.h > 0.0f) {
|
||||
const f64 y = plot.y_transform(0u).to_data_y(
|
||||
static_cast<f64>(crosshair.horizontal.y + 0.5f * crosshair.horizontal.h));
|
||||
const Range x_range = plot.x_range(0u);
|
||||
stage_segment(x_range.min, y, x_range.max, y, plot.x_axis(0u).log, plot.y_axis(0u).log);
|
||||
flush_lines(data, style_.crosshair, stroke);
|
||||
}
|
||||
|
||||
// A small cross at each trace's intersection, in the trace's own colour -- opaque, like a
|
||||
// legend's swatch, so a trace drawn at 15% still has a marker that can be seen. The arm
|
||||
// length is a logical size like every other marker's, and the position is the layout's: a
|
||||
// marker for a sample far outside the visible y range is staged where the trace is and the
|
||||
// axes clip keeps it inside, rather than being skipped by a range test of this layer's own.
|
||||
const f32 arm = style_.device(3.0f);
|
||||
for (const CrosshairRow& row : crosshair.rows) {
|
||||
if (!row.valid) continue;
|
||||
Color color = plot.trace(row.trace).color;
|
||||
color.a = 1.0f;
|
||||
lines_.add_segment({row.marker.x - arm, row.marker.y}, {row.marker.x + arm, row.marker.y});
|
||||
lines_.add_segment({row.marker.x, row.marker.y - arm}, {row.marker.x, row.marker.y + arm});
|
||||
flush_lines(pixel_mapping(), color, style_.device(1.0f));
|
||||
}
|
||||
|
||||
// The readout: the same backplate a legend has -- the frame's background at a partial
|
||||
// alpha, so the data stays legible through it -- with its border in the crosshair's own
|
||||
// colour, which is what makes the panel read as part of the instrument that placed it
|
||||
// rather than as a second legend.
|
||||
if (crosshair.panel.w > 0.0f && crosshair.panel.h > 0.0f) {
|
||||
const Rect& panel = crosshair.panel;
|
||||
Color background = style_.background;
|
||||
background.a *= kLegendPanelAlpha;
|
||||
fill_pixels(panel, background);
|
||||
// The crisp-hairline rule of the legend's border: the outermost *lit* columns and rows of
|
||||
// the fill's span, not the measured rect's fractional edges, so all four sides are one
|
||||
// hairline rather than three bright ones and a dim one.
|
||||
const f32 left = std::ceil(panel.x - 0.5f) + 0.5f;
|
||||
const f32 right = std::ceil(panel.right() - 0.5f) - 0.5f;
|
||||
const f32 top = std::ceil(panel.y - 0.5f) + 0.5f;
|
||||
const f32 bottom = std::ceil(panel.bottom() - 0.5f) - 0.5f;
|
||||
lines_.add_segment({left, top}, {right, top});
|
||||
lines_.add_segment({left, bottom}, {right, bottom});
|
||||
lines_.add_segment({left, top}, {left, bottom});
|
||||
lines_.add_segment({right, top}, {right, bottom});
|
||||
flush_lines(pixel_mapping(), style_.crosshair, style_.device(1.0f));
|
||||
}
|
||||
|
||||
for (const CrosshairRow& row : crosshair.rows) {
|
||||
if (!row.listed) continue;
|
||||
// The swatch is the trace's colour opaque, as a legend's is: it is the one element that
|
||||
// says which signal the row is about.
|
||||
Color swatch = plot.trace(row.trace).color;
|
||||
swatch.a = 1.0f;
|
||||
fill_pixels(row.swatch, swatch);
|
||||
stage_text({row.label_box.x, row.label_box.y}, row.label.c_str());
|
||||
// A row with no sample at this x has no value to write -- and it is *not* written as a
|
||||
// zero or a dash: the box is empty and the number is simply absent (see CrosshairRow).
|
||||
if (row.valid) stage_text({row.value_box.x, row.value_box.y}, row.value.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------- dump
|
||||
|
||||
bool Renderer::dump_frame_ppm(const char* path, Status* st) const {
|
||||
|
||||
Reference in new issue
Block a user