Data holes + fix colours

This commit is contained in:
2026-05-24 15:37:42 +02:00
parent 91d549588b
commit cbaaa691a6
2 changed files with 59 additions and 6 deletions
+43 -6
View File
@@ -3,8 +3,31 @@ use egui::ecolor::Hsva;
use egui::{Color32, Vec2b};
use egui_plot::{AxisHints, Legend, Line, Plot, PlotPoints, VPlacement};
use egui_plot::PlotPoint;
use crate::signal::{decimate, nearest_y, PlottedSignal};
/// Split points into runs separated by x-gaps larger than `threshold`,
/// so the line is broken across holes in the data instead of bridged.
fn split_at_gaps(points: Vec<PlotPoint>, threshold: f64) -> Vec<Vec<PlotPoint>> {
let mut segments = Vec::new();
let mut current = Vec::new();
let mut prev_x: Option<f64> = None;
for p in points {
if let Some(px) = prev_x {
if p.x - px > threshold && !current.is_empty() {
segments.push(std::mem::take(&mut current));
}
}
prev_x = Some(p.x);
current.push(p);
}
if !current.is_empty() {
segments.push(current);
}
segments
}
const MIN_PLOT_HEIGHT: f32 = 80.0;
const Y_AXIS_MIN_THICKNESS: f32 = 48.0;
@@ -41,9 +64,11 @@ fn full_x_range(signals: &[PlottedSignal]) -> (f64, f64) {
(min, max)
}
fn color_for(id: u64) -> Color32 {
let mixed = (id.wrapping_mul(2654435761)) as u32;
let hue = mixed as f32 / u32::MAX as f32;
/// Per-plot signal color. Index-based with golden-ratio hue spacing so
/// neighbours on the same plot stay visually distinct.
fn color_for_index(index: usize) -> Color32 {
const GOLDEN_RATIO_CONJUGATE: f32 = 0.618_034;
let hue = (index as f32 * GOLDEN_RATIO_CONJUGATE).fract();
Hsva::new(hue, 0.85, 0.9, 1.0).into()
}
@@ -119,12 +144,24 @@ pub fn show_plot_panel(
let x_min = bounds.min()[0];
let x_max = bounds.max()[0];
let pixel_width = plot_ui.transform().frame().width() as usize;
let bucket_w = (x_max - x_min) / pixel_width.max(1) as f64;
let mut buf = Vec::new();
for sig in &group.signals {
for (sig_index, sig) in group.signals.iter().enumerate() {
decimate(&sig.points, x_min, x_max, pixel_width.max(1), &mut buf);
let plot_points = PlotPoints::Owned(std::mem::take(&mut buf));
// When zoomed out, adjacent decimated points are bucket_w
// apart, so don't split on anything smaller than that.
let threshold = sig.gap_threshold.max(bucket_w * 1.5);
let segments = split_at_gaps(std::mem::take(&mut buf), threshold);
let color = color_for_index(sig_index);
let name = line_label(sig, prev_cursor_x);
plot_ui.line(Line::new(name, plot_points).color(color_for(sig.id)));
for (i, seg) in segments.into_iter().enumerate() {
// Only the first segment carries the name so the
// legend shows a single entry per signal.
let seg_name = if i == 0 { name.clone() } else { String::new() };
plot_ui.line(
Line::new(seg_name, PlotPoints::Owned(seg)).color(color),
);
}
}
plot_ui.pointer_coordinate().map(|c| c.x)
});
+16
View File
@@ -6,6 +6,20 @@ pub struct PlottedSignal {
pub label: String,
pub unit: String,
pub points: Vec<PlotPoint>,
pub gap_threshold: f64,
}
/// A jump in x larger than this is treated as a hole in the data and the
/// line is broken rather than bridged. Based on the median sample interval.
fn compute_gap_threshold(points: &[PlotPoint]) -> f64 {
if points.len() < 2 {
return f64::INFINITY;
}
let mut deltas: Vec<f64> =
points.windows(2).map(|w| w[1].x - w[0].x).collect();
deltas.sort_by(|a, b| a.total_cmp(b));
let median = deltas[deltas.len() / 2];
(median * 10.0).max(f64::MIN_POSITIVE)
}
pub fn compute_signal(
@@ -25,11 +39,13 @@ pub fn compute_signal(
y: libsvld::motec::extract_signal(bytes, &sig.signal),
});
}
let gap_threshold = compute_gap_threshold(&points);
Some(PlottedSignal {
id,
label: sig.name.clone(),
unit: sig.unit.clone(),
points,
gap_threshold,
})
}