1use super::style::{AxisStyle, SeriesStyle};
4use astrelis_render::Color;
5use glam::Vec2;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
12pub struct AxisId(pub u32);
13
14impl AxisId {
15 pub const X_PRIMARY: AxisId = AxisId(0);
17 pub const Y_PRIMARY: AxisId = AxisId(1);
19 pub const X_SECONDARY: AxisId = AxisId(2);
21 pub const Y_SECONDARY: AxisId = AxisId(3);
23
24 pub fn custom(id: u32) -> Self {
28 Self(id + 4) }
30
31 pub fn from_name(name: &str) -> Self {
44 const FNV_OFFSET_BASIS: u32 = 2166136261;
46 const FNV_PRIME: u32 = 16777619;
47
48 let mut hash = FNV_OFFSET_BASIS;
49 for byte in name.bytes() {
50 hash ^= u32::from(byte);
51 hash = hash.wrapping_mul(FNV_PRIME);
52 }
53
54 Self(hash | 0x8000_0000)
56 }
57
58 pub fn is_standard(&self) -> bool {
60 self.0 < 4
61 }
62
63 pub fn is_custom(&self) -> bool {
65 !self.is_standard()
66 }
67
68 pub fn raw(&self) -> u32 {
70 self.0
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76pub enum AxisPosition {
77 #[default]
79 Left,
80 Right,
82 Top,
84 Bottom,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
90pub enum AxisOrientation {
91 #[default]
93 Horizontal,
94 Vertical,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
100pub struct SeriesId(pub u32);
101
102impl SeriesId {
103 pub fn from_index(index: usize) -> Self {
105 Self(index as u32)
106 }
107
108 pub fn from_name(name: &str) -> Self {
110 const FNV_OFFSET_BASIS: u32 = 2166136261;
112 const FNV_PRIME: u32 = 16777619;
113
114 let mut hash = FNV_OFFSET_BASIS;
115 for byte in name.bytes() {
116 hash ^= u32::from(byte);
117 hash = hash.wrapping_mul(FNV_PRIME);
118 }
119
120 Self(hash)
121 }
122
123 pub fn raw(&self) -> u32 {
125 self.0
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Default)]
131pub struct DataPoint {
132 pub x: f64,
134 pub y: f64,
136}
137
138impl DataPoint {
139 pub fn new(x: f64, y: f64) -> Self {
141 Self { x, y }
142 }
143}
144
145impl From<(f64, f64)> for DataPoint {
146 fn from((x, y): (f64, f64)) -> Self {
147 Self { x, y }
148 }
149}
150
151impl From<(f32, f32)> for DataPoint {
152 fn from((x, y): (f32, f32)) -> Self {
153 Self {
154 x: x as f64,
155 y: y as f64,
156 }
157 }
158}
159
160#[derive(Debug, Clone)]
162pub struct Series {
163 pub name: String,
165 pub data: Vec<DataPoint>,
167 pub style: SeriesStyle,
169 pub x_axis: AxisId,
171 pub y_axis: AxisId,
173}
174
175impl Series {
176 pub fn new(name: impl Into<String>, data: Vec<DataPoint>, style: SeriesStyle) -> Self {
178 Self {
179 name: name.into(),
180 data,
181 style,
182 x_axis: AxisId::X_PRIMARY,
183 y_axis: AxisId::Y_PRIMARY,
184 }
185 }
186
187 pub fn from_tuples<T: Into<DataPoint> + Copy>(
189 name: impl Into<String>,
190 data: &[T],
191 style: SeriesStyle,
192 ) -> Self {
193 Self {
194 name: name.into(),
195 data: data.iter().map(|&d| d.into()).collect(),
196 style,
197 x_axis: AxisId::X_PRIMARY,
198 y_axis: AxisId::Y_PRIMARY,
199 }
200 }
201
202 pub fn with_axes(mut self, x_axis: AxisId, y_axis: AxisId) -> Self {
204 self.x_axis = x_axis;
205 self.y_axis = y_axis;
206 self
207 }
208
209 pub fn bounds(&self) -> Option<(DataPoint, DataPoint)> {
211 if self.data.is_empty() {
212 return None;
213 }
214
215 let mut min = DataPoint::new(f64::INFINITY, f64::INFINITY);
216 let mut max = DataPoint::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
217
218 for p in &self.data {
219 min.x = min.x.min(p.x);
220 min.y = min.y.min(p.y);
221 max.x = max.x.max(p.x);
222 max.y = max.y.max(p.y);
223 }
224
225 Some((min, max))
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
231pub enum ChartType {
232 #[default]
234 Line,
235 Bar,
237 Scatter,
239 Area,
241}
242
243#[derive(Debug, Clone)]
245pub struct Axis {
246 pub id: AxisId,
248 pub label: Option<String>,
250 pub min: Option<f64>,
252 pub max: Option<f64>,
254 pub tick_count: usize,
256 pub grid_lines: bool,
258 pub style: AxisStyle,
260 pub position: AxisPosition,
262 pub orientation: AxisOrientation,
264 pub visible: bool,
266 pub custom_ticks: Option<Vec<(f64, String)>>,
268}
269
270impl Default for Axis {
271 fn default() -> Self {
272 Self {
273 id: AxisId::default(),
274 label: None,
275 min: None,
276 max: None,
277 tick_count: 5,
278 grid_lines: true,
279 style: AxisStyle::default(),
280 position: AxisPosition::Left,
281 orientation: AxisOrientation::Vertical,
282 visible: true,
283 custom_ticks: None,
284 }
285 }
286}
287
288impl Axis {
289 pub fn x() -> Self {
291 Self {
292 id: AxisId::X_PRIMARY,
293 orientation: AxisOrientation::Horizontal,
294 position: AxisPosition::Bottom,
295 ..Default::default()
296 }
297 }
298
299 pub fn y() -> Self {
301 Self {
302 id: AxisId::Y_PRIMARY,
303 orientation: AxisOrientation::Vertical,
304 position: AxisPosition::Left,
305 ..Default::default()
306 }
307 }
308
309 pub fn x_secondary() -> Self {
311 Self {
312 id: AxisId::X_SECONDARY,
313 orientation: AxisOrientation::Horizontal,
314 position: AxisPosition::Top,
315 ..Default::default()
316 }
317 }
318
319 pub fn y_secondary() -> Self {
321 Self {
322 id: AxisId::Y_SECONDARY,
323 orientation: AxisOrientation::Vertical,
324 position: AxisPosition::Right,
325 ..Default::default()
326 }
327 }
328
329 pub fn new(label: impl Into<String>) -> Self {
331 Self {
332 label: Some(label.into()),
333 ..Default::default()
334 }
335 }
336
337 pub fn with_id(mut self, id: AxisId) -> Self {
339 self.id = id;
340 self
341 }
342
343 pub fn with_range(mut self, min: f64, max: f64) -> Self {
345 self.min = Some(min);
346 self.max = Some(max);
347 self
348 }
349
350 pub fn with_ticks(mut self, count: usize) -> Self {
352 self.tick_count = count;
353 self
354 }
355
356 pub fn with_custom_ticks(mut self, ticks: Vec<(f64, String)>) -> Self {
358 self.custom_ticks = Some(ticks);
359 self
360 }
361
362 pub fn with_grid(mut self, enabled: bool) -> Self {
364 self.grid_lines = enabled;
365 self
366 }
367
368 pub fn with_position(mut self, position: AxisPosition) -> Self {
370 self.position = position;
371 self
372 }
373
374 pub fn with_visible(mut self, visible: bool) -> Self {
376 self.visible = visible;
377 self
378 }
379
380 pub fn with_label(mut self, label: impl Into<String>) -> Self {
382 self.label = Some(label.into());
383 self
384 }
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
389pub enum LegendPosition {
390 TopLeft,
392 #[default]
394 TopRight,
395 BottomLeft,
397 BottomRight,
399 None,
401}
402
403#[derive(Debug, Clone)]
405pub struct LegendConfig {
406 pub position: LegendPosition,
408 pub padding: f32,
410}
411
412impl Default for LegendConfig {
413 fn default() -> Self {
414 Self {
415 position: LegendPosition::TopRight,
416 padding: 10.0,
417 }
418 }
419}
420
421#[derive(Debug, Clone, Copy)]
423pub struct BarConfig {
424 pub bar_width: f32,
426 pub gap: f32,
428}
429
430impl Default for BarConfig {
431 fn default() -> Self {
432 Self {
433 bar_width: 20.0,
434 gap: 5.0,
435 }
436 }
437}
438
439#[derive(Debug, Clone)]
441pub struct TextAnnotation {
442 pub text: String,
444 pub data_position: Option<DataPoint>,
446 pub pixel_position: Vec2,
448 pub color: Color,
450 pub font_size: f32,
452 pub anchor: Vec2,
454 pub x_axis: AxisId,
456 pub y_axis: AxisId,
457}
458
459impl TextAnnotation {
460 pub fn at_data(text: impl Into<String>, x: f64, y: f64) -> Self {
462 Self {
463 text: text.into(),
464 data_position: Some(DataPoint::new(x, y)),
465 pixel_position: Vec2::ZERO,
466 color: Color::WHITE,
467 font_size: 12.0,
468 anchor: Vec2::new(0.5, 0.5),
469 x_axis: AxisId::X_PRIMARY,
470 y_axis: AxisId::Y_PRIMARY,
471 }
472 }
473
474 pub fn at_pixel(text: impl Into<String>, x: f32, y: f32) -> Self {
476 Self {
477 text: text.into(),
478 data_position: None,
479 pixel_position: Vec2::new(x, y),
480 color: Color::WHITE,
481 font_size: 12.0,
482 anchor: Vec2::new(0.5, 0.5),
483 x_axis: AxisId::X_PRIMARY,
484 y_axis: AxisId::Y_PRIMARY,
485 }
486 }
487
488 pub fn with_color(mut self, color: Color) -> Self {
490 self.color = color;
491 self
492 }
493
494 pub fn with_font_size(mut self, size: f32) -> Self {
496 self.font_size = size;
497 self
498 }
499
500 pub fn with_anchor(mut self, anchor: Vec2) -> Self {
502 self.anchor = anchor;
503 self
504 }
505}
506
507#[derive(Debug, Clone)]
509pub struct LineAnnotation {
510 pub start: DataPoint,
512 pub end: DataPoint,
514 pub color: Color,
516 pub width: f32,
518 pub dash: Option<f32>,
520 pub x_axis: AxisId,
522 pub y_axis: AxisId,
523}
524
525impl LineAnnotation {
526 pub fn horizontal(y: f64, x_min: f64, x_max: f64) -> Self {
528 Self {
529 start: DataPoint::new(x_min, y),
530 end: DataPoint::new(x_max, y),
531 color: Color::rgba(0.5, 0.5, 0.5, 0.8),
532 width: 1.0,
533 dash: None,
534 x_axis: AxisId::X_PRIMARY,
535 y_axis: AxisId::Y_PRIMARY,
536 }
537 }
538
539 pub fn vertical(x: f64, y_min: f64, y_max: f64) -> Self {
541 Self {
542 start: DataPoint::new(x, y_min),
543 end: DataPoint::new(x, y_max),
544 color: Color::rgba(0.5, 0.5, 0.5, 0.8),
545 width: 1.0,
546 dash: None,
547 x_axis: AxisId::X_PRIMARY,
548 y_axis: AxisId::Y_PRIMARY,
549 }
550 }
551
552 pub fn with_color(mut self, color: Color) -> Self {
554 self.color = color;
555 self
556 }
557
558 pub fn with_width(mut self, width: f32) -> Self {
560 self.width = width;
561 self
562 }
563
564 pub fn with_dash(mut self, dash: f32) -> Self {
566 self.dash = Some(dash);
567 self
568 }
569}
570
571#[derive(Debug, Clone)]
573pub struct FillRegion {
574 pub kind: FillRegionKind,
576 pub color: Color,
578 pub x_axis: AxisId,
580 pub y_axis: AxisId,
581}
582
583#[derive(Debug, Clone)]
585pub enum FillRegionKind {
586 HorizontalBand { y_min: f64, y_max: f64 },
588 VerticalBand { x_min: f64, x_max: f64 },
590 BelowSeries {
592 series_index: usize,
593 y_baseline: f64,
594 },
595 BetweenSeries {
597 series_index_1: usize,
598 series_index_2: usize,
599 },
600 Rectangle {
602 x_min: f64,
603 y_min: f64,
604 x_max: f64,
605 y_max: f64,
606 },
607 Polygon { points: Vec<DataPoint> },
609}
610
611impl FillRegion {
612 pub fn horizontal_band(y_min: f64, y_max: f64, color: Color) -> Self {
614 Self {
615 kind: FillRegionKind::HorizontalBand { y_min, y_max },
616 color,
617 x_axis: AxisId::X_PRIMARY,
618 y_axis: AxisId::Y_PRIMARY,
619 }
620 }
621
622 pub fn vertical_band(x_min: f64, x_max: f64, color: Color) -> Self {
624 Self {
625 kind: FillRegionKind::VerticalBand { x_min, x_max },
626 color,
627 x_axis: AxisId::X_PRIMARY,
628 y_axis: AxisId::Y_PRIMARY,
629 }
630 }
631
632 pub fn below_series(series_index: usize, y_baseline: f64, color: Color) -> Self {
634 Self {
635 kind: FillRegionKind::BelowSeries {
636 series_index,
637 y_baseline,
638 },
639 color,
640 x_axis: AxisId::X_PRIMARY,
641 y_axis: AxisId::Y_PRIMARY,
642 }
643 }
644
645 pub fn between_series(series_index_1: usize, series_index_2: usize, color: Color) -> Self {
647 Self {
648 kind: FillRegionKind::BetweenSeries {
649 series_index_1,
650 series_index_2,
651 },
652 color,
653 x_axis: AxisId::X_PRIMARY,
654 y_axis: AxisId::Y_PRIMARY,
655 }
656 }
657
658 pub fn rectangle(x_min: f64, y_min: f64, x_max: f64, y_max: f64, color: Color) -> Self {
660 Self {
661 kind: FillRegionKind::Rectangle {
662 x_min,
663 y_min,
664 x_max,
665 y_max,
666 },
667 color,
668 x_axis: AxisId::X_PRIMARY,
669 y_axis: AxisId::Y_PRIMARY,
670 }
671 }
672
673 pub fn polygon(points: Vec<DataPoint>, color: Color) -> Self {
675 Self {
676 kind: FillRegionKind::Polygon { points },
677 color,
678 x_axis: AxisId::X_PRIMARY,
679 y_axis: AxisId::Y_PRIMARY,
680 }
681 }
682
683 pub fn with_axes(mut self, x_axis: AxisId, y_axis: AxisId) -> Self {
685 self.x_axis = x_axis;
686 self.y_axis = y_axis;
687 self
688 }
689}
690
691#[derive(Debug, Clone)]
693pub struct ChartTitle {
694 pub text: String,
696 pub font_size: f32,
698 pub color: Color,
700 pub position: TitlePosition,
702}
703
704#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
706pub enum TitlePosition {
707 #[default]
709 Top,
710 Bottom,
712 Left,
714 Right,
716}
717
718impl ChartTitle {
719 pub fn new(text: impl Into<String>) -> Self {
721 Self {
722 text: text.into(),
723 font_size: 16.0,
724 color: Color::WHITE,
725 position: TitlePosition::Top,
726 }
727 }
728
729 pub fn with_font_size(mut self, size: f32) -> Self {
731 self.font_size = size;
732 self
733 }
734
735 pub fn with_color(mut self, color: Color) -> Self {
737 self.color = color;
738 self
739 }
740
741 pub fn with_position(mut self, position: TitlePosition) -> Self {
743 self.position = position;
744 self
745 }
746}
747
748#[derive(Debug, Clone)]
750pub struct InteractiveState {
751 pub pan_offset: Vec2,
753 pub zoom: Vec2,
755 pub pan_enabled: bool,
757 pub zoom_enabled: bool,
759 pub zoom_min: f32,
761 pub zoom_max: f32,
763 pub hovered_point: Option<(usize, usize)>,
765 pub selected_points: Vec<(usize, usize)>,
767 pub is_dragging: bool,
769 pub drag_start: Option<Vec2>,
771}
772
773impl Default for InteractiveState {
774 fn default() -> Self {
775 Self {
776 pan_offset: Vec2::ZERO,
777 zoom: Vec2::ONE,
778 pan_enabled: true,
779 zoom_enabled: true,
780 zoom_min: 0.1,
781 zoom_max: 10.0,
782 hovered_point: None,
783 selected_points: Vec::new(),
784 is_dragging: false,
785 drag_start: None,
786 }
787 }
788}
789
790impl InteractiveState {
791 pub fn reset(&mut self) {
793 self.pan_offset = Vec2::ZERO;
794 self.zoom = Vec2::ONE;
795 }
796
797 pub fn zoom_by(&mut self, factor: f32) {
799 self.zoom =
800 (self.zoom * factor).clamp(Vec2::splat(self.zoom_min), Vec2::splat(self.zoom_max));
801 }
802
803 pub fn zoom_xy(&mut self, factor_x: f32, factor_y: f32) {
805 self.zoom.x = (self.zoom.x * factor_x).clamp(self.zoom_min, self.zoom_max);
806 self.zoom.y = (self.zoom.y * factor_y).clamp(self.zoom_min, self.zoom_max);
807 }
808
809 pub fn zoom_x(&mut self, factor: f32) {
811 self.zoom.x = (self.zoom.x * factor).clamp(self.zoom_min, self.zoom_max);
812 }
813
814 pub fn zoom_y(&mut self, factor: f32) {
816 self.zoom.y = (self.zoom.y * factor).clamp(self.zoom_min, self.zoom_max);
817 }
818
819 pub fn zoom_at_normalized(&mut self, normalized_center: Vec2, factor: f32) {
824 let old_zoom = self.zoom;
825 let new_zoom =
826 (self.zoom * factor).clamp(Vec2::splat(self.zoom_min), Vec2::splat(self.zoom_max));
827
828 if new_zoom == old_zoom {
829 return;
830 }
831
832 let offset_from_center = normalized_center - Vec2::splat(0.5);
843 let zoom_diff = Vec2::new(
844 1.0 / old_zoom.x - 1.0 / new_zoom.x,
845 1.0 / old_zoom.y - 1.0 / new_zoom.y,
846 );
847
848 self.pan_offset += offset_from_center * zoom_diff * 2.0;
852 self.zoom = new_zoom;
853 }
854
855 pub fn zoom_at(&mut self, _center: Vec2, factor: f32) {
860 self.zoom_by(factor);
862 }
863
864 pub fn pan(&mut self, delta: Vec2) {
866 if self.pan_enabled {
867 self.pan_offset += delta;
868 }
869 }
870}
871
872#[derive(Debug, Clone)]
874pub struct Chart {
875 pub chart_type: ChartType,
877 pub series: Vec<Series>,
879 pub axes: Vec<Axis>,
881 pub title: Option<ChartTitle>,
883 pub subtitle: Option<ChartTitle>,
885 pub legend: Option<LegendConfig>,
887 pub background_color: Color,
889 pub bar_config: BarConfig,
891 pub padding: f32,
893 pub text_annotations: Vec<TextAnnotation>,
895 pub line_annotations: Vec<LineAnnotation>,
897 pub fill_regions: Vec<FillRegion>,
899 pub interactive: InteractiveState,
901 pub show_crosshair: bool,
903 pub show_tooltips: bool,
905}
906
907impl Default for Chart {
908 fn default() -> Self {
909 Self {
910 chart_type: ChartType::Line,
911 series: Vec::new(),
912 axes: vec![Axis::x(), Axis::y()],
913 title: None,
914 subtitle: None,
915 legend: Some(LegendConfig::default()),
916 background_color: Color::rgba(0.12, 0.12, 0.14, 1.0),
917 bar_config: BarConfig::default(),
918 padding: 50.0,
919 text_annotations: Vec::new(),
920 line_annotations: Vec::new(),
921 fill_regions: Vec::new(),
922 interactive: InteractiveState::default(),
923 show_crosshair: false,
924 show_tooltips: true,
925 }
926 }
927}
928
929impl Chart {
930 pub fn append_data(&mut self, series_idx: usize, points: &[DataPoint]) {
946 if let Some(series) = self.series.get_mut(series_idx) {
947 series.data.extend_from_slice(points);
948 }
949 }
950
951 pub fn push_point(&mut self, series_idx: usize, point: DataPoint, max_points: Option<usize>) {
964 if let Some(series) = self.series.get_mut(series_idx) {
965 series.data.push(point);
966
967 if let Some(max) = max_points
969 && series.data.len() > max
970 {
971 let excess = series.data.len() - max;
972 series.data.drain(..excess);
973 }
974 }
975 }
976
977 pub fn set_data(&mut self, series_idx: usize, data: Vec<DataPoint>) {
987 if let Some(series) = self.series.get_mut(series_idx) {
988 series.data = data;
989 }
990 }
991
992 pub fn clear_data(&mut self, series_idx: usize) {
994 if let Some(series) = self.series.get_mut(series_idx) {
995 series.data.clear();
996 }
997 }
998
999 pub fn series_mut(&mut self, series_idx: usize) -> Option<&mut Series> {
1001 self.series.get_mut(series_idx)
1002 }
1003
1004 pub fn series_len(&self, series_idx: usize) -> usize {
1006 self.series
1007 .get(series_idx)
1008 .map(|s| s.data.len())
1009 .unwrap_or(0)
1010 }
1011
1012 pub fn total_points(&self) -> usize {
1014 self.series.iter().map(|s| s.data.len()).sum()
1015 }
1016
1017 pub fn get_axis(&self, id: AxisId) -> Option<&Axis> {
1023 self.axes.iter().find(|a| a.id == id)
1024 }
1025
1026 pub fn get_axis_mut(&mut self, id: AxisId) -> Option<&mut Axis> {
1028 self.axes.iter_mut().find(|a| a.id == id)
1029 }
1030
1031 pub fn set_axis(&mut self, axis: Axis) {
1033 if let Some(existing) = self.axes.iter_mut().find(|a| a.id == axis.id) {
1034 *existing = axis;
1035 } else {
1036 self.axes.push(axis);
1037 }
1038 }
1039
1040 pub fn x_axis(&self) -> Option<&Axis> {
1042 self.get_axis(AxisId::X_PRIMARY)
1043 }
1044
1045 pub fn y_axis(&self) -> Option<&Axis> {
1047 self.get_axis(AxisId::Y_PRIMARY)
1048 }
1049
1050 pub fn data_bounds_for_axis(&self, axis_id: AxisId) -> Option<(f64, f64)> {
1052 let mut min = f64::INFINITY;
1053 let mut max = f64::NEG_INFINITY;
1054 let mut has_data = false;
1055
1056 for series in &self.series {
1057 let is_x_axis = series.x_axis == axis_id;
1058 let is_y_axis = series.y_axis == axis_id;
1059
1060 if !is_x_axis && !is_y_axis {
1061 continue;
1062 }
1063
1064 if let Some((series_min, series_max)) = series.bounds() {
1065 has_data = true;
1066 if is_x_axis {
1067 min = min.min(series_min.x);
1068 max = max.max(series_max.x);
1069 } else {
1070 min = min.min(series_min.y);
1071 max = max.max(series_max.y);
1072 }
1073 }
1074 }
1075
1076 if has_data { Some((min, max)) } else { None }
1077 }
1078
1079 pub fn data_bounds(&self) -> Option<(DataPoint, DataPoint)> {
1081 let mut combined_min = DataPoint::new(f64::INFINITY, f64::INFINITY);
1082 let mut combined_max = DataPoint::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
1083 let mut has_data = false;
1084
1085 for series in &self.series {
1086 if let Some((min, max)) = series.bounds() {
1087 has_data = true;
1088 combined_min.x = combined_min.x.min(min.x);
1089 combined_min.y = combined_min.y.min(min.y);
1090 combined_max.x = combined_max.x.max(max.x);
1091 combined_max.y = combined_max.y.max(max.y);
1092 }
1093 }
1094
1095 if has_data {
1096 Some((combined_min, combined_max))
1097 } else {
1098 None
1099 }
1100 }
1101
1102 pub fn axis_range(&self, axis_id: AxisId) -> (f64, f64) {
1104 let axis = self.get_axis(axis_id);
1105 let bounds = self.data_bounds_for_axis(axis_id);
1106
1107 let (data_min, data_max) = bounds.unwrap_or((0.0, 1.0));
1108
1109 let min = axis.and_then(|a| a.min).unwrap_or(data_min);
1110 let max = axis.and_then(|a| a.max).unwrap_or(data_max);
1111
1112 let zoom = if axis.map(|a| a.orientation) == Some(AxisOrientation::Horizontal) {
1114 self.interactive.zoom.x
1115 } else {
1116 self.interactive.zoom.y
1117 };
1118
1119 let pan = if axis.map(|a| a.orientation) == Some(AxisOrientation::Horizontal) {
1120 self.interactive.pan_offset.x as f64
1121 } else {
1122 self.interactive.pan_offset.y as f64
1123 };
1124
1125 let range = max - min;
1126 let zoomed_range = range / zoom as f64;
1127 let center = (min + max) / 2.0 + pan;
1128
1129 (center - zoomed_range / 2.0, center + zoomed_range / 2.0)
1130 }
1131
1132 pub fn x_range(&self) -> (f64, f64) {
1134 self.axis_range(AxisId::X_PRIMARY)
1135 }
1136
1137 pub fn y_range(&self) -> (f64, f64) {
1139 self.axis_range(AxisId::Y_PRIMARY)
1140 }
1141}