Skip to main content

kithe_plot/controller/
mod.rs

1//! Controller module (MVC "C").
2//!
3//! Responsibilities:
4//! - Accept `Action` values from the view.
5//! - Validate and translate actions into `Command`s.
6//! - Mutate `PlotModel` and data table state.
7//! - Maintain undo/redo stacks.
8//! - Handle import/export workflows.
9//!
10//! Architecture notes:
11//! - Actions are user intents; Controller validates and turns them into Commands.
12//! - Commands are the single source of truth for mutations, enabling undo/redo.
13//! - Export path aims to mirror on-screen layout to avoid visual drift.
14//! - Axis titles are positioned relative to the plotting area, not the full canvas, to avoid
15//!   vertical drift with font-size changes.
16
17pub mod action;
18pub mod command;
19
20pub use action::*;
21pub use command::*;
22
23use std::fmt::{Display, Formatter};
24use std::path::Path;
25
26use crate::model::{
27    AxisConfig, AxisKind, AxesConfig, Color, DataSource, DataTable, LayoutConfig, LegendConfig,
28    LegendPosition, LineStyle, MarkerStyle, PlotModel, RangePolicy, ScaleType, SeriesId,
29    SeriesModel, SeriesStyle, TickConfig, ImageFormat, ImageSize,
30};
31use plotters::coord::Shift;
32use plotters::coord::types::RangedCoordf32;
33use plotters::prelude::*;
34use plotters::style::Color as PlottersColor;
35
36/// EN: UI notification severity.
37/// RU: Uroven uvedomleniya dlya UI.
38#[derive(Clone, Copy)]
39pub enum NotificationLevel {
40    Info,
41    Error,
42}
43
44/// EN: Notification shown in the status block of the editor.
45/// RU: Soobshchenie v statuse redaktora.
46#[derive(Clone)]
47pub struct Notification {
48    pub level: NotificationLevel,
49    pub message: String,
50}
51
52/// EN: Controller-level error for validation and user-safe feedback.
53/// RU: Oshibka kontrollera dlya validatsii i bezopasnoy obratnoy svyazi.
54#[derive(Debug)]
55pub enum ControllerError {
56    EmptyAxisLabel,
57    EmptyChartTitle,
58    InvalidRange { min: f64, max: f64 },
59    InvalidLineWidth(f32),
60    SeriesNotFound(SeriesId),
61    ColumnNotFound(String),
62    DataLoadFailed(String),
63    ExportFailed(String),
64    NoDataLoaded,
65    CannotRemoveLastSeries,
66    UnsupportedAction(&'static str),
67}
68
69impl Display for ControllerError {
70    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
71        match self {
72            ControllerError::EmptyAxisLabel => {
73                write!(f, "Axis label cannot be empty / Podpis osi ne mozhet byt pustoy")
74            }
75            ControllerError::EmptyChartTitle => {
76                write!(f, "Chart title cannot be empty / Zagolovok ne mozhet byt pustym")
77            }
78            ControllerError::InvalidRange { min, max } => write!(
79                f,
80                "Invalid range: min ({min}) must be < max ({max}) / Nekorrektnyy diapazon"
81            ),
82            ControllerError::InvalidLineWidth(width) => write!(
83                f,
84                "Invalid line width: {width} / Nekorrektnaya tolshchina linii"
85            ),
86            ControllerError::SeriesNotFound(id) => {
87                write!(f, "Series {:?} not found / Seriya ne naydena", id)
88            }
89            ControllerError::ColumnNotFound(name) => {
90                write!(f, "Column not found: {name} / Stolbets ne naiden")
91            }
92            ControllerError::DataLoadFailed(err) => {
93                write!(f, "Failed to load data: {err} / Ne udalos zagruzit dannye")
94            }
95            ControllerError::ExportFailed(err) => {
96                write!(f, "Failed to export: {err} / Ne udalos sohranit grafik")
97            }
98            ControllerError::NoDataLoaded => {
99                write!(f, "No data loaded / Dannye ne zagruzheny")
100            }
101            ControllerError::CannotRemoveLastSeries => write!(
102                f,
103                "Cannot remove last series / Nelyzya udalit poslednyuyu seriyu"
104            ),
105            ControllerError::UnsupportedAction(name) => write!(
106                f,
107                "Action is not implemented yet: {name} / Deystvie poka ne realizovano"
108            ),
109        }
110    }
111}
112
113impl std::error::Error for ControllerError {}
114
115/// EN: Main MVC controller. Owns model, data table, command history and notifications.
116/// RU: Glavnyy MVC-kontroller. Hranit model, tablitsu dannyh, istoriyu komand i uvedomleniya.
117pub struct PlotController {
118    pub model: PlotModel,
119    pub undo_stack: Vec<Command>,
120    pub redo_stack: Vec<Command>,
121    data_table: Option<DataTable>,
122    notification: Option<Notification>,
123    next_series_id: u64,
124}
125
126impl PlotController {
127    /// EN: Empty initial state; data is loaded via File menu.
128    /// RU: Pustoe nachalnoe sostoyanie; dannye zagruzhayutsya cherez File menu.
129    pub fn new() -> Self {
130        Self {
131            model: PlotModel::default(),
132            undo_stack: Vec::new(),
133            redo_stack: Vec::new(),
134            data_table: None,
135            notification: Some(Notification {
136                level: NotificationLevel::Info,
137                message: "Ready. Load CSV/TXT from Files menu / Gotovo. Zagruzite CSV/TXT".to_owned(),
138            }),
139            next_series_id: 2,
140        }
141    }
142
143    /// Returns latest notification for status banner.
144    pub fn notification(&self) -> Option<&Notification> {
145        self.notification.as_ref()
146    }
147
148    /// Returns names of all loaded numeric columns.
149    pub fn available_columns(&self) -> Vec<String> {
150        self.data_table
151            .as_ref()
152            .map(|t| t.column_names())
153            .unwrap_or_default()
154    }
155
156    /// Indicates whether any dataset has been loaded.
157    pub fn has_data(&self) -> bool {
158        self.data_table.is_some()
159    }
160
161    /// EN: Public API for embedding editor in another crate.
162    /// RU: Public API dlya vstraivaniya redaktora v drugoy crate.
163    pub fn load_from_data_source(
164        &mut self,
165        source: &dyn DataSource,
166    ) -> Result<(), ControllerError> {
167        let table = DataTable::from_data_source(source).map_err(ControllerError::DataLoadFailed)?;
168        self.set_table(table)
169    }
170
171    /// Returns currently selected `(x, y)` points for a series.
172    pub fn points_for_series(&self, series_id: SeriesId) -> Result<Vec<(f32, f32)>, ControllerError> {
173        let table = self.data_table.as_ref().ok_or(ControllerError::NoDataLoaded)?;
174        let series = self.find_series(series_id)?;
175        table
176            .points_for_columns(&series.x_column, &series.y_column)
177            .map_err(ControllerError::DataLoadFailed)
178    }
179
180    /// Main action dispatcher invoked from the view each frame.
181    pub fn dispatch(&mut self, action: Action) -> Result<(), ControllerError> {
182        let result = match action {
183            Action::ImportFromCsv { path } => self.import_csv(&path),
184            Action::ImportFromTxt { path } => self.import_txt(&path),
185            Action::SetChartTitle(new_title) => {
186                let trimmed = new_title.trim().to_owned();
187                if trimmed.is_empty() {
188                    return self.fail(ControllerError::EmptyChartTitle);
189                }
190                self.execute_command(Command::SetChartTitle {
191                    old: self.model.layout.title.clone(),
192                    new: trimmed,
193                })
194            }
195            Action::SetAxisLabel { axis, label } => {
196                let trimmed = label.trim().to_owned();
197                if trimmed.is_empty() {
198                    return self.fail(ControllerError::EmptyAxisLabel);
199                }
200                self.execute_command(Command::SetAxisLabel {
201                    axis,
202                    old: self.axis(axis).label.clone(),
203                    new: trimmed,
204                })
205            }
206            Action::SetAxisLabelFontSize { axis, font_size } => {
207                self.execute_command(Command::SetAxisLabelFontSize {
208                    axis,
209                    old: self.axis(axis).label_font_size,
210                    new: font_size.max(8),
211                })
212            }
213            Action::SetAxisTitleFontSize { axis, font_size } => {
214                self.execute_command(Command::SetAxisTitleFontSize {
215                    axis,
216                    old: self.axis(axis).axis_title_font_size,
217                    new: font_size.max(8),
218                })
219            }
220            Action::SetAxisScale { axis, scale } => self.execute_command(Command::SetAxisScale {
221                axis,
222                old: self.axis(axis).scale,
223                new: scale,
224            }),
225            Action::SetAxisRange { axis, range } => {
226                if let RangePolicy::Manual { min, max } = range {
227                    if min >= max {
228                        return self.fail(ControllerError::InvalidRange { min, max });
229                    }
230                }
231                self.execute_command(Command::SetAxisRange {
232                    axis,
233                    old: self.axis(axis).range.clone(),
234                    new: range,
235                })
236            }
237            Action::SetAxisMajorTickStep { axis, step } => {
238                self.execute_command(Command::SetAxisMajorTickStep {
239                    axis,
240                    old: self.axis(axis).ticks.major_step,
241                    new: step,
242                })
243            }
244            Action::SetAxisMinorTicks { axis, per_major } => {
245                self.execute_command(Command::SetAxisMinorTicks {
246                    axis,
247                    old: self.axis(axis).ticks.minor_per_major,
248                    new: per_major,
249                })
250            }
251            Action::SetLegendVisible(visible) => {
252                let mut next = self.model.legend.clone();
253                next.visible = visible;
254                self.execute_command(Command::ReplaceLegend {
255                    old: self.model.legend.clone(),
256                    new: next,
257                })
258            }
259            Action::SetLegendTitle(title) => {
260                let mut next = self.model.legend.clone();
261                next.title = title.map(|v| v.trim().to_owned()).filter(|v| !v.is_empty());
262                self.execute_command(Command::ReplaceLegend {
263                    old: self.model.legend.clone(),
264                    new: next,
265                })
266            }
267            Action::SetLegendPosition(position) => {
268                let mut next = self.model.legend.clone();
269                next.position = position;
270                self.execute_command(Command::ReplaceLegend {
271                    old: self.model.legend.clone(),
272                    new: next,
273                })
274            }
275            Action::SetLegendFontSize(font_size) => {
276                let mut next = self.model.legend.clone();
277                next.font_size = font_size.max(8);
278                self.execute_command(Command::ReplaceLegend {
279                    old: self.model.legend.clone(),
280                    new: next,
281                })
282            }
283            Action::SetLegendFontColor(color) => {
284                let mut next = self.model.legend.clone();
285                next.font_color = color;
286                self.execute_command(Command::ReplaceLegend {
287                    old: self.model.legend.clone(),
288                    new: next,
289                })
290            }
291            Action::SetLayoutMargin(margin) => {
292                let mut next = self.model.layout.clone();
293                next.margin = margin;
294                self.execute_command(Command::ReplaceLayout {
295                    old: self.model.layout.clone(),
296                    new: next,
297                })
298            }
299            Action::SetXLabelAreaSize(size) => {
300                let mut next = self.model.layout.clone();
301                next.x_label_area_size = size;
302                self.execute_command(Command::ReplaceLayout {
303                    old: self.model.layout.clone(),
304                    new: next,
305                })
306            }
307            Action::SetYLabelAreaSize(size) => {
308                let mut next = self.model.layout.clone();
309                next.y_label_area_size = size;
310                self.execute_command(Command::ReplaceLayout {
311                    old: self.model.layout.clone(),
312                    new: next,
313                })
314            }
315            Action::SetLabelFontSize(size) => {
316                let mut next = self.model.layout.clone();
317                next.title_font_size = size.max(8);
318                self.execute_command(Command::ReplaceLayout {
319                    old: self.model.layout.clone(),
320                    new: next,
321                })
322            }
323            Action::SetLabelFontColor(color) => {
324                let mut next = self.model.layout.clone();
325                next.title_font_color = color;
326                self.execute_command(Command::ReplaceLayout {
327                    old: self.model.layout.clone(),
328                    new: next,
329                })
330            }
331            Action::AddSeries { name, x_column, y_column } => {
332                let (default_x, default_y) = self.default_xy_columns()?;
333                let x_final = if x_column.is_empty() { default_x } else { x_column };
334                let y_final = if y_column.is_empty() { default_y } else { y_column };
335                self.ensure_column_exists(&x_final)?;
336                self.ensure_column_exists(&y_final)?;
337
338                let series = SeriesModel {
339                    id: SeriesId(self.next_series_id),
340                    name: if name.trim().is_empty() {
341                        format!("Series {}", self.next_series_id)
342                    } else {
343                        name
344                    },
345                    x_column: x_final,
346                    y_column: y_final,
347                    style: SeriesStyle {
348                        color: self.color_for_series(self.next_series_id),
349                        line_width: 2.0,
350                        line_style: LineStyle::Solid,
351                        marker: None,
352                    },
353                    visible: true,
354                };
355                self.next_series_id += 1;
356                self.execute_command(Command::AddSeries {
357                    series,
358                    index: self.model.series.len(),
359                })
360            }
361            Action::RemoveSeries { series_id } => {
362                if self.model.series.len() <= 1 {
363                    return self.fail(ControllerError::CannotRemoveLastSeries);
364                }
365                let Some((index, series)) = self.find_series_index(series_id) else {
366                    return self.fail(ControllerError::SeriesNotFound(series_id));
367                };
368                self.execute_command(Command::RemoveSeries { series, index })
369            }
370            Action::RenameSeries { series_id, name } => {
371                let old = self.find_series(series_id)?.name.clone();
372                self.execute_command(Command::RenameSeries {
373                    series_id,
374                    old,
375                    new: name,
376                })
377            }
378            Action::SetSeriesVisibility { series_id, visible } => {
379                let old = self.find_series(series_id)?.visible;
380                self.execute_command(Command::SetSeriesVisibility {
381                    series_id,
382                    old,
383                    new: visible,
384                })
385            }
386            Action::SetSeriesXColumn { series_id, x_column } => {
387                self.ensure_column_exists(&x_column)?;
388                let old = self.find_series(series_id)?.x_column.clone();
389                self.execute_command(Command::SetSeriesXColumn {
390                    series_id,
391                    old,
392                    new: x_column,
393                })
394            }
395            Action::SetSeriesYColumn { series_id, y_column } => {
396                self.ensure_column_exists(&y_column)?;
397                let old = self.find_series(series_id)?.y_column.clone();
398                self.execute_command(Command::SetSeriesYColumn {
399                    series_id,
400                    old,
401                    new: y_column,
402                })
403            }
404            Action::SetSeriesColor { series_id, color } => {
405                let old = self.find_series(series_id)?.style.color;
406                self.execute_command(Command::SetSeriesColor {
407                    series_id,
408                    old,
409                    new: color,
410                })
411            }
412            Action::SetSeriesLineWidth { series_id, width } => {
413                if width <= 0.0 {
414                    return self.fail(ControllerError::InvalidLineWidth(width));
415                }
416                let old = self.find_series(series_id)?.style.line_width;
417                self.execute_command(Command::SetSeriesLineWidth {
418                    series_id,
419                    old,
420                    new: width,
421                })
422            }
423            Action::SetSeriesLineStyle { series_id, line_style } => {
424                let old = self.find_series(series_id)?.style.line_style;
425                self.execute_command(Command::SetSeriesLineStyle {
426                    series_id,
427                    old,
428                    new: line_style,
429                })
430            }
431            Action::SetSeriesMarker { series_id, marker, size } => {
432                let old = self.find_series(series_id)?.style.marker.clone();
433                let new = marker.map(|shape| MarkerStyle { shape, size });
434                self.execute_command(Command::SetSeriesMarker {
435                    series_id,
436                    old,
437                    new,
438                })
439            }
440            Action::RequestSaveAs { path, format, size } => self.export_plot(&path, format, size),
441            Action::Undo => self.undo(),
442            Action::Redo => self.redo(),
443            other => self.fail(ControllerError::UnsupportedAction(match other {
444                Action::ResetPlot => "ResetPlot",
445                _ => "UnknownAction",
446            })),
447        };
448
449        if result.is_ok() {
450            self.notification = Some(Notification {
451                level: NotificationLevel::Info,
452                message: "Updated / Obnovleno".to_owned(),
453            });
454        }
455
456        result
457    }
458
459    fn import_csv(&mut self, path: &str) -> Result<(), ControllerError> {
460        let table = DataTable::from_csv_path(Path::new(path)).map_err(ControllerError::DataLoadFailed)?;
461        self.set_table(table)
462    }
463
464    fn import_txt(&mut self, path: &str) -> Result<(), ControllerError> {
465        let table = DataTable::from_txt_path(Path::new(path)).map_err(ControllerError::DataLoadFailed)?;
466        self.set_table(table)
467    }
468
469    fn set_table(&mut self, table: DataTable) -> Result<(), ControllerError> {
470        let names = table.column_names();
471        if names.len() < 2 {
472            return self.fail(ControllerError::DataLoadFailed(
473                "Need at least two numeric columns".to_owned(),
474            ));
475        }
476        self.data_table = Some(table);
477        let x = names[0].clone();
478        let y = names[1].clone();
479
480        if self.model.series.is_empty() {
481            self.model.series.push(SeriesModel {
482                id: SeriesId(1),
483                name: "Series 1".to_owned(),
484                x_column: x,
485                y_column: y,
486                style: SeriesStyle {
487                    color: Color {
488                        r: 220,
489                        g: 50,
490                        b: 47,
491                        a: 255,
492                    },
493                    line_width: 2.0,
494                    line_style: LineStyle::Solid,
495                    marker: None,
496                },
497                visible: true,
498            });
499        } else {
500            self.model.series[0].x_column = x;
501            self.model.series[0].y_column = y;
502        }
503
504        self.notification = Some(Notification {
505            level: NotificationLevel::Info,
506            message: "Data imported / Dannye zagruzheny".to_owned(),
507        });
508        Ok(())
509    }
510
511    fn default_xy_columns(&self) -> Result<(String, String), ControllerError> {
512        let table = self.data_table.as_ref().ok_or(ControllerError::NoDataLoaded)?;
513        let names = table.column_names();
514        if names.len() < 2 {
515            return Err(ControllerError::DataLoadFailed(
516                "Need at least two columns".to_owned(),
517            ));
518        }
519        Ok((names[0].clone(), names[1].clone()))
520    }
521
522    fn ensure_column_exists(&self, name: &str) -> Result<(), ControllerError> {
523        let table = self.data_table.as_ref().ok_or(ControllerError::NoDataLoaded)?;
524        if table.has_column(name) {
525            Ok(())
526        } else {
527            Err(ControllerError::ColumnNotFound(name.to_owned()))
528        }
529    }
530
531    fn export_plot(
532        &self,
533        path: &str,
534        format: ImageFormat,
535        size: ImageSize,
536    ) -> Result<(), ControllerError> {
537        match format {
538            ImageFormat::Png => {
539                let backend = BitMapBackend::new(path, (size.width, size.height));
540                let root = backend.into_drawing_area();
541                self.draw_export_chart(&root)?;
542                root.present()
543                    .map_err(|e| ControllerError::ExportFailed(e.to_string()))?;
544            }
545            ImageFormat::Svg => {
546                let backend = SVGBackend::new(path, (size.width, size.height));
547                let root = backend.into_drawing_area();
548                self.draw_export_chart(&root)?;
549                root.present()
550                    .map_err(|e| ControllerError::ExportFailed(e.to_string()))?;
551            }
552        }
553        Ok(())
554    }
555
556    fn draw_export_chart<DB: DrawingBackend>(
557        &self,
558        root: &DrawingArea<DB, Shift>,
559    ) -> Result<(), ControllerError> {
560        root.fill(&WHITE)
561            .map_err(|e| ControllerError::ExportFailed(e.to_string()))?;
562
563        if !self.has_data() {
564            return Err(ControllerError::NoDataLoaded);
565        }
566
567        let mut rendered = Vec::new();
568        for series in &self.model.series {
569            if !series.visible {
570                continue;
571            }
572            let points = self.points_for_series(series.id)?;
573            let scaled = points
574                .iter()
575                .copied()
576                .filter_map(|(x, y)| apply_scale(x, y, self.model.axes.x.scale, self.model.axes.y.scale))
577                .collect::<Vec<_>>();
578            rendered.push((series, scaled));
579        }
580
581        let x_range = resolve_range(&self.model.axes.x.range, &rendered, true, -1.0..1.0);
582        let y_range = resolve_range(&self.model.axes.y.range, &rendered, false, -1.0..1.0);
583
584        let effective_x_label_area = self
585            .model
586            .layout
587            .x_label_area_size
588            .max(self.model.axes.x.label_font_size + 18)
589            .max(self.model.axes.x.axis_title_font_size + 20);
590        // Use conservative y-label area to avoid overlap between Y tick labels and Y-axis title
591        let effective_y_label_area = self
592            .model
593            .layout
594            .y_label_area_size
595            .max(((self.model.axes.y.label_font_size as f32 * 1.6) as u32) + 16)
596            .max(self.model.axes.y.axis_title_font_size + 28)
597            .max(self.model.axes.y.label_font_size + self.model.axes.y.axis_title_font_size + 28);
598
599        let mut chart = ChartBuilder::on(root)
600            .caption(
601                self.model.layout.title.clone(),
602                ("sans-serif", self.model.layout.title_font_size)
603                    .into_font()
604                    .color(&RGBColor(
605                        self.model.layout.title_font_color.r,
606                        self.model.layout.title_font_color.g,
607                        self.model.layout.title_font_color.b,
608                    )),
609            )
610            .margin(self.model.layout.margin)
611            .x_label_area_size(effective_x_label_area)
612            .y_label_area_size(effective_y_label_area)
613            .build_cartesian_2d(x_range.clone(), y_range.clone())
614            .map_err(|e| ControllerError::ExportFailed(e.to_string()))?;
615
616        configure_mesh(
617            &mut chart,
618            self.model.axes.x.label_font_size,
619            self.model.axes.y.label_font_size,
620            &self.model.axes.x.ticks,
621            &self.model.axes.y.ticks,
622            x_range,
623            y_range,
624        )?;
625        draw_axis_titles(
626            root,
627            &format!(
628                "{}{}",
629                self.model.axes.x.label,
630                scale_suffix(self.model.axes.x.scale)
631            ),
632            &format!(
633                "{}{}",
634                self.model.axes.y.label,
635                scale_suffix(self.model.axes.y.scale)
636            ),
637            self.model.axes.x.axis_title_font_size,
638            self.model.axes.y.axis_title_font_size,
639            effective_x_label_area,
640            effective_y_label_area,
641            self.model.layout.title_font_size,
642            self.model.layout.margin,
643        )?;
644
645        for (series, points) in &rendered {
646            if points.is_empty() {
647                continue;
648            }
649            let color = RGBColor(series.style.color.r, series.style.color.g, series.style.color.b);
650            let style = ShapeStyle::from(&color).stroke_width(series.style.line_width.max(1.0) as u32);
651
652            if series.style.line_style == LineStyle::Dotted {
653                chart
654                    .draw_series(points.iter().map(|(x, y)| Circle::new((*x, *y), 2, style.filled())))
655                    .map_err(|e| ControllerError::ExportFailed(e.to_string()))?;
656            } else {
657                let drawn = chart
658                    .draw_series(LineSeries::new(points.iter().copied(), style))
659                    .map_err(|e| ControllerError::ExportFailed(e.to_string()))?;
660                if self.model.legend.visible {
661                    drawn
662                        .label(series.name.clone())
663                        .legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color));
664                }
665            }
666        }
667
668        if self.model.legend.visible {
669            chart
670                .configure_series_labels()
671                .label_font(
672                    ("sans-serif", self.model.legend.font_size)
673                        .into_font()
674                        .color(&RGBColor(
675                            self.model.legend.font_color.r,
676                            self.model.legend.font_color.g,
677                            self.model.legend.font_color.b,
678                        )),
679                )
680                .position(series_label_position(self.model.legend.position))
681                .background_style(WHITE.mix(0.8))
682                .border_style(BLACK)
683                .draw()
684                .map_err(|e| ControllerError::ExportFailed(e.to_string()))?;
685        }
686
687        Ok(())
688    }
689
690    fn undo(&mut self) -> Result<(), ControllerError> {
691        if let Some(cmd) = self.undo_stack.pop() {
692            self.apply_inverse_command(&cmd);
693            self.redo_stack.push(cmd);
694            Ok(())
695        } else {
696            self.notification = Some(Notification {
697                level: NotificationLevel::Info,
698                message: "Nothing to undo / Nechego otmenyat".to_owned(),
699            });
700            Ok(())
701        }
702    }
703
704    fn redo(&mut self) -> Result<(), ControllerError> {
705        if let Some(cmd) = self.redo_stack.pop() {
706            self.apply_command(&cmd);
707            self.undo_stack.push(cmd);
708            Ok(())
709        } else {
710            self.notification = Some(Notification {
711                level: NotificationLevel::Info,
712                message: "Nothing to redo / Nechego povtoryat".to_owned(),
713            });
714            Ok(())
715        }
716    }
717
718    fn fail<T>(&mut self, error: ControllerError) -> Result<T, ControllerError> {
719        self.notification = Some(Notification {
720            level: NotificationLevel::Error,
721            message: error.to_string(),
722        });
723        Err(error)
724    }
725
726    fn execute_command(&mut self, command: Command) -> Result<(), ControllerError> {
727        self.apply_command(&command);
728        self.undo_stack.push(command);
729        self.redo_stack.clear();
730        Ok(())
731    }
732
733    fn apply_command(&mut self, command: &Command) {
734        match command {
735            Command::SetChartTitle { new, .. } => self.model.layout.title = new.clone(),
736            Command::SetAxisLabel { axis, new, .. } => self.axis_mut(*axis).label = new.clone(),
737            Command::SetAxisLabelFontSize { axis, new, .. } => {
738                self.axis_mut(*axis).label_font_size = *new
739            }
740            Command::SetAxisTitleFontSize { axis, new, .. } => {
741                self.axis_mut(*axis).axis_title_font_size = *new
742            }
743            Command::SetAxisScale { axis, new, .. } => self.axis_mut(*axis).scale = *new,
744            Command::SetAxisRange { axis, new, .. } => self.axis_mut(*axis).range = new.clone(),
745            Command::SetAxisMajorTickStep { axis, new, .. } => {
746                self.axis_mut(*axis).ticks.major_step = *new
747            }
748            Command::SetAxisMinorTicks { axis, new, .. } => {
749                self.axis_mut(*axis).ticks.minor_per_major = *new
750            }
751            Command::ReplaceAxisConfig { axis, new, .. } => *self.axis_mut(*axis) = new.clone(),
752            Command::AddSeries { series, index } => self.model.series.insert(*index, series.clone()),
753            Command::RemoveSeries { index, .. } => {
754                self.model.series.remove(*index);
755            }
756            Command::RenameSeries { series_id, new, .. } => {
757                if let Some(s) = self.find_series_mut_raw(*series_id) {
758                    s.name = new.clone();
759                }
760            }
761            Command::SetSeriesVisibility { series_id, new, .. } => {
762                if let Some(s) = self.find_series_mut_raw(*series_id) {
763                    s.visible = *new;
764                }
765            }
766            Command::SetSeriesXColumn { series_id, new, .. } => {
767                if let Some(s) = self.find_series_mut_raw(*series_id) {
768                    s.x_column = new.clone();
769                }
770            }
771            Command::SetSeriesYColumn { series_id, new, .. } => {
772                if let Some(s) = self.find_series_mut_raw(*series_id) {
773                    s.y_column = new.clone();
774                }
775            }
776            Command::SetSeriesLineWidth { series_id, new, .. } => {
777                if let Some(s) = self.find_series_mut_raw(*series_id) {
778                    s.style.line_width = *new;
779                }
780            }
781            Command::SetSeriesLineStyle { series_id, new, .. } => {
782                if let Some(s) = self.find_series_mut_raw(*series_id) {
783                    s.style.line_style = *new;
784                }
785            }
786            Command::SetSeriesColor { series_id, new, .. } => {
787                if let Some(s) = self.find_series_mut_raw(*series_id) {
788                    s.style.color = *new;
789                }
790            }
791            Command::SetSeriesMarker { series_id, new, .. } => {
792                if let Some(s) = self.find_series_mut_raw(*series_id) {
793                    s.style.marker = new.clone();
794                }
795            }
796            Command::ReplaceLegend { new, .. } => self.model.legend = new.clone(),
797            Command::ReplaceLayout { new, .. } => self.model.layout = new.clone(),
798            Command::Batch { commands } => {
799                for c in commands {
800                    self.apply_command(c);
801                }
802            }
803        }
804    }
805
806    fn apply_inverse_command(&mut self, command: &Command) {
807        match command {
808            Command::SetChartTitle { old, .. } => self.model.layout.title = old.clone(),
809            Command::SetAxisLabel { axis, old, .. } => self.axis_mut(*axis).label = old.clone(),
810            Command::SetAxisLabelFontSize { axis, old, .. } => {
811                self.axis_mut(*axis).label_font_size = *old
812            }
813            Command::SetAxisTitleFontSize { axis, old, .. } => {
814                self.axis_mut(*axis).axis_title_font_size = *old
815            }
816            Command::SetAxisScale { axis, old, .. } => self.axis_mut(*axis).scale = *old,
817            Command::SetAxisRange { axis, old, .. } => self.axis_mut(*axis).range = old.clone(),
818            Command::SetAxisMajorTickStep { axis, old, .. } => self.axis_mut(*axis).ticks.major_step = *old,
819            Command::SetAxisMinorTicks { axis, old, .. } => self.axis_mut(*axis).ticks.minor_per_major = *old,
820            Command::ReplaceAxisConfig { axis, old, .. } => *self.axis_mut(*axis) = old.clone(),
821            Command::AddSeries { index, .. } => {
822                self.model.series.remove(*index);
823            }
824            Command::RemoveSeries { series, index } => self.model.series.insert(*index, series.clone()),
825            Command::RenameSeries { series_id, old, .. } => {
826                if let Some(s) = self.find_series_mut_raw(*series_id) {
827                    s.name = old.clone();
828                }
829            }
830            Command::SetSeriesVisibility { series_id, old, .. } => {
831                if let Some(s) = self.find_series_mut_raw(*series_id) {
832                    s.visible = *old;
833                }
834            }
835            Command::SetSeriesXColumn { series_id, old, .. } => {
836                if let Some(s) = self.find_series_mut_raw(*series_id) {
837                    s.x_column = old.clone();
838                }
839            }
840            Command::SetSeriesYColumn { series_id, old, .. } => {
841                if let Some(s) = self.find_series_mut_raw(*series_id) {
842                    s.y_column = old.clone();
843                }
844            }
845            Command::SetSeriesLineWidth { series_id, old, .. } => {
846                if let Some(s) = self.find_series_mut_raw(*series_id) {
847                    s.style.line_width = *old;
848                }
849            }
850            Command::SetSeriesLineStyle { series_id, old, .. } => {
851                if let Some(s) = self.find_series_mut_raw(*series_id) {
852                    s.style.line_style = *old;
853                }
854            }
855            Command::SetSeriesColor { series_id, old, .. } => {
856                if let Some(s) = self.find_series_mut_raw(*series_id) {
857                    s.style.color = *old;
858                }
859            }
860            Command::SetSeriesMarker { series_id, old, .. } => {
861                if let Some(s) = self.find_series_mut_raw(*series_id) {
862                    s.style.marker = old.clone();
863                }
864            }
865            Command::ReplaceLegend { old, .. } => self.model.legend = old.clone(),
866            Command::ReplaceLayout { old, .. } => self.model.layout = old.clone(),
867            Command::Batch { commands } => {
868                for c in commands.iter().rev() {
869                    self.apply_inverse_command(c);
870                }
871            }
872        }
873    }
874
875    fn axis(&self, axis: AxisKind) -> &AxisConfig {
876        match axis {
877            AxisKind::X => &self.model.axes.x,
878            AxisKind::Y => &self.model.axes.y,
879        }
880    }
881
882    fn axis_mut(&mut self, axis: AxisKind) -> &mut AxisConfig {
883        match axis {
884            AxisKind::X => &mut self.model.axes.x,
885            AxisKind::Y => &mut self.model.axes.y,
886        }
887    }
888
889    fn find_series(&self, series_id: SeriesId) -> Result<&SeriesModel, ControllerError> {
890        self.model
891            .series
892            .iter()
893            .find(|s| s.id == series_id)
894            .ok_or(ControllerError::SeriesNotFound(series_id))
895    }
896
897    fn find_series_mut_raw(&mut self, series_id: SeriesId) -> Option<&mut SeriesModel> {
898        self.model.series.iter_mut().find(|s| s.id == series_id)
899    }
900
901    fn find_series_index(&self, series_id: SeriesId) -> Option<(usize, SeriesModel)> {
902        self.model
903            .series
904            .iter()
905            .enumerate()
906            .find(|(_, s)| s.id == series_id)
907            .map(|(idx, s)| (idx, s.clone()))
908    }
909
910    fn color_for_series(&self, n: u64) -> Color {
911        match n % 6 {
912            0 => Color { r: 220, g: 50, b: 47, a: 255 },
913            1 => Color { r: 38, g: 139, b: 210, a: 255 },
914            2 => Color { r: 133, g: 153, b: 0, a: 255 },
915            3 => Color { r: 203, g: 75, b: 22, a: 255 },
916            4 => Color { r: 42, g: 161, b: 152, a: 255 },
917            _ => Color { r: 108, g: 113, b: 196, a: 255 },
918        }
919    }
920}
921
922impl Default for PlotModel {
923    fn default() -> Self {
924        Self {
925            axes: AxesConfig {
926                x: AxisConfig {
927                    label: "X".to_owned(),
928                    axis_title_font_size: 18,
929                    label_font_size: 16,
930                    scale: ScaleType::Linear,
931                    range: RangePolicy::Auto,
932                    ticks: TickConfig {
933                        major_step: None,
934                        minor_per_major: 4,
935                    },
936                },
937                y: AxisConfig {
938                    label: "Y".to_owned(),
939                    axis_title_font_size: 18,
940                    label_font_size: 16,
941                    scale: ScaleType::Linear,
942                    range: RangePolicy::Auto,
943                    ticks: TickConfig {
944                        major_step: None,
945                        minor_per_major: 4,
946                    },
947                },
948            },
949            series: vec![SeriesModel {
950                id: SeriesId(1),
951                name: "Series 1".to_owned(),
952                x_column: String::new(),
953                y_column: String::new(),
954                style: SeriesStyle {
955                    color: Color {
956                        r: 220,
957                        g: 50,
958                        b: 47,
959                        a: 255,
960                    },
961                    line_width: 2.0,
962                    line_style: LineStyle::Solid,
963                    marker: None,
964                },
965                visible: true,
966            }],
967            legend: LegendConfig {
968                visible: true,
969                title: Some("Legend".to_owned()),
970                position: LegendPosition::TopRight,
971                font_size: 16,
972                font_color: Color {
973                    r: 20,
974                    g: 20,
975                    b: 20,
976                    a: 255,
977                },
978            },
979            layout: LayoutConfig {
980                title: "Plot".to_owned(),
981                x_label_area_size: 35,
982                y_label_area_size: 35,
983                margin: 8,
984                title_font_size: 24,
985                title_font_color: Color {
986                    r: 20,
987                    g: 20,
988                    b: 20,
989                    a: 255,
990                },
991            },
992        }
993    }
994}
995
996fn resolve_range(
997    policy: &RangePolicy,
998    data: &[(&SeriesModel, Vec<(f32, f32)>)],
999    is_x: bool,
1000    fallback: std::ops::Range<f32>,
1001) -> std::ops::Range<f32> {
1002    match policy {
1003        RangePolicy::Manual { min, max } => (*min as f32)..(*max as f32),
1004        RangePolicy::Auto => {
1005            let mut min_v = f32::INFINITY;
1006            let mut max_v = f32::NEG_INFINITY;
1007            for (_, points) in data {
1008                for (x, y) in points {
1009                    let v = if is_x { *x } else { *y };
1010                    min_v = min_v.min(v);
1011                    max_v = max_v.max(v);
1012                }
1013            }
1014            if !min_v.is_finite() || !max_v.is_finite() || min_v >= max_v {
1015                return fallback;
1016            }
1017            let pad = ((max_v - min_v) * 0.05).max(0.1);
1018            (min_v - pad)..(max_v + pad)
1019        }
1020    }
1021}
1022
1023fn apply_scale(x: f32, y: f32, x_scale: ScaleType, y_scale: ScaleType) -> Option<(f32, f32)> {
1024    let sx = match x_scale {
1025        ScaleType::Linear => Some(x),
1026        ScaleType::Log10 => (x > 0.0).then(|| x.log10()),
1027        ScaleType::LogE => (x > 0.0).then(|| x.ln()),
1028    }?;
1029    let sy = match y_scale {
1030        ScaleType::Linear => Some(y),
1031        ScaleType::Log10 => (y > 0.0).then(|| y.log10()),
1032        ScaleType::LogE => (y > 0.0).then(|| y.ln()),
1033    }?;
1034    Some((sx, sy))
1035}
1036
1037fn configure_mesh<DB: DrawingBackend>(
1038    chart: &mut ChartContext<'_, DB, Cartesian2d<RangedCoordf32, RangedCoordf32>>,
1039    x_label_font_size: u32,
1040    y_label_font_size: u32,
1041    x_ticks: &TickConfig,
1042    y_ticks: &TickConfig,
1043    x_range: std::ops::Range<f32>,
1044    y_range: std::ops::Range<f32>,
1045) -> Result<(), ControllerError> {
1046    let x_labels = labels_from_step(x_range, x_ticks.major_step).unwrap_or(10);
1047    let y_labels = labels_from_step(y_range, y_ticks.major_step).unwrap_or(10);
1048
1049    chart
1050        .configure_mesh()
1051        .x_desc("")
1052        .y_desc("")
1053        .x_label_style(("sans-serif", x_label_font_size))
1054        .y_label_style(("sans-serif", y_label_font_size))
1055        .x_labels(x_labels)
1056        .y_labels(y_labels)
1057        .max_light_lines(x_ticks.minor_per_major.max(y_ticks.minor_per_major) as usize)
1058        .draw()
1059        .map_err(|e| ControllerError::ExportFailed(e.to_string()))
1060}
1061
1062fn draw_axis_titles<DB: DrawingBackend>(
1063    area: &DrawingArea<DB, Shift>,
1064    x_label: &str,
1065    y_label: &str,
1066    x_font_size: u32,
1067    y_font_size: u32,
1068    x_label_area: u32,
1069    y_label_area: u32,
1070    title_font_size: u32,
1071    margin: u32,
1072) -> Result<(), ControllerError> {
1073    // Anchor axis titles to the plotting area to avoid vertical drift with changing fonts
1074    let (w, h) = area.dim_in_pixel();
1075    let x_style = ("sans-serif", x_font_size.max(8)).into_font().color(&BLACK);
1076    let y_style = ("sans-serif", y_font_size.max(8))
1077        .into_font()
1078        .transform(plotters::style::FontTransform::Rotate270)
1079        .color(&BLACK);
1080
1081    let cap_h = (title_font_size as i32 + 10).max(12);
1082    let m = margin as i32;
1083    let top_y = cap_h + m;
1084    let bottom_y = h as i32 - (x_label_area as i32) - m;
1085    let plot_center_y = (top_y + bottom_y) / 2;
1086
1087    area.draw(&Text::new(
1088        x_label.to_owned(),
1089        (w as i32 / 2, h as i32 - (x_label_area as i32 / 2).max(8)),
1090        x_style,
1091    ))
1092    .map_err(|e| ControllerError::ExportFailed(e.to_string()))?;
1093
1094    // Place Y title farther left to avoid touching tick numbers: use 2/3 of label area
1095    let y_x = ((y_label_area as i32 * 2) / 3).max(12);
1096    area.draw(&Text::new(
1097        y_label.to_owned(),
1098        (y_x, plot_center_y),
1099        y_style,
1100    ))
1101    .map_err(|e| ControllerError::ExportFailed(e.to_string()))?;
1102
1103    Ok(())
1104}
1105
1106fn labels_from_step(range: std::ops::Range<f32>, step: Option<f64>) -> Option<usize> {
1107    let step = step?;
1108    if step <= 0.0 {
1109        return None;
1110    }
1111    let span = (range.end - range.start).abs() as f64;
1112    if span <= 0.0 {
1113        return None;
1114    }
1115    Some(((span / step).round() as usize + 1).clamp(2, 100))
1116}
1117
1118fn scale_suffix(scale: ScaleType) -> &'static str {
1119    match scale {
1120        ScaleType::Linear => "",
1121        ScaleType::Log10 => " [log10]",
1122        ScaleType::LogE => " [ln]",
1123    }
1124}
1125
1126fn series_label_position(value: LegendPosition) -> SeriesLabelPosition {
1127    match value {
1128        LegendPosition::TopLeft => SeriesLabelPosition::UpperLeft,
1129        LegendPosition::TopRight => SeriesLabelPosition::UpperRight,
1130        LegendPosition::BottomLeft => SeriesLabelPosition::LowerLeft,
1131        LegendPosition::BottomRight => SeriesLabelPosition::LowerRight,
1132    }
1133}