1use astrelis_render::Color;
9
10#[derive(Debug, Clone, PartialEq)]
15pub struct DashPattern {
16 pub segments: Vec<f32>,
20
21 pub phase: f32,
23}
24
25impl Default for DashPattern {
26 fn default() -> Self {
27 Self::SOLID
28 }
29}
30
31impl DashPattern {
32 pub const SOLID: DashPattern = DashPattern {
34 segments: Vec::new(),
35 phase: 0.0,
36 };
37
38 pub fn dashed(dash: f32, gap: f32) -> Self {
45 Self {
46 segments: vec![dash, gap],
47 phase: 0.0,
48 }
49 }
50
51 pub fn dotted(size: f32) -> Self {
57 Self {
58 segments: vec![size, size],
59 phase: 0.0,
60 }
61 }
62
63 pub fn dash_dot(dash: f32, dot: f32, gap: f32) -> Self {
71 Self {
72 segments: vec![dash, gap, dot, gap],
73 phase: 0.0,
74 }
75 }
76
77 pub fn dash_dot_dot(dash: f32, dot: f32, gap: f32) -> Self {
79 Self {
80 segments: vec![dash, gap, dot, gap, dot, gap],
81 phase: 0.0,
82 }
83 }
84
85 pub fn custom(segments: Vec<f32>) -> Self {
87 Self {
88 segments,
89 phase: 0.0,
90 }
91 }
92
93 pub fn with_phase(mut self, phase: f32) -> Self {
95 self.phase = phase;
96 self
97 }
98
99 pub fn is_solid(&self) -> bool {
101 self.segments.is_empty()
102 }
103
104 pub fn cycle_length(&self) -> f32 {
106 self.segments.iter().sum()
107 }
108
109 pub fn short_dash() -> Self {
111 Self::dashed(4.0, 2.0)
112 }
113
114 pub fn medium_dash() -> Self {
115 Self::dashed(8.0, 4.0)
116 }
117
118 pub fn long_dash() -> Self {
119 Self::dashed(12.0, 6.0)
120 }
121
122 pub fn fine_dot() -> Self {
123 Self::dotted(1.0)
124 }
125}
126
127#[derive(Debug, Clone, PartialEq)]
129pub struct GridLevel {
130 pub enabled: bool,
132
133 pub thickness: f32,
135
136 pub color: Color,
138
139 pub dash: DashPattern,
141
142 pub z_order: i32,
144}
145
146impl Default for GridLevel {
147 fn default() -> Self {
148 Self {
149 enabled: true,
150 thickness: 1.0,
151 color: Color::rgba(0.25, 0.25, 0.28, 1.0),
152 dash: DashPattern::SOLID,
153 z_order: 0,
154 }
155 }
156}
157
158impl GridLevel {
159 pub fn new() -> Self {
161 Self::default()
162 }
163
164 pub fn disabled() -> Self {
166 Self {
167 enabled: false,
168 ..Default::default()
169 }
170 }
171
172 pub fn with_enabled(mut self, enabled: bool) -> Self {
174 self.enabled = enabled;
175 self
176 }
177
178 pub fn with_thickness(mut self, thickness: f32) -> Self {
180 self.thickness = thickness;
181 self
182 }
183
184 pub fn with_color(mut self, color: Color) -> Self {
186 self.color = color;
187 self
188 }
189
190 pub fn with_dash(mut self, dash: DashPattern) -> Self {
192 self.dash = dash;
193 self
194 }
195
196 pub fn with_z_order(mut self, z_order: i32) -> Self {
198 self.z_order = z_order;
199 self
200 }
201
202 pub fn dotted(mut self) -> Self {
204 self.dash = DashPattern::dotted(2.0);
205 self
206 }
207
208 pub fn dashed(mut self) -> Self {
210 self.dash = DashPattern::medium_dash();
211 self
212 }
213
214 pub fn major() -> Self {
216 Self {
217 enabled: true,
218 thickness: 1.0,
219 color: Color::rgba(0.3, 0.3, 0.33, 1.0),
220 dash: DashPattern::SOLID,
221 z_order: 0,
222 }
223 }
224
225 pub fn minor() -> Self {
227 Self {
228 enabled: true,
229 thickness: 0.5,
230 color: Color::rgba(0.2, 0.2, 0.22, 0.8),
231 dash: DashPattern::SOLID,
232 z_order: -1,
233 }
234 }
235
236 pub fn tertiary() -> Self {
238 Self {
239 enabled: false, thickness: 0.25,
241 color: Color::rgba(0.15, 0.15, 0.17, 0.5),
242 dash: DashPattern::dotted(1.0),
243 z_order: -2,
244 }
245 }
246}
247
248#[derive(Debug, Clone, PartialEq)]
252pub enum GridSpacing {
253 Auto {
255 target_count: usize,
257 },
258
259 Fixed {
261 interval: f64,
263 },
264
265 Custom {
267 values: Vec<f64>,
269 },
270
271 LogDecades {
273 subdivisions: usize,
275 },
276
277 TimeAware,
281}
282
283impl Default for GridSpacing {
284 fn default() -> Self {
285 Self::Auto { target_count: 5 }
286 }
287}
288
289impl GridSpacing {
290 pub fn auto(count: usize) -> Self {
292 Self::Auto {
293 target_count: count,
294 }
295 }
296
297 pub fn fixed(interval: f64) -> Self {
299 Self::Fixed { interval }
300 }
301
302 pub fn custom(values: Vec<f64>) -> Self {
304 Self::Custom { values }
305 }
306
307 pub fn log_decades(subdivisions: usize) -> Self {
309 Self::LogDecades { subdivisions }
310 }
311
312 pub fn calculate_positions(
316 &self,
317 min: f64,
318 max: f64,
319 minor_divisions: usize,
320 ) -> (Vec<f64>, Vec<f64>) {
321 let range = max - min;
322 if range.abs() < f64::EPSILON {
323 return (vec![], vec![]);
324 }
325
326 let (major, minor) = match self {
327 Self::Auto { target_count } => {
328 self.calculate_auto(min, max, *target_count, minor_divisions)
329 }
330
331 Self::Fixed { interval } => {
332 let major = self.calculate_fixed(min, max, *interval);
333 let minor = if minor_divisions > 1 {
334 self.calculate_fixed(min, max, interval / minor_divisions as f64)
335 .into_iter()
336 .filter(|v| !major.iter().any(|m| (v - m).abs() < interval * 0.01))
337 .collect()
338 } else {
339 vec![]
340 };
341 (major, minor)
342 }
343
344 Self::Custom { values } => {
345 let major: Vec<f64> = values
346 .iter()
347 .filter(|&&v| v >= min && v <= max)
348 .copied()
349 .collect();
350 (major, vec![])
351 }
352
353 Self::LogDecades { subdivisions } => {
354 self.calculate_log_decades(min, max, *subdivisions)
355 }
356
357 Self::TimeAware => self.calculate_time_aware(min, max, minor_divisions),
358 };
359
360 (major, minor)
361 }
362
363 fn calculate_auto(
364 &self,
365 min: f64,
366 max: f64,
367 target_count: usize,
368 minor_divisions: usize,
369 ) -> (Vec<f64>, Vec<f64>) {
370 let range = max - min;
371
372 let rough_interval = range / target_count as f64;
374 let magnitude = 10f64.powf(rough_interval.log10().floor());
375 let normalized = rough_interval / magnitude;
376
377 let nice_interval = if normalized < 1.5 {
378 magnitude
379 } else if normalized < 3.0 {
380 2.0 * magnitude
381 } else if normalized < 7.0 {
382 5.0 * magnitude
383 } else {
384 10.0 * magnitude
385 };
386
387 let major = self.calculate_fixed(min, max, nice_interval);
388
389 let minor = if minor_divisions > 1 {
390 let minor_interval = nice_interval / minor_divisions as f64;
391 self.calculate_fixed(min, max, minor_interval)
392 .into_iter()
393 .filter(|v| !major.iter().any(|m| (v - m).abs() < nice_interval * 0.01))
394 .collect()
395 } else {
396 vec![]
397 };
398
399 (major, minor)
400 }
401
402 fn calculate_fixed(&self, min: f64, max: f64, interval: f64) -> Vec<f64> {
403 if interval <= 0.0 {
404 return vec![];
405 }
406
407 let start = (min / interval).ceil() * interval;
408 let mut positions = Vec::new();
409 let mut current = start;
410
411 while current <= max {
412 positions.push(current);
413 current += interval;
414 }
415
416 positions
417 }
418
419 fn calculate_log_decades(
420 &self,
421 min: f64,
422 max: f64,
423 subdivisions: usize,
424 ) -> (Vec<f64>, Vec<f64>) {
425 if min <= 0.0 || max <= 0.0 {
426 return (vec![], vec![]);
427 }
428
429 let log_min = min.log10().floor() as i32;
430 let log_max = max.log10().ceil() as i32;
431
432 let mut major = Vec::new();
433 let mut minor = Vec::new();
434
435 for exp in log_min..=log_max {
436 let decade = 10f64.powi(exp);
437 if decade >= min && decade <= max {
438 major.push(decade);
439 }
440
441 if subdivisions > 1 {
442 let subdivision_values: Vec<f64> = match subdivisions {
443 2 => vec![2.0, 5.0],
444 3 => vec![2.0, 4.0, 6.0, 8.0],
445 _ => vec![2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
446 };
447
448 for &mult in &subdivision_values {
449 let value = decade * mult;
450 if value >= min && value <= max {
451 minor.push(value);
452 }
453 }
454 }
455 }
456
457 (major, minor)
458 }
459
460 fn calculate_time_aware(
461 &self,
462 min: f64,
463 max: f64,
464 minor_divisions: usize,
465 ) -> (Vec<f64>, Vec<f64>) {
466 let range = max - min;
467
468 let interval = if range < 60.0 {
470 self.nice_time_interval(range, &[1.0, 2.0, 5.0, 10.0, 15.0, 30.0])
472 } else if range < 3600.0 {
473 self.nice_time_interval(range, &[60.0, 120.0, 300.0, 600.0, 900.0, 1800.0])
475 } else if range < 86400.0 {
476 self.nice_time_interval(range, &[3600.0, 7200.0, 10800.0, 21600.0, 43200.0])
478 } else if range < 604800.0 {
479 self.nice_time_interval(range, &[86400.0, 172800.0])
481 } else {
482 self.nice_time_interval(range, &[604800.0, 2592000.0])
484 };
485
486 let major = self.calculate_fixed(min, max, interval);
487
488 let minor = if minor_divisions > 1 {
489 let minor_interval = interval / minor_divisions as f64;
490 self.calculate_fixed(min, max, minor_interval)
491 .into_iter()
492 .filter(|v| !major.iter().any(|m| (v - m).abs() < interval * 0.01))
493 .collect()
494 } else {
495 vec![]
496 };
497
498 (major, minor)
499 }
500
501 fn nice_time_interval(&self, range: f64, candidates: &[f64]) -> f64 {
502 let target_count = 5;
503 let ideal_interval = range / target_count as f64;
504
505 candidates
506 .iter()
507 .copied()
508 .min_by(|&a, &b| {
509 let a_diff = (a - ideal_interval).abs();
510 let b_diff = (b - ideal_interval).abs();
511 a_diff.partial_cmp(&b_diff).unwrap()
512 })
513 .unwrap_or(ideal_interval)
514 }
515}
516
517#[derive(Debug, Clone)]
519pub struct GridConfig {
520 pub major: GridLevel,
522
523 pub minor: Option<GridLevel>,
525
526 pub tertiary: Option<GridLevel>,
528
529 pub spacing: GridSpacing,
531
532 pub minor_divisions: usize,
534
535 pub extend_beyond_plot: bool,
537}
538
539impl Default for GridConfig {
540 fn default() -> Self {
541 Self {
542 major: GridLevel::major(),
543 minor: None,
544 tertiary: None,
545 spacing: GridSpacing::default(),
546 minor_divisions: 4,
547 extend_beyond_plot: false,
548 }
549 }
550}
551
552impl GridConfig {
553 pub fn new() -> Self {
555 Self::default()
556 }
557
558 pub fn none() -> Self {
560 Self {
561 major: GridLevel::disabled(),
562 minor: None,
563 tertiary: None,
564 ..Default::default()
565 }
566 }
567
568 pub fn minimal() -> Self {
570 Self {
571 major: GridLevel::major(),
572 minor: None,
573 tertiary: None,
574 ..Default::default()
575 }
576 }
577
578 pub fn detailed() -> Self {
580 Self {
581 major: GridLevel::major(),
582 minor: Some(GridLevel::minor()),
583 tertiary: None,
584 minor_divisions: 5,
585 ..Default::default()
586 }
587 }
588
589 pub fn fine() -> Self {
591 Self {
592 major: GridLevel::major(),
593 minor: Some(GridLevel::minor()),
594 tertiary: Some(GridLevel::tertiary().with_enabled(true)),
595 minor_divisions: 5,
596 ..Default::default()
597 }
598 }
599
600 pub fn with_major(mut self, major: GridLevel) -> Self {
602 self.major = major;
603 self
604 }
605
606 pub fn with_minor(mut self, minor: GridLevel) -> Self {
608 self.minor = Some(minor);
609 self
610 }
611
612 pub fn with_tertiary(mut self, tertiary: GridLevel) -> Self {
614 self.tertiary = Some(tertiary);
615 self
616 }
617
618 pub fn with_spacing(mut self, spacing: GridSpacing) -> Self {
620 self.spacing = spacing;
621 self
622 }
623
624 pub fn with_minor_divisions(mut self, divisions: usize) -> Self {
626 self.minor_divisions = divisions;
627 self
628 }
629
630 pub fn extend_beyond(mut self) -> Self {
632 self.extend_beyond_plot = true;
633 self
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640
641 #[test]
642 fn test_dash_pattern_cycle_length() {
643 let dashed = DashPattern::dashed(10.0, 5.0);
644 assert_eq!(dashed.cycle_length(), 15.0);
645
646 let solid = DashPattern::SOLID;
647 assert_eq!(solid.cycle_length(), 0.0);
648 assert!(solid.is_solid());
649 }
650
651 #[test]
652 fn test_grid_spacing_auto() {
653 let spacing = GridSpacing::auto(5);
654 let (major, _minor) = spacing.calculate_positions(0.0, 100.0, 2);
655
656 assert!(!major.is_empty());
657 for &pos in &major {
659 assert!((0.0..=100.0).contains(&pos));
661 }
662 }
663
664 #[test]
665 fn test_grid_spacing_fixed() {
666 let spacing = GridSpacing::fixed(10.0);
667 let (major, _) = spacing.calculate_positions(0.0, 50.0, 1);
668
669 assert_eq!(major, vec![0.0, 10.0, 20.0, 30.0, 40.0, 50.0]);
670 }
671
672 #[test]
673 fn test_grid_spacing_log_decades() {
674 let spacing = GridSpacing::log_decades(2);
675 let (major, minor) = spacing.calculate_positions(1.0, 1000.0, 1);
676
677 assert!(major.contains(&1.0));
678 assert!(major.contains(&10.0));
679 assert!(major.contains(&100.0));
680 assert!(major.contains(&1000.0));
681 assert!(!minor.is_empty());
682 }
683
684 #[test]
685 fn test_grid_config_presets() {
686 let minimal = GridConfig::minimal();
687 assert!(minimal.major.enabled);
688 assert!(minimal.minor.is_none());
689
690 let detailed = GridConfig::detailed();
691 assert!(detailed.major.enabled);
692 assert!(detailed.minor.is_some());
693 assert!(detailed.minor.as_ref().unwrap().enabled);
694 }
695}