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