use crate::data::trace_look::TraceLook;
use crate::data::traces::{TraceRef, TracesCollection};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeFormat {
Iso8601WithDate,
Iso8601Time,
MinuteSecondMillis,
SecondMillis,
MillisOnly,
}
impl Default for TimeFormat {
fn default() -> Self {
TimeFormat::Iso8601Time
}
}
impl TimeFormat {
fn format_fraction(nsecs: u32, digits: usize) -> String {
let digits = digits.min(9);
if digits == 0 {
return String::new();
}
let scale = 10_u32.pow((9 - digits) as u32);
let frac = nsecs / scale;
format!(".{:0width$}", frac, width = digits)
}
pub fn format_value(&self, x_seconds: f64, decimals: usize) -> String {
let secs = x_seconds as i64;
let nsecs = ((x_seconds - secs as f64) * 1e9) as u32;
let dt_utc = chrono::DateTime::from_timestamp(secs, nsecs)
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap());
let local = dt_utc.with_timezone(&chrono::Local);
let frac = Self::format_fraction(nsecs, decimals);
match self {
TimeFormat::Iso8601WithDate => {
format!("{}{}", local.format("%Y-%m-%d %H:%M:%S"), frac)
}
TimeFormat::Iso8601Time => format!("{}{}", local.format("%H:%M:%S"), frac),
TimeFormat::MinuteSecondMillis => format!("{}{}", local.format("%M:%S"), frac),
TimeFormat::SecondMillis => format!("{}{}", local.format("%S"), frac),
TimeFormat::MillisOnly => {
let digits = decimals.min(9);
if digits == 0 {
"0".to_string()
} else {
let scale = 10_u32.pow((9 - digits) as u32);
let frac_only = nsecs / scale;
format!("{:0width$}", frac_only, width = digits)
}
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ValueFormat {
pub scientific_min_exp: i32,
pub scientific_max_exp: i32,
pub always_scientific: bool,
pub unit: Option<String>,
}
impl Default for ValueFormat {
fn default() -> Self {
Self {
scientific_min_exp: -4,
scientific_max_exp: 6,
always_scientific: false,
unit: None,
}
}
}
impl ValueFormat {
pub fn format_value(&self, v: f64, decimals: usize, step: Option<f64>) -> String {
let sci = if self.always_scientific {
true
} else if !v.is_finite() || v == 0.0 {
false
} else {
let exp_value = v.abs().log10().floor() as i32;
let exp_step = if let Some(step) = step {
if step <= 0.0 || !step.is_finite() {
0
} else {
step.abs().log10().floor() as i32
}
} else {
0
};
exp_value <= self.scientific_min_exp
|| exp_value >= self.scientific_max_exp
|| exp_step <= self.scientific_min_exp
|| exp_step >= self.scientific_max_exp
};
let formatted = if sci {
if v == 0.0 || !v.is_finite() {
format!("{:.*}", decimals, v)
} else {
let sign = if v.is_sign_negative() { -1.0 } else { 1.0 };
let av = v.abs();
let exp = av.log10().floor() as i32;
let pow = 10f64.powi(exp);
let mant = sign * (av / pow);
if exp == 0 {
format!("{:.*}", decimals, mant)
} else {
format!("{:.*}e{}", decimals, mant, exp)
}
}
} else {
format!("{:.*}", decimals, v)
};
if let Some(unit) = &self.unit {
format!("{} {}", formatted, unit)
} else {
formatted
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AxisType {
Time(TimeFormat),
Value(ValueFormat),
}
#[derive(Clone, Debug)]
pub struct AxisSettings {
pub log_scale: bool,
pub name: Option<String>,
pub bounds: (f64, f64),
pub auto_fit: bool,
pub keep_max_fit: bool,
pub axis_type: AxisType,
pub value_decimals: usize,
pub show_label: bool,
}
impl Default for AxisSettings {
fn default() -> Self {
Self {
log_scale: false,
name: None,
bounds: (0.0, 1.0),
auto_fit: false,
keep_max_fit: false,
axis_type: AxisType::Value(ValueFormat::default()),
value_decimals: 4,
show_label: false,
}
}
}
impl AxisSettings {
pub fn new_time_axis() -> Self {
Self {
name: Some("Time".to_string()),
axis_type: AxisType::Time(TimeFormat::default()),
..Default::default()
}
}
pub fn get_unit(&self) -> Option<String> {
match &self.axis_type {
AxisType::Time(_) => Some("s".to_string()),
AxisType::Value(format) => format.unit.clone(),
}
}
pub fn set_unit(&mut self, unit: Option<String>) {
match &mut self.axis_type {
AxisType::Time(_) => {}
AxisType::Value(format) => format.unit = unit,
}
}
pub fn format_value(&self, v: f64, step: Option<f64>) -> String {
match &self.axis_type {
AxisType::Time(fmt) => fmt.format_value(v, self.value_decimals),
AxisType::Value(fmt) => fmt.format_value(v, self.value_decimals, step),
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum ScopeType {
TimeScope,
XYScope,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum LegendPosition {
#[default]
LeftTop,
RightTop,
LeftBottom,
RightBottom,
}
impl From<LegendPosition> for egui_plot::Corner {
fn from(pos: LegendPosition) -> Self {
match pos {
LegendPosition::LeftTop => egui_plot::Corner::LeftTop,
LegendPosition::RightTop => egui_plot::Corner::RightTop,
LegendPosition::LeftBottom => egui_plot::Corner::LeftBottom,
LegendPosition::RightBottom => egui_plot::Corner::RightBottom,
}
}
}
pub struct ScopeData {
pub id: usize,
pub name: String,
pub y_axis: AxisSettings,
pub x_axis: AxisSettings,
pub time_window: f64,
pub scope_type: ScopeType,
pub xy_pairs: Vec<(Option<TraceRef>, Option<TraceRef>, TraceLook)>,
pub paused: bool,
pub show_legend: bool,
pub force_hide_legend: bool,
pub show_info_in_legend: bool,
pub legend_position: LegendPosition,
pub show_grid: bool,
pub trace_order: Vec<TraceRef>,
pub clicked_point: Option<[f64; 2]>,
pub clicked_screen_pos: Option<[f32; 2]>,
pub measurement_active: bool,
pub pause_on_click: bool,
pub measurement_x_range: Option<(f64, f64)>,
pub last_plot_bounds: Option<([f64; 2], [f64; 2])>,
pub last_plot_screen_rect: Option<[f32; 4]>,
pub rendered_this_frame: bool,
cached_y_fit_key: Option<(usize, Option<f64>)>,
cached_x_fit_key: Option<(usize, Option<f64>)>,
}
impl Default for ScopeData {
fn default() -> Self {
Self {
id: 0,
name: "Scope".to_string(),
y_axis: AxisSettings::default(),
x_axis: AxisSettings::new_time_axis(),
time_window: 10.0,
scope_type: ScopeType::TimeScope,
xy_pairs: Vec::new(),
paused: false,
show_legend: true,
force_hide_legend: false,
show_info_in_legend: false,
legend_position: LegendPosition::default(),
show_grid: true,
trace_order: Vec::new(),
clicked_point: None,
clicked_screen_pos: None,
measurement_active: false,
pause_on_click: false,
measurement_x_range: None,
last_plot_bounds: None,
last_plot_screen_rect: None,
rendered_this_frame: false,
cached_y_fit_key: None,
cached_x_fit_key: None,
}
}
}
impl ScopeData {
pub fn remove_trace(&mut self, trace: &TraceRef) {
self.trace_order.retain(|t| t != trace);
for (x, y, _look) in self.xy_pairs.iter_mut() {
if x.as_ref() == Some(trace) {
*x = None;
}
if y.as_ref() == Some(trace) {
*y = None;
}
}
self.xy_pairs.retain(|(x, y, _)| x.is_some() || y.is_some());
}
pub fn update(&mut self, traces: &TracesCollection) {
self.trace_order.retain(|n| traces.contains_key(n));
self.xy_pairs.retain(|(x, y, _)| {
let x_ok = x.as_ref().is_none_or(|t| traces.contains_key(t));
let y_ok = y.as_ref().is_none_or(|t| traces.contains_key(t));
x_ok && y_ok && !(x.is_none() && y.is_none())
});
let (total_pts, max_ts) = self
.trace_order
.iter()
.filter_map(|name| traces.get_trace(name))
.filter(|t| t.look.visible)
.fold((0usize, None::<f64>), |(len, max_ts), t| {
let ts = if self.paused {
t.snap
.as_ref()
.and_then(|s| s.back().map(|p| p[0]))
.or_else(|| t.live.back().map(|p| p[0]))
} else {
t.live.back().map(|p| p[0])
};
let buf_len = if self.paused {
t.snap.as_ref().map(|s| s.len()).unwrap_or(t.live.len())
} else {
t.live.len()
};
let new_max = match (max_ts, ts) {
(Some(a), Some(b)) => Some(a.max(b)),
(a, None) => a,
(None, b) => b,
};
(len + buf_len, new_max.map(|t| (t * 10.0).round() / 10.0))
});
if self.x_axis.auto_fit {
let key = (total_pts, max_ts);
if self.cached_x_fit_key != Some(key) {
self.fit_x_bounds(traces, self.x_axis.keep_max_fit);
self.cached_x_fit_key = Some(key);
}
}
self.live_update(traces);
if self.y_axis.auto_fit {
let key = (total_pts, max_ts);
if self.cached_y_fit_key != Some(key) {
self.fit_y_bounds(traces, self.y_axis.keep_max_fit);
self.cached_y_fit_key = Some(key);
}
}
}
fn live_update(&mut self, traces: &TracesCollection) {
if self.scope_type == ScopeType::TimeScope {
if !self.paused {
let now = self
.trace_order
.iter()
.filter_map(|name| traces.get_trace(name))
.filter_map(|trace| trace.live.back().map(|last| last[0]))
.fold(None, |acc: Option<f64>, val| {
Some(acc.map_or(val, |a: f64| a.max(val)))
})
.unwrap_or(self.time_window);
let time_lower = now - self.time_window;
let x_lower = if let Some((m_min, _)) = self.measurement_x_range {
m_min.min(time_lower)
} else {
time_lower
};
self.x_axis.bounds = (x_lower, now);
} else {
let diff = ((self.x_axis.bounds.1 - self.x_axis.bounds.0) - self.time_window) / 2.0;
self.x_axis.bounds = (self.x_axis.bounds.0 + diff, self.x_axis.bounds.1 - diff);
}
}
}
pub fn fit_x_bounds(&mut self, traces: &TracesCollection, not_shrink: bool) {
if self.scope_type == ScopeType::XYScope && !self.xy_pairs.is_empty() {
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let tol = 1e-9_f64;
for (x_name, y_name, pair_look) in self.xy_pairs.iter() {
let (Some(x_name), Some(y_name)) = (x_name.as_ref(), y_name.as_ref()) else {
continue;
};
let (Some(x_tr), Some(y_tr)) = (traces.get_trace(x_name), traces.get_trace(y_name))
else {
continue;
};
if !pair_look.visible || !x_tr.look.visible || !y_tr.look.visible {
continue;
}
let x_pts = traces.get_points_ref(x_name, self.paused);
let y_pts = traces.get_points_ref(y_name, self.paused);
let (Some(x_pts), Some(y_pts)) = (x_pts, y_pts) else {
continue;
};
let mut i = 0usize;
let mut j = 0usize;
while i < x_pts.len() && j < y_pts.len() {
let tx = x_pts[i][0];
let ty = y_pts[j][0];
let dt = tx - ty;
if dt.abs() <= tol {
let x = x_pts[i][1] + x_tr.offset;
if x < min_x {
min_x = x;
}
if x > max_x {
max_x = x;
}
i += 1;
j += 1;
} else if dt < 0.0 {
i += 1;
} else {
j += 1;
}
}
}
if min_x < max_x {
if not_shrink {
let cur = self.x_axis.bounds;
self.x_axis.bounds = (min_x.min(cur.0), max_x.max(cur.1));
} else {
self.x_axis.bounds = (min_x, max_x);
}
self.time_window = self.x_axis.bounds.1 - self.x_axis.bounds.0;
}
return;
}
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
for name in self.trace_order.iter() {
let Some(trace) = traces.get_trace(name) else {
continue;
};
if !trace.look.visible {
continue;
}
let points = if self.paused {
if let Some(snap) = &trace.snap {
snap
} else {
&trace.live
}
} else {
&trace.live
};
for p in points.iter() {
if p[0] < min_x {
min_x = p[0];
}
if p[0] > max_x {
max_x = p[0];
}
}
}
if min_x < max_x {
if not_shrink {
let cur = self.x_axis.bounds;
self.x_axis.bounds = (min_x.min(cur.0), max_x.max(cur.1));
} else {
self.x_axis.bounds = (min_x, max_x);
}
self.time_window = self.x_axis.bounds.1 - self.x_axis.bounds.0;
} else if min_x == min_x {
if min_x < 0.0 {
self.y_axis.bounds = (min_x, 0.0);
self.time_window = -min_x;
} else if min_x > 0.0 {
self.y_axis.bounds = (0.0, min_x);
self.time_window = min_x;
} else {
self.y_axis.bounds = (-1.0, 1.0);
self.time_window = 2.0;
}
}
}
pub fn fit_y_bounds(&mut self, traces: &TracesCollection, not_shrink: bool) {
if self.scope_type == ScopeType::XYScope && !self.xy_pairs.is_empty() {
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
let tol = 1e-9_f64;
for (x_name, y_name, pair_look) in self.xy_pairs.iter() {
let (Some(x_name), Some(y_name)) = (x_name.as_ref(), y_name.as_ref()) else {
continue;
};
let (Some(x_tr), Some(y_tr)) = (traces.get_trace(x_name), traces.get_trace(y_name))
else {
continue;
};
if !pair_look.visible || !x_tr.look.visible || !y_tr.look.visible {
continue;
}
let x_pts = traces.get_points_ref(x_name, self.paused);
let y_pts = traces.get_points_ref(y_name, self.paused);
let (Some(x_pts), Some(y_pts)) = (x_pts, y_pts) else {
continue;
};
let mut i = 0usize;
let mut j = 0usize;
while i < x_pts.len() && j < y_pts.len() {
let tx = x_pts[i][0];
let ty = y_pts[j][0];
let dt = tx - ty;
if dt.abs() <= tol {
let y = y_pts[j][1] + y_tr.offset;
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
i += 1;
j += 1;
} else if dt < 0.0 {
i += 1;
} else {
j += 1;
}
}
}
if min_y < max_y {
if not_shrink {
let cur = self.y_axis.bounds;
self.y_axis.bounds = (min_y.min(cur.0), max_y.max(cur.1));
} else {
self.y_axis.bounds = (min_y, max_y);
}
}
return;
}
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
let x_bounds = self.x_axis.bounds;
for name in self.trace_order.iter() {
let Some(trace) = traces.get_trace(name) else {
continue;
};
if !trace.look.visible {
continue;
}
let points = if self.paused {
if let Some(snap) = &trace.snap {
snap
} else {
&trace.live
}
} else {
&trace.live
};
for p in points.iter() {
if p[0] < x_bounds.0 {
continue;
}
if p[0] > x_bounds.1 {
break;
}
let y = p[1] + trace.offset;
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
}
if min_y < max_y {
if not_shrink {
let cur = self.y_axis.bounds;
self.y_axis.bounds = (min_y.min(cur.0), max_y.max(cur.1));
} else {
self.y_axis.bounds = (min_y, max_y);
}
} else if min_y == max_y {
if min_y < 0.0 {
self.y_axis.bounds = (min_y, 0.0);
} else if min_y > 0.0 {
self.y_axis.bounds = (0.0, max_y);
} else {
self.y_axis.bounds = (-1.0, 1.0);
}
}
}
pub fn fit_bounds(&mut self, traces: &TracesCollection, not_shrink: bool) {
self.fit_x_bounds(traces, not_shrink);
self.fit_y_bounds(traces, not_shrink);
}
pub fn get_drawn_points(
&self,
name: &TraceRef,
traces: &TracesCollection,
) -> Option<Vec<[f64; 2]>> {
if self.scope_type == ScopeType::XYScope {
traces
.get_points_ref(name, self.paused)
.map(|v| v.iter().copied().collect())
} else {
traces.get_drawn_points_decimated(name, self.paused, self.x_axis.bounds, 2000)
}
}
pub fn get_all_drawn_points(
&self,
traces: &TracesCollection,
) -> HashMap<TraceRef, Vec<[f64; 2]>> {
let mut result = HashMap::new();
for name in self.trace_order.iter() {
if let Some(pts) = self.get_drawn_points(name, traces) {
result.insert(name.clone(), pts);
}
}
result
}
}