1use std::ops::Range;
14
15use crate::controller::action::Action;
16use crate::controller::{NotificationLevel, PlotController};
17use crate::model::{
18 AxisKind, LegendPosition, LineStyle, MarkerShape, RangePolicy, ScaleType, TickConfig,
19};
20use crate::view::FilesMenu;
21use eframe::egui::{self, Color32, RichText, SidePanel};
22use egui_plotter::{Chart, MouseConfig};
23use plotters::coord::Shift;
24use plotters::coord::types::RangedCoordf32;
25use plotters::prelude::*;
26use plotters::style::Color as PlottersColor;
27
28pub struct PlotEditorView;
31
32impl PlotEditorView {
33 pub fn new() -> Self {
35 Self
36 }
37
38 pub fn draw(&mut self, ctx: &egui::Context, controller: &PlotController) -> Vec<Action> {
40 let mut actions = FilesMenu::draw(ctx);
41 self.draw_controls(ctx, controller, &mut actions);
42 self.draw_plot(ctx, controller);
43 actions
44 }
45
46 fn draw_controls(
47 &mut self,
48 ctx: &egui::Context,
49 controller: &PlotController,
50 actions: &mut Vec<Action>,
51 ) {
52 SidePanel::right("control_panel")
53 .resizable(true)
54 .default_width(360.0)
55 .show(ctx, |ui| {
56 ui.heading("Controls");
57 ui.separator();
58
59 if let Some(n) = controller.notification() {
60 let color = match n.level {
61 NotificationLevel::Info => Color32::DARK_GREEN,
62 NotificationLevel::Error => Color32::RED,
63 };
64 ui.colored_label(color, &n.message);
65 ui.separator();
66 }
67
68 ui.label(RichText::new("Axis").strong());
69 axis_editor(ui, "X Axis", AxisKind::X, controller, actions);
70 axis_editor(ui, "Y Axis", AxisKind::Y, controller, actions);
71
72 ui.separator();
73 ui.label(RichText::new("Legend").strong());
74 let mut legend_visible = controller.model.legend.visible;
75 if ui.checkbox(&mut legend_visible, "Visible").changed() {
76 actions.push(Action::SetLegendVisible(legend_visible));
77 }
78 let mut legend_title = controller.model.legend.title.clone().unwrap_or_default();
79 if ui.text_edit_singleline(&mut legend_title).changed() {
80 actions.push(Action::SetLegendTitle(if legend_title.trim().is_empty() {
81 None
82 } else {
83 Some(legend_title)
84 }));
85 }
86 let mut legend_pos = controller.model.legend.position;
87 egui::ComboBox::from_label("Position")
88 .selected_text(legend_position_text(legend_pos))
89 .show_ui(ui, |ui| {
90 ui.selectable_value(&mut legend_pos, LegendPosition::TopLeft, "Top Left");
91 ui.selectable_value(
92 &mut legend_pos,
93 LegendPosition::TopRight,
94 "Top Right",
95 );
96 ui.selectable_value(
97 &mut legend_pos,
98 LegendPosition::BottomLeft,
99 "Bottom Left",
100 );
101 ui.selectable_value(
102 &mut legend_pos,
103 LegendPosition::BottomRight,
104 "Bottom Right",
105 );
106 });
107 if legend_pos != controller.model.legend.position {
108 actions.push(Action::SetLegendPosition(legend_pos));
109 }
110 let mut legend_font_size = controller.model.legend.font_size;
111 if ui
112 .add(
113 egui::Slider::new(&mut legend_font_size, 8..=64)
114 .text("Legend font size"),
115 )
116 .changed()
117 {
118 actions.push(Action::SetLegendFontSize(legend_font_size));
119 }
120 let mut legend_color = Color32::from_rgba_premultiplied(
121 controller.model.legend.font_color.r,
122 controller.model.legend.font_color.g,
123 controller.model.legend.font_color.b,
124 controller.model.legend.font_color.a,
125 );
126 if ui.color_edit_button_srgba(&mut legend_color).changed() {
127 actions.push(Action::SetLegendFontColor(crate::model::Color {
128 r: legend_color.r(),
129 g: legend_color.g(),
130 b: legend_color.b(),
131 a: legend_color.a(),
132 }));
133 }
134
135 ui.separator();
136 ui.label(RichText::new("Label").strong());
137 let mut title = controller.model.layout.title.clone();
138 if ui.text_edit_singleline(&mut title).changed() {
139 actions.push(Action::SetChartTitle(title));
140 }
141 let mut title_font_size = controller.model.layout.title_font_size;
142 if ui
143 .add(egui::Slider::new(&mut title_font_size, 8..=72).text("Title font size"))
144 .changed()
145 {
146 actions.push(Action::SetLabelFontSize(title_font_size));
147 }
148 let mut title_color = Color32::from_rgba_premultiplied(
149 controller.model.layout.title_font_color.r,
150 controller.model.layout.title_font_color.g,
151 controller.model.layout.title_font_color.b,
152 controller.model.layout.title_font_color.a,
153 );
154 if ui.color_edit_button_srgba(&mut title_color).changed() {
155 actions.push(Action::SetLabelFontColor(crate::model::Color {
156 r: title_color.r(),
157 g: title_color.g(),
158 b: title_color.b(),
159 a: title_color.a(),
160 }));
161 }
162
163 ui.separator();
164 ui.label(RichText::new("Series").strong());
165 let columns = controller.available_columns();
166
167 for series in &controller.model.series {
168 ui.push_id(series.id.0, |ui| {
169 ui.group(|ui| {
170 ui.horizontal(|ui| {
171 ui.label(format!("ID {}", series.id.0));
172 if ui.button("Remove").clicked() {
173 actions.push(Action::RemoveSeries {
174 series_id: series.id,
175 });
176 }
177 });
178
179 let mut name = series.name.clone();
180 if ui.text_edit_singleline(&mut name).changed() {
181 actions.push(Action::RenameSeries {
182 series_id: series.id,
183 name,
184 });
185 }
186
187 let mut visible = series.visible;
188 if ui.checkbox(&mut visible, "Visible").changed() {
189 actions.push(Action::SetSeriesVisibility {
190 series_id: series.id,
191 visible,
192 });
193 }
194
195 if columns.is_empty() {
196 ui.label("Load CSV/TXT to select X/Y columns");
197 } else {
198 let mut x_col = if series.x_column.is_empty() {
199 columns[0].clone()
200 } else {
201 series.x_column.clone()
202 };
203 egui::ComboBox::from_label("X column")
204 .selected_text(x_col.clone())
205 .show_ui(ui, |ui| {
206 for col in &columns {
207 ui.selectable_value(&mut x_col, col.clone(), col);
208 }
209 });
210 if x_col != series.x_column {
211 actions.push(Action::SetSeriesXColumn {
212 series_id: series.id,
213 x_column: x_col,
214 });
215 }
216
217 let default_y =
218 columns.get(1).cloned().unwrap_or_else(|| columns[0].clone());
219 let mut y_col = if series.y_column.is_empty() {
220 default_y
221 } else {
222 series.y_column.clone()
223 };
224 egui::ComboBox::from_label("Y column")
225 .selected_text(y_col.clone())
226 .show_ui(ui, |ui| {
227 for col in &columns {
228 ui.selectable_value(&mut y_col, col.clone(), col);
229 }
230 });
231 if y_col != series.y_column {
232 actions.push(Action::SetSeriesYColumn {
233 series_id: series.id,
234 y_column: y_col,
235 });
236 }
237 }
238
239 let mut width = series.style.line_width;
240 if ui
241 .add(egui::Slider::new(&mut width, 1.0..=10.0).text("Width"))
242 .changed()
243 {
244 actions.push(Action::SetSeriesLineWidth {
245 series_id: series.id,
246 width,
247 });
248 }
249
250 let mut style = series.style.line_style;
251 egui::ComboBox::from_label("Line style")
252 .selected_text(line_style_text(style))
253 .show_ui(ui, |ui| {
254 ui.selectable_value(&mut style, LineStyle::Solid, "Solid");
255 ui.selectable_value(&mut style, LineStyle::Dashed, "Dashed");
256 ui.selectable_value(&mut style, LineStyle::Dotted, "Dotted");
257 });
258 if style != series.style.line_style {
259 actions.push(Action::SetSeriesLineStyle {
260 series_id: series.id,
261 line_style: style,
262 });
263 }
264
265 let mut color = Color32::from_rgba_premultiplied(
266 series.style.color.r,
267 series.style.color.g,
268 series.style.color.b,
269 series.style.color.a,
270 );
271 if ui.color_edit_button_srgba(&mut color).changed() {
272 actions.push(Action::SetSeriesColor {
273 series_id: series.id,
274 color: crate::model::Color {
275 r: color.r(),
276 g: color.g(),
277 b: color.b(),
278 a: color.a(),
279 },
280 });
281 }
282
283 let mut marker_enabled = series.style.marker.is_some();
284 if ui.checkbox(&mut marker_enabled, "Marker").changed() {
285 actions.push(Action::SetSeriesMarker {
286 series_id: series.id,
287 marker: if marker_enabled {
288 Some(MarkerShape::Circle)
289 } else {
290 None
291 },
292 size: series.style.marker.as_ref().map(|m| m.size).unwrap_or(3.0),
293 });
294 }
295 });
296 });
297 ui.add_space(8.0);
298 }
299
300 if ui.button("Add series").clicked() {
301 actions.push(Action::AddSeries {
302 name: String::new(),
303 x_column: String::new(),
304 y_column: String::new(),
305 });
306 }
307 ui.horizontal(|ui| {
308 if ui.button("Undo").clicked() {
309 actions.push(Action::Undo);
310 }
311 if ui.button("Redo").clicked() {
312 actions.push(Action::Redo);
313 }
314 });
315 });
316 }
317
318 fn draw_plot(&mut self, ctx: &egui::Context, controller: &PlotController) {
319 egui::CentralPanel::default().show(ctx, |ui| {
320 if !controller.has_data() {
321 ui.heading("No data loaded");
322 ui.label("Use Files > From CSV or Files > From TXT");
323 return;
324 }
325
326 let mut rendered = Vec::new();
327 for series in &controller.model.series {
328 if !series.visible {
329 continue;
330 }
331 if let Ok(points) = controller.points_for_series(series.id) {
332 let scaled = points
333 .iter()
334 .copied()
335 .filter_map(|(x, y)| {
336 apply_scale(
337 x,
338 y,
339 controller.model.axes.x.scale,
340 controller.model.axes.y.scale,
341 )
342 })
343 .collect::<Vec<_>>();
344 rendered.push((series.clone(), scaled));
345 }
346 }
347
348 let x_range = resolve_range(
349 &controller.model.axes.x.range,
350 &rendered,
351 true,
352 -1.0..1.0,
353 );
354 let y_range = resolve_range(
355 &controller.model.axes.y.range,
356 &rendered,
357 false,
358 -1.0..1.0,
359 );
360
361 let title = controller.model.layout.title.clone();
362 let x_label = format!(
363 "{}{}",
364 controller.model.axes.x.label,
365 scale_suffix(controller.model.axes.x.scale)
366 );
367 let y_label = format!(
368 "{}{}",
369 controller.model.axes.y.label,
370 scale_suffix(controller.model.axes.y.scale)
371 );
372 let x_ticks = controller.model.axes.x.ticks.clone();
373 let y_ticks = controller.model.axes.y.ticks.clone();
374 let x_axis_title_font_size = controller.model.axes.x.axis_title_font_size;
375 let y_axis_title_font_size = controller.model.axes.y.axis_title_font_size;
376 let x_label_font_size = controller.model.axes.x.label_font_size;
377 let y_label_font_size = controller.model.axes.y.label_font_size;
378 let legend_visible = controller.model.legend.visible;
379 let legend_pos = controller.model.legend.position;
380 let legend_font_size = controller.model.legend.font_size;
381 let legend_font_color = RGBColor(
382 controller.model.legend.font_color.r,
383 controller.model.legend.font_color.g,
384 controller.model.legend.font_color.b,
385 );
386 let margin = controller.model.layout.margin;
387 let x_label_area = controller.model.layout.x_label_area_size;
388 let y_label_area = controller.model.layout.y_label_area_size;
389 let effective_x_label_area = x_label_area
390 .max(x_label_font_size + 18)
391 .max(x_axis_title_font_size + 20);
392 let effective_y_label_area = y_label_area
396 .max((y_label_font_size as f32 * 1.6) as u32 + 16)
397 .max(y_axis_title_font_size + 28)
398 .max(y_label_font_size + y_axis_title_font_size + 28);
399 let title_font_size = controller.model.layout.title_font_size;
400 let title_font_color = RGBColor(
401 controller.model.layout.title_font_color.r,
402 controller.model.layout.title_font_color.g,
403 controller.model.layout.title_font_color.b,
404 );
405
406 let mut chart = Chart::new((x_range.clone(), y_range.clone()))
407 .mouse(MouseConfig::enabled())
408 .builder_cb(Box::new(move |area, _t, _ranges| {
409 let mut chart = ChartBuilder::on(area)
410 .caption(
411 title.clone(),
412 ("sans-serif", title_font_size)
413 .into_font()
414 .color(&title_font_color),
415 )
416 .margin(margin)
417 .x_label_area_size(effective_x_label_area)
418 .y_label_area_size(effective_y_label_area)
419 .build_cartesian_2d(x_range.clone(), y_range.clone())
420 .expect("build chart failed");
421
422 configure_mesh(
423 &mut chart,
424 x_label_font_size,
425 y_label_font_size,
426 &x_ticks,
427 &y_ticks,
428 x_range.clone(),
429 y_range.clone(),
430 );
431 draw_axis_titles(
432 area,
433 &x_label,
434 &y_label,
435 x_axis_title_font_size,
436 y_axis_title_font_size,
437 effective_x_label_area,
438 effective_y_label_area,
439 title_font_size,
440 margin,
441 );
442
443 for (series, points) in &rendered {
444 if points.is_empty() {
445 continue;
446 }
447 let color = RGBColor(
448 series.style.color.r,
449 series.style.color.g,
450 series.style.color.b,
451 );
452 let style = ShapeStyle::from(&color)
453 .stroke_width(series.style.line_width.max(1.0) as u32);
454
455 if series.style.line_style == LineStyle::Dotted {
456 let _ = chart.draw_series(
457 points
458 .iter()
459 .map(|(x, y)| Circle::new((*x, *y), 2, style.filled())),
460 );
461 } else {
462 let drawn = chart
463 .draw_series(LineSeries::new(points.iter().copied(), style))
464 .expect("draw series failed");
465 if legend_visible {
466 drawn.label(series.name.clone()).legend(move |(x, y)| {
467 PathElement::new(vec![(x, y), (x + 20, y)], color)
468 });
469 }
470 }
471 }
472
473 if legend_visible {
474 chart
475 .configure_series_labels()
476 .label_font(
477 ("sans-serif", legend_font_size)
478 .into_font()
479 .color(&legend_font_color),
480 )
481 .position(series_label_position(legend_pos))
482 .background_style(WHITE.mix(0.8))
483 .border_style(BLACK)
484 .draw()
485 .expect("draw legend failed");
486 }
487 }));
488
489 chart.draw(ui);
490 });
491 }
492}
493
494fn axis_editor(
495 ui: &mut egui::Ui,
496 title: &str,
497 axis: AxisKind,
498 controller: &PlotController,
499 actions: &mut Vec<Action>,
500) {
501 ui.push_id(title, |ui| {
502 ui.collapsing(title, |ui| {
503 let axis_ref = match axis {
504 AxisKind::X => &controller.model.axes.x,
505 AxisKind::Y => &controller.model.axes.y,
506 };
507
508 let mut label = axis_ref.label.clone();
509 if ui.text_edit_singleline(&mut label).changed() {
510 actions.push(Action::SetAxisLabel { axis, label });
511 }
512 ui.horizontal(|ui| {
513 ui.label("Label font");
514 let mut label_font = axis_ref.axis_title_font_size;
515 let slider_changed = ui
516 .add(egui::Slider::new(&mut label_font, 8..=200).show_value(false))
517 .changed();
518 let input_changed = ui
519 .add(egui::DragValue::new(&mut label_font).range(8..=200))
520 .changed();
521 if (slider_changed || input_changed) && label_font != axis_ref.axis_title_font_size {
522 actions.push(Action::SetAxisTitleFontSize {
523 axis,
524 font_size: label_font,
525 });
526 }
527 });
528
529 ui.horizontal(|ui| {
530 ui.label("Tick font");
531 let mut tick_font = axis_ref.label_font_size;
532 let slider_changed = ui
533 .add(egui::Slider::new(&mut tick_font, 8..=200).show_value(false))
534 .changed();
535 let input_changed = ui
536 .add(egui::DragValue::new(&mut tick_font).range(8..=200))
537 .changed();
538 if (slider_changed || input_changed) && tick_font != axis_ref.label_font_size {
539 actions.push(Action::SetAxisLabelFontSize {
540 axis,
541 font_size: tick_font,
542 });
543 }
544 });
545
546 let mut scale = axis_ref.scale;
547 egui::ComboBox::from_label("Scale")
548 .selected_text(scale_text(scale))
549 .show_ui(ui, |ui| {
550 ui.selectable_value(&mut scale, ScaleType::Linear, "Linear");
551 ui.selectable_value(&mut scale, ScaleType::Log10, "Log10");
552 ui.selectable_value(&mut scale, ScaleType::LogE, "LogE");
553 });
554 if scale != axis_ref.scale {
555 actions.push(Action::SetAxisScale { axis, scale });
556 }
557
558 let mut auto = matches!(axis_ref.range, RangePolicy::Auto);
559 ui.horizontal(|ui| {
560 if ui.radio_value(&mut auto, true, "Auto").clicked() {
561 actions.push(Action::SetAxisRange {
562 axis,
563 range: RangePolicy::Auto,
564 });
565 }
566 if ui.radio_value(&mut auto, false, "Manual").clicked()
567 && !matches!(axis_ref.range, RangePolicy::Manual { .. })
568 {
569 actions.push(Action::SetAxisRange {
570 axis,
571 range: RangePolicy::Manual {
572 min: -1.0,
573 max: 1.0,
574 },
575 });
576 }
577 });
578
579 let (mut min, mut max) = match axis_ref.range {
580 RangePolicy::Auto => (-1.0, 1.0),
581 RangePolicy::Manual { min, max } => (min, max),
582 };
583 ui.horizontal(|ui| {
584 ui.label("Min");
585 let min_changed = ui.add(egui::DragValue::new(&mut min).speed(0.1)).changed();
586 ui.label("Max");
587 let max_changed = ui.add(egui::DragValue::new(&mut max).speed(0.1)).changed();
588 if (min_changed || max_changed) && !auto {
589 actions.push(Action::SetAxisRange {
590 axis,
591 range: RangePolicy::Manual { min, max },
592 });
593 }
594 });
595
596 ui.separator();
597 ui.label("Ticks");
598
599 let mut major_auto = axis_ref.ticks.major_step.is_none();
600 if ui.checkbox(&mut major_auto, "Auto major step").changed() {
601 actions.push(Action::SetAxisMajorTickStep {
602 axis,
603 step: if major_auto { None } else { Some(1.0) },
604 });
605 }
606 if !major_auto {
607 let mut step = axis_ref.ticks.major_step.unwrap_or(1.0);
608 if ui
609 .add(
610 egui::DragValue::new(&mut step)
611 .speed(0.1)
612 .range(0.01..=1_000.0),
613 )
614 .changed()
615 {
616 actions.push(Action::SetAxisMajorTickStep {
617 axis,
618 step: Some(step),
619 });
620 }
621 }
622
623 let mut minor = axis_ref.ticks.minor_per_major;
624 ui.horizontal(|ui| {
625 ui.label("Minor per major");
626 if ui
627 .add(egui::DragValue::new(&mut minor).range(0..=20))
628 .changed()
629 {
630 actions.push(Action::SetAxisMinorTicks {
631 axis,
632 per_major: minor,
633 });
634 }
635 });
636 });
637 });
638}
639
640fn resolve_range(
641 policy: &RangePolicy,
642 data: &[(crate::model::SeriesModel, Vec<(f32, f32)>)],
643 is_x: bool,
644 fallback: Range<f32>,
645) -> Range<f32> {
646 match policy {
647 RangePolicy::Manual { min, max } => (*min as f32)..(*max as f32),
648 RangePolicy::Auto => {
649 let mut min_v = f32::INFINITY;
650 let mut max_v = f32::NEG_INFINITY;
651 for (_, points) in data {
652 for (x, y) in points {
653 let v = if is_x { *x } else { *y };
654 min_v = min_v.min(v);
655 max_v = max_v.max(v);
656 }
657 }
658 if !min_v.is_finite() || !max_v.is_finite() || min_v >= max_v {
659 return fallback;
660 }
661 let pad = ((max_v - min_v) * 0.05).max(0.1);
662 (min_v - pad)..(max_v + pad)
663 }
664 }
665}
666
667fn apply_scale(x: f32, y: f32, x_scale: ScaleType, y_scale: ScaleType) -> Option<(f32, f32)> {
668 let sx = match x_scale {
669 ScaleType::Linear => Some(x),
670 ScaleType::Log10 => (x > 0.0).then(|| x.log10()),
671 ScaleType::LogE => (x > 0.0).then(|| x.ln()),
672 }?;
673 let sy = match y_scale {
674 ScaleType::Linear => Some(y),
675 ScaleType::Log10 => (y > 0.0).then(|| y.log10()),
676 ScaleType::LogE => (y > 0.0).then(|| y.ln()),
677 }?;
678 Some((sx, sy))
679}
680
681fn configure_mesh<DB: DrawingBackend>(
682 chart: &mut ChartContext<'_, DB, Cartesian2d<RangedCoordf32, RangedCoordf32>>,
683 x_label_font_size: u32,
684 y_label_font_size: u32,
685 x_ticks: &TickConfig,
686 y_ticks: &TickConfig,
687 x_range: Range<f32>,
688 y_range: Range<f32>,
689) {
690 let x_labels = labels_from_step(x_range, x_ticks.major_step).unwrap_or(10);
691 let y_labels = labels_from_step(y_range, y_ticks.major_step).unwrap_or(10);
692
693 chart
694 .configure_mesh()
695 .x_desc("")
696 .y_desc("")
697 .x_label_style(("sans-serif", x_label_font_size))
698 .y_label_style(("sans-serif", y_label_font_size))
699 .x_labels(x_labels)
700 .y_labels(y_labels)
701 .max_light_lines(x_ticks.minor_per_major.max(y_ticks.minor_per_major) as usize)
702 .draw()
703 .expect("draw mesh failed");
704}
705
706fn draw_axis_titles<DB: DrawingBackend>(
707 area: &DrawingArea<DB, Shift>,
708 x_label: &str,
709 y_label: &str,
710 x_font_size: u32,
711 y_font_size: u32,
712 x_label_area: u32,
713 y_label_area: u32,
714 title_font_size: u32,
715 margin: u32,
716) {
717 let (w, h) = area.dim_in_pixel();
720 let x_style = ("sans-serif", x_font_size.max(8)).into_font().color(&BLACK);
721 let y_style = ("sans-serif", y_font_size.max(8))
722 .into_font()
723 .transform(plotters::style::FontTransform::Rotate270)
724 .color(&BLACK);
725
726 let cap_h = (title_font_size as i32 + 10).max(12);
728 let m = margin as i32;
729
730 let top_y = cap_h + m;
732 let bottom_y = h as i32 - (x_label_area as i32) - m;
733 let plot_center_y = (top_y + bottom_y) / 2;
734
735 let _ = area.draw(&Text::new(
737 x_label.to_owned(),
738 (w as i32 / 2, h as i32 - (x_label_area as i32 / 2).max(10)),
739 x_style,
740 ));
741
742 let y_x = (y_label_area as i32 / 4).max(12);
744 let _ = area.draw(&Text::new(
745 y_label.to_owned(),
746 (y_x, plot_center_y),
747 y_style,
748 ));
749}
750
751fn labels_from_step(range: Range<f32>, step: Option<f64>) -> Option<usize> {
752 let step = step?;
753 if step <= 0.0 {
754 return None;
755 }
756 let span = (range.end - range.start).abs() as f64;
757 if span <= 0.0 {
758 return None;
759 }
760 Some(((span / step).round() as usize + 1).clamp(2, 100))
761}
762
763fn line_style_text(value: LineStyle) -> &'static str {
764 match value {
765 LineStyle::Solid => "Solid",
766 LineStyle::Dashed => "Dashed",
767 LineStyle::Dotted => "Dotted",
768 }
769}
770
771fn scale_text(value: ScaleType) -> &'static str {
772 match value {
773 ScaleType::Linear => "Linear",
774 ScaleType::Log10 => "Log10",
775 ScaleType::LogE => "LogE",
776 }
777}
778
779fn scale_suffix(value: ScaleType) -> &'static str {
780 match value {
781 ScaleType::Linear => "",
782 ScaleType::Log10 => " [log10]",
783 ScaleType::LogE => " [ln]",
784 }
785}
786
787fn legend_position_text(value: LegendPosition) -> &'static str {
788 match value {
789 LegendPosition::TopLeft => "Top Left",
790 LegendPosition::TopRight => "Top Right",
791 LegendPosition::BottomLeft => "Bottom Left",
792 LegendPosition::BottomRight => "Bottom Right",
793 }
794}
795
796fn series_label_position(value: LegendPosition) -> SeriesLabelPosition {
797 match value {
798 LegendPosition::TopLeft => SeriesLabelPosition::UpperLeft,
799 LegendPosition::TopRight => SeriesLabelPosition::UpperRight,
800 LegendPosition::BottomLeft => SeriesLabelPosition::LowerLeft,
801 LegendPosition::BottomRight => SeriesLabelPosition::LowerRight,
802 }
803}