feat: a measurement accumulates how it varies, so one reading can be told from a trend

This commit is contained in:
Martino Ferrari committed 2026-10-08 09:22:35 +02:00
1 parent d79810c7ba
commit 1a8218887c
2 files changed
+79 -8

No files matched your search

+36 -8
View File
@@ -269,6 +269,11 @@ struct App {
/// of the signal and survives the window it was measured in, so the rulers do not blink off when
/// a pass lands on a window with no samples (a rebuild, a capture boundary, a mode change).
std::vector<MeasureResult> measure_last_valid;
/// Running statistics, one per entry of `measures` and reset whenever that vector is
/// rebuilt. Not reset by a pause, a latch or a pan: those change what is measured *now*,
/// not what the measurement means, and a statistic the user cannot keep across them is
/// not worth having.
std::vector<MeasureStatsAccum> measure_stats;
/// The marks the last computation produced: re-placed every frame, which is what makes the
/// measurement cursors read-only.
///
@@ -2179,6 +2184,13 @@ void sync_window_display(App& app, double seconds) {
app.updating_panels = false;
}
/// Clears every measurement's statistics. Called explicitly by the pane's Reset, and
/// implicitly wherever the series of results stops being one series: a measurement edit
/// (`rebuild_measures_pane()`), a change of display window, and a CONFIG.
void reset_measure_stats(App& app) {
for (MeasureStatsAccum& s : app.measure_stats) s.reset();
}
/// The one place the time scale changes. Clamps to what the buffer can hold, moves the plot and
/// the trigger's event span together -- its pre-trigger share is a share of *this* window -- and
/// then restates the result in the control, so a clamped request reads back as the window that
@@ -2209,6 +2221,9 @@ void apply_time_window(App& app, double seconds) {
// The partition before the axis, so the axis is framed over the rings that will exist.
app.rows->set_time_window(clamped);
sync_window_display(app, clamped);
// A measurement over a different window is a different figure -- a mean over 20 ms and a
// mean over 2 s are not observations of one quantity -- so the history behind it goes.
reset_measure_stats(app);
schedule_layout_save(app);
}
@@ -2659,6 +2674,9 @@ void rebuild_measures_pane(App& app) {
}
app.measure_labels.clear();
app.measure_text.clear();
// Editing a measurement changes what the figures *mean*, so the history behind them is no
// longer one series. This is the implicit reset for an add, an edit and a delete alike.
app.measure_stats.assign(app.measures.size(), MeasureStatsAccum{});
if (app.measures.empty()) {
GtkWidget* empty = gtk_label_new("no measurements -- [+] to add one");
gtk_widget_add_css_class(empty, "dim-label");
@@ -3422,16 +3440,17 @@ void update_measures(App& app, bool force) {
const MeasureResult r = compute_measure(w.stats, level, m.type, m.options);
app.measure_results[i] = r;
if (r.valid) app.measure_last_valid[i] = r;
// Time-valued types carry seconds and a frequency carries hertz whatever the signal's
// own unit is; everything else carries that unit. Hoisted out of the valid branch below
// because the statistics line needs the same unit.
const bool is_time =
m.type == MeasureType::PulseDuration || m.type == MeasureType::Period;
const std::string unit = is_time ? "s"
: m.type == MeasureType::Frequency ? "Hz"
: measure_unit(app, m.signal);
if (label != nullptr) {
std::string text = "--";
if (r.valid) {
// Time-valued types carry seconds and a frequency carries hertz whatever the
// signal's own unit is; everything else carries that unit.
const bool is_time = m.type == MeasureType::PulseDuration ||
m.type == MeasureType::Period;
const std::string unit = is_time ? "s"
: m.type == MeasureType::Frequency ? "Hz"
: measure_unit(app, m.signal);
text = format_measure(r.primary, unit);
if (r.has_secondary) {
// Pulse duration also reports the duty, which has no unit.
@@ -3441,6 +3460,10 @@ void update_measures(App& app, bool force) {
app.measure_text[i] = text;
gtk_label_set_text(GTK_LABEL(label), text.c_str());
}
// One observation per recompute, and only when the pass produced one. An invalid pass
// contributes nothing -- the marks' `measure_last_valid` fallback below is a *drawing*
// convenience, and accumulating it would count one answer as many observations.
if (r.valid && i < app.measure_stats.size()) app.measure_stats[i].add(r.primary);
}
// The marks last, in their own pass over the measurements, so that no `continue` above can skip
@@ -5891,7 +5914,12 @@ gboolean tick(gpointer user) {
app->dbg_measures_pane_w = gtk_widget_get_width(app->measures_list);
app->dbg_measures_pane_h = gtk_widget_get_height(app->measures_list);
}
if (model.generation() != app->signal_generation) rebuild_signal_panels(*app);
if (model.generation() != app->signal_generation) {
// A CONFIG re-describes the stream and drops the buffer, so nothing measured before it
// belongs in the same series as what comes after.
reset_measure_stats(*app);
rebuild_signal_panels(*app);
}
// A gesture moves the *plot's* cursor; adopting it here is what makes the two
// vertical markers one time across every row, and what the pane then reads.
app->rows->update_time_labels();