Add mixed text/plot transport and improve plot UI
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
use egui::{ComboBox, DragValue, ScrollArea, TextEdit, Ui};
|
||||
use xserial_client::config::{DecoderConfig, FramerConfig, PipelineConfig, SessionConfig};
|
||||
use xserial_core::protocol::plot::PlotFormat;
|
||||
use xserial_core::transport::TransportConfig;
|
||||
use xserial_core::transport::serial::{
|
||||
SerialDataBits, SerialFlowControl, SerialParity, SerialStopBits,
|
||||
};
|
||||
|
||||
// ── 表单数据结构(简化版,方便 UI 渲染)────────────────────────────
|
||||
|
||||
@@ -9,14 +13,27 @@ pub struct ConfigForm {
|
||||
pub transport: TransportChoice,
|
||||
pub port: String,
|
||||
pub baud: u32,
|
||||
pub serial_data_bits: SerialDataBits,
|
||||
pub serial_parity: SerialParity,
|
||||
pub serial_stop_bits: SerialStopBits,
|
||||
pub serial_flow_control: SerialFlowControl,
|
||||
pub addr: String,
|
||||
pub udp_bind: String,
|
||||
pub udp_remote: String,
|
||||
pub pipelines: Vec<(String, FramerChoice, DecoderChoice)>,
|
||||
pub pipelines: Vec<PipelineForm>,
|
||||
pub history: usize,
|
||||
pub auto_reconnect: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PipelineForm {
|
||||
pub name: String,
|
||||
pub framer: FramerChoice,
|
||||
pub decoder: DecoderChoice,
|
||||
pub plot_channels: usize,
|
||||
pub plot_format: PlotFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum TransportChoice {
|
||||
Serial,
|
||||
@@ -28,6 +45,7 @@ pub enum TransportChoice {
|
||||
pub enum FramerChoice {
|
||||
Line,
|
||||
Fixed(usize),
|
||||
MixedTextPlot,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
@@ -35,6 +53,7 @@ pub enum DecoderChoice {
|
||||
Text,
|
||||
Hex,
|
||||
Plot,
|
||||
MixedTextPlot,
|
||||
}
|
||||
|
||||
impl Default for ConfigForm {
|
||||
@@ -43,10 +62,20 @@ impl Default for ConfigForm {
|
||||
transport: TransportChoice::Tcp,
|
||||
port: "/dev/ttyUSB0".into(),
|
||||
baud: 115200,
|
||||
serial_data_bits: SerialDataBits::Eight,
|
||||
serial_parity: SerialParity::None,
|
||||
serial_stop_bits: SerialStopBits::One,
|
||||
serial_flow_control: SerialFlowControl::None,
|
||||
addr: "127.0.0.1:8080".into(),
|
||||
udp_bind: "0.0.0.0:9000".into(),
|
||||
udp_remote: String::new(),
|
||||
pipelines: vec![("text".into(), FramerChoice::Line, DecoderChoice::Text)],
|
||||
pipelines: vec![PipelineForm {
|
||||
name: "text".into(),
|
||||
framer: FramerChoice::Line,
|
||||
decoder: DecoderChoice::Text,
|
||||
plot_channels: 1,
|
||||
plot_format: PlotFormat::Interleaved,
|
||||
}],
|
||||
history: 10000,
|
||||
auto_reconnect: false,
|
||||
}
|
||||
@@ -69,6 +98,22 @@ impl ConfigForm {
|
||||
TransportConfig::Serial { baud_rate, .. } => *baud_rate,
|
||||
_ => 115200,
|
||||
},
|
||||
serial_data_bits: match &config.transport {
|
||||
TransportConfig::Serial { data_bits, .. } => *data_bits,
|
||||
_ => SerialDataBits::Eight,
|
||||
},
|
||||
serial_parity: match &config.transport {
|
||||
TransportConfig::Serial { parity, .. } => *parity,
|
||||
_ => SerialParity::None,
|
||||
},
|
||||
serial_stop_bits: match &config.transport {
|
||||
TransportConfig::Serial { stop_bits, .. } => *stop_bits,
|
||||
_ => SerialStopBits::One,
|
||||
},
|
||||
serial_flow_control: match &config.transport {
|
||||
TransportConfig::Serial { flow_control, .. } => *flow_control,
|
||||
_ => SerialFlowControl::None,
|
||||
},
|
||||
addr: match &config.transport {
|
||||
TransportConfig::Tcp { addr } => addr.clone(),
|
||||
_ => String::from("127.0.0.1:8080"),
|
||||
@@ -88,6 +133,7 @@ impl ConfigForm {
|
||||
let framer = match &pipeline.framer {
|
||||
FramerConfig::Line { .. } => FramerChoice::Line,
|
||||
FramerConfig::Fixed { frame_len } => FramerChoice::Fixed(*frame_len),
|
||||
FramerConfig::MixedTextPlot { .. } => FramerChoice::MixedTextPlot,
|
||||
FramerConfig::Length { .. } | FramerConfig::Cobs { .. } => {
|
||||
FramerChoice::Line
|
||||
}
|
||||
@@ -96,8 +142,23 @@ impl ConfigForm {
|
||||
DecoderConfig::Text { .. } => DecoderChoice::Text,
|
||||
DecoderConfig::Hex { .. } => DecoderChoice::Hex,
|
||||
DecoderConfig::Plot { .. } => DecoderChoice::Plot,
|
||||
DecoderConfig::MixedTextPlot { .. } => DecoderChoice::MixedTextPlot,
|
||||
};
|
||||
(pipeline.name.clone(), framer, decoder)
|
||||
let plot_channels = match &pipeline.decoder {
|
||||
DecoderConfig::Plot { channels, .. } => *channels,
|
||||
_ => 1,
|
||||
};
|
||||
let plot_format = match &pipeline.decoder {
|
||||
DecoderConfig::Plot { format, .. } => *format,
|
||||
_ => PlotFormat::Interleaved,
|
||||
};
|
||||
PipelineForm {
|
||||
name: pipeline.name.clone(),
|
||||
framer,
|
||||
decoder,
|
||||
plot_channels,
|
||||
plot_format,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
history: config.history_limit,
|
||||
@@ -112,6 +173,10 @@ impl ConfigForm {
|
||||
TransportChoice::Serial => TransportConfig::Serial {
|
||||
port: self.port.clone(),
|
||||
baud_rate: self.baud,
|
||||
data_bits: self.serial_data_bits,
|
||||
parity: self.serial_parity,
|
||||
stop_bits: self.serial_stop_bits,
|
||||
flow_control: self.serial_flow_control,
|
||||
},
|
||||
TransportChoice::Tcp => TransportConfig::Tcp {
|
||||
addr: self.addr.clone(),
|
||||
@@ -128,16 +193,21 @@ impl ConfigForm {
|
||||
pipelines: self
|
||||
.pipelines
|
||||
.iter()
|
||||
.map(|(name, fc, dc)| PipelineConfig {
|
||||
name: name.clone(),
|
||||
framer: match fc {
|
||||
.map(|pipeline| PipelineConfig {
|
||||
name: pipeline.name.clone(),
|
||||
framer: match &pipeline.framer {
|
||||
FramerChoice::Line => FramerConfig::Line {
|
||||
strip_cr: true,
|
||||
max_line_len: 1_048_576,
|
||||
},
|
||||
FramerChoice::Fixed(n) => FramerConfig::Fixed { frame_len: *n },
|
||||
FramerChoice::MixedTextPlot => FramerConfig::MixedTextPlot {
|
||||
strip_cr: true,
|
||||
max_line_len: 1_048_576,
|
||||
max_plot_frame: 1_048_576,
|
||||
},
|
||||
},
|
||||
decoder: match dc {
|
||||
decoder: match &pipeline.decoder {
|
||||
DecoderChoice::Text => DecoderConfig::Text {
|
||||
encoding: xserial_core::protocol::text::TextEncoding::Utf8,
|
||||
},
|
||||
@@ -150,8 +220,11 @@ impl ConfigForm {
|
||||
DecoderChoice::Plot => DecoderConfig::Plot {
|
||||
sample_type: xserial_core::protocol::plot::SampleType::F32,
|
||||
endian: xserial_core::protocol::Endian::Little,
|
||||
channels: 1,
|
||||
format: xserial_core::protocol::plot::PlotFormat::Interleaved,
|
||||
channels: pipeline.plot_channels.max(1),
|
||||
format: pipeline.plot_format,
|
||||
},
|
||||
DecoderChoice::MixedTextPlot => DecoderConfig::MixedTextPlot {
|
||||
encoding: xserial_core::protocol::text::TextEncoding::Utf8,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -166,6 +239,7 @@ impl ConfigForm {
|
||||
|
||||
pub fn render(ui: &mut Ui, form: &mut ConfigForm, submit_label: &str) -> Option<SessionConfig> {
|
||||
let mut result = None;
|
||||
let mut validation_errors = Vec::new();
|
||||
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.heading("Transport");
|
||||
@@ -194,6 +268,106 @@ pub fn render(ui: &mut Ui, form: &mut ConfigForm, submit_label: &str) -> Option<
|
||||
ui.label("Baud:");
|
||||
ui.add(DragValue::new(&mut form.baud).range(300..=12_000_000));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Data bits:");
|
||||
ComboBox::from_id_salt("serial_data_bits")
|
||||
.selected_text(match form.serial_data_bits {
|
||||
SerialDataBits::Five => "5",
|
||||
SerialDataBits::Six => "6",
|
||||
SerialDataBits::Seven => "7",
|
||||
SerialDataBits::Eight => "8",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
&mut form.serial_data_bits,
|
||||
SerialDataBits::Five,
|
||||
"5",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut form.serial_data_bits,
|
||||
SerialDataBits::Six,
|
||||
"6",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut form.serial_data_bits,
|
||||
SerialDataBits::Seven,
|
||||
"7",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut form.serial_data_bits,
|
||||
SerialDataBits::Eight,
|
||||
"8",
|
||||
);
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Parity:");
|
||||
ComboBox::from_id_salt("serial_parity")
|
||||
.selected_text(match form.serial_parity {
|
||||
SerialParity::None => "None",
|
||||
SerialParity::Odd => "Odd",
|
||||
SerialParity::Even => "Even",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
&mut form.serial_parity,
|
||||
SerialParity::None,
|
||||
"None",
|
||||
);
|
||||
ui.selectable_value(&mut form.serial_parity, SerialParity::Odd, "Odd");
|
||||
ui.selectable_value(
|
||||
&mut form.serial_parity,
|
||||
SerialParity::Even,
|
||||
"Even",
|
||||
);
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Stop bits:");
|
||||
ComboBox::from_id_salt("serial_stop_bits")
|
||||
.selected_text(match form.serial_stop_bits {
|
||||
SerialStopBits::One => "1",
|
||||
SerialStopBits::Two => "2",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
&mut form.serial_stop_bits,
|
||||
SerialStopBits::One,
|
||||
"1",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut form.serial_stop_bits,
|
||||
SerialStopBits::Two,
|
||||
"2",
|
||||
);
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Flow:");
|
||||
ComboBox::from_id_salt("serial_flow_control")
|
||||
.selected_text(match form.serial_flow_control {
|
||||
SerialFlowControl::None => "None",
|
||||
SerialFlowControl::Software => "Software",
|
||||
SerialFlowControl::Hardware => "Hardware",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
&mut form.serial_flow_control,
|
||||
SerialFlowControl::None,
|
||||
"None",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut form.serial_flow_control,
|
||||
SerialFlowControl::Software,
|
||||
"Software",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut form.serial_flow_control,
|
||||
SerialFlowControl::Hardware,
|
||||
"Hardware",
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
TransportChoice::Tcp => {
|
||||
ui.horizontal(|ui| {
|
||||
@@ -219,47 +393,60 @@ pub fn render(ui: &mut Ui, form: &mut ConfigForm, submit_label: &str) -> Option<
|
||||
let mut remove = None;
|
||||
|
||||
// 管道列表
|
||||
for (i, (name, fc, dc)) in form.pipelines.iter_mut().enumerate() {
|
||||
for (i, pipeline) in form.pipelines.iter_mut().enumerate() {
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Name:");
|
||||
ui.add(TextEdit::singleline(name).desired_width(60.0));
|
||||
ui.add(TextEdit::singleline(&mut pipeline.name).desired_width(60.0));
|
||||
|
||||
// Framer 选择
|
||||
let fsel = match fc {
|
||||
let fsel = match &pipeline.framer {
|
||||
FramerChoice::Line => "Line".to_string(),
|
||||
FramerChoice::Fixed(n) => format!("Fixed({})", n),
|
||||
FramerChoice::MixedTextPlot => "MixedTextPlot".to_string(),
|
||||
};
|
||||
ComboBox::from_id_salt(format!("f{}", i))
|
||||
.selected_text(fsel)
|
||||
.show_ui(ui, |ui| {
|
||||
if ui.selectable_label(false, "Line").clicked() {
|
||||
*fc = FramerChoice::Line;
|
||||
pipeline.framer = FramerChoice::Line;
|
||||
}
|
||||
if ui.selectable_label(false, "Fixed").clicked() {
|
||||
*fc = FramerChoice::Fixed(8);
|
||||
pipeline.framer = FramerChoice::Fixed(8);
|
||||
}
|
||||
if ui.selectable_label(false, "MixedTextPlot").clicked() {
|
||||
pipeline.framer = FramerChoice::MixedTextPlot;
|
||||
pipeline.decoder = DecoderChoice::MixedTextPlot;
|
||||
}
|
||||
});
|
||||
if let FramerChoice::Fixed(n) = fc {
|
||||
if let FramerChoice::Fixed(n) = &mut pipeline.framer {
|
||||
ui.add(DragValue::new(n).range(1..=65536));
|
||||
}
|
||||
|
||||
// Decoder 选择
|
||||
ComboBox::from_id_salt(format!("d{}", i))
|
||||
.selected_text(match dc {
|
||||
.selected_text(match pipeline.decoder {
|
||||
DecoderChoice::Text => "Text",
|
||||
DecoderChoice::Hex => "Hex",
|
||||
DecoderChoice::Plot => "Plot",
|
||||
DecoderChoice::MixedTextPlot => "MixedTextPlot",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
if ui.selectable_label(false, "Text").clicked() {
|
||||
*dc = DecoderChoice::Text;
|
||||
pipeline.decoder = DecoderChoice::Text;
|
||||
}
|
||||
if ui.selectable_label(false, "Hex").clicked() {
|
||||
*dc = DecoderChoice::Hex;
|
||||
pipeline.decoder = DecoderChoice::Hex;
|
||||
}
|
||||
if ui.selectable_label(false, "Plot").clicked() {
|
||||
*dc = DecoderChoice::Plot;
|
||||
if matches!(pipeline.framer, FramerChoice::Line) {
|
||||
pipeline.framer = FramerChoice::Fixed(256);
|
||||
}
|
||||
pipeline.decoder = DecoderChoice::Plot;
|
||||
}
|
||||
if ui.selectable_label(false, "MixedTextPlot").clicked() {
|
||||
pipeline.framer = FramerChoice::MixedTextPlot;
|
||||
pipeline.decoder = DecoderChoice::MixedTextPlot;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -267,6 +454,88 @@ pub fn render(ui: &mut Ui, form: &mut ConfigForm, submit_label: &str) -> Option<
|
||||
remove = Some(i);
|
||||
}
|
||||
});
|
||||
|
||||
if matches!(pipeline.decoder, DecoderChoice::Plot) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Plot channels:");
|
||||
ui.add(DragValue::new(&mut pipeline.plot_channels).range(1..=32));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Plot format:");
|
||||
ComboBox::from_id_salt(format!("plot_format{}", i))
|
||||
.selected_text(match pipeline.plot_format {
|
||||
PlotFormat::Interleaved => "Interleaved",
|
||||
PlotFormat::Block => "Block",
|
||||
PlotFormat::XY => "XY",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
&mut pipeline.plot_format,
|
||||
PlotFormat::Interleaved,
|
||||
"Interleaved",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut pipeline.plot_format,
|
||||
PlotFormat::Block,
|
||||
"Block",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut pipeline.plot_format,
|
||||
PlotFormat::XY,
|
||||
"XY",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if matches!(pipeline.plot_format, PlotFormat::XY) {
|
||||
pipeline.plot_channels = 2;
|
||||
}
|
||||
|
||||
match &pipeline.framer {
|
||||
FramerChoice::Line => {
|
||||
let message = "Plot decoder requires Fixed framer for raw binary samples";
|
||||
ui.colored_label(egui::Color32::RED, message);
|
||||
validation_errors
|
||||
.push(format!("pipeline '{}': {message}", pipeline.name));
|
||||
}
|
||||
FramerChoice::Fixed(n)
|
||||
if *n % (pipeline.plot_channels.max(1) * 4) != 0 =>
|
||||
{
|
||||
let message = format!(
|
||||
"Plot decoder currently expects interleaved f32 data, so frame length must be a multiple of {} bytes",
|
||||
pipeline.plot_channels.max(1) * 4
|
||||
);
|
||||
ui.colored_label(egui::Color32::RED, &message);
|
||||
validation_errors
|
||||
.push(format!("pipeline '{}': {message}", pipeline.name));
|
||||
}
|
||||
FramerChoice::MixedTextPlot => {
|
||||
let message = "Raw Plot decoder cannot be used with MixedTextPlot framer";
|
||||
ui.colored_label(egui::Color32::RED, message);
|
||||
validation_errors
|
||||
.push(format!("pipeline '{}': {message}", pipeline.name));
|
||||
}
|
||||
FramerChoice::Fixed(_) => {}
|
||||
}
|
||||
|
||||
if matches!(pipeline.plot_format, PlotFormat::XY) && pipeline.plot_channels != 2
|
||||
{
|
||||
let message = "XY plot requires exactly 2 channels";
|
||||
ui.colored_label(egui::Color32::RED, message);
|
||||
validation_errors.push(format!(
|
||||
"pipeline '{}': {message}",
|
||||
pipeline.name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(pipeline.decoder, DecoderChoice::MixedTextPlot)
|
||||
&& !matches!(pipeline.framer, FramerChoice::MixedTextPlot)
|
||||
{
|
||||
let message = "MixedTextPlot decoder requires MixedTextPlot framer";
|
||||
ui.colored_label(egui::Color32::RED, message);
|
||||
validation_errors.push(format!("pipeline '{}': {message}", pipeline.name));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -275,11 +544,13 @@ pub fn render(ui: &mut Ui, form: &mut ConfigForm, submit_label: &str) -> Option<
|
||||
}
|
||||
|
||||
if ui.button("+ Add Pipeline").clicked() {
|
||||
form.pipelines.push((
|
||||
format!("p{}", form.pipelines.len()),
|
||||
FramerChoice::Line,
|
||||
DecoderChoice::Text,
|
||||
));
|
||||
form.pipelines.push(PipelineForm {
|
||||
name: format!("p{}", form.pipelines.len()),
|
||||
framer: FramerChoice::Line,
|
||||
decoder: DecoderChoice::Text,
|
||||
plot_channels: 1,
|
||||
plot_format: PlotFormat::Interleaved,
|
||||
});
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
@@ -290,8 +561,13 @@ pub fn render(ui: &mut Ui, form: &mut ConfigForm, submit_label: &str) -> Option<
|
||||
ui.checkbox(&mut form.auto_reconnect, "Auto reconnect");
|
||||
|
||||
ui.separator();
|
||||
for error in &validation_errors {
|
||||
ui.colored_label(egui::Color32::RED, error);
|
||||
}
|
||||
if ui.button(submit_label).clicked() {
|
||||
result = Some(form.to_session_config());
|
||||
if validation_errors.is_empty() {
|
||||
result = Some(form.to_session_config());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod config;
|
||||
pub mod console;
|
||||
pub mod hex_view;
|
||||
pub mod plot_view;
|
||||
pub mod sidebar;
|
||||
|
||||
199
crates/xserial-gui/src/panels/plot_view.rs
Normal file
199
crates/xserial-gui/src/panels/plot_view.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use crate::buffers::{PlotBuffer, PlotSeriesKind};
|
||||
use egui::{Button, Ui, vec2};
|
||||
use egui_plot::{Legend, Line, Plot, PlotBounds, PlotPoints};
|
||||
|
||||
pub struct PlotViewState {
|
||||
pub lock_x: bool,
|
||||
pub lock_y: bool,
|
||||
pub box_zoom: bool,
|
||||
pub follow_latest: bool,
|
||||
pub last_bounds: Option<PlotBounds>,
|
||||
}
|
||||
|
||||
impl Default for PlotViewState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lock_x: false,
|
||||
lock_y: false,
|
||||
box_zoom: false,
|
||||
follow_latest: false,
|
||||
last_bounds: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotViewState) {
|
||||
if buf.is_empty() {
|
||||
ui.label("no plot data");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut auto_fit = false;
|
||||
let mut reset_view = false;
|
||||
let mut zoom_x = 1.0_f32;
|
||||
let mut zoom_y = 1.0_f32;
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if ui.button("Auto Fit").clicked() {
|
||||
auto_fit = true;
|
||||
}
|
||||
if ui.button("Reset View").clicked() {
|
||||
reset_view = true;
|
||||
}
|
||||
ui.separator();
|
||||
ui.checkbox(&mut state.lock_x, "Lock X");
|
||||
ui.checkbox(&mut state.lock_y, "Lock Y");
|
||||
ui.checkbox(&mut state.box_zoom, "Box Zoom");
|
||||
if matches!(buf.kind(), PlotSeriesKind::TimeSeries) {
|
||||
ui.checkbox(&mut state.follow_latest, "Follow Latest");
|
||||
} else {
|
||||
state.follow_latest = false;
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.add_enabled(!state.lock_x, Button::new("X-"))
|
||||
.on_hover_text("Zoom out X axis")
|
||||
.clicked()
|
||||
{
|
||||
zoom_x = 0.8;
|
||||
}
|
||||
if ui
|
||||
.add_enabled(!state.lock_x, Button::new("X+"))
|
||||
.on_hover_text("Zoom in X axis")
|
||||
.clicked()
|
||||
{
|
||||
zoom_x = 1.25;
|
||||
}
|
||||
if ui
|
||||
.add_enabled(!state.lock_y, Button::new("Y-"))
|
||||
.on_hover_text("Zoom out Y axis")
|
||||
.clicked()
|
||||
{
|
||||
zoom_y = 0.8;
|
||||
}
|
||||
if ui
|
||||
.add_enabled(!state.lock_y, Button::new("Y+"))
|
||||
.on_hover_text("Zoom in Y axis")
|
||||
.clicked()
|
||||
{
|
||||
zoom_y = 1.25;
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(bounds) = state.last_bounds {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.monospace(format!(
|
||||
"X [{:.3}, {:.3}] Y [{:.3}, {:.3}]",
|
||||
bounds.min()[0],
|
||||
bounds.max()[0],
|
||||
bounds.min()[1],
|
||||
bounds.max()[1],
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
let data_bounds = plot_bounds(buf);
|
||||
let mut plot = Plot::new(format!("plot_view_{session_id}"))
|
||||
.legend(Legend::default())
|
||||
.allow_boxed_zoom(state.box_zoom)
|
||||
.allow_zoom([!state.lock_x, !state.lock_y])
|
||||
.allow_drag([!state.lock_x, !state.lock_y])
|
||||
.allow_axis_zoom_drag([!state.lock_x, !state.lock_y])
|
||||
.allow_scroll([
|
||||
!state.lock_x && matches!(buf.kind(), PlotSeriesKind::TimeSeries),
|
||||
!state.lock_y && matches!(buf.kind(), PlotSeriesKind::TimeSeries),
|
||||
]);
|
||||
if reset_view {
|
||||
plot = plot.reset();
|
||||
}
|
||||
|
||||
plot.show(ui, |plot_ui| {
|
||||
let current_bounds = plot_ui.plot_bounds();
|
||||
state.last_bounds = current_bounds.is_finite().then_some(current_bounds);
|
||||
let pixel_width = plot_ui.response().rect.width().max(64.0);
|
||||
let max_render_points = (pixel_width as usize).saturating_mul(2);
|
||||
|
||||
if auto_fit {
|
||||
if let Some(bounds) = data_bounds {
|
||||
plot_ui.set_auto_bounds(false);
|
||||
plot_ui.set_plot_bounds(bounds);
|
||||
state.last_bounds = Some(bounds);
|
||||
}
|
||||
} else if state.follow_latest && matches!(buf.kind(), PlotSeriesKind::TimeSeries) {
|
||||
if let Some(bounds) = data_bounds {
|
||||
let current_width = current_bounds.width();
|
||||
let data_width = bounds.width();
|
||||
let x_range = if current_width.is_finite() && current_width > 0.0 && data_width > current_width {
|
||||
(bounds.max()[0] - current_width)..=bounds.max()[0]
|
||||
} else {
|
||||
bounds.min()[0]..=bounds.max()[0]
|
||||
};
|
||||
plot_ui.set_auto_bounds([false, plot_ui.auto_bounds().y]);
|
||||
plot_ui.set_plot_bounds_x(x_range.clone());
|
||||
|
||||
let mut followed = current_bounds;
|
||||
followed.set_x(&PlotBounds::from_min_max([*x_range.start(), current_bounds.min()[1]], [*x_range.end(), current_bounds.max()[1]]));
|
||||
state.last_bounds = Some(followed);
|
||||
}
|
||||
} else if zoom_x != 1.0 || zoom_y != 1.0 {
|
||||
let center = plot_ui.plot_bounds().center();
|
||||
plot_ui.zoom_bounds(vec2(zoom_x, zoom_y), center);
|
||||
}
|
||||
|
||||
if let Some(bounds) = data_bounds {
|
||||
if !bounds.is_valid_x() {
|
||||
plot_ui.set_auto_bounds([true, plot_ui.auto_bounds().y]);
|
||||
}
|
||||
if !bounds.is_valid_y() {
|
||||
plot_ui.set_auto_bounds([plot_ui.auto_bounds().x, true]);
|
||||
}
|
||||
}
|
||||
|
||||
for series in buf.iter() {
|
||||
let sampled_points = match buf.kind() {
|
||||
PlotSeriesKind::TimeSeries => {
|
||||
series.render_points_time_series(
|
||||
current_bounds.min()[0],
|
||||
current_bounds.max()[0],
|
||||
max_render_points,
|
||||
)
|
||||
}
|
||||
PlotSeriesKind::XY => series.render_points_xy(max_render_points),
|
||||
};
|
||||
let points = PlotPoints::from_iter(sampled_points);
|
||||
plot_ui.line(Line::new(series.name.clone(), points));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn plot_bounds(buf: &PlotBuffer) -> Option<PlotBounds> {
|
||||
let mut bounds = PlotBounds::NOTHING;
|
||||
|
||||
for series in buf.iter() {
|
||||
for [x, y] in series.points() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
bounds.extend_with(&[x, y].into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !bounds.is_finite() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if bounds.width() <= 0.0 {
|
||||
let center_x = bounds.center().x;
|
||||
bounds.set_x_center_width(center_x, 1.0);
|
||||
} else {
|
||||
bounds.expand_x(bounds.width() * 0.05);
|
||||
}
|
||||
|
||||
if bounds.height() <= 0.0 {
|
||||
let center_y = bounds.center().y;
|
||||
bounds.set_y_center_height(center_y, 1.0);
|
||||
} else {
|
||||
bounds.expand_y(bounds.height() * 0.1);
|
||||
}
|
||||
|
||||
Some(bounds)
|
||||
}
|
||||
Reference in New Issue
Block a user