1use std::fmt;
9use std::mem;
10use std::ops::{Deref, DerefMut};
11use std::path::{Path, PathBuf};
12
13use egui::Color32;
14use egui_wgpu::RenderState;
15
16use crate::core::backend::{
17 Backend, CurveColor, CurveSpec, ImagePixelsSpec, ImageSpec, ItemHandle, MarkerSpec, PickResult,
18 ShapeSpec, TriangleSpec,
19};
20use crate::core::calibration::Calibration;
21use crate::core::colormap::{AutoscaleMode, Colormap, Normalization};
22use crate::core::items::{Baseline, LineStyle, ScalarMask, Symbol};
23use crate::core::marker::{Marker, MarkerConstraint, MarkerKind};
24use crate::core::plot::{DataMargins, DataRange, GraphGrid, Plot, PlotId};
25use crate::core::roi::{ManagedRoi, Roi, RoiInteractionMode, RoiLineStyle};
26use crate::core::scatter_viz::{GridImage, ScatterLineProfile};
27use crate::core::shape::{Shape, ShapeKind};
28use crate::core::sift_align::{
29 AffineTransformation, MatchedKeypoint, SiftAlignment, sift_auto_align,
30};
31use crate::core::transform::{AxisSide, Margins, Scale, YAxis};
32use crate::core::triangles::Triangles;
33use crate::render::backend_wgpu::WgpuBackend;
34use crate::render::gpu_curve::CurveData;
35use crate::render::gpu_image::{AggregationMode, ImageData, ImagePixels, InterpolationMode};
36use crate::render::save::{SaveError, SaveFormat};
37use crate::widget::interaction::{DrawEvent, DrawMode, DrawParams, MouseButton, RoiDrawKind};
38use crate::widget::plot_widget::{PlotInteractionMode, PlotResponse, PlotView};
39use crate::widget::position_info::{
40 SNAP_THRESHOLD_DIST, Snap, SnapItem, SnapItemKind, SnappingMode, snap_to_nearest,
41 snapping_candidates,
42};
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub enum ProfileMode {
50 #[default]
52 None,
53 Horizontal,
55 Vertical,
57 Line,
59 Rectangle,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct MedianFilterParams {
71 pub kernel_width: usize,
73 pub conditional: bool,
76}
77
78impl Default for MedianFilterParams {
79 fn default() -> Self {
80 Self {
82 kernel_width: 3,
83 conditional: false,
84 }
85 }
86}
87
88#[derive(Clone, Debug, PartialEq, Eq)]
90pub enum PlotDataError {
91 ImageDataLength { expected: usize, actual: usize },
93 HistogramLength { bins: usize, edges: usize },
95 ProfileRow { row: u32, height: u32 },
97 ProfileColumn { column: u32, width: u32 },
99}
100
101impl fmt::Display for PlotDataError {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 match self {
104 Self::ImageDataLength { expected, actual } => {
105 write!(
106 f,
107 "image data length {actual} does not match expected {expected}"
108 )
109 }
110 Self::HistogramLength { bins, edges } => {
111 write!(
112 f,
113 "histogram with {bins} bins requires {bins_plus_one} edges",
114 bins_plus_one = bins + 1
115 )?;
116 write!(f, ", got {edges}")
117 }
118 Self::ProfileRow { row, height } => {
119 write!(f, "profile row {row} is outside image height {height}")
120 }
121 Self::ProfileColumn { column, width } => {
122 write!(f, "profile column {column} is outside image width {width}")
123 }
124 }
125 }
126}
127
128impl std::error::Error for PlotDataError {}
129
130#[derive(Clone, Copy, Debug, Default, PartialEq)]
132pub struct ValueStats {
133 pub count: usize,
135 pub finite_count: usize,
137 pub min: Option<f64>,
139 pub max: Option<f64>,
141 pub mean: Option<f64>,
143}
144
145impl ValueStats {
146 pub fn from_f64(values: &[f64]) -> Self {
155 Self::from_stats(crate::core::stats::Stats::for_image(
156 values,
157 values.len(),
158 1,
159 (0.0, 0.0),
160 (1.0, 1.0),
161 crate::core::stats::StatScope::All,
162 ))
163 }
164
165 pub fn from_f32(values: &[f32]) -> Self {
171 let widened: Vec<f64> = values.iter().map(|&v| v as f64).collect();
172 Self::from_f64(&widened)
173 }
174
175 fn from_stats(stats: crate::core::stats::Stats) -> Self {
179 Self {
180 count: stats.count,
181 finite_count: stats.finite_count,
182 min: stats.min,
183 max: stats.max,
184 mean: stats.mean,
185 }
186 }
187}
188
189#[derive(Clone, Copy, Debug, PartialEq)]
191pub struct CurveStats {
192 pub x: ValueStats,
194 pub y: ValueStats,
196 pub y_axis: YAxis,
198}
199
200#[derive(Clone, Copy, Debug, PartialEq)]
202pub struct ImageStats {
203 pub width: u32,
205 pub height: u32,
207 pub pixel_count: usize,
209 pub scalar: Option<ValueStats>,
211}
212
213#[derive(Clone, Copy, Debug, PartialEq)]
215pub struct ImageGeometry {
216 pub origin: (f64, f64),
218 pub scale: (f64, f64),
220 pub alpha: f32,
222}
223
224impl Default for ImageGeometry {
225 fn default() -> Self {
226 Self {
227 origin: (0.0, 0.0),
228 scale: (1.0, 1.0),
229 alpha: 1.0,
230 }
231 }
232}
233
234#[derive(Clone, Copy, Debug, PartialEq)]
236pub enum ItemStats {
237 Curve(CurveStats),
238 Image(ImageStats),
239}
240
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243pub enum PlotItemKind {
244 Curve,
245 Histogram,
246 Scatter,
247 Image,
248 Mask,
249 Triangles,
250 Shape,
251 Marker,
252}
253
254impl PlotItemKind {
255 pub fn as_str(self) -> &'static str {
257 match self {
258 Self::Curve => "curve",
259 Self::Histogram => "histogram",
260 Self::Scatter => "scatter",
261 Self::Image => "image",
262 Self::Mask => "mask",
263 Self::Triangles => "triangles",
264 Self::Shape => "shape",
265 Self::Marker => "marker",
266 }
267 }
268
269 pub fn is_curve_like(self) -> bool {
271 matches!(self, Self::Curve | Self::Histogram | Self::Scatter)
272 }
273
274 pub fn is_image_like(self) -> bool {
276 matches!(self, Self::Image | Self::Mask)
277 }
278}
279
280fn snap_item_kind(kind: PlotItemKind) -> SnapItemKind {
283 match kind {
284 PlotItemKind::Curve => SnapItemKind::Curve,
285 PlotItemKind::Histogram => SnapItemKind::Histogram,
286 PlotItemKind::Scatter => SnapItemKind::Scatter,
287 _ => SnapItemKind::Other,
288 }
289}
290
291#[derive(Clone, Debug, PartialEq)]
293pub enum PlotEvent {
294 ItemAdded {
296 handle: ItemHandle,
297 kind: PlotItemKind,
298 },
299 ItemUpdated {
301 handle: ItemHandle,
302 kind: PlotItemKind,
303 },
304 ItemRemoved {
306 handle: ItemHandle,
307 kind: PlotItemKind,
308 },
309 ActiveItemChanged {
311 previous: Option<ItemHandle>,
312 current: Option<ItemHandle>,
313 },
314 LimitsChanged {
319 x: (f64, f64),
320 y: (f64, f64),
321 y2: Option<(f64, f64)>,
322 },
323 RoiAdded { index: usize },
331 RoiChanged { index: usize },
335 RoiCreated { index: usize },
342 DrawingProgress {
346 mode: DrawMode,
347 points: Vec<(f64, f64)>,
348 },
349 DrawingFinished { mode: DrawMode, params: DrawParams },
354 RoiAboutToBeRemoved { index: usize },
359 RoisCleared,
363 CurrentRoiChanged {
367 previous: Option<usize>,
368 current: Option<usize>,
369 },
370 RoiInteractionModeChanged {
375 index: usize,
376 mode: RoiInteractionMode,
377 },
378 MarkerMoved { handle: ItemHandle },
383 MarkerDragStarted { handle: ItemHandle },
388 MarkerDragFinished { handle: ItemHandle },
392 CurveClicked {
396 handle: ItemHandle,
397 index: usize,
398 x: f64,
399 y: f64,
400 button: MouseButton,
401 },
402 ImageClicked {
405 handle: ItemHandle,
406 col: u32,
407 row: u32,
408 button: MouseButton,
409 },
410 ItemClicked {
414 handle: ItemHandle,
415 button: MouseButton,
416 },
417 ItemHovered {
425 handle: ItemHandle,
426 kind: PlotItemKind,
427 label: Option<String>,
428 x: f64,
429 y: f64,
430 xpixel: f32,
431 ypixel: f32,
432 draggable: bool,
433 },
434}
435
436#[derive(Clone, Copy, Debug, PartialEq, Eq)]
441pub enum LegendAction {
442 SetActive,
444 MapToLeft,
446 MapToRight,
448 TogglePoints,
450 ToggleLines,
452 Remove,
454 Rename,
456}
457
458#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
460pub struct LegendResponse {
461 pub selected: Option<ItemHandle>,
463 pub activated: Option<ItemHandle>,
465 pub visibility_changed: Option<ItemHandle>,
467 pub context_action: Option<(ItemHandle, LegendAction)>,
471}
472
473#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
475pub struct ToolbarResponse {
476 pub reset_zoom: bool,
478 pub interaction_mode_changed: bool,
480 pub cursor_changed: bool,
482 pub grid_changed: bool,
484 pub minor_grid_changed: bool,
486 pub aspect_changed: bool,
488 pub x_log_changed: bool,
490 pub y_log_changed: bool,
492 pub autoscale_x_changed: bool,
494 pub autoscale_y_changed: bool,
496 pub x_inverted_changed: bool,
498 pub y_inverted_changed: bool,
500 pub show_axis_changed: bool,
502 pub curve_style_changed: bool,
504 pub zoom_in: bool,
506 pub zoom_out: bool,
508 pub zoom_back: bool,
510 pub zoom_axes_changed: bool,
512 pub save: bool,
514 pub copy: bool,
516 pub print: bool,
518}
519
520pub struct PlotWithToolbarResponse {
522 pub toolbar: ToolbarResponse,
524 pub plot: PlotResponse,
526}
527
528pub type PlotWindow = PlotWidget;
534
535const DEFAULT_SAVE_SIZE: (u32, u32) = (1024, 768);
539
540const DEFAULT_SAVE_DPI: u32 = 96;
545
546fn ordered_limits(a: f64, b: f64) -> (f64, f64) {
551 if a <= b { (a, b) } else { (b, a) }
552}
553
554#[derive(Clone, Copy, Debug, PartialEq, Eq)]
555enum ToolbarIcon {
556 Home,
557 Select,
558 Pan,
559 Zoom,
560 Cursor,
561 Grid,
562 MinorGrid,
563 Aspect,
564 LogX,
565 LogY,
566 InvertX,
567 InvertY,
568 ShowAxis,
569 CurveStyle,
570 ZoomIn,
571 ZoomOut,
572 ZoomBack,
573 Save,
574 Copy,
575 Print,
576 AutoscaleX,
577 AutoscaleY,
578 MedianFilter,
579 PixelHistogram,
580}
581
582impl ToolbarIcon {
583 fn size(self) -> egui::Vec2 {
584 match self {
585 Self::LogX | Self::LogY => egui::vec2(34.0, 24.0),
586 _ => egui::vec2(28.0, 24.0),
587 }
588 }
589}
590
591fn expected_image_len(width: u32, height: u32) -> usize {
592 (width as usize).saturating_mul(height as usize)
593}
594
595fn force_odd(n: usize) -> usize {
598 if n <= 1 {
599 1
600 } else if n % 2 == 1 {
601 n
602 } else {
603 n + 1
604 }
605}
606
607fn print_temp_png_path(dir: &Path, pid: u32) -> PathBuf {
612 dir.join(format!("siplot-print-{pid}.png"))
613}
614
615fn validate_image_len(width: u32, height: u32, actual: usize) -> Result<usize, PlotDataError> {
616 let expected = expected_image_len(width, height);
617 if actual == expected {
618 Ok(expected)
619 } else {
620 Err(PlotDataError::ImageDataLength { expected, actual })
621 }
622}
623
624fn toolbar_icon_button(
625 ui: &mut egui::Ui,
626 icon: ToolbarIcon,
627 selected: bool,
628 tooltip: &str,
629) -> egui::Response {
630 let (rect, response) = ui.allocate_exact_size(icon.size(), egui::Sense::click());
631 let response = response.on_hover_text(tooltip);
632 if ui.is_rect_visible(rect) {
633 draw_toolbar_button(ui, rect, &response, selected, icon);
634 }
635 response
636}
637
638fn draw_toolbar_button(
639 ui: &egui::Ui,
640 rect: egui::Rect,
641 response: &egui::Response,
642 selected: bool,
643 icon: ToolbarIcon,
644) {
645 let visuals = ui.style().interact_selectable(response, selected);
646 let painter = ui.painter();
647 let button_rect = rect.shrink(1.0);
648 if selected || response.hovered() || response.has_focus() {
649 painter.rect_filled(button_rect, 2.0, visuals.weak_bg_fill);
650 painter.rect_stroke(
651 button_rect,
652 2.0,
653 visuals.bg_stroke,
654 egui::StrokeKind::Inside,
655 );
656 }
657
658 let color = if !ui.is_enabled() {
659 ui.visuals().weak_text_color()
660 } else if selected {
661 ui.visuals().selection.stroke.color
662 } else {
663 visuals.fg_stroke.color
664 };
665 draw_toolbar_icon(painter, rect.shrink(5.0), icon, color);
666}
667
668fn draw_toolbar_icon(painter: &egui::Painter, rect: egui::Rect, icon: ToolbarIcon, color: Color32) {
669 let stroke = egui::Stroke::new(1.6, color);
670 match icon {
671 ToolbarIcon::Home => draw_home_icon(painter, rect, stroke),
672 ToolbarIcon::Select => draw_select_icon(painter, rect, stroke),
673 ToolbarIcon::Pan => draw_pan_icon(painter, rect, stroke),
674 ToolbarIcon::Zoom => draw_zoom_icon(painter, rect, stroke),
675 ToolbarIcon::Cursor => draw_cursor_icon(painter, rect, stroke),
676 ToolbarIcon::Grid => draw_grid_icon(painter, rect, stroke, 3),
677 ToolbarIcon::MinorGrid => draw_grid_icon(painter, rect, stroke, 4),
678 ToolbarIcon::Aspect => draw_center_text(painter, rect, "1:1", 11.0, color),
679 ToolbarIcon::LogX => draw_log_icon(painter, rect, false, stroke),
680 ToolbarIcon::LogY => draw_log_icon(painter, rect, true, stroke),
681 ToolbarIcon::InvertX => draw_axis_icon(painter, rect, "X", false, stroke),
682 ToolbarIcon::InvertY => draw_axis_icon(painter, rect, "Y", true, stroke),
683 ToolbarIcon::ShowAxis => draw_show_axis_icon(painter, rect, stroke),
684 ToolbarIcon::CurveStyle => draw_curve_style_icon(painter, rect, stroke),
685 ToolbarIcon::ZoomIn => draw_zoom_step_icon(painter, rect, stroke, true),
686 ToolbarIcon::ZoomOut => draw_zoom_step_icon(painter, rect, stroke, false),
687 ToolbarIcon::ZoomBack => draw_zoom_back_icon(painter, rect, stroke),
688 ToolbarIcon::Save => draw_save_icon(painter, rect, stroke),
689 ToolbarIcon::Copy => draw_copy_icon(painter, rect, stroke),
690 ToolbarIcon::Print => draw_print_icon(painter, rect, stroke),
691 ToolbarIcon::AutoscaleX => draw_autoscale_icon(painter, rect, "X", false, stroke),
692 ToolbarIcon::AutoscaleY => draw_autoscale_icon(painter, rect, "Y", true, stroke),
693 ToolbarIcon::MedianFilter => draw_median_filter_icon(painter, rect, stroke),
694 ToolbarIcon::PixelHistogram => draw_pixel_histogram_icon(painter, rect, stroke),
695 }
696}
697
698fn draw_pixel_histogram_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
701 let base = rect.bottom();
702 let bar_w = rect.width() / 4.0;
703 let heights = [0.4_f32, 0.75, 1.0];
704 for (i, h) in heights.iter().enumerate() {
705 let x = rect.left() + bar_w * (i as f32 * 1.2 + 0.2);
706 let top = base - rect.height() * h;
707 let bar = egui::Rect::from_min_max(egui::pos2(x, top), egui::pos2(x + bar_w * 0.8, base));
708 painter.rect_stroke(bar, 0.0, stroke, egui::StrokeKind::Inside);
709 }
710}
711
712fn draw_median_filter_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
716 let grid = rect.shrink(1.0);
717 let third_x = grid.width() / 3.0;
718 let third_y = grid.height() / 3.0;
719 painter.rect_stroke(grid, 0.0, stroke, egui::StrokeKind::Inside);
721 for i in 1..3 {
722 let x = grid.left() + third_x * i as f32;
723 painter.line_segment(
724 [egui::pos2(x, grid.top()), egui::pos2(x, grid.bottom())],
725 stroke,
726 );
727 let y = grid.top() + third_y * i as f32;
728 painter.line_segment(
729 [egui::pos2(grid.left(), y), egui::pos2(grid.right(), y)],
730 stroke,
731 );
732 }
733 let center = egui::Rect::from_min_size(
735 egui::pos2(grid.left() + third_x, grid.top() + third_y),
736 egui::vec2(third_x, third_y),
737 );
738 painter.rect_filled(center.shrink(1.0), 0.0, stroke.color);
739}
740
741fn draw_axis_arrow_icon(
754 painter: &egui::Painter,
755 rect: egui::Rect,
756 label: &str,
757 vertical: bool,
758 inward: bool,
759 font_size: f32,
760 stroke: egui::Stroke,
761) {
762 let a = 2.0;
766 let font = egui::FontId::proportional(font_size);
767 if vertical {
768 let x = rect.left() + 2.5;
770 let (ytop, ybot) = (rect.top() + 1.5, rect.bottom() - 1.5);
771 painter.line_segment([egui::pos2(x, ytop), egui::pos2(x, ybot)], stroke);
772 if inward {
773 painter.line_segment([egui::pos2(x, ytop + a), egui::pos2(x - a, ytop)], stroke);
775 painter.line_segment([egui::pos2(x, ytop + a), egui::pos2(x + a, ytop)], stroke);
776 painter.line_segment([egui::pos2(x, ybot - a), egui::pos2(x - a, ybot)], stroke);
777 painter.line_segment([egui::pos2(x, ybot - a), egui::pos2(x + a, ybot)], stroke);
778 } else {
779 painter.line_segment([egui::pos2(x, ytop), egui::pos2(x - a, ytop + a)], stroke);
781 painter.line_segment([egui::pos2(x, ytop), egui::pos2(x + a, ytop + a)], stroke);
782 painter.line_segment([egui::pos2(x, ybot), egui::pos2(x - a, ybot - a)], stroke);
783 painter.line_segment([egui::pos2(x, ybot), egui::pos2(x + a, ybot - a)], stroke);
784 }
785 let lx = (x + a + rect.right()) * 0.5;
787 painter.text(
788 egui::pos2(lx, rect.center().y),
789 egui::Align2::CENTER_CENTER,
790 label,
791 font,
792 stroke.color,
793 );
794 } else {
795 let y = rect.bottom() - 2.0;
797 let (xl, xr) = (rect.left() + 1.5, rect.right() - 1.5);
798 painter.line_segment([egui::pos2(xl, y), egui::pos2(xr, y)], stroke);
799 if inward {
800 painter.line_segment([egui::pos2(xl + a, y), egui::pos2(xl, y - a)], stroke);
801 painter.line_segment([egui::pos2(xl + a, y), egui::pos2(xl, y + a)], stroke);
802 painter.line_segment([egui::pos2(xr - a, y), egui::pos2(xr, y - a)], stroke);
803 painter.line_segment([egui::pos2(xr - a, y), egui::pos2(xr, y + a)], stroke);
804 } else {
805 painter.line_segment([egui::pos2(xl, y), egui::pos2(xl + a, y - a)], stroke);
806 painter.line_segment([egui::pos2(xl, y), egui::pos2(xl + a, y + a)], stroke);
807 painter.line_segment([egui::pos2(xr, y), egui::pos2(xr - a, y - a)], stroke);
808 painter.line_segment([egui::pos2(xr, y), egui::pos2(xr - a, y + a)], stroke);
809 }
810 let ly = (rect.top() + (y - a)) * 0.5;
812 painter.text(
813 egui::pos2(rect.center().x, ly),
814 egui::Align2::CENTER_CENTER,
815 label,
816 font,
817 stroke.color,
818 );
819 }
820}
821
822fn draw_autoscale_icon(
826 painter: &egui::Painter,
827 rect: egui::Rect,
828 axis: &str,
829 vertical: bool,
830 stroke: egui::Stroke,
831) {
832 draw_axis_arrow_icon(painter, rect, axis, vertical, false, 11.0, stroke);
833}
834
835fn draw_copy_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
837 let back = egui::Rect::from_min_max(
838 egui::pos2(rect.left() + 2.0, rect.top() + 2.0),
839 egui::pos2(rect.right() - 5.0, rect.bottom() - 5.0),
840 );
841 let front = egui::Rect::from_min_max(
842 egui::pos2(rect.left() + 5.0, rect.top() + 5.0),
843 egui::pos2(rect.right() - 2.0, rect.bottom() - 2.0),
844 );
845 painter.rect_stroke(back, 1.0, stroke, egui::StrokeKind::Inside);
846 painter.rect_stroke(front, 1.0, stroke, egui::StrokeKind::Inside);
847}
848
849fn draw_save_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
851 let body = egui::Rect::from_min_max(
852 egui::pos2(rect.left() + 2.0, rect.top() + 2.0),
853 egui::pos2(rect.right() - 2.0, rect.bottom() - 2.0),
854 );
855 painter.rect_stroke(body, 1.0, stroke, egui::StrokeKind::Inside);
856 let label = egui::Rect::from_min_max(
858 egui::pos2(body.left() + 3.0, body.top()),
859 egui::pos2(body.right() - 3.0, body.top() + body.height() * 0.35),
860 );
861 painter.rect_stroke(label, 0.0, stroke, egui::StrokeKind::Inside);
862 let notch = egui::Rect::from_min_max(
864 egui::pos2(body.right() - 6.0, body.top() + 1.0),
865 egui::pos2(body.right() - 3.0, body.top() + body.height() * 0.3),
866 );
867 painter.rect_filled(notch, 0.0, stroke.color);
868}
869
870fn draw_print_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
873 let cx_left = rect.left() + 5.0;
874 let cx_right = rect.right() - 5.0;
875 let feed = egui::Rect::from_min_max(
877 egui::pos2(cx_left + 2.0, rect.top() + 2.0),
878 egui::pos2(cx_right - 2.0, rect.top() + rect.height() * 0.32),
879 );
880 painter.rect_stroke(feed, 0.0, stroke, egui::StrokeKind::Inside);
881 let body = egui::Rect::from_min_max(
883 egui::pos2(cx_left, rect.top() + rect.height() * 0.34),
884 egui::pos2(cx_right, rect.bottom() - rect.height() * 0.18),
885 );
886 painter.rect_stroke(body, 1.0, stroke, egui::StrokeKind::Inside);
887 let tray = egui::Rect::from_min_max(
889 egui::pos2(cx_left + 2.0, rect.bottom() - rect.height() * 0.34),
890 egui::pos2(cx_right - 2.0, rect.bottom() - 2.0),
891 );
892 painter.rect_stroke(tray, 0.0, stroke, egui::StrokeKind::Inside);
893}
894
895fn draw_zoom_back_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
897 let c = rect.center();
898 let left = egui::pos2(rect.left() + 2.0, c.y);
899 let right = egui::pos2(rect.right() - 2.0, c.y);
900 painter.line_segment([left, right], stroke);
901 let arrow = 4.0;
902 painter.line_segment([left, left + egui::vec2(arrow, -arrow)], stroke);
903 painter.line_segment([left, left + egui::vec2(arrow, arrow)], stroke);
904}
905
906fn draw_zoom_step_icon(
909 painter: &egui::Painter,
910 rect: egui::Rect,
911 stroke: egui::Stroke,
912 plus: bool,
913) {
914 let radius = rect.width().min(rect.height()) * 0.28;
915 let center = egui::pos2(rect.left() + radius + 2.0, rect.top() + radius + 2.0);
916 painter.circle_stroke(center, radius, stroke);
917 painter.line_segment(
918 [
919 center + egui::vec2(radius * 0.7, radius * 0.7),
920 egui::pos2(rect.right() - 2.0, rect.bottom() - 2.0),
921 ],
922 stroke,
923 );
924 let bar = radius * 0.55;
925 painter.line_segment(
927 [center - egui::vec2(bar, 0.0), center + egui::vec2(bar, 0.0)],
928 stroke,
929 );
930 if plus {
931 painter.line_segment(
933 [center - egui::vec2(0.0, bar), center + egui::vec2(0.0, bar)],
934 stroke,
935 );
936 }
937}
938
939fn draw_curve_style_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
942 let y_top = rect.top() + rect.height() * 0.35;
943 let y_bot = rect.top() + rect.height() * 0.65;
944 let left = rect.left() + 1.0;
945 let right = rect.right() - 1.0;
946 let mut x = left;
948 while x < right {
949 let seg_end = (x + 3.0).min(right);
950 painter.line_segment([egui::pos2(x, y_top), egui::pos2(seg_end, y_top)], stroke);
951 x += 5.0;
952 }
953 let mut x = left;
955 while x < right {
956 painter.line_segment([egui::pos2(x, y_bot), egui::pos2(x + 1.0, y_bot)], stroke);
957 x += 3.0;
958 }
959}
960
961fn draw_show_axis_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
964 let origin = egui::pos2(rect.left() + 3.0, rect.bottom() - 3.0);
965 let x_end = egui::pos2(rect.right() - 1.0, rect.bottom() - 3.0);
966 let y_end = egui::pos2(rect.left() + 3.0, rect.top() + 1.0);
967 painter.line_segment([origin, x_end], stroke);
968 painter.line_segment([origin, y_end], stroke);
969 let arrow = 3.0;
970 painter.line_segment([x_end, x_end + egui::vec2(-arrow, -arrow)], stroke);
972 painter.line_segment([x_end, x_end + egui::vec2(-arrow, arrow)], stroke);
973 painter.line_segment([y_end, y_end + egui::vec2(-arrow, arrow)], stroke);
975 painter.line_segment([y_end, y_end + egui::vec2(arrow, arrow)], stroke);
976}
977
978fn draw_home_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
979 let top = egui::pos2(rect.center().x, rect.top());
980 let left_roof = egui::pos2(rect.left(), rect.center().y - 1.0);
981 let right_roof = egui::pos2(rect.right(), rect.center().y - 1.0);
982 painter.line_segment([left_roof, top], stroke);
983 painter.line_segment([top, right_roof], stroke);
984 let house = egui::Rect::from_min_max(
985 egui::pos2(rect.left() + 3.0, rect.center().y - 1.0),
986 egui::pos2(rect.right() - 3.0, rect.bottom()),
987 );
988 painter.rect_stroke(house, 1.0, stroke, egui::StrokeKind::Inside);
989}
990
991fn draw_select_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
992 let points = vec![
993 egui::pos2(rect.left() + 2.0, rect.top() + 1.0),
994 egui::pos2(rect.left() + 2.0, rect.bottom() - 2.0),
995 egui::pos2(rect.left() + 7.0, rect.bottom() - 6.0),
996 egui::pos2(rect.left() + 10.0, rect.bottom() - 1.0),
997 egui::pos2(rect.left() + 13.0, rect.bottom() - 2.5),
998 egui::pos2(rect.left() + 10.0, rect.bottom() - 7.0),
999 egui::pos2(rect.right() - 2.0, rect.bottom() - 7.0),
1000 ];
1001 painter.add(egui::Shape::convex_polygon(
1002 points,
1003 Color32::TRANSPARENT,
1004 stroke,
1005 ));
1006}
1007
1008fn draw_pan_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
1009 let c = rect.center();
1010 let arrow = 3.0;
1011 painter.line_segment(
1012 [egui::pos2(rect.left(), c.y), egui::pos2(rect.right(), c.y)],
1013 stroke,
1014 );
1015 painter.line_segment(
1016 [egui::pos2(c.x, rect.top()), egui::pos2(c.x, rect.bottom())],
1017 stroke,
1018 );
1019 for (tip, a, b) in [
1020 (
1021 egui::pos2(rect.left(), c.y),
1022 egui::pos2(rect.left() + arrow, c.y - arrow),
1023 egui::pos2(rect.left() + arrow, c.y + arrow),
1024 ),
1025 (
1026 egui::pos2(rect.right(), c.y),
1027 egui::pos2(rect.right() - arrow, c.y - arrow),
1028 egui::pos2(rect.right() - arrow, c.y + arrow),
1029 ),
1030 (
1031 egui::pos2(c.x, rect.top()),
1032 egui::pos2(c.x - arrow, rect.top() + arrow),
1033 egui::pos2(c.x + arrow, rect.top() + arrow),
1034 ),
1035 (
1036 egui::pos2(c.x, rect.bottom()),
1037 egui::pos2(c.x - arrow, rect.bottom() - arrow),
1038 egui::pos2(c.x + arrow, rect.bottom() - arrow),
1039 ),
1040 ] {
1041 painter.line_segment([tip, a], stroke);
1042 painter.line_segment([tip, b], stroke);
1043 }
1044}
1045
1046fn draw_zoom_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
1047 let radius = rect.width().min(rect.height()) * 0.28;
1048 let center = egui::pos2(rect.left() + radius + 2.0, rect.top() + radius + 2.0);
1049 painter.circle_stroke(center, radius, stroke);
1050 painter.line_segment(
1051 [
1052 center + egui::vec2(radius * 0.7, radius * 0.7),
1053 egui::pos2(rect.right() - 2.0, rect.bottom() - 2.0),
1054 ],
1055 stroke,
1056 );
1057}
1058
1059fn draw_cursor_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
1060 let center = rect.center();
1061 painter.line_segment(
1062 [
1063 egui::pos2(rect.left(), center.y),
1064 egui::pos2(rect.right(), center.y),
1065 ],
1066 stroke,
1067 );
1068 painter.line_segment(
1069 [
1070 egui::pos2(center.x, rect.top()),
1071 egui::pos2(center.x, rect.bottom()),
1072 ],
1073 stroke,
1074 );
1075 painter.circle_stroke(center, rect.width().min(rect.height()) * 0.28, stroke);
1076}
1077
1078fn draw_grid_icon(
1079 painter: &egui::Painter,
1080 rect: egui::Rect,
1081 stroke: egui::Stroke,
1082 divisions: usize,
1083) {
1084 painter.rect_stroke(rect, 0.0, stroke, egui::StrokeKind::Inside);
1085 for index in 1..divisions {
1086 let t = index as f32 / divisions as f32;
1087 let x = egui::lerp(rect.left()..=rect.right(), t);
1088 let y = egui::lerp(rect.top()..=rect.bottom(), t);
1089 painter.line_segment(
1090 [egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())],
1091 stroke,
1092 );
1093 painter.line_segment(
1094 [egui::pos2(rect.left(), y), egui::pos2(rect.right(), y)],
1095 stroke,
1096 );
1097 }
1098}
1099
1100fn draw_log_icon(painter: &egui::Painter, rect: egui::Rect, vertical: bool, stroke: egui::Stroke) {
1106 draw_axis_arrow_icon(painter, rect, "log", vertical, false, 9.0, stroke);
1107}
1108
1109fn draw_axis_icon(
1114 painter: &egui::Painter,
1115 rect: egui::Rect,
1116 axis: &str,
1117 vertical: bool,
1118 stroke: egui::Stroke,
1119) {
1120 draw_axis_arrow_icon(painter, rect, axis, vertical, true, 11.0, stroke);
1121}
1122
1123fn draw_center_text(
1124 painter: &egui::Painter,
1125 rect: egui::Rect,
1126 text: &str,
1127 size: f32,
1128 color: Color32,
1129) {
1130 painter.text(
1131 rect.center(),
1132 egui::Align2::CENTER_CENTER,
1133 text,
1134 egui::FontId::proportional(size),
1135 color,
1136 );
1137}
1138
1139fn mask_rgba_pixels(mask: &[bool], color: Color32) -> Vec<[u8; 4]> {
1140 let rgba = color.to_srgba_unmultiplied();
1141 mask.iter()
1142 .map(|masked| if *masked { rgba } else { [0, 0, 0, 0] })
1143 .collect()
1144}
1145
1146pub fn histogram_step_values(
1148 edges: &[f64],
1149 counts: &[f64],
1150) -> Result<(Vec<f64>, Vec<f64>), PlotDataError> {
1151 if edges.len() != counts.len() + 1 {
1152 return Err(PlotDataError::HistogramLength {
1153 bins: counts.len(),
1154 edges: edges.len(),
1155 });
1156 }
1157 if counts.is_empty() {
1158 return Ok((Vec::new(), Vec::new()));
1159 }
1160
1161 let mut x = Vec::with_capacity(counts.len() * 2 + 2);
1162 let mut y = Vec::with_capacity(counts.len() * 2 + 2);
1163 x.push(edges[0]);
1164 y.push(0.0);
1165 for (index, count) in counts.iter().copied().enumerate() {
1166 x.push(edges[index]);
1167 y.push(count);
1168 x.push(edges[index + 1]);
1169 y.push(count);
1170 }
1171 x.push(edges[counts.len()]);
1172 y.push(0.0);
1173 Ok((x, y))
1174}
1175
1176#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1180pub enum HistogramAlign {
1181 Left,
1185 #[default]
1188 Center,
1189 Right,
1192}
1193
1194pub fn histogram_edges(positions: &[f64], align: HistogramAlign) -> Vec<f64> {
1211 if positions.is_empty() {
1212 return Vec::new();
1213 }
1214 let n = positions.len();
1215 match align {
1216 HistogramAlign::Left => {
1217 let width = if n > 1 {
1220 positions[n - 1] - positions[n - 2]
1221 } else {
1222 1.0
1223 };
1224 let mut edges = positions.to_vec();
1225 edges.push(positions[n - 1] + width);
1226 edges
1227 }
1228 HistogramAlign::Right => {
1229 let width = if n > 1 {
1232 positions[1] - positions[0]
1233 } else {
1234 1.0
1235 };
1236 let mut edges = Vec::with_capacity(n + 1);
1237 edges.push(positions[0] - width);
1238 edges.extend_from_slice(positions);
1239 edges
1240 }
1241 HistogramAlign::Center => {
1242 let right = histogram_edges(positions, HistogramAlign::Left);
1245 let mut widths: Vec<f64> = right.windows(2).map(|w| (w[1] - w[0]) / 2.0).collect();
1246 if let Some(&last) = widths.last() {
1247 widths.push(last);
1248 }
1249 right
1250 .iter()
1251 .zip(widths.iter())
1252 .map(|(&edge, &width)| edge - width)
1253 .collect()
1254 }
1255 }
1256}
1257
1258pub fn pick_histogram(
1273 edges: &[f64],
1274 values: &[f64],
1275 baseline: f64,
1276 x_data: f64,
1277 y_data: f64,
1278) -> Option<usize> {
1279 if values.is_empty() || edges.len() != values.len() + 1 {
1280 return None;
1281 }
1282
1283 let xmin = edges[0];
1286 let xmax = edges[edges.len() - 1];
1287 let mut vmin = f64::INFINITY;
1288 let mut vmax = f64::NEG_INFINITY;
1289 for &v in values {
1290 if v.is_finite() {
1291 vmin = vmin.min(v);
1292 vmax = vmax.max(v);
1293 }
1294 }
1295 if !vmin.is_finite() {
1296 return None;
1298 }
1299 let ymin = vmin.min(0.0);
1300 let ymax = vmax.max(0.0);
1301 if x_data <= xmin || x_data >= xmax || y_data <= ymin || y_data >= ymax {
1302 return None;
1303 }
1304
1305 let index = edges
1308 .partition_point(|&e| e < x_data)
1309 .saturating_sub(1)
1310 .min(values.len() - 1);
1311
1312 let value = values[index];
1313 let hit = (baseline <= value && baseline <= y_data && y_data <= value)
1314 || (value < baseline && value <= y_data && y_data <= baseline);
1315 if hit { Some(index) } else { None }
1316}
1317
1318pub fn horizontal_profile_values(
1320 width: u32,
1321 height: u32,
1322 data: &[f32],
1323 row: u32,
1324) -> Result<Vec<f64>, PlotDataError> {
1325 validate_image_len(width, height, data.len())?;
1326 if row >= height {
1327 return Err(PlotDataError::ProfileRow { row, height });
1328 }
1329 let width = width as usize;
1330 let start = row as usize * width;
1331 Ok(data[start..start + width]
1332 .iter()
1333 .map(|value| *value as f64)
1334 .collect())
1335}
1336
1337pub fn vertical_profile_values(
1339 width: u32,
1340 height: u32,
1341 data: &[f32],
1342 column: u32,
1343) -> Result<Vec<f64>, PlotDataError> {
1344 validate_image_len(width, height, data.len())?;
1345 if column >= width {
1346 return Err(PlotDataError::ProfileColumn { column, width });
1347 }
1348 let width = width as usize;
1349 let column = column as usize;
1350 Ok((0..height as usize)
1351 .map(|row| data[row * width + column] as f64)
1352 .collect())
1353}
1354
1355#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1359pub enum ProfileMethod {
1360 #[default]
1362 Mean,
1363 Sum,
1365}
1366
1367pub fn aligned_profile_values(
1386 width: u32,
1387 height: u32,
1388 data: &[f32],
1389 position: f64,
1390 roi_width: u32,
1391 horizontal: bool,
1392 method: ProfileMethod,
1393) -> Result<Vec<f64>, PlotDataError> {
1394 validate_image_len(width, height, data.len())?;
1395 let w = width as usize;
1396 let h = height as usize;
1397 if w == 0 || h == 0 {
1398 return Ok(Vec::new());
1399 }
1400
1401 let (band_dim, profile_len) = if horizontal { (h, w) } else { (w, h) };
1403
1404 let band = (roi_width.max(1) as usize).min(band_dim);
1406
1407 let start_f = position.trunc() + 0.5 - band as f64 / 2.0;
1410 let start = (start_f.trunc() as i64).clamp(0, (band_dim - band) as i64) as usize;
1411 let end = start + band;
1412 let denom = band as f64;
1413
1414 let profile = (0..profile_len)
1415 .map(|p| {
1416 let acc: f64 = (start..end)
1417 .map(|b| {
1418 let idx = if horizontal { b * w + p } else { p * w + b };
1420 data[idx] as f64
1421 })
1422 .sum();
1423 match method {
1424 ProfileMethod::Mean => acc / denom,
1425 ProfileMethod::Sum => acc,
1426 }
1427 })
1428 .collect();
1429 Ok(profile)
1430}
1431
1432pub fn line_profile_values(
1435 width: u32,
1436 height: u32,
1437 data: &[f32],
1438 start: (f64, f64),
1439 end: (f64, f64),
1440) -> Result<(Vec<f64>, Vec<f64>), PlotDataError> {
1441 validate_image_len(width, height, data.len())?;
1442 let (x0, y0) = start;
1443 let (x1, y1) = end;
1444 let dx = x1 - x0;
1445 let dy = y1 - y0;
1446 let dist = (dx * dx + dy * dy).sqrt();
1447 let n_points = dist.ceil() as usize + 1;
1448
1449 let mut x_vals = Vec::with_capacity(n_points);
1450 let mut y_vals = Vec::with_capacity(n_points);
1451
1452 let w = width as i64;
1453 let h = height as i64;
1454
1455 for i in 0..n_points {
1456 let t = if n_points > 1 {
1457 i as f64 / (n_points - 1) as f64
1458 } else {
1459 0.0
1460 };
1461 let x = x0 + t * dx;
1462 let y = y0 + t * dy;
1463 let col = x.round() as i64;
1464 let row = y.round() as i64;
1465
1466 let val = if col >= 0 && col < w && row >= 0 && row < h {
1467 data[(row as usize) * (width as usize) + (col as usize)] as f64
1468 } else {
1469 f64::NAN
1470 };
1471 x_vals.push(t * dist);
1472 y_vals.push(val);
1473 }
1474
1475 Ok((x_vals, y_vals))
1476}
1477
1478fn bilinear_sample(width: usize, height: usize, data: &[f32], col: f64, row: f64) -> f64 {
1484 let d0 = row.clamp(0.0, height as f64 - 1.0);
1486 let d1 = col.clamp(0.0, width as f64 - 1.0);
1487 let r0 = d0.floor();
1488 let r1 = d0.ceil();
1489 let c0 = d1.floor();
1490 let c1 = d1.ceil();
1491 let (i0, i1) = (r0 as usize, r1 as usize); let (j0, j1) = (c0 as usize, c1 as usize); let at = |i: usize, j: usize| data[i * width + j] as f64;
1494 if i0 == i1 && j0 == j1 {
1495 at(i0, j0)
1496 } else if i0 == i1 {
1497 at(i0, j0) * (c1 - d1) + at(i0, j1) * (d1 - c0)
1499 } else if j0 == j1 {
1500 at(i0, j0) * (r1 - d0) + at(i1, j0) * (d0 - r0)
1502 } else {
1503 at(i0, j0) * (r1 - d0) * (c1 - d1)
1505 + at(i1, j0) * (d0 - r0) * (c1 - d1)
1506 + at(i0, j1) * (r1 - d0) * (d1 - c0)
1507 + at(i1, j1) * (d0 - r0) * (d1 - c0)
1508 }
1509}
1510
1511pub fn line_profile_band(
1527 width: u32,
1528 height: u32,
1529 data: &[f32],
1530 start: (f64, f64),
1531 end: (f64, f64),
1532 linewidth: u32,
1533 method: ProfileMethod,
1534) -> Result<(Vec<f64>, Vec<f64>), PlotDataError> {
1535 validate_image_len(width, height, data.len())?;
1536 let w = width as usize;
1537 let h = height as usize;
1538 let (src_col0, src_row0) = start;
1539 let (dst_col, dst_row) = end;
1540 if src_row0 == dst_row && src_col0 == dst_col {
1542 return Ok((
1543 vec![0.0],
1544 vec![bilinear_sample(w, h, data, src_col0, src_row0)],
1545 ));
1546 }
1547 let lw = linewidth.max(1) as usize;
1548 let d_row = dst_row - src_row0;
1549 let d_col = dst_col - src_col0;
1550 let length = (d_row * d_row + d_col * d_col).sqrt();
1551 let row_width = d_col / length;
1553 let col_width = -d_row / length;
1554 let count = (length + 1.0).ceil() as usize; let denom = (count - 1) as f64; let step_row = d_row / denom;
1557 let step_col = d_col / denom;
1558 let src_row = src_row0 - row_width * (lw as f64 - 1.0) / 2.0;
1561 let src_col = src_col0 - col_width * (lw as f64 - 1.0) / 2.0;
1562
1563 let mut x_vals = Vec::with_capacity(count);
1564 let mut y_vals = Vec::with_capacity(count);
1565 for i in 0..count {
1566 let row = src_row + i as f64 * step_row;
1567 let col = src_col + i as f64 * step_col;
1568 let mut sum = 0.0;
1569 let mut cnt = 0usize;
1570 for j in 0..lw {
1571 let nr = row + j as f64 * row_width;
1572 let nc = col + j as f64 * col_width;
1573 if nc >= 0.0 && nc < width as f64 && nr >= 0.0 && nr < height as f64 {
1576 let val = bilinear_sample(w, h, data, nc, nr);
1577 if val.is_finite() {
1578 cnt += 1;
1579 sum += val;
1580 }
1581 }
1582 }
1583 let value = match (cnt > 0, method) {
1584 (true, ProfileMethod::Mean) => sum / cnt as f64,
1585 (true, ProfileMethod::Sum) => sum,
1586 (false, ProfileMethod::Mean) => f64::NAN,
1587 (false, ProfileMethod::Sum) => 0.0,
1588 };
1589 x_vals.push(i as f64 / denom * length);
1590 y_vals.push(value);
1591 }
1592 Ok((x_vals, y_vals))
1593}
1594
1595pub fn rect_profile_values(
1602 width: u32,
1603 height: u32,
1604 data: &[f32],
1605 rect: (f64, f64, f64, f64),
1606 horizontal: bool,
1607 method: ProfileMethod,
1608) -> Result<(Vec<f64>, Vec<f64>), PlotDataError> {
1609 validate_image_len(width, height, data.len())?;
1610 let (x_min, x_max, y_min, y_max) = rect;
1611
1612 let col_min = x_min.round().max(0.0) as usize;
1613 let col_max = x_max.round().min(width as f64 - 1.0) as usize;
1614 let row_min = y_min.round().max(0.0) as usize;
1615 let row_max = y_max.round().min(height as f64 - 1.0) as usize;
1616
1617 if col_min > col_max
1618 || row_min > row_max
1619 || col_max >= width as usize
1620 || row_max >= height as usize
1621 {
1622 return Ok((vec![], vec![]));
1623 }
1624
1625 let reduce = |sum: f64, count: f64| match method {
1626 ProfileMethod::Mean => sum / count,
1627 ProfileMethod::Sum => sum,
1628 };
1629
1630 if horizontal {
1631 let num_rows = (row_max - row_min + 1) as f64;
1632 let mut x_vals = Vec::with_capacity(col_max - col_min + 1);
1633 let mut y_vals = Vec::with_capacity(col_max - col_min + 1);
1634
1635 for col in col_min..=col_max {
1636 let mut sum = 0.0;
1637 for row in row_min..=row_max {
1638 sum += data[row * width as usize + col] as f64;
1639 }
1640 x_vals.push(col as f64);
1641 y_vals.push(reduce(sum, num_rows));
1642 }
1643 Ok((x_vals, y_vals))
1644 } else {
1645 let num_cols = (col_max - col_min + 1) as f64;
1646 let mut x_vals = Vec::with_capacity(row_max - row_min + 1);
1647 let mut y_vals = Vec::with_capacity(row_max - row_min + 1);
1648
1649 for row in row_min..=row_max {
1650 let mut sum = 0.0;
1651 for col in col_min..=col_max {
1652 sum += data[row * width as usize + col] as f64;
1653 }
1654 x_vals.push(row as f64);
1655 y_vals.push(reduce(sum, num_cols));
1656 }
1657 Ok((x_vals, y_vals))
1658 }
1659}
1660
1661fn fmt_stat(value: Option<f64>) -> String {
1662 value.map_or_else(|| "n/a".to_owned(), |value| format!("{value:.6}"))
1663}
1664
1665fn show_value_stats(ui: &mut egui::Ui, label: &str, stats: ValueStats) {
1666 ui.label(format!(
1667 "{label}: n={} finite={} min={} max={} mean={}",
1668 stats.count,
1669 stats.finite_count,
1670 fmt_stat(stats.min),
1671 fmt_stat(stats.max),
1672 fmt_stat(stats.mean)
1673 ));
1674}
1675
1676#[derive(Clone, Copy, Debug, Default)]
1677struct Bounds1D {
1678 min: f64,
1679 max: f64,
1680}
1681
1682impl Bounds1D {
1683 fn new(min: f64, max: f64) -> Option<Self> {
1684 (min.is_finite() && max.is_finite()).then(|| Self {
1685 min: min.min(max),
1686 max: min.max(max),
1687 })
1688 }
1689
1690 fn include(&mut self, other: Self) {
1691 self.min = self.min.min(other.min);
1692 self.max = self.max.max(other.max);
1693 }
1694
1695 fn as_non_degenerate(self) -> (f64, f64) {
1696 if self.max > self.min {
1697 (self.min, self.max)
1698 } else {
1699 let pad = (self.min.abs() * 0.05).max(0.5);
1700 (self.min - pad, self.max + pad)
1701 }
1702 }
1703}
1704
1705#[derive(Clone, Debug, Default)]
1708struct DataBounds {
1709 x: Option<Bounds1D>,
1710 y_left: Option<Bounds1D>,
1711 y_right: Option<Bounds1D>,
1712 extra: Vec<Option<Bounds1D>>,
1715}
1716
1717impl DataBounds {
1718 fn include(&mut self, x: Bounds1D, y: Bounds1D, axis: YAxis) {
1719 include_axis(&mut self.x, x);
1720 match axis {
1721 YAxis::Left => include_axis(&mut self.y_left, y),
1722 YAxis::Right => include_axis(&mut self.y_right, y),
1723 YAxis::Extra(n) => {
1724 if self.extra.len() <= n {
1725 self.extra.resize(n + 1, None);
1726 }
1727 include_axis(&mut self.extra[n], y);
1728 }
1729 }
1730 }
1731
1732 fn include_bounds(&mut self, other: &Self) {
1733 if let Some(x) = other.x {
1734 include_axis(&mut self.x, x);
1735 }
1736 if let Some(y) = other.y_left {
1737 include_axis(&mut self.y_left, y);
1738 }
1739 if let Some(y) = other.y_right {
1740 include_axis(&mut self.y_right, y);
1741 }
1742 for (n, slot) in other.extra.iter().enumerate() {
1743 if let Some(y) = slot {
1744 if self.extra.len() <= n {
1745 self.extra.resize(n + 1, None);
1746 }
1747 include_axis(&mut self.extra[n], *y);
1748 }
1749 }
1750 }
1751}
1752
1753fn include_axis(slot: &mut Option<Bounds1D>, bounds: Bounds1D) {
1754 match slot {
1755 Some(existing) => existing.include(bounds),
1756 None => *slot = Some(bounds),
1757 }
1758}
1759
1760fn data_range_from_bounds(bounds: &DataBounds) -> DataRange {
1767 DataRange {
1768 x: bounds.x.map(Bounds1D::as_non_degenerate),
1769 y: bounds.y_left.map(Bounds1D::as_non_degenerate),
1770 y2: bounds.y_right.map(Bounds1D::as_non_degenerate),
1771 }
1772}
1773
1774fn extra_data_ranges(bounds: &DataBounds) -> Vec<Option<(f64, f64)>> {
1779 bounds
1780 .extra
1781 .iter()
1782 .map(|b| b.map(Bounds1D::as_non_degenerate))
1783 .collect()
1784}
1785
1786fn raw_data_range_from_bounds(bounds: &DataBounds) -> DataRange {
1794 DataRange {
1795 x: bounds.x.map(|b| (b.min, b.max)),
1796 y: bounds.y_left.map(|b| (b.min, b.max)),
1797 y2: bounds.y_right.map(|b| (b.min, b.max)),
1798 }
1799}
1800
1801fn finite_bounds(values: &[f64]) -> Option<Bounds1D> {
1802 values
1803 .iter()
1804 .copied()
1805 .filter(|v| v.is_finite())
1806 .fold(None, |bounds, value| match bounds {
1807 Some(mut bounds) => {
1808 bounds.include(Bounds1D::new(value, value).expect("finite value"));
1809 Some(bounds)
1810 }
1811 None => Bounds1D::new(value, value),
1812 })
1813}
1814
1815fn image_bounds(image: &ImageSpec<'_>) -> Option<(Bounds1D, Bounds1D)> {
1816 let (width, height) = match image {
1817 ImageSpec {
1818 pixels: ImagePixelsSpec::Scalar { width, height, .. },
1819 ..
1820 }
1821 | ImageSpec {
1822 pixels: ImagePixelsSpec::Rgba { width, height, .. },
1823 ..
1824 } => (*width, *height),
1825 };
1826 let x0 = image.origin.0;
1827 let y0 = image.origin.1;
1828 let x1 = x0 + image.scale.0 * width as f64;
1829 let y1 = y0 + image.scale.1 * height as f64;
1830 Some((Bounds1D::new(x0, x1)?, Bounds1D::new(y0, y1)?))
1831}
1832
1833fn curve_spec_bounds(spec: &CurveSpec<'_>) -> DataBounds {
1834 let mut bounds = DataBounds::default();
1835 if let (Some(x), Some(y)) = (finite_bounds(spec.x), finite_bounds(spec.y)) {
1836 bounds.include(x, y, spec.y_axis);
1837 }
1838 bounds
1839}
1840
1841fn curve_spec_stats(spec: &CurveSpec<'_>) -> ItemStats {
1842 ItemStats::Curve(CurveStats {
1843 x: ValueStats::from_f64(spec.x),
1844 y: ValueStats::from_f64(spec.y),
1845 y_axis: spec.y_axis,
1846 })
1847}
1848
1849fn curve_spec_retained_data(spec: &CurveSpec<'_>) -> RetainedItemData {
1851 RetainedItemData::Curve {
1852 x: spec.x.to_vec(),
1853 y: spec.y.to_vec(),
1854 }
1855}
1856
1857fn apply_curve_alpha(color: Color32, alpha: f32) -> Color32 {
1864 crate::core::color::scale_alpha(color, alpha)
1865}
1866
1867fn compose_per_point_alpha(colors: &mut [Color32], alpha: &[f64]) {
1885 for (color, &a) in colors.iter_mut().zip(alpha) {
1886 let [r, g, b, sa] = color.to_srgba_unmultiplied();
1887 let sa = ((a.clamp(0.0, 1.0) as f32) * (sa as f32)).round() as u8;
1888 *color = Color32::from_rgba_unmultiplied(r, g, b, sa);
1889 }
1890}
1891
1892fn point_colors(values: &[f64], colormap: &Colormap, alpha: Option<&[f64]>) -> Vec<Color32> {
1905 let mut colors: Vec<Color32> = values
1906 .iter()
1907 .map(|&v| {
1908 let t = colormap.normalize(v);
1909 let idx = (t * 255.0).clamp(0.0, 255.0) as usize;
1910 let [r, g, b, a] = colormap.lut[idx];
1911 Color32::from_rgba_unmultiplied(r, g, b, a)
1912 })
1913 .collect();
1914 if let Some(alpha) = alpha {
1915 compose_per_point_alpha(&mut colors, alpha);
1916 }
1917 colors
1918}
1919
1920fn clamp_alpha(mut alpha: Vec<f64>) -> Vec<f64> {
1925 for a in &mut alpha {
1926 *a = a.clamp(0.0, 1.0);
1927 }
1928 alpha
1929}
1930
1931fn curve_data_from_spec_hl(spec: &CurveSpec<'_>) -> CurveData {
1937 let color = match &spec.color {
1938 CurveColor::Uniform(color) => apply_curve_alpha(*color, spec.alpha),
1939 CurveColor::PerVertex(colors) => colors
1940 .first()
1941 .copied()
1942 .map(|color| apply_curve_alpha(color, spec.alpha))
1943 .unwrap_or(Color32::WHITE),
1944 };
1945 let mut curve = CurveData::new(spec.x.to_vec(), spec.y.to_vec(), color)
1946 .with_width(spec.line_width)
1947 .with_line_style(spec.line_style.clone())
1948 .with_marker_size(spec.symbol_size)
1949 .with_y_axis(spec.y_axis);
1950 if let CurveColor::PerVertex(colors) = &spec.color {
1951 curve = curve.with_colors(
1952 colors
1953 .iter()
1954 .copied()
1955 .map(|color| apply_curve_alpha(color, spec.alpha))
1956 .collect(),
1957 );
1958 }
1959 if let Some(gap_color) = spec.gap_color {
1960 curve = curve.with_gap_color(apply_curve_alpha(gap_color, spec.alpha));
1961 }
1962 if let Some(symbol) = spec.symbol {
1963 curve = curve.with_symbol(symbol);
1964 }
1965 if let Some(error) = &spec.x_error {
1966 curve = curve.with_x_error(error.clone());
1967 }
1968 if let Some(error) = &spec.y_error {
1969 curve = curve.with_y_error(error.clone());
1970 }
1971 if spec.fill {
1972 curve = curve.with_fill(spec.baseline.clone());
1973 }
1974 curve
1975}
1976
1977fn retained_data_to_stats_input(
1983 data: &RetainedItemData,
1984) -> crate::widget::stats_widget::StatsInput<'_> {
1985 use crate::widget::stats_widget::StatsInput;
1986 match data {
1987 RetainedItemData::Curve { x, y } => StatsInput::Curve { xs: x, ys: y },
1988 RetainedItemData::Image {
1989 data,
1990 width,
1991 height,
1992 origin,
1993 scale,
1994 ..
1995 } => StatsInput::Image {
1996 data,
1997 width: *width,
1998 height: *height,
1999 origin: *origin,
2000 scale: *scale,
2001 },
2002 }
2003}
2004
2005fn autoscaled_colormap(base: &Colormap, mode: AutoscaleMode, pixels: &[f64]) -> Colormap {
2016 let (vmin, vmax) = mode.range(pixels, base.autoscale_percentiles);
2017 let mut cm = base.clone();
2018 cm.vmin = vmin;
2019 cm.vmax = vmax;
2020 cm
2021}
2022
2023fn retained_curve_xy(data: &RetainedItemData) -> Option<(&[f64], &[f64])> {
2030 match data {
2031 RetainedItemData::Curve { x, y } => Some((x, y)),
2032 RetainedItemData::Image { .. } => None,
2033 }
2034}
2035
2036fn roi_stats_rows(
2048 rois: &[ManagedRoi],
2049 data: &RetainedItemData,
2050) -> Vec<crate::widget::roi_stats_widget::RoiStatsRow> {
2051 use crate::widget::roi_stats::{curve_roi_stats, image_roi_stats};
2052 use crate::widget::roi_stats_widget::RoiStatsRow;
2053
2054 rois.iter()
2055 .enumerate()
2056 .map(|(index, managed)| {
2057 let stats = match data {
2058 RetainedItemData::Image {
2059 data,
2060 width,
2061 height,
2062 origin,
2063 scale,
2064 ..
2065 } => {
2066 let pixels: Vec<f32> = data.iter().map(|&v| v as f32).collect();
2070 image_roi_stats(
2071 &managed.roi,
2072 &pixels,
2073 *width,
2074 *height,
2075 [origin.0, origin.1],
2076 [scale.0, scale.1],
2077 )
2078 }
2079 RetainedItemData::Curve { x, y } => curve_roi_stats(&managed.roi, x, y),
2080 };
2081 let label = if managed.name.is_empty() {
2082 format!("ROI {index}")
2083 } else {
2084 managed.name.clone()
2085 };
2086 RoiStatsRow { label, stats }
2087 })
2088 .collect()
2089}
2090
2091fn curve_roi_rows(
2099 rois: &[ManagedRoi],
2100 x: &[f64],
2101 y: &[f64],
2102) -> Vec<crate::widget::curves_roi_widget::CurveRoiRow> {
2103 use crate::widget::curves_roi_widget::CurveRoiRow;
2104 use crate::widget::roi_stats::{curve_roi_counts, roi_x_span};
2105
2106 rois.iter()
2107 .enumerate()
2108 .filter_map(|(index, managed)| {
2109 let (from, to) = roi_x_span(&managed.roi)?;
2110 let counts = curve_roi_counts(&managed.roi, x, y)?;
2111 let label = if managed.name.is_empty() {
2112 format!("ROI {index}")
2113 } else {
2114 managed.name.clone()
2115 };
2116 Some(CurveRoiRow {
2117 label,
2118 from,
2119 to,
2120 counts,
2121 })
2122 })
2123 .collect()
2124}
2125
2126fn image_spec_retained_data(spec: &ImageSpec<'_>) -> Option<RetainedItemData> {
2129 match &spec.pixels {
2130 ImagePixelsSpec::Scalar {
2131 width,
2132 height,
2133 data,
2134 colormap,
2135 } => Some(RetainedItemData::Image {
2136 data: data.iter().map(|&v| v as f64).collect(),
2137 width: *width as usize,
2138 height: *height as usize,
2139 origin: spec.origin,
2140 scale: spec.scale,
2141 colormap: colormap.clone(),
2142 alpha: spec.alpha,
2143 interpolation: spec.interpolation,
2144 aggregation: spec.aggregation,
2145 aggregation_block: spec.aggregation_block,
2146 alpha_map: spec.alpha_map.map(|a| a.to_vec()),
2147 }),
2148 ImagePixelsSpec::Rgba { .. } => None,
2149 }
2150}
2151
2152fn image_spec_bounds(spec: &ImageSpec<'_>) -> DataBounds {
2153 let mut bounds = DataBounds::default();
2154 if let Some((x, y)) = image_bounds(spec) {
2155 bounds.include(x, y, YAxis::Left);
2156 }
2157 bounds
2158}
2159
2160fn image_spec_stats(spec: &ImageSpec<'_>) -> ItemStats {
2161 let (width, height, scalar) = match &spec.pixels {
2162 ImagePixelsSpec::Scalar {
2163 width,
2164 height,
2165 data,
2166 ..
2167 } => (*width, *height, Some(ValueStats::from_f32(data))),
2168 ImagePixelsSpec::Rgba { width, height, .. } => (*width, *height, None),
2169 };
2170 ItemStats::Image(ImageStats {
2171 width,
2172 height,
2173 pixel_count: expected_image_len(width, height),
2174 scalar,
2175 })
2176}
2177
2178fn rgba_to_color32(rgba: [u8; 4]) -> Color32 {
2179 Color32::from_rgba_unmultiplied(rgba[0], rgba[1], rgba[2], rgba[3])
2180}
2181
2182fn fallback_legend_color(kind: PlotItemKind) -> Color32 {
2183 match kind {
2184 PlotItemKind::Curve => Color32::LIGHT_BLUE,
2185 PlotItemKind::Histogram => Color32::LIGHT_GREEN,
2186 PlotItemKind::Scatter => Color32::LIGHT_BLUE,
2187 PlotItemKind::Image => Color32::GRAY,
2188 PlotItemKind::Mask => Color32::from_rgba_unmultiplied(255, 80, 80, 160),
2189 PlotItemKind::Triangles => Color32::LIGHT_BLUE,
2190 PlotItemKind::Shape => Color32::WHITE,
2191 PlotItemKind::Marker => Color32::YELLOW,
2192 }
2193}
2194
2195fn curve_spec_legend_visual(spec: &CurveSpec<'_>, kind: PlotItemKind) -> LegendVisual {
2196 let color = match spec.color {
2197 CurveColor::Uniform(color) => color,
2198 CurveColor::PerVertex(colors) => colors
2199 .first()
2200 .copied()
2201 .unwrap_or_else(|| fallback_legend_color(kind)),
2202 };
2203 LegendVisual::curve(color, spec.line_style.clone(), spec.symbol)
2204}
2205
2206fn image_spec_legend_visual(spec: &ImageSpec<'_>, kind: PlotItemKind) -> LegendVisual {
2207 match &spec.pixels {
2208 ImagePixelsSpec::Scalar { colormap, .. } => LegendVisual::with_secondary(
2209 rgba_to_color32(colormap.lut[48]),
2210 rgba_to_color32(colormap.lut[208]),
2211 ),
2212 ImagePixelsSpec::Rgba { data, .. } => {
2213 let color = data
2214 .iter()
2215 .copied()
2216 .find(|rgba| rgba[3] != 0)
2217 .map(rgba_to_color32)
2218 .unwrap_or_else(|| fallback_legend_color(kind));
2219 LegendVisual::new(color)
2220 }
2221 }
2222}
2223
2224fn triangle_spec_legend_visual(spec: &TriangleSpec<'_>) -> LegendVisual {
2225 LegendVisual::new(
2226 spec.colors
2227 .first()
2228 .copied()
2229 .unwrap_or_else(|| fallback_legend_color(PlotItemKind::Triangles)),
2230 )
2231}
2232
2233fn shape_spec_legend_visual(spec: &ShapeSpec<'_>) -> LegendVisual {
2234 LegendVisual::new(spec.color)
2235}
2236
2237fn marker_spec_legend_visual(spec: &MarkerSpec<'_>) -> LegendVisual {
2238 LegendVisual::new(spec.color)
2239}
2240
2241fn xy_bounds(x: &[f64], y: &[f64], axis: YAxis) -> DataBounds {
2242 let mut bounds = DataBounds::default();
2243 if let (Some(x), Some(y)) = (finite_bounds(x), finite_bounds(y)) {
2244 bounds.include(x, y, axis);
2245 }
2246 bounds
2247}
2248
2249fn active_axis_label_overrides(
2256 x_label: Option<&str>,
2257 y_label: Option<&str>,
2258 y_axis: YAxis,
2259) -> (Option<String>, Option<String>, Option<String>) {
2260 let x = x_label.map(ToOwned::to_owned);
2261 let y = y_label.map(ToOwned::to_owned);
2262 match y_axis {
2263 YAxis::Left => (x, y, None),
2264 YAxis::Right => (x, None, y),
2265 YAxis::Extra(_) => (x, None, None),
2269 }
2270}
2271
2272fn curve_spec_from_data(curve: &CurveData) -> CurveSpec<'_> {
2273 CurveSpec {
2274 x: &curve.x,
2275 y: &curve.y,
2276 color: curve
2277 .colors
2278 .as_deref()
2279 .map_or(CurveColor::Uniform(curve.color), CurveColor::PerVertex),
2280 gap_color: curve.gap_color,
2281 symbol: curve.symbol,
2282 line_width: curve.width,
2283 line_style: curve.line_style.clone(),
2284 y_axis: curve.y_axis,
2285 x_error: curve.x_error.clone(),
2286 y_error: curve.y_error.clone(),
2287 fill: curve.fill,
2288 alpha: 1.0,
2289 symbol_size: curve.marker_size,
2290 baseline: curve.baseline.clone(),
2291 x_label: None,
2295 y_label: None,
2296 }
2297}
2298
2299fn image_spec_from_data(image: &ImageData) -> ImageSpec<'_> {
2300 match &image.pixels {
2301 ImagePixels::Scalar { data, colormap } => ImageSpec {
2302 pixels: ImagePixelsSpec::Scalar {
2303 width: image.width,
2304 height: image.height,
2305 data,
2306 colormap: colormap.clone(),
2307 },
2308 origin: image.origin,
2309 scale: image.scale,
2310 alpha: image.alpha,
2311 interpolation: image.interpolation,
2312 aggregation: AggregationMode::None,
2315 aggregation_block: (1, 1),
2316 alpha_map: image.alpha_map.as_deref(),
2319 },
2320 ImagePixels::Rgba { data } => ImageSpec {
2321 pixels: ImagePixelsSpec::Rgba {
2322 width: image.width,
2323 height: image.height,
2324 data,
2325 },
2326 origin: image.origin,
2327 scale: image.scale,
2328 alpha: image.alpha,
2329 interpolation: image.interpolation,
2330 aggregation: AggregationMode::None,
2331 aggregation_block: (1, 1),
2332 alpha_map: None,
2334 },
2335 }
2336}
2337
2338fn apply_image_mask(
2349 width: u32,
2350 height: u32,
2351 data: &[f32],
2352 mask: &ScalarMask,
2353) -> Result<Vec<f32>, PlotDataError> {
2354 let expected = (width as usize).saturating_mul(height as usize);
2355 if data.len() != expected {
2356 return Err(PlotDataError::ImageDataLength {
2357 expected,
2358 actual: data.len(),
2359 });
2360 }
2361 if mask.width() != width as usize || mask.height() != height as usize {
2362 return Err(PlotDataError::ImageDataLength {
2363 expected,
2364 actual: mask.width().saturating_mul(mask.height()),
2365 });
2366 }
2367 Ok(mask.apply(data))
2368}
2369
2370fn triangle_spec_from_data(triangles: &Triangles) -> TriangleSpec<'_> {
2371 TriangleSpec {
2372 x: &triangles.x,
2373 y: &triangles.y,
2374 triangles: &triangles.indices,
2375 colors: &triangles.colors,
2376 alpha: triangles.alpha,
2377 }
2378}
2379
2380fn shape_spec_from_data(shape: &Shape) -> ShapeSpec<'_> {
2381 ShapeSpec {
2382 x: &shape.x,
2383 y: &shape.y,
2384 kind: shape.kind,
2385 color: shape.color,
2386 fill: shape.fill,
2387 overlay: false,
2388 line_style: shape.line_style.clone(),
2389 line_width: shape.line_width,
2390 gap_color: shape.gap_color,
2391 }
2392}
2393
2394fn marker_spec_from_data(marker: &Marker) -> MarkerSpec<'_> {
2395 let (x, y, symbol, symbol_size) = match marker.kind {
2396 MarkerKind::Point { x, y, symbol, size } => (Some(x), Some(y), Some(symbol), size),
2397 MarkerKind::VLine { x } => (Some(x), None, None, 0.0),
2398 MarkerKind::HLine { y } => (None, Some(y), None, 0.0),
2399 };
2400 MarkerSpec {
2401 x,
2402 y,
2403 text: marker.text.as_deref(),
2404 color: marker.color,
2405 symbol,
2406 symbol_size,
2407 line_style: marker.line_style.clone(),
2408 line_width: marker.line_width,
2409 y_axis: marker.y_axis,
2410 bg_color: marker.bgcolor,
2411 is_draggable: marker.is_draggable,
2412 constraint: marker.constraint,
2413 }
2414}
2415
2416#[derive(Clone, Debug)]
2422enum RetainedItemData {
2423 Curve { x: Vec<f64>, y: Vec<f64> },
2425 Image {
2431 data: Vec<f64>,
2432 width: usize,
2433 height: usize,
2434 origin: (f64, f64),
2435 scale: (f64, f64),
2436 colormap: Box<Colormap>,
2437 alpha: f32,
2446 interpolation: InterpolationMode,
2451 aggregation: AggregationMode,
2454 aggregation_block: (u32, u32),
2457 alpha_map: Option<Vec<f32>>,
2464 },
2465}
2466
2467#[derive(Clone, Debug)]
2481struct ImageDisplayAttrs {
2482 origin: (f64, f64),
2483 scale: (f64, f64),
2484 alpha: f32,
2485 interpolation: InterpolationMode,
2486 aggregation: AggregationMode,
2487 aggregation_block: (u32, u32),
2488 alpha_map: Option<Vec<f32>>,
2489}
2490
2491impl ImageDisplayAttrs {
2492 fn apply<'a>(&'a self, spec: &mut ImageSpec<'a>) {
2497 spec.origin = self.origin;
2498 spec.scale = self.scale;
2499 spec.alpha = self.alpha;
2500 spec.interpolation = self.interpolation;
2501 spec.aggregation = self.aggregation;
2502 spec.aggregation_block = self.aggregation_block;
2503 spec.alpha_map = self.alpha_map.as_deref();
2504 }
2505}
2506
2507impl RetainedItemData {
2508 fn image_display_attrs(&self) -> Option<ImageDisplayAttrs> {
2512 match self {
2513 RetainedItemData::Image {
2514 origin,
2515 scale,
2516 alpha,
2517 interpolation,
2518 aggregation,
2519 aggregation_block,
2520 alpha_map,
2521 ..
2522 } => Some(ImageDisplayAttrs {
2523 origin: *origin,
2524 scale: *scale,
2525 alpha: *alpha,
2526 interpolation: *interpolation,
2527 aggregation: *aggregation,
2528 aggregation_block: *aggregation_block,
2529 alpha_map: alpha_map.clone(),
2530 }),
2531 RetainedItemData::Curve { .. } => None,
2532 }
2533 }
2534}
2535
2536#[derive(Clone, Debug)]
2537struct ItemRecord {
2538 handle: ItemHandle,
2539 kind: PlotItemKind,
2540 bounds: DataBounds,
2541 legend: Option<String>,
2542 x_label: Option<String>,
2547 y_label: Option<String>,
2551 stats: Option<ItemStats>,
2552 visual: LegendVisual,
2553 data: Option<RetainedItemData>,
2556 curve_data: Option<CurveData>,
2561 hidden_symbol: Option<Symbol>,
2568 hidden_line_style: Option<LineStyle>,
2575}
2576
2577#[derive(Clone, Debug)]
2578struct LegendVisual {
2579 color: Color32,
2580 secondary: Option<Color32>,
2581 line_style: LineStyle,
2586 symbol: Option<Symbol>,
2590}
2591
2592const LEGEND_ROW_HEIGHT: f32 = 24.0;
2593const LEGEND_SWATCH_WIDTH: f32 = 54.0;
2594const LEGEND_CHECK_WIDTH: f32 = 22.0;
2595const LEGEND_ROW_MIN_WIDTH: f32 = 1.0;
2596
2597impl LegendVisual {
2598 fn new(color: Color32) -> Self {
2599 Self {
2600 color,
2601 secondary: None,
2602 line_style: LineStyle::Solid,
2603 symbol: None,
2604 }
2605 }
2606
2607 fn with_secondary(color: Color32, secondary: Color32) -> Self {
2608 Self {
2609 color,
2610 secondary: Some(secondary),
2611 line_style: LineStyle::Solid,
2612 symbol: None,
2613 }
2614 }
2615
2616 fn curve(color: Color32, line_style: LineStyle, symbol: Option<Symbol>) -> Self {
2619 Self {
2620 color,
2621 secondary: None,
2622 line_style,
2623 symbol,
2624 }
2625 }
2626}
2627
2628fn legend_row_width(available_width: f32) -> f32 {
2629 available_width.max(LEGEND_ROW_MIN_WIDTH)
2630}
2631
2632const DEFAULT_RESTORE_SYMBOL: Symbol = Symbol::Point;
2637
2638const DEFAULT_RESTORE_LINE_STYLE: LineStyle = LineStyle::Solid;
2643
2644fn set_symbol_visibility(
2660 current: Option<Symbol>,
2661 visible: bool,
2662 cache: &mut Option<Symbol>,
2663) -> Option<Symbol> {
2664 match (visible, current) {
2665 (true, Some(symbol)) => Some(symbol),
2667 (true, None) => Some(cache.take().unwrap_or(DEFAULT_RESTORE_SYMBOL)),
2670 (false, Some(symbol)) => {
2672 *cache = Some(symbol);
2673 None
2674 }
2675 (false, None) => None,
2677 }
2678}
2679
2680fn set_line_visibility(
2696 current: LineStyle,
2697 visible: bool,
2698 cache: &mut Option<LineStyle>,
2699) -> LineStyle {
2700 match (visible, current.draws_line()) {
2701 (true, true) => current,
2703 (true, false) => cache.take().unwrap_or(DEFAULT_RESTORE_LINE_STYLE),
2706 (false, true) => {
2708 *cache = Some(current);
2709 LineStyle::None
2710 }
2711 (false, false) => current,
2713 }
2714}
2715
2716#[derive(Clone, Debug, PartialEq, Default)]
2731pub struct CurveStyle {
2732 pub color: Option<Color32>,
2735 pub line_width: Option<f32>,
2738 pub line_style: Option<LineStyle>,
2741 pub symbol: Option<Symbol>,
2747 pub symbol_size: Option<f32>,
2750 pub gap_color: Option<Color32>,
2753}
2754
2755fn current_curve_style(base: &CurveData, highlight: &CurveStyle, highlighted: bool) -> CurveData {
2770 let mut resolved = base.clone();
2771 if highlighted {
2772 if let Some(color) = highlight.color {
2773 resolved.color = color;
2774 }
2775 if let Some(width) = highlight.line_width {
2776 resolved.width = width;
2777 }
2778 if let Some(line_style) = highlight.line_style.clone() {
2779 resolved.line_style = line_style;
2780 }
2781 if let Some(symbol) = highlight.symbol {
2782 resolved.symbol = Some(symbol);
2783 }
2784 if let Some(symbol_size) = highlight.symbol_size {
2785 resolved.marker_size = symbol_size;
2786 }
2787 if let Some(gap_color) = highlight.gap_color {
2788 resolved.gap_color = Some(gap_color);
2789 }
2790 }
2791 resolved
2792}
2793
2794struct LegendRowResult {
2796 row_clicked: bool,
2798 eye_clicked: bool,
2800 row_response: egui::Response,
2803}
2804
2805fn legend_row_response(
2806 ui: &mut egui::Ui,
2807 width: f32,
2808 kind: PlotItemKind,
2809 label: &str,
2810 active: bool,
2811 visible: bool,
2812 visual: LegendVisual,
2813) -> LegendRowResult {
2814 let (rect, row_response) =
2815 ui.allocate_exact_size(egui::vec2(width, LEGEND_ROW_HEIGHT), egui::Sense::click());
2816 let eye_rect = egui::Rect::from_min_max(
2817 egui::pos2(rect.right() - LEGEND_CHECK_WIDTH, rect.top()),
2818 rect.right_bottom(),
2819 );
2820 let eye_response = ui.interact(eye_rect, row_response.id.with("eye"), egui::Sense::click());
2821 if ui.is_rect_visible(rect) {
2822 draw_legend_row(
2823 ui,
2824 LegendRowDraw {
2825 rect,
2826 response: &row_response,
2827 kind,
2828 label,
2829 active,
2830 visible,
2831 visual,
2832 },
2833 );
2834 }
2835 LegendRowResult {
2836 row_clicked: row_response.clicked() && !eye_response.clicked(),
2837 eye_clicked: eye_response.clicked(),
2838 row_response,
2839 }
2840}
2841
2842struct LegendRowDraw<'a> {
2843 rect: egui::Rect,
2844 response: &'a egui::Response,
2845 kind: PlotItemKind,
2846 label: &'a str,
2847 active: bool,
2848 visible: bool,
2849 visual: LegendVisual,
2850}
2851
2852fn draw_legend_row(ui: &egui::Ui, p: LegendRowDraw<'_>) {
2853 let LegendRowDraw {
2854 rect,
2855 response,
2856 kind,
2857 label,
2858 active,
2859 visible,
2860 visual,
2861 } = p;
2862 let visuals = ui.visuals();
2863 let row_rect = rect.shrink2(egui::vec2(1.0, 0.0));
2864 let row_clip = row_rect.intersect(ui.clip_rect());
2865 let painter = ui.painter().with_clip_rect(row_clip);
2866
2867 let fill = if active {
2868 visuals.selection.bg_fill
2869 } else if response.hovered() {
2870 visuals.widgets.hovered.weak_bg_fill
2871 } else {
2872 Color32::TRANSPARENT
2873 };
2874 painter.rect_filled(row_rect, 0.0, fill);
2875
2876 let check_rect = egui::Rect::from_min_max(
2877 egui::pos2(row_rect.right() - LEGEND_CHECK_WIDTH, row_rect.top()),
2878 row_rect.right_bottom(),
2879 );
2880 let swatch_right = (row_rect.left() + 4.0 + LEGEND_SWATCH_WIDTH)
2881 .min(check_rect.left() - 4.0)
2882 .max(row_rect.left() + 4.0);
2883 let swatch_rect = egui::Rect::from_min_max(
2884 egui::pos2(row_rect.left() + 4.0, row_rect.top() + 4.0),
2885 egui::pos2(swatch_right, row_rect.bottom() - 4.0),
2886 );
2887 if swatch_rect.width() >= 8.0 {
2888 draw_legend_swatch(&painter, swatch_rect, kind, visual);
2889 }
2890
2891 let text_color = if active {
2892 visuals.selection.stroke.color
2893 } else {
2894 visuals.text_color()
2895 };
2896 let text_left = swatch_rect.right() + 6.0;
2897 let text_right = check_rect.left() - 2.0;
2898 if text_right > text_left {
2899 let text_clip = egui::Rect::from_min_max(
2900 egui::pos2(text_left, row_rect.top()),
2901 egui::pos2(text_right, row_rect.bottom()),
2902 )
2903 .intersect(row_clip);
2904 painter.with_clip_rect(text_clip).text(
2905 egui::pos2(text_left, row_rect.center().y),
2906 egui::Align2::LEFT_CENTER,
2907 label,
2908 egui::FontId::proportional(12.0),
2909 text_color,
2910 );
2911 }
2912
2913 draw_legend_eye(
2914 &painter,
2915 check_rect,
2916 visible,
2917 active,
2918 visuals.widgets.inactive.fg_stroke.color,
2919 );
2920 painter.line_segment(
2921 [
2922 egui::pos2(row_rect.left(), row_rect.bottom()),
2923 egui::pos2(row_rect.right(), row_rect.bottom()),
2924 ],
2925 egui::Stroke::new(1.0, visuals.widgets.noninteractive.bg_stroke.color),
2926 );
2927}
2928
2929fn draw_legend_swatch(
2930 painter: &egui::Painter,
2931 rect: egui::Rect,
2932 kind: PlotItemKind,
2933 visual: LegendVisual,
2934) {
2935 match kind {
2938 PlotItemKind::Curve => {
2939 let y = rect.center().y;
2942 let a = egui::pos2(rect.left() + 4.0, y);
2943 let b = egui::pos2(rect.right() - 4.0, y);
2944 if visual.line_style.draws_line() {
2945 let stroke = egui::Stroke::new(2.0, visual.color);
2946 match visual.line_style.painter_dashes(stroke.width) {
2947 None => {
2948 painter.line_segment([a, b], stroke);
2949 }
2950 Some((dashes, gaps, offset)) => {
2951 for shape in egui::Shape::dashed_line_with_offset(
2952 &[a, b],
2953 stroke,
2954 &dashes,
2955 &gaps,
2956 offset,
2957 ) {
2958 painter.add(shape);
2959 }
2960 }
2961 }
2962 }
2963 if let Some(symbol) = visual.symbol {
2964 draw_legend_symbol(painter, rect.center(), 4.0, symbol, visual.color);
2965 }
2966 }
2967 PlotItemKind::Histogram => {
2968 let fill = visual.color.linear_multiply(0.45);
2969 let bar_width = rect.width() / 5.0;
2970 for (index, height) in [0.35, 0.65, 0.9, 0.55].iter().copied().enumerate() {
2971 let left = rect.left() + 4.0 + index as f32 * bar_width;
2972 let right = left + bar_width * 0.7;
2973 let top = rect.bottom() - 3.0 - rect.height() * height;
2974 let bar = egui::Rect::from_min_max(
2975 egui::pos2(left, top),
2976 egui::pos2(right, rect.bottom() - 3.0),
2977 );
2978 painter.rect_filled(bar, 0.0, fill);
2979 painter.rect_stroke(
2980 bar,
2981 0.0,
2982 egui::Stroke::new(1.0, visual.color),
2983 egui::StrokeKind::Inside,
2984 );
2985 }
2986 }
2987 PlotItemKind::Scatter => {
2988 for t in [0.25, 0.5, 0.75] {
2989 let center = egui::pos2(
2990 egui::lerp(rect.left() + 6.0..=rect.right() - 6.0, t),
2991 egui::lerp(rect.bottom() - 4.0..=rect.top() + 4.0, t),
2992 );
2993 painter.circle_filled(center, 3.0, visual.color);
2994 }
2995 }
2996 PlotItemKind::Image | PlotItemKind::Mask => {
2997 if let Some(secondary) = visual.secondary {
2998 let split = rect.center().x;
2999 painter.rect_filled(
3000 egui::Rect::from_min_max(rect.left_top(), egui::pos2(split, rect.bottom())),
3001 0.0,
3002 visual.color,
3003 );
3004 painter.rect_filled(
3005 egui::Rect::from_min_max(egui::pos2(split, rect.top()), rect.right_bottom()),
3006 0.0,
3007 secondary,
3008 );
3009 } else {
3010 painter.rect_filled(rect.shrink(2.0), 0.0, visual.color);
3011 }
3012 }
3013 PlotItemKind::Triangles => {
3014 let points = vec![
3015 egui::pos2(rect.left() + 7.0, rect.bottom() - 4.0),
3016 egui::pos2(rect.center().x, rect.top() + 4.0),
3017 egui::pos2(rect.right() - 7.0, rect.bottom() - 4.0),
3018 ];
3019 painter.add(egui::Shape::convex_polygon(
3020 points,
3021 visual.color.linear_multiply(0.45),
3022 egui::Stroke::new(1.0, visual.color),
3023 ));
3024 }
3025 PlotItemKind::Shape => {
3026 let shape = rect.shrink2(egui::vec2(12.0, 3.0));
3027 painter.rect_stroke(
3028 shape,
3029 0.0,
3030 egui::Stroke::new(1.5, visual.color),
3031 egui::StrokeKind::Inside,
3032 );
3033 }
3034 PlotItemKind::Marker => {
3035 let center = rect.center();
3036 let stroke = egui::Stroke::new(1.8, visual.color);
3037 painter.line_segment(
3038 [
3039 egui::pos2(center.x - 7.0, center.y),
3040 egui::pos2(center.x + 7.0, center.y),
3041 ],
3042 stroke,
3043 );
3044 painter.line_segment(
3045 [
3046 egui::pos2(center.x, center.y - 7.0),
3047 egui::pos2(center.x, center.y + 7.0),
3048 ],
3049 stroke,
3050 );
3051 }
3052 }
3053}
3054
3055fn draw_legend_symbol(
3062 painter: &egui::Painter,
3063 center: egui::Pos2,
3064 half: f32,
3065 symbol: Symbol,
3066 color: Color32,
3067) {
3068 let c = center;
3069 let stroke = egui::Stroke::new(1.5, color);
3070 let caret = |apex: egui::Pos2, arm_a: egui::Pos2, arm_b: egui::Pos2| {
3072 painter.line_segment([apex, arm_a], stroke);
3073 painter.line_segment([apex, arm_b], stroke);
3074 };
3075 match symbol {
3076 Symbol::Circle => {
3077 painter.circle_filled(c, half, color);
3078 }
3079 Symbol::Point => {
3080 painter.circle_filled(c, half * 0.6, color);
3081 }
3082 Symbol::Pixel => {
3083 painter.rect_filled(
3084 egui::Rect::from_center_size(c, egui::Vec2::splat(half * 0.9)),
3085 0.0,
3086 color,
3087 );
3088 }
3089 Symbol::Square => {
3090 painter.rect_filled(
3091 egui::Rect::from_center_size(c, egui::Vec2::splat(half * 2.0)),
3092 0.0,
3093 color,
3094 );
3095 }
3096 Symbol::Diamond => {
3097 painter.add(egui::Shape::convex_polygon(
3098 vec![
3099 egui::pos2(c.x, c.y - half),
3100 egui::pos2(c.x + half, c.y),
3101 egui::pos2(c.x, c.y + half),
3102 egui::pos2(c.x - half, c.y),
3103 ],
3104 color,
3105 egui::Stroke::NONE,
3106 ));
3107 }
3108 Symbol::Triangle => {
3109 painter.add(egui::Shape::convex_polygon(
3110 vec![
3111 egui::pos2(c.x, c.y - half),
3112 egui::pos2(c.x + half, c.y + half),
3113 egui::pos2(c.x - half, c.y + half),
3114 ],
3115 color,
3116 egui::Stroke::NONE,
3117 ));
3118 }
3119 Symbol::Cross => {
3120 painter.line_segment(
3121 [
3122 egui::pos2(c.x - half, c.y - half),
3123 egui::pos2(c.x + half, c.y + half),
3124 ],
3125 stroke,
3126 );
3127 painter.line_segment(
3128 [
3129 egui::pos2(c.x - half, c.y + half),
3130 egui::pos2(c.x + half, c.y - half),
3131 ],
3132 stroke,
3133 );
3134 }
3135 Symbol::Plus => {
3136 painter.line_segment(
3137 [egui::pos2(c.x, c.y - half), egui::pos2(c.x, c.y + half)],
3138 stroke,
3139 );
3140 painter.line_segment(
3141 [egui::pos2(c.x - half, c.y), egui::pos2(c.x + half, c.y)],
3142 stroke,
3143 );
3144 }
3145 Symbol::VerticalLine => {
3146 painter.line_segment(
3147 [egui::pos2(c.x, c.y - half), egui::pos2(c.x, c.y + half)],
3148 stroke,
3149 );
3150 }
3151 Symbol::HorizontalLine => {
3152 painter.line_segment(
3153 [egui::pos2(c.x - half, c.y), egui::pos2(c.x + half, c.y)],
3154 stroke,
3155 );
3156 }
3157 Symbol::TickLeft => {
3158 painter.line_segment([egui::pos2(c.x - half, c.y), c], stroke);
3159 }
3160 Symbol::TickRight => {
3161 painter.line_segment([c, egui::pos2(c.x + half, c.y)], stroke);
3162 }
3163 Symbol::TickUp => {
3164 painter.line_segment([egui::pos2(c.x, c.y - half), c], stroke);
3165 }
3166 Symbol::TickDown => {
3167 painter.line_segment([c, egui::pos2(c.x, c.y + half)], stroke);
3168 }
3169 Symbol::CaretLeft => caret(
3170 egui::pos2(c.x - half, c.y),
3171 egui::pos2(c.x + half, c.y - half),
3172 egui::pos2(c.x + half, c.y + half),
3173 ),
3174 Symbol::CaretRight => caret(
3175 egui::pos2(c.x + half, c.y),
3176 egui::pos2(c.x - half, c.y - half),
3177 egui::pos2(c.x - half, c.y + half),
3178 ),
3179 Symbol::CaretUp => caret(
3180 egui::pos2(c.x, c.y - half),
3181 egui::pos2(c.x - half, c.y + half),
3182 egui::pos2(c.x + half, c.y + half),
3183 ),
3184 Symbol::CaretDown => caret(
3185 egui::pos2(c.x, c.y + half),
3186 egui::pos2(c.x - half, c.y - half),
3187 egui::pos2(c.x + half, c.y - half),
3188 ),
3189 Symbol::Heart => {
3190 let hump = half * 0.5;
3193 let hy = c.y - half * 0.35;
3194 painter.circle_filled(egui::pos2(c.x - half * 0.45, hy), hump, color);
3195 painter.circle_filled(egui::pos2(c.x + half * 0.45, hy), hump, color);
3196 painter.add(egui::Shape::convex_polygon(
3197 vec![
3198 egui::pos2(c.x - half * 0.95, hy),
3199 egui::pos2(c.x + half * 0.95, hy),
3200 egui::pos2(c.x, c.y + half * 0.95),
3201 ],
3202 color,
3203 egui::Stroke::NONE,
3204 ));
3205 }
3206 }
3207}
3208
3209fn draw_legend_eye(
3212 painter: &egui::Painter,
3213 rect: egui::Rect,
3214 visible: bool,
3215 active: bool,
3216 color: Color32,
3217) {
3218 let cx = rect.center().x;
3219 let cy = rect.center().y;
3220 let r = 3.5_f32;
3221 let eye_color = if active {
3222 color
3223 } else {
3224 crate::core::color::with_alpha(color, 180)
3225 };
3226 if visible {
3227 painter.circle_stroke(egui::pos2(cx, cy), r, egui::Stroke::new(1.5, eye_color));
3228 painter.circle_filled(egui::pos2(cx, cy), r * 0.45, eye_color);
3229 } else {
3230 let dim = crate::core::color::with_alpha(color, 80);
3231 painter.circle_stroke(egui::pos2(cx, cy), r, egui::Stroke::new(1.5, dim));
3232 painter.line_segment(
3233 [egui::pos2(cx - r * 1.3, cy), egui::pos2(cx + r * 1.3, cy)],
3234 egui::Stroke::new(1.5, dim),
3235 );
3236 }
3237}
3238
3239type LimitsSnapshot = ((f64, f64, f64, f64), Option<(f64, f64)>);
3240
3241pub struct PlotWidget {
3246 backend: WgpuBackend,
3247 item_records: Vec<ItemRecord>,
3248 data_bounds: DataBounds,
3249 default_colormap: Colormap,
3250 auto_reset_zoom: bool,
3251 interaction_mode: PlotInteractionMode,
3252 active_item: Option<ItemHandle>,
3253 active_curve_style: CurveStyle,
3259 active_curve_handling: bool,
3263 default_plot_lines: bool,
3267 default_plot_points: bool,
3273 events: Vec<PlotEvent>,
3274 rename_state: Option<(ItemHandle, String)>,
3277 print_dialog: crate::widget::print_dialog::PrintDialog,
3280 ruler_active: bool,
3285 ruler_roi: Option<usize>,
3289 ruler_prev_mode: Option<PlotInteractionMode>,
3291}
3292
3293impl PlotWidget {
3294 pub fn new(render_state: &RenderState, id: PlotId) -> Self {
3296 Self::from_backend(WgpuBackend::new(render_state, id))
3297 }
3298
3299 pub fn from_backend(backend: WgpuBackend) -> Self {
3301 Self {
3302 backend,
3303 item_records: Vec::new(),
3304 data_bounds: DataBounds::default(),
3305 default_colormap: Colormap::viridis(0.0, 1.0),
3306 auto_reset_zoom: true,
3307 interaction_mode: PlotInteractionMode::Zoom,
3308 active_item: None,
3309 active_curve_style: CurveStyle {
3310 line_width: Some(2.0),
3311 ..CurveStyle::default()
3312 },
3313 active_curve_handling: true,
3314 default_plot_lines: true,
3317 default_plot_points: false,
3318 events: Vec::new(),
3319 rename_state: None,
3320 print_dialog: crate::widget::print_dialog::PrintDialog::new(),
3321 ruler_active: false,
3322 ruler_roi: None,
3323 ruler_prev_mode: None,
3324 }
3325 }
3326
3327 pub fn show(&mut self, ui: &mut egui::Ui) -> PlotResponse {
3329 self.show_inner(ui, None)
3330 }
3331
3332 pub fn show_with_context_menu(
3345 &mut self,
3346 ui: &mut egui::Ui,
3347 mut menu_ext: impl FnMut(&mut egui::Ui),
3348 ) -> PlotResponse {
3349 self.show_inner(ui, Some(&mut menu_ext))
3350 }
3351
3352 fn show_inner(
3353 &mut self,
3354 ui: &mut egui::Ui,
3355 menu_ext: Option<&mut dyn FnMut(&mut egui::Ui)>,
3356 ) -> PlotResponse {
3357 let before = self.limits_snapshot();
3358 self.sync_active_axis_labels();
3359 let mut view = PlotView::new();
3360 if let Some(ext) = menu_ext {
3361 view = view.with_context_menu(ext);
3362 }
3363 let response =
3364 view.show_with_interaction(ui, self.backend.plot_mut(), self.interaction_mode);
3365 self.backend
3366 .set_plot_bounds_in_pixels(response.transform.area);
3367 self.select_item_from_plot_response(&response);
3368 self.emit_item_pointer_events(&response);
3369 self.push_limits_changed_if(before);
3370 if let Some(index) = response.roi_changed {
3371 self.events.push(PlotEvent::RoiChanged { index });
3372 }
3373 if let Some(index) = response.roi_created {
3374 self.events.push(PlotEvent::RoiAdded { index });
3377 self.events.push(PlotEvent::RoiCreated { index });
3378 }
3379 if self.ruler_active {
3384 if response.roi_created.is_some() {
3385 if let Some(old) = self.ruler_roi.take() {
3389 self.remove_roi(old);
3390 }
3391 let index = self.backend.plot().rois.len().saturating_sub(1);
3392 self.ruler_roi = Some(index);
3393 self.relabel_ruler(index);
3394 } else if let Some(index) = response.roi_changed
3395 && Some(index) == self.ruler_roi
3396 {
3397 self.relabel_ruler(index);
3398 }
3399 }
3400 if let Some(index) = response.roi_make_current {
3408 self.set_current_roi(Some(index));
3409 }
3410 if let Some((index, mode)) = response.roi_set_interaction_mode {
3411 self.set_roi_interaction_mode(index, mode);
3412 }
3413 if let Some(index) = response.roi_removed {
3414 self.remove_roi(index);
3415 }
3416 match response.draw_event.clone() {
3420 Some(DrawEvent::InProgress { mode, points }) => {
3421 self.events
3422 .push(PlotEvent::DrawingProgress { mode, points });
3423 }
3424 Some(DrawEvent::Finished { mode, params }) => {
3425 self.events
3426 .push(PlotEvent::DrawingFinished { mode, params });
3427 }
3428 None => {}
3429 }
3430 if let Some(handle) = response.marker_drag_started {
3435 self.events.push(PlotEvent::MarkerDragStarted { handle });
3436 }
3437 if let Some(handle) = response.marker_moved {
3443 let plot = self.backend.plot();
3444 let moved = plot
3445 .marker_handles
3446 .iter()
3447 .position(|&h| h == handle)
3448 .and_then(|index| plot.markers.get(index).cloned());
3449 if let Some(marker) = moved {
3450 self.backend.update_marker(handle, marker);
3451 self.events.push(PlotEvent::MarkerMoved { handle });
3452 }
3453 }
3454 if let Some(handle) = response.marker_drag_finished {
3457 self.events.push(PlotEvent::MarkerDragFinished { handle });
3458 }
3459 response
3460 }
3461
3462 fn sync_active_axis_labels(&mut self) {
3469 let (x, y, y2) = self
3470 .active_curve()
3471 .and_then(|handle| self.item_record(handle))
3472 .map(|record| {
3473 let y_axis = record
3474 .curve_data
3475 .as_ref()
3476 .map(|data| data.y_axis)
3477 .unwrap_or(YAxis::Left);
3478 active_axis_label_overrides(
3479 record.x_label.as_deref(),
3480 record.y_label.as_deref(),
3481 y_axis,
3482 )
3483 })
3484 .unwrap_or((None, None, None));
3485 let plot = self.backend.plot_mut();
3486 plot.active_x_label = x;
3487 plot.active_y_label = y;
3488 plot.active_y2_label = y2;
3489 }
3490
3491 pub fn plot(&self) -> &Plot {
3493 self.backend.plot()
3494 }
3495
3496 pub fn plot_mut(&mut self) -> &mut Plot {
3498 self.backend.plot_mut()
3499 }
3500
3501 pub fn backend(&self) -> &WgpuBackend {
3503 &self.backend
3504 }
3505
3506 pub fn backend_mut(&mut self) -> &mut WgpuBackend {
3508 &mut self.backend
3509 }
3510
3511 pub fn set_auto_reset_zoom(&mut self, on: bool) {
3513 self.auto_reset_zoom = on;
3514 }
3515
3516 pub fn auto_reset_zoom(&self) -> bool {
3518 self.auto_reset_zoom
3519 }
3520
3521 pub fn set_interaction_mode(&mut self, mode: PlotInteractionMode) {
3523 self.interaction_mode = mode;
3524 }
3525
3526 pub fn interaction_mode(&self) -> PlotInteractionMode {
3528 self.interaction_mode
3529 }
3530
3531 pub fn set_roi_create_mode(&mut self, kind: RoiDrawKind) {
3539 self.set_interaction_mode(PlotInteractionMode::RoiCreate(kind));
3540 }
3541
3542 pub fn roi_creation_message(&self) -> Option<String> {
3549 self.interaction_mode
3550 .roi_creation_message(self.backend.plot().rois.len())
3551 }
3552
3553 pub fn events(&self) -> &[PlotEvent] {
3555 &self.events
3556 }
3557
3558 pub fn drain_events(&mut self) -> Vec<PlotEvent> {
3560 mem::take(&mut self.events)
3561 }
3562
3563 fn limits_snapshot(&self) -> LimitsSnapshot {
3564 (self.backend.plot().limits, self.backend.plot().y2)
3565 }
3566
3567 fn push_limits_changed_if(&mut self, before: LimitsSnapshot) {
3568 let after = self.limits_snapshot();
3569 if before != after {
3570 let ((xmin, xmax, ymin, ymax), y2) = after;
3571 self.events.push(PlotEvent::LimitsChanged {
3572 x: (xmin, xmax),
3573 y: (ymin, ymax),
3574 y2,
3575 });
3576 }
3577 }
3578
3579 fn select_item_from_plot_response(&mut self, response: &PlotResponse) {
3580 if !response.response.clicked_by(egui::PointerButton::Primary) {
3581 return;
3582 }
3583 let Some(pos) = response.response.interact_pointer_pos() else {
3584 return;
3585 };
3586 if !response.transform.area.contains(pos) {
3587 return;
3588 }
3589 if let Some(handle) = self.pick_topmost_item(pos) {
3590 self.set_active_item(Some(handle));
3591 }
3592 }
3593
3594 fn pick_topmost(&self, pos: egui::Pos2) -> Option<(ItemHandle, PickResult)> {
3599 self.backend
3600 .items_back_to_front()
3601 .into_iter()
3602 .rev()
3603 .find_map(|handle| {
3604 self.backend
3605 .pick_item(pos, handle)
3606 .map(|pick| (handle, pick))
3607 })
3608 }
3609
3610 fn pick_topmost_item(&self, pos: egui::Pos2) -> Option<ItemHandle> {
3611 self.pick_topmost(pos).map(|(handle, _)| handle)
3612 }
3613
3614 fn click_event_for_pick(
3619 handle: ItemHandle,
3620 pick: &PickResult,
3621 button: MouseButton,
3622 ) -> PlotEvent {
3623 match *pick {
3624 PickResult::CurvePoint { index, x, y, .. } => PlotEvent::CurveClicked {
3625 handle,
3626 index,
3627 x,
3628 y,
3629 button,
3630 },
3631 PickResult::ImagePixel { col, row } => PlotEvent::ImageClicked {
3632 handle,
3633 col,
3634 row,
3635 button,
3636 },
3637 PickResult::Item { .. } => PlotEvent::ItemClicked { handle, button },
3638 }
3639 }
3640
3641 fn emit_item_pointer_events(&mut self, response: &PlotResponse) {
3649 use crate::widget::interaction::PlotPointerEvent;
3650 let Some(event) = response.pointer_event.as_ref() else {
3651 return;
3652 };
3653 match *event {
3654 PlotPointerEvent::Clicked { button, pixel, .. } => {
3655 let pos = egui::pos2(pixel.0, pixel.1);
3656 if let Some((handle, pick)) = self.pick_topmost(pos) {
3657 self.events
3658 .push(Self::click_event_for_pick(handle, &pick, button));
3659 }
3660 }
3661 PlotPointerEvent::Moved {
3662 button: None,
3663 data,
3664 pixel,
3665 } => {
3666 let pos = egui::pos2(pixel.0, pixel.1);
3667 if let Some((handle, _)) = self.pick_topmost(pos)
3668 && let Some(kind) = self.item_kind(handle)
3669 {
3670 let label = self.item_legend(handle).map(str::to_owned);
3674 let draggable = self.backend.marker(handle).is_some_and(|m| m.is_draggable);
3675 self.events.push(PlotEvent::ItemHovered {
3676 handle,
3677 kind,
3678 label,
3679 x: data.0,
3680 y: data.1,
3681 xpixel: pixel.0,
3682 ypixel: pixel.1,
3683 draggable,
3684 });
3685 }
3686 }
3687 _ => {}
3688 }
3689 }
3690
3691 fn set_limits_internal(
3692 &mut self,
3693 xmin: f64,
3694 xmax: f64,
3695 ymin: f64,
3696 ymax: f64,
3697 y2: Option<(f64, f64)>,
3698 ) {
3699 let before = self.limits_snapshot();
3700 self.backend.set_limits(xmin, xmax, ymin, ymax, y2);
3701 self.push_limits_changed_if(before);
3702 }
3703
3704 fn record_item(
3705 &mut self,
3706 handle: ItemHandle,
3707 kind: PlotItemKind,
3708 bounds: DataBounds,
3709 stats: Option<ItemStats>,
3710 visual: LegendVisual,
3711 ) {
3712 self.item_records.push(ItemRecord {
3713 handle,
3714 kind,
3715 bounds,
3716 legend: None,
3717 x_label: None,
3718 y_label: None,
3719 stats,
3720 visual,
3721 data: None,
3722 curve_data: None,
3723 hidden_symbol: None,
3724 hidden_line_style: None,
3725 });
3726 self.events.push(PlotEvent::ItemAdded { handle, kind });
3727 if self.active_item.is_none() {
3728 self.set_active_item(Some(handle));
3729 }
3730 self.recompute_data_bounds();
3731 self.apply_auto_limits();
3732 }
3733
3734 fn update_item_record(
3735 &mut self,
3736 handle: ItemHandle,
3737 kind: PlotItemKind,
3738 bounds: DataBounds,
3739 stats: Option<ItemStats>,
3740 visual: LegendVisual,
3741 ) {
3742 if let Some(record) = self
3743 .item_records
3744 .iter_mut()
3745 .find(|record| record.handle == handle)
3746 {
3747 record.kind = kind;
3748 record.bounds = bounds;
3749 record.stats = stats;
3750 record.visual = visual;
3751 self.events.push(PlotEvent::ItemUpdated { handle, kind });
3755 } else {
3756 self.item_records.push(ItemRecord {
3757 handle,
3758 kind,
3759 bounds,
3760 legend: None,
3761 x_label: None,
3762 y_label: None,
3763 stats,
3764 visual,
3765 data: None,
3766 curve_data: None,
3767 hidden_symbol: None,
3768 hidden_line_style: None,
3769 });
3770 self.events.push(PlotEvent::ItemAdded { handle, kind });
3771 }
3772 self.recompute_data_bounds();
3773 self.apply_auto_limits();
3774 }
3775
3776 fn recompute_data_bounds(&mut self) {
3777 let mut bounds = DataBounds::default();
3778 for record in &self.item_records {
3779 bounds.include_bounds(&record.bounds);
3780 }
3781 self.data_bounds = bounds;
3782 self.backend
3790 .plot_mut()
3791 .set_data_range(raw_data_range_from_bounds(&self.data_bounds));
3792 }
3793
3794 fn remove_records_by_kinds(&mut self, predicate: impl Fn(PlotItemKind) -> bool) {
3795 let removed: Vec<(ItemHandle, PlotItemKind)> = self
3796 .item_records
3797 .iter()
3798 .filter_map(|record| predicate(record.kind).then_some((record.handle, record.kind)))
3799 .collect();
3800 for (handle, _) in &removed {
3801 self.backend.remove(*handle);
3802 }
3803 self.item_records.retain(|record| !predicate(record.kind));
3804 for (handle, kind) in removed {
3805 self.events.push(PlotEvent::ItemRemoved { handle, kind });
3806 }
3807 self.clear_active_if_missing();
3808 self.recompute_data_bounds();
3809 self.apply_auto_limits();
3810 }
3811
3812 fn has_item(&self, handle: ItemHandle) -> bool {
3813 self.item_records
3814 .iter()
3815 .any(|record| record.handle == handle)
3816 }
3817
3818 fn item_record(&self, handle: ItemHandle) -> Option<&ItemRecord> {
3819 self.item_records
3820 .iter()
3821 .find(|record| record.handle == handle)
3822 }
3823
3824 fn item_record_mut(&mut self, handle: ItemHandle) -> Option<&mut ItemRecord> {
3825 self.item_records
3826 .iter_mut()
3827 .find(|record| record.handle == handle)
3828 }
3829
3830 fn set_retained_data(&mut self, handle: ItemHandle, data: Option<RetainedItemData>) {
3834 if let Some(record) = self.item_record_mut(handle) {
3835 record.data = data;
3836 }
3837 }
3838
3839 fn retained_data(&self, handle: ItemHandle) -> Option<&RetainedItemData> {
3841 self.item_record(handle)
3842 .and_then(|record| record.data.as_ref())
3843 }
3844
3845 fn set_record_curve_data(&mut self, handle: ItemHandle, curve_data: Option<CurveData>) {
3849 if let Some(record) = self.item_record_mut(handle) {
3850 record.curve_data = curve_data;
3851 }
3852 }
3853
3854 fn record_curve_data(&self, handle: ItemHandle) -> Option<&CurveData> {
3856 self.item_record(handle)
3857 .and_then(|record| record.curve_data.as_ref())
3858 }
3859
3860 fn is_highlighted_curve(&self, handle: ItemHandle) -> bool {
3869 self.active_curve_handling
3870 && self.active_item == Some(handle)
3871 && self.item_kind(handle) == Some(PlotItemKind::Curve)
3872 }
3873
3874 fn sync_curve_highlight(&mut self, handle: ItemHandle) {
3884 let Some(base) = self.record_curve_data(handle).cloned() else {
3885 return;
3886 };
3887 let highlighted = self.is_highlighted_curve(handle);
3888 let effective = current_curve_style(&base, &self.active_curve_style, highlighted);
3889 self.backend
3890 .update_curve(handle, curve_spec_from_data(&effective));
3891 }
3892
3893 fn clear_active_if_missing(&mut self) {
3894 if self
3895 .active_item
3896 .is_some_and(|handle| !self.has_item(handle))
3897 {
3898 let previous = self.active_item.take();
3899 self.events.push(PlotEvent::ActiveItemChanged {
3900 previous,
3901 current: None,
3902 });
3903 }
3904 }
3905
3906 pub fn add_curve(&mut self, x: &[f64], y: &[f64], color: Color32) -> ItemHandle {
3908 self.add_curve_spec(CurveSpec::new(x, y, color))
3909 }
3910
3911 pub fn add_curve_with_legend(
3913 &mut self,
3914 x: &[f64],
3915 y: &[f64],
3916 color: Color32,
3917 legend: impl Into<String>,
3918 ) -> ItemHandle {
3919 let handle = self.add_curve(x, y, color);
3920 self.set_item_legend(handle, legend);
3921 handle
3922 }
3923
3924 pub fn add_curve_data(&mut self, curve: &CurveData) -> ItemHandle {
3926 self.add_curve_spec(curve_spec_from_data(curve))
3927 }
3928
3929 pub fn add_curve_data_with_legend(
3931 &mut self,
3932 curve: &CurveData,
3933 legend: impl Into<String>,
3934 ) -> ItemHandle {
3935 let handle = self.add_curve_data(curve);
3936 self.set_item_legend(handle, legend);
3937 handle
3938 }
3939
3940 pub fn add_curve_spec(&mut self, spec: CurveSpec<'_>) -> ItemHandle {
3942 self.add_curve_spec_as_kind(spec, PlotItemKind::Curve)
3943 }
3944
3945 fn add_curve_spec_as_kind(&mut self, spec: CurveSpec<'_>, kind: PlotItemKind) -> ItemHandle {
3946 let bounds = curve_spec_bounds(&spec);
3947 let stats = Some(curve_spec_stats(&spec));
3948 let visual = curve_spec_legend_visual(&spec, kind);
3949 let data = curve_spec_retained_data(&spec);
3950 let curve_data = curve_data_from_spec_hl(&spec);
3951 let x_label = spec.x_label.map(ToOwned::to_owned);
3955 let y_label = spec.y_label.map(ToOwned::to_owned);
3956 let handle = self.backend.add_curve(spec);
3957 self.record_item(handle, kind, bounds, stats, visual);
3958 self.set_retained_data(handle, Some(data));
3959 self.set_record_curve_data(handle, Some(curve_data));
3960 if let Some(record) = self.item_record_mut(handle) {
3961 record.x_label = x_label;
3962 record.y_label = y_label;
3963 }
3964 handle
3965 }
3966
3967 pub fn update_curve_spec(&mut self, handle: ItemHandle, spec: CurveSpec<'_>) -> bool {
3969 let bounds = curve_spec_bounds(&spec);
3970 let stats = Some(curve_spec_stats(&spec));
3971 let kind = self
3972 .item_kind(handle)
3973 .filter(|kind| kind.is_curve_like())
3974 .unwrap_or(PlotItemKind::Curve);
3975 let visual = curve_spec_legend_visual(&spec, kind);
3976 let data = curve_spec_retained_data(&spec);
3977 let curve_data = curve_data_from_spec_hl(&spec);
3978 if self.backend.update_curve(handle, spec) {
3979 self.update_item_record(handle, kind, bounds, stats, visual);
3980 self.set_retained_data(handle, Some(data));
3981 self.set_record_curve_data(handle, Some(curve_data));
3982 if self.is_highlighted_curve(handle) {
3988 self.sync_curve_highlight(handle);
3989 }
3990 true
3991 } else {
3992 false
3993 }
3994 }
3995
3996 pub fn update_curve_data(&mut self, handle: ItemHandle, curve: &CurveData) -> bool {
3998 self.update_curve_spec(handle, curve_spec_from_data(curve))
3999 }
4000
4001 pub fn add_scatter(&mut self, x: &[f64], y: &[f64], color: Color32) -> ItemHandle {
4003 self.add_scatter_with_symbol(x, y, color, Symbol::Circle, 7.0)
4004 }
4005
4006 pub fn add_scatter_with_legend(
4008 &mut self,
4009 x: &[f64],
4010 y: &[f64],
4011 color: Color32,
4012 legend: impl Into<String>,
4013 ) -> ItemHandle {
4014 let handle = self.add_scatter(x, y, color);
4015 self.set_item_legend(handle, legend);
4016 handle
4017 }
4018
4019 pub fn add_scatter_with_symbol(
4021 &mut self,
4022 x: &[f64],
4023 y: &[f64],
4024 color: Color32,
4025 symbol: Symbol,
4026 symbol_size: f32,
4027 ) -> ItemHandle {
4028 let mut spec = CurveSpec::new(x, y, color);
4029 spec.line_style = LineStyle::None;
4030 spec.line_width = 0.0;
4031 spec.symbol = Some(symbol);
4032 spec.symbol_size = symbol_size;
4033 self.add_curve_spec_as_kind(spec, PlotItemKind::Scatter)
4034 }
4035
4036 pub fn add_histogram(
4038 &mut self,
4039 edges: &[f64],
4040 counts: &[f64],
4041 color: Color32,
4042 ) -> Result<ItemHandle, PlotDataError> {
4043 let (x, y) = histogram_step_values(edges, counts)?;
4044 let mut spec = CurveSpec::new(&x, &y, color);
4045 spec.fill = true;
4046 spec.baseline = Baseline::Scalar(0.0);
4047 Ok(self.add_curve_spec_as_kind(spec, PlotItemKind::Histogram))
4048 }
4049
4050 pub fn add_histogram_with_legend(
4052 &mut self,
4053 edges: &[f64],
4054 counts: &[f64],
4055 color: Color32,
4056 legend: impl Into<String>,
4057 ) -> Result<ItemHandle, PlotDataError> {
4058 let handle = self.add_histogram(edges, counts, color)?;
4059 self.set_item_legend(handle, legend);
4060 Ok(handle)
4061 }
4062
4063 pub fn add_histogram_aligned(
4070 &mut self,
4071 positions: &[f64],
4072 counts: &[f64],
4073 color: Color32,
4074 align: HistogramAlign,
4075 ) -> Result<ItemHandle, PlotDataError> {
4076 let edges = histogram_edges(positions, align);
4077 self.add_histogram(&edges, counts, color)
4078 }
4079
4080 pub fn add_histogram_aligned_with_legend(
4083 &mut self,
4084 positions: &[f64],
4085 counts: &[f64],
4086 color: Color32,
4087 align: HistogramAlign,
4088 legend: impl Into<String>,
4089 ) -> Result<ItemHandle, PlotDataError> {
4090 let handle = self.add_histogram_aligned(positions, counts, color, align)?;
4091 self.set_item_legend(handle, legend);
4092 Ok(handle)
4093 }
4094
4095 pub fn add_image(
4097 &mut self,
4098 width: u32,
4099 height: u32,
4100 data: &[f32],
4101 colormap: Colormap,
4102 ) -> ItemHandle {
4103 self.add_image_spec(ImageSpec::scalar(width, height, data, colormap))
4104 }
4105
4106 pub fn try_add_image(
4108 &mut self,
4109 width: u32,
4110 height: u32,
4111 data: &[f32],
4112 colormap: Colormap,
4113 ) -> Result<ItemHandle, PlotDataError> {
4114 validate_image_len(width, height, data.len())?;
4115 Ok(self.add_image(width, height, data, colormap))
4116 }
4117
4118 pub fn add_image_default(&mut self, width: u32, height: u32, data: &[f32]) -> ItemHandle {
4120 self.add_image(width, height, data, self.default_colormap.clone())
4121 }
4122
4123 pub fn try_add_image_default(
4126 &mut self,
4127 width: u32,
4128 height: u32,
4129 data: &[f32],
4130 ) -> Result<ItemHandle, PlotDataError> {
4131 self.try_add_image(width, height, data, self.default_colormap.clone())
4132 }
4133
4134 pub fn add_image_with_geometry(
4136 &mut self,
4137 width: u32,
4138 height: u32,
4139 data: &[f32],
4140 colormap: Colormap,
4141 geometry: ImageGeometry,
4142 ) -> Result<ItemHandle, PlotDataError> {
4143 validate_image_len(width, height, data.len())?;
4144 let mut spec = ImageSpec::scalar(width, height, data, colormap);
4145 spec.origin = geometry.origin;
4146 spec.scale = geometry.scale;
4147 spec.alpha = geometry.alpha;
4148 Ok(self.add_image_spec(spec))
4149 }
4150
4151 pub fn add_image_with_legend(
4153 &mut self,
4154 width: u32,
4155 height: u32,
4156 data: &[f32],
4157 legend: impl Into<String>,
4158 ) -> ItemHandle {
4159 let handle = self.add_image_default(width, height, data);
4160 self.set_item_legend(handle, legend);
4161 handle
4162 }
4163
4164 pub fn add_rgba_image(&mut self, width: u32, height: u32, data: &[[u8; 4]]) -> ItemHandle {
4166 self.add_image_spec(ImageSpec::rgba(width, height, data))
4167 }
4168
4169 pub fn add_rgba_image_with_legend(
4171 &mut self,
4172 width: u32,
4173 height: u32,
4174 data: &[[u8; 4]],
4175 legend: impl Into<String>,
4176 ) -> ItemHandle {
4177 let handle = self.add_rgba_image(width, height, data);
4178 self.set_item_legend(handle, legend);
4179 handle
4180 }
4181
4182 pub fn try_add_rgba_image(
4184 &mut self,
4185 width: u32,
4186 height: u32,
4187 data: &[[u8; 4]],
4188 ) -> Result<ItemHandle, PlotDataError> {
4189 validate_image_len(width, height, data.len())?;
4190 Ok(self.add_rgba_image(width, height, data))
4191 }
4192
4193 pub fn add_rgba_image_with_geometry(
4195 &mut self,
4196 width: u32,
4197 height: u32,
4198 data: &[[u8; 4]],
4199 geometry: ImageGeometry,
4200 ) -> Result<ItemHandle, PlotDataError> {
4201 validate_image_len(width, height, data.len())?;
4202 let mut spec = ImageSpec::rgba(width, height, data);
4203 spec.origin = geometry.origin;
4204 spec.scale = geometry.scale;
4205 spec.alpha = geometry.alpha;
4206 Ok(self.add_image_spec(spec))
4207 }
4208
4209 pub fn add_image_data(&mut self, image: &ImageData) -> ItemHandle {
4211 self.add_image_spec(image_spec_from_data(image))
4212 }
4213
4214 pub fn add_image_spec(&mut self, spec: ImageSpec<'_>) -> ItemHandle {
4216 self.add_image_spec_as_kind(spec, PlotItemKind::Image)
4217 }
4218
4219 fn add_image_spec_as_kind(&mut self, spec: ImageSpec<'_>, kind: PlotItemKind) -> ItemHandle {
4220 let bounds = image_spec_bounds(&spec);
4221 let stats = Some(image_spec_stats(&spec));
4222 let visual = image_spec_legend_visual(&spec, kind);
4223 let data = image_spec_retained_data(&spec);
4224 let handle = self.backend.add_image(spec);
4225 self.record_item(handle, kind, bounds, stats, visual);
4226 self.set_retained_data(handle, data);
4227 handle
4228 }
4229
4230 pub fn update_image_spec(&mut self, handle: ItemHandle, spec: ImageSpec<'_>) -> bool {
4232 let bounds = image_spec_bounds(&spec);
4233 let stats = Some(image_spec_stats(&spec));
4234 let kind = self
4235 .item_kind(handle)
4236 .filter(|kind| kind.is_image_like())
4237 .unwrap_or(PlotItemKind::Image);
4238 let visual = image_spec_legend_visual(&spec, kind);
4239 let data = image_spec_retained_data(&spec);
4240 if self.backend.update_image(handle, spec) {
4241 self.update_item_record(handle, kind, bounds, stats, visual);
4242 self.set_retained_data(handle, data);
4243 true
4244 } else {
4245 false
4246 }
4247 }
4248
4249 pub fn try_update_image(
4252 &mut self,
4253 handle: ItemHandle,
4254 width: u32,
4255 height: u32,
4256 data: &[f32],
4257 colormap: Colormap,
4258 ) -> Result<bool, PlotDataError> {
4259 validate_image_len(width, height, data.len())?;
4260 Ok(self.update_image_spec(handle, ImageSpec::scalar(width, height, data, colormap)))
4261 }
4262
4263 pub fn try_update_rgba_image(
4266 &mut self,
4267 handle: ItemHandle,
4268 width: u32,
4269 height: u32,
4270 data: &[[u8; 4]],
4271 ) -> Result<bool, PlotDataError> {
4272 validate_image_len(width, height, data.len())?;
4273 Ok(self.update_image_spec(handle, ImageSpec::rgba(width, height, data)))
4274 }
4275
4276 pub fn update_image_data(&mut self, handle: ItemHandle, image: &ImageData) -> bool {
4278 self.update_image_spec(handle, image_spec_from_data(image))
4279 }
4280
4281 pub fn add_mask(
4283 &mut self,
4284 width: u32,
4285 height: u32,
4286 mask: &[bool],
4287 color: Color32,
4288 ) -> Result<ItemHandle, PlotDataError> {
4289 self.add_mask_with_geometry(width, height, mask, color, ImageGeometry::default())
4290 }
4291
4292 pub fn add_mask_with_geometry(
4294 &mut self,
4295 width: u32,
4296 height: u32,
4297 mask: &[bool],
4298 color: Color32,
4299 geometry: ImageGeometry,
4300 ) -> Result<ItemHandle, PlotDataError> {
4301 validate_image_len(width, height, mask.len())?;
4302 let rgba = mask_rgba_pixels(mask, color);
4303 let mut spec = ImageSpec::rgba(width, height, &rgba);
4304 spec.origin = geometry.origin;
4305 spec.scale = geometry.scale;
4306 spec.alpha = geometry.alpha;
4307 Ok(self.add_image_spec_as_kind(spec, PlotItemKind::Mask))
4308 }
4309
4310 pub fn add_rgba_mask(
4318 &mut self,
4319 width: u32,
4320 height: u32,
4321 pixels: &[[u8; 4]],
4322 ) -> Result<ItemHandle, PlotDataError> {
4323 validate_image_len(width, height, pixels.len())?;
4324 let spec = ImageSpec::rgba(width, height, pixels);
4325 Ok(self.add_image_spec_as_kind(spec, PlotItemKind::Mask))
4326 }
4327
4328 pub fn add_mask_with_legend(
4330 &mut self,
4331 width: u32,
4332 height: u32,
4333 mask: &[bool],
4334 color: Color32,
4335 legend: impl Into<String>,
4336 ) -> Result<ItemHandle, PlotDataError> {
4337 let handle = self.add_mask(width, height, mask, color)?;
4338 self.set_item_legend(handle, legend);
4339 Ok(handle)
4340 }
4341
4342 pub fn add_horizontal_profile_curve(
4344 &mut self,
4345 width: u32,
4346 height: u32,
4347 data: &[f32],
4348 row: u32,
4349 color: Color32,
4350 ) -> Result<ItemHandle, PlotDataError> {
4351 let y = horizontal_profile_values(width, height, data, row)?;
4352 let x: Vec<f64> = (0..width).map(|col| col as f64).collect();
4353 Ok(self.add_curve(&x, &y, color))
4354 }
4355
4356 pub fn add_vertical_profile_curve(
4358 &mut self,
4359 width: u32,
4360 height: u32,
4361 data: &[f32],
4362 column: u32,
4363 color: Color32,
4364 ) -> Result<ItemHandle, PlotDataError> {
4365 let y = vertical_profile_values(width, height, data, column)?;
4366 let x: Vec<f64> = (0..height).map(|row| row as f64).collect();
4367 Ok(self.add_curve(&x, &y, color))
4368 }
4369
4370 pub fn add_triangles(&mut self, spec: TriangleSpec<'_>) -> ItemHandle {
4372 let bounds = xy_bounds(spec.x, spec.y, YAxis::Left);
4373 let visual = triangle_spec_legend_visual(&spec);
4374 let handle = self.backend.add_triangles(spec);
4375 self.record_item(handle, PlotItemKind::Triangles, bounds, None, visual);
4376 handle
4377 }
4378
4379 pub fn add_triangles_data(&mut self, triangles: &Triangles) -> ItemHandle {
4381 self.add_triangles(triangle_spec_from_data(triangles))
4382 }
4383
4384 pub fn add_triangles_data_with_legend(
4386 &mut self,
4387 triangles: &Triangles,
4388 legend: impl Into<String>,
4389 ) -> ItemHandle {
4390 let handle = self.add_triangles_data(triangles);
4391 self.set_item_legend(handle, legend);
4392 handle
4393 }
4394
4395 pub fn add_shape(&mut self, spec: ShapeSpec<'_>) -> ItemHandle {
4397 let bounds = xy_bounds(spec.x, spec.y, YAxis::Left);
4398 let visual = shape_spec_legend_visual(&spec);
4399 let handle = self.backend.add_shape(spec);
4400 self.record_item(handle, PlotItemKind::Shape, bounds, None, visual);
4401 handle
4402 }
4403
4404 pub fn add_shape_data(&mut self, shape: &Shape) -> ItemHandle {
4406 self.add_shape(shape_spec_from_data(shape))
4407 }
4408
4409 pub fn add_shape_data_with_legend(
4411 &mut self,
4412 shape: &Shape,
4413 legend: impl Into<String>,
4414 ) -> ItemHandle {
4415 let handle = self.add_shape_data(shape);
4416 self.set_item_legend(handle, legend);
4417 handle
4418 }
4419
4420 pub fn add_rectangle(
4422 &mut self,
4423 x0: f64,
4424 y0: f64,
4425 x1: f64,
4426 y1: f64,
4427 color: Color32,
4428 fill: bool,
4429 ) -> ItemHandle {
4430 let x = [x0, x1];
4431 let y = [y0, y1];
4432 self.add_shape(ShapeSpec {
4433 x: &x,
4434 y: &y,
4435 kind: ShapeKind::Rectangle,
4436 color,
4437 fill,
4438 overlay: false,
4439 line_style: LineStyle::Solid,
4440 line_width: 1.0,
4441 gap_color: None,
4442 })
4443 }
4444
4445 pub fn add_marker(&mut self, spec: MarkerSpec<'_>) -> ItemHandle {
4447 let visual = marker_spec_legend_visual(&spec);
4448 let handle = self.backend.add_marker(spec);
4449 self.record_item(
4450 handle,
4451 PlotItemKind::Marker,
4452 DataBounds::default(),
4453 None,
4454 visual,
4455 );
4456 handle
4457 }
4458
4459 pub fn add_marker_data(&mut self, marker: &Marker) -> ItemHandle {
4461 self.add_marker(marker_spec_from_data(marker))
4462 }
4463
4464 pub fn add_marker_data_with_legend(
4466 &mut self,
4467 marker: &Marker,
4468 legend: impl Into<String>,
4469 ) -> ItemHandle {
4470 let handle = self.add_marker_data(marker);
4471 self.set_item_legend(handle, legend);
4472 handle
4473 }
4474
4475 pub fn add_point_marker(
4477 &mut self,
4478 x: f64,
4479 y: f64,
4480 color: Color32,
4481 symbol: Symbol,
4482 ) -> ItemHandle {
4483 self.add_marker(MarkerSpec {
4484 x: Some(x),
4485 y: Some(y),
4486 text: None,
4487 color,
4488 symbol: Some(symbol),
4489 symbol_size: 8.0,
4490 line_style: LineStyle::Solid,
4491 line_width: 1.0,
4492 y_axis: YAxis::Left,
4493 bg_color: None,
4494 is_draggable: false,
4495 constraint: MarkerConstraint::None,
4496 })
4497 }
4498
4499 pub fn add_x_marker(&mut self, x: f64, color: Color32) -> ItemHandle {
4501 self.add_marker(MarkerSpec {
4502 x: Some(x),
4503 y: None,
4504 text: None,
4505 color,
4506 symbol: None,
4507 symbol_size: 0.0,
4508 line_style: LineStyle::Solid,
4509 line_width: 1.0,
4510 y_axis: YAxis::Left,
4511 bg_color: None,
4512 is_draggable: false,
4513 constraint: MarkerConstraint::None,
4514 })
4515 }
4516
4517 pub fn add_y_marker(&mut self, y: f64, color: Color32, axis: YAxis) -> ItemHandle {
4519 self.add_marker(MarkerSpec {
4520 x: None,
4521 y: Some(y),
4522 text: None,
4523 color,
4524 symbol: None,
4525 symbol_size: 0.0,
4526 line_style: LineStyle::Solid,
4527 line_width: 1.0,
4528 y_axis: axis,
4529 bg_color: None,
4530 is_draggable: false,
4531 constraint: MarkerConstraint::None,
4532 })
4533 }
4534
4535 pub fn marker_position(&self, handle: ItemHandle) -> Option<(f64, f64)> {
4540 self.backend.marker(handle).map(Marker::position)
4541 }
4542
4543 pub fn set_marker_position(&mut self, handle: ItemHandle, x: f64, y: f64) -> bool {
4555 let Some(mut marker) = self.backend.marker(handle).cloned() else {
4556 return false;
4557 };
4558 marker.drag(marker.position(), (x, y));
4559 self.backend.update_marker(handle, marker);
4560 self.events.push(PlotEvent::MarkerMoved { handle });
4561 true
4562 }
4563
4564 pub fn remove(&mut self, handle: ItemHandle) -> bool {
4566 let kind = self.item_record(handle).map(|record| record.kind);
4567 let removed = self.backend.remove(handle);
4568 if removed {
4569 self.item_records.retain(|record| record.handle != handle);
4570 if let Some(kind) = kind {
4571 self.events.push(PlotEvent::ItemRemoved { handle, kind });
4572 }
4573 self.clear_active_if_missing();
4574 self.recompute_data_bounds();
4575 self.apply_auto_limits();
4576 }
4577 removed
4578 }
4579
4580 pub fn clear(&mut self) {
4582 let removed: Vec<(ItemHandle, PlotItemKind)> = self
4583 .item_records
4584 .iter()
4585 .map(|record| (record.handle, record.kind))
4586 .collect();
4587 self.backend.clear_items();
4588 self.item_records.clear();
4589 for (handle, kind) in removed {
4590 self.events.push(PlotEvent::ItemRemoved { handle, kind });
4591 }
4592 self.clear_active_if_missing();
4593 self.recompute_data_bounds();
4594 }
4595
4596 pub fn clear_curves(&mut self) {
4598 self.remove_records_by_kinds(PlotItemKind::is_curve_like);
4599 }
4600
4601 pub fn clear_images(&mut self) {
4603 self.remove_records_by_kinds(PlotItemKind::is_image_like);
4604 }
4605
4606 pub fn clear_items(&mut self) {
4608 self.remove_records_by_kinds(|kind| {
4609 matches!(kind, PlotItemKind::Shape | PlotItemKind::Triangles)
4610 });
4611 }
4612
4613 pub fn clear_markers(&mut self) {
4615 self.remove_records_by_kinds(|kind| kind == PlotItemKind::Marker);
4616 }
4617
4618 pub fn clear_histograms(&mut self) {
4620 self.remove_records_by_kinds(|kind| kind == PlotItemKind::Histogram);
4621 }
4622
4623 pub fn clear_scatters(&mut self) {
4625 self.remove_records_by_kinds(|kind| kind == PlotItemKind::Scatter);
4626 }
4627
4628 pub fn clear_masks(&mut self) {
4630 self.remove_records_by_kinds(|kind| kind == PlotItemKind::Mask);
4631 }
4632
4633 fn remove_if_kind(
4634 &mut self,
4635 handle: ItemHandle,
4636 predicate: impl Fn(PlotItemKind) -> bool,
4637 ) -> bool {
4638 if self.item_kind(handle).is_some_and(predicate) {
4639 self.remove(handle)
4640 } else {
4641 false
4642 }
4643 }
4644
4645 pub fn remove_curve(&mut self, handle: ItemHandle) -> bool {
4647 self.remove_if_kind(handle, PlotItemKind::is_curve_like)
4648 }
4649
4650 pub fn remove_image(&mut self, handle: ItemHandle) -> bool {
4652 self.remove_if_kind(handle, PlotItemKind::is_image_like)
4653 }
4654
4655 pub fn remove_histogram(&mut self, handle: ItemHandle) -> bool {
4657 self.remove_if_kind(handle, |kind| kind == PlotItemKind::Histogram)
4658 }
4659
4660 pub fn remove_scatter(&mut self, handle: ItemHandle) -> bool {
4662 self.remove_if_kind(handle, |kind| kind == PlotItemKind::Scatter)
4663 }
4664
4665 pub fn remove_mask(&mut self, handle: ItemHandle) -> bool {
4667 self.remove_if_kind(handle, |kind| kind == PlotItemKind::Mask)
4668 }
4669
4670 pub fn remove_marker(&mut self, handle: ItemHandle) -> bool {
4672 self.remove_if_kind(handle, |kind| kind == PlotItemKind::Marker)
4673 }
4674
4675 pub fn remove_overlay_item(&mut self, handle: ItemHandle) -> bool {
4677 self.remove_if_kind(handle, |kind| {
4678 matches!(kind, PlotItemKind::Shape | PlotItemKind::Triangles)
4679 })
4680 }
4681
4682 pub fn get_items(&self) -> Vec<ItemHandle> {
4684 self.backend.items_back_to_front()
4685 }
4686
4687 pub fn get_all_curves(&self) -> Vec<ItemHandle> {
4689 self.handles_by_predicate(PlotItemKind::is_curve_like)
4690 }
4691
4692 pub fn get_all_images(&self) -> Vec<ItemHandle> {
4694 self.handles_by_predicate(PlotItemKind::is_image_like)
4695 }
4696
4697 pub fn get_all_markers(&self) -> Vec<ItemHandle> {
4699 self.handles_by_kind(PlotItemKind::Marker)
4700 }
4701
4702 pub fn get_all_histograms(&self) -> Vec<ItemHandle> {
4704 self.handles_by_kind(PlotItemKind::Histogram)
4705 }
4706
4707 pub fn get_all_scatters(&self) -> Vec<ItemHandle> {
4709 self.handles_by_kind(PlotItemKind::Scatter)
4710 }
4711
4712 pub fn get_all_masks(&self) -> Vec<ItemHandle> {
4714 self.handles_by_kind(PlotItemKind::Mask)
4715 }
4716
4717 fn handles_by_kind(&self, kind: PlotItemKind) -> Vec<ItemHandle> {
4718 self.handles_by_predicate(|record_kind| record_kind == kind)
4719 }
4720
4721 fn handles_by_predicate(&self, predicate: impl Fn(PlotItemKind) -> bool) -> Vec<ItemHandle> {
4722 self.item_records
4723 .iter()
4724 .filter_map(|record| predicate(record.kind).then_some(record.handle))
4725 .collect()
4726 }
4727
4728 fn handle_by_legend_and_kind(
4729 &self,
4730 legend: &str,
4731 predicate: impl Fn(PlotItemKind) -> bool,
4732 ) -> Option<ItemHandle> {
4733 self.item_records.iter().find_map(|record| {
4734 (record.legend.as_deref() == Some(legend) && predicate(record.kind))
4735 .then_some(record.handle)
4736 })
4737 }
4738
4739 pub fn item_by_legend(&self, legend: &str) -> Option<ItemHandle> {
4741 self.handle_by_legend_and_kind(legend, |_| true)
4742 }
4743
4744 pub fn curve_by_legend(&self, legend: &str) -> Option<ItemHandle> {
4746 self.handle_by_legend_and_kind(legend, PlotItemKind::is_curve_like)
4747 }
4748
4749 pub fn image_by_legend(&self, legend: &str) -> Option<ItemHandle> {
4751 self.handle_by_legend_and_kind(legend, PlotItemKind::is_image_like)
4752 }
4753
4754 pub fn histogram_by_legend(&self, legend: &str) -> Option<ItemHandle> {
4756 self.handle_by_legend_and_kind(legend, |kind| kind == PlotItemKind::Histogram)
4757 }
4758
4759 pub fn scatter_by_legend(&self, legend: &str) -> Option<ItemHandle> {
4761 self.handle_by_legend_and_kind(legend, |kind| kind == PlotItemKind::Scatter)
4762 }
4763
4764 pub fn mask_by_legend(&self, legend: &str) -> Option<ItemHandle> {
4766 self.handle_by_legend_and_kind(legend, |kind| kind == PlotItemKind::Mask)
4767 }
4768
4769 pub fn item_kind(&self, handle: ItemHandle) -> Option<PlotItemKind> {
4771 self.item_record(handle).map(|record| record.kind)
4772 }
4773
4774 pub fn set_item_legend(&mut self, handle: ItemHandle, legend: impl Into<String>) -> bool {
4776 let Some(record) = self.item_record_mut(handle) else {
4777 return false;
4778 };
4779 record.legend = Some(legend.into());
4780 let kind = record.kind;
4781 self.events.push(PlotEvent::ItemUpdated { handle, kind });
4782 true
4783 }
4784
4785 pub fn clear_item_legend(&mut self, handle: ItemHandle) -> bool {
4787 let Some(record) = self.item_record_mut(handle) else {
4788 return false;
4789 };
4790 record.legend = None;
4791 let kind = record.kind;
4792 self.events.push(PlotEvent::ItemUpdated { handle, kind });
4793 true
4794 }
4795
4796 pub fn item_legend(&self, handle: ItemHandle) -> Option<&str> {
4798 self.item_record(handle)
4799 .and_then(|record| record.legend.as_deref())
4800 }
4801
4802 pub fn set_curve_x_label(&mut self, handle: ItemHandle, label: impl Into<String>) -> bool {
4807 let Some(record) = self.item_record_mut(handle) else {
4808 return false;
4809 };
4810 record.x_label = Some(label.into());
4811 let kind = record.kind;
4812 self.events.push(PlotEvent::ItemUpdated { handle, kind });
4813 true
4814 }
4815
4816 pub fn clear_curve_x_label(&mut self, handle: ItemHandle) -> bool {
4819 let Some(record) = self.item_record_mut(handle) else {
4820 return false;
4821 };
4822 record.x_label = None;
4823 let kind = record.kind;
4824 self.events.push(PlotEvent::ItemUpdated { handle, kind });
4825 true
4826 }
4827
4828 pub fn curve_x_label(&self, handle: ItemHandle) -> Option<&str> {
4830 self.item_record(handle)
4831 .and_then(|record| record.x_label.as_deref())
4832 }
4833
4834 pub fn set_curve_y_label(&mut self, handle: ItemHandle, label: impl Into<String>) -> bool {
4838 let Some(record) = self.item_record_mut(handle) else {
4839 return false;
4840 };
4841 record.y_label = Some(label.into());
4842 let kind = record.kind;
4843 self.events.push(PlotEvent::ItemUpdated { handle, kind });
4844 true
4845 }
4846
4847 pub fn clear_curve_y_label(&mut self, handle: ItemHandle) -> bool {
4850 let Some(record) = self.item_record_mut(handle) else {
4851 return false;
4852 };
4853 record.y_label = None;
4854 let kind = record.kind;
4855 self.events.push(PlotEvent::ItemUpdated { handle, kind });
4856 true
4857 }
4858
4859 pub fn curve_y_label(&self, handle: ItemHandle) -> Option<&str> {
4861 self.item_record(handle)
4862 .and_then(|record| record.y_label.as_deref())
4863 }
4864
4865 fn legend_label(&self, record: &ItemRecord) -> String {
4866 record
4867 .legend
4868 .clone()
4869 .unwrap_or_else(|| format!("{} #{}", record.kind.as_str(), record.handle))
4870 }
4871
4872 pub fn active_item(&self) -> Option<ItemHandle> {
4874 self.active_item
4875 }
4876
4877 pub fn set_active_item(&mut self, item: Option<ItemHandle>) -> bool {
4879 if item.is_some_and(|handle| !self.has_item(handle)) {
4880 return false;
4881 }
4882 if self.active_item == item {
4883 return true;
4884 }
4885 let previous = self.active_item;
4886 self.active_item = item;
4887 if let Some(previous) = previous {
4893 self.sync_curve_highlight(previous);
4894 }
4895 if let Some(current) = item {
4896 self.sync_curve_highlight(current);
4897 }
4898 self.events.push(PlotEvent::ActiveItemChanged {
4899 previous,
4900 current: item,
4901 });
4902 true
4903 }
4904
4905 pub fn active_curve(&self) -> Option<ItemHandle> {
4907 self.active_item.filter(|handle| {
4908 self.item_kind(*handle)
4909 .is_some_and(PlotItemKind::is_curve_like)
4910 })
4911 }
4912
4913 pub fn active_curve_style(&self) -> &CurveStyle {
4916 &self.active_curve_style
4917 }
4918
4919 pub fn is_active_curve_handling(&self) -> bool {
4922 self.active_curve_handling
4923 }
4924
4925 pub fn set_active_curve_style(&mut self, style: CurveStyle) {
4929 self.active_curve_style = style;
4930 if let Some(handle) = self.active_curve() {
4931 self.sync_curve_highlight(handle);
4932 }
4933 }
4934
4935 pub fn set_active_curve_handling(&mut self, enabled: bool) {
4939 self.active_curve_handling = enabled;
4940 if let Some(handle) = self.active_curve() {
4941 self.sync_curve_highlight(handle);
4942 }
4943 }
4944
4945 pub fn active_curve_line_style(&self) -> Option<LineStyle> {
4948 let handle = self.active_curve()?;
4949 self.record_curve_data(handle)
4950 .map(|data| data.line_style.clone())
4951 }
4952
4953 pub fn is_default_plot_lines(&self) -> bool {
4956 self.default_plot_lines
4957 }
4958
4959 pub fn is_default_plot_points(&self) -> bool {
4962 self.default_plot_points
4963 }
4964
4965 pub fn set_default_plot_lines(&mut self, flag: bool) -> usize {
4973 self.default_plot_lines = flag;
4974 let line_style = if flag {
4975 LineStyle::Solid
4976 } else {
4977 LineStyle::None
4978 };
4979 let mut changed = 0;
4980 for handle in self.handles_by_kind(PlotItemKind::Curve) {
4981 let Some(mut data) = self.record_curve_data(handle).cloned() else {
4982 continue;
4983 };
4984 if data.line_style == line_style {
4985 continue;
4986 }
4987 data.line_style = line_style.clone();
4988 if self.update_curve_data(handle, &data) {
4989 changed += 1;
4990 }
4991 }
4992 changed
4993 }
4994
4995 pub fn set_default_plot_points(&mut self, flag: bool) -> usize {
5002 self.default_plot_points = flag;
5003 let symbol = if flag { Some(Symbol::Circle) } else { None };
5004 let mut changed = 0;
5005 for handle in self.handles_by_kind(PlotItemKind::Curve) {
5006 let Some(mut data) = self.record_curve_data(handle).cloned() else {
5007 continue;
5008 };
5009 if data.symbol == symbol {
5010 continue;
5011 }
5012 data.symbol = symbol;
5013 if self.update_curve_data(handle, &data) {
5014 changed += 1;
5015 }
5016 }
5017 changed
5018 }
5019
5020 pub fn cycle_curve_style(&mut self) -> (bool, bool) {
5028 let next = crate::widget::actions::control::next_curve_style_state((
5029 self.default_plot_lines,
5030 self.default_plot_points,
5031 ));
5032 self.set_default_plot_lines(next.0);
5033 self.set_default_plot_points(next.1);
5034 next
5035 }
5036
5037 pub fn set_curve_y_axis(&mut self, handle: ItemHandle, axis: YAxis) -> bool {
5044 let Some(mut data) = self.record_curve_data(handle).cloned() else {
5045 return false;
5046 };
5047 data.y_axis = axis;
5048 if !self.update_curve_data(handle, &data) {
5049 return false;
5050 }
5051 self.recompute_data_bounds();
5055 self.apply_auto_limits();
5056 true
5057 }
5058
5059 pub fn set_curve_points_visible(&mut self, handle: ItemHandle, visible: bool) -> bool {
5067 let Some(mut data) = self.record_curve_data(handle).cloned() else {
5068 return false;
5069 };
5070 let mut cache = self
5076 .item_record(handle)
5077 .and_then(|record| record.hidden_symbol);
5078 let next = set_symbol_visibility(data.symbol, visible, &mut cache);
5079 data.symbol = next;
5080 if !self.update_curve_data(handle, &data) {
5081 return false;
5082 }
5083 if let Some(record) = self.item_record_mut(handle) {
5084 record.hidden_symbol = cache;
5085 }
5086 true
5087 }
5088
5089 pub fn set_curve_lines_visible(&mut self, handle: ItemHandle, visible: bool) -> bool {
5098 let Some(mut data) = self.record_curve_data(handle).cloned() else {
5099 return false;
5100 };
5101 let mut cache = self
5105 .item_record(handle)
5106 .and_then(|record| record.hidden_line_style.clone());
5107 let next = set_line_visibility(data.line_style.clone(), visible, &mut cache);
5108 data.line_style = next;
5109 if !self.update_curve_data(handle, &data) {
5110 return false;
5111 }
5112 if let Some(record) = self.item_record_mut(handle) {
5113 record.hidden_line_style = cache;
5114 }
5115 true
5116 }
5117
5118 fn symbol_bearing_handles(&self) -> Vec<ItemHandle> {
5122 self.item_records
5123 .iter()
5124 .filter(|record| record.curve_data.is_some())
5125 .map(|record| record.handle)
5126 .collect()
5127 }
5128
5129 pub fn set_all_symbols(&mut self, symbol: Option<Symbol>) -> usize {
5136 let mut changed = 0;
5137 for handle in self.symbol_bearing_handles() {
5138 let Some(mut data) = self.record_curve_data(handle).cloned() else {
5139 continue;
5140 };
5141 if data.symbol == symbol {
5142 continue;
5143 }
5144 data.symbol = symbol;
5145 if self.update_curve_data(handle, &data) {
5146 changed += 1;
5147 }
5148 }
5149 changed
5150 }
5151
5152 pub fn set_all_symbol_sizes(&mut self, size: f32) -> usize {
5159 let mut changed = 0;
5160 for handle in self.symbol_bearing_handles() {
5161 let Some(mut data) = self.record_curve_data(handle).cloned() else {
5162 continue;
5163 };
5164 if data.marker_size == size {
5165 continue;
5166 }
5167 data.marker_size = size;
5168 if self.update_curve_data(handle, &data) {
5169 changed += 1;
5170 }
5171 }
5172 changed
5173 }
5174
5175 pub fn set_active_curve(&mut self, item: Option<ItemHandle>) -> bool {
5177 if item.is_some_and(|handle| {
5178 !self
5179 .item_kind(handle)
5180 .is_some_and(PlotItemKind::is_curve_like)
5181 }) {
5182 return false;
5183 }
5184 self.set_active_item(item)
5185 }
5186
5187 pub fn active_image(&self) -> Option<ItemHandle> {
5189 self.active_item.filter(|handle| {
5190 self.item_kind(*handle)
5191 .is_some_and(PlotItemKind::is_image_like)
5192 })
5193 }
5194
5195 pub fn set_active_image(&mut self, item: Option<ItemHandle>) -> bool {
5197 if item.is_some_and(|handle| {
5198 !self
5199 .item_kind(handle)
5200 .is_some_and(PlotItemKind::is_image_like)
5201 }) {
5202 return false;
5203 }
5204 self.set_active_item(item)
5205 }
5206
5207 pub fn set_item_visible(&mut self, handle: ItemHandle, visible: bool) -> bool {
5210 self.backend.set_item_visible(handle, visible)
5211 }
5212
5213 pub fn is_item_visible(&self, handle: ItemHandle) -> bool {
5215 self.backend.is_item_visible(handle)
5216 }
5217
5218 pub fn set_item_z(&mut self, handle: ItemHandle, z: f32) -> bool {
5222 self.backend.set_item_z(handle, z)
5223 }
5224
5225 pub fn item_z_value(&self, handle: ItemHandle) -> f32 {
5227 self.backend.item_z(handle)
5228 }
5229
5230 pub fn item_stats(&self, handle: ItemHandle) -> Option<&ItemStats> {
5232 self.item_record(handle)
5233 .and_then(|record| record.stats.as_ref())
5234 }
5235
5236 pub fn curve_stats(&self, handle: ItemHandle) -> Option<&CurveStats> {
5238 match self.item_stats(handle)? {
5239 ItemStats::Curve(stats) => Some(stats),
5240 ItemStats::Image(_) => None,
5241 }
5242 }
5243
5244 pub fn image_stats(&self, handle: ItemHandle) -> Option<&ImageStats> {
5246 match self.item_stats(handle)? {
5247 ItemStats::Curve(_) => None,
5248 ItemStats::Image(stats) => Some(stats),
5249 }
5250 }
5251
5252 pub fn show_legend(&mut self, ui: &mut egui::Ui) -> LegendResponse {
5255 let rows: Vec<(ItemHandle, PlotItemKind, String, bool, bool, LegendVisual)> = self
5256 .item_records
5257 .iter()
5258 .map(|record| {
5259 let visible = self.backend.is_item_visible(record.handle);
5260 (
5261 record.handle,
5262 record.kind,
5263 self.legend_label(record),
5264 self.active_item == Some(record.handle),
5265 visible,
5266 record.visual.clone(),
5267 )
5268 })
5269 .collect();
5270
5271 let mut out = LegendResponse::default();
5272 if rows.is_empty() {
5273 ui.label("no items");
5274 return out;
5275 }
5276
5277 egui::Frame::new()
5278 .inner_margin(1)
5279 .stroke(ui.visuals().widgets.noninteractive.bg_stroke)
5280 .show(ui, |ui| {
5281 ui.spacing_mut().item_spacing = egui::Vec2::ZERO;
5282 let width = legend_row_width(ui.available_width());
5283 for (handle, kind, label, active, visible, visual) in rows {
5284 let result =
5285 legend_row_response(ui, width, kind, &label, active, visible, visual);
5286 if result.row_clicked {
5287 out.selected = Some(handle);
5288 if self.set_active_item(Some(handle)) {
5289 out.activated = Some(handle);
5290 }
5291 }
5292 if result.eye_clicked {
5293 self.backend.set_item_visible(handle, !visible);
5294 out.visibility_changed = Some(handle);
5295 }
5296 result.row_response.context_menu(|ui| {
5299 if let Some(action) = self.legend_context_menu_ui(ui, handle, kind, active)
5300 {
5301 out.context_action = Some((handle, action));
5302 }
5303 });
5304 }
5305 });
5306 self.show_rename_popup(ui);
5310 out
5311 }
5312
5313 fn legend_context_menu_ui(
5318 &mut self,
5319 ui: &mut egui::Ui,
5320 handle: ItemHandle,
5321 kind: PlotItemKind,
5322 active: bool,
5323 ) -> Option<LegendAction> {
5324 let mut fired: Option<LegendAction> = None;
5325
5326 if ui
5329 .add_enabled(!active, egui::Button::new("Set Active"))
5330 .clicked()
5331 {
5332 self.set_active_item(Some(handle));
5333 fired = Some(LegendAction::SetActive);
5334 ui.close();
5335 }
5336
5337 if matches!(kind, PlotItemKind::Curve) {
5338 let (y_axis, symbol_visible, line_visible) = self
5340 .record_curve_data(handle)
5341 .map(|data| {
5342 (
5343 data.y_axis,
5344 data.symbol.is_some(),
5345 data.line_style.draws_line(),
5346 )
5347 })
5348 .unwrap_or((YAxis::Left, false, false));
5349
5350 if ui
5353 .add_enabled(y_axis != YAxis::Left, egui::Button::new("Map to Y Left"))
5354 .clicked()
5355 {
5356 self.set_curve_y_axis(handle, YAxis::Left);
5357 fired = Some(LegendAction::MapToLeft);
5358 ui.close();
5359 }
5360 if ui
5361 .add_enabled(y_axis != YAxis::Right, egui::Button::new("Map to Y Right"))
5362 .clicked()
5363 {
5364 self.set_curve_y_axis(handle, YAxis::Right);
5365 fired = Some(LegendAction::MapToRight);
5366 ui.close();
5367 }
5368
5369 let mut points = symbol_visible;
5372 if ui.checkbox(&mut points, "Points").clicked() {
5373 self.set_curve_points_visible(handle, points);
5374 fired = Some(LegendAction::TogglePoints);
5375 ui.close();
5376 }
5377 let mut lines = line_visible;
5378 if ui.checkbox(&mut lines, "Lines").clicked() {
5379 self.set_curve_lines_visible(handle, lines);
5380 fired = Some(LegendAction::ToggleLines);
5381 ui.close();
5382 }
5383 }
5384 ui.separator();
5389
5390 if ui.button("Rename").clicked() {
5391 let current = self
5394 .item_record(handle)
5395 .map(|record| self.legend_label(record))
5396 .unwrap_or_default();
5397 self.rename_state = Some((handle, current));
5398 fired = Some(LegendAction::Rename);
5399 ui.close();
5400 }
5401 if ui.button("Remove").clicked() {
5402 self.remove(handle);
5403 fired = Some(LegendAction::Remove);
5404 ui.close();
5405 }
5406
5407 fired
5408 }
5409
5410 fn show_rename_popup(&mut self, ui: &mut egui::Ui) {
5416 let Some((handle, mut buffer)) = self.rename_state.take() else {
5417 return;
5418 };
5419 let mut keep_open = true;
5423 let mut apply = false;
5424 let id = ui.id().with(("legend_rename", handle));
5425 let signals = crate::widget::detached::show_detached(
5426 ui.ctx(),
5427 id,
5428 "Rename",
5429 egui::vec2(260.0, 110.0),
5430 None,
5431 |ui| {
5432 let edit = ui.add(egui::TextEdit::singleline(&mut buffer).desired_width(200.0));
5433 if edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
5435 apply = true;
5436 }
5437 ui.horizontal(|ui| {
5438 if ui.button("Apply").clicked() {
5439 apply = true;
5440 }
5441 if ui.button("Cancel").clicked() {
5442 keep_open = false;
5443 }
5444 });
5445 },
5446 );
5447 if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
5448 keep_open = false;
5449 }
5450 if apply {
5451 self.set_item_legend(handle, buffer.clone());
5452 keep_open = false;
5453 }
5454 if signals.close_requested {
5456 keep_open = false;
5457 }
5458 if keep_open {
5459 self.rename_state = Some((handle, buffer));
5460 }
5461 }
5462
5463 pub fn show_stats(&self, ui: &mut egui::Ui, handle: ItemHandle) -> bool {
5465 let Some(record) = self.item_record(handle) else {
5466 return false;
5467 };
5468 ui.label(self.legend_label(record));
5469 match record.stats {
5470 Some(ItemStats::Curve(stats)) => {
5471 show_value_stats(ui, "x", stats.x);
5472 show_value_stats(ui, "y", stats.y);
5473 ui.label(format!("axis: {:?}", stats.y_axis));
5474 }
5475 Some(ItemStats::Image(stats)) => {
5476 ui.label(format!("size: {} x {}", stats.width, stats.height));
5477 ui.label(format!("pixels: {}", stats.pixel_count));
5478 if let Some(scalar) = stats.scalar {
5479 show_value_stats(ui, "value", scalar);
5480 }
5481 }
5482 None => {
5483 ui.label("no retained statistics");
5484 }
5485 }
5486 true
5487 }
5488
5489 pub fn show_active_stats(&self, ui: &mut egui::Ui) -> bool {
5491 self.active_item
5492 .is_some_and(|handle| self.show_stats(ui, handle))
5493 }
5494
5495 fn stats_input(
5499 &self,
5500 handle: ItemHandle,
5501 ) -> Option<crate::widget::stats_widget::StatsInput<'_>> {
5502 self.retained_data(handle).map(retained_data_to_stats_input)
5503 }
5504
5505 pub fn feed_active_stats(
5514 &self,
5515 stats: &mut crate::widget::stats_widget::StatsWidget,
5516 viewport: Option<((f64, f64), (f64, f64))>,
5517 ) -> bool {
5518 let Some(handle) = self.active_item else {
5519 stats.recompute(&[], viewport);
5520 return false;
5521 };
5522 let label = self
5523 .item_record(handle)
5524 .map(|record| self.legend_label(record))
5525 .unwrap_or_else(|| "item".to_owned());
5526 match self.stats_input(handle) {
5527 Some(input) => {
5528 stats.recompute(&[(label.as_str(), input)], viewport);
5529 true
5530 }
5531 None => {
5532 stats.recompute(&[], viewport);
5533 false
5534 }
5535 }
5536 }
5537
5538 pub fn feed_all_stats(
5548 &self,
5549 stats: &mut crate::widget::stats_widget::StatsWidget,
5550 viewport: Option<((f64, f64), (f64, f64))>,
5551 ) -> usize {
5552 let labeled = self.all_stats_labeled_data();
5553 let inputs: Vec<(&str, crate::widget::stats_widget::StatsInput<'_>)> = labeled
5554 .iter()
5555 .map(|(label, data)| (label.as_str(), retained_data_to_stats_input(data)))
5556 .collect();
5557 stats.recompute(&inputs, viewport);
5558 inputs.len()
5559 }
5560
5561 fn all_stats_labeled_data(&self) -> Vec<(String, &RetainedItemData)> {
5565 self.item_records
5566 .iter()
5567 .filter_map(|record| {
5568 record
5569 .data
5570 .as_ref()
5571 .map(|data| (self.legend_label(record), data))
5572 })
5573 .collect()
5574 }
5575
5576 pub fn feed_roi_stats(
5586 &self,
5587 widget: &mut crate::widget::roi_stats_widget::RoiStatsWidget,
5588 ) -> bool {
5589 match self
5590 .active_item
5591 .and_then(|handle| self.retained_data(handle))
5592 {
5593 Some(data) => {
5594 widget.set_rows(roi_stats_rows(self.rois(), data));
5595 true
5596 }
5597 None => {
5598 widget.set_rows(Vec::new());
5599 false
5600 }
5601 }
5602 }
5603
5604 pub fn show_roi_stats_widget(
5611 &self,
5612 ui: &mut egui::Ui,
5613 widget: &mut crate::widget::roi_stats_widget::RoiStatsWidget,
5614 ) {
5615 self.feed_roi_stats(widget);
5616 widget.ui(ui);
5617 }
5618
5619 pub fn feed_curves_roi_stats(
5628 &self,
5629 widget: &mut crate::widget::curves_roi_widget::CurvesRoiWidget,
5630 ) -> bool {
5631 match self
5632 .active_item
5633 .and_then(|handle| self.retained_data(handle))
5634 {
5635 Some(RetainedItemData::Curve { x, y }) => {
5636 widget.set_rows(curve_roi_rows(self.rois(), x, y));
5637 true
5638 }
5639 _ => {
5640 widget.set_rows(Vec::new());
5641 false
5642 }
5643 }
5644 }
5645
5646 pub fn show_curves_roi_widget(
5653 &self,
5654 ui: &mut egui::Ui,
5655 widget: &mut crate::widget::curves_roi_widget::CurvesRoiWidget,
5656 ) {
5657 self.feed_curves_roi_stats(widget);
5658 widget.ui(ui);
5659 }
5660
5661 pub fn set_fit_target(
5667 &self,
5668 fit: &mut crate::widget::fit_widget::FitWidget,
5669 handle: ItemHandle,
5670 ) -> bool {
5671 match self.retained_data(handle).and_then(retained_curve_xy) {
5672 Some((x, y)) => {
5673 fit.set_data(x, y);
5674 true
5675 }
5676 None => false,
5677 }
5678 }
5679
5680 pub fn set_active_fit_target(&self, fit: &mut crate::widget::fit_widget::FitWidget) -> bool {
5684 self.active_item
5685 .is_some_and(|handle| self.set_fit_target(fit, handle))
5686 }
5687
5688 pub fn show_active_stats_widget(
5692 &self,
5693 ui: &mut egui::Ui,
5694 stats: &mut crate::widget::stats_widget::StatsWidget,
5695 viewport: Option<((f64, f64), (f64, f64))>,
5696 ) {
5697 match self.active_item.and_then(|handle| {
5698 self.stats_input(handle).map(|input| {
5699 let label = self
5700 .item_record(handle)
5701 .map(|record| self.legend_label(record))
5702 .unwrap_or_else(|| "item".to_owned());
5703 (label, input)
5704 })
5705 }) {
5706 Some((label, input)) => {
5707 stats.ui(ui, &[(label.as_str(), input)], viewport);
5708 }
5709 None => {
5710 stats.ui(ui, &[], viewport);
5711 }
5712 }
5713 }
5714
5715 pub fn show_all_stats_widget(
5722 &self,
5723 ui: &mut egui::Ui,
5724 stats: &mut crate::widget::stats_widget::StatsWidget,
5725 viewport: Option<((f64, f64), (f64, f64))>,
5726 ) {
5727 let labeled = self.all_stats_labeled_data();
5728 let inputs: Vec<(&str, crate::widget::stats_widget::StatsInput<'_>)> = labeled
5729 .iter()
5730 .map(|(label, data)| (label.as_str(), retained_data_to_stats_input(data)))
5731 .collect();
5732 stats.ui(ui, &inputs, viewport);
5733 }
5734
5735 pub fn show_toolbar(&mut self, ui: &mut egui::Ui) -> ToolbarResponse {
5737 let mut out = ToolbarResponse::default();
5738 ui.scope(|ui| {
5739 ui.spacing_mut().item_spacing.x = 2.0;
5740 ui.horizontal_wrapped(|ui| {
5741 self.show_toolbar_controls(ui, &mut out);
5742 });
5743 });
5744 out
5745 }
5746
5747 pub fn show_limits_toolbar(&mut self, ui: &mut egui::Ui) {
5759 let (xmin0, xmax0, ymin0, ymax0) = self.backend.plot().limits;
5760 ui.horizontal(|ui| {
5761 ui.label("Limits:");
5762 ui.label("X:");
5763 let mut xmin = xmin0;
5764 let mut xmax = xmax0;
5765 let x_changed = ui.add(egui::DragValue::new(&mut xmin)).changed()
5766 | ui.add(egui::DragValue::new(&mut xmax)).changed();
5767 if x_changed {
5768 let (lo, hi) = ordered_limits(xmin, xmax);
5769 self.set_graph_x_limits(lo, hi);
5770 }
5771 ui.label("Y:");
5772 let mut ymin = ymin0;
5773 let mut ymax = ymax0;
5774 let y_changed = ui.add(egui::DragValue::new(&mut ymin)).changed()
5775 | ui.add(egui::DragValue::new(&mut ymax)).changed();
5776 if y_changed {
5777 let (lo, hi) = ordered_limits(ymin, ymax);
5778 self.set_graph_y_limits(lo, hi, YAxis::Left);
5779 }
5780 });
5781 }
5782
5783 pub fn show_toolbar_with<R>(
5790 &mut self,
5791 ui: &mut egui::Ui,
5792 add_contents: impl FnOnce(&mut egui::Ui, &mut Self) -> R,
5793 ) -> (ToolbarResponse, R) {
5794 let mut out = ToolbarResponse::default();
5795 let mut extra = None;
5796 ui.scope(|ui| {
5797 ui.spacing_mut().item_spacing.x = 2.0;
5798 ui.horizontal_wrapped(|ui| {
5799 self.show_toolbar_controls(ui, &mut out);
5800 ui.separator();
5801 extra = Some(add_contents(ui, self));
5802 });
5803 });
5804 (
5805 out,
5806 extra.expect("egui horizontal layout closure should run exactly once"),
5807 )
5808 }
5809
5810 pub fn show_with_toolbar(&mut self, ui: &mut egui::Ui) -> PlotWithToolbarResponse {
5812 let toolbar = self.show_toolbar(ui);
5813 let plot = self.show(ui);
5814 PlotWithToolbarResponse { toolbar, plot }
5815 }
5816
5817 pub fn show_with_toolbar_with<R>(
5819 &mut self,
5820 ui: &mut egui::Ui,
5821 add_toolbar_contents: impl FnOnce(&mut egui::Ui, &mut Self) -> R,
5822 ) -> (PlotWithToolbarResponse, R) {
5823 let (toolbar, extra) = self.show_toolbar_with(ui, add_toolbar_contents);
5824 let plot = self.show(ui);
5825 (PlotWithToolbarResponse { toolbar, plot }, extra)
5826 }
5827
5828 pub fn show_profile_toolbar(&self, ui: &mut egui::Ui) -> ProfileMode {
5842 let id = egui::Id::new(self.backend().plot().id).with("profile_mode");
5843 let mut mode = ui
5844 .data(|d| d.get_temp::<ProfileMode>(id))
5845 .unwrap_or_default();
5846
5847 ui.horizontal(|ui| {
5848 if ui
5849 .selectable_label(mode == ProfileMode::None, "○")
5850 .on_hover_text("No profile")
5851 .clicked()
5852 {
5853 mode = ProfileMode::None;
5854 }
5855 if ui
5856 .selectable_label(mode == ProfileMode::Horizontal, "H")
5857 .on_hover_text("Horizontal profile (row slice)")
5858 .clicked()
5859 {
5860 mode = ProfileMode::Horizontal;
5861 }
5862 if ui
5863 .selectable_label(mode == ProfileMode::Vertical, "V")
5864 .on_hover_text("Vertical profile (column slice)")
5865 .clicked()
5866 {
5867 mode = ProfileMode::Vertical;
5868 }
5869 if ui
5870 .selectable_label(mode == ProfileMode::Line, "L")
5871 .on_hover_text("Line profile (draw line ROI)")
5872 .clicked()
5873 {
5874 mode = ProfileMode::Line;
5875 }
5876 if ui
5877 .selectable_label(mode == ProfileMode::Rectangle, "R")
5878 .on_hover_text("Rectangle profile (draw rect ROI)")
5879 .clicked()
5880 {
5881 mode = ProfileMode::Rectangle;
5882 }
5883 });
5884
5885 ui.data_mut(|d| d.insert_temp(id, mode));
5886 mode
5887 }
5888
5889 fn show_toolbar_controls(&mut self, ui: &mut egui::Ui, out: &mut ToolbarResponse) {
5890 if toolbar_icon_button(ui, ToolbarIcon::Home, false, "Reset zoom").clicked() {
5891 self.reset_zoom();
5892 out.reset_zoom = true;
5893 }
5894 if toolbar_icon_button(ui, ToolbarIcon::ZoomIn, false, "Zoom in").clicked() {
5895 crate::widget::actions::control::zoom_in(self);
5896 out.zoom_in = true;
5897 }
5898 if toolbar_icon_button(ui, ToolbarIcon::ZoomOut, false, "Zoom out").clicked() {
5899 crate::widget::actions::control::zoom_out(self);
5900 out.zoom_out = true;
5901 }
5902 if toolbar_icon_button(ui, ToolbarIcon::ZoomBack, false, "Zoom back").clicked() {
5903 crate::widget::actions::control::zoom_back(self);
5904 out.zoom_back = true;
5905 }
5906
5907 ui.separator();
5908
5909 let mode = self.interaction_mode();
5910 if toolbar_icon_button(
5911 ui,
5912 ToolbarIcon::Select,
5913 mode == PlotInteractionMode::Select,
5914 "Select items and edit handles",
5915 )
5916 .clicked()
5917 {
5918 self.set_interaction_mode(PlotInteractionMode::Select);
5919 out.interaction_mode_changed = true;
5920 }
5921 if toolbar_icon_button(
5922 ui,
5923 ToolbarIcon::Zoom,
5924 mode == PlotInteractionMode::Zoom,
5925 "Box zoom",
5926 )
5927 .clicked()
5928 {
5929 self.set_interaction_mode(PlotInteractionMode::Zoom);
5930 out.interaction_mode_changed = true;
5931 }
5932 let mut zoom_x = self.plot().zoom_x_enabled();
5937 let mut zoom_y = self.plot().zoom_y_enabled();
5938 ui.menu_button("Zoom axes", |ui| {
5939 let cx = ui.checkbox(&mut zoom_x, "X axis").changed();
5940 let cy = ui.checkbox(&mut zoom_y, "Y axis").changed();
5941 if cx || cy {
5942 self.plot_mut().set_zoom_enabled_axes(zoom_x, zoom_y);
5943 out.zoom_axes_changed = true;
5944 }
5945 })
5946 .response
5947 .on_hover_text("Choose which axes a box zoom affects");
5948 if toolbar_icon_button(
5949 ui,
5950 ToolbarIcon::Pan,
5951 mode == PlotInteractionMode::Pan,
5952 "Pan",
5953 )
5954 .clicked()
5955 {
5956 self.set_interaction_mode(PlotInteractionMode::Pan);
5957 out.interaction_mode_changed = true;
5958 }
5959
5960 ui.separator();
5961
5962 let mut x_inv = self.is_x_inverted();
5963 if toolbar_icon_button(ui, ToolbarIcon::InvertX, x_inv, "Invert X axis").clicked() {
5964 x_inv = !x_inv;
5965 self.set_x_inverted(x_inv);
5966 out.x_inverted_changed = true;
5967 }
5968
5969 let mut y_inv = self.is_y_inverted();
5970 if toolbar_icon_button(ui, ToolbarIcon::InvertY, y_inv, "Invert Y axis").clicked() {
5971 y_inv = !y_inv;
5972 self.set_y_inverted(y_inv);
5973 out.y_inverted_changed = true;
5974 }
5975
5976 ui.separator();
5977
5978 let mut x_log = self.is_x_logarithmic();
5979 if toolbar_icon_button(ui, ToolbarIcon::LogX, x_log, "Toggle X log scale").clicked() {
5980 x_log = !x_log;
5981 self.set_x_log(x_log);
5982 out.x_log_changed = true;
5983 }
5984
5985 let mut y_log = self.is_y_logarithmic();
5986 if toolbar_icon_button(ui, ToolbarIcon::LogY, y_log, "Toggle Y log scale").clicked() {
5987 y_log = !y_log;
5988 self.set_y_log(y_log);
5989 out.y_log_changed = true;
5990 }
5991
5992 ui.separator();
5993
5994 let x_auto = self.plot().x_autoscale();
5995 if toolbar_icon_button(
5996 ui,
5997 ToolbarIcon::AutoscaleX,
5998 x_auto,
5999 "Auto-scale X axis on reset zoom",
6000 )
6001 .clicked()
6002 {
6003 crate::widget::actions::control::toggle_x_autoscale(self);
6004 out.autoscale_x_changed = true;
6005 }
6006
6007 let y_auto = self.plot().y_autoscale();
6008 if toolbar_icon_button(
6009 ui,
6010 ToolbarIcon::AutoscaleY,
6011 y_auto,
6012 "Auto-scale Y axis on reset zoom",
6013 )
6014 .clicked()
6015 {
6016 crate::widget::actions::control::toggle_y_autoscale(self);
6017 out.autoscale_y_changed = true;
6018 }
6019
6020 ui.separator();
6021
6022 let mut grid = self.graph_grid();
6023 if toolbar_icon_button(ui, ToolbarIcon::Grid, grid, "Toggle grid").clicked() {
6024 grid = !grid;
6025 self.set_graph_grid(grid);
6026 out.grid_changed = true;
6027 }
6028
6029 let mut minor = self.graph_minor_grid();
6030 let minor_response = ui
6031 .add_enabled_ui(grid, |ui| {
6032 toolbar_icon_button(ui, ToolbarIcon::MinorGrid, minor, "Toggle minor grid")
6033 })
6034 .inner;
6035 if minor_response.clicked() {
6036 minor = !minor;
6037 self.set_graph_minor_grid(minor);
6038 out.minor_grid_changed = true;
6039 }
6040
6041 ui.separator();
6042
6043 let mut aspect = self.is_keep_data_aspect_ratio();
6044 if toolbar_icon_button(ui, ToolbarIcon::Aspect, aspect, "Keep data aspect ratio").clicked()
6045 {
6046 aspect = !aspect;
6047 self.set_keep_data_aspect_ratio(aspect);
6048 out.aspect_changed = true;
6049 }
6050
6051 let mut cursor = self.graph_cursor();
6052 if toolbar_icon_button(ui, ToolbarIcon::Cursor, cursor, "Show cursor coordinates").clicked()
6053 {
6054 cursor = !cursor;
6055 self.set_graph_cursor(cursor);
6056 out.cursor_changed = true;
6057 }
6058
6059 ui.separator();
6060
6061 let show_axis = self.plot().axes_displayed();
6062 if toolbar_icon_button(ui, ToolbarIcon::ShowAxis, show_axis, "Show/hide axes").clicked() {
6063 crate::widget::actions::control::show_axis_toggle(self);
6064 out.show_axis_changed = true;
6065 }
6066
6067 let has_curve = self.active_curve().is_some();
6068 let curve_style_response = ui
6069 .add_enabled_ui(has_curve, |ui| {
6070 toolbar_icon_button(
6071 ui,
6072 ToolbarIcon::CurveStyle,
6073 false,
6074 "Cycle active curve line style",
6075 )
6076 })
6077 .inner;
6078 if curve_style_response.clicked() {
6079 crate::widget::actions::control::curve_style_cycle(self);
6080 out.curve_style_changed = true;
6081 }
6082
6083 ui.separator();
6084
6085 if toolbar_icon_button(ui, ToolbarIcon::Save, false, "Save figure or curve data").clicked()
6086 {
6087 let _ = self.save_dialog(DEFAULT_SAVE_SIZE);
6090 out.save = true;
6091 }
6092 if toolbar_icon_button(ui, ToolbarIcon::Copy, false, "Copy figure to clipboard").clicked() {
6093 let _ = self.copy_to_clipboard(DEFAULT_SAVE_SIZE);
6095 out.copy = true;
6096 }
6097 if toolbar_icon_button(ui, ToolbarIcon::Print, false, "Print figure").clicked() {
6098 self.print_dialog.open_with_system_printers();
6102 out.print = true;
6103 }
6104 if let Some(action) = self.print_dialog.show(ui.ctx()) {
6108 match action {
6109 crate::widget::print_dialog::PrintDialogAction::Print { printer } => {
6110 let _ = self.print_graph_to(&printer, DEFAULT_SAVE_SIZE);
6111 }
6112 crate::widget::print_dialog::PrintDialogAction::SaveToFile => {
6113 let _ = self.save_dialog(DEFAULT_SAVE_SIZE);
6114 }
6115 }
6116 }
6117 }
6118
6119 pub fn set_limits(
6121 &mut self,
6122 xmin: f64,
6123 xmax: f64,
6124 ymin: f64,
6125 ymax: f64,
6126 y2: Option<(f64, f64)>,
6127 ) {
6128 self.set_limits_internal(xmin, xmax, ymin, ymax, y2);
6129 }
6130
6131 pub fn reset_zoom(&mut self) {
6133 self.reset_zoom_to_data();
6134 }
6135
6136 pub fn zoom_back(&mut self) -> bool {
6142 let before = self.limits_snapshot();
6143 let restored = self.backend.plot_mut().zoom_back();
6144 self.push_limits_changed_if(before);
6145 restored
6146 }
6147
6148 pub fn x_limits(&self) -> (f64, f64) {
6150 self.backend.x_limits()
6151 }
6152
6153 pub fn get_graph_x_limits(&self) -> (f64, f64) {
6155 self.x_limits()
6156 }
6157
6158 pub fn set_graph_x_limits(&mut self, xmin: f64, xmax: f64) {
6160 let (_, _, ymin, ymax) = self.backend.plot().limits;
6161 self.set_limits_internal(xmin, xmax, ymin, ymax, self.backend.plot().y2);
6162 }
6163
6164 pub fn y_limits(&self, axis: YAxis) -> Option<(f64, f64)> {
6167 self.backend.y_limits(axis)
6168 }
6169
6170 pub fn get_graph_y_limits(&self, axis: YAxis) -> Option<(f64, f64)> {
6172 self.y_limits(axis)
6173 }
6174
6175 pub fn set_graph_y_limits(&mut self, ymin: f64, ymax: f64, axis: YAxis) {
6177 match axis {
6178 YAxis::Left => {
6179 let (xmin, xmax, _, _) = self.backend.plot().limits;
6180 self.set_limits_internal(xmin, xmax, ymin, ymax, self.backend.plot().y2);
6181 }
6182 YAxis::Right => {
6183 let before = self.limits_snapshot();
6184 self.backend.plot_mut().y2 = Some((ymin, ymax));
6185 self.push_limits_changed_if(before);
6186 }
6187 YAxis::Extra(n) => {
6188 if let Some(ax) = self.backend.plot_mut().extra_axis_mut(n) {
6189 ax.range = Some((ymin, ymax));
6190 }
6191 }
6192 }
6193 }
6194
6195 pub fn add_extra_y_axis(&mut self, side: AxisSide) -> usize {
6207 self.backend.plot_mut().add_extra_axis(side)
6208 }
6209
6210 pub fn extra_y_axis_count(&self) -> usize {
6212 self.backend.plot().extra_axes().len()
6213 }
6214
6215 pub fn set_extra_y_autoscale(&mut self, index: usize, on: bool) -> bool {
6218 match self.backend.plot_mut().extra_axis_mut(index) {
6219 Some(ax) => {
6220 ax.autoscale = on;
6221 true
6222 }
6223 None => false,
6224 }
6225 }
6226
6227 pub fn set_extra_y_log(&mut self, index: usize, on: bool) -> bool {
6230 match self.backend.plot_mut().extra_axis_mut(index) {
6231 Some(ax) => {
6232 ax.scale = if on { Scale::Log10 } else { Scale::Linear };
6233 true
6234 }
6235 None => false,
6236 }
6237 }
6238
6239 pub fn is_extra_y_log(&self, index: usize) -> bool {
6242 self.backend
6243 .plot()
6244 .extra_axis(index)
6245 .map(|ax| ax.scale == Scale::Log10)
6246 .unwrap_or(false)
6247 }
6248
6249 pub fn set_x_log(&mut self, on: bool) {
6251 self.backend.set_x_log(on);
6252 }
6253
6254 pub fn set_graph_x_log(&mut self, on: bool) {
6256 self.set_x_log(on);
6257 }
6258
6259 pub fn is_x_logarithmic(&self) -> bool {
6261 self.backend.plot().x_scale == Scale::Log10
6262 }
6263
6264 pub fn is_graph_x_log(&self) -> bool {
6266 self.is_x_logarithmic()
6267 }
6268
6269 pub fn set_y_log(&mut self, on: bool) {
6271 self.backend.set_y_log(on);
6272 }
6273
6274 pub fn set_graph_y_log(&mut self, on: bool) {
6276 self.set_y_log(on);
6277 }
6278
6279 pub fn is_y_logarithmic(&self) -> bool {
6281 self.backend.plot().y_scale == Scale::Log10
6282 }
6283
6284 pub fn is_graph_y_log(&self) -> bool {
6286 self.is_y_logarithmic()
6287 }
6288
6289 pub fn set_x_inverted(&mut self, on: bool) {
6291 self.backend.set_x_inverted(on);
6292 }
6293
6294 pub fn is_x_inverted(&self) -> bool {
6296 self.backend.plot().x_inverted
6297 }
6298
6299 pub fn set_x_min_range(&mut self, min: Option<f64>) {
6303 self.backend.plot_mut().x_constraints.min_range = min;
6304 }
6305
6306 pub fn set_x_max_range(&mut self, max: Option<f64>) {
6308 self.backend.plot_mut().x_constraints.max_range = max;
6309 }
6310
6311 pub fn set_x_min_pos(&mut self, min: Option<f64>) {
6313 self.backend.plot_mut().x_constraints.min_pos = min;
6314 }
6315
6316 pub fn set_x_max_pos(&mut self, max: Option<f64>) {
6318 self.backend.plot_mut().x_constraints.max_pos = max;
6319 }
6320
6321 pub fn set_y_min_range(&mut self, min: Option<f64>) {
6323 self.backend.plot_mut().y_constraints.min_range = min;
6324 }
6325
6326 pub fn set_y_max_range(&mut self, max: Option<f64>) {
6328 self.backend.plot_mut().y_constraints.max_range = max;
6329 }
6330
6331 pub fn set_y_min_pos(&mut self, min: Option<f64>) {
6333 self.backend.plot_mut().y_constraints.min_pos = min;
6334 }
6335
6336 pub fn set_y_max_pos(&mut self, max: Option<f64>) {
6338 self.backend.plot_mut().y_constraints.max_pos = max;
6339 }
6340
6341 pub fn x_constraints(&self) -> crate::core::plot::AxisConstraints {
6343 self.backend.plot().x_constraints
6344 }
6345
6346 pub fn y_constraints(&self) -> crate::core::plot::AxisConstraints {
6348 self.backend.plot().y_constraints
6349 }
6350
6351 pub fn set_y_inverted(&mut self, on: bool) {
6353 self.backend.set_y_inverted(on);
6354 }
6355
6356 pub fn is_y_inverted(&self) -> bool {
6358 self.backend.plot().y_inverted
6359 }
6360
6361 pub fn set_x_tick_count(&mut self, n: Option<usize>) {
6367 self.backend.plot_mut().x_max_ticks = n;
6368 }
6369
6370 pub fn x_tick_count(&self) -> Option<usize> {
6372 self.backend.plot().x_max_ticks
6373 }
6374
6375 pub fn set_y_tick_count(&mut self, n: Option<usize>) {
6379 self.backend.plot_mut().y_max_ticks = n;
6380 }
6381
6382 pub fn y_tick_count(&self) -> Option<usize> {
6384 self.backend.plot().y_max_ticks
6385 }
6386
6387 pub fn set_keep_data_aspect_ratio(&mut self, on: bool) {
6389 self.backend.set_keep_data_aspect_ratio(on);
6390 }
6391
6392 pub fn is_keep_data_aspect_ratio(&self) -> bool {
6393 self.backend.plot().keep_aspect
6394 }
6395
6396 pub fn set_axes_margins(&mut self, margins: Margins) {
6397 self.backend.set_axes_margins(margins);
6398 }
6399
6400 pub fn axes_margins(&self) -> Margins {
6401 self.backend.plot().margins
6402 }
6403
6404 pub fn set_graph_title(&mut self, title: impl Into<String>) {
6405 let title = title.into();
6406 self.backend.set_title(Some(&title));
6407 }
6408
6409 pub fn graph_title(&self) -> Option<&str> {
6410 self.backend.plot().title.as_deref()
6411 }
6412
6413 pub fn clear_graph_title(&mut self) {
6414 self.backend.set_title(None);
6415 }
6416
6417 pub fn set_graph_x_label(&mut self, label: impl Into<String>) {
6418 let label = label.into();
6419 self.backend.set_x_label(Some(&label));
6420 }
6421
6422 pub fn graph_x_label(&self) -> Option<&str> {
6423 self.backend.plot().x_label.as_deref()
6424 }
6425
6426 pub fn clear_graph_x_label(&mut self) {
6427 self.backend.set_x_label(None);
6428 }
6429
6430 pub fn set_graph_y_label(&mut self, label: impl Into<String>, axis: YAxis) {
6431 let label = label.into();
6432 self.backend.set_y_label(Some(&label), axis);
6433 }
6434
6435 pub fn graph_y_label(&self, axis: YAxis) -> Option<&str> {
6436 match axis {
6437 YAxis::Left => self.backend.plot().y_label.as_deref(),
6438 YAxis::Right => self.backend.plot().y2_label.as_deref(),
6439 YAxis::Extra(n) => self
6440 .backend
6441 .plot()
6442 .extra_axis(n)
6443 .and_then(|a| a.label.as_deref()),
6444 }
6445 }
6446
6447 pub fn clear_graph_y_label(&mut self, axis: YAxis) {
6448 self.backend.set_y_label(None, axis);
6449 }
6450
6451 pub fn set_foreground_colors(&mut self, foreground: Color32, grid: Color32) {
6452 self.backend.set_foreground_colors(foreground, grid);
6453 }
6454
6455 pub fn set_background_colors(&mut self, background: Color32, data_background: Color32) {
6456 self.backend
6457 .set_background_colors(background, data_background);
6458 }
6459
6460 pub fn data_background_color(&self) -> Color32 {
6461 self.backend.plot().data_background
6462 }
6463
6464 pub fn foreground_color(&self) -> Option<Color32> {
6465 self.backend.plot().foreground
6466 }
6467
6468 pub fn grid_color(&self) -> Option<Color32> {
6469 self.backend.plot().grid_color
6470 }
6471
6472 pub fn set_graph_grid(&mut self, on: bool) {
6473 self.backend.plot_mut().grid = if on {
6474 GraphGrid::Major
6475 } else {
6476 GraphGrid::None
6477 };
6478 }
6479
6480 pub fn graph_grid(&self) -> bool {
6481 self.backend.plot().grid.major()
6482 }
6483
6484 pub fn set_graph_grid_mode(&mut self, mode: GraphGrid) {
6485 self.backend.plot_mut().grid = mode;
6486 }
6487
6488 pub fn graph_grid_mode(&self) -> GraphGrid {
6489 self.backend.plot().grid
6490 }
6491
6492 pub fn show_colorbar(&self) -> bool {
6495 self.backend.plot().show_colorbar
6496 }
6497
6498 pub fn set_show_colorbar(&mut self, show: bool) {
6503 self.backend.plot_mut().show_colorbar = show;
6504 }
6505
6506 pub fn interactive_colorbar(&self) -> bool {
6510 self.backend.plot().colorbar_interactive
6511 }
6512
6513 pub fn set_interactive_colorbar(&mut self, interactive: bool) {
6519 self.backend.plot_mut().colorbar_interactive = interactive;
6520 }
6521
6522 pub fn set_colorbar_histogram(&mut self, histogram: Option<(Vec<u64>, Vec<f64>)>) {
6527 self.backend.plot_mut().colorbar_histogram = histogram;
6528 }
6529
6530 pub fn set_colorbar_value_range(&mut self, range: Option<(f64, f64)>) {
6534 self.backend.plot_mut().colorbar_value_range = range;
6535 }
6536
6537 pub fn set_graph_minor_grid(&mut self, on: bool) {
6538 self.backend.plot_mut().grid = if on {
6539 GraphGrid::MajorAndMinor
6540 } else if self.graph_grid() {
6541 GraphGrid::Major
6542 } else {
6543 GraphGrid::None
6544 };
6545 }
6546
6547 pub fn graph_minor_grid(&self) -> bool {
6548 self.backend.plot().grid.minor()
6549 }
6550
6551 pub fn default_colormap(&self) -> &Colormap {
6552 &self.default_colormap
6553 }
6554
6555 pub fn set_default_colormap(&mut self, colormap: Colormap) {
6556 self.default_colormap = colormap;
6557 }
6558
6559 pub fn colorbar_colormap(&self) -> Option<&Colormap> {
6560 self.backend.plot().colormap.as_ref()
6561 }
6562
6563 pub fn get_image_pixels_raw(&self) -> Option<Vec<f64>> {
6571 match self
6572 .active_item
6573 .and_then(|handle| self.retained_data(handle))
6574 {
6575 Some(RetainedItemData::Image { data, .. }) => Some(data.clone()),
6576 _ => None,
6577 }
6578 }
6579
6580 pub fn autoscale_active_image(&mut self, mode: AutoscaleMode) -> Option<(f64, f64)> {
6591 let handle = self.active_item?;
6592 let (data, width, height, base, attrs) = match self.retained_data(handle)? {
6593 retained @ RetainedItemData::Image {
6594 data,
6595 width,
6596 height,
6597 colormap,
6598 ..
6599 } => (
6600 data.clone(),
6601 *width,
6602 *height,
6603 (**colormap).clone(),
6604 retained.image_display_attrs()?,
6605 ),
6606 RetainedItemData::Curve { .. } => return None,
6607 };
6608 let cm = autoscaled_colormap(&base, mode, &data);
6609 let limits = (cm.vmin, cm.vmax);
6610 let pixels: Vec<f32> = data.iter().map(|&v| v as f32).collect();
6611 let mut spec = ImageSpec::scalar(width as u32, height as u32, &pixels, cm);
6612 attrs.apply(&mut spec);
6613 self.update_image_spec(handle, spec);
6614 Some(limits)
6615 }
6616
6617 pub fn set_active_image_levels(&mut self, vmin: f64, vmax: f64) -> bool {
6626 let Some(handle) = self.active_item else {
6627 return false;
6628 };
6629 let (data, width, height, mut cm, attrs) = match self.retained_data(handle) {
6630 Some(
6631 retained @ RetainedItemData::Image {
6632 data,
6633 width,
6634 height,
6635 colormap,
6636 ..
6637 },
6638 ) => (
6639 data.clone(),
6640 *width,
6641 *height,
6642 (**colormap).clone(),
6643 retained.image_display_attrs().expect("image has attrs"),
6644 ),
6645 _ => return false,
6646 };
6647 cm.vmin = vmin;
6648 cm.vmax = vmax;
6649 let pixels: Vec<f32> = data.iter().map(|&v| v as f32).collect();
6650 let mut spec = ImageSpec::scalar(width as u32, height as u32, &pixels, cm);
6651 attrs.apply(&mut spec);
6652 self.update_image_spec(handle, spec)
6653 }
6654
6655 pub fn image_alpha(&self, handle: ItemHandle) -> Option<f32> {
6669 match self.retained_data(handle) {
6670 Some(RetainedItemData::Image { alpha, .. }) => Some(*alpha),
6671 _ => None,
6672 }
6673 }
6674
6675 pub fn set_image_alpha(&mut self, handle: ItemHandle, alpha: f32) -> bool {
6687 let (data, width, height, cm, mut attrs) = match self.retained_data(handle) {
6688 Some(
6689 retained @ RetainedItemData::Image {
6690 data,
6691 width,
6692 height,
6693 colormap,
6694 ..
6695 },
6696 ) => (
6697 data.clone(),
6698 *width,
6699 *height,
6700 (**colormap).clone(),
6701 retained.image_display_attrs().expect("image has attrs"),
6702 ),
6703 _ => return false,
6704 };
6705 attrs.alpha = alpha.clamp(0.0, 1.0);
6708 let pixels: Vec<f32> = data.iter().map(|&v| v as f32).collect();
6709 let mut spec = ImageSpec::scalar(width as u32, height as u32, &pixels, cm);
6710 attrs.apply(&mut spec);
6711 self.update_image_spec(handle, spec)
6712 }
6713
6714 pub fn active_image_handle(&self) -> Option<ItemHandle> {
6721 let handle = self.active_item?;
6722 matches!(
6723 self.retained_data(handle),
6724 Some(RetainedItemData::Image { .. })
6725 )
6726 .then_some(handle)
6727 }
6728
6729 pub fn active_image_alpha(&self) -> Option<f32> {
6733 self.active_item.and_then(|handle| self.image_alpha(handle))
6734 }
6735
6736 pub fn set_active_image_alpha(&mut self, alpha: f32) -> bool {
6740 match self.active_item {
6741 Some(handle) => self.set_image_alpha(handle, alpha),
6742 None => false,
6743 }
6744 }
6745
6746 pub fn apply_median_filter(&mut self, kernel_width: usize, conditional: bool) -> bool {
6761 self.apply_median_filter_kernel(kernel_width, kernel_width, conditional)
6762 }
6763
6764 pub fn apply_median_filter_1d(&mut self, kernel_width: usize, conditional: bool) -> bool {
6770 self.apply_median_filter_kernel(kernel_width, 1, conditional)
6771 }
6772
6773 fn apply_median_filter_kernel(
6775 &mut self,
6776 kernel_h: usize,
6777 kernel_w: usize,
6778 conditional: bool,
6779 ) -> bool {
6780 let kernel_h = force_odd(kernel_h);
6782 let kernel_w = force_odd(kernel_w);
6783
6784 let handle = match self.active_item {
6785 Some(h) => h,
6786 None => return false,
6787 };
6788 let (data, width, height, colormap, attrs) = match self.retained_data(handle) {
6789 Some(
6790 retained @ RetainedItemData::Image {
6791 data,
6792 width,
6793 height,
6794 colormap,
6795 ..
6796 },
6797 ) => (
6798 data.clone(),
6799 *width,
6800 *height,
6801 (**colormap).clone(),
6802 retained.image_display_attrs().expect("image has attrs"),
6803 ),
6804 _ => return false,
6805 };
6806
6807 let filtered = crate::widget::actions::analysis::median_filter_2d(
6808 &data,
6809 width,
6810 height,
6811 kernel_h,
6812 kernel_w,
6813 conditional,
6814 );
6815
6816 let pixels: Vec<f32> = filtered.iter().map(|&v| v as f32).collect();
6817 let mut spec = ImageSpec::scalar(width as u32, height as u32, &pixels, colormap);
6818 attrs.apply(&mut spec);
6819 self.update_image_spec(handle, spec);
6820 true
6821 }
6822
6823 pub fn active_image_histogram(
6836 &self,
6837 n_bins: Option<usize>,
6838 ) -> Option<crate::widget::actions::analysis::PixelHistogram> {
6839 let pixels = self.get_image_pixels_raw()?;
6840 crate::widget::actions::analysis::pixel_intensity_histogram(&pixels, n_bins)
6841 }
6842
6843 pub fn set_graph_cursor(&mut self, on: bool) {
6844 self.backend.plot_mut().crosshair = on;
6845 }
6846
6847 pub fn graph_cursor(&self) -> bool {
6848 self.backend.plot().crosshair
6849 }
6850
6851 pub fn data_to_pixel(&self, x: f64, y: f64, axis: YAxis) -> Option<egui::Pos2> {
6852 self.backend.data_to_pixel(x, y, axis)
6853 }
6854
6855 pub fn pixel_to_data(&self, p: egui::Pos2, axis: YAxis) -> Option<(f64, f64)> {
6856 self.backend.pixel_to_data(p, axis)
6857 }
6858
6859 pub fn plot_bounds_in_pixels(&self) -> Option<egui::Rect> {
6860 self.backend.plot_bounds_in_pixels()
6861 }
6862
6863 pub fn snap_cursor(&self, cursor: [f64; 2], mode: SnappingMode) -> Option<Snap> {
6890 let items: Vec<SnapItem> = self
6892 .item_records
6893 .iter()
6894 .map(|record| SnapItem {
6895 kind: snap_item_kind(record.kind),
6896 visible: self.is_item_visible(record.handle),
6897 has_symbol: record
6898 .curve_data
6899 .as_ref()
6900 .and_then(|curve| curve.symbol)
6901 .is_some(),
6902 active: self.active_item == Some(record.handle),
6903 })
6904 .collect();
6905 let candidates = snapping_candidates(mode, &items);
6906 if candidates.is_empty() {
6907 return None;
6908 }
6909
6910 let mut points: Vec<([f64; 2], [f64; 2])> = Vec::new();
6913 for &index in &candidates {
6914 let record = &self.item_records[index];
6915 let axis = record
6916 .curve_data
6917 .as_ref()
6918 .map_or(YAxis::Left, |curve| curve.y_axis);
6919 if let Some(RetainedItemData::Curve { x, y }) = &record.data {
6923 for (&dx, &dy) in x.iter().zip(y) {
6924 if let Some(px) = self.data_to_pixel(dx, dy, axis) {
6925 points.push(([px.x as f64, px.y as f64], [dx, dy]));
6926 }
6927 }
6928 }
6929 }
6930
6931 let cursor_px = self.data_to_pixel(cursor[0], cursor[1], YAxis::Left)?;
6932 snap_to_nearest(
6933 [cursor_px.x as f64, cursor_px.y as f64],
6934 &points,
6935 SNAP_THRESHOLD_DIST,
6936 )
6937 }
6938
6939 pub fn add_roi(&mut self, roi: Roi) -> usize {
6940 self.backend.plot_mut().rois.push(ManagedRoi::new(roi));
6941 let index = self.backend.plot().rois.len() - 1;
6942 self.events.push(PlotEvent::RoiAdded { index });
6943 index
6944 }
6945
6946 pub fn rois(&self) -> &[ManagedRoi] {
6947 &self.backend.plot().rois
6948 }
6949
6950 pub fn rois_mut(&mut self) -> &mut [ManagedRoi] {
6951 &mut self.backend.plot_mut().rois
6952 }
6953
6954 pub fn clear_rois(&mut self) {
6955 self.backend.plot_mut().clear_rois();
6956 self.events.push(PlotEvent::RoisCleared);
6957 }
6958
6959 pub fn remove_roi(&mut self, index: usize) {
6965 if index >= self.backend.plot().rois.len() {
6966 return; }
6968 self.events.push(PlotEvent::RoiAboutToBeRemoved { index });
6970 self.backend.plot_mut().remove_roi(index);
6971 }
6972
6973 pub fn add_managed_roi(&mut self, managed: ManagedRoi) -> usize {
6979 self.backend.plot_mut().rois.push(managed);
6980 let index = self.backend.plot().rois.len() - 1;
6981 self.events.push(PlotEvent::RoiAdded { index });
6982 index
6983 }
6984
6985 pub fn ruler_active(&self) -> bool {
6988 self.ruler_active
6989 }
6990
6991 pub fn ruler_roi(&self) -> Option<usize> {
6994 self.ruler_roi
6995 }
6996
6997 pub fn set_ruler_active(&mut self, active: bool) {
7009 if active == self.ruler_active {
7010 return;
7011 }
7012 self.ruler_active = active;
7013 if active {
7014 self.ruler_prev_mode = Some(self.interaction_mode);
7015 self.set_interaction_mode(PlotInteractionMode::RoiCreate(RoiDrawKind::Line));
7016 } else {
7017 if let Some(index) = self.ruler_roi.take() {
7018 self.remove_roi(index);
7019 }
7020 let restore = self
7021 .ruler_prev_mode
7022 .take()
7023 .unwrap_or(PlotInteractionMode::Zoom);
7024 self.set_interaction_mode(restore);
7025 }
7026 }
7027
7028 fn relabel_ruler(&mut self, index: usize) {
7032 let label = match self.backend.plot().rois.get(index).map(|r| &r.roi) {
7033 Some(Roi::Line { start, end }) => {
7034 crate::widget::tool_buttons::RulerToolButton::distance_text(
7035 [start.0, start.1],
7036 [end.0, end.1],
7037 )
7038 }
7039 _ => return,
7040 };
7041 self.set_roi_name(index, label);
7042 }
7043
7044 #[must_use]
7048 pub fn roi_interaction_mode(&self, index: usize) -> Option<RoiInteractionMode> {
7049 self.backend.plot().rois.get(index)?.interaction_mode()
7050 }
7051
7052 pub fn set_roi_interaction_mode(&mut self, index: usize, mode: RoiInteractionMode) -> bool {
7059 let Some(roi) = self.backend.plot_mut().rois.get_mut(index) else {
7060 return false;
7061 };
7062 if roi.set_interaction_mode(mode) {
7063 self.events
7064 .push(PlotEvent::RoiInteractionModeChanged { index, mode });
7065 true
7066 } else {
7067 false
7068 }
7069 }
7070
7071 pub fn set_roi_color(&mut self, index: usize, color: Color32) {
7074 if let Some(r) = self.backend.plot_mut().rois.get_mut(index) {
7075 r.color = Some(color);
7076 }
7077 }
7078
7079 pub fn set_roi_name(&mut self, index: usize, name: impl Into<String>) {
7082 if let Some(r) = self.backend.plot_mut().rois.get_mut(index) {
7083 r.name = name.into();
7084 }
7085 }
7086
7087 pub fn set_roi_line_width(&mut self, index: usize, width: f32) {
7090 if let Some(r) = self.backend.plot_mut().rois.get_mut(index) {
7091 r.line_width = width;
7092 }
7093 }
7094
7095 pub fn set_roi_line_style(&mut self, index: usize, style: RoiLineStyle) {
7098 if let Some(r) = self.backend.plot_mut().rois.get_mut(index) {
7099 r.line_style = style;
7100 }
7101 }
7102
7103 pub fn set_roi_line_gap_color(&mut self, index: usize, gap_color: Option<Color32>) {
7107 if let Some(r) = self.backend.plot_mut().rois.get_mut(index) {
7108 r.gap_color = gap_color;
7109 }
7110 }
7111
7112 pub fn set_roi_fill(&mut self, index: usize, fill: bool) {
7115 if let Some(r) = self.backend.plot_mut().rois.get_mut(index) {
7116 r.fill = fill;
7117 }
7118 }
7119
7120 pub fn current_roi(&self) -> Option<usize> {
7123 self.backend.plot().current_roi()
7124 }
7125
7126 pub fn set_current_roi(&mut self, index: Option<usize>) {
7132 let previous = self.backend.plot().current_roi();
7133 self.backend.plot_mut().set_current_roi(index);
7134 let current = self.backend.plot().current_roi();
7135 if current != previous {
7136 self.events
7137 .push(PlotEvent::CurrentRoiChanged { previous, current });
7138 }
7139 }
7140
7141 pub fn show_roi_manager(&mut self, ui: &mut egui::Ui) -> Option<usize> {
7155 let mut added: Option<usize> = None;
7156 let mut remove_idx: Option<usize> = None;
7157 let mut make_current: Option<usize> = None;
7158 let mut rename: Option<(usize, String)> = None;
7159
7160 let current = self.current_roi();
7161
7162 egui::ScrollArea::vertical()
7168 .max_height(200.0)
7169 .show(ui, |ui| {
7170 egui::Grid::new("roi_manager_table")
7171 .num_columns(3)
7172 .striped(true)
7173 .show(ui, |ui| {
7174 ui.label("Name");
7175 ui.label("Region");
7176 ui.label("");
7177 ui.end_row();
7178
7179 for (i, managed) in self.backend.plot().rois.iter().enumerate() {
7180 let mut name = managed.name.clone();
7184 if ui
7185 .add(
7186 egui::TextEdit::singleline(&mut name)
7187 .desired_width(90.0)
7188 .hint_text("(unnamed)"),
7189 )
7190 .changed()
7191 {
7192 rename = Some((i, name));
7193 }
7194
7195 let desc = roi_description(&managed.roi);
7198 if ui
7199 .selectable_label(current == Some(i), desc)
7200 .on_hover_text("Make current")
7201 .clicked()
7202 {
7203 make_current = Some(i);
7204 }
7205
7206 if ui.small_button("×").on_hover_text("Remove").clicked() {
7207 remove_idx = Some(i);
7208 }
7209 ui.end_row();
7210 }
7211 });
7212 });
7213
7214 if let Some((idx, name)) = rename {
7215 self.set_roi_name(idx, name);
7216 }
7217 if let Some(idx) = make_current {
7218 self.set_current_roi(Some(idx));
7220 }
7221 if let Some(idx) = remove_idx {
7222 self.remove_roi(idx);
7224 }
7225
7226 let (x0, x1, y0, y1) = self.backend.plot().limits;
7228 let cx = (x0 + x1) * 0.5;
7229 let cy = (y0 + y1) * 0.5;
7230 let dx = (x1 - x0) * 0.2;
7231 let dy = (y1 - y0) * 0.2;
7232
7233 ui.horizontal_wrapped(|ui| {
7234 if ui.button("+ Rect").clicked() {
7235 let idx = self.add_roi(Roi::Rect {
7236 x: (cx - dx, cx + dx),
7237 y: (cy - dy, cy + dy),
7238 });
7239 added = Some(idx);
7240 }
7241 if ui.button("+ HRange").clicked() {
7242 let idx = self.add_roi(Roi::HRange {
7243 y: (cy - dy, cy + dy),
7244 });
7245 added = Some(idx);
7246 }
7247 if ui.button("+ VRange").clicked() {
7248 let idx = self.add_roi(Roi::VRange {
7249 x: (cx - dx, cx + dx),
7250 });
7251 added = Some(idx);
7252 }
7253 if ui.button("+ Point").clicked() {
7254 let idx = self.add_roi(Roi::Point { x: cx, y: cy });
7255 added = Some(idx);
7256 }
7257 if ui.button("+ Line").clicked() {
7258 let idx = self.add_roi(Roi::Line {
7259 start: (cx - dx, cy),
7260 end: (cx + dx, cy),
7261 });
7262 added = Some(idx);
7263 }
7264 });
7265
7266 if !self.backend.plot().rois.is_empty() && ui.button("Clear all").clicked() {
7268 self.clear_rois();
7269 }
7270
7271 added
7272 }
7273
7274 pub fn pick_item(&self, p: egui::Pos2, item: ItemHandle) -> Option<PickResult> {
7275 self.backend.pick_item(p, item)
7276 }
7277
7278 pub fn items_back_to_front(&self) -> Vec<ItemHandle> {
7279 self.backend.items_back_to_front()
7280 }
7281
7282 pub fn replot(&mut self) {
7283 self.backend.replot();
7284 }
7285
7286 pub fn save_graph(&self, path: &Path, size: (u32, u32)) -> Result<(), SaveError> {
7287 self.backend.save_graph(path, size)
7288 }
7289
7290 pub fn save_graph_with_format(
7297 &self,
7298 path: &Path,
7299 size: (u32, u32),
7300 format: SaveFormat,
7301 dpi: u32,
7302 ) -> Result<(), SaveError> {
7303 self.backend.save_graph_with_format(path, size, format, dpi)
7304 }
7305
7306 pub fn save_to_path(&self, path: &Path, size: (u32, u32)) -> Result<bool, SaveError> {
7319 use crate::widget::actions::io::{SaveTarget, curve_to_csv};
7320
7321 match SaveTarget::from_path(path) {
7322 Some(SaveTarget::Figure(format)) => {
7323 self.save_graph_with_format(path, size, format, DEFAULT_SAVE_DPI)?;
7324 Ok(true)
7325 }
7326 Some(SaveTarget::CurveCsv) => {
7327 let Some(handle) = self.active_curve() else {
7328 return Ok(false);
7329 };
7330 let Some((x, y)) = self.retained_data(handle).and_then(retained_curve_xy) else {
7331 return Ok(false);
7332 };
7333 let csv = curve_to_csv(x, y);
7334 std::fs::write(path, csv)?;
7335 Ok(true)
7336 }
7337 None => Ok(false),
7338 }
7339 }
7340
7341 pub fn save_dialog(&self, size: (u32, u32)) -> Result<bool, SaveError> {
7348 let Some(path) = rfd::FileDialog::new()
7349 .add_filter("PNG figure", &["png"])
7350 .add_filter("PPM figure", &["ppm"])
7351 .add_filter("SVG figure", &["svg"])
7352 .add_filter("TIFF figure", &["tif", "tiff"])
7353 .add_filter("JPEG figure", &["jpg", "jpeg"])
7354 .add_filter("EPS figure", &["eps"])
7355 .add_filter("PDF figure", &["pdf"])
7356 .add_filter("Curve CSV", &["csv"])
7357 .save_file()
7358 else {
7359 return Ok(false);
7360 };
7361 self.save_to_path(&path, size)
7362 }
7363
7364 pub fn save_rois_to_path(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
7368 crate::core::roi_io::save_rois(path, self.rois())
7369 }
7370
7371 pub fn load_rois_from_path(
7376 &mut self,
7377 path: impl AsRef<std::path::Path>,
7378 ) -> std::io::Result<()> {
7379 let loaded = crate::core::roi_io::load_rois(path)?;
7380 self.clear_rois(); for roi in loaded {
7382 self.add_managed_roi(roi); }
7384 Ok(())
7385 }
7386
7387 pub fn save_rois_dialog(&self) -> std::io::Result<bool> {
7392 let Some(path) = rfd::FileDialog::new()
7393 .add_filter("siplot ROIs", &["rois", "txt"])
7394 .save_file()
7395 else {
7396 return Ok(false);
7397 };
7398 self.save_rois_to_path(&path)?;
7399 Ok(true)
7400 }
7401
7402 pub fn load_rois_dialog(&mut self) -> std::io::Result<bool> {
7407 let Some(path) = rfd::FileDialog::new()
7408 .add_filter("siplot ROIs", &["rois", "txt"])
7409 .pick_file()
7410 else {
7411 return Ok(false);
7412 };
7413 self.load_rois_from_path(&path)?;
7414 Ok(true)
7415 }
7416
7417 pub fn copy_to_clipboard(&self, size: (u32, u32)) -> Result<bool, SaveError> {
7430 use crate::widget::actions::io::{decode_png_to_rgba, rgba_to_clipboard_image};
7431
7432 let mut path = std::env::temp_dir();
7435 path.push(format!("siplot-copy-{}.png", std::process::id()));
7436 self.save_graph(&path, size)?;
7437 let png = std::fs::read(&path)?;
7438 let _ = std::fs::remove_file(&path);
7439
7440 let (w, h, rgba) = decode_png_to_rgba(&png)?;
7441 let Some(image) = rgba_to_clipboard_image(&rgba, w, h) else {
7442 return Err(SaveError::Readback("clipboard image shaping failed".into()));
7443 };
7444 let mut clipboard = arboard::Clipboard::new()
7445 .map_err(|e| SaveError::Readback(format!("clipboard open: {e}")))?;
7446 clipboard
7447 .set_image(image)
7448 .map_err(|e| SaveError::Readback(format!("clipboard set_image: {e}")))?;
7449 Ok(true)
7450 }
7451
7452 pub fn print_graph(&self, size: (u32, u32)) -> Result<bool, SaveError> {
7472 let Some(printer) = printers::get_default_printer() else {
7473 return Ok(false);
7474 };
7475 self.print_to_printer(&printer, size)
7476 }
7477
7478 pub fn print_graph_to(&self, printer_name: &str, size: (u32, u32)) -> Result<bool, SaveError> {
7483 let Some(printer) = printers::get_printer_by_name(printer_name) else {
7484 return Ok(false);
7485 };
7486 self.print_to_printer(&printer, size)
7487 }
7488
7489 fn print_to_printer(
7492 &self,
7493 printer: &printers::common::base::printer::Printer,
7494 size: (u32, u32),
7495 ) -> Result<bool, SaveError> {
7496 let path = print_temp_png_path(&std::env::temp_dir(), std::process::id());
7500 self.save_graph(&path, size)?;
7501 let submit = printer.print_file(
7502 &path.to_string_lossy(),
7503 printers::common::base::job::PrinterJobOptions::none(),
7504 );
7505 let _ = std::fs::remove_file(&path);
7506 submit.map_err(|e| SaveError::Readback(format!("print submit: {}", e.message)))?;
7507 Ok(true)
7508 }
7509
7510 pub fn reset_zoom_to_data(&mut self) {
7512 self.apply_limits_from_data_bounds();
7513 }
7514
7515 fn apply_auto_limits(&mut self) {
7516 if self.auto_reset_zoom {
7517 self.apply_limits_from_data_bounds();
7518 }
7519 }
7520
7521 fn apply_limits_from_data_bounds(&mut self) {
7522 let extra = extra_data_ranges(&self.data_bounds);
7528 if !extra.is_empty() {
7529 self.backend.plot_mut().reset_extra_axes_to(&extra);
7530 }
7531 if self.data_bounds.x.is_none() || self.data_bounds.y_left.is_none() {
7534 return;
7535 }
7536 let range = data_range_from_bounds(&self.data_bounds);
7544 let before = self.limits_snapshot();
7545 self.backend.plot_mut().reset_zoom_to_data_range(range);
7546 self.push_limits_changed_if(before);
7547 }
7548}
7549
7550pub struct Plot1D {
7552 inner: PlotWidget,
7553}
7554
7555impl Plot1D {
7556 pub fn new(render_state: &RenderState, id: PlotId) -> Self {
7558 let mut inner = PlotWidget::new(render_state, id);
7559 inner.set_graph_x_label("X");
7560 inner.set_graph_y_label("Y", YAxis::Left);
7561 inner.set_graph_grid(true);
7562 Self { inner }
7563 }
7564
7565 pub fn add_histogram(
7567 &mut self,
7568 edges: &[f64],
7569 counts: &[f64],
7570 color: Color32,
7571 ) -> Result<ItemHandle, PlotDataError> {
7572 self.inner.add_histogram(edges, counts, color)
7573 }
7574
7575 pub fn add_scatter(&mut self, x: &[f64], y: &[f64], color: Color32) -> ItemHandle {
7577 self.inner.add_scatter(x, y, color)
7578 }
7579
7580 pub fn add_horizontal_profile_curve(
7582 &mut self,
7583 width: u32,
7584 height: u32,
7585 data: &[f32],
7586 row: u32,
7587 color: Color32,
7588 ) -> Result<ItemHandle, PlotDataError> {
7589 self.inner
7590 .add_horizontal_profile_curve(width, height, data, row, color)
7591 }
7592
7593 pub fn add_vertical_profile_curve(
7595 &mut self,
7596 width: u32,
7597 height: u32,
7598 data: &[f32],
7599 column: u32,
7600 color: Color32,
7601 ) -> Result<ItemHandle, PlotDataError> {
7602 self.inner
7603 .add_vertical_profile_curve(width, height, data, column, color)
7604 }
7605
7606 pub fn into_inner(self) -> PlotWidget {
7608 self.inner
7609 }
7610}
7611
7612impl Deref for Plot1D {
7613 type Target = PlotWidget;
7614
7615 fn deref(&self) -> &Self::Target {
7616 &self.inner
7617 }
7618}
7619
7620impl DerefMut for Plot1D {
7621 fn deref_mut(&mut self) -> &mut Self::Target {
7622 &mut self.inner
7623 }
7624}
7625
7626pub struct Plot2D {
7628 inner: PlotWidget,
7629}
7630
7631impl Plot2D {
7632 pub fn new(render_state: &RenderState, id: PlotId) -> Self {
7634 let mut inner = PlotWidget::new(render_state, id);
7635 inner.set_graph_x_label("Columns");
7636 inner.set_graph_y_label("Rows", YAxis::Left);
7637 inner.set_graph_grid(false);
7638 inner.set_keep_data_aspect_ratio(true);
7639 inner.set_y_inverted(true);
7640 Self { inner }
7641 }
7642
7643 pub fn add_default_image(&mut self, width: u32, height: u32, data: &[f32]) -> ItemHandle {
7645 self.inner.add_image_default(width, height, data)
7646 }
7647
7648 pub fn try_add_default_image(
7651 &mut self,
7652 width: u32,
7653 height: u32,
7654 data: &[f32],
7655 ) -> Result<ItemHandle, PlotDataError> {
7656 self.inner.try_add_image_default(width, height, data)
7657 }
7658
7659 pub fn try_add_masked_image(
7673 &mut self,
7674 width: u32,
7675 height: u32,
7676 data: &[f32],
7677 mask: &ScalarMask,
7678 ) -> Result<ItemHandle, PlotDataError> {
7679 let masked = apply_image_mask(width, height, data, mask)?;
7682 self.inner.try_add_image_default(width, height, &masked)
7683 }
7684
7685 pub fn add_mask(
7687 &mut self,
7688 width: u32,
7689 height: u32,
7690 mask: &[bool],
7691 color: Color32,
7692 ) -> Result<ItemHandle, PlotDataError> {
7693 self.inner.add_mask(width, height, mask, color)
7694 }
7695
7696 pub fn add_mask_with_geometry(
7698 &mut self,
7699 width: u32,
7700 height: u32,
7701 mask: &[bool],
7702 color: Color32,
7703 geometry: ImageGeometry,
7704 ) -> Result<ItemHandle, PlotDataError> {
7705 self.inner
7706 .add_mask_with_geometry(width, height, mask, color, geometry)
7707 }
7708
7709 pub fn horizontal_profile(
7711 &self,
7712 width: u32,
7713 height: u32,
7714 data: &[f32],
7715 row: u32,
7716 ) -> Result<Vec<f64>, PlotDataError> {
7717 horizontal_profile_values(width, height, data, row)
7718 }
7719
7720 pub fn vertical_profile(
7722 &self,
7723 width: u32,
7724 height: u32,
7725 data: &[f32],
7726 column: u32,
7727 ) -> Result<Vec<f64>, PlotDataError> {
7728 vertical_profile_values(width, height, data, column)
7729 }
7730
7731 pub fn profile_at_cursor(
7745 &self,
7746 plot_response: &PlotResponse,
7747 pixels: &[f32],
7748 width: u32,
7749 height: u32,
7750 mode: ProfileMode,
7751 ) -> Option<(Vec<f64>, Vec<f64>)> {
7752 if mode == ProfileMode::None {
7753 return None;
7754 }
7755 let hover_px = plot_response.response.hover_pos()?;
7756 let (data_x, data_y) = plot_response.transform.pixel_to_data(hover_px);
7757
7758 let col = data_x.floor() as i64;
7759 let row = data_y.floor() as i64;
7760
7761 match mode {
7762 ProfileMode::None => None,
7763 ProfileMode::Horizontal => {
7764 if row < 0 || row >= height as i64 {
7765 return None;
7766 }
7767 horizontal_profile_values(width, height, pixels, row as u32)
7768 .ok()
7769 .map(|y| {
7770 let x: Vec<f64> = (0..width as usize).map(|i| i as f64).collect();
7771 (x, y)
7772 })
7773 }
7774 ProfileMode::Vertical => {
7775 if col < 0 || col >= width as i64 {
7776 return None;
7777 }
7778 vertical_profile_values(width, height, pixels, col as u32)
7779 .ok()
7780 .map(|y| {
7781 let x: Vec<f64> = (0..height as usize).map(|i| i as f64).collect();
7782 (x, y)
7783 })
7784 }
7785 _ => None,
7786 }
7787 }
7788
7789 pub fn show_median_filter(
7807 &mut self,
7808 ui: &mut egui::Ui,
7809 params: &mut MedianFilterParams,
7810 ) -> bool {
7811 ui.horizontal(|ui| {
7812 ui.label("Kernel width:");
7813 let mut width = params.kernel_width.max(1);
7816 if ui
7817 .add(egui::DragValue::new(&mut width).range(1..=99).speed(2.0))
7818 .changed()
7819 {
7820 params.kernel_width = force_odd(width);
7821 }
7822 });
7823 ui.checkbox(&mut params.conditional, "Conditional")
7824 .on_hover_text("Replace a pixel only if it is the window min or max");
7825
7826 let mut applied = false;
7827 let has_image = self.get_image_pixels_raw().is_some();
7828 if ui
7829 .add_enabled(has_image, egui::Button::new("Apply"))
7830 .on_hover_text("Replace the active image with its median-filtered copy")
7831 .clicked()
7832 {
7833 applied = self.apply_median_filter(params.kernel_width, params.conditional);
7834 }
7835 applied
7836 }
7837
7838 pub fn show_median_filter_toolbar(&mut self, ui: &mut egui::Ui) -> bool {
7856 let plot_id = self.backend().plot().id;
7857 let open_id = egui::Id::new(plot_id).with("median_filter_open");
7858 let params_id = egui::Id::new(plot_id).with("median_filter_params");
7859
7860 let mut open = ui.data(|d| d.get_temp::<bool>(open_id)).unwrap_or(false);
7861 let has_image = self.get_image_pixels_raw().is_some();
7862
7863 let button = ui
7864 .add_enabled_ui(has_image, |ui| {
7865 toolbar_icon_button(ui, ToolbarIcon::MedianFilter, open, "Median filter")
7866 })
7867 .inner;
7868 if button.clicked() {
7869 open = !open;
7870 }
7871
7872 let mut applied = false;
7873 if open {
7874 let mut params = ui
7875 .data(|d| d.get_temp::<MedianFilterParams>(params_id))
7876 .unwrap_or_default();
7877 let signals = crate::widget::detached::show_detached(
7878 ui.ctx(),
7879 open_id.with("window"),
7880 "Median filter",
7881 egui::vec2(320.0, 220.0),
7882 None,
7883 |ui| {
7884 applied = self.show_median_filter(ui, &mut params);
7885 },
7886 );
7887 ui.data_mut(|d| d.insert_temp(params_id, params));
7888 if signals.close_requested {
7889 open = false;
7890 }
7891 }
7892
7893 ui.data_mut(|d| d.insert_temp(open_id, open));
7894 applied
7895 }
7896
7897 pub fn symbol_tool_button(&mut self, ui: &mut egui::Ui) {
7911 let plot_id = self.backend().plot().id;
7912 let size_id = egui::Id::new(plot_id).with("symbol_tool_size");
7913 let mut size = ui.data(|d| d.get_temp::<f32>(size_id)).unwrap_or(7.0);
7914
7915 ui.menu_button("Symbol", |ui| {
7916 ui.horizontal(|ui| {
7917 ui.label("Size:");
7918 if ui
7919 .add(egui::DragValue::new(&mut size).range(1.0..=20.0).speed(0.5))
7920 .on_hover_text("Marker size for every curve/scatter")
7921 .changed()
7922 {
7923 self.set_all_symbol_sizes(size);
7924 }
7925 });
7926 ui.separator();
7927 if ui.button("None").clicked() {
7928 self.set_all_symbols(None);
7929 ui.close();
7930 }
7931 for symbol in Symbol::ALL {
7932 if ui.button(symbol.name()).clicked() {
7933 self.set_all_symbols(Some(symbol));
7934 ui.close();
7935 }
7936 }
7937 });
7938
7939 ui.data_mut(|d| d.insert_temp(size_id, size));
7940 }
7941
7942 pub fn show_pixel_histogram(
7956 &mut self,
7957 ui: &mut egui::Ui,
7958 n_bins: &mut Option<usize>,
7959 ) -> Option<crate::widget::actions::analysis::PixelHistogram> {
7960 let histogram = self.active_image_histogram(*n_bins);
7961 let Some(histo) = histogram else {
7962 ui.label("No image with finite pixels.");
7963 return None;
7964 };
7965
7966 let mut bins = n_bins.unwrap_or(histo.n_bins).max(2);
7969 ui.horizontal(|ui| {
7970 ui.label("Bins:");
7971 if ui
7972 .add(egui::DragValue::new(&mut bins).range(2..=1024))
7973 .changed()
7974 {
7975 *n_bins = Some(bins.max(2));
7976 }
7977 });
7978 let histo = if *n_bins == Some(histo.n_bins) || n_bins.is_none() {
7980 histo
7981 } else {
7982 match self.active_image_histogram(*n_bins) {
7983 Some(h) => h,
7984 None => return Some(histo),
7985 }
7986 };
7987
7988 ui.label(format!(
7990 "min {:.4} max {:.4} mean {:.4} std {:.4} sum {:.4}",
7991 histo.min, histo.max, histo.mean, histo.std, histo.sum
7992 ));
7993
7994 let max_count = histo.counts.iter().copied().max().unwrap_or(0).max(1) as f32;
7996 let desired = egui::vec2(ui.available_width().max(120.0), 120.0);
7997 let (rect, _resp) = ui.allocate_exact_size(desired, egui::Sense::hover());
7998 if ui.is_rect_visible(rect) {
7999 let painter = ui.painter_at(rect);
8000 painter.rect_filled(rect, 0.0, ui.visuals().extreme_bg_color);
8001 let n = histo.counts.len().max(1);
8002 let bar_w = rect.width() / n as f32;
8003 let fill = Color32::from_rgb(0x66, 0xaa, 0xd7); for (i, &count) in histo.counts.iter().enumerate() {
8005 let h = (count as f32 / max_count) * rect.height();
8006 let x0 = rect.left() + i as f32 * bar_w;
8007 let bar = egui::Rect::from_min_max(
8008 egui::pos2(x0, rect.bottom() - h),
8009 egui::pos2(x0 + bar_w, rect.bottom()),
8010 );
8011 painter.rect_filled(bar.shrink(0.5), 0.0, fill);
8012 }
8013 }
8014
8015 Some(histo)
8016 }
8017
8018 pub fn show_pixel_histogram_toolbar(&mut self, ui: &mut egui::Ui) -> bool {
8026 let plot_id = self.backend().plot().id;
8027 let open_id = egui::Id::new(plot_id).with("pixel_histogram_open");
8028 let bins_id = egui::Id::new(plot_id).with("pixel_histogram_bins");
8029
8030 let mut open = ui.data(|d| d.get_temp::<bool>(open_id)).unwrap_or(false);
8031 let has_image = self.get_image_pixels_raw().is_some();
8032
8033 let button = ui
8034 .add_enabled_ui(has_image, |ui| {
8035 toolbar_icon_button(ui, ToolbarIcon::PixelHistogram, open, "Pixel intensity")
8036 })
8037 .inner;
8038 if button.clicked() {
8039 open = !open;
8040 }
8041
8042 if open {
8043 let mut n_bins = ui.data(|d| d.get_temp::<Option<usize>>(bins_id)).flatten();
8044 let signals = crate::widget::detached::show_detached(
8045 ui.ctx(),
8046 open_id.with("window"),
8047 "Pixel intensity",
8048 egui::vec2(480.0, 360.0),
8049 None,
8050 |ui| {
8051 self.show_pixel_histogram(ui, &mut n_bins);
8052 },
8053 );
8054 ui.data_mut(|d| d.insert_temp(bins_id, n_bins));
8055 if signals.close_requested {
8056 open = false;
8057 }
8058 }
8059
8060 ui.data_mut(|d| d.insert_temp(open_id, open));
8061 open
8062 }
8063
8064 pub fn into_inner(self) -> PlotWidget {
8066 self.inner
8067 }
8068}
8069
8070impl Deref for Plot2D {
8071 type Target = PlotWidget;
8072
8073 fn deref(&self) -> &Self::Target {
8074 &self.inner
8075 }
8076}
8077
8078impl DerefMut for Plot2D {
8079 fn deref_mut(&mut self) -> &mut Self::Target {
8080 &mut self.inner
8081 }
8082}
8083
8084#[derive(Clone, Copy, Debug, Default, PartialEq)]
8090pub enum CompareMode {
8091 OnlyA,
8093 OnlyB,
8095 #[default]
8099 HalfHalf,
8100 SplitHorizontal,
8104 Subtract,
8106 RedBlueGray,
8110 RedBlueGrayNeg,
8114}
8115
8116#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8126pub enum CompareAlignment {
8127 #[default]
8131 Origin,
8132 Center,
8136 Stretch,
8140 Auto,
8147}
8148
8149const KEYPOINT_COLOR: Color32 = Color32::from_rgb(255, 0, 255);
8152
8153pub struct CompareImages {
8168 inner: PlotWidget,
8169 width_a: u32,
8170 height_a: u32,
8171 width_b: u32,
8172 height_b: u32,
8173 data_a: Vec<f32>,
8174 data_b: Vec<f32>,
8175 colormap: Colormap,
8176 composite_handle: Option<ItemHandle>,
8177 split: f32,
8178 mode: CompareMode,
8179 alignment: CompareAlignment,
8181 dirty: bool,
8182 cursor: Option<[f64; 2]>,
8185 separator: Option<ItemHandle>,
8191 separator_horizontal: bool,
8195 separator_dragging: bool,
8199 composite_w: u32,
8203 composite_h: u32,
8204 auto: Option<SiftAlignment>,
8210 keypoints_visible: bool,
8214 keypoint_overlay: Option<ItemHandle>,
8217}
8218
8219impl CompareImages {
8220 pub fn new(render_state: &RenderState, id: PlotId) -> Self {
8222 let mut inner = PlotWidget::new(render_state, id);
8223 inner.set_keep_data_aspect_ratio(true);
8224 Self {
8225 inner,
8226 width_a: 0,
8227 height_a: 0,
8228 width_b: 0,
8229 height_b: 0,
8230 data_a: Vec::new(),
8231 data_b: Vec::new(),
8232 colormap: Colormap::viridis(0.0, 1.0),
8233 composite_handle: None,
8234 split: 0.5,
8235 mode: CompareMode::HalfHalf,
8236 alignment: CompareAlignment::default(),
8237 dirty: false,
8238 cursor: None,
8239 separator: None,
8240 separator_horizontal: false,
8241 separator_dragging: false,
8242 composite_w: 0,
8243 composite_h: 0,
8244 auto: None,
8245 keypoints_visible: false,
8246 keypoint_overlay: None,
8247 }
8248 }
8249
8250 pub fn set_images(
8258 &mut self,
8259 shape_a: (u32, u32),
8260 data_a: &[f32],
8261 shape_b: (u32, u32),
8262 data_b: &[f32],
8263 colormap: Colormap,
8264 ) -> Result<(), PlotDataError> {
8265 let (width_a, height_a) = shape_a;
8266 let (width_b, height_b) = shape_b;
8267 let expected_a = (width_a as usize).saturating_mul(height_a as usize);
8268 if data_a.len() != expected_a {
8269 return Err(PlotDataError::ImageDataLength {
8270 expected: expected_a,
8271 actual: data_a.len(),
8272 });
8273 }
8274 let expected_b = (width_b as usize).saturating_mul(height_b as usize);
8275 if data_b.len() != expected_b {
8276 return Err(PlotDataError::ImageDataLength {
8277 expected: expected_b,
8278 actual: data_b.len(),
8279 });
8280 }
8281 self.width_a = width_a;
8282 self.height_a = height_a;
8283 self.width_b = width_b;
8284 self.height_b = height_b;
8285 self.data_a = data_a.to_vec();
8286 self.data_b = data_b.to_vec();
8287 self.colormap = colormap;
8288 self.dirty = true;
8289 Ok(())
8290 }
8291
8292 pub fn alignment(&self) -> CompareAlignment {
8294 self.alignment
8295 }
8296
8297 pub fn set_alignment(&mut self, alignment: CompareAlignment) {
8299 if alignment != self.alignment {
8300 self.alignment = alignment;
8301 self.dirty = true;
8302 }
8303 }
8304
8305 pub fn transformation(&self) -> Option<AffineTransformation> {
8316 self.auto.as_ref().map(|a| a.transformation)
8317 }
8318
8319 pub fn keypoints_visible(&self) -> bool {
8322 self.keypoints_visible
8323 }
8324
8325 pub fn set_keypoints_visible(&mut self, visible: bool) {
8330 if visible != self.keypoints_visible {
8331 self.keypoints_visible = visible;
8332 self.dirty = true;
8333 }
8334 }
8335
8336 pub fn matched_keypoints(&self) -> &[MatchedKeypoint] {
8340 self.auto
8341 .as_ref()
8342 .map(|a| a.matches.as_slice())
8343 .unwrap_or(&[])
8344 }
8345
8346 pub fn split(&self) -> f32 {
8348 self.split
8349 }
8350
8351 pub fn set_split(&mut self, split: f32) {
8353 let clamped = split.clamp(0.0, 1.0);
8354 if (clamped - self.split).abs() > 1e-6 {
8355 self.split = clamped;
8356 self.dirty = true;
8357 }
8358 }
8359
8360 pub fn mode(&self) -> CompareMode {
8362 self.mode
8363 }
8364
8365 pub fn set_mode(&mut self, mode: CompareMode) {
8367 if mode != self.mode {
8368 self.mode = mode;
8369 self.dirty = true;
8370 }
8371 }
8372
8373 pub fn show_toolbar(&mut self, ui: &mut egui::Ui) -> CompareMode {
8377 ui.horizontal_wrapped(|ui| {
8378 ui.spacing_mut().item_spacing.x = 2.0;
8379
8380 for (label, tooltip, m) in [
8381 ("A", "Show only image A", CompareMode::OnlyA),
8382 ("B", "Show only image B", CompareMode::OnlyB),
8383 (
8384 "½",
8385 "Vertical split: A left / B right (drag the separator or slider)",
8386 CompareMode::HalfHalf,
8387 ),
8388 (
8389 "═",
8390 "Horizontal split: A top / B bottom (drag the separator or slider)",
8391 CompareMode::SplitHorizontal,
8392 ),
8393 ("A-B", "Subtract: A minus B", CompareMode::Subtract),
8394 (
8395 "R/B",
8396 "Composite: A in red, B in blue, half-sum in green",
8397 CompareMode::RedBlueGray,
8398 ),
8399 (
8400 "R/B⁻",
8401 "Negative composite: red-blue channels inverted",
8402 CompareMode::RedBlueGrayNeg,
8403 ),
8404 ] {
8405 if ui
8406 .selectable_label(self.mode == m, label)
8407 .on_hover_text(tooltip)
8408 .clicked()
8409 && self.mode != m
8410 {
8411 self.mode = m;
8412 self.dirty = true;
8413 }
8414 }
8415
8416 let is_split = matches!(
8417 self.mode,
8418 CompareMode::HalfHalf | CompareMode::SplitHorizontal
8419 );
8420 if is_split && !self.data_a.is_empty() {
8421 ui.add_space(4.0);
8422 if ui
8423 .add(egui::Slider::new(&mut self.split, 0.0..=1.0).text("split"))
8424 .changed()
8425 {
8426 self.dirty = true;
8427 }
8428 }
8429
8430 ui.add_space(8.0);
8431 ui.label("align:");
8432 for (label, tooltip, a) in [
8433 (
8434 "orig",
8435 "Align both images at the top-left origin",
8436 CompareAlignment::Origin,
8437 ),
8438 (
8439 "ctr",
8440 "Center both images on the common grid",
8441 CompareAlignment::Center,
8442 ),
8443 (
8444 "fit",
8445 "Stretch image B to image A's shape (bilinear)",
8446 CompareAlignment::Stretch,
8447 ),
8448 (
8449 "auto",
8450 "Auto-align image B to A by SIFT keypoint registration",
8451 CompareAlignment::Auto,
8452 ),
8453 ] {
8454 if ui
8455 .selectable_label(self.alignment == a, label)
8456 .on_hover_text(tooltip)
8457 .clicked()
8458 && self.alignment != a
8459 {
8460 self.alignment = a;
8461 self.dirty = true;
8462 }
8463 }
8464
8465 ui.add_space(8.0);
8468 let mut visible = self.keypoints_visible;
8469 if ui
8470 .checkbox(&mut visible, "kp")
8471 .on_hover_text("Show the matched SIFT keypoints (AUTO alignment)")
8472 .changed()
8473 {
8474 self.set_keypoints_visible(visible);
8475 }
8476 });
8477
8478 self.mode
8479 }
8480
8481 pub fn show(&mut self, ui: &mut egui::Ui) -> PlotResponse {
8483 if self.dirty && !self.data_a.is_empty() {
8484 if self.alignment == CompareAlignment::Auto {
8490 self.auto = self.compute_auto_alignment();
8491 if self.auto.is_none() {
8492 self.alignment = CompareAlignment::Origin;
8493 }
8494 } else {
8495 self.auto = None;
8496 }
8497 let (composite, cw, ch) = self.build_composite();
8498 self.composite_w = cw;
8499 self.composite_h = ch;
8500 if let Some(handle) = self.composite_handle {
8501 self.inner
8502 .try_update_rgba_image(handle, cw, ch, &composite)
8503 .ok();
8504 } else {
8505 let handle = self.inner.add_rgba_image(cw, ch, &composite);
8506 self.composite_handle = Some(handle);
8507 }
8508 self.sync_keypoint_overlay();
8509 self.dirty = false;
8510 }
8511 self.sync_separator();
8514 let response = self.inner.show(ui);
8515 self.read_separator_drag(&response);
8517 if let Some(cursor) = cursor_from_pointer_event(response.pointer_event.as_ref()) {
8521 self.cursor = Some(cursor);
8522 }
8523 response
8524 }
8525
8526 fn sync_separator(&mut self) {
8534 let want = if self.data_a.is_empty() {
8535 None
8536 } else {
8537 match self.mode {
8538 CompareMode::HalfHalf => Some(false),
8539 CompareMode::SplitHorizontal => Some(true),
8540 _ => None,
8541 }
8542 };
8543
8544 let Some(horizontal) = want else {
8545 if let Some(handle) = self.separator.take() {
8546 self.inner.remove(handle);
8547 }
8548 self.separator_dragging = false;
8549 return;
8550 };
8551
8552 if self.separator.is_some() && self.separator_horizontal != horizontal {
8555 if let Some(handle) = self.separator.take() {
8556 self.inner.remove(handle);
8557 }
8558 self.separator_dragging = false;
8559 }
8560
8561 let (x, y) = self.separator_position(horizontal);
8562 match self.separator {
8563 Some(handle) => {
8564 if !self.separator_dragging {
8565 self.inner.set_marker_position(handle, x, y);
8566 }
8567 }
8568 None => {
8569 let marker = if horizontal {
8570 Marker::hline(y)
8571 } else {
8572 Marker::vline(x)
8573 }
8574 .with_color(Color32::BLUE)
8575 .with_draggable(true);
8576 self.separator = Some(self.inner.add_marker_data(&marker));
8577 self.separator_horizontal = horizontal;
8578 }
8579 }
8580 }
8581
8582 fn separator_position(&self, horizontal: bool) -> (f64, f64) {
8587 if horizontal {
8588 (0.0, self.split as f64 * self.composite_h as f64)
8589 } else {
8590 (self.split as f64 * self.composite_w as f64, 0.0)
8591 }
8592 }
8593
8594 fn read_separator_drag(&mut self, response: &PlotResponse) {
8599 let Some(sep) = self.separator else { return };
8600 if response.marker_drag_started == Some(sep) {
8601 self.separator_dragging = true;
8602 }
8603 if (response.marker_moved == Some(sep) || response.marker_drag_finished == Some(sep))
8604 && let Some((x, y)) = self.inner.marker_position(sep)
8605 {
8606 let (pos, extent) = if self.separator_horizontal {
8607 (y, self.composite_h)
8608 } else {
8609 (x, self.composite_w)
8610 };
8611 if extent > 0 {
8612 self.set_split((pos / extent as f64) as f32);
8613 }
8614 }
8615 if response.marker_drag_finished == Some(sep) {
8616 self.separator_dragging = false;
8617 }
8618 }
8619
8620 pub fn data_to_pixel(&self, x: f64, y: f64, axis: YAxis) -> Option<egui::Pos2> {
8624 self.inner.data_to_pixel(x, y, axis)
8625 }
8626
8627 pub fn raw_pixel_data(&self, x: f64, y: f64) -> (Option<f32>, Option<f32>) {
8634 let ((xa, ya), (xb, yb)) = match (self.alignment, self.auto.as_ref()) {
8638 (CompareAlignment::Auto, Some(al)) => {
8639 let [[a, b], [c, d]] = al.matrix;
8640 let (tx, ty) = al.offset;
8641 ((x, y), (a * x + b * y + tx, c * x + d * y + ty))
8642 }
8643 _ => compare_aligned_coords(
8644 self.alignment,
8645 x,
8646 y,
8647 self.width_a,
8648 self.height_a,
8649 self.width_b,
8650 self.height_b,
8651 ),
8652 };
8653 (
8654 compare_pixel_at(self.width_a, self.height_a, &self.data_a, xa, ya),
8655 compare_pixel_at(self.width_b, self.height_b, &self.data_b, xb, yb),
8656 )
8657 }
8658
8659 pub fn show_status_bar(&self, ui: &mut egui::Ui) {
8669 ui.horizontal(|ui| {
8670 ui.spacing_mut().item_spacing.x = 12.0;
8671 let (a_text, b_text) = match self.cursor {
8672 Some([x, y]) => {
8673 ui.label(format!("X: {x:.1} Y: {y:.1}"));
8674 let (a, b) = self.raw_pixel_data(x, y);
8675 (
8676 format_compare_value(self.data_a.is_empty(), a),
8677 format_compare_value(self.data_b.is_empty(), b),
8678 )
8679 }
8680 None => ("NA".to_string(), "NA".to_string()),
8681 };
8682 ui.label(format!("ImageA: {a_text}"));
8683 ui.label(format!("ImageB: {b_text}"));
8684 if let Some(t) = self.transformation() {
8685 ui.label(format!("Align: {}", align_summary(&t)))
8686 .on_hover_text(align_tooltip(&t));
8687 }
8688 });
8689 }
8690
8691 fn compute_auto_alignment(&self) -> Option<SiftAlignment> {
8703 sift_auto_align(
8704 &self.data_a,
8705 self.width_a as usize,
8706 self.height_a as usize,
8707 &self.data_b,
8708 self.width_b as usize,
8709 self.height_b as usize,
8710 )
8711 }
8712
8713 fn sync_keypoint_overlay(&mut self) {
8719 if let Some(handle) = self.keypoint_overlay.take() {
8720 self.inner.remove(handle);
8721 }
8722 if !self.keypoints_visible {
8723 return;
8724 }
8725 let Some(al) = self.auto.as_ref() else { return };
8726 if al.matches.is_empty() {
8727 return;
8728 }
8729 let xs: Vec<f64> = al.matches.iter().map(|m| m.ax as f64).collect();
8730 let ys: Vec<f64> = al.matches.iter().map(|m| m.ay as f64).collect();
8731 let handle =
8732 self.inner
8733 .add_scatter_with_symbol(&xs, &ys, KEYPOINT_COLOR, Symbol::Cross, 6.0);
8734 self.keypoint_overlay = Some(handle);
8735 }
8736
8737 fn build_composite(&self) -> (Vec<[u8; 4]>, u32, u32) {
8738 let (data1, data2, cw, ch) = match (self.alignment, self.auto.as_ref()) {
8742 (CompareAlignment::Auto, Some(al)) => {
8743 let cw = al.width as u32;
8744 let ch = al.height as u32;
8745 let data1 = margin_image(
8746 &self.data_a,
8747 self.width_a as usize,
8748 self.height_a as usize,
8749 al.width,
8750 al.height,
8751 false,
8752 );
8753 (data1, al.aligned.clone(), cw, ch)
8754 }
8755 _ => align_compare_images(
8756 self.alignment,
8757 &self.data_a,
8758 self.width_a,
8759 self.height_a,
8760 &self.data_b,
8761 self.width_b,
8762 self.height_b,
8763 ),
8764 };
8765 let w = cw as usize;
8766 let h = ch as usize;
8767
8768 let pixels = match self.mode {
8769 CompareMode::OnlyA => colormap_to_rgba(cw, &data1, &self.colormap),
8770 CompareMode::OnlyB => colormap_to_rgba(cw, &data2, &self.colormap),
8771 CompareMode::HalfHalf => {
8772 let rgba_a = colormap_to_rgba(cw, &data1, &self.colormap);
8773 let rgba_b = colormap_to_rgba(cw, &data2, &self.colormap);
8774 let split_col = (self.split * cw as f32).round() as usize;
8775 split_composite(&rgba_a, &rgba_b, w, h, split_col, false)
8776 }
8777 CompareMode::SplitHorizontal => {
8778 let rgba_a = colormap_to_rgba(cw, &data1, &self.colormap);
8779 let rgba_b = colormap_to_rgba(cw, &data2, &self.colormap);
8780 let split_row = (self.split * ch as f32).round() as usize;
8781 split_composite(&rgba_a, &rgba_b, w, h, split_row, true)
8782 }
8783 CompareMode::Subtract => data1
8784 .iter()
8785 .zip(data2.iter())
8786 .map(|(&a, &b)| {
8787 let diff = (a - b).clamp(-1.0, 1.0);
8788 if diff > 0.0 {
8789 [(diff * 255.0) as u8, 0, 0, 255]
8790 } else if diff < 0.0 {
8791 [0, 0, ((-diff) * 255.0) as u8, 255]
8792 } else {
8793 [128, 128, 128, 255]
8794 }
8795 })
8796 .collect(),
8797 CompareMode::RedBlueGray => {
8798 red_blue_gray_composite(&data1, &data2, &self.colormap, false)
8799 }
8800 CompareMode::RedBlueGrayNeg => {
8801 red_blue_gray_composite(&data1, &data2, &self.colormap, true)
8802 }
8803 };
8804 (pixels, cw, ch)
8805 }
8806}
8807
8808pub fn compare_pixel_at(width: u32, height: u32, data: &[f32], x: f64, y: f64) -> Option<f32> {
8816 if x < 0.0 || y < 0.0 {
8817 return None;
8818 }
8819 let col = x as usize;
8820 let row = y as usize;
8821 if col >= width as usize || row >= height as usize {
8822 return None;
8823 }
8824 data.get(row * width as usize + col).copied()
8825}
8826
8827fn margin_image(
8836 src: &[f32],
8837 src_w: usize,
8838 src_h: usize,
8839 dst_w: usize,
8840 dst_h: usize,
8841 center: bool,
8842) -> Vec<f32> {
8843 let mut out = vec![0.0f32; dst_w * dst_h];
8844 if src_w == 0 || src_h == 0 || src_w > dst_w || src_h > dst_h {
8845 return out;
8846 }
8847 let (pos_row, pos_col) = if center {
8850 (dst_h / 2 - src_h / 2, dst_w / 2 - src_w / 2)
8851 } else {
8852 (0, 0)
8853 };
8854 for r in 0..src_h {
8855 let dst_base = (pos_row + r) * dst_w + pos_col;
8856 let src_base = r * src_w;
8857 out[dst_base..dst_base + src_w].copy_from_slice(&src[src_base..src_base + src_w]);
8858 }
8859 out
8860}
8861
8862fn rescale_array(src: &[f32], src_w: usize, src_h: usize, dst_w: usize, dst_h: usize) -> Vec<f32> {
8872 let mut out = vec![0.0f32; dst_w * dst_h];
8873 if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
8874 return out;
8875 }
8876 let row_scale = if dst_h > 1 {
8877 (src_h - 1) as f64 / (dst_h - 1) as f64
8878 } else {
8879 0.0
8880 };
8881 let col_scale = if dst_w > 1 {
8882 (src_w - 1) as f64 / (dst_w - 1) as f64
8883 } else {
8884 0.0
8885 };
8886 let sample = |row: f64, col: f64| -> f32 {
8887 let row = row.clamp(0.0, (src_h - 1) as f64);
8889 let col = col.clamp(0.0, (src_w - 1) as f64);
8890 let r0 = row.floor() as usize;
8891 let c0 = col.floor() as usize;
8892 let r1 = (r0 + 1).min(src_h - 1);
8893 let c1 = (c0 + 1).min(src_w - 1);
8894 let fr = row - r0 as f64;
8895 let fc = col - c0 as f64;
8896 let at = |r: usize, c: usize| src[r * src_w + c] as f64;
8897 let top = at(r0, c0) * (1.0 - fc) + at(r0, c1) * fc;
8898 let bot = at(r1, c0) * (1.0 - fc) + at(r1, c1) * fc;
8899 (top * (1.0 - fr) + bot * fr) as f32
8900 };
8901 for or in 0..dst_h {
8902 for oc in 0..dst_w {
8903 out[or * dst_w + oc] = sample(or as f64 * row_scale, oc as f64 * col_scale);
8904 }
8905 }
8906 out
8907}
8908
8909fn align_compare_images(
8920 mode: CompareAlignment,
8921 a: &[f32],
8922 wa: u32,
8923 ha: u32,
8924 b: &[f32],
8925 wb: u32,
8926 hb: u32,
8927) -> (Vec<f32>, Vec<f32>, u32, u32) {
8928 match mode {
8929 CompareAlignment::Origin | CompareAlignment::Center => {
8930 let cw = wa.max(wb);
8931 let ch = ha.max(hb);
8932 let center = matches!(mode, CompareAlignment::Center);
8933 let (cwu, chu) = (cw as usize, ch as usize);
8934 let d1 = margin_image(a, wa as usize, ha as usize, cwu, chu, center);
8935 let d2 = margin_image(b, wb as usize, hb as usize, cwu, chu, center);
8936 (d1, d2, cw, ch)
8937 }
8938 CompareAlignment::Stretch => {
8939 let d2 = rescale_array(b, wb as usize, hb as usize, wa as usize, ha as usize);
8940 (a.to_vec(), d2, wa, ha)
8941 }
8942 CompareAlignment::Auto => {
8947 let cw = wa.max(wb);
8948 let ch = ha.max(hb);
8949 let (cwu, chu) = (cw as usize, ch as usize);
8950 let d1 = margin_image(a, wa as usize, ha as usize, cwu, chu, false);
8951 let d2 = margin_image(b, wb as usize, hb as usize, cwu, chu, false);
8952 (d1, d2, cw, ch)
8953 }
8954 }
8955}
8956
8957fn compare_aligned_coords(
8973 mode: CompareAlignment,
8974 x: f64,
8975 y: f64,
8976 wa: u32,
8977 ha: u32,
8978 wb: u32,
8979 hb: u32,
8980) -> ((f64, f64), (f64, f64)) {
8981 match mode {
8982 CompareAlignment::Origin => ((x, y), (x, y)),
8983 CompareAlignment::Center => {
8984 let xx = wa.max(wb) as f64;
8985 let yy = ha.max(hb) as f64;
8986 let xa = x - (xx - wa as f64) * 0.5;
8987 let xb = x - (xx - wb as f64) * 0.5;
8988 let ya = y - (yy - ha as f64) * 0.5;
8989 let yb = y - (yy - hb as f64) * 0.5;
8990 ((xa, ya), (xb, yb))
8991 }
8992 CompareAlignment::Stretch => {
8993 let xb = x * wb as f64 / wa as f64;
8994 let yb = y * hb as f64 / ha as f64;
8995 ((x, y), (xb, yb))
8996 }
8997 CompareAlignment::Auto => ((x, y), (x, y)),
9001 }
9002}
9003
9004fn format_compare_value(no_image: bool, value: Option<f32>) -> String {
9010 if no_image {
9011 "no image".to_string()
9012 } else {
9013 match value {
9014 Some(v) => format!("{v:.6}"),
9015 None => "NA".to_string(),
9016 }
9017 }
9018}
9019
9020fn isclose(a: f64, b: f64, atol: f64) -> bool {
9022 (a - b).abs() <= atol + 1e-5 * b.abs()
9023}
9024
9025fn align_summary(t: &AffineTransformation) -> String {
9031 let notable_translation = !isclose(t.tx, 0.0, 0.01) || !isclose(t.ty, 0.0, 0.01);
9032 let notable_scale = !isclose(t.sx, 1.0, 0.01) || !isclose(t.sy, 1.0, 0.01);
9033 let notable_rotation = !isclose(t.rotation, 0.0, 0.01);
9034
9035 let mut parts = Vec::new();
9036 if notable_translation {
9037 parts.push("Translation");
9038 }
9039 if notable_scale {
9040 parts.push("Scale");
9041 }
9042 if notable_rotation {
9043 parts.push("Rotation");
9044 }
9045 if !parts.is_empty() {
9046 return parts.join("+");
9047 }
9048 let any_translation = !isclose(t.tx, 0.0, 1e-8) || !isclose(t.ty, 0.0, 1e-8);
9051 let any_scale = !isclose(t.sx, 1.0, 1e-8) || !isclose(t.sy, 1.0, 1e-8);
9052 let any_rotation = !isclose(t.rotation, 0.0, 1e-8);
9053 if any_translation || any_scale || any_rotation {
9054 "No big changes".to_string()
9055 } else {
9056 "No changes".to_string()
9057 }
9058}
9059
9060fn align_tooltip(t: &AffineTransformation) -> String {
9067 let mut lines = Vec::new();
9068 if !isclose(t.tx, 0.0, 1e-8) {
9069 lines.push(format!("Translation x: {:.3}px", t.tx));
9070 }
9071 if !isclose(t.ty, 0.0, 1e-8) {
9072 lines.push(format!("Translation y: {:.3}px", t.ty));
9073 }
9074 if !isclose(t.sx, 1.0, 1e-8) {
9075 lines.push(format!("Scale x: {:.3}", t.sx));
9076 }
9077 if !isclose(t.sy, 1.0, 1e-8) {
9078 lines.push(format!("Scale y: {:.3}", t.sy));
9079 }
9080 if !isclose(t.rotation, 0.0, 1e-8) {
9081 lines.push(format!(
9082 "Rotation: {:.3}deg",
9083 t.rotation * 180.0 / std::f64::consts::PI
9084 ));
9085 }
9086 if lines.is_empty() {
9087 "No transformation".to_string()
9088 } else {
9089 lines.join("\n")
9090 }
9091}
9092
9093impl Deref for CompareImages {
9094 type Target = PlotWidget;
9095
9096 fn deref(&self) -> &Self::Target {
9097 &self.inner
9098 }
9099}
9100
9101impl DerefMut for CompareImages {
9102 fn deref_mut(&mut self) -> &mut Self::Target {
9103 &mut self.inner
9104 }
9105}
9106
9107fn colormap_to_rgba(_width: u32, data: &[f32], colormap: &Colormap) -> Vec<[u8; 4]> {
9109 data.iter()
9110 .map(|&v| {
9111 let t = colormap.normalize(v as f64);
9112 let idx = (t * 255.0).clamp(0.0, 255.0) as usize;
9113 colormap.lut[idx]
9114 })
9115 .collect()
9116}
9117
9118fn red_blue_gray_composite(
9127 data_a: &[f32],
9128 data_b: &[f32],
9129 colormap: &Colormap,
9130 neg: bool,
9131) -> Vec<[u8; 4]> {
9132 let byte = |v: f32| (colormap.normalize(v as f64) * 255.0).clamp(0.0, 255.0) as u8;
9133 data_a
9134 .iter()
9135 .zip(data_b.iter())
9136 .map(|(&va, &vb)| {
9137 let a = byte(va);
9138 let b = byte(vb);
9139 let g = a / 2 + b / 2;
9140 if neg {
9141 [255 - b, 255 - g, 255 - a, 255]
9142 } else {
9143 [a, g, b, 255]
9144 }
9145 })
9146 .collect()
9147}
9148
9149fn split_composite(
9159 a: &[[u8; 4]],
9160 b: &[[u8; 4]],
9161 width: usize,
9162 height: usize,
9163 split: usize,
9164 horizontal: bool,
9165) -> Vec<[u8; 4]> {
9166 let mut out = vec![[0u8; 4]; width * height];
9167 for row in 0..height {
9168 let base = row * width;
9169 for col in 0..width {
9170 let i = base + col;
9171 let use_a = if horizontal { row < split } else { col < split };
9172 out[i] = if use_a { a[i] } else { b[i] };
9173 }
9174 }
9175 out
9176}
9177
9178const COLORBAR_WIDTH: f32 = 70.0;
9201
9202const INTERACTIVE_COLORBAR_WIDTH: f32 = 175.0;
9207
9208fn image_view_colorbar(colormap: &Colormap) -> crate::widget::colorbar::ColorBarWidget {
9214 crate::widget::colorbar::ColorBarWidget::new(colormap.clone())
9215}
9216
9217fn scatter_view_colorbar(
9223 colormap: Option<&Colormap>,
9224) -> Option<crate::widget::colorbar::ColorBarWidget> {
9225 colormap.map(image_view_colorbar)
9226}
9227
9228fn scatter_masked_selection(mask: &[u8]) -> Vec<bool> {
9234 mask.iter().map(|&level| level != 0).collect()
9235}
9236
9237fn cursor_from_pointer_event(
9243 event: Option<&crate::widget::interaction::PlotPointerEvent>,
9244) -> Option<[f64; 2]> {
9245 use crate::widget::interaction::PlotPointerEvent;
9246 match event? {
9247 PlotPointerEvent::Moved { data, .. }
9248 | PlotPointerEvent::Clicked { data, .. }
9249 | PlotPointerEvent::DoubleClicked { data, .. } => Some([data.0, data.1]),
9250 PlotPointerEvent::LimitsChanged { .. } => None,
9251 }
9252}
9253
9254fn image_view_profile_values(
9271 mode: ProfileMode,
9272 width: u32,
9273 height: u32,
9274 pixels: &[f32],
9275 start: (f64, f64),
9276 end: (f64, f64),
9277) -> Option<(Vec<f64>, Vec<f64>)> {
9278 match mode {
9279 ProfileMode::None => None,
9280 ProfileMode::Line => line_profile_values(width, height, pixels, start, end).ok(),
9281 ProfileMode::Horizontal => {
9282 let row = end.1.floor();
9283 if row < 0.0 || row >= height as f64 {
9284 return None;
9285 }
9286 horizontal_profile_values(width, height, pixels, row as u32)
9287 .ok()
9288 .map(|y| {
9289 let x: Vec<f64> = (0..width as usize).map(|i| i as f64).collect();
9290 (x, y)
9291 })
9292 }
9293 ProfileMode::Vertical => {
9294 let col = end.0.floor();
9295 if col < 0.0 || col >= width as f64 {
9296 return None;
9297 }
9298 vertical_profile_values(width, height, pixels, col as u32)
9299 .ok()
9300 .map(|y| {
9301 let x: Vec<f64> = (0..height as usize).map(|i| i as f64).collect();
9302 (x, y)
9303 })
9304 }
9305 ProfileMode::Rectangle => {
9306 let rect = (
9307 start.0.min(end.0),
9308 start.0.max(end.0),
9309 start.1.min(end.1),
9310 start.1.max(end.1),
9311 );
9312 rect_profile_values(width, height, pixels, rect, true, ProfileMethod::Mean).ok()
9313 }
9314 }
9315}
9316
9317fn profile_roi_from_drag(mode: ProfileMode, start: (f64, f64), end: (f64, f64)) -> Option<Roi> {
9333 match mode {
9334 ProfileMode::None => None,
9335 ProfileMode::Line => Some(Roi::Line { start, end }),
9336 ProfileMode::Horizontal => {
9337 let row = end.1.floor();
9338 Some(Roi::HRange { y: (row, row) })
9339 }
9340 ProfileMode::Vertical => {
9341 let col = end.0.floor();
9342 Some(Roi::VRange { x: (col, col) })
9343 }
9344 ProfileMode::Rectangle => Some(Roi::Rect {
9345 x: (start.0.min(end.0), start.0.max(end.0)),
9346 y: (start.1.min(end.1), start.1.max(end.1)),
9347 }),
9348 }
9349}
9350
9351fn image_view_should_paint_mask(mode: PlotInteractionMode, mask_enabled: bool) -> bool {
9358 mask_enabled && mode == PlotInteractionMode::MaskDraw
9359}
9360
9361fn scalar_mask_from_level_buffer(width: u32, height: u32, levels: &[u8]) -> ScalarMask {
9371 let mut mask = ScalarMask::new(width as usize, height as usize);
9372 mask.set_mask_data(levels, width as usize);
9373 mask
9374}
9375
9376#[allow(clippy::too_many_arguments)]
9381fn image_view_image_spec<'a>(
9382 width: u32,
9383 height: u32,
9384 pixels: &'a [f32],
9385 colormap: &Colormap,
9386 alpha: f32,
9387 interpolation: InterpolationMode,
9388 aggregation: AggregationMode,
9389 aggregation_block: (u32, u32),
9390) -> ImageSpec<'a> {
9391 let mut spec = ImageSpec::scalar(width, height, pixels, colormap.clone());
9392 spec.alpha = alpha;
9393 spec.interpolation = interpolation;
9394 spec.aggregation = aggregation;
9395 spec.aggregation_block = aggregation_block;
9396 spec
9397}
9398
9399pub struct ImageView {
9400 image_plot: Plot2D,
9401 histo_h: Plot1D,
9402 histo_v: Plot1D,
9403 sync_x: crate::widget::sync::SyncAxes,
9404 sync_y: crate::widget::sync::SyncAxes,
9405 image_handle: Option<ItemHandle>,
9406 histo_h_curve: Option<ItemHandle>,
9407 histo_v_curve: Option<ItemHandle>,
9408 width: u32,
9409 height: u32,
9410 pixels: Vec<f32>,
9411 colormap: Colormap,
9415 alpha: crate::widget::alpha_slider::AlphaSlider,
9418 interpolation: InterpolationMode,
9421 aggregation: AggregationMode,
9424 aggregation_block: (u32, u32),
9429 position_info: crate::widget::position_info::PositionInfo,
9432 cursor: Option<[f64; 2]>,
9435 radar: crate::widget::radar_view::RadarView,
9439 profile_mode: ProfileMode,
9443 profile_window: crate::widget::profile_window::ProfileWindow,
9445 profile_drag_start: Option<(f64, f64)>,
9448 show_colorbar: bool,
9452 show_side_histograms: bool,
9457 interactive_colorbar: bool,
9465 value_histogram: Option<(Vec<u64>, Vec<f64>)>,
9469 value_range: (f64, f64),
9472 histogram_norm: Option<Normalization>,
9476 mask: crate::widget::mask_tools::MaskToolsWidget,
9483}
9484
9485fn colorbar_column_width(show: bool, has_colorbar: bool) -> f32 {
9491 if show && has_colorbar {
9492 COLORBAR_WIDTH
9493 } else {
9494 0.0
9495 }
9496}
9497
9498fn row_content_width(avail_x: f32, side_columns: f32, gaps: u32, spacing: f32) -> f32 {
9505 (avail_x - side_columns - gaps as f32 * spacing).max(0.0)
9506}
9507
9508fn finite_minmax(data: &[f64]) -> Option<(f64, f64)> {
9511 let mut lo = f64::INFINITY;
9512 let mut hi = f64::NEG_INFINITY;
9513 for &v in data {
9514 if v.is_finite() {
9515 lo = lo.min(v);
9516 hi = hi.max(v);
9517 }
9518 }
9519 (lo.is_finite() && hi.is_finite()).then_some((lo, hi))
9520}
9521
9522fn side_histogram_extent(show: bool, requested: f32) -> f32 {
9527 if show { requested } else { 0.0 }
9528}
9529
9530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9533pub enum ImageHistogramAxis {
9534 X,
9536 Y,
9538}
9539
9540#[derive(Debug, Clone, PartialEq)]
9546pub struct ImageProfileHistogram {
9547 pub data: Vec<f64>,
9549 pub extent: (f64, f64),
9551}
9552
9553fn image_column_sums(pixels: &[f32], w: usize, h: usize) -> Vec<f64> {
9556 (0..w)
9557 .map(|col| (0..h).map(|row| pixels[row * w + col] as f64).sum())
9558 .collect()
9559}
9560
9561fn image_row_sums(pixels: &[f32], w: usize, h: usize) -> Vec<f64> {
9564 (0..h)
9565 .map(|row| (0..w).map(|col| pixels[row * w + col] as f64).sum())
9566 .collect()
9567}
9568
9569fn image_value_at(
9577 x: f64,
9578 y: f64,
9579 pixels: &[f32],
9580 width: usize,
9581 height: usize,
9582) -> Option<(f64, f64, f64)> {
9583 if width == 0 || height == 0 || pixels.len() < width * height {
9584 return None;
9585 }
9586 if x < 0.0 || y < 0.0 {
9588 return None;
9589 }
9590 let col = x as usize;
9593 let row = y as usize;
9594 if col >= width || row >= height {
9595 return None;
9596 }
9597 Some((col as f64, row as f64, pixels[row * width + col] as f64))
9598}
9599
9600impl ImageView {
9601 pub fn new(render_state: &RenderState, image_id: PlotId) -> Self {
9607 let mut image_plot = Plot2D::new(render_state, image_id);
9608 image_plot.set_graph_cursor(true);
9609 image_plot.set_keep_data_aspect_ratio(true);
9610 image_plot.set_show_colorbar(false);
9616
9617 let mut histo_h = Plot1D::new(render_state, image_id + 1);
9626 histo_h.clear_graph_x_label();
9627 histo_h.clear_graph_y_label(YAxis::Left);
9628 histo_h.set_graph_grid(false);
9629 histo_h.plot_mut().reserve_y_label_gutter = true;
9633 histo_h.plot_mut().set_data_margins(DataMargins {
9634 x_min: 0.0,
9635 x_max: 0.0,
9636 y_min: 0.1,
9637 y_max: 0.1,
9638 });
9639
9640 let mut histo_v = Plot1D::new(render_state, image_id + 2);
9641 histo_v.clear_graph_x_label();
9642 histo_v.clear_graph_y_label(YAxis::Left);
9643 histo_v.set_graph_grid(false);
9644 histo_v.plot_mut().reserve_x_label_gutter = true;
9648 histo_v.plot_mut().set_data_margins(DataMargins {
9649 x_min: 0.1,
9650 x_max: 0.1,
9651 y_min: 0.0,
9652 y_max: 0.0,
9653 });
9654
9655 Self {
9656 image_plot,
9657 histo_h,
9658 histo_v,
9659 sync_x: crate::widget::sync::SyncAxes::new().with_sync_y(false),
9660 sync_y: crate::widget::sync::SyncAxes::new().with_sync_x(false),
9661 image_handle: None,
9662 histo_h_curve: None,
9663 histo_v_curve: None,
9664 width: 0,
9665 height: 0,
9666 pixels: Vec::new(),
9667 colormap: Colormap::viridis(0.0, 1.0),
9668 alpha: crate::widget::alpha_slider::AlphaSlider::default(),
9669 interpolation: InterpolationMode::default(),
9670 aggregation: AggregationMode::default(),
9671 aggregation_block: (1, 1),
9672 position_info: crate::widget::position_info::PositionInfo::with_xy(),
9673 cursor: None,
9674 radar: crate::widget::radar_view::RadarView::default(),
9675 profile_mode: ProfileMode::None,
9676 profile_window: crate::widget::profile_window::ProfileWindow::new(
9677 render_state,
9678 image_id + 3,
9679 ),
9680 profile_drag_start: None,
9681 show_colorbar: true,
9682 show_side_histograms: true,
9683 interactive_colorbar: false,
9684 value_histogram: None,
9685 value_range: (0.0, 1.0),
9686 histogram_norm: None,
9687 mask: crate::widget::mask_tools::MaskToolsWidget::new(0, 0),
9688 }
9689 }
9690
9691 pub fn show_colorbar(&self) -> bool {
9693 self.show_colorbar
9694 }
9695
9696 pub fn set_show_colorbar(&mut self, show: bool) {
9700 self.show_colorbar = show;
9701 }
9702
9703 pub fn interactive_colorbar(&self) -> bool {
9706 self.interactive_colorbar
9707 }
9708
9709 pub fn set_interactive_colorbar(&mut self, interactive: bool) {
9716 self.interactive_colorbar = interactive;
9717 }
9718
9719 pub fn is_side_histogram_displayed(&self) -> bool {
9722 self.show_side_histograms
9723 }
9724
9725 pub fn set_side_histogram_displayed(&mut self, show: bool) {
9730 self.show_side_histograms = show;
9731 }
9732
9733 pub fn set_image(
9735 &mut self,
9736 width: u32,
9737 height: u32,
9738 pixels: &[f32],
9739 colormap: Colormap,
9740 ) -> Result<(), PlotDataError> {
9741 let expected = (width as usize).saturating_mul(height as usize);
9742 if pixels.len() != expected {
9743 return Err(PlotDataError::ImageDataLength {
9744 expected,
9745 actual: pixels.len(),
9746 });
9747 }
9748 self.width = width;
9749 self.height = height;
9750 self.pixels = pixels.to_vec();
9751 self.histogram_norm = None;
9754 self.value_histogram = None;
9755 self.colormap = colormap.clone();
9756 self.image_plot.set_default_colormap(colormap);
9757
9758 if self.mask.width != width || self.mask.height != height {
9761 self.mask.reset_geometry(width, height);
9762 }
9763
9764 self.radar
9768 .set_data_bounds(0.0, width as f64, 0.0, height as f64);
9769
9770 self.upload_image();
9771 self.rebuild_histograms();
9772 Ok(())
9773 }
9774
9775 fn upload_image(&mut self) {
9780 if self.width == 0 || self.pixels.is_empty() {
9781 return;
9782 }
9783 let masked: Option<Vec<f32>> = if self.mask.width == self.width
9789 && self.mask.height == self.height
9790 && self.mask.mask.iter().any(|&level| level != 0)
9791 {
9792 let scalar_mask =
9793 scalar_mask_from_level_buffer(self.width, self.height, &self.mask.mask);
9794 Some(scalar_mask.apply(&self.pixels))
9795 } else {
9796 None
9797 };
9798 let pixels: &[f32] = masked.as_deref().unwrap_or(&self.pixels);
9799
9800 let spec = image_view_image_spec(
9801 self.width,
9802 self.height,
9803 pixels,
9804 &self.colormap,
9805 self.alpha.alpha(),
9806 self.interpolation,
9807 self.aggregation,
9808 self.aggregation_block,
9809 );
9810 if let Some(handle) = self.image_handle {
9811 self.image_plot.update_image_spec(handle, spec);
9812 } else {
9813 let h = self.image_plot.add_image_spec(spec);
9814 self.image_handle = Some(h);
9815 }
9816 }
9817
9818 fn ensure_value_histogram(&mut self) {
9824 let norm = self.colormap.normalization;
9825 if self.histogram_norm == Some(norm) {
9826 return;
9827 }
9828 let data: Vec<f64> = self.pixels.iter().map(|&p| p as f64).collect();
9829 let log = norm == Normalization::Log;
9830 self.value_histogram = crate::core::histogram::compute_histogram(&data, None, log);
9831 self.value_range = finite_minmax(&data).unwrap_or((self.colormap.vmin, self.colormap.vmax));
9832 self.histogram_norm = Some(norm);
9833 }
9834
9835 pub fn alpha(&self) -> f32 {
9838 self.alpha.alpha()
9839 }
9840
9841 pub fn set_alpha(&mut self, alpha: f32) {
9845 self.alpha.set_alpha(alpha);
9846 self.upload_image();
9847 }
9848
9849 pub fn interpolation(&self) -> InterpolationMode {
9852 self.interpolation
9853 }
9854
9855 pub fn set_interpolation(&mut self, interpolation: InterpolationMode) {
9858 if interpolation != self.interpolation {
9859 self.interpolation = interpolation;
9860 self.upload_image();
9861 }
9862 }
9863
9864 pub fn aggregation(&self) -> AggregationMode {
9866 self.aggregation
9867 }
9868
9869 pub fn aggregation_block(&self) -> (u32, u32) {
9872 self.aggregation_block
9873 }
9874
9875 pub fn set_aggregation(&mut self, mode: AggregationMode, block: (u32, u32)) {
9879 let block = (block.0.max(1), block.1.max(1));
9880 if mode != self.aggregation || block != self.aggregation_block {
9881 self.aggregation = mode;
9882 self.aggregation_block = block;
9883 self.upload_image();
9884 }
9885 }
9886
9887 pub fn show_toolbar(&mut self, ui: &mut egui::Ui) {
9893 ui.horizontal_wrapped(|ui| {
9894 let mut interpolation = self.interpolation;
9896 egui::ComboBox::from_label("interp")
9897 .selected_text(match interpolation {
9898 InterpolationMode::Nearest => "nearest",
9899 InterpolationMode::Linear => "linear",
9900 })
9901 .show_ui(ui, |ui| {
9902 ui.selectable_value(&mut interpolation, InterpolationMode::Nearest, "nearest");
9903 ui.selectable_value(&mut interpolation, InterpolationMode::Linear, "linear");
9904 });
9905 if interpolation != self.interpolation {
9906 self.set_interpolation(interpolation);
9907 }
9908
9909 let mut aggregation = self.aggregation;
9911 egui::ComboBox::from_label("agg")
9912 .selected_text(match aggregation {
9913 AggregationMode::None => "none",
9914 AggregationMode::Max => "max",
9915 AggregationMode::Mean => "mean",
9916 AggregationMode::Min => "min",
9917 })
9918 .show_ui(ui, |ui| {
9919 ui.selectable_value(&mut aggregation, AggregationMode::None, "none");
9920 ui.selectable_value(&mut aggregation, AggregationMode::Max, "max");
9921 ui.selectable_value(&mut aggregation, AggregationMode::Mean, "mean");
9922 ui.selectable_value(&mut aggregation, AggregationMode::Min, "min");
9923 });
9924
9925 let mut block = self.aggregation_block;
9927 let bx = ui.add(
9928 egui::DragValue::new(&mut block.0)
9929 .range(1..=64)
9930 .prefix("bx "),
9931 );
9932 let by = ui.add(
9933 egui::DragValue::new(&mut block.1)
9934 .range(1..=64)
9935 .prefix("by "),
9936 );
9937 if aggregation != self.aggregation || bx.changed() || by.changed() {
9938 self.set_aggregation(aggregation, block);
9939 }
9940
9941 if ui
9943 .selectable_label(self.show_colorbar, "colorbar")
9944 .on_hover_text("Show/hide the colorbar")
9945 .clicked()
9946 {
9947 crate::widget::actions::control::image_colorbar_toggle(self);
9948 }
9949 });
9950
9951 let response = self.alpha.ui(ui);
9952 if response.changed() {
9953 self.upload_image();
9954 }
9955
9956 ui.horizontal(|ui| {
9959 ui.spacing_mut().item_spacing.x = 2.0;
9960 ui.label("profile:");
9961 for (label, tooltip, mode) in [
9962 ("—", "Row profile (horizontal)", ProfileMode::Horizontal),
9963 ("|", "Column profile (vertical)", ProfileMode::Vertical),
9964 ("/", "Line profile (drag)", ProfileMode::Line),
9965 ("□", "Rectangle profile (drag)", ProfileMode::Rectangle),
9966 ] {
9967 if ui
9968 .selectable_label(self.profile_mode == mode, label)
9969 .on_hover_text(tooltip)
9970 .clicked()
9971 {
9972 let next = if self.profile_mode == mode {
9973 ProfileMode::None
9974 } else {
9975 mode
9976 };
9977 self.set_profile_mode(next);
9978 }
9979 }
9980 });
9981
9982 let in_mask_draw = self.image_plot.interaction_mode() == PlotInteractionMode::MaskDraw;
9988 ui.horizontal(|ui| {
9989 ui.spacing_mut().item_spacing.x = 2.0;
9990 ui.label("mask:");
9991 if ui
9992 .selectable_label(in_mask_draw, "✏")
9993 .on_hover_text("Draw mask (pencil): primary drag paints the mask")
9994 .clicked()
9995 {
9996 self.set_mask_draw(!in_mask_draw);
9997 }
9998 if in_mask_draw {
9999 ui.selectable_value(
10000 &mut self.mask.active_tool,
10001 crate::widget::mask_tools::MaskTool::Pencil,
10002 "pencil",
10003 )
10004 .on_hover_text("Paint mask");
10005 ui.selectable_value(
10006 &mut self.mask.active_tool,
10007 crate::widget::mask_tools::MaskTool::Eraser,
10008 "eraser",
10009 )
10010 .on_hover_text("Erase mask");
10011 ui.add(egui::Slider::new(&mut self.mask.brush_size, 1..=50).text("brush"));
10015 ui.add(
10016 egui::DragValue::new(&mut self.mask.brush_size)
10017 .range(1..=1024)
10018 .speed(1.0),
10019 )
10020 .on_hover_text("Brush width in pixels (1-1024)");
10021 if ui.button("clear mask").clicked() {
10022 self.mask.clear_all();
10023 self.mask.commit();
10024 self.upload_image();
10025 }
10026 if ui
10031 .button("invert")
10032 .on_hover_text("Invert the current mask level")
10033 .clicked()
10034 {
10035 self.mask.invert();
10036 self.mask.commit();
10037 self.upload_image();
10038 }
10039 let mask_matches_image = self.mask.width == self.width
10044 && self.mask.height == self.height
10045 && !self.pixels.is_empty();
10046 if ui
10047 .add_enabled(mask_matches_image, egui::Button::new("mask non-finite"))
10048 .on_hover_text("Mask all NaN / infinite pixels at the current level")
10049 .clicked()
10050 {
10051 self.mask.mask_not_finite(&self.pixels);
10052 self.mask.commit();
10053 self.upload_image();
10054 }
10055 }
10056 });
10057 if in_mask_draw {
10064 use crate::widget::mask_tools::ThresholdMode;
10065 ui.horizontal(|ui| {
10066 ui.spacing_mut().item_spacing.x = 2.0;
10067 ui.label("threshold:");
10068 egui::ComboBox::from_id_salt("mask_threshold_mode")
10069 .selected_text(match self.mask.threshold_mode {
10070 ThresholdMode::Below => "below",
10071 ThresholdMode::Between => "between",
10072 ThresholdMode::Above => "above",
10073 })
10074 .show_ui(ui, |ui| {
10075 ui.selectable_value(
10076 &mut self.mask.threshold_mode,
10077 ThresholdMode::Below,
10078 "below",
10079 );
10080 ui.selectable_value(
10081 &mut self.mask.threshold_mode,
10082 ThresholdMode::Between,
10083 "between",
10084 );
10085 ui.selectable_value(
10086 &mut self.mask.threshold_mode,
10087 ThresholdMode::Above,
10088 "above",
10089 );
10090 });
10091 let mode = self.mask.threshold_mode;
10092 let show_min = matches!(mode, ThresholdMode::Below | ThresholdMode::Between);
10093 let show_max = matches!(mode, ThresholdMode::Between | ThresholdMode::Above);
10094 if show_min {
10095 ui.label("min");
10096 ui.add(egui::DragValue::new(&mut self.mask.threshold_min).speed(0.1));
10097 }
10098 if show_max {
10099 ui.label("max");
10100 ui.add(egui::DragValue::new(&mut self.mask.threshold_max).speed(0.1));
10101 }
10102 let apply_label = match mode {
10103 ThresholdMode::Below => "Mask below",
10104 ThresholdMode::Between => "Mask between",
10105 ThresholdMode::Above => "Mask above",
10106 };
10107 let mask_matches_image = self.mask.width == self.width
10108 && self.mask.height == self.height
10109 && !self.pixels.is_empty();
10110 let (min, max) = (self.mask.threshold_min, self.mask.threshold_max);
10111 if ui
10112 .add_enabled(mask_matches_image, egui::Button::new(apply_label))
10113 .on_hover_text("Mask pixels matching the threshold at the current level")
10114 .clicked()
10115 {
10116 self.mask.update_threshold(&self.pixels, mode, min, max);
10117 self.mask.commit();
10118 self.upload_image();
10119 }
10120 if ui
10126 .button("Min-max from colormap")
10127 .on_hover_text("Copy the colormap's value range into the threshold fields")
10128 .clicked()
10129 {
10130 self.mask.threshold_min = self.colormap.vmin as f32;
10131 self.mask.threshold_max = self.colormap.vmax as f32;
10132 }
10133 });
10134 }
10135 }
10136
10137 pub fn set_mask_draw(&mut self, on: bool) {
10144 if on {
10145 crate::widget::actions::mode::mask_draw_mode(&mut self.image_plot);
10146 if !matches!(
10147 self.mask.active_tool,
10148 crate::widget::mask_tools::MaskTool::Pencil
10149 | crate::widget::mask_tools::MaskTool::Eraser
10150 ) {
10151 self.mask.active_tool = crate::widget::mask_tools::MaskTool::Pencil;
10152 }
10153 } else {
10154 crate::widget::actions::mode::zoom_mode(&mut self.image_plot);
10155 self.mask.active_tool = crate::widget::mask_tools::MaskTool::None;
10156 }
10157 }
10158
10159 pub fn is_mask_draw(&self) -> bool {
10162 self.image_plot.interaction_mode() == PlotInteractionMode::MaskDraw
10163 }
10164
10165 pub fn mask(&self) -> &crate::widget::mask_tools::MaskToolsWidget {
10169 &self.mask
10170 }
10171
10172 pub fn mask_mut(&mut self) -> &mut crate::widget::mask_tools::MaskToolsWidget {
10177 &mut self.mask
10178 }
10179
10180 pub fn colormap(&self) -> &Colormap {
10182 &self.colormap
10183 }
10184
10185 pub fn colorbar(&self) -> crate::widget::colorbar::ColorBarWidget {
10190 image_view_colorbar(&self.colormap)
10191 }
10192
10193 pub fn show(&mut self, ui: &mut egui::Ui, histo_height: Option<f32>, histo_width: Option<f32>) {
10199 let show_histos = self.show_side_histograms;
10205 let histo_h_h = side_histogram_extent(show_histos, histo_height.unwrap_or(200.0));
10206 let histo_v_w = side_histogram_extent(show_histos, histo_width.unwrap_or(200.0));
10207
10208 self.sync_x
10210 .sync(&mut [self.image_plot.plot_mut(), self.histo_h.plot_mut()]);
10211 self.sync_y
10212 .sync(&mut [self.image_plot.plot_mut(), self.histo_v.plot_mut()]);
10213
10214 self.histo_v.plot_mut().reserve_title_gutter = self.image_plot.graph_title().is_some();
10219
10220 let avail = ui.available_size();
10221
10222 let colorbar_w = colorbar_column_width(self.show_colorbar, true);
10227 let colorbar_w = if self.interactive_colorbar && colorbar_w > 0.0 {
10228 INTERACTIVE_COLORBAR_WIDTH
10229 } else {
10230 colorbar_w
10231 };
10232 if self.interactive_colorbar && colorbar_w > 0.0 {
10233 self.ensure_value_histogram();
10234 }
10235
10236 let spacing = ui.spacing().item_spacing.x;
10243 let row_gaps = u32::from(show_histos) + u32::from(colorbar_w > 0.0);
10244 let img_w = row_content_width(avail.x, histo_v_w + colorbar_w, row_gaps, spacing);
10245
10246 if show_histos {
10250 ui.horizontal(|ui| {
10251 ui.allocate_ui(egui::vec2(img_w, histo_h_h), |ui| {
10255 self.histo_h.show(ui);
10256 });
10257
10258 let (xmin, xmax) = self.image_plot.x_limits();
10263 if let Some((ymin, ymax)) = self.image_plot.y_limits(YAxis::Left) {
10264 self.radar.set_viewport_limits(xmin, xmax, ymin, ymax);
10265 }
10266 let radar = self.radar.ui(
10267 ui,
10268 egui::vec2((avail.x - img_w - spacing).max(0.0), histo_h_h),
10269 );
10270 if let Some((rx0, rx1, ry0, ry1)) = radar.dragged_limits {
10271 self.image_plot.set_limits(rx0, rx1, ry0, ry1, None);
10272 }
10273 });
10274 }
10275
10276 let img_h = avail.y - histo_h_h;
10278 let mut dragged_levels: Option<(f64, f64)> = None;
10282 let response = ui.horizontal(|ui| {
10283 let response = ui
10284 .allocate_ui(egui::vec2(img_w, img_h), |ui| self.image_plot.show(ui))
10285 .inner;
10286 if show_histos {
10288 ui.allocate_ui(egui::vec2(histo_v_w, img_h), |ui| {
10289 self.histo_v.show(ui);
10290 });
10291 }
10292 if colorbar_w > 0.0 {
10294 if self.interactive_colorbar {
10295 let guides = response.transform.area;
10300 let bar = crate::widget::histogram_colorbar::HistogramColorBar::new(
10301 self.colormap.clone(),
10302 )
10303 .with_data_range(self.value_range)
10304 .with_histogram(self.value_histogram.clone())
10305 .with_levels(self.colormap.vmin, self.colormap.vmax)
10306 .with_bar_bounds(guides.top(), guides.bottom());
10307 dragged_levels = bar.ui(ui, egui::vec2(colorbar_w, img_h)).dragged_levels;
10308 } else {
10309 self.colorbar().ui(ui, egui::vec2(colorbar_w, img_h));
10310 }
10311 }
10312 response
10313 });
10314
10315 if let Some((vmin, vmax)) = dragged_levels {
10319 self.colormap.vmin = vmin;
10320 self.colormap.vmax = vmax;
10321 self.image_plot.set_default_colormap(self.colormap.clone());
10322 self.upload_image();
10323 }
10324
10325 let plot_response = response.inner;
10326
10327 if let Some(cursor) = cursor_from_pointer_event(plot_response.pointer_event.as_ref()) {
10330 self.cursor = Some(cursor);
10331 }
10332 self.position_info.ui(ui, self.cursor);
10333
10334 self.handle_mask_paint(&plot_response);
10338
10339 self.handle_mask_shape_draw(ui, &plot_response);
10342
10343 self.draw_brush_preview(ui, &plot_response);
10346
10347 self.handle_profile_drag(&plot_response);
10351 self.profile_window.show(ui.ctx());
10352 }
10353
10354 fn handle_mask_paint(&mut self, plot_response: &PlotResponse) {
10361 let mode = self.image_plot.interaction_mode();
10362 let mask_enabled =
10363 self.mask.width == self.width && self.mask.height == self.height && self.width != 0;
10364 if !image_view_should_paint_mask(mode, mask_enabled) {
10365 return;
10366 }
10367 let before = self.mask.mask.clone();
10368 self.mask.handle_interaction(plot_response);
10369 if self.mask.mask != before {
10370 self.upload_image();
10373 }
10374 }
10375
10376 fn handle_mask_shape_draw(&mut self, ui: &egui::Ui, plot_response: &PlotResponse) {
10386 let mode = self.image_plot.interaction_mode();
10387 let mask_enabled =
10388 self.mask.width == self.width && self.mask.height == self.height && self.width != 0;
10389 if !image_view_should_paint_mask(mode, mask_enabled)
10390 || self.mask.active_tool.draw_mode().is_none()
10391 {
10392 self.mask.cancel_shape_draw();
10395 return;
10396 }
10397 let before = self.mask.mask.clone();
10398 let event = self.mask.handle_shape_draw(plot_response);
10399 if self.mask.mask != before {
10400 self.upload_image();
10403 }
10404 self.mask
10408 .paint_shape_preview(ui, plot_response, event.as_ref());
10409 }
10410
10411 fn draw_brush_preview(&self, ui: &egui::Ui, plot_response: &PlotResponse) {
10421 let mode = self.image_plot.interaction_mode();
10422 let mask_enabled =
10423 self.mask.width == self.width && self.mask.height == self.height && self.width != 0;
10424 if !image_view_should_paint_mask(mode, mask_enabled) {
10425 return;
10426 }
10427 self.mask.paint_brush_preview(ui, plot_response);
10431 }
10432
10433 fn handle_profile_drag(&mut self, plot_response: &PlotResponse) {
10438 if self.profile_mode == ProfileMode::None || self.pixels.is_empty() {
10439 self.profile_drag_start = None;
10440 return;
10441 }
10442 let response = &plot_response.response;
10443 let transform = &plot_response.transform;
10444
10445 if response.drag_started()
10446 && let Some(p) = response.interact_pointer_pos()
10447 {
10448 self.profile_drag_start = Some(transform.pixel_to_data(p));
10449 }
10450
10451 if response.dragged()
10452 && let (Some(start), Some(p)) =
10453 (self.profile_drag_start, response.interact_pointer_pos())
10454 {
10455 let end = transform.pixel_to_data(p);
10456 if let Some(roi) = profile_roi_from_drag(self.profile_mode, start, end) {
10457 self.profile_window
10460 .update_profile(self.width, self.height, &self.pixels, &roi);
10461 self.profile_window.set_open(true);
10462 }
10463 }
10464
10465 if response.drag_stopped() {
10466 self.profile_drag_start = None;
10467 }
10468 }
10469
10470 pub fn profile_mode(&self) -> ProfileMode {
10473 self.profile_mode
10474 }
10475
10476 pub fn set_profile_mode(&mut self, mode: ProfileMode) {
10479 self.profile_mode = mode;
10480 if mode == ProfileMode::None {
10481 self.profile_drag_start = None;
10482 self.profile_window.set_open(false);
10483 }
10484 }
10485
10486 pub fn profile_values(
10490 &self,
10491 mode: ProfileMode,
10492 start: (f64, f64),
10493 end: (f64, f64),
10494 ) -> Option<(Vec<f64>, Vec<f64>)> {
10495 image_view_profile_values(mode, self.width, self.height, &self.pixels, start, end)
10496 }
10497
10498 pub fn radar(&self) -> &crate::widget::radar_view::RadarView {
10501 &self.radar
10502 }
10503
10504 pub fn cursor(&self) -> Option<[f64; 2]> {
10507 self.cursor
10508 }
10509
10510 pub fn position_info(&self) -> &crate::widget::position_info::PositionInfo {
10513 &self.position_info
10514 }
10515
10516 pub fn position_info_mut(&mut self) -> &mut crate::widget::position_info::PositionInfo {
10519 &mut self.position_info
10520 }
10521
10522 pub fn position_info_values(&self) -> Vec<String> {
10526 self.position_info.values(self.cursor)
10527 }
10528
10529 pub fn image_plot(&self) -> &Plot2D {
10531 &self.image_plot
10532 }
10533
10534 pub fn image_plot_mut(&mut self) -> &mut Plot2D {
10536 &mut self.image_plot
10537 }
10538
10539 pub fn histogram(&self, axis: ImageHistogramAxis) -> Option<ImageProfileHistogram> {
10546 if self.width == 0 || self.height == 0 || self.pixels.is_empty() {
10547 return None;
10548 }
10549 let w = self.width as usize;
10550 let h = self.height as usize;
10551 let (data, extent) = match axis {
10552 ImageHistogramAxis::X => (image_column_sums(&self.pixels, w, h), (0.0, w as f64)),
10553 ImageHistogramAxis::Y => (image_row_sums(&self.pixels, w, h), (0.0, h as f64)),
10554 };
10555 Some(ImageProfileHistogram { data, extent })
10556 }
10557
10558 pub fn value_changed(&self) -> Option<(f64, f64, f64)> {
10565 let [x, y] = self.cursor?;
10566 image_value_at(
10567 x,
10568 y,
10569 &self.pixels,
10570 self.width as usize,
10571 self.height as usize,
10572 )
10573 }
10574
10575 fn rebuild_histograms(&mut self) {
10576 if self.width == 0 || self.pixels.is_empty() {
10577 return;
10578 }
10579 let w = self.width as usize;
10580 let h = self.height as usize;
10581
10582 let col_sums = image_column_sums(&self.pixels, w, h);
10587 let row_sums = image_row_sums(&self.pixels, w, h);
10588 let col_x: Vec<f64> = (0..w).map(|i| i as f64).collect();
10589 let row_y: Vec<f64> = (0..h).map(|i| i as f64).collect();
10590
10591 if let Some(h) = self.histo_h_curve {
10592 self.histo_h
10593 .update_curve_data(h, &CurveData::new(col_x, col_sums, Color32::YELLOW));
10594 } else {
10595 let h =
10596 self.histo_h
10597 .add_curve_with_legend(&col_x, &col_sums, Color32::YELLOW, "col sums");
10598 self.histo_h_curve = Some(h);
10599 }
10600
10601 if let Some(h) = self.histo_v_curve {
10602 self.histo_v
10603 .update_curve_data(h, &CurveData::new(row_sums, row_y, Color32::LIGHT_BLUE));
10604 } else {
10605 let h = self.histo_v.add_curve_with_legend(
10606 &row_sums,
10607 &row_y,
10608 Color32::LIGHT_BLUE,
10609 "row sums",
10610 );
10611 self.histo_v_curve = Some(h);
10612 }
10613 }
10614}
10615
10616#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10628pub enum ScatterVisualization {
10629 #[default]
10631 Points,
10632 Solid,
10641 IrregularGrid,
10648 RegularGrid,
10652 BinnedStatistic,
10655}
10656
10657impl ScatterVisualization {
10658 pub const ALL: [ScatterVisualization; 5] = [
10661 ScatterVisualization::Points,
10662 ScatterVisualization::Solid,
10663 ScatterVisualization::RegularGrid,
10664 ScatterVisualization::IrregularGrid,
10665 ScatterVisualization::BinnedStatistic,
10666 ];
10667
10668 pub fn label(self) -> &'static str {
10670 match self {
10671 ScatterVisualization::Points => "Points",
10672 ScatterVisualization::Solid => "Solid",
10673 ScatterVisualization::RegularGrid => "Regular Grid",
10674 ScatterVisualization::IrregularGrid => "Irregular Grid",
10675 ScatterVisualization::BinnedStatistic => "Binned Statistic",
10676 }
10677 }
10678}
10679
10680fn regular_grid_image(x: &[f64], y: &[f64], values: &[f64]) -> Option<GridImage> {
10696 let grid = crate::core::scatter_viz::detect_regular_grid(x, y)?;
10697 let (mut rows, mut cols) = grid.shape;
10698
10699 let n = values.len();
10702 if n > rows * cols {
10703 match grid.order {
10704 crate::core::scatter_viz::GridMajorOrder::Row => {
10705 rows = n.div_ceil(cols.max(1));
10706 }
10707 crate::core::scatter_viz::GridMajorOrder::Column => {
10708 cols = n.div_ceil(rows.max(1));
10709 }
10710 }
10711 }
10712 if rows == 0 || cols == 0 {
10713 return None;
10714 }
10715
10716 let (xb, xe) = grid_axis_bounds(x);
10719 let (yb, ye) = grid_axis_bounds(y);
10720 let mut sx = if cols > 1 {
10721 (xe - xb) / (cols - 1) as f64
10722 } else {
10723 0.0
10724 };
10725 let mut sy = if rows > 1 {
10726 (ye - yb) / (rows - 1) as f64
10727 } else {
10728 0.0
10729 };
10730 match (sx == 0.0, sy == 0.0) {
10732 (true, true) => {
10733 sx = 1.0;
10734 sy = 1.0;
10735 }
10736 (true, false) => sx = sy,
10737 (false, true) => sy = sx,
10738 (false, false) => {}
10739 }
10740 let origin = (xb - 0.5 * sx, yb - 0.5 * sy);
10741
10742 let mut data = vec![f64::NAN; rows * cols];
10746 match grid.order {
10747 crate::core::scatter_viz::GridMajorOrder::Row => {
10748 for (i, &v) in values.iter().enumerate().take(rows * cols) {
10749 data[i] = v;
10750 }
10751 }
10752 crate::core::scatter_viz::GridMajorOrder::Column => {
10753 for (i, &v) in values.iter().enumerate().take(rows * cols) {
10755 let r = i % rows;
10756 let c = i / rows;
10757 data[r * cols + c] = v;
10758 }
10759 }
10760 }
10761
10762 Some(GridImage {
10763 data,
10764 shape: (rows, cols),
10765 origin,
10766 scale: (sx, sy),
10767 })
10768}
10769
10770fn grid_axis_bounds(coord: &[f64]) -> (f64, f64) {
10774 let mut min = f64::INFINITY;
10775 let mut max = f64::NEG_INFINITY;
10776 for &v in coord {
10777 if v.is_finite() {
10778 min = min.min(v);
10779 max = max.max(v);
10780 }
10781 }
10782 if min > max {
10783 return (0.0, 0.0);
10784 }
10785 let first = coord.first().copied().unwrap_or(min);
10786 if (first - min) <= (max - first) {
10787 (min, max)
10788 } else {
10789 (max, min)
10790 }
10791}
10792
10793fn binned_statistic_image(bs: &crate::core::scatter_viz::BinnedStatistic) -> GridImage {
10797 GridImage {
10798 data: bs.select(crate::core::scatter_viz::BinnedStatisticFunction::Mean),
10799 shape: bs.shape,
10800 origin: bs.origin,
10801 scale: bs.scale,
10802 }
10803}
10804
10805fn scatter_grid_image(
10820 mode: ScatterVisualization,
10821 x: &[f64],
10822 y: &[f64],
10823 values: &[f64],
10824 resolution: (usize, usize),
10825) -> Option<GridImage> {
10826 let (rows, cols) = resolution;
10827 match mode {
10828 ScatterVisualization::Points
10832 | ScatterVisualization::Solid
10833 | ScatterVisualization::IrregularGrid => None,
10834 ScatterVisualization::RegularGrid => regular_grid_image(x, y, values),
10835 ScatterVisualization::BinnedStatistic => {
10836 crate::core::scatter_viz::binned_statistic(x, y, values, rows, cols)
10837 .as_ref()
10838 .map(binned_statistic_image)
10839 }
10840 }
10841}
10842
10843#[derive(Debug, Clone, Copy, PartialEq)]
10847pub struct ScatterPick {
10848 pub index: usize,
10850 pub x: f64,
10852 pub y: f64,
10854 pub value: f64,
10856}
10857
10858const SCATTER_PICK_RADIUS_PX: f32 = crate::core::marker::DEFAULT_MARKER_SIZE;
10861
10862pub fn scatter_pick_pixels(
10869 cursor: (f32, f32),
10870 points: &[(f32, f32)],
10871 radius: f32,
10872) -> Option<usize> {
10873 let r2 = radius * radius;
10874 let mut best: Option<(usize, f32)> = None;
10875 for (i, &(px, py)) in points.iter().enumerate() {
10876 let d2 = (px - cursor.0).powi(2) + (py - cursor.1).powi(2);
10877 if d2 > r2 {
10878 continue;
10879 }
10880 let take = match best {
10882 None => true,
10883 Some((_, best_d2)) => d2 <= best_d2,
10884 };
10885 if take {
10886 best = Some((i, d2));
10887 }
10888 }
10889 best.map(|(i, _)| i)
10890}
10891
10892fn nearest_candidate_in_data(
10899 candidates: &[usize],
10900 xs: &[f64],
10901 ys: &[f64],
10902 cx: f64,
10903 cy: f64,
10904) -> Option<usize> {
10905 let mut best: Option<(usize, f64)> = None;
10906 for &i in candidates {
10907 let d2 = (xs[i] - cx).powi(2) + (ys[i] - cy).powi(2);
10908 let take = match best {
10911 None => true,
10912 Some((_, best_d2)) => d2 <= best_d2,
10913 };
10914 if take {
10915 best = Some((i, d2));
10916 }
10917 }
10918 best.map(|(i, _)| i)
10919}
10920
10921pub fn scatter_position_info(
10927 pick: Option<ScatterPick>,
10928) -> crate::widget::position_info::PositionInfo {
10929 use crate::widget::position_info::{Converter, PositionInfo, format_value};
10930 let columns: Vec<(String, Converter)> = vec![
10931 (
10932 "X".to_owned(),
10933 Box::new(move |x, _| match pick {
10934 Some(p) => format_value(p.x),
10935 None => format_value(x),
10936 }),
10937 ),
10938 (
10939 "Y".to_owned(),
10940 Box::new(move |_, y| match pick {
10941 Some(p) => format_value(p.y),
10942 None => format_value(y),
10943 }),
10944 ),
10945 (
10946 "Data".to_owned(),
10947 Box::new(move |_, _| match pick {
10948 Some(p) => format_value(p.value),
10949 None => "-".to_owned(),
10950 }),
10951 ),
10952 (
10953 "Index".to_owned(),
10954 Box::new(move |_, _| match pick {
10955 Some(p) => p.index.to_string(),
10956 None => "-".to_owned(),
10957 }),
10958 ),
10959 ];
10960 PositionInfo::new(columns)
10961}
10962
10963const SCATTER_PROFILE_NPOINTS: usize = 1024;
10976
10977pub struct ScatterView {
10978 inner: PlotWidget,
10979 scatter_handle: Option<ItemHandle>,
10980 grid_handle: Option<ItemHandle>,
10985 triangles_handle: Option<ItemHandle>,
10990 irregular_grid_mesh: Option<Triangles>,
10995 colormap: Option<Colormap>,
11000 points: Option<(Vec<f64>, Vec<f64>, Vec<f64>)>,
11004 visualization: ScatterVisualization,
11006 grid_resolution: (usize, usize),
11010 mask: crate::widget::scatter_mask::ScatterMaskWidget,
11014 show_colorbar: bool,
11018 alpha: Option<Vec<f64>>,
11023 cursor: Option<[f64; 2]>,
11027 profile_window: crate::widget::profile_window::ProfileWindow,
11031 profile_mode: bool,
11036 profile_drag_start: Option<(f64, f64)>,
11039}
11040
11041impl ScatterView {
11042 pub fn new(render_state: &RenderState, id: PlotId) -> Self {
11048 let mut inner = PlotWidget::new(render_state, id);
11049 inner.set_graph_cursor(true);
11050 Self {
11051 inner,
11052 scatter_handle: None,
11053 grid_handle: None,
11054 triangles_handle: None,
11055 irregular_grid_mesh: None,
11056 colormap: None,
11057 points: None,
11058 visualization: ScatterVisualization::Points,
11059 grid_resolution: (100, 100),
11060 mask: crate::widget::scatter_mask::ScatterMaskWidget::new(0),
11061 show_colorbar: true,
11062 alpha: None,
11063 cursor: None,
11064 profile_window: crate::widget::profile_window::ProfileWindow::new(render_state, id + 1),
11065 profile_mode: false,
11066 profile_drag_start: None,
11067 }
11068 }
11069
11070 pub fn show_colorbar(&self) -> bool {
11072 self.show_colorbar
11073 }
11074
11075 pub fn set_show_colorbar(&mut self, show: bool) {
11079 self.show_colorbar = show;
11080 }
11081
11082 pub fn alpha(&self) -> Option<&[f64]> {
11085 self.alpha.as_deref()
11086 }
11087
11088 #[must_use]
11097 pub fn with_alpha(mut self, alpha: Vec<f64>) -> Self {
11098 self.alpha = Some(clamp_alpha(alpha));
11099 self
11100 }
11101
11102 pub fn set_alpha(&mut self, alpha: Vec<f64>) {
11106 self.alpha = Some(clamp_alpha(alpha));
11107 self.rebuild_visualization();
11108 }
11109
11110 pub fn clear_alpha(&mut self) {
11113 self.alpha = None;
11114 self.rebuild_visualization();
11115 }
11116
11117 pub fn set_data(
11121 &mut self,
11122 x: &[f64],
11123 y: &[f64],
11124 values: &[f64],
11125 colormap: Colormap,
11126 ) -> Result<(), PlotDataError> {
11127 if x.len() != y.len() || x.len() != values.len() {
11128 return Err(PlotDataError::ImageDataLength {
11129 expected: x.len(),
11130 actual: if y.len() != x.len() {
11131 y.len()
11132 } else {
11133 values.len()
11134 },
11135 });
11136 }
11137
11138 if self.mask.len() != x.len() {
11142 self.mask.reset_len(x.len());
11143 }
11144 self.points = Some((x.to_vec(), y.to_vec(), values.to_vec()));
11145 self.colormap = Some(colormap);
11146 self.rebuild_visualization();
11147 Ok(())
11148 }
11149
11150 pub fn visualization(&self) -> ScatterVisualization {
11152 self.visualization
11153 }
11154
11155 pub fn line_profile(
11166 &self,
11167 start: (f64, f64),
11168 end: (f64, f64),
11169 n_points: usize,
11170 ) -> Option<ScatterLineProfile> {
11171 let (x, y, values) = self.points.as_ref()?;
11172 let profile =
11173 crate::core::scatter_viz::scatter_line_profile(x, y, values, start, end, n_points);
11174 if profile.values.iter().all(Option::is_none) {
11175 return None;
11176 }
11177 Some(profile)
11178 }
11179
11180 pub fn show_line_profile(
11188 &mut self,
11189 start: (f64, f64),
11190 end: (f64, f64),
11191 n_points: usize,
11192 ) -> bool {
11193 let Some(profile) = self.line_profile(start, end, n_points) else {
11194 return false;
11195 };
11196 let (distance, value) = profile.distance_value_curve();
11197 self.profile_window.set_profile_curve(
11199 "Profile",
11200 Color32::from_rgb(31, 119, 180),
11201 distance,
11202 value,
11203 );
11204 self.profile_window.set_open(true);
11205 true
11206 }
11207
11208 pub fn profile_window(&self) -> &crate::widget::profile_window::ProfileWindow {
11211 &self.profile_window
11212 }
11213
11214 pub fn profile_window_mut(&mut self) -> &mut crate::widget::profile_window::ProfileWindow {
11217 &mut self.profile_window
11218 }
11219
11220 pub fn profile_mode(&self) -> bool {
11223 self.profile_mode
11224 }
11225
11226 pub fn set_profile_mode(&mut self, enabled: bool) {
11237 if self.profile_mode == enabled {
11238 return;
11239 }
11240 self.profile_mode = enabled;
11241 if enabled {
11242 self.inner.set_interaction_mode(PlotInteractionMode::Select);
11243 } else {
11244 self.inner.set_interaction_mode(PlotInteractionMode::Zoom);
11245 self.profile_drag_start = None;
11246 self.profile_window.set_open(false);
11247 }
11248 }
11249
11250 fn handle_profile_drag(&mut self, plot_response: &PlotResponse) {
11256 if !self.profile_mode || self.points.is_none() {
11257 self.profile_drag_start = None;
11258 return;
11259 }
11260 let response = &plot_response.response;
11261 let transform = &plot_response.transform;
11262
11263 if response.drag_started()
11264 && let Some(p) = response.interact_pointer_pos()
11265 {
11266 self.profile_drag_start = Some(transform.pixel_to_data(p));
11267 }
11268
11269 if response.dragged()
11270 && let (Some(start), Some(p)) =
11271 (self.profile_drag_start, response.interact_pointer_pos())
11272 {
11273 let end = transform.pixel_to_data(p);
11274 self.show_line_profile(start, end, SCATTER_PROFILE_NPOINTS);
11275 }
11276
11277 if response.drag_stopped() {
11278 self.profile_drag_start = None;
11279 }
11280 }
11281
11282 pub fn set_visualization(&mut self, mode: ScatterVisualization) {
11292 if self.visualization == mode {
11293 return;
11294 }
11295 self.visualization = mode;
11296 self.rebuild_visualization();
11297 }
11298
11299 pub fn grid_resolution(&self) -> (usize, usize) {
11302 self.grid_resolution
11303 }
11304
11305 pub fn set_grid_resolution(&mut self, rows: usize, cols: usize) {
11311 self.grid_resolution = (rows, cols);
11312 self.rebuild_visualization();
11313 }
11314
11315 pub fn grid_image(&self) -> Option<GridImage> {
11325 let (x, y, values) = self.points.as_ref()?;
11326 scatter_grid_image(self.visualization, x, y, values, self.grid_resolution)
11327 }
11328
11329 fn rebuild_visualization(&mut self) {
11341 let Some((x, y, values)) = self.points.clone() else {
11342 return;
11343 };
11344 let Some(colormap) = self.colormap.clone() else {
11345 return;
11346 };
11347
11348 self.irregular_grid_mesh = None;
11351
11352 match self.visualization {
11353 ScatterVisualization::Points => {
11354 if let Some(h) = self.grid_handle.take() {
11357 self.inner.remove(h);
11358 }
11359 if let Some(h) = self.triangles_handle.take() {
11360 self.inner.remove(h);
11361 }
11362 let colors = point_colors(&values, &colormap, self.alpha.as_deref());
11368
11369 let mut spec = CurveSpec::new(&x, &y, Color32::WHITE);
11370 spec.color = crate::core::backend::CurveColor::PerVertex(&colors);
11371 spec.line_style = LineStyle::None;
11372 spec.symbol = Some(crate::core::items::Symbol::Circle);
11373 spec.symbol_size = 6.0;
11374
11375 if let Some(h) = self.scatter_handle {
11376 self.inner.update_curve_spec(h, spec);
11377 } else {
11378 let h = self.inner.add_curve_spec(spec);
11379 self.scatter_handle = Some(h);
11380 self.inner.set_item_legend(h, "scatter");
11381 }
11382 }
11383 ScatterVisualization::Solid => {
11384 if let Some(h) = self.scatter_handle.take() {
11388 self.inner.remove(h);
11389 }
11390 if let Some(h) = self.grid_handle.take() {
11391 self.inner.remove(h);
11392 }
11393 let colors = point_colors(&values, &colormap, self.alpha.as_deref());
11398
11399 let Some(tri) = crate::core::scatter_viz::solid_triangles(&x, &y, &colors) else {
11404 if let Some(h) = self.triangles_handle.take() {
11405 self.inner.remove(h);
11406 }
11407 return;
11408 };
11409
11410 if let Some(h) = self.triangles_handle.take() {
11414 self.inner.remove(h);
11415 }
11416 let h = self.inner.add_triangles_data(&tri);
11417 self.triangles_handle = Some(h);
11418 self.inner.set_item_legend(h, "scatter solid");
11419 }
11420 ScatterVisualization::IrregularGrid => {
11421 if let Some(h) = self.scatter_handle.take() {
11425 self.inner.remove(h);
11426 }
11427 if let Some(h) = self.grid_handle.take() {
11428 self.inner.remove(h);
11429 }
11430 let colors = point_colors(&values, &colormap, self.alpha.as_deref());
11434
11435 let Some(tri) = crate::core::scatter_viz::irregular_grid_triangles(&x, &y, &colors)
11440 else {
11441 if let Some(h) = self.triangles_handle.take() {
11442 self.inner.remove(h);
11443 }
11444 return;
11445 };
11446
11447 if let Some(h) = self.triangles_handle.take() {
11452 self.inner.remove(h);
11453 }
11454 let h = self.inner.add_triangles_data(&tri);
11455 self.triangles_handle = Some(h);
11456 self.inner.set_item_legend(h, "scatter irregular grid");
11457 self.irregular_grid_mesh = Some(tri);
11458 }
11459 mode => {
11460 if let Some(h) = self.scatter_handle.take() {
11464 self.inner.remove(h);
11465 }
11466 if let Some(h) = self.triangles_handle.take() {
11467 self.inner.remove(h);
11468 }
11469 let Some(grid) = scatter_grid_image(mode, &x, &y, &values, self.grid_resolution)
11470 else {
11471 if let Some(h) = self.grid_handle.take() {
11474 self.inner.remove(h);
11475 }
11476 return;
11477 };
11478 let pixels: Vec<f32> = grid.data.iter().map(|&v| v as f32).collect();
11479 let geometry = ImageGeometry {
11480 origin: grid.origin,
11481 scale: grid.scale,
11482 alpha: 1.0,
11483 };
11484 let mut spec =
11485 ImageSpec::scalar(grid.shape.1 as u32, grid.shape.0 as u32, &pixels, colormap);
11486 spec.origin = geometry.origin;
11487 spec.scale = geometry.scale;
11488 spec.alpha = geometry.alpha;
11489
11490 if let Some(h) = self.grid_handle {
11491 self.inner.update_image_spec(h, spec);
11492 } else {
11493 let h = self.inner.add_image_spec(spec);
11494 self.grid_handle = Some(h);
11495 self.inner.set_item_legend(h, "scatter grid");
11496 }
11497 }
11498 }
11499 }
11500
11501 pub fn colormap(&self) -> Option<&Colormap> {
11505 self.colormap.as_ref()
11506 }
11507
11508 pub fn colorbar(&self) -> Option<crate::widget::colorbar::ColorBarWidget> {
11516 scatter_view_colorbar(self.colormap.as_ref())
11517 }
11518
11519 pub fn scatter_mask(&self) -> &crate::widget::scatter_mask::ScatterMaskWidget {
11528 &self.mask
11529 }
11530
11531 pub fn scatter_mask_mut(&mut self) -> &mut crate::widget::scatter_mask::ScatterMaskWidget {
11534 &mut self.mask
11535 }
11536
11537 pub fn masked_selection(&self) -> Vec<bool> {
11540 scatter_masked_selection(&self.mask.mask)
11541 }
11542
11543 pub fn selection_mask(&self) -> &[u8] {
11549 &self.mask.mask
11550 }
11551
11552 pub fn set_selection_mask(&mut self, mask: &[u8]) -> Result<usize, PlotDataError> {
11562 if mask.len() != self.mask.mask.len() {
11563 return Err(PlotDataError::ImageDataLength {
11564 expected: self.mask.mask.len(),
11565 actual: mask.len(),
11566 });
11567 }
11568 self.mask.mask.copy_from_slice(mask);
11569 self.mask.commit();
11570 Ok(mask.len())
11571 }
11572
11573 pub fn cursor(&self) -> Option<[f64; 2]> {
11577 self.cursor
11578 }
11579
11580 pub fn show_position_info(&mut self, ui: &mut egui::Ui, response: &PlotResponse) {
11592 if let Some(cursor) = cursor_from_pointer_event(response.pointer_event.as_ref()) {
11593 self.cursor = Some(cursor);
11594 }
11595 let pick = self.cursor.and_then(|[cx, cy]| {
11596 use crate::core::scatter_viz;
11597 let (xs, ys, vs) = self.points.as_ref()?;
11598 let i = match self.visualization {
11602 ScatterVisualization::RegularGrid => {
11606 let image = regular_grid_image(xs, ys, vs)?;
11607 let order = scatter_viz::detect_regular_grid(xs, ys)?.order;
11608 scatter_viz::regular_grid_pick(&image, order, xs.len(), cx, cy)?
11609 }
11610 ScatterVisualization::BinnedStatistic => {
11614 let (rows, cols) = self.grid_resolution;
11615 let bs = scatter_viz::binned_statistic(xs, ys, vs, rows, cols)?;
11616 let candidates = bs.pick(xs, ys, cx, cy)?;
11617 nearest_candidate_in_data(&candidates, xs, ys, cx, cy)?
11618 }
11619 ScatterVisualization::IrregularGrid => {
11624 let mesh = self.irregular_grid_mesh.as_ref()?;
11625 scatter_viz::irregular_grid_pick(mesh, cx, cy)?
11626 }
11627 ScatterVisualization::Points | ScatterVisualization::Solid => {
11630 let cursor_px = response.transform.data_to_pixel(cx, cy);
11631 let points_px: Vec<(f32, f32)> = xs
11632 .iter()
11633 .zip(ys)
11634 .map(|(&x, &y)| {
11635 let p = response.transform.data_to_pixel(x, y);
11636 (p.x, p.y)
11637 })
11638 .collect();
11639 scatter_pick_pixels(
11640 (cursor_px.x, cursor_px.y),
11641 &points_px,
11642 SCATTER_PICK_RADIUS_PX,
11643 )?
11644 }
11645 };
11646 Some(ScatterPick {
11647 index: i,
11648 x: xs[i],
11649 y: ys[i],
11650 value: vs[i],
11651 })
11652 });
11653 scatter_position_info(pick).ui(ui, self.cursor);
11654 }
11655
11656 pub fn mask_rectangle(&mut self, anchor: (f64, f64), size: (f64, f64), mask: bool) {
11662 let (px, py) = self.mask_point_coords();
11663 let level = self.mask.level;
11664 self.mask.update_rectangle(
11665 level,
11666 (anchor.1 as f32, anchor.0 as f32),
11667 (size.1 as f32, size.0 as f32),
11668 &px,
11669 &py,
11670 mask,
11671 );
11672 self.mask.commit();
11673 }
11674
11675 pub fn mask_polygon(&mut self, vertices: &[(f64, f64)], mask: bool) {
11680 let (px, py) = self.mask_point_coords();
11681 let verts: Vec<(f32, f32)> = vertices
11683 .iter()
11684 .map(|&(x, y)| (y as f32, x as f32))
11685 .collect();
11686 let level = self.mask.level;
11687 self.mask.update_polygon(level, &verts, &px, &py, mask);
11688 self.mask.commit();
11689 }
11690
11691 fn mask_point_coords(&self) -> (Vec<f32>, Vec<f32>) {
11694 match &self.points {
11695 Some((x, y, _)) => (
11696 x.iter().map(|&v| v as f32).collect(),
11697 y.iter().map(|&v| v as f32).collect(),
11698 ),
11699 None => (Vec::new(), Vec::new()),
11700 }
11701 }
11702
11703 fn mask_values(&self) -> Vec<f32> {
11706 match &self.points {
11707 Some((_, _, v)) => v.iter().map(|&val| val as f32).collect(),
11708 None => Vec::new(),
11709 }
11710 }
11711
11712 pub fn show_mask_tools(&mut self, ui: &mut egui::Ui) -> bool {
11723 let before = self.mask.mask.clone();
11724 ui.horizontal(|ui| {
11725 ui.label("Mask level:");
11726 let mut level = self.mask.level;
11727 if ui
11728 .add(egui::DragValue::new(&mut level).range(1..=255))
11729 .changed()
11730 {
11731 self.mask.level = level;
11732 }
11733 });
11734 ui.horizontal(|ui| {
11735 if ui.button("Clear level").clicked() {
11736 self.mask.clear();
11737 self.mask.commit();
11738 }
11739 if ui.button("Clear all").clicked() {
11740 self.mask.clear_all();
11741 self.mask.commit();
11742 }
11743 if ui.button("Invert").clicked() {
11744 self.mask.invert();
11745 self.mask.commit();
11746 }
11747 });
11748 ui.horizontal(|ui| {
11749 if ui
11750 .add_enabled(self.mask.can_undo(), egui::Button::new("Undo"))
11751 .clicked()
11752 {
11753 self.mask.undo();
11754 }
11755 if ui
11756 .add_enabled(self.mask.can_redo(), egui::Button::new("Redo"))
11757 .clicked()
11758 {
11759 self.mask.redo();
11760 }
11761 if ui.button("Mask non-finite").clicked() {
11762 let values = self.mask_values();
11763 self.mask.mask_not_finite(&values);
11764 self.mask.commit();
11765 }
11766 });
11767
11768 let changed = self.mask.mask != before;
11769 ui.label(format!(
11770 "{} / {} points masked",
11771 self.masked_selection().iter().filter(|&&m| m).count(),
11772 self.mask.len()
11773 ));
11774 changed
11775 }
11776
11777 pub fn show_toolbar(&mut self, ui: &mut egui::Ui) -> ToolbarResponse {
11782 let show_colorbar = self.show_colorbar;
11783 let current_viz = self.visualization;
11784 let mut toggle = false;
11785 let mut picked_viz = current_viz;
11786 let (out, ()) = self.inner.show_toolbar_with(ui, |ui, _| {
11787 ui.separator();
11788 if ui
11789 .selectable_label(show_colorbar, "colorbar")
11790 .on_hover_text("Show/hide the colorbar")
11791 .clicked()
11792 {
11793 toggle = true;
11794 }
11795 ui.separator();
11799 egui::ComboBox::from_id_salt("scatter_visualization")
11800 .selected_text(current_viz.label())
11801 .show_ui(ui, |ui| {
11802 for mode in ScatterVisualization::ALL {
11803 ui.selectable_value(&mut picked_viz, mode, mode.label());
11804 }
11805 });
11806 });
11807 if toggle {
11808 crate::widget::actions::control::scatter_colorbar_toggle(self);
11809 }
11810 self.set_visualization(picked_viz);
11813 out
11814 }
11815
11816 pub fn show(&mut self, ui: &mut egui::Ui) -> PlotResponse {
11822 let avail = ui.available_size();
11823 let colorbar = self.colorbar();
11824 let colorbar_w = colorbar_column_width(self.show_colorbar, colorbar.is_some());
11825 let spacing = ui.spacing().item_spacing.x;
11829 let plot_w = row_content_width(avail.x, colorbar_w, u32::from(colorbar_w > 0.0), spacing);
11830 let response = ui
11831 .horizontal(|ui| {
11832 let response = ui
11833 .allocate_ui(egui::vec2(plot_w, avail.y), |ui| self.inner.show(ui))
11834 .inner;
11835 if colorbar_w > 0.0
11836 && let Some(bar) = colorbar
11837 {
11838 bar.ui(ui, egui::vec2(colorbar_w, avail.y));
11839 }
11840 response
11841 })
11842 .inner;
11843 self.handle_profile_drag(&response);
11846 self.profile_window.show(ui.ctx());
11849 response
11850 }
11851}
11852
11853impl Deref for ScatterView {
11854 type Target = PlotWidget;
11855
11856 fn deref(&self) -> &Self::Target {
11857 &self.inner
11858 }
11859}
11860
11861impl DerefMut for ScatterView {
11862 fn deref_mut(&mut self) -> &mut Self::Target {
11863 &mut self.inner
11864 }
11865}
11866
11867#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11882pub enum StackPerspective {
11883 #[default]
11885 Axis0,
11886 Axis1,
11888 Axis2,
11890}
11891
11892impl StackPerspective {
11893 pub fn axis(self) -> usize {
11895 match self {
11896 StackPerspective::Axis0 => 0,
11897 StackPerspective::Axis1 => 1,
11898 StackPerspective::Axis2 => 2,
11899 }
11900 }
11901
11902 pub fn display_axes(self) -> (usize, usize) {
11906 match self {
11907 StackPerspective::Axis0 => (1, 2),
11908 StackPerspective::Axis1 => (0, 2),
11909 StackPerspective::Axis2 => (0, 1),
11910 }
11911 }
11912}
11913
11914pub fn stack_frame_count(shape: [usize; 3], perspective: StackPerspective) -> usize {
11917 shape[perspective.axis()]
11918}
11919
11920pub fn stack_frame(
11930 data: &[f32],
11931 shape: [usize; 3],
11932 perspective: StackPerspective,
11933 index: usize,
11934) -> Option<(u32, u32, Vec<f32>)> {
11935 let [d0, d1, d2] = shape;
11936 if data.len() != d0.checked_mul(d1)?.checked_mul(d2)? {
11937 return None;
11938 }
11939 if index >= shape[perspective.axis()] {
11940 return None;
11941 }
11942 let (height_axis, width_axis) = perspective.display_axes();
11943 let height = shape[height_axis];
11944 let width = shape[width_axis];
11945 let at = |i: usize, j: usize, k: usize| data[(i * d1 + j) * d2 + k];
11946 let mut pixels = Vec::with_capacity(width.saturating_mul(height));
11947 for row in 0..height {
11948 for col in 0..width {
11949 let value = match perspective {
11952 StackPerspective::Axis0 => at(index, row, col),
11953 StackPerspective::Axis1 => at(row, index, col),
11954 StackPerspective::Axis2 => at(row, col, index),
11955 };
11956 pixels.push(value);
11957 }
11958 }
11959 Some((width as u32, height as u32, pixels))
11960}
11961
11962#[derive(Debug, Clone, PartialEq)]
11969pub struct StackProfile {
11970 pub frame_count: usize,
11972 pub profile_len: usize,
11974 pub values: Vec<f64>,
11976}
11977
11978#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11982pub enum StackProfileDimension {
11983 #[default]
11987 OneD,
11988 TwoD,
11992}
11993
11994fn stack_profile_with<F>(
12002 data: &[f32],
12003 shape: [usize; 3],
12004 perspective: StackPerspective,
12005 mut extract: F,
12006) -> Option<StackProfile>
12007where
12008 F: FnMut(u32, u32, &[f32]) -> Option<Vec<f64>>,
12009{
12010 let frame_count = stack_frame_count(shape, perspective);
12011 if frame_count == 0 {
12012 return None;
12013 }
12014 let mut values: Vec<f64> = Vec::new();
12015 let mut profile_len: Option<usize> = None;
12016 for index in 0..frame_count {
12017 let (w, h, pixels) = stack_frame(data, shape, perspective, index)?;
12018 let profile = extract(w, h, &pixels)?;
12019 match profile_len {
12020 None => profile_len = Some(profile.len()),
12021 Some(len) if len != profile.len() => return None,
12022 _ => {}
12023 }
12024 values.extend(profile);
12025 }
12026 Some(StackProfile {
12027 frame_count,
12028 profile_len: profile_len.unwrap_or(0),
12029 values,
12030 })
12031}
12032
12033pub fn stack_aligned_profile(
12040 data: &[f32],
12041 shape: [usize; 3],
12042 perspective: StackPerspective,
12043 position: f64,
12044 roi_width: u32,
12045 horizontal: bool,
12046 method: ProfileMethod,
12047) -> Option<StackProfile> {
12048 stack_profile_with(data, shape, perspective, |w, h, pixels| {
12049 aligned_profile_values(w, h, pixels, position, roi_width, horizontal, method).ok()
12050 })
12051}
12052
12053pub fn stack_line_profile(
12056 data: &[f32],
12057 shape: [usize; 3],
12058 perspective: StackPerspective,
12059 start: (f64, f64),
12060 end: (f64, f64),
12061) -> Option<StackProfile> {
12062 stack_profile_with(data, shape, perspective, |w, h, pixels| {
12063 line_profile_values(w, h, pixels, start, end)
12066 .ok()
12067 .map(|(_positions, values)| values)
12068 })
12069}
12070
12071pub fn default_dimension_label(axis: usize) -> String {
12074 format!("Dimension {axis}")
12075}
12076
12077pub fn dimension_axis_labels(
12081 perspective: StackPerspective,
12082 labels: &[String; 3],
12083) -> (String, String) {
12084 let (height_axis, width_axis) = perspective.display_axes();
12085 (labels[width_axis].clone(), labels[height_axis].clone())
12086}
12087
12088pub fn calibrations_axes_order(
12096 perspective: StackPerspective,
12097 calibrations: &[Calibration; 3],
12098) -> (Calibration, Calibration, Calibration) {
12099 let (height_axis, width_axis) = perspective.display_axes(); (
12101 calibrations[width_axis], calibrations[height_axis], calibrations[perspective.axis()], )
12105}
12106
12107pub fn calibrated_image_geometry(
12111 perspective: StackPerspective,
12112 calibrations: &[Calibration; 3],
12113) -> ((f64, f64), (f64, f64)) {
12114 let (xcalib, ycalib, _zcalib) = calibrations_axes_order(perspective, calibrations);
12115 let origin = (xcalib.apply(0.0), ycalib.apply(0.0));
12116 let scale = (xcalib.slope(), ycalib.slope());
12117 (origin, scale)
12118}
12119
12120pub fn calibrated_image_z(
12123 index: usize,
12124 perspective: StackPerspective,
12125 calibrations: &[Calibration; 3],
12126) -> f64 {
12127 let (_xcalib, _ycalib, zcalib) = calibrations_axes_order(perspective, calibrations);
12128 zcalib.apply(index as f64)
12129}
12130
12131pub struct StackView {
12146 inner: Plot2D,
12147 width: u32,
12148 height: u32,
12149 frames: Vec<Vec<f32>>,
12150 colormap: Colormap,
12151 image_handle: Option<ItemHandle>,
12152 current_frame: usize,
12153 dirty: bool,
12154 volume: Option<(Vec<f32>, [usize; 3])>,
12158 perspective: StackPerspective,
12160 dim_labels: [String; 3],
12163 calibrations: [Calibration; 3],
12167 aggregation: AggregationMode,
12172 aggregation_block: (u32, u32),
12175 profile_mode: ProfileMode,
12179 profile_dimension: StackProfileDimension,
12182 profile_drag_start: Option<(f64, f64)>,
12185 profile_window: crate::widget::profile_window::ProfileWindow,
12188 stack_profile_window: crate::widget::stack_profile_window::StackProfileWindow,
12191}
12192
12193impl StackView {
12194 pub fn new(render_state: &RenderState, id: PlotId) -> Self {
12200 let mut inner = Plot2D::new(render_state, id);
12201 inner.set_keep_data_aspect_ratio(true);
12202 inner.set_graph_cursor(true);
12203 Self {
12204 inner,
12205 width: 0,
12206 height: 0,
12207 frames: Vec::new(),
12208 colormap: Colormap::viridis(0.0, 1.0),
12209 image_handle: None,
12210 current_frame: 0,
12211 dirty: false,
12212 volume: None,
12213 perspective: StackPerspective::default(),
12214 dim_labels: [
12215 default_dimension_label(0),
12216 default_dimension_label(1),
12217 default_dimension_label(2),
12218 ],
12219 calibrations: [Calibration::None; 3],
12220 aggregation: AggregationMode::None,
12221 aggregation_block: (1, 1),
12222 profile_mode: ProfileMode::None,
12223 profile_dimension: StackProfileDimension::default(),
12224 profile_drag_start: None,
12225 profile_window: crate::widget::profile_window::ProfileWindow::new(render_state, id + 1),
12226 stack_profile_window: crate::widget::stack_profile_window::StackProfileWindow::new(
12227 render_state,
12228 id + 2,
12229 ),
12230 }
12231 }
12232
12233 pub fn set_stack(
12235 &mut self,
12236 width: u32,
12237 height: u32,
12238 frames: Vec<Vec<f32>>,
12239 colormap: Colormap,
12240 ) -> Result<(), PlotDataError> {
12241 let expected = (width as usize).saturating_mul(height as usize);
12242 for frame in &frames {
12243 if frame.len() != expected {
12244 return Err(PlotDataError::ImageDataLength {
12245 expected,
12246 actual: frame.len(),
12247 });
12248 }
12249 }
12250 self.width = width;
12251 self.height = height;
12252 self.frames = frames;
12253 self.colormap = colormap;
12254 self.current_frame = 0;
12255 self.dirty = true;
12256 self.volume = None;
12258 Ok(())
12259 }
12260
12261 pub fn set_volume(
12271 &mut self,
12272 data: Vec<f32>,
12273 shape: [usize; 3],
12274 colormap: Colormap,
12275 ) -> Result<(), PlotDataError> {
12276 let [d0, d1, d2] = shape;
12277 let expected = d0.saturating_mul(d1).saturating_mul(d2);
12278 if data.len() != expected {
12279 return Err(PlotDataError::ImageDataLength {
12280 expected,
12281 actual: data.len(),
12282 });
12283 }
12284 self.volume = Some((data, shape));
12285 self.colormap = colormap;
12286 self.rebuild_volume_frames();
12287 Ok(())
12288 }
12289
12290 pub fn perspective(&self) -> StackPerspective {
12293 self.perspective
12294 }
12295
12296 pub fn set_perspective(&mut self, perspective: StackPerspective) {
12301 if perspective == self.perspective {
12302 return;
12303 }
12304 self.perspective = perspective;
12305 if self.volume.is_some() {
12306 self.rebuild_volume_frames();
12307 self.inner.reset_zoom();
12308 }
12309 }
12310
12311 fn rebuild_volume_frames(&mut self) {
12314 let Some((data, shape)) = self.volume.as_ref() else {
12315 return;
12316 };
12317 let (data, shape) = (data.clone(), *shape);
12318 let n = stack_frame_count(shape, self.perspective);
12319 let mut frames = Vec::with_capacity(n);
12320 let (mut width, mut height) = (0u32, 0u32);
12321 for index in 0..n {
12322 if let Some((w, h, pixels)) = stack_frame(&data, shape, self.perspective, index) {
12323 width = w;
12324 height = h;
12325 frames.push(pixels);
12326 }
12327 }
12328 self.width = width;
12329 self.height = height;
12330 self.frames = frames;
12331 self.current_frame = 0;
12332 if let Some(handle) = self.image_handle.take() {
12335 self.inner.remove_image(handle);
12336 }
12337 self.dirty = true;
12338 self.apply_axis_labels();
12339 }
12340
12341 pub fn dimension_labels(&self) -> &[String; 3] {
12343 &self.dim_labels
12344 }
12345
12346 pub fn set_dimension_labels(&mut self, labels: [&str; 3]) {
12352 for (i, label) in labels.iter().enumerate() {
12353 self.dim_labels[i] = if label.is_empty() {
12354 default_dimension_label(i)
12355 } else {
12356 (*label).to_string()
12357 };
12358 }
12359 self.apply_axis_labels();
12360 }
12361
12362 fn apply_axis_labels(&mut self) {
12365 let (x_label, y_label) = dimension_axis_labels(self.perspective, &self.dim_labels);
12366 self.inner.set_graph_x_label(x_label);
12367 self.inner.set_graph_y_label(y_label, YAxis::Left);
12368 }
12369
12370 pub fn calibrations(&self) -> &[Calibration; 3] {
12374 &self.calibrations
12375 }
12376
12377 pub fn calibrations_axes(&self) -> (Calibration, Calibration, Calibration) {
12380 calibrations_axes_order(self.perspective, &self.calibrations)
12381 }
12382
12383 pub fn set_calibrations(&mut self, calibrations: [Calibration; 3]) {
12389 if calibrations == self.calibrations {
12390 return;
12391 }
12392 self.calibrations = calibrations;
12393 if let Some(handle) = self.image_handle.take() {
12396 self.inner.remove_image(handle);
12397 }
12398 self.dirty = true;
12399 if !self.frames.is_empty() {
12400 self.inner.reset_zoom();
12401 }
12402 }
12403
12404 pub fn image_z(&self, index: usize) -> f64 {
12407 calibrated_image_z(index, self.perspective, &self.calibrations)
12408 }
12409
12410 pub fn stack_aligned_profile(
12420 &self,
12421 position: f64,
12422 roi_width: u32,
12423 horizontal: bool,
12424 method: ProfileMethod,
12425 ) -> Option<StackProfile> {
12426 let (data, shape) = self.volume.as_ref()?;
12427 stack_aligned_profile(
12428 data,
12429 *shape,
12430 self.perspective,
12431 position,
12432 roi_width,
12433 horizontal,
12434 method,
12435 )
12436 }
12437
12438 pub fn stack_line_profile(&self, start: (f64, f64), end: (f64, f64)) -> Option<StackProfile> {
12443 let (data, shape) = self.volume.as_ref()?;
12444 stack_line_profile(data, *shape, self.perspective, start, end)
12445 }
12446
12447 pub fn profile_mode(&self) -> ProfileMode {
12450 self.profile_mode
12451 }
12452
12453 pub fn set_profile_mode(&mut self, mode: ProfileMode) {
12459 self.profile_mode = mode;
12460 if mode == ProfileMode::None {
12461 self.profile_drag_start = None;
12462 self.profile_window.set_open(false);
12463 self.stack_profile_window.set_open(false);
12464 }
12465 }
12466
12467 pub fn profile_dimension(&self) -> StackProfileDimension {
12470 self.profile_dimension
12471 }
12472
12473 pub fn set_profile_dimension(&mut self, dimension: StackProfileDimension) {
12477 if dimension == self.profile_dimension {
12478 return;
12479 }
12480 self.profile_dimension = dimension;
12481 match dimension {
12482 StackProfileDimension::OneD => self.stack_profile_window.set_open(false),
12483 StackProfileDimension::TwoD => self.profile_window.set_open(false),
12484 }
12485 }
12486
12487 pub fn profile_window(&self) -> &crate::widget::profile_window::ProfileWindow {
12489 &self.profile_window
12490 }
12491
12492 pub fn profile_window_mut(&mut self) -> &mut crate::widget::profile_window::ProfileWindow {
12494 &mut self.profile_window
12495 }
12496
12497 pub fn stack_profile_window(&self) -> &crate::widget::stack_profile_window::StackProfileWindow {
12499 &self.stack_profile_window
12500 }
12501
12502 pub fn stack_profile_window_mut(
12504 &mut self,
12505 ) -> &mut crate::widget::stack_profile_window::StackProfileWindow {
12506 &mut self.stack_profile_window
12507 }
12508
12509 pub fn show_profile(&mut self, start: (f64, f64), end: (f64, f64)) -> bool {
12522 if self.frames.is_empty() {
12523 return false;
12524 }
12525 match self.profile_dimension {
12526 StackProfileDimension::OneD => {
12527 let Some(roi) = profile_roi_from_drag(self.profile_mode, start, end) else {
12528 return false;
12529 };
12530 let frame = &self.frames[self.current_frame];
12531 self.profile_window
12532 .update_profile(self.width, self.height, frame, &roi);
12533 self.profile_window.set_open(true);
12534 true
12535 }
12536 StackProfileDimension::TwoD => {
12537 let profile = match self.profile_mode {
12538 ProfileMode::Line => self.stack_line_profile(start, end),
12539 ProfileMode::Horizontal => {
12540 self.stack_aligned_profile(end.1.floor(), 1, true, ProfileMethod::Mean)
12541 }
12542 ProfileMode::Vertical => {
12543 self.stack_aligned_profile(end.0.floor(), 1, false, ProfileMethod::Mean)
12544 }
12545 ProfileMode::Rectangle | ProfileMode::None => None,
12546 };
12547 let Some(profile) = profile else {
12548 return false;
12549 };
12550 self.stack_profile_window
12551 .set_profile(&profile, self.colormap.clone());
12552 self.stack_profile_window.set_open(true);
12553 true
12554 }
12555 }
12556 }
12557
12558 fn handle_profile_drag(&mut self, plot_response: &PlotResponse) {
12564 if self.profile_mode == ProfileMode::None || self.frames.is_empty() {
12565 self.profile_drag_start = None;
12566 return;
12567 }
12568 let response = &plot_response.response;
12569 let transform = &plot_response.transform;
12570
12571 if response.drag_started()
12572 && let Some(p) = response.interact_pointer_pos()
12573 {
12574 self.profile_drag_start = Some(transform.pixel_to_data(p));
12575 }
12576
12577 if response.dragged()
12578 && let (Some(start), Some(p)) =
12579 (self.profile_drag_start, response.interact_pointer_pos())
12580 {
12581 let end = transform.pixel_to_data(p);
12582 self.show_profile(start, end);
12583 }
12584
12585 if response.drag_stopped() {
12586 self.profile_drag_start = None;
12587 }
12588 }
12589
12590 pub fn show_profile3d_toolbar(&mut self, ui: &mut egui::Ui) {
12595 ui.horizontal(|ui| {
12596 let mut mode = self.profile_mode;
12597 if ui
12598 .selectable_label(mode == ProfileMode::None, "○")
12599 .on_hover_text("No profile")
12600 .clicked()
12601 {
12602 mode = ProfileMode::None;
12603 }
12604 if ui
12605 .selectable_label(mode == ProfileMode::Horizontal, "H")
12606 .on_hover_text("Horizontal line profile over the stack")
12607 .clicked()
12608 {
12609 mode = ProfileMode::Horizontal;
12610 }
12611 if ui
12612 .selectable_label(mode == ProfileMode::Vertical, "V")
12613 .on_hover_text("Vertical line profile over the stack")
12614 .clicked()
12615 {
12616 mode = ProfileMode::Vertical;
12617 }
12618 if ui
12619 .selectable_label(mode == ProfileMode::Line, "L")
12620 .on_hover_text("Line profile over the stack (draw a line)")
12621 .clicked()
12622 {
12623 mode = ProfileMode::Line;
12624 }
12625 if mode != self.profile_mode {
12626 self.set_profile_mode(mode);
12627 }
12628
12629 ui.separator();
12630 ui.label("Profile:");
12631 let mut dimension = self.profile_dimension;
12632 if ui
12633 .selectable_label(dimension == StackProfileDimension::OneD, "1D")
12634 .on_hover_text("Profile of the current frame")
12635 .clicked()
12636 {
12637 dimension = StackProfileDimension::OneD;
12638 }
12639 if ui
12640 .selectable_label(dimension == StackProfileDimension::TwoD, "2D")
12641 .on_hover_text("Profile stacked over all frames")
12642 .clicked()
12643 {
12644 dimension = StackProfileDimension::TwoD;
12645 }
12646 if dimension != self.profile_dimension {
12647 self.set_profile_dimension(dimension);
12648 }
12649 });
12650 }
12651
12652 pub fn frame_count(&self) -> usize {
12654 self.frames.len()
12655 }
12656
12657 pub fn frame(&self) -> usize {
12659 self.current_frame
12660 }
12661
12662 pub fn set_frame(&mut self, index: usize) {
12664 let clamped = index.min(self.frames.len().saturating_sub(1));
12665 if clamped != self.current_frame {
12666 self.current_frame = clamped;
12667 self.dirty = true;
12668 }
12669 }
12670
12671 pub fn set_colormap(&mut self, colormap: Colormap) {
12673 self.colormap = colormap;
12674 self.dirty = true;
12675 }
12676
12677 pub fn perspective_ui(&mut self, ui: &mut egui::Ui) {
12682 if self.volume.is_none() {
12683 return;
12684 }
12685 let labels = self.dim_labels.clone();
12686 let mut selected = self.perspective;
12687 egui::ComboBox::from_label("Browse dimension")
12688 .selected_text(labels[selected.axis()].clone())
12689 .show_ui(ui, |ui| {
12690 for option in [
12691 StackPerspective::Axis0,
12692 StackPerspective::Axis1,
12693 StackPerspective::Axis2,
12694 ] {
12695 ui.selectable_value(&mut selected, option, labels[option.axis()].clone());
12696 }
12697 });
12698 self.set_perspective(selected);
12699 }
12700
12701 pub fn aggregation(&self) -> AggregationMode {
12704 self.aggregation
12705 }
12706
12707 pub fn aggregation_block(&self) -> (u32, u32) {
12709 self.aggregation_block
12710 }
12711
12712 pub fn set_aggregation(&mut self, mode: AggregationMode, block: (u32, u32)) {
12718 let block = (block.0.max(1), block.1.max(1));
12719 if mode != self.aggregation || block != self.aggregation_block {
12720 self.aggregation = mode;
12721 self.aggregation_block = block;
12722 self.dirty = true;
12723 }
12724 }
12725
12726 pub fn show_frame_controls(&mut self, ui: &mut egui::Ui) {
12730 if self.frames.is_empty() {
12731 return;
12732 }
12733 let n = self.frames.len();
12734 ui.horizontal(|ui| {
12735 if ui.button("◀").on_hover_text("Previous frame").clicked() && self.current_frame > 0
12736 {
12737 self.current_frame -= 1;
12738 self.dirty = true;
12739 }
12740 let mut idx = self.current_frame;
12741 if ui
12742 .add(egui::Slider::new(&mut idx, 0..=n.saturating_sub(1)).text("frame"))
12743 .changed()
12744 {
12745 self.current_frame = idx;
12746 self.dirty = true;
12747 }
12748 if ui.button("▶").on_hover_text("Next frame").clicked() && self.current_frame + 1 < n
12749 {
12750 self.current_frame += 1;
12751 self.dirty = true;
12752 }
12753 ui.label(format!("{}/{}", self.current_frame + 1, n));
12754
12755 let mut aggregation = self.aggregation;
12759 egui::ComboBox::from_label("agg")
12760 .selected_text(match aggregation {
12761 AggregationMode::None => "none",
12762 AggregationMode::Max => "max",
12763 AggregationMode::Mean => "mean",
12764 AggregationMode::Min => "min",
12765 })
12766 .show_ui(ui, |ui| {
12767 ui.selectable_value(&mut aggregation, AggregationMode::None, "none");
12768 ui.selectable_value(&mut aggregation, AggregationMode::Max, "max");
12769 ui.selectable_value(&mut aggregation, AggregationMode::Mean, "mean");
12770 ui.selectable_value(&mut aggregation, AggregationMode::Min, "min");
12771 });
12772
12773 let mut block = self.aggregation_block;
12775 let bx = ui.add(
12776 egui::DragValue::new(&mut block.0)
12777 .range(1..=64)
12778 .prefix("bx "),
12779 );
12780 let by = ui.add(
12781 egui::DragValue::new(&mut block.1)
12782 .range(1..=64)
12783 .prefix("by "),
12784 );
12785 if aggregation != self.aggregation || bx.changed() || by.changed() {
12786 self.set_aggregation(aggregation, block);
12787 }
12788 });
12789 }
12790
12791 pub fn show(&mut self, ui: &mut egui::Ui) -> PlotResponse {
12793 if self.dirty && !self.frames.is_empty() {
12794 let frame = &self.frames[self.current_frame];
12795 let (origin, scale) = calibrated_image_geometry(self.perspective, &self.calibrations);
12800 let mut spec = ImageSpec::scalar(self.width, self.height, frame, self.colormap.clone());
12801 spec.origin = origin;
12802 spec.scale = scale;
12803 spec.aggregation = self.aggregation;
12804 spec.aggregation_block = self.aggregation_block;
12805 if let Some(handle) = self.image_handle {
12806 self.inner.update_image_spec(handle, spec);
12807 } else {
12808 self.image_handle = Some(self.inner.add_image_spec(spec));
12809 }
12810 self.dirty = false;
12811 }
12812 let response = self.inner.show(ui);
12813 self.handle_profile_drag(&response);
12817 self.profile_window.show(ui.ctx());
12818 self.stack_profile_window.show(ui.ctx());
12819 response
12820 }
12821}
12822
12823impl Deref for StackView {
12824 type Target = Plot2D;
12825
12826 fn deref(&self) -> &Self::Target {
12827 &self.inner
12828 }
12829}
12830
12831impl DerefMut for StackView {
12832 fn deref_mut(&mut self) -> &mut Self::Target {
12833 &mut self.inner
12834 }
12835}
12836
12837fn roi_description(roi: &Roi) -> String {
12841 match roi {
12842 Roi::Rect { x, y } => format!(
12843 "Rect x=[{:.3}, {:.3}] y=[{:.3}, {:.3}]",
12844 x.0, x.1, y.0, y.1
12845 ),
12846 Roi::HRange { y } => format!("HRange y=[{:.3}, {:.3}]", y.0, y.1),
12847 Roi::VRange { x } => format!("VRange x=[{:.3}, {:.3}]", x.0, x.1),
12848 Roi::HLine { y } => format!("HLine y={y:.3}"),
12849 Roi::VLine { x } => format!("VLine x={x:.3}"),
12850 Roi::Point { x, y } => format!("Point ({x:.3}, {y:.3})"),
12851 Roi::Line { start, end } => format!(
12852 "Line ({:.3},{:.3}) → ({:.3},{:.3})",
12853 start.0, start.1, end.0, end.1
12854 ),
12855 Roi::Polygon { vertices } => format!("Polygon {} vertices", vertices.len()),
12856 Roi::Cross { center } => format!("Cross ({:.3}, {:.3})", center.0, center.1),
12857 Roi::Circle { center, radius } => {
12858 format!(
12859 "Circle c=({:.3}, {:.3}) r={radius:.3}",
12860 center.0, center.1
12861 )
12862 }
12863 Roi::Ellipse {
12864 center,
12865 radii,
12866 orientation,
12867 } => format!(
12868 "Ellipse c=({:.3}, {:.3}) r=({:.3}, {:.3}) θ={:.1}°",
12869 center.0,
12870 center.1,
12871 radii.0,
12872 radii.1,
12873 orientation.to_degrees()
12874 ),
12875 Roi::Arc {
12876 center,
12877 inner_radius,
12878 outer_radius,
12879 start_angle,
12880 end_angle,
12881 } => format!(
12882 "Arc c=({:.3}, {:.3}) r=[{:.3}, {:.3}] θ=[{:.3}, {:.3}]",
12883 center.0, center.1, inner_radius, outer_radius, start_angle, end_angle
12884 ),
12885 Roi::Band { begin, end, width } => format!(
12886 "Band ({:.3},{:.3}) → ({:.3},{:.3}) w={width:.3}",
12887 begin.0, begin.1, end.0, end.1
12888 ),
12889 }
12890}
12891
12892#[cfg(test)]
12893mod tests {
12894 use super::*;
12895
12896 fn sample_volume() -> (Vec<f32>, [usize; 3]) {
12899 let shape = [2usize, 3, 4];
12900 let [d0, d1, d2] = shape;
12901 let mut data = vec![0.0f32; d0 * d1 * d2];
12902 for i in 0..d0 {
12903 for j in 0..d1 {
12904 for k in 0..d2 {
12905 data[(i * d1 + j) * d2 + k] = (100 * i + 10 * j + k) as f32;
12906 }
12907 }
12908 }
12909 (data, shape)
12910 }
12911
12912 #[test]
12913 fn stack_frame_count_is_the_browsed_dimension() {
12914 let shape = [2usize, 3, 4];
12915 assert_eq!(stack_frame_count(shape, StackPerspective::Axis0), 2);
12916 assert_eq!(stack_frame_count(shape, StackPerspective::Axis1), 3);
12917 assert_eq!(stack_frame_count(shape, StackPerspective::Axis2), 4);
12918 }
12919
12920 #[test]
12921 fn stack_perspective_display_axes_are_non_browsed_ascending() {
12922 assert_eq!(StackPerspective::Axis0.display_axes(), (1, 2));
12923 assert_eq!(StackPerspective::Axis1.display_axes(), (0, 2));
12924 assert_eq!(StackPerspective::Axis2.display_axes(), (0, 1));
12925 }
12926
12927 #[test]
12928 fn stack_frame_axis0_browses_dim0_without_transpose() {
12929 let (data, shape) = sample_volume();
12930 let (w, h, pixels) = stack_frame(&data, shape, StackPerspective::Axis0, 1).unwrap();
12932 assert_eq!((w, h), (4, 3));
12933 assert_eq!(
12934 pixels,
12935 vec![
12936 100.0, 101.0, 102.0, 103.0, 110.0, 111.0, 112.0, 113.0, 120.0, 121.0, 122.0, 123.0
12937 ]
12938 );
12939 }
12940
12941 #[test]
12942 fn stack_frame_axis1_transposes_1_0_2() {
12943 let (data, shape) = sample_volume();
12944 let (w, h, pixels) = stack_frame(&data, shape, StackPerspective::Axis1, 2).unwrap();
12946 assert_eq!((w, h), (4, 2));
12947 assert_eq!(
12948 pixels,
12949 vec![20.0, 21.0, 22.0, 23.0, 120.0, 121.0, 122.0, 123.0]
12950 );
12951 }
12952
12953 #[test]
12954 fn stack_frame_axis2_transposes_2_0_1() {
12955 let (data, shape) = sample_volume();
12956 let (w, h, pixels) = stack_frame(&data, shape, StackPerspective::Axis2, 3).unwrap();
12958 assert_eq!((w, h), (3, 2));
12959 assert_eq!(pixels, vec![3.0, 13.0, 23.0, 103.0, 113.0, 123.0]);
12960 }
12961
12962 #[test]
12963 fn stack_frame_rejects_length_mismatch_and_out_of_range() {
12964 let (data, shape) = sample_volume();
12965 assert!(stack_frame(&data[..23], shape, StackPerspective::Axis0, 0).is_none());
12967 assert!(stack_frame(&data, shape, StackPerspective::Axis0, 2).is_none());
12969 }
12970
12971 #[test]
12972 fn stack_aligned_profile_horizontal_stacks_each_frame_row() {
12973 let (data, shape) = sample_volume(); let sp = stack_aligned_profile(
12976 &data,
12977 shape,
12978 StackPerspective::Axis0,
12979 1.0,
12980 1,
12981 true,
12982 ProfileMethod::Mean,
12983 )
12984 .unwrap();
12985 assert_eq!(sp.frame_count, 2);
12986 assert_eq!(sp.profile_len, 4);
12987 assert_eq!(
12989 sp.values,
12990 vec![10.0, 11.0, 12.0, 13.0, 110.0, 111.0, 112.0, 113.0]
12991 );
12992 }
12993
12994 #[test]
12995 fn stack_line_profile_separates_frames() {
12996 let (data, shape) = sample_volume();
12997 let sp = stack_line_profile(
12999 &data,
13000 shape,
13001 StackPerspective::Axis0,
13002 (0.0, 0.0),
13003 (3.0, 0.0),
13004 )
13005 .unwrap();
13006 assert_eq!(sp.frame_count, 2);
13007 assert!(sp.profile_len > 0);
13008 assert_eq!(sp.values.len(), 2 * sp.profile_len);
13009 let (frame0, frame1) = sp.values.split_at(sp.profile_len);
13011 assert!(frame0.iter().all(|&v| v < 100.0), "frame0 {frame0:?}");
13012 assert!(frame1.iter().all(|&v| v >= 100.0), "frame1 {frame1:?}");
13013 }
13014
13015 #[test]
13016 fn stack_profile_rejects_shape_mismatch() {
13017 let (data, shape) = sample_volume();
13018 assert!(
13019 stack_aligned_profile(
13020 &data[..23],
13021 shape,
13022 StackPerspective::Axis0,
13023 1.0,
13024 1,
13025 true,
13026 ProfileMethod::Mean,
13027 )
13028 .is_none()
13029 );
13030 }
13031
13032 #[test]
13033 fn stack_profile_empty_stack_is_none() {
13034 assert!(
13036 stack_aligned_profile(
13037 &[],
13038 [0, 3, 4],
13039 StackPerspective::Axis0,
13040 0.0,
13041 1,
13042 true,
13043 ProfileMethod::Mean,
13044 )
13045 .is_none()
13046 );
13047 }
13048
13049 #[test]
13050 fn dimension_axis_labels_use_width_for_x_and_height_for_y() {
13051 let labels = ["z".to_string(), "y".to_string(), "x".to_string()];
13052 assert_eq!(
13054 dimension_axis_labels(StackPerspective::Axis0, &labels),
13055 ("x".to_string(), "y".to_string())
13056 );
13057 assert_eq!(
13058 dimension_axis_labels(StackPerspective::Axis1, &labels),
13059 ("x".to_string(), "z".to_string())
13060 );
13061 assert_eq!(
13062 dimension_axis_labels(StackPerspective::Axis2, &labels),
13063 ("y".to_string(), "z".to_string())
13064 );
13065 }
13066
13067 #[test]
13068 fn calibrations_axes_order_maps_x_to_width_y_to_height_z_to_browsed() {
13069 let calibs = [
13071 Calibration::linear(0.0, 1.0), Calibration::linear(10.0, 2.0), Calibration::linear(100.0, 3.0), ];
13075 let (x, y, z) = calibrations_axes_order(StackPerspective::Axis0, &calibs);
13077 assert_eq!(x, calibs[2]);
13078 assert_eq!(y, calibs[1]);
13079 assert_eq!(z, calibs[0]);
13080 let (x, y, z) = calibrations_axes_order(StackPerspective::Axis1, &calibs);
13082 assert_eq!(x, calibs[2]);
13083 assert_eq!(y, calibs[0]);
13084 assert_eq!(z, calibs[1]);
13085 let (x, y, z) = calibrations_axes_order(StackPerspective::Axis2, &calibs);
13087 assert_eq!(x, calibs[1]);
13088 assert_eq!(y, calibs[0]);
13089 assert_eq!(z, calibs[2]);
13090 }
13091
13092 #[test]
13093 fn calibrated_image_geometry_uses_intercept_for_origin_and_slope_for_scale() {
13094 let calibs = [
13097 Calibration::None,
13098 Calibration::linear(10.0, 2.0),
13099 Calibration::linear(100.0, 3.0),
13100 ];
13101 let (origin, scale) = calibrated_image_geometry(StackPerspective::Axis0, &calibs);
13102 assert_eq!(origin, (100.0, 10.0));
13103 assert_eq!(scale, (3.0, 2.0));
13104 }
13105
13106 #[test]
13107 fn calibrated_image_geometry_defaults_to_identity() {
13108 let calibs = [Calibration::None; 3];
13109 let (origin, scale) = calibrated_image_geometry(StackPerspective::Axis0, &calibs);
13110 assert_eq!(origin, (0.0, 0.0));
13111 assert_eq!(scale, (1.0, 1.0));
13112 }
13113
13114 #[test]
13115 fn calibrated_image_z_applies_browsed_dim_calibration() {
13116 let calibs = [
13118 Calibration::linear(5.0, 0.5),
13119 Calibration::None,
13120 Calibration::None,
13121 ];
13122 assert_eq!(calibrated_image_z(0, StackPerspective::Axis0, &calibs), 5.0);
13123 assert_eq!(calibrated_image_z(4, StackPerspective::Axis0, &calibs), 7.0);
13124 assert_eq!(calibrated_image_z(4, StackPerspective::Axis1, &calibs), 4.0);
13126 }
13127
13128 #[test]
13129 fn default_dimension_labels_drive_axis_labels_per_perspective() {
13130 let labels = [
13131 default_dimension_label(0),
13132 default_dimension_label(1),
13133 default_dimension_label(2),
13134 ];
13135 assert_eq!(labels, ["Dimension 0", "Dimension 1", "Dimension 2"]);
13136 assert_eq!(
13138 dimension_axis_labels(StackPerspective::Axis0, &labels),
13139 ("Dimension 2".to_string(), "Dimension 1".to_string())
13140 );
13141 assert_eq!(
13143 dimension_axis_labels(StackPerspective::Axis2, &labels),
13144 ("Dimension 1".to_string(), "Dimension 0".to_string())
13145 );
13146 }
13147
13148 #[test]
13149 fn ordered_limits_swaps_reversed_bounds() {
13150 assert_eq!(ordered_limits(1.0, 5.0), (1.0, 5.0));
13152 assert_eq!(ordered_limits(5.0, 1.0), (1.0, 5.0));
13154 assert_eq!(ordered_limits(2.0, 2.0), (2.0, 2.0));
13156 }
13157
13158 #[test]
13159 fn scatter_pick_returns_nearest_point_within_radius() {
13160 let points = [(10.0, 0.0), (3.0, 4.0), (100.0, 100.0)];
13162 assert_eq!(scatter_pick_pixels((0.0, 0.0), &points, 8.0), Some(1));
13164 }
13165
13166 #[test]
13167 fn scatter_pick_none_when_all_outside_radius() {
13168 let points = [(100.0, 0.0), (0.0, 100.0)];
13169 assert_eq!(scatter_pick_pixels((0.0, 0.0), &points, 8.0), None);
13170 }
13171
13172 #[test]
13173 fn scatter_pick_ties_resolve_to_highest_index() {
13174 let points = [(5.0, 0.0), (5.0, 0.0)];
13176 assert_eq!(scatter_pick_pixels((0.0, 0.0), &points, 8.0), Some(1));
13177 }
13178
13179 #[test]
13180 fn nearest_candidate_in_data_picks_closest_then_highest_index() {
13181 let xs = [0.0, 1.0, 2.0, 3.0];
13182 let ys = [0.0, 0.0, 0.0, 0.0];
13183 assert_eq!(
13186 nearest_candidate_in_data(&[0, 1, 2], &xs, &ys, 1.9, 0.0),
13187 Some(2)
13188 );
13189 assert_eq!(nearest_candidate_in_data(&[], &xs, &ys, 0.0, 0.0), None);
13191 }
13192
13193 #[test]
13194 fn nearest_candidate_in_data_ties_resolve_to_highest_index() {
13195 let xs = [5.0, 5.0];
13198 let ys = [0.0, 0.0];
13199 assert_eq!(
13200 nearest_candidate_in_data(&[0, 1], &xs, &ys, 0.0, 0.0),
13201 Some(1)
13202 );
13203 }
13204
13205 #[test]
13206 fn scatter_position_info_snaps_to_pick() {
13207 let pick = Some(ScatterPick {
13208 index: 7,
13209 x: 1.5,
13210 y: 2.5,
13211 value: 3.5,
13212 });
13213 let cols = scatter_position_info(pick).values(Some([9.0, 9.0]));
13215 assert_eq!(cols, vec!["1.5", "2.5", "3.5", "7"]);
13216 }
13217
13218 #[test]
13219 fn scatter_position_info_falls_back_without_pick() {
13220 let cols = scatter_position_info(None).values(Some([1.5, 2.5]));
13222 assert_eq!(cols, vec!["1.5", "2.5", "-", "-"]);
13223 let cols = scatter_position_info(None).values(None);
13225 assert_eq!(cols, vec!["------", "------", "------", "------"]);
13226 }
13227
13228 #[test]
13229 fn active_axis_label_overrides_routes_y_by_axis() {
13230 use crate::core::transform::YAxis;
13231 assert_eq!(
13233 active_axis_label_overrides(Some("Time"), Some("Counts"), YAxis::Left),
13234 (Some("Time".to_string()), Some("Counts".to_string()), None)
13235 );
13236 assert_eq!(
13238 active_axis_label_overrides(Some("Time"), Some("Counts"), YAxis::Right),
13239 (Some("Time".to_string()), None, Some("Counts".to_string()))
13240 );
13241 assert_eq!(
13244 active_axis_label_overrides(None, None, YAxis::Left),
13245 (None, None, None)
13246 );
13247 }
13248
13249 #[test]
13250 fn set_symbol_visibility_hide_then_show_is_lossless() {
13251 let mut cache = None;
13254 let hidden = set_symbol_visibility(Some(Symbol::Diamond), false, &mut cache);
13255 assert_eq!(hidden, None);
13256 assert_eq!(cache, Some(Symbol::Diamond));
13257 let shown = set_symbol_visibility(hidden, true, &mut cache);
13258 assert_eq!(shown, Some(Symbol::Diamond));
13259 assert_eq!(cache, None);
13261 }
13262
13263 #[test]
13264 fn set_line_visibility_hide_then_show_is_lossless() {
13265 let mut cache = None;
13268 let hidden = set_line_visibility(LineStyle::Dashed, false, &mut cache);
13269 assert_eq!(hidden, LineStyle::None);
13270 assert_eq!(cache, Some(LineStyle::Dashed));
13271 let shown = set_line_visibility(hidden, true, &mut cache);
13272 assert_eq!(shown, LineStyle::Dashed);
13273 assert_eq!(cache, None);
13274 }
13275
13276 #[test]
13277 fn curve_legend_visual_carries_line_style_and_symbol() {
13278 let x = [0.0, 1.0];
13282 let y = [0.0, 1.0];
13283
13284 let mut dashed = CurveSpec::new(&x, &y, Color32::RED);
13285 dashed.line_style = LineStyle::Dashed;
13286 dashed.symbol = Some(Symbol::Square);
13287 let v = curve_spec_legend_visual(&dashed, PlotItemKind::Curve);
13288 assert_eq!(v.color, Color32::RED);
13289 assert_eq!(v.line_style, LineStyle::Dashed);
13290 assert_eq!(v.symbol, Some(Symbol::Square));
13291
13292 let mut markers = CurveSpec::new(&x, &y, Color32::BLUE);
13294 markers.line_style = LineStyle::None;
13295 markers.symbol = Some(Symbol::Circle);
13296 let v = curve_spec_legend_visual(&markers, PlotItemKind::Scatter);
13297 assert_eq!(v.line_style, LineStyle::None);
13298 assert!(!v.line_style.draws_line());
13299 assert_eq!(v.symbol, Some(Symbol::Circle));
13300
13301 let v =
13303 curve_spec_legend_visual(&CurveSpec::new(&x, &y, Color32::GREEN), PlotItemKind::Curve);
13304 assert_eq!(v.line_style, LineStyle::Solid);
13305 assert_eq!(v.symbol, None);
13306 }
13307
13308 #[test]
13309 fn set_symbol_visibility_no_op_when_already_in_state() {
13310 let mut cache = Some(Symbol::Square);
13313 let out = set_symbol_visibility(Some(Symbol::Circle), true, &mut cache);
13314 assert_eq!(out, Some(Symbol::Circle));
13315 assert_eq!(cache, Some(Symbol::Square));
13316
13317 let mut cache = Some(Symbol::Diamond);
13319 let out = set_symbol_visibility(None, false, &mut cache);
13320 assert_eq!(out, None);
13321 assert_eq!(cache, Some(Symbol::Diamond));
13322 }
13323
13324 #[test]
13325 fn set_line_visibility_no_op_when_already_in_state() {
13326 let mut cache = Some(LineStyle::Dotted);
13328 let out = set_line_visibility(LineStyle::Dashed, true, &mut cache);
13329 assert_eq!(out, LineStyle::Dashed);
13330 assert_eq!(cache, Some(LineStyle::Dotted));
13331
13332 let mut cache = Some(LineStyle::Dashed);
13334 let out = set_line_visibility(LineStyle::None, false, &mut cache);
13335 assert_eq!(out, LineStyle::None);
13336 assert_eq!(cache, Some(LineStyle::Dashed));
13337 }
13338
13339 fn highlight_base() -> CurveData {
13340 let mut base = CurveData::new(vec![0.0, 1.0], vec![0.0, 1.0], Color32::RED);
13342 base.width = 1.0;
13343 base.line_style = LineStyle::Solid;
13344 base.symbol = Some(Symbol::Circle);
13345 base.marker_size = 7.0;
13346 base.gap_color = Some(Color32::BLUE);
13347 base
13348 }
13349
13350 #[test]
13351 fn current_curve_style_not_highlighted_returns_base_unchanged() {
13352 let base = highlight_base();
13355 let highlight = CurveStyle {
13356 color: Some(Color32::GREEN),
13357 line_width: Some(9.0),
13358 line_style: Some(LineStyle::Dashed),
13359 symbol: Some(Symbol::Square),
13360 symbol_size: Some(99.0),
13361 gap_color: Some(Color32::WHITE),
13362 };
13363 let resolved = current_curve_style(&base, &highlight, false);
13364 assert_eq!(resolved.color, base.color);
13365 assert_eq!(resolved.width, base.width);
13366 assert_eq!(resolved.line_style, base.line_style);
13367 assert_eq!(resolved.symbol, base.symbol);
13368 assert_eq!(resolved.marker_size, base.marker_size);
13369 assert_eq!(resolved.gap_color, base.gap_color);
13370 }
13371
13372 #[test]
13373 fn current_curve_style_default_highlight_overrides_only_width() {
13374 let base = highlight_base();
13378 let highlight = CurveStyle {
13379 line_width: Some(2.0),
13380 ..CurveStyle::default()
13381 };
13382 let resolved = current_curve_style(&base, &highlight, true);
13383 assert_eq!(resolved.width, 2.0);
13384 assert_eq!(resolved.color, base.color);
13385 assert_eq!(resolved.line_style, base.line_style);
13386 assert_eq!(resolved.symbol, base.symbol);
13387 assert_eq!(resolved.marker_size, base.marker_size);
13388 assert_eq!(resolved.gap_color, base.gap_color);
13389 }
13390
13391 #[test]
13392 fn current_curve_style_per_field_override_color_and_line_style_only() {
13393 let base = highlight_base();
13396 let highlight = CurveStyle {
13397 color: Some(Color32::GREEN),
13398 line_style: Some(LineStyle::Dashed),
13399 ..CurveStyle::default()
13400 };
13401 let resolved = current_curve_style(&base, &highlight, true);
13402 assert_eq!(resolved.color, Color32::GREEN);
13403 assert_eq!(resolved.line_style, LineStyle::Dashed);
13404 assert_eq!(resolved.width, base.width); assert_eq!(resolved.symbol, base.symbol);
13406 assert_eq!(resolved.marker_size, base.marker_size);
13407 assert_eq!(resolved.gap_color, base.gap_color);
13408 }
13409
13410 #[test]
13411 fn default_active_curve_style_is_width_two_only() {
13412 let default_style = CurveStyle {
13414 line_width: Some(2.0),
13415 ..CurveStyle::default()
13416 };
13417 assert_eq!(default_style.line_width, Some(2.0));
13418 assert_eq!(default_style.color, None);
13419 assert_eq!(default_style.line_style, None);
13420 assert_eq!(default_style.symbol, None);
13421 assert_eq!(default_style.symbol_size, None);
13422 assert_eq!(default_style.gap_color, None);
13423 }
13424
13425 #[test]
13426 fn set_symbol_visibility_show_from_never_cached_uses_default() {
13427 let mut cache = None;
13430 let out = set_symbol_visibility(None, true, &mut cache);
13431 assert_eq!(out, Some(Symbol::Point));
13432 assert_eq!(out, Some(DEFAULT_RESTORE_SYMBOL));
13433 assert_eq!(cache, None);
13434 }
13435
13436 #[test]
13437 fn set_line_visibility_show_from_never_cached_uses_default() {
13438 let mut cache = None;
13441 let out = set_line_visibility(LineStyle::None, true, &mut cache);
13442 assert_eq!(out, LineStyle::Solid);
13443 assert_eq!(out, DEFAULT_RESTORE_LINE_STYLE);
13444 assert_eq!(cache, None);
13445 }
13446
13447 #[test]
13448 fn clamp_alpha_clamps_out_of_range_entries() {
13449 assert_eq!(
13452 clamp_alpha(vec![1.5, -0.5, 0.25, 1.0, 0.0]),
13453 vec![1.0, 0.0, 0.25, 1.0, 0.0]
13454 );
13455 }
13456
13457 #[test]
13458 fn split_composite_vertical_splits_columns() {
13459 let a = vec![[1u8, 1, 1, 1]; 6];
13462 let b = vec![[2u8, 2, 2, 2]; 6];
13463 let out = split_composite(&a, &b, 3, 2, 2, false);
13464 assert_eq!(out[0], a[0]);
13466 assert_eq!(out[1], a[1]);
13467 assert_eq!(out[2], b[2]);
13468 assert_eq!(out[3], a[3]);
13469 assert_eq!(out[4], a[4]);
13470 assert_eq!(out[5], b[5]);
13471 }
13472
13473 #[test]
13474 fn split_composite_horizontal_splits_rows() {
13475 let a = vec![[1u8, 1, 1, 1]; 6];
13478 let b = vec![[2u8, 2, 2, 2]; 6];
13479 let out = split_composite(&a, &b, 3, 2, 1, true);
13480 assert_eq!(&out[0..3], &[a[0], a[1], a[2]]);
13482 assert_eq!(&out[3..6], &[b[3], b[4], b[5]]);
13483 }
13484
13485 #[test]
13486 fn split_composite_extremes_show_one_image() {
13487 let a = vec![[1u8, 1, 1, 1]; 6];
13488 let b = vec![[2u8, 2, 2, 2]; 6];
13489 assert!(
13491 split_composite(&a, &b, 3, 2, 0, false)
13492 .iter()
13493 .all(|&p| p == b[0])
13494 );
13495 assert!(
13496 split_composite(&a, &b, 3, 2, 0, true)
13497 .iter()
13498 .all(|&p| p == b[0])
13499 );
13500 assert!(
13502 split_composite(&a, &b, 3, 2, 3, false)
13503 .iter()
13504 .all(|&p| p == a[0])
13505 );
13506 assert!(
13507 split_composite(&a, &b, 3, 2, 2, true)
13508 .iter()
13509 .all(|&p| p == a[0])
13510 );
13511 }
13512
13513 #[test]
13514 fn red_blue_gray_composite_matches_silx_channel_layout() {
13515 let cm = Colormap::viridis(0.0, 1.0);
13519 let data_a = vec![0.0f32, 1.0];
13520 let data_b = vec![1.0f32, 0.0];
13521 let byte = |v: f32| (cm.normalize(v as f64) * 255.0).clamp(0.0, 255.0) as u8;
13522 let (a0, b0) = (byte(0.0), byte(1.0));
13523 let (a1, b1) = (byte(1.0), byte(0.0));
13524
13525 let pos = red_blue_gray_composite(&data_a, &data_b, &cm, false);
13526 assert_eq!(pos[0], [a0, a0 / 2 + b0 / 2, b0, 255]);
13527 assert_eq!(pos[1], [a1, a1 / 2 + b1 / 2, b1, 255]);
13528 assert!(pos[0][2] > pos[0][0]);
13530 assert!(pos[1][0] > pos[1][2]);
13531
13532 let neg = red_blue_gray_composite(&data_a, &data_b, &cm, true);
13533 assert_eq!(neg[0], [255 - b0, 255 - (a0 / 2 + b0 / 2), 255 - a0, 255]);
13534 assert_eq!(neg[1], [255 - b1, 255 - (a1 / 2 + b1 / 2), 255 - a1, 255]);
13535 }
13536
13537 #[test]
13538 fn compose_per_point_alpha_multiplies_color_alpha() {
13539 let mut colors = vec![Color32::from_rgba_unmultiplied(10, 20, 30, 200)];
13542 compose_per_point_alpha(&mut colors, &[0.5]);
13543 assert_eq!(colors[0], Color32::from_rgba_unmultiplied(10, 20, 30, 100));
13544 }
13545
13546 #[test]
13547 fn compose_per_point_alpha_clamps_each_entry() {
13548 let mut colors = vec![
13551 Color32::from_rgba_unmultiplied(10, 20, 30, 200),
13552 Color32::from_rgba_unmultiplied(40, 50, 60, 200),
13553 ];
13554 compose_per_point_alpha(&mut colors, &[2.0, -1.0]);
13555 assert_eq!(colors[0], Color32::from_rgba_unmultiplied(10, 20, 30, 200));
13556 assert_eq!(colors[1], Color32::from_rgba_unmultiplied(40, 50, 60, 0));
13557 }
13558
13559 #[test]
13560 fn compose_per_point_alpha_handles_length_mismatch() {
13561 let mut colors = vec![
13564 Color32::from_rgba_unmultiplied(0, 0, 0, 200),
13565 Color32::from_rgba_unmultiplied(0, 0, 0, 200),
13566 ];
13567 compose_per_point_alpha(&mut colors, &[0.5]);
13568 assert_eq!(colors[0].a(), 100);
13569 assert_eq!(colors[1].a(), 200);
13570
13571 let mut colors = vec![Color32::from_rgba_unmultiplied(0, 0, 0, 200)];
13573 compose_per_point_alpha(&mut colors, &[0.5, 0.25, 0.1]);
13574 assert_eq!(colors[0].a(), 100);
13575 }
13576
13577 fn data_bounds(x: (f64, f64), y_left: (f64, f64), y_right: Option<(f64, f64)>) -> DataBounds {
13580 DataBounds {
13581 x: Some(Bounds1D::new(x.0, x.1).unwrap()),
13582 y_left: Some(Bounds1D::new(y_left.0, y_left.1).unwrap()),
13583 y_right: y_right.map(|(lo, hi)| Bounds1D::new(lo, hi).unwrap()),
13584 extra: Vec::new(),
13585 }
13586 }
13587
13588 fn apply_widget_reset(plot: &mut Plot, bounds: DataBounds) {
13594 plot.reset_zoom_to_data_range(data_range_from_bounds(&bounds));
13595 }
13596
13597 #[test]
13598 fn widget_reset_keeps_x_and_refits_y_when_only_y_autoscale_on() {
13599 let mut plot = Plot::new(0);
13601 plot.limits = (0.0, 1.0, 0.0, 1.0);
13602 plot.set_x_autoscale(false);
13603 plot.set_y_autoscale(true);
13604 apply_widget_reset(&mut plot, data_bounds((10.0, 20.0), (-5.0, 5.0), None));
13605 assert_eq!(plot.limits, (0.0, 1.0, -5.0, 5.0));
13606 }
13607
13608 #[test]
13609 fn widget_reset_keeps_y_and_refits_x_when_only_x_autoscale_on() {
13610 let mut plot = Plot::new(0);
13612 plot.limits = (0.0, 1.0, 0.0, 1.0);
13613 plot.set_x_autoscale(true);
13614 plot.set_y_autoscale(false);
13615 apply_widget_reset(&mut plot, data_bounds((10.0, 20.0), (-5.0, 5.0), None));
13616 assert_eq!(plot.limits, (10.0, 20.0, 0.0, 1.0));
13617 }
13618
13619 #[test]
13620 fn widget_reset_with_all_autoscale_off_is_noop() {
13621 let mut plot = Plot::new(0);
13622 plot.limits = (0.0, 1.0, 0.0, 1.0);
13623 plot.y2 = Some((0.0, 2.0));
13624 plot.set_x_autoscale(false);
13625 plot.set_y_autoscale(false);
13626 plot.set_y2_autoscale(false);
13627 apply_widget_reset(
13628 &mut plot,
13629 data_bounds((10.0, 20.0), (-5.0, 5.0), Some((-1.0, 1.0))),
13630 );
13631 assert_eq!(plot.limits, (0.0, 1.0, 0.0, 1.0));
13632 assert_eq!(plot.y2, Some((0.0, 2.0)));
13633 }
13634
13635 #[test]
13636 fn data_range_from_bounds_pads_degenerate_axis() {
13637 let bounds = DataBounds {
13640 x: Some(Bounds1D::new(4.0, 4.0).unwrap()),
13641 y_left: Some(Bounds1D::new(-1.0, 1.0).unwrap()),
13642 y_right: None,
13643 extra: Vec::new(),
13644 };
13645 let range = data_range_from_bounds(&bounds);
13646 let (xmin, xmax) = range.x.unwrap();
13647 assert!(xmax > xmin, "degenerate X must be padded: {xmin}..{xmax}");
13648 assert_eq!(range.y, Some((-1.0, 1.0)));
13649 assert_eq!(range.y2, None);
13650 }
13651
13652 #[test]
13653 fn raw_data_range_from_bounds_keeps_raw_bounds_unpadded() {
13654 let bounds = DataBounds {
13658 x: Some(Bounds1D::new(4.0, 4.0).unwrap()),
13659 y_left: Some(Bounds1D::new(-5.0, 5.0).unwrap()),
13660 y_right: None,
13661 extra: Vec::new(),
13662 };
13663 let range = raw_data_range_from_bounds(&bounds);
13664 assert_eq!(range.x, Some((4.0, 4.0)), "single point must stay (v, v)");
13665 assert_eq!(range.y, Some((-5.0, 5.0)));
13666 assert_eq!(range.y2, None);
13667 }
13668
13669 #[test]
13670 fn recompute_data_bounds_populates_live_data_range_cache() {
13671 let mut plot = Plot::new(0);
13676 assert_eq!(
13677 plot.data_range(),
13678 DataRange::default(),
13679 "empty before any data"
13680 );
13681 let bounds = data_bounds((10.0, 20.0), (-5.0, 5.0), Some((-1.0, 1.0)));
13682 plot.set_data_range(raw_data_range_from_bounds(&bounds));
13683 let range = plot.data_range();
13684 assert_eq!(range.x, Some((10.0, 20.0)));
13685 assert_eq!(range.y, Some((-5.0, 5.0)));
13686 assert_eq!(range.y2, Some((-1.0, 1.0)));
13687 }
13688
13689 #[test]
13690 fn save_to_path_dispatch_resolves_format_per_extension() {
13691 use crate::widget::actions::io::SaveTarget;
13696
13697 assert_eq!(
13698 SaveTarget::from_path(Path::new("/tmp/fig.png")),
13699 Some(SaveTarget::Figure(SaveFormat::Png))
13700 );
13701 assert_eq!(
13702 SaveTarget::from_path(Path::new("/tmp/fig.ppm")),
13703 Some(SaveTarget::Figure(SaveFormat::Ppm))
13704 );
13705 assert_eq!(
13706 SaveTarget::from_path(Path::new("/tmp/fig.svg")),
13707 Some(SaveTarget::Figure(SaveFormat::Svg))
13708 );
13709 assert_eq!(
13710 SaveTarget::from_path(Path::new("/tmp/fig.tif")),
13711 Some(SaveTarget::Figure(SaveFormat::Tiff))
13712 );
13713 assert_eq!(
13714 SaveTarget::from_path(Path::new("/tmp/fig.tiff")),
13715 Some(SaveTarget::Figure(SaveFormat::Tiff))
13716 );
13717 assert_eq!(
13718 SaveTarget::from_path(Path::new("/tmp/fig.eps")),
13719 Some(SaveTarget::Figure(SaveFormat::Eps))
13720 );
13721 assert_eq!(
13722 SaveTarget::from_path(Path::new("/tmp/fig.pdf")),
13723 Some(SaveTarget::Figure(SaveFormat::Pdf))
13724 );
13725 assert_eq!(
13726 SaveTarget::from_path(Path::new("/tmp/curve.csv")),
13727 Some(SaveTarget::CurveCsv)
13728 );
13729 assert_eq!(
13730 SaveTarget::from_path(Path::new("/tmp/fig.jpeg")),
13731 Some(SaveTarget::Figure(SaveFormat::Jpeg))
13732 );
13733 assert_eq!(SaveTarget::from_path(Path::new("/tmp/fig.ps")), None);
13736 assert_eq!(SaveTarget::from_path(Path::new("/tmp/noext")), None);
13737 }
13738
13739 #[test]
13740 fn print_temp_png_path_is_process_unique_under_dir() {
13741 let dir = Path::new("/tmp/siplot-test");
13744 let p = print_temp_png_path(dir, 4242);
13745 assert_eq!(p, Path::new("/tmp/siplot-test/siplot-print-4242.png"));
13746 assert!(p.starts_with(dir));
13749 assert_eq!(p.extension().and_then(|e| e.to_str()), Some("png"));
13750 assert_ne!(p, print_temp_png_path(dir, 4243));
13751 }
13752
13753 #[test]
13754 fn colorbar_column_width_reserves_only_when_shown_and_available() {
13755 assert_eq!(colorbar_column_width(true, true), COLORBAR_WIDTH);
13757 assert_eq!(colorbar_column_width(false, true), 0.0);
13759 assert_eq!(colorbar_column_width(true, false), 0.0);
13761 assert_eq!(colorbar_column_width(false, false), 0.0);
13763 }
13764
13765 #[test]
13766 fn row_content_width_subtracts_columns_and_gaps() {
13767 let w = row_content_width(1000.0, 200.0 + 175.0, 2, 8.0);
13772 assert_eq!(w + 200.0 + 175.0 + 2.0 * 8.0, 1000.0);
13773 assert_eq!(row_content_width(1000.0, 0.0, 0, 8.0), 1000.0);
13775 assert_eq!(row_content_width(100.0, 375.0, 2, 8.0), 0.0);
13777 }
13778
13779 #[test]
13780 fn side_histogram_extent_reserves_only_when_shown() {
13781 assert_eq!(side_histogram_extent(true, 200.0), 200.0);
13783 assert_eq!(side_histogram_extent(true, 80.0), 80.0);
13784 assert_eq!(side_histogram_extent(false, 200.0), 0.0);
13786 assert_eq!(side_histogram_extent(false, 80.0), 0.0);
13787 }
13788
13789 #[test]
13792 fn image_column_and_row_sums_match_silx_profile() {
13793 let pixels = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
13797 let (w, h) = (3usize, 2usize);
13798 assert_eq!(image_column_sums(&pixels, w, h), vec![5.0, 7.0, 9.0]);
13800 assert_eq!(image_row_sums(&pixels, w, h), vec![6.0, 15.0]);
13802 }
13803
13804 #[test]
13805 fn image_profile_sums_have_one_entry_per_index() {
13806 let pixels = vec![1.0f32; 12]; let (w, h) = (4usize, 3usize);
13810 assert_eq!(image_column_sums(&pixels, w, h).len(), w);
13811 assert_eq!(image_row_sums(&pixels, w, h).len(), h);
13812 assert!(
13814 image_column_sums(&pixels, w, h)
13815 .iter()
13816 .all(|&s| s == h as f64)
13817 );
13818 assert!(image_row_sums(&pixels, w, h).iter().all(|&s| s == w as f64));
13819 }
13820
13821 #[test]
13824 fn image_value_at_maps_cursor_to_pixel_like_silx() {
13825 let pixels = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
13829 let (w, h) = (3usize, 2usize);
13830 assert_eq!(
13833 image_value_at(0.4, 0.9, &pixels, w, h),
13834 Some((0.0, 0.0, 1.0))
13835 );
13836 assert_eq!(
13837 image_value_at(2.7, 1.2, &pixels, w, h),
13838 Some((2.0, 1.0, 6.0))
13839 );
13840 assert_eq!(
13842 image_value_at(1.0, 1.0, &pixels, w, h),
13843 Some((1.0, 1.0, 5.0))
13844 );
13845 }
13846
13847 #[test]
13848 fn image_value_at_returns_none_off_image() {
13849 let pixels = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
13850 let (w, h) = (3usize, 2usize);
13851 assert_eq!(image_value_at(-0.1, 0.5, &pixels, w, h), None);
13853 assert_eq!(image_value_at(0.5, -0.1, &pixels, w, h), None);
13854 assert_eq!(image_value_at(3.0, 0.5, &pixels, w, h), None);
13856 assert_eq!(image_value_at(0.5, 2.0, &pixels, w, h), None);
13857 assert_eq!(image_value_at(0.5, 0.5, &[], 0, 0), None);
13859 }
13860
13861 #[test]
13862 fn colorbar_toggle_frees_and_restores_the_column() {
13863 let mut show = true;
13870 assert_eq!(
13871 colorbar_column_width(show, true),
13872 COLORBAR_WIDTH,
13873 "shown reserves the column"
13874 );
13875 show = !show; assert_eq!(
13877 colorbar_column_width(show, true),
13878 0.0,
13879 "toggling off frees the column"
13880 );
13881 show = !show; assert_eq!(
13883 colorbar_column_width(show, true),
13884 COLORBAR_WIDTH,
13885 "toggling back on reserves it again"
13886 );
13887 }
13888
13889 #[test]
13890 fn finite_bounds_ignores_non_finite_values() {
13891 let bounds = finite_bounds(&[f64::NAN, 2.0, -1.0, f64::INFINITY]).unwrap();
13892 assert_eq!(bounds.as_non_degenerate(), (-1.0, 2.0));
13893 }
13894
13895 #[test]
13896 fn degenerate_bounds_get_padded() {
13897 assert_eq!(
13898 Bounds1D::new(2.0, 2.0).unwrap().as_non_degenerate(),
13899 (1.5, 2.5)
13900 );
13901 }
13902
13903 #[test]
13904 fn data_bounds_tracks_left_and_right_y_separately() {
13905 let mut bounds = DataBounds::default();
13906 bounds.include(
13907 Bounds1D::new(0.0, 10.0).unwrap(),
13908 Bounds1D::new(-1.0, 1.0).unwrap(),
13909 YAxis::Left,
13910 );
13911 bounds.include(
13912 Bounds1D::new(5.0, 20.0).unwrap(),
13913 Bounds1D::new(100.0, 200.0).unwrap(),
13914 YAxis::Right,
13915 );
13916 assert_eq!(bounds.x.unwrap().as_non_degenerate(), (0.0, 20.0));
13917 assert_eq!(bounds.y_left.unwrap().as_non_degenerate(), (-1.0, 1.0));
13918 assert_eq!(bounds.y_right.unwrap().as_non_degenerate(), (100.0, 200.0));
13919 }
13920
13921 #[test]
13922 fn histogram_step_values_builds_closed_outline() {
13923 let (x, y) = histogram_step_values(&[0.0, 1.0, 3.0], &[2.0, 4.0]).unwrap();
13924 assert_eq!(x, vec![0.0, 0.0, 1.0, 1.0, 3.0, 3.0]);
13925 assert_eq!(y, vec![0.0, 2.0, 2.0, 4.0, 4.0, 0.0]);
13926 }
13927
13928 #[test]
13929 fn histogram_step_values_validates_edges() {
13930 assert_eq!(
13931 histogram_step_values(&[0.0, 1.0], &[2.0, 4.0]).unwrap_err(),
13932 PlotDataError::HistogramLength { bins: 2, edges: 2 }
13933 );
13934 }
13935
13936 #[test]
13937 fn histogram_edges_left_treats_positions_as_left_edges() {
13938 assert_eq!(
13941 histogram_edges(&[0.0, 1.0, 2.0], HistogramAlign::Left),
13942 vec![0.0, 1.0, 2.0, 3.0]
13943 );
13944 }
13945
13946 #[test]
13947 fn histogram_edges_right_treats_positions_as_right_edges() {
13948 assert_eq!(
13951 histogram_edges(&[1.0, 2.0, 3.0], HistogramAlign::Right),
13952 vec![0.0, 1.0, 2.0, 3.0]
13953 );
13954 }
13955
13956 #[test]
13957 fn histogram_edges_center_puts_positions_at_bin_centres() {
13958 assert_eq!(
13961 histogram_edges(&[1.0, 2.0, 3.0], HistogramAlign::Center),
13962 vec![0.5, 1.5, 2.5, 3.5]
13963 );
13964 }
13965
13966 #[test]
13967 fn histogram_edges_single_position_uses_unit_gap() {
13968 assert_eq!(
13970 histogram_edges(&[5.0], HistogramAlign::Left),
13971 vec![5.0, 6.0]
13972 );
13973 assert_eq!(
13974 histogram_edges(&[5.0], HistogramAlign::Right),
13975 vec![4.0, 5.0]
13976 );
13977 assert_eq!(
13978 histogram_edges(&[5.0], HistogramAlign::Center),
13979 vec![4.5, 5.5]
13980 );
13981 }
13982
13983 #[test]
13984 fn histogram_edges_nonuniform_center_uses_following_gap() {
13985 assert_eq!(
13990 histogram_edges(&[0.0, 1.0, 3.0], HistogramAlign::Center),
13991 vec![-0.5, 0.0, 2.0, 4.0]
13992 );
13993 }
13994
13995 #[test]
13996 fn histogram_edges_empty_is_empty() {
13997 assert!(histogram_edges(&[], HistogramAlign::Center).is_empty());
13998 }
13999
14000 #[test]
14001 fn pick_histogram_locates_bin_and_checks_fill() {
14002 let edges = [0.0, 1.0, 2.0, 3.0];
14004 let values = [2.0, -1.0, 3.0];
14005 assert_eq!(pick_histogram(&edges, &values, 0.0, 0.5, 1.0), Some(0));
14007 assert_eq!(pick_histogram(&edges, &values, 0.0, 0.5, 2.5), None);
14009 assert_eq!(pick_histogram(&edges, &values, 0.0, 1.5, -0.5), Some(1));
14011 assert_eq!(pick_histogram(&edges, &values, 0.0, 1.5, 0.5), None);
14013 assert_eq!(pick_histogram(&edges, &values, 0.0, 2.5, 2.0), Some(2));
14015 }
14016
14017 #[test]
14018 fn pick_histogram_outside_bbox_is_none() {
14019 let edges = [0.0, 1.0, 2.0, 3.0];
14020 let values = [2.0, 1.0, 3.0];
14021 assert_eq!(pick_histogram(&edges, &values, 0.0, -0.1, 1.0), None);
14023 assert_eq!(pick_histogram(&edges, &values, 0.0, 3.1, 1.0), None);
14024 assert_eq!(pick_histogram(&edges, &values, 0.0, 0.5, -0.1), None);
14026 assert_eq!(pick_histogram(&edges, &values, 0.0, 0.5, 3.1), None);
14027 assert_eq!(pick_histogram(&edges, &values, 0.0, 0.0, 1.0), None);
14029 assert_eq!(pick_histogram(&edges, &[1.0, 2.0], 0.0, 0.5, 1.0), None);
14031 assert_eq!(pick_histogram(&[], &[], 0.0, 0.5, 1.0), None);
14032 }
14033
14034 #[test]
14035 fn pick_histogram_honours_nonzero_baseline() {
14036 let edges = [0.0, 1.0, 2.0];
14038 let values = [8.0, 3.0];
14039 assert_eq!(pick_histogram(&edges, &values, 5.0, 0.5, 6.0), Some(0));
14041 assert_eq!(pick_histogram(&edges, &values, 5.0, 1.5, 4.0), Some(1));
14043 assert_eq!(pick_histogram(&edges, &values, 5.0, 0.5, 4.0), None);
14045 }
14046
14047 #[test]
14048 fn aligned_histogram_edges_feed_valid_step_values() {
14049 let positions = [1.0, 2.0, 3.0];
14053 let counts = [5.0, 6.0, 7.0];
14054 let edges = histogram_edges(&positions, HistogramAlign::Center);
14055 let (x, y) = histogram_step_values(&edges, &counts).unwrap();
14056 assert_eq!(x, vec![0.5, 0.5, 1.5, 1.5, 2.5, 2.5, 3.5, 3.5]);
14057 assert_eq!(y, vec![0.0, 5.0, 5.0, 6.0, 6.0, 7.0, 7.0, 0.0]);
14058 }
14059
14060 #[test]
14061 fn profile_helpers_extract_rows_and_columns() {
14062 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
14063 assert_eq!(
14064 horizontal_profile_values(3, 2, &data, 1).unwrap(),
14065 vec![4.0, 5.0, 6.0]
14066 );
14067 assert_eq!(
14068 vertical_profile_values(3, 2, &data, 2).unwrap(),
14069 vec![3.0, 6.0]
14070 );
14071 }
14072
14073 #[test]
14074 fn aligned_profile_width_one_mean_matches_single_line() {
14075 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
14077 assert_eq!(
14079 aligned_profile_values(3, 2, &data, 0.0, 1, true, ProfileMethod::Mean).unwrap(),
14080 vec![1.0, 2.0, 3.0]
14081 );
14082 assert_eq!(
14084 aligned_profile_values(3, 2, &data, 1.0, 1, false, ProfileMethod::Mean).unwrap(),
14085 vec![2.0, 5.0]
14086 );
14087 }
14088
14089 #[test]
14090 fn aligned_profile_full_band_mean_and_sum() {
14091 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
14093 assert_eq!(
14095 aligned_profile_values(3, 2, &data, 0.5, 2, true, ProfileMethod::Mean).unwrap(),
14096 vec![2.5, 3.5, 4.5]
14097 );
14098 assert_eq!(
14100 aligned_profile_values(3, 2, &data, 0.5, 2, true, ProfileMethod::Sum).unwrap(),
14101 vec![5.0, 7.0, 9.0]
14102 );
14103 assert_eq!(
14105 aligned_profile_values(3, 2, &data, 1.0, 3, false, ProfileMethod::Sum).unwrap(),
14106 vec![6.0, 15.0]
14107 );
14108 }
14109
14110 #[test]
14111 fn rect_profile_mean_and_sum_reduce_the_band() {
14112 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
14114 let rect = (0.0, 2.0, 0.0, 1.0);
14115 let (x, y) = rect_profile_values(3, 2, &data, rect, true, ProfileMethod::Mean).unwrap();
14117 assert_eq!(x, vec![0.0, 1.0, 2.0]);
14118 assert_eq!(y, vec![2.5, 3.5, 4.5]);
14119 let (_, y_sum) = rect_profile_values(3, 2, &data, rect, true, ProfileMethod::Sum).unwrap();
14121 assert_eq!(y_sum, vec![5.0, 7.0, 9.0]);
14122 let (xr, yr) = rect_profile_values(3, 2, &data, rect, false, ProfileMethod::Sum).unwrap();
14124 assert_eq!(xr, vec![0.0, 1.0]);
14125 assert_eq!(yr, vec![6.0, 15.0]);
14126 }
14127
14128 #[test]
14129 fn line_profile_band_width_one_samples_along_row() {
14130 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
14132 let (x, y) =
14133 line_profile_band(3, 2, &data, (0.0, 0.0), (2.0, 0.0), 1, ProfileMethod::Mean).unwrap();
14134 assert_eq!(x, vec![0.0, 1.0, 2.0]);
14135 assert_eq!(y, vec![1.0, 2.0, 3.0]);
14136 }
14137
14138 #[test]
14139 fn line_profile_band_width_two_averages_perpendicular_rows() {
14140 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
14143 let (_, mean) =
14144 line_profile_band(3, 2, &data, (0.0, 0.5), (2.0, 0.5), 2, ProfileMethod::Mean).unwrap();
14145 assert_eq!(mean, vec![2.5, 3.5, 4.5]);
14146 let (_, sum) =
14147 line_profile_band(3, 2, &data, (0.0, 0.5), (2.0, 0.5), 2, ProfileMethod::Sum).unwrap();
14148 assert_eq!(sum, vec![5.0, 7.0, 9.0]);
14149 }
14150
14151 #[test]
14152 fn line_profile_band_vertical_width_one() {
14153 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
14155 let (x, y) =
14156 line_profile_band(3, 2, &data, (0.0, 0.0), (0.0, 1.0), 1, ProfileMethod::Mean).unwrap();
14157 assert_eq!(x, vec![0.0, 1.0]);
14158 assert_eq!(y, vec![1.0, 4.0]);
14159 }
14160
14161 #[test]
14162 fn line_profile_band_diagonal_is_linear_on_gradient() {
14163 let data = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
14166 let (_, y) =
14167 line_profile_band(3, 3, &data, (0.0, 0.0), (2.0, 2.0), 1, ProfileMethod::Mean).unwrap();
14168 assert_eq!(y.len(), 4);
14170 let diffs: Vec<f64> = y.windows(2).map(|w| w[1] - w[0]).collect();
14171 for d in &diffs {
14172 assert!((d - diffs[0]).abs() < 1e-9, "diffs not constant: {diffs:?}");
14173 }
14174 assert!((y[0] - 0.0).abs() < 1e-9);
14176 assert!((y[3] - 8.0).abs() < 1e-9);
14177 }
14178
14179 #[test]
14180 fn line_profile_band_degenerate_returns_single_sample() {
14181 let data = [1.0, 2.0, 3.0, 4.0];
14182 let (x, y) =
14183 line_profile_band(2, 2, &data, (1.0, 1.0), (1.0, 1.0), 1, ProfileMethod::Mean).unwrap();
14184 assert_eq!(x, vec![0.0]);
14185 assert_eq!(y, vec![4.0]); }
14187
14188 #[test]
14189 fn line_profile_band_validates_length() {
14190 assert_eq!(
14191 line_profile_band(
14192 2,
14193 2,
14194 &[1.0, 2.0, 3.0],
14195 (0.0, 0.0),
14196 (1.0, 1.0),
14197 1,
14198 ProfileMethod::Mean
14199 )
14200 .unwrap_err(),
14201 PlotDataError::ImageDataLength {
14202 expected: 4,
14203 actual: 3,
14204 }
14205 );
14206 }
14207
14208 #[test]
14209 fn aligned_profile_band_clamps_to_image_edges() {
14210 let data = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0];
14212 assert_eq!(
14214 aligned_profile_values(2, 4, &data, 3.0, 2, true, ProfileMethod::Mean).unwrap(),
14215 vec![15.0, 16.0]
14216 );
14217 assert_eq!(
14219 aligned_profile_values(2, 4, &data, 0.0, 2, true, ProfileMethod::Mean).unwrap(),
14220 vec![11.0, 12.0]
14221 );
14222 assert_eq!(
14224 aligned_profile_values(2, 4, &data, 1.0, 10, true, ProfileMethod::Mean).unwrap(),
14225 vec![13.0, 14.0]
14226 );
14227 }
14228
14229 #[test]
14230 fn aligned_profile_validates_length() {
14231 assert_eq!(
14232 aligned_profile_values(2, 2, &[1.0, 2.0, 3.0], 0.0, 1, true, ProfileMethod::Mean)
14233 .unwrap_err(),
14234 PlotDataError::ImageDataLength {
14235 expected: 4,
14236 actual: 3,
14237 }
14238 );
14239 }
14240
14241 #[test]
14242 fn profile_helpers_validate_shape_and_index() {
14243 assert_eq!(
14244 horizontal_profile_values(2, 2, &[1.0, 2.0, 3.0], 0).unwrap_err(),
14245 PlotDataError::ImageDataLength {
14246 expected: 4,
14247 actual: 3,
14248 }
14249 );
14250 assert_eq!(
14251 horizontal_profile_values(2, 2, &[1.0, 2.0, 3.0, 4.0], 2).unwrap_err(),
14252 PlotDataError::ProfileRow { row: 2, height: 2 }
14253 );
14254 assert_eq!(
14255 vertical_profile_values(2, 2, &[1.0, 2.0, 3.0, 4.0], 2).unwrap_err(),
14256 PlotDataError::ProfileColumn {
14257 column: 2,
14258 width: 2,
14259 }
14260 );
14261 }
14262
14263 #[test]
14264 fn value_stats_ignore_non_finite_values() {
14265 let stats = ValueStats::from_f64(&[1.0, f64::NAN, 4.0, f64::INFINITY]);
14266 assert_eq!(stats.count, 4);
14267 assert_eq!(stats.finite_count, 2);
14268 assert_eq!(stats.min, Some(1.0));
14269 assert_eq!(stats.max, Some(4.0));
14270 assert_eq!(stats.mean, Some(2.5));
14271 }
14272
14273 #[test]
14274 fn value_stats_from_f32_matches_f64_semantics() {
14275 let stats = ValueStats::from_f32(&[1.0, f32::NAN, 4.0, f32::INFINITY]);
14276 assert_eq!(stats.count, 4);
14277 assert_eq!(stats.finite_count, 2);
14278 assert_eq!(stats.min, Some(1.0));
14279 assert_eq!(stats.max, Some(4.0));
14280 assert_eq!(stats.mean, Some(2.5));
14281 }
14282
14283 #[test]
14284 fn image_view_alpha_propagates_to_image_spec() {
14285 let pixels = [1.0_f32, 2.0, 3.0, 4.0];
14288 let cmap = Colormap::viridis(0.0, 4.0);
14289 let mut slider = crate::widget::alpha_slider::AlphaSlider::default();
14290 slider.set_alpha(0.25);
14291 let spec = image_view_image_spec(
14292 2,
14293 2,
14294 &pixels,
14295 &cmap,
14296 slider.alpha(),
14297 InterpolationMode::default(),
14298 AggregationMode::default(),
14299 (1, 1),
14300 );
14301 assert_eq!(spec.alpha, slider.alpha());
14303 assert!((spec.alpha - 64.0 / 255.0).abs() < 1e-6);
14304 }
14305
14306 #[test]
14307 fn image_view_interpolation_aggregation_update_spec() {
14308 let pixels = [1.0_f32, 2.0, 3.0, 4.0];
14312 let cmap = Colormap::viridis(0.0, 4.0);
14313 let spec = image_view_image_spec(
14314 2,
14315 2,
14316 &pixels,
14317 &cmap,
14318 1.0,
14319 InterpolationMode::Linear,
14320 AggregationMode::Mean,
14321 (2, 3),
14322 );
14323 assert_eq!(spec.interpolation, InterpolationMode::Linear);
14324 assert_eq!(spec.aggregation, AggregationMode::Mean);
14325 assert_eq!(spec.aggregation_block, (2, 3));
14326 }
14327
14328 #[test]
14329 fn image_view_profile_drag_samples_expected_values() {
14330 let pixels = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0];
14336
14337 let (hx, hy) = image_view_profile_values(
14339 ProfileMode::Horizontal,
14340 3,
14341 2,
14342 &pixels,
14343 (0.0, 1.0),
14344 (2.0, 1.0),
14345 )
14346 .unwrap();
14347 assert_eq!(hx, vec![0.0, 1.0, 2.0]);
14348 assert_eq!(hy, vec![4.0, 5.0, 6.0]);
14349
14350 let (vx, vy) =
14352 image_view_profile_values(ProfileMode::Vertical, 3, 2, &pixels, (2.0, 0.0), (2.0, 1.0))
14353 .unwrap();
14354 assert_eq!(vx, vec![0.0, 1.0]);
14355 assert_eq!(vy, vec![3.0, 6.0]);
14356
14357 let (_, ly) =
14359 image_view_profile_values(ProfileMode::Line, 3, 2, &pixels, (0.0, 0.0), (2.0, 0.0))
14360 .unwrap();
14361 assert_eq!(ly, vec![1.0, 2.0, 3.0]);
14362
14363 assert!(
14365 image_view_profile_values(ProfileMode::None, 3, 2, &pixels, (0.0, 0.0), (2.0, 0.0))
14366 .is_none()
14367 );
14368 }
14369
14370 #[test]
14371 fn profile_roi_from_drag_matches_mode() {
14372 assert_eq!(
14374 profile_roi_from_drag(ProfileMode::Line, (1.0, 2.0), (3.0, 4.0)),
14375 Some(Roi::Line {
14376 start: (1.0, 2.0),
14377 end: (3.0, 4.0)
14378 })
14379 );
14380 assert_eq!(
14381 profile_roi_from_drag(ProfileMode::Horizontal, (0.0, 1.7), (5.0, 1.7)),
14382 Some(Roi::HRange { y: (1.0, 1.0) })
14383 );
14384 assert_eq!(
14385 profile_roi_from_drag(ProfileMode::Vertical, (2.9, 0.0), (2.9, 4.0)),
14386 Some(Roi::VRange { x: (2.0, 2.0) })
14387 );
14388 assert_eq!(
14389 profile_roi_from_drag(ProfileMode::Rectangle, (4.0, 5.0), (1.0, 2.0)),
14390 Some(Roi::Rect {
14391 x: (1.0, 4.0),
14392 y: (2.0, 5.0)
14393 })
14394 );
14395 assert_eq!(
14396 profile_roi_from_drag(ProfileMode::None, (0.0, 0.0), (1.0, 1.0)),
14397 None
14398 );
14399 }
14400
14401 #[test]
14402 fn click_event_for_pick_maps_each_pick_variant() {
14403 let handle: ItemHandle = 7;
14406
14407 assert_eq!(
14409 PlotWidget::click_event_for_pick(
14410 handle,
14411 &PickResult::CurvePoint {
14412 index: 4,
14413 x: 1.5,
14414 y: -2.0,
14415 distance_px: 3.0,
14416 },
14417 MouseButton::Left,
14418 ),
14419 PlotEvent::CurveClicked {
14420 handle,
14421 index: 4,
14422 x: 1.5,
14423 y: -2.0,
14424 button: MouseButton::Left,
14425 }
14426 );
14427
14428 assert_eq!(
14430 PlotWidget::click_event_for_pick(
14431 handle,
14432 &PickResult::ImagePixel { col: 12, row: 9 },
14433 MouseButton::Middle,
14434 ),
14435 PlotEvent::ImageClicked {
14436 handle,
14437 col: 12,
14438 row: 9,
14439 button: MouseButton::Middle,
14440 }
14441 );
14442
14443 assert_eq!(
14445 PlotWidget::click_event_for_pick(
14446 handle,
14447 &PickResult::Item { handle: 99 },
14448 MouseButton::Right,
14449 ),
14450 PlotEvent::ItemClicked {
14451 handle,
14452 button: MouseButton::Right,
14453 }
14454 );
14455 }
14456
14457 #[test]
14458 fn radar_drag_output_maps_to_image_plot_limits() {
14459 use crate::widget::radar_view::{DataRect, RadarView, clamp_viewport};
14463
14464 let mut radar = RadarView::default();
14466 radar.set_data_bounds(0.0, 100.0, 0.0, 80.0);
14467 radar.set_viewport(DataRect::new(0.0, 0.0, 20.0, 16.0));
14468
14469 let moved = DataRect {
14470 left: radar.viewport.left + 30.0,
14471 top: radar.viewport.top + 20.0,
14472 width: radar.viewport.width,
14473 height: radar.viewport.height,
14474 };
14475 let clamped = clamp_viewport(moved, &radar.data_extent);
14476 let (x0, x1, y0, y1) = clamped.limits();
14477 assert_eq!((x0, x1, y0, y1), (30.0, 50.0, 20.0, 36.0));
14479 assert!(x1 > x0 && y1 > y0);
14481 }
14482
14483 #[test]
14484 fn image_view_position_info_reads_hover_cursor() {
14485 use crate::widget::interaction::PlotPointerEvent;
14488 let moved = PlotPointerEvent::Moved {
14489 button: None,
14490 data: (12.5, -3.0),
14491 pixel: (40.0, 60.0),
14492 };
14493 let cursor = cursor_from_pointer_event(Some(&moved));
14494 assert_eq!(cursor, Some([12.5, -3.0]));
14495
14496 let info = crate::widget::position_info::PositionInfo::with_xy();
14497 let values = info.values(cursor);
14498 assert_eq!(values, vec!["12.5".to_owned(), "-3".to_owned()]);
14499
14500 let clicked = PlotPointerEvent::Clicked {
14504 button: crate::widget::interaction::MouseButton::Left,
14505 data: (4.0, 8.0),
14506 pixel: (10.0, 20.0),
14507 };
14508 assert_eq!(cursor_from_pointer_event(Some(&clicked)), Some([4.0, 8.0]));
14509 let double = PlotPointerEvent::DoubleClicked {
14510 button: crate::widget::interaction::MouseButton::Left,
14511 data: (-1.5, 2.25),
14512 pixel: (10.0, 20.0),
14513 };
14514 assert_eq!(cursor_from_pointer_event(Some(&double)), Some([-1.5, 2.25]));
14515
14516 let limits = PlotPointerEvent::LimitsChanged {
14518 x: (0.0, 1.0),
14519 y: (0.0, 1.0),
14520 y2: None,
14521 };
14522 assert_eq!(cursor_from_pointer_event(Some(&limits)), None);
14523 assert_eq!(cursor_from_pointer_event(None), None);
14524 assert_eq!(
14526 info.values(None),
14527 vec!["------".to_owned(), "------".to_owned()]
14528 );
14529 }
14530
14531 #[test]
14532 fn image_view_colorbar_tracks_colormap_limits() {
14533 let cmap = Colormap::viridis(-3.0, 9.5);
14536 let bar = image_view_colorbar(&cmap);
14537 assert_eq!(bar.colormap.vmin, -3.0);
14538 assert_eq!(bar.colormap.vmax, 9.5);
14539 assert_eq!(
14540 bar.orientation,
14541 crate::widget::colorbar::ColorBarOrientation::Vertical
14542 );
14543 }
14544
14545 #[test]
14546 fn scatter_view_colorbar_tracks_value_colormap_limits() {
14547 assert!(
14551 scatter_view_colorbar(None).is_none(),
14552 "no colorbar before set_data"
14553 );
14554 let cmap = Colormap::viridis(2.5, 41.0);
14555 let bar = scatter_view_colorbar(Some(&cmap)).expect("colorbar after set_data");
14556 assert_eq!(bar.colormap.vmin, 2.5);
14557 assert_eq!(bar.colormap.vmax, 41.0);
14558 assert_eq!(
14559 bar.orientation,
14560 crate::widget::colorbar::ColorBarOrientation::Vertical
14561 );
14562 }
14563
14564 #[test]
14565 fn scatter_points_mode_has_no_grid_image() {
14566 let x = [0.0, 1.0, 0.0];
14568 let y = [0.0, 0.0, 1.0];
14569 let v = [1.0, 2.0, 3.0];
14570 assert!(scatter_grid_image(ScatterVisualization::Points, &x, &y, &v, (4, 4)).is_none());
14571 }
14572
14573 #[test]
14574 fn scatter_irregular_grid_mode_has_no_grid_image() {
14575 let x = [0.0, 4.0, 0.0];
14582 let y = [0.0, 0.0, 4.0];
14583 let v = [0.0, 4.0, 0.0];
14584 assert!(
14585 scatter_grid_image(ScatterVisualization::IrregularGrid, &x, &y, &v, (4, 4)).is_none()
14586 );
14587 }
14588
14589 #[test]
14590 fn scatter_binned_statistic_mode_means_and_nan_empty() {
14591 let x = [0.0, 0.5, 2.0];
14594 let y = [0.0, 0.5, 2.0];
14595 let v = [10.0, 30.0, 7.0];
14596 let img = scatter_grid_image(ScatterVisualization::BinnedStatistic, &x, &y, &v, (2, 2))
14597 .expect("binned");
14598 assert_eq!(img.shape, (2, 2));
14599 assert!((img.get(0, 0).unwrap() - 20.0).abs() < 1e-12);
14601 assert!(img.get(0, 1).unwrap().is_nan());
14603 assert!((img.get(1, 1).unwrap() - 7.0).abs() < 1e-12);
14605 assert_eq!(img.origin, (0.0, 0.0));
14606 assert_eq!(img.scale, (1.0, 1.0));
14607 }
14608
14609 #[test]
14610 fn scatter_regular_grid_mode_reshapes_row_major() {
14611 let x = [0.0, 1.0, 2.0, 0.0, 1.0, 2.0];
14614 let y = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
14615 let v = [10.0, 11.0, 12.0, 20.0, 21.0, 22.0];
14616 let img = scatter_grid_image(ScatterVisualization::RegularGrid, &x, &y, &v, (0, 0))
14617 .expect("grid detected");
14618 assert_eq!(img.shape, (2, 3));
14619 assert_eq!(img.get(0, 0), Some(10.0));
14620 assert_eq!(img.get(0, 2), Some(12.0));
14621 assert_eq!(img.get(1, 0), Some(20.0));
14622 assert_eq!(img.get(1, 2), Some(22.0));
14623 assert_eq!(img.scale, (1.0, 1.0));
14625 assert_eq!(img.origin, (-0.5, -0.5));
14627 }
14628
14629 #[test]
14630 fn scatter_regular_grid_mode_column_major_transposes() {
14631 let mut x = Vec::new();
14635 let mut y = Vec::new();
14636 let mut v = Vec::new();
14637 for c in 0..2 {
14638 for r in 0..3 {
14639 x.push(c as f64);
14640 y.push(r as f64);
14641 v.push((c * 10 + r) as f64); }
14643 }
14644 let img = scatter_grid_image(ScatterVisualization::RegularGrid, &x, &y, &v, (0, 0))
14645 .expect("grid detected");
14646 assert_eq!(img.shape, (3, 2));
14647 assert_eq!(img.get(0, 0), Some(0.0));
14649 assert_eq!(img.get(0, 1), Some(10.0));
14650 assert_eq!(img.get(2, 0), Some(2.0));
14651 assert_eq!(img.get(2, 1), Some(12.0));
14652 }
14653
14654 #[test]
14655 fn scatter_regular_grid_mode_trailing_cells_are_nan() {
14656 let x = [0.0, 1.0, 2.0, 3.0, 4.0];
14661 let y = [0.0, 0.0, 0.0, 0.0, 0.0];
14662 let v = [1.0, 2.0, 3.0, 4.0, 5.0];
14663 let img = scatter_grid_image(ScatterVisualization::RegularGrid, &x, &y, &v, (0, 0))
14664 .expect("line detected");
14665 assert_eq!(img.shape, (1, 5));
14667 assert_eq!(img.get(0, 0), Some(1.0));
14668 assert_eq!(img.get(0, 4), Some(5.0));
14669 }
14670
14671 #[test]
14672 fn scatter_masked_selection_flags_nonzero_levels() {
14673 let mask = [0u8, 1, 0, 3, 0];
14676 assert_eq!(
14677 scatter_masked_selection(&mask),
14678 vec![false, true, false, true, false]
14679 );
14680 assert!(scatter_masked_selection(&[]).is_empty());
14682 }
14683
14684 #[test]
14685 fn scatter_mask_rectangle_selection_applies_to_points() {
14686 let px: Vec<f32> = vec![0.0, 1.0, 2.0, 3.0, 4.0];
14690 let py: Vec<f32> = vec![0.0, 0.0, 0.0, 0.0, 0.0];
14691 let mut mask = crate::widget::scatter_mask::ScatterMaskWidget::new(px.len());
14692 mask.update_rectangle(1, (-0.5, 0.5), (1.0, 2.0), &px, &py, true);
14695 let selection = scatter_masked_selection(&mask.mask);
14696 assert_eq!(selection, vec![false, true, true, false, false]);
14698 }
14699
14700 fn ramp_colormap() -> Colormap {
14704 let mut lut = [[0u8; 4]; 256];
14705 for (i, entry) in lut.iter_mut().enumerate() {
14706 *entry = [i as u8, 255 - i as u8, 0, 255];
14707 }
14708 Colormap::viridis(0.0, 4.0).with_lut(lut)
14709 }
14710
14711 fn ramp_color_at(cmap: &Colormap, v: f64) -> Color32 {
14715 let idx = (cmap.normalize(v) * 255.0).clamp(0.0, 255.0) as usize;
14716 let [r, g, b, a] = cmap.lut[idx];
14717 Color32::from_rgba_unmultiplied(r, g, b, a)
14718 }
14719
14720 #[test]
14721 fn point_colors_maps_values_through_colormap_lut() {
14722 let cmap = ramp_colormap();
14726 let values = [0.0, 2.0, 4.0];
14727 let colors = point_colors(&values, &cmap, None);
14728 assert_eq!(
14729 colors,
14730 vec![
14731 ramp_color_at(&cmap, 0.0),
14732 ramp_color_at(&cmap, 2.0),
14733 ramp_color_at(&cmap, 4.0),
14734 ]
14735 );
14736 assert_eq!(colors[0], Color32::from_rgba_unmultiplied(0, 255, 0, 255));
14738 assert_eq!(colors[2], Color32::from_rgba_unmultiplied(255, 0, 0, 255));
14739 }
14740
14741 #[test]
14742 fn point_colors_composes_per_point_alpha() {
14743 let cmap = ramp_colormap();
14747 let values = [0.0, 2.0, 4.0];
14748 let alpha = [0.5, 0.25, 1.0];
14749
14750 let with_alpha = point_colors(&values, &cmap, Some(&alpha));
14751
14752 let mut expected = point_colors(&values, &cmap, None);
14753 compose_per_point_alpha(&mut expected, &alpha);
14754 assert_eq!(with_alpha, expected);
14755
14756 assert_eq!(with_alpha[0].to_srgba_unmultiplied()[3], 128);
14758 assert_eq!(with_alpha[1].to_srgba_unmultiplied()[3], 64);
14760 assert_eq!(with_alpha[2].to_srgba_unmultiplied()[3], 255);
14762 }
14763
14764 #[test]
14765 fn solid_path_colors_identical_to_points_path() {
14766 let cmap = ramp_colormap();
14771 let values = [0.0, 1.0, 3.0, 4.0];
14772 let alpha = [1.0, 0.5, 0.25, 0.75];
14773
14774 let shared = point_colors(&values, &cmap, Some(&alpha));
14776
14777 let mut points_inline: Vec<Color32> =
14779 values.iter().map(|&v| ramp_color_at(&cmap, v)).collect();
14780 compose_per_point_alpha(&mut points_inline, &alpha);
14781
14782 assert_eq!(shared, points_inline);
14783 }
14784
14785 #[test]
14786 fn solid_path_builds_triangles_with_per_vertex_colors() {
14787 let cmap = ramp_colormap();
14791 let x = [0.0, 4.0, 0.0, 4.0];
14792 let y = [0.0, 0.0, 4.0, 4.0];
14793 let values = [0.0, 2.0, 4.0, 1.0];
14794
14795 let colors = point_colors(&values, &cmap, None);
14796 let tri = crate::core::scatter_viz::solid_triangles(&x, &y, &colors)
14797 .expect("four non-collinear points triangulate");
14798
14799 assert_eq!(tri.colors, colors);
14801 assert_eq!(tri.x, x.to_vec());
14802 assert_eq!(tri.y, y.to_vec());
14803 assert!(
14805 tri.indices.len() >= 2,
14806 "square triangulates into >= 2 triangles, got {}",
14807 tri.indices.len()
14808 );
14809 }
14810
14811 #[test]
14812 fn solid_path_degenerate_input_yields_no_triangles() {
14813 let cmap = ramp_colormap();
14817
14818 let x2 = [0.0, 1.0];
14820 let y2 = [0.0, 1.0];
14821 let c2 = point_colors(&[0.0, 4.0], &cmap, None);
14822 assert!(crate::core::scatter_viz::solid_triangles(&x2, &y2, &c2).is_none());
14823
14824 let xc = [0.0, 1.0, 2.0];
14826 let yc = [0.0, 1.0, 2.0];
14827 let cc = point_colors(&[0.0, 2.0, 4.0], &cmap, None);
14828 assert!(crate::core::scatter_viz::solid_triangles(&xc, &yc, &cc).is_none());
14829 }
14830
14831 #[test]
14832 fn live_curve_stats_match_core_stats_engine() {
14833 use crate::widget::stats_widget::{StatsWidget, UpdateMode};
14836 let xs = vec![0.0, 1.0, 2.0, 3.0, 4.0];
14837 let ys = vec![2.0, -1.0, 5.0, f64::NAN, 0.5];
14838 let data = RetainedItemData::Curve {
14839 x: xs.clone(),
14840 y: ys.clone(),
14841 };
14842 let input = retained_data_to_stats_input(&data);
14843 let mut w = StatsWidget::new();
14844 w.set_update_mode(UpdateMode::Auto);
14845 w.recompute(&[("curve", input)], None);
14846 let rows = w.rows();
14847 assert_eq!(rows.len(), 1);
14848 assert_eq!(rows[0].0, "curve");
14849 let core =
14850 crate::core::stats::Stats::for_curve(&xs, &ys, crate::core::stats::StatScope::All);
14851 assert_eq!(rows[0].1, core);
14852 }
14853
14854 #[test]
14855 fn live_image_stats_match_core_stats_engine() {
14856 use crate::widget::stats_widget::{StatsWidget, UpdateMode};
14859 let pixels = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
14860 let data = RetainedItemData::Image {
14861 data: pixels.clone(),
14862 width: 3,
14863 height: 2,
14864 origin: (0.0, 0.0),
14865 scale: (1.0, 1.0),
14866 colormap: Box::new(Colormap::viridis(0.0, 1.0)),
14867 alpha: 1.0,
14868 interpolation: InterpolationMode::Nearest,
14869 aggregation: AggregationMode::None,
14870 aggregation_block: (1, 1),
14871 alpha_map: None,
14872 };
14873 let input = retained_data_to_stats_input(&data);
14874 let mut w = StatsWidget::new();
14875 w.set_update_mode(UpdateMode::Auto);
14876 w.recompute(&[("image", input)], None);
14877 let rows = w.rows();
14878 assert_eq!(rows.len(), 1);
14879 let core = crate::core::stats::Stats::for_image(
14880 &pixels,
14881 3,
14882 2,
14883 (0.0, 0.0),
14884 (1.0, 1.0),
14885 crate::core::stats::StatScope::All,
14886 );
14887 assert_eq!(rows[0].1, core);
14888 }
14889
14890 #[test]
14891 fn image_display_attrs_round_trip_preserves_every_reupload_field() {
14892 let pixels = [1.0f32, 2.0, 3.0, 4.0];
14901 let am = [0.0f32, 0.25, 0.75, 1.0];
14902 let mut spec = ImageSpec::scalar(2, 2, &pixels, Colormap::viridis(0.0, 4.0));
14903 spec.origin = (5.0, 6.0);
14904 spec.scale = (2.0, 3.0);
14905 spec.alpha = 0.4;
14906 spec.interpolation = InterpolationMode::Linear;
14907 spec.aggregation = AggregationMode::Max;
14908 spec.aggregation_block = (2, 2);
14909 spec.alpha_map = Some(&am);
14910
14911 let retained = image_spec_retained_data(&spec).expect("scalar image retains data");
14912 let attrs = retained
14913 .image_display_attrs()
14914 .expect("image has display attrs");
14915
14916 let mut rebuilt = ImageSpec::scalar(2, 2, &pixels, Colormap::viridis(0.0, 4.0));
14919 assert_eq!(rebuilt.interpolation, InterpolationMode::Nearest);
14920 assert_eq!(rebuilt.aggregation, AggregationMode::None);
14921 assert_eq!(rebuilt.alpha, 1.0);
14922 assert!(rebuilt.alpha_map.is_none());
14923 attrs.apply(&mut rebuilt);
14924
14925 assert_eq!(rebuilt.origin, (5.0, 6.0));
14926 assert_eq!(rebuilt.scale, (2.0, 3.0));
14927 assert_eq!(rebuilt.alpha, 0.4);
14928 assert_eq!(rebuilt.interpolation, InterpolationMode::Linear);
14929 assert_eq!(rebuilt.aggregation, AggregationMode::Max);
14930 assert_eq!(rebuilt.aggregation_block, (2, 2));
14931 assert_eq!(rebuilt.alpha_map, Some(am.as_slice()));
14932 }
14933
14934 #[test]
14935 fn roi_stats_rows_image_match_image_roi_stats_per_roi() {
14936 use crate::widget::roi_stats::image_roi_stats;
14940 let pixels = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
14941 let data = RetainedItemData::Image {
14942 data: pixels.clone(),
14943 width: 3,
14944 height: 2,
14945 origin: (0.0, 0.0),
14946 scale: (1.0, 1.0),
14947 colormap: Box::new(Colormap::viridis(0.0, 1.0)),
14948 alpha: 1.0,
14949 interpolation: InterpolationMode::Nearest,
14950 aggregation: AggregationMode::None,
14951 aggregation_block: (1, 1),
14952 alpha_map: None,
14953 };
14954 let mut named = ManagedRoi::new(Roi::Rect {
14955 x: (0.0, 2.0),
14956 y: (0.0, 1.0),
14957 });
14958 named.name = "left".to_owned();
14959 let rois = vec![
14960 named,
14961 ManagedRoi::new(Roi::Rect {
14962 x: (1.0, 3.0),
14963 y: (0.0, 2.0),
14964 }),
14965 ];
14966
14967 let rows = roi_stats_rows(&rois, &data);
14968 assert_eq!(rows.len(), 2);
14969 assert_eq!(rows[0].label, "left");
14970 assert_eq!(rows[1].label, "ROI 1");
14971
14972 let f32_pixels: Vec<f32> = pixels.iter().map(|&v| v as f32).collect();
14974 for (row, managed) in rows.iter().zip(rois.iter()) {
14975 let expected = image_roi_stats(&managed.roi, &f32_pixels, 3, 2, [0.0, 0.0], [1.0, 1.0]);
14976 assert_eq!(row.stats, expected);
14977 }
14978 }
14979
14980 #[test]
14981 fn roi_stats_rows_curve_match_curve_roi_stats_per_roi() {
14982 use crate::widget::roi_stats::curve_roi_stats;
14985 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
14986 let y = vec![10.0, 20.0, 30.0, 40.0, 50.0];
14987 let data = RetainedItemData::Curve {
14988 x: x.clone(),
14989 y: y.clone(),
14990 };
14991 let rois = vec![
14992 ManagedRoi::new(Roi::VRange { x: (1.0, 3.0) }),
14993 ManagedRoi::new(Roi::Rect {
14994 x: (0.0, 2.0),
14995 y: (0.0, 100.0),
14996 }),
14997 ];
14998
14999 let rows = roi_stats_rows(&rois, &data);
15000 assert_eq!(rows.len(), 2);
15001 for (row, managed) in rows.iter().zip(rois.iter()) {
15002 assert_eq!(row.stats, curve_roi_stats(&managed.roi, &x, &y));
15003 }
15004 }
15005
15006 #[test]
15007 fn roi_stats_rows_empty_without_rois() {
15008 let data = RetainedItemData::Curve {
15010 x: vec![0.0, 1.0],
15011 y: vec![1.0, 2.0],
15012 };
15013 assert!(roi_stats_rows(&[], &data).is_empty());
15014 }
15015
15016 #[test]
15017 fn curve_roi_rows_match_curve_roi_counts_and_skip_non_curve_rois() {
15018 use crate::widget::roi_stats::{curve_roi_counts, roi_x_span};
15022 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
15023 let y = vec![0.0, 0.0, 10.0, 0.0, 0.0];
15024
15025 let mut named = ManagedRoi::new(Roi::VRange { x: (0.0, 4.0) });
15026 named.name = "peak".to_owned();
15027 let rois = vec![
15028 named,
15029 ManagedRoi::new(Roi::HRange { y: (0.0, 5.0) }),
15031 ManagedRoi::new(Roi::Rect {
15033 x: (1.0, 3.0),
15034 y: (-100.0, 100.0),
15035 }),
15036 ];
15037
15038 let rows = curve_roi_rows(&rois, &x, &y);
15039 assert_eq!(rows.len(), 2); assert_eq!(rows[0].label, "peak");
15043 assert_eq!(
15044 (rows[0].from, rows[0].to),
15045 roi_x_span(&rois[0].roi).unwrap()
15046 );
15047 assert_eq!(
15048 rows[0].counts,
15049 curve_roi_counts(&rois[0].roi, &x, &y).unwrap()
15050 );
15051
15052 assert_eq!(rows[1].label, "ROI 2");
15054 assert_eq!(
15055 (rows[1].from, rows[1].to),
15056 roi_x_span(&rois[2].roi).unwrap()
15057 );
15058 assert_eq!(
15059 rows[1].counts,
15060 curve_roi_counts(&rois[2].roi, &x, &y).unwrap()
15061 );
15062 }
15063
15064 #[test]
15065 fn curve_roi_rows_empty_without_rois() {
15066 let x = vec![0.0, 1.0, 2.0];
15068 let y = vec![1.0, 2.0, 3.0];
15069 assert!(curve_roi_rows(&[], &x, &y).is_empty());
15070 }
15071
15072 #[test]
15073 fn fit_target_feeds_curve_xy_not_image() {
15074 let xs = vec![1.0, 2.0, 3.0, 4.0];
15077 let ys = vec![10.0, 20.0, 30.0, 40.0];
15078 let curve = RetainedItemData::Curve {
15079 x: xs.clone(),
15080 y: ys.clone(),
15081 };
15082 let (fx, fy) = retained_curve_xy(&curve).expect("curve feeds its xy");
15083 assert_eq!(fx, xs.as_slice());
15084 assert_eq!(fy, ys.as_slice());
15085
15086 let image = RetainedItemData::Image {
15088 data: vec![1.0, 2.0, 3.0, 4.0],
15089 width: 2,
15090 height: 2,
15091 origin: (0.0, 0.0),
15092 scale: (1.0, 1.0),
15093 colormap: Box::new(Colormap::viridis(0.0, 1.0)),
15094 alpha: 1.0,
15095 interpolation: InterpolationMode::Nearest,
15096 aggregation: AggregationMode::None,
15097 aggregation_block: (1, 1),
15098 alpha_map: None,
15099 };
15100 assert!(retained_curve_xy(&image).is_none());
15101 }
15102
15103 #[test]
15104 fn raw_pixel_stddev3_autoscale_matches_mean_plus_minus_3std() {
15105 let pixels = [-1.0, 0.0, 1.0, f64::NAN];
15111 let base = Colormap::viridis(7.0, 9.0); let cm = autoscaled_colormap(&base, AutoscaleMode::Stddev3, &pixels);
15113 let (evmin, evmax) =
15115 AutoscaleMode::Stddev3.range(&pixels, crate::core::colormap::DEFAULT_PERCENTILES);
15116 assert_eq!(cm.vmin, evmin);
15117 assert_eq!(cm.vmax, evmax);
15118 assert_eq!((cm.vmin, cm.vmax), (-1.0, 1.0));
15119 assert_eq!(cm.lut, base.lut);
15121 }
15122
15123 #[test]
15124 fn raw_pixel_percentile_autoscale_matches_percentile_bounds() {
15125 let pixels = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, f64::NAN];
15128 let mut base = Colormap::viridis(0.0, 1.0);
15129 base.set_autoscale_percentiles(10.0, 90.0);
15130 let cm = autoscaled_colormap(&base, AutoscaleMode::Percentile, &pixels);
15131 let (evmin, evmax) = AutoscaleMode::Percentile.range(&pixels, (10.0, 90.0));
15135 assert_eq!(cm.vmin, evmin);
15136 assert_eq!(cm.vmax, evmax);
15137 assert!((cm.vmin - 0.9).abs() < 1e-12, "vmin {}", cm.vmin);
15138 assert!((cm.vmax - 8.1).abs() < 1e-12, "vmax {}", cm.vmax);
15139 }
15140
15141 #[test]
15142 fn masked_image_yields_nan_at_masked_pixels_before_upload() {
15143 let data = [1.0_f32, 2.0, 3.0, 4.0];
15146 let mut mask = ScalarMask::new(2, 2);
15147 mask.set_mask_data(&[0, 1, 1, 0], 2);
15149 let out = apply_image_mask(2, 2, &data, &mask).unwrap();
15150 assert_eq!(out[0], 1.0);
15151 assert!(out[1].is_nan());
15152 assert!(out[2].is_nan());
15153 assert_eq!(out[3], 4.0);
15154 }
15155
15156 #[test]
15157 fn masked_image_validates_shape() {
15158 let mask = ScalarMask::new(2, 2);
15159 assert_eq!(
15161 apply_image_mask(2, 2, &[1.0, 2.0, 3.0], &mask).unwrap_err(),
15162 PlotDataError::ImageDataLength {
15163 expected: 4,
15164 actual: 3,
15165 }
15166 );
15167 let small = ScalarMask::new(1, 1);
15169 assert_eq!(
15170 apply_image_mask(2, 2, &[1.0, 2.0, 3.0, 4.0], &small).unwrap_err(),
15171 PlotDataError::ImageDataLength {
15172 expected: 4,
15173 actual: 1,
15174 }
15175 );
15176 }
15177
15178 #[test]
15179 fn image_view_paints_mask_only_in_mask_draw_mode() {
15180 for mode in [
15184 PlotInteractionMode::Pan,
15185 PlotInteractionMode::Zoom,
15186 PlotInteractionMode::Select,
15187 ] {
15188 assert!(
15189 !image_view_should_paint_mask(mode, true),
15190 "{mode:?} must not paint even with the mask panel enabled",
15191 );
15192 }
15193 assert!(
15194 !image_view_should_paint_mask(PlotInteractionMode::MaskDraw, false),
15195 "MaskDraw must not paint when the mask panel is disabled",
15196 );
15197 assert!(
15198 image_view_should_paint_mask(PlotInteractionMode::MaskDraw, true),
15199 "MaskDraw with the mask panel enabled is the only painting state",
15200 );
15201 }
15202
15203 #[test]
15204 fn painted_level_buffer_round_trips_to_reupload_mask() {
15205 let levels = [0u8, 1, 0, 0, 7, 0]; let scalar_mask = scalar_mask_from_level_buffer(2, 3, &levels);
15214
15215 assert_eq!(scalar_mask.width(), 2);
15216 assert_eq!(scalar_mask.height(), 3);
15217 for (i, &lvl) in levels.iter().enumerate() {
15219 let col = i % 2;
15220 let row = i / 2;
15221 assert_eq!(
15222 scalar_mask.is_masked(col, row),
15223 lvl != 0,
15224 "pixel ({col},{row}) level {lvl}",
15225 );
15226 }
15227
15228 let pixels = [10.0_f32, 20.0, 30.0, 40.0, 50.0, 60.0];
15231 let masked = scalar_mask.apply(&pixels);
15232 for (i, (&lvl, &out)) in levels.iter().zip(masked.iter()).enumerate() {
15233 if lvl != 0 {
15234 assert!(out.is_nan(), "masked pixel {i} (level {lvl}) must be NaN");
15235 } else {
15236 assert_eq!(out, pixels[i], "unmasked pixel {i} unchanged");
15237 }
15238 }
15239
15240 let mut direct = ScalarMask::new(2, 3);
15243 direct.set_mask_data(&levels, 2);
15244 assert_eq!(scalar_mask, direct);
15245 }
15246
15247 #[test]
15248 fn scalar_mask_from_level_buffer_clips_oversized_to_image_shape() {
15249 let oversized = [1u8; 10]; let mask = scalar_mask_from_level_buffer(2, 2, &oversized);
15254 assert_eq!(mask.width(), 2);
15255 assert_eq!(mask.height(), 2);
15256 assert_eq!(mask.get_mask_data().len(), 4);
15257 assert!(mask.get_mask_data().iter().all(|&m| m != 0));
15258 }
15259
15260 #[test]
15261 fn value_stats_match_core_stats_engine() {
15262 let data = [3.0, -2.0, 7.5, f64::NAN, 0.0, -2.0];
15265 let vs = ValueStats::from_f64(&data);
15266 let core = crate::core::stats::Stats::for_image(
15267 &data,
15268 data.len(),
15269 1,
15270 (0.0, 0.0),
15271 (1.0, 1.0),
15272 crate::core::stats::StatScope::All,
15273 );
15274 assert_eq!(vs.count, core.count);
15275 assert_eq!(vs.finite_count, core.finite_count);
15276 assert_eq!(vs.min, core.min);
15277 assert_eq!(vs.max, core.max);
15278 assert_eq!(vs.mean, core.mean);
15279 }
15280
15281 #[test]
15282 fn mask_rgba_pixels_are_transparent_where_false() {
15283 let color = Color32::from_rgba_unmultiplied(10, 20, 30, 40);
15284 let pixels = mask_rgba_pixels(&[true, false, true], color);
15285 let rgba = color.to_srgba_unmultiplied();
15286 assert_eq!(pixels, vec![rgba, [0, 0, 0, 0], rgba]);
15287 }
15288
15289 #[test]
15290 fn legend_row_width_stays_within_parent_width() {
15291 assert_eq!(legend_row_width(180.0), 180.0);
15292 assert_eq!(legend_row_width(80.0), 80.0);
15293 assert_eq!(legend_row_width(0.0), LEGEND_ROW_MIN_WIDTH);
15294 }
15295
15296 #[test]
15297 fn scatter_visualization_catalog_is_silx_order_with_unique_labels() {
15298 assert_eq!(
15300 ScatterVisualization::ALL,
15301 [
15302 ScatterVisualization::Points,
15303 ScatterVisualization::Solid,
15304 ScatterVisualization::RegularGrid,
15305 ScatterVisualization::IrregularGrid,
15306 ScatterVisualization::BinnedStatistic,
15307 ]
15308 );
15309 assert_eq!(
15311 ScatterVisualization::default(),
15312 ScatterVisualization::Points
15313 );
15314 let labels: Vec<&str> = ScatterVisualization::ALL
15316 .iter()
15317 .map(|m| m.label())
15318 .collect();
15319 let mut deduped = labels.clone();
15320 deduped.sort_unstable();
15321 deduped.dedup();
15322 assert_eq!(labels.len(), deduped.len(), "labels must be unique");
15323 }
15324
15325 #[test]
15326 fn compare_pixel_at_looks_up_origin_aligned_pixel() {
15327 let data = [0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0];
15329 assert_eq!(compare_pixel_at(3, 2, &data, 0.0, 0.0), Some(0.0));
15331 assert_eq!(compare_pixel_at(3, 2, &data, 2.9, 1.9), Some(5.0));
15332 assert_eq!(compare_pixel_at(3, 2, &data, 1.7, 0.2), Some(1.0));
15334 assert_eq!(compare_pixel_at(3, 2, &data, 3.0, 0.0), None);
15336 assert_eq!(compare_pixel_at(3, 2, &data, 0.0, 2.0), None);
15337 assert_eq!(compare_pixel_at(3, 2, &data, -0.1, 0.0), None);
15339 assert_eq!(compare_pixel_at(3, 2, &[], 0.0, 0.0), None);
15341 }
15342
15343 #[test]
15344 fn format_compare_value_matches_silx_status_bar() {
15345 assert_eq!(format_compare_value(true, None), "no image");
15347 assert_eq!(format_compare_value(true, Some(1.0)), "no image");
15348 assert_eq!(format_compare_value(false, None), "NA");
15350 assert_eq!(format_compare_value(false, Some(1.5)), "1.500000");
15352 assert_eq!(format_compare_value(false, Some(0.0)), "0.000000");
15353 }
15354
15355 #[test]
15356 fn margin_image_origin_anchors_top_left() {
15357 let src = [1.0_f32, 2.0, 3.0, 4.0]; let out = margin_image(&src, 2, 2, 4, 3, false);
15362 assert_eq!(
15363 out,
15364 vec![
15365 1.0, 2.0, 0.0, 0.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]
15369 );
15370 }
15371
15372 #[test]
15373 fn margin_image_center_uses_silx_floor_offset() {
15374 let src = [1.0_f32, 2.0, 3.0, 4.0];
15378 let out = margin_image(&src, 2, 2, 4, 4, true);
15379 assert_eq!(
15380 out,
15381 vec![
15382 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 0.0, 0.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]
15387 );
15388 }
15389
15390 #[test]
15391 fn rescale_array_identity_returns_input() {
15392 let src = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]; let out = rescale_array(&src, 3, 2, 3, 2);
15396 assert_eq!(out, src.to_vec());
15397 }
15398
15399 #[test]
15400 fn rescale_array_upscales_bilinearly() {
15401 let src = [0.0_f32, 10.0];
15405 let out = rescale_array(&src, 2, 1, 3, 1);
15406 assert_eq!(out, vec![0.0, 5.0, 10.0]);
15407 }
15408
15409 #[test]
15410 fn align_compare_images_origin_pads_to_max_grid() {
15411 let a = [9.0_f32];
15414 let b = [1.0_f32, 2.0, 3.0, 4.0];
15415 let (d1, d2, cw, ch) = align_compare_images(CompareAlignment::Origin, &a, 1, 1, &b, 2, 2);
15416 assert_eq!((cw, ch), (2, 2));
15417 assert_eq!(d1, vec![9.0, 0.0, 0.0, 0.0]); assert_eq!(d2, vec![1.0, 2.0, 3.0, 4.0]); }
15420
15421 #[test]
15422 fn align_compare_images_center_centers_smaller() {
15423 let a = [7.0_f32];
15426 let b: Vec<f32> = (1..=9).map(|v| v as f32).collect();
15427 let (d1, d2, cw, ch) = align_compare_images(CompareAlignment::Center, &a, 1, 1, &b, 3, 3);
15428 assert_eq!((cw, ch), (3, 3));
15429 assert_eq!(
15430 d1,
15431 vec![0.0, 0.0, 0.0, 0.0, 7.0, 0.0, 0.0, 0.0, 0.0] );
15433 assert_eq!(d2, b); }
15435
15436 #[test]
15437 fn align_compare_images_stretch_resamples_b_to_a_shape() {
15438 let a = [1.0_f32, 2.0, 3.0];
15441 let b = [0.0_f32, 10.0];
15442 let (d1, d2, cw, ch) = align_compare_images(CompareAlignment::Stretch, &a, 3, 1, &b, 2, 1);
15443 assert_eq!((cw, ch), (3, 1));
15444 assert_eq!(d1, a.to_vec());
15445 assert_eq!(d2, vec![0.0, 5.0, 10.0]);
15446 }
15447
15448 #[test]
15449 fn compare_aligned_coords_remaps_per_mode() {
15450 assert_eq!(
15452 compare_aligned_coords(CompareAlignment::Origin, 4.0, 5.0, 2, 2, 6, 6),
15453 ((4.0, 5.0), (4.0, 5.0))
15454 );
15455 assert_eq!(
15458 compare_aligned_coords(CompareAlignment::Center, 3.0, 3.0, 2, 2, 6, 6),
15459 ((1.0, 1.0), (3.0, 3.0))
15460 );
15461 assert_eq!(
15465 compare_aligned_coords(CompareAlignment::Stretch, 2.0, 1.0, 4, 2, 8, 6),
15466 ((2.0, 1.0), (4.0, 3.0))
15467 );
15468 }
15469
15470 #[test]
15471 fn align_summary_classifies_the_sift_transform() {
15472 let id = AffineTransformation {
15473 tx: 0.0,
15474 ty: 0.0,
15475 sx: 1.0,
15476 sy: 1.0,
15477 rotation: 0.0,
15478 };
15479 assert_eq!(align_summary(&id), "No changes");
15480
15481 let tiny = AffineTransformation { tx: 0.001, ..id };
15483 assert_eq!(align_summary(&tiny), "No big changes");
15484
15485 let shift = AffineTransformation {
15487 tx: 4.0,
15488 ty: 3.0,
15489 ..id
15490 };
15491 assert_eq!(align_summary(&shift), "Translation");
15492
15493 let all = AffineTransformation {
15495 tx: 4.0,
15496 ty: 0.0,
15497 sx: 1.2,
15498 sy: 1.0,
15499 rotation: 0.3,
15500 };
15501 assert_eq!(align_summary(&all), "Translation+Scale+Rotation");
15502 }
15503
15504 #[test]
15505 fn align_tooltip_lists_nonidentity_components() {
15506 let id = AffineTransformation {
15507 tx: 0.0,
15508 ty: 0.0,
15509 sx: 1.0,
15510 sy: 1.0,
15511 rotation: 0.0,
15512 };
15513 assert_eq!(align_tooltip(&id), "No transformation");
15514
15515 let t = AffineTransformation {
15516 tx: 4.0,
15517 ty: -2.0,
15518 sx: 1.0,
15519 sy: 1.0,
15520 rotation: std::f64::consts::FRAC_PI_2, };
15522 assert_eq!(
15523 align_tooltip(&t),
15524 "Translation x: 4.000px\nTranslation y: -2.000px\nRotation: 90.000deg"
15525 );
15526 }
15527}