1pub 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#[derive(Clone, Copy)]
39pub enum NotificationLevel {
40 Info,
41 Error,
42}
43
44#[derive(Clone)]
47pub struct Notification {
48 pub level: NotificationLevel,
49 pub message: String,
50}
51
52#[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
115pub 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 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 pub fn notification(&self) -> Option<&Notification> {
145 self.notification.as_ref()
146 }
147
148 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 pub fn has_data(&self) -> bool {
158 self.data_table.is_some()
159 }
160
161 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 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 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 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 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 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}