1use super::style::AxisStyle;
10use super::types::{AxisId, AxisOrientation, AxisPosition};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum TimeEpoch {
15 #[default]
17 Unix,
18 J2000,
20 Custom(i64),
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Default)]
28pub enum ScaleType {
29 #[default]
33 Linear,
34
35 Logarithmic {
40 base: f64,
42 },
43
44 Symlog {
49 lin_threshold: f64,
51 },
52
53 Time {
57 epoch: TimeEpoch,
59 },
60}
61
62impl ScaleType {
63 pub fn log10() -> Self {
65 Self::Logarithmic { base: 10.0 }
66 }
67
68 pub fn ln() -> Self {
70 Self::Logarithmic {
71 base: std::f64::consts::E,
72 }
73 }
74
75 pub fn symlog(threshold: f64) -> Self {
77 Self::Symlog {
78 lin_threshold: threshold,
79 }
80 }
81
82 pub fn time() -> Self {
84 Self::Time {
85 epoch: TimeEpoch::Unix,
86 }
87 }
88
89 pub fn normalize(&self, value: f64, min: f64, max: f64) -> f64 {
93 if (max - min).abs() < f64::EPSILON {
94 return 0.5;
95 }
96
97 match self {
98 Self::Linear | Self::Time { .. } => (value - min) / (max - min),
99
100 Self::Logarithmic { base } => {
101 if value <= 0.0 || min <= 0.0 || max <= 0.0 {
102 return (value - min) / (max - min);
104 }
105 let log_value = value.log(*base);
106 let log_min = min.log(*base);
107 let log_max = max.log(*base);
108 (log_value - log_min) / (log_max - log_min)
109 }
110
111 Self::Symlog { lin_threshold } => {
112 let symlog = |x: f64| -> f64 {
113 let thresh = *lin_threshold;
114 if x.abs() < thresh {
115 x / thresh
116 } else {
117 x.signum() * (1.0 + (x.abs() / thresh).ln())
118 }
119 };
120
121 let sym_value = symlog(value);
122 let sym_min = symlog(min);
123 let sym_max = symlog(max);
124 (sym_value - sym_min) / (sym_max - sym_min)
125 }
126 }
127 }
128
129 pub fn denormalize(&self, normalized: f64, min: f64, max: f64) -> f64 {
133 match self {
134 Self::Linear | Self::Time { .. } => min + normalized * (max - min),
135
136 Self::Logarithmic { base } => {
137 if min <= 0.0 || max <= 0.0 {
138 return min + normalized * (max - min);
139 }
140 let log_min = min.log(*base);
141 let log_max = max.log(*base);
142 let log_value = log_min + normalized * (log_max - log_min);
143 base.powf(log_value)
144 }
145
146 Self::Symlog { lin_threshold } => {
147 let thresh = *lin_threshold;
148
149 let symlog = |x: f64| -> f64 {
150 if x.abs() < thresh {
151 x / thresh
152 } else {
153 x.signum() * (1.0 + (x.abs() / thresh).ln())
154 }
155 };
156
157 let inv_symlog = |y: f64| -> f64 {
158 if y.abs() < 1.0 {
159 y * thresh
160 } else {
161 y.signum() * thresh * (y.abs() - 1.0).exp()
162 }
163 };
164
165 let sym_min = symlog(min);
166 let sym_max = symlog(max);
167 let sym_value = sym_min + normalized * (sym_max - sym_min);
168 inv_symlog(sym_value)
169 }
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
179pub struct AxisLink {
180 pub pan_group: Option<u32>,
185
186 pub zoom_group: Option<u32>,
191
192 pub inverted: bool,
196}
197
198impl AxisLink {
199 pub fn none() -> Self {
201 Self::default()
202 }
203
204 pub fn linked(group: u32) -> Self {
206 Self {
207 pan_group: Some(group),
208 zoom_group: Some(group),
209 inverted: false,
210 }
211 }
212
213 pub fn pan_only(group: u32) -> Self {
215 Self {
216 pan_group: Some(group),
217 zoom_group: None,
218 inverted: false,
219 }
220 }
221
222 pub fn zoom_only(group: u32) -> Self {
224 Self {
225 pan_group: None,
226 zoom_group: Some(group),
227 inverted: false,
228 }
229 }
230
231 pub fn inverted(mut self) -> Self {
233 self.inverted = true;
234 self
235 }
236}
237
238#[derive(Debug, Clone, Copy, PartialEq)]
240pub enum ExtendedAxisPosition {
241 Standard(AxisPosition),
243
244 AtValue(f64),
248
249 AtPercent(f32),
253}
254
255impl Default for ExtendedAxisPosition {
256 fn default() -> Self {
257 Self::Standard(AxisPosition::Left)
258 }
259}
260
261impl From<AxisPosition> for ExtendedAxisPosition {
262 fn from(pos: AxisPosition) -> Self {
263 Self::Standard(pos)
264 }
265}
266
267impl ExtendedAxisPosition {
268 pub fn is_standard(&self) -> bool {
270 matches!(self, Self::Standard(_))
271 }
272
273 pub fn standard(&self) -> Option<AxisPosition> {
275 match self {
276 Self::Standard(pos) => Some(*pos),
277 _ => None,
278 }
279 }
280}
281
282#[derive(Debug, Clone)]
284pub struct TickConfig {
285 pub major_count: Option<usize>,
287
288 pub minor_count: usize,
290
291 pub show_labels: bool,
293
294 pub label_format: Option<String>,
297
298 pub custom_ticks: Option<Vec<(f64, String)>>,
300
301 pub label_rotation: f32,
303
304 pub ticks_inward: bool,
306}
307
308impl Default for TickConfig {
309 fn default() -> Self {
310 Self {
311 major_count: Some(5),
312 minor_count: 0,
313 show_labels: true,
314 label_format: None,
315 custom_ticks: None,
316 label_rotation: 0.0,
317 ticks_inward: false,
318 }
319 }
320}
321
322impl TickConfig {
323 pub fn with_count(count: usize) -> Self {
325 Self {
326 major_count: Some(count),
327 ..Default::default()
328 }
329 }
330
331 pub fn custom(ticks: Vec<(f64, String)>) -> Self {
333 Self {
334 custom_ticks: Some(ticks),
335 ..Default::default()
336 }
337 }
338
339 pub fn minor(mut self, count: usize) -> Self {
341 self.minor_count = count;
342 self
343 }
344
345 pub fn no_labels(mut self) -> Self {
347 self.show_labels = false;
348 self
349 }
350
351 pub fn format(mut self, fmt: impl Into<String>) -> Self {
353 self.label_format = Some(fmt.into());
354 self
355 }
356
357 pub fn rotated(mut self, degrees: f32) -> Self {
359 self.label_rotation = degrees;
360 self
361 }
362
363 pub fn inward(mut self) -> Self {
365 self.ticks_inward = true;
366 self
367 }
368}
369
370#[derive(Debug, Clone)]
372pub struct EnhancedAxis {
373 pub id: AxisId,
375
376 pub name: Option<String>,
378
379 pub label: Option<String>,
381
382 pub orientation: AxisOrientation,
384
385 pub position: ExtendedAxisPosition,
387
388 pub position_offset: f32,
390
391 pub min: Option<f64>,
393
394 pub max: Option<f64>,
396
397 pub scale: ScaleType,
399
400 pub ticks: TickConfig,
402
403 pub style: AxisStyle,
405
406 pub visible: bool,
408
409 pub link: AxisLink,
411
412 pub auto_range: bool,
414
415 pub range_padding: f64,
417}
418
419impl Default for EnhancedAxis {
420 fn default() -> Self {
421 Self {
422 id: AxisId::default(),
423 name: None,
424 label: None,
425 orientation: AxisOrientation::Vertical,
426 position: ExtendedAxisPosition::Standard(AxisPosition::Left),
427 position_offset: 0.0,
428 min: None,
429 max: None,
430 scale: ScaleType::Linear,
431 ticks: TickConfig::default(),
432 style: AxisStyle::default(),
433 visible: true,
434 link: AxisLink::none(),
435 auto_range: true,
436 range_padding: 0.05,
437 }
438 }
439}
440
441impl EnhancedAxis {
442 pub fn x() -> Self {
444 Self {
445 id: AxisId::X_PRIMARY,
446 orientation: AxisOrientation::Horizontal,
447 position: ExtendedAxisPosition::Standard(AxisPosition::Bottom),
448 ..Default::default()
449 }
450 }
451
452 pub fn y() -> Self {
454 Self {
455 id: AxisId::Y_PRIMARY,
456 orientation: AxisOrientation::Vertical,
457 position: ExtendedAxisPosition::Standard(AxisPosition::Left),
458 ..Default::default()
459 }
460 }
461
462 pub fn x_secondary() -> Self {
464 Self {
465 id: AxisId::X_SECONDARY,
466 orientation: AxisOrientation::Horizontal,
467 position: ExtendedAxisPosition::Standard(AxisPosition::Top),
468 ..Default::default()
469 }
470 }
471
472 pub fn y_secondary() -> Self {
474 Self {
475 id: AxisId::Y_SECONDARY,
476 orientation: AxisOrientation::Vertical,
477 position: ExtendedAxisPosition::Standard(AxisPosition::Right),
478 ..Default::default()
479 }
480 }
481
482 pub fn custom(id: u32, name: impl Into<String>) -> Self {
484 Self {
485 id: AxisId::custom(id),
486 name: Some(name.into()),
487 ..Default::default()
488 }
489 }
490
491 pub fn with_label(mut self, label: impl Into<String>) -> Self {
493 self.label = Some(label.into());
494 self
495 }
496
497 pub fn with_range(mut self, min: f64, max: f64) -> Self {
499 self.min = Some(min);
500 self.max = Some(max);
501 self.auto_range = false;
502 self
503 }
504
505 pub fn with_scale(mut self, scale: ScaleType) -> Self {
507 self.scale = scale;
508 self
509 }
510
511 pub fn with_position(mut self, position: impl Into<ExtendedAxisPosition>) -> Self {
513 self.position = position.into();
514 self
515 }
516
517 pub fn with_offset(mut self, offset: f32) -> Self {
519 self.position_offset = offset;
520 self
521 }
522
523 pub fn with_ticks(mut self, ticks: TickConfig) -> Self {
525 self.ticks = ticks;
526 self
527 }
528
529 pub fn with_tick_count(mut self, count: usize) -> Self {
531 self.ticks.major_count = Some(count);
532 self
533 }
534
535 pub fn with_style(mut self, style: AxisStyle) -> Self {
537 self.style = style;
538 self
539 }
540
541 pub fn with_visible(mut self, visible: bool) -> Self {
543 self.visible = visible;
544 self
545 }
546
547 pub fn with_link(mut self, link: AxisLink) -> Self {
549 self.link = link;
550 self
551 }
552
553 pub fn auto_ranged(mut self, padding: f64) -> Self {
555 self.auto_range = true;
556 self.range_padding = padding;
557 self
558 }
559
560 pub fn effective_range(&self, data_bounds: Option<(f64, f64)>) -> (f64, f64) {
565 match (self.min, self.max) {
566 (Some(min), Some(max)) => (min, max),
567 (Some(min), None) => {
568 let max = data_bounds.map(|(_, max)| max).unwrap_or(1.0);
569 let padded_max = if self.auto_range {
570 max + (max - min).abs() * self.range_padding
571 } else {
572 max
573 };
574 (min, padded_max)
575 }
576 (None, Some(max)) => {
577 let min = data_bounds.map(|(min, _)| min).unwrap_or(0.0);
578 let padded_min = if self.auto_range {
579 min - (max - min).abs() * self.range_padding
580 } else {
581 min
582 };
583 (padded_min, max)
584 }
585 (None, None) => {
586 let (min, max) = data_bounds.unwrap_or((0.0, 1.0));
587 if self.auto_range {
588 let range = (max - min).abs();
589 let padding = if range < f64::EPSILON {
590 0.5 } else {
592 range * self.range_padding
593 };
594 (min - padding, max + padding)
595 } else {
596 (min, max)
597 }
598 }
599 }
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 #[test]
608 fn test_linear_scale() {
609 let scale = ScaleType::Linear;
610
611 assert!((scale.normalize(50.0, 0.0, 100.0) - 0.5).abs() < 0.001);
612 assert!((scale.denormalize(0.5, 0.0, 100.0) - 50.0).abs() < 0.001);
613 }
614
615 #[test]
616 fn test_log_scale() {
617 let scale = ScaleType::log10();
618
619 let normalized = scale.normalize(10.0, 1.0, 100.0);
621 assert!((normalized - 0.5).abs() < 0.001);
622
623 let denormalized = scale.denormalize(0.5, 1.0, 100.0);
624 assert!((denormalized - 10.0).abs() < 0.001);
625 }
626
627 #[test]
628 fn test_symlog_scale() {
629 let scale = ScaleType::symlog(1.0);
630
631 let norm_zero = scale.normalize(0.0, -10.0, 10.0);
633 assert!((norm_zero - 0.5).abs() < 0.001);
634
635 let value = -5.0;
637 let normalized = scale.normalize(value, -10.0, 10.0);
638 let denormalized = scale.denormalize(normalized, -10.0, 10.0);
639 assert!((denormalized - value).abs() < 0.001);
640 }
641
642 #[test]
643 fn test_axis_effective_range() {
644 let axis = EnhancedAxis::y().auto_ranged(0.1);
645
646 let range = axis.effective_range(Some((0.0, 100.0)));
647 assert!(range.0 < 0.0); assert!(range.1 > 100.0); }
650
651 #[test]
652 fn test_axis_link() {
653 let link = AxisLink::linked(1).inverted();
654
655 assert_eq!(link.pan_group, Some(1));
656 assert_eq!(link.zoom_group, Some(1));
657 assert!(link.inverted);
658 }
659}