fix(plot,render,glfw): a hue-neutral crosshair, an em-dash placeholder, one layout per figure event, and a usable readout switch

This commit is contained in:
Martino Ferrari committed 2026-09-28 19:38:30 +02:00
1 parent 10d5129fbf
commit a14f333dbe
10 files changed
+175 -70

No files matched your search

+17 -13
View File
@@ -1305,9 +1305,10 @@ void Plot::build_crosshair() {
row.label_box = Rect{};
row.value_box = Rect{};
row.label.assign(trace.name.empty() ? crosshair_unnamed_label(i) : trace.name);
row.value.clear();
// The value, and with it the marker: a trace with no sample at this x writes no number
// and gets no marker, rather than a value clamped to the end of its series.
// The value: the interpolated number where the trace has a sample at this x, and the em dash
// where it does not. The row is listed either way, so the absence has to be *said* -- a blank
// value cell reads as a value that has not arrived -- and the dash is measured into the panel's
// width like any other value, so the row's boxes are placed by one rule.
if (crosshair_value(i, crosshair_.x, row.y)) {
row.valid = true;
char value[32];
@@ -1319,6 +1320,8 @@ void Plot::build_crosshair() {
// actually drawn however its axes are scaled.
row.marker = Vec2{static_cast<f32>(x_transform(trace.axis_x).to_screen_x(crosshair_.x)),
static_cast<f32>(y_transform(trace.axis_y).to_screen_y(row.y))};
} else {
row.value.assign(kCrosshairNoValue);
}
}
crosshair.rows.resize(count);
@@ -1328,13 +1331,13 @@ void Plot::build_crosshair() {
if (count == 0u || !crosshair_.show_readout || text_ == nullptr) return;
const ReadoutChrome chrome = readout_chrome(legend_, style_, *text_);
// Each row's content: its label, the gutter, and its value when it has one -- an invalid
// row is narrower, and the panel is sized by the widest row that will be listed, which the
// prefix-max reads in O(1) after a row was dropped from the end.
// Each row's content: its label, the gutter, and its value -- which is a number or the em dash,
// so every row is the same two runs and the panel is sized by the widest *listed* one, read in
// O(1) by the prefix-max after a row was dropped from the end.
crosshair_content_px_.clear();
for (const CrosshairRow& row : crosshair.rows) {
f32 content = text_->measure(row.label.c_str());
if (row.valid) content += chrome.pad + text_->measure(row.value.c_str());
const f32 content =
text_->measure(row.label.c_str()) + chrome.pad + text_->measure(row.value.c_str());
crosshair_content_px_.push_back(crosshair_content_px_.empty()
? content
: std::max(crosshair_content_px_.back(), content));
@@ -1393,11 +1396,12 @@ void Plot::build_crosshair() {
row.swatch = readout_swatch(chrome, crosshair.panel, i);
const Vec2 pen = readout_text_pen(chrome, crosshair.panel, i);
row.label_box = Rect{pen.x, pen.y, text_->measure(row.label.c_str()), chrome.line_h};
if (row.valid) {
const f32 width = text_->measure(row.value.c_str());
row.value_box =
Rect{crosshair.panel.right() - chrome.pad - width, pen.y, width, chrome.line_h};
}
// Every listed row stages a value run: the number, or `kCrosshairNoValue` where the trace has
// no sample here. A blank cell would read as a value still to come, which is a different thing
// from "nothing at this x".
const f32 width = text_->measure(row.value.c_str());
row.value_box =
Rect{crosshair.panel.right() - chrome.pad - width, pen.y, width, chrome.line_h};
}
}
+5 -5
View File
@@ -1190,7 +1190,7 @@ void Renderer::draw_swatch(const Plot& plot, u32 trace, const Rect& box) {
void Renderer::draw_crosshair_lines(const Plot& plot, const PlotLayout& layout) {
const CrosshairLayout& crosshair = layout.crosshair;
if (!crosshair.active) return;
if (crosshair.empty()) 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
@@ -1231,7 +1231,7 @@ void Renderer::draw_crosshair_lines(const Plot& plot, const PlotLayout& layout)
void Renderer::draw_crosshair_readout(const Plot& plot, const PlotLayout& layout) {
const CrosshairLayout& crosshair = layout.crosshair;
if (!crosshair.active) return;
if (crosshair.empty()) return;
// The readout: the same backplate a legend has (the shared `draw_readout_panel()`), with its
// border in the crosshair's own colour -- which is what makes the panel read as part of the
@@ -1242,9 +1242,9 @@ void Renderer::draw_crosshair_readout(const Plot& plot, const PlotLayout& layout
if (!row.listed) continue;
draw_swatch(plot, row.trace, row.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());
// The value is a number, or `kCrosshairNoValue` for a trace with no sample at this x (the plot
// put it there): either way there is a run to stage, so a row is never a blank cell.
stage_text({row.value_box.x, row.value_box.y}, row.value.c_str());
}
}