1use crate::color::Color;
14use crate::config::{AxisOptions, Config, TickVisibility};
15use crate::data_config::{DataRenderType, SeriesConfig};
16
17const fn rgb8(r: u8, g: u8, b: u8) -> Color {
18 Color::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0)
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum AxisPreset {
34 BoxedInward,
37 BoxedOutward,
40 OpenOutward,
43 OpenInward,
46 Minimal,
49}
50
51#[derive(Clone)]
53struct SideStyle {
54 line: bool,
55 tick: TickVisibility,
56 labels: bool,
57}
58
59fn apply_side(axis: &mut AxisOptions, s: SideStyle) {
60 axis.line_visible = s.line;
61 axis.tick = s.tick;
62 axis.label_style.label_visible = s.labels;
63}
64
65impl Config {
66 pub fn apply_axis_preset(&mut self, preset: AxisPreset) {
69 use TickVisibility::{Inside, None as NoTick, Outside};
70
71 let (labeled, unlabeled) = match preset {
73 AxisPreset::BoxedInward => (
74 SideStyle {
75 line: true,
76 tick: Inside,
77 labels: true,
78 },
79 SideStyle {
80 line: true,
81 tick: Inside,
82 labels: false,
83 },
84 ),
85 AxisPreset::BoxedOutward => (
86 SideStyle {
87 line: true,
88 tick: Outside,
89 labels: true,
90 },
91 SideStyle {
92 line: true,
93 tick: NoTick,
94 labels: false,
95 },
96 ),
97 AxisPreset::OpenOutward => (
98 SideStyle {
99 line: true,
100 tick: Outside,
101 labels: true,
102 },
103 SideStyle {
104 line: false,
105 tick: NoTick,
106 labels: false,
107 },
108 ),
109 AxisPreset::OpenInward => (
110 SideStyle {
111 line: true,
112 tick: Inside,
113 labels: true,
114 },
115 SideStyle {
116 line: false,
117 tick: NoTick,
118 labels: false,
119 },
120 ),
121 AxisPreset::Minimal => (
122 SideStyle {
123 line: false,
124 tick: NoTick,
125 labels: true,
126 },
127 SideStyle {
128 line: false,
129 tick: NoTick,
130 labels: false,
131 },
132 ),
133 };
134
135 apply_side(&mut self.bottom_x, labeled.clone());
136 apply_side(&mut self.left_y, labeled);
137 apply_side(&mut self.top_x, unlabeled.clone());
138 apply_side(&mut self.right_y, unlabeled);
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum ColorCycle {
150 Classic,
153 Vivid,
156 Balanced,
159 ColorblindSafe,
162 Monochrome,
164}
165
166const CLASSIC: &[Color] = &[
167 rgb8(0, 0, 0), rgb8(255, 0, 0), rgb8(0, 128, 0), rgb8(0, 0, 255), rgb8(255, 0, 255), rgb8(0, 139, 139), rgb8(184, 134, 11), rgb8(0, 0, 128), rgb8(128, 0, 128), rgb8(128, 0, 0), rgb8(128, 128, 0), rgb8(255, 102, 0), ];
180
181const VIVID: &[Color] = &[
182 rgb8(0, 32, 240), rgb8(230, 16, 16), rgb8(0, 153, 0), rgb8(128, 0, 192), rgb8(255, 128, 0), rgb8(0, 0, 0), rgb8(230, 0, 172), rgb8(0, 153, 153), ];
191
192const BALANCED: &[Color] = &[
193 rgb8(0x1f, 0x77, 0xb4),
194 rgb8(0xff, 0x7f, 0x0e),
195 rgb8(0x2c, 0xa0, 0x2c),
196 rgb8(0xd6, 0x27, 0x28),
197 rgb8(0x94, 0x67, 0xbd),
198 rgb8(0x8c, 0x56, 0x4b),
199 rgb8(0xe3, 0x77, 0xc2),
200 rgb8(0x7f, 0x7f, 0x7f),
201 rgb8(0xbc, 0xbd, 0x22),
202 rgb8(0x17, 0xbe, 0xcf),
203];
204
205const COLORBLIND_SAFE: &[Color] = &[
206 rgb8(0xe6, 0x9f, 0x00), rgb8(0x56, 0xb4, 0xe9), rgb8(0x00, 0x9e, 0x73), rgb8(0xf0, 0xe4, 0x42), rgb8(0x00, 0x72, 0xb2), rgb8(0xd5, 0x5e, 0x00), rgb8(0xcc, 0x79, 0xa7), rgb8(0x00, 0x00, 0x00), ];
215
216const MONOCHROME: &[Color] = &[
217 rgb8(0, 0, 0),
218 rgb8(89, 89, 89),
219 rgb8(140, 140, 140),
220 rgb8(191, 191, 191),
221];
222
223impl ColorCycle {
224 pub fn colors(self) -> &'static [Color] {
226 match self {
227 Self::Classic => CLASSIC,
228 Self::Vivid => VIVID,
229 Self::Balanced => BALANCED,
230 Self::ColorblindSafe => COLORBLIND_SAFE,
231 Self::Monochrome => MONOCHROME,
232 }
233 }
234
235 pub fn color(self, index: usize) -> Color {
238 let palette = self.colors();
239 palette[index % palette.len()]
240 }
241
242 pub fn len(self) -> usize {
243 self.colors().len()
244 }
245
246 pub fn is_empty(self) -> bool {
247 false
248 }
249
250 pub fn apply_to_series(self, series: &mut SeriesConfig, index: usize) {
255 let c = self.color(index);
256 match &mut series.render_type {
257 DataRenderType::Scatter { scatter } => {
258 scatter.point_color = c;
259 }
260 DataRenderType::Line { line } => {
261 line.line_color = c;
262 }
263 DataRenderType::ScatterLine { scatter, line } => {
264 scatter.point_color = c;
265 line.line_color = c;
266 }
267 DataRenderType::ScatterErrorbarX {
268 scatter, err_style, ..
269 }
270 | DataRenderType::ScatterErrorbarY {
271 scatter, err_style, ..
272 }
273 | DataRenderType::ScatterErrorbarXY {
274 scatter, err_style, ..
275 } => {
276 scatter.point_color = c;
277 err_style.error_bar_color = c;
278 }
279 DataRenderType::LineScatterErrorbarX {
280 scatter,
281 line,
282 err_style,
283 ..
284 }
285 | DataRenderType::LineScatterErrorbarY {
286 scatter,
287 line,
288 err_style,
289 ..
290 }
291 | DataRenderType::LineScatterErrorbarXY {
292 scatter,
293 line,
294 err_style,
295 ..
296 } => {
297 scatter.point_color = c;
298 line.line_color = c;
299 err_style.error_bar_color = c;
300 }
301 DataRenderType::Histogram { bar } => {
302 bar.fill_color = c;
303 }
304 DataRenderType::Contour { contour, .. }
308 | DataRenderType::HeatmapContour { contour, .. } => {
309 contour.line.line_color = c;
310 }
311 DataRenderType::Heatmap { .. } => {}
312 }
313 }
314
315 pub fn apply_to_all(self, series: &mut [SeriesConfig]) {
317 for (i, s) in series.iter_mut().enumerate() {
318 self.apply_to_series(s, i);
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::data_config::{
327 DataErrorBarStyleConfig, DataLineStyleConfig, DataScatterStyleConfig, ErrorRef,
328 ScatterShape,
329 };
330 use crate::default::default_config;
331 use crate::line::LineStylePreset;
332
333 #[test]
334 fn boxed_inward_shows_full_frame_with_inside_ticks() {
335 let mut cfg = default_config();
336 cfg.apply_axis_preset(AxisPreset::BoxedInward);
337 for axis in [&cfg.top_x, &cfg.bottom_x, &cfg.left_y, &cfg.right_y] {
338 assert!(axis.line_visible);
339 assert_eq!(axis.tick, TickVisibility::Inside);
340 }
341 assert!(cfg.bottom_x.label_style.label_visible);
342 assert!(cfg.left_y.label_style.label_visible);
343 assert!(!cfg.top_x.label_style.label_visible);
344 assert!(!cfg.right_y.label_style.label_visible);
345 }
346
347 #[test]
348 fn open_outward_hides_top_right_entirely() {
349 let mut cfg = default_config();
350 cfg.apply_axis_preset(AxisPreset::OpenOutward);
351 assert!(cfg.bottom_x.line_visible);
352 assert_eq!(cfg.bottom_x.tick, TickVisibility::Outside);
353 assert!(!cfg.top_x.line_visible);
354 assert_eq!(cfg.top_x.tick, TickVisibility::None);
355 assert!(!cfg.right_y.line_visible);
356 }
357
358 #[test]
359 fn minimal_drops_lines_and_ticks_but_keeps_labels() {
360 let mut cfg = default_config();
361 cfg.apply_axis_preset(AxisPreset::Minimal);
362 for axis in [&cfg.top_x, &cfg.bottom_x, &cfg.left_y, &cfg.right_y] {
363 assert!(!axis.line_visible);
364 assert_eq!(axis.tick, TickVisibility::None);
365 }
366 assert!(cfg.bottom_x.label_style.label_visible);
367 }
368
369 #[test]
371 fn preset_leaves_ranges_margins_titles_untouched() {
372 let mut cfg = default_config();
373 let before = cfg.clone();
374 cfg.apply_axis_preset(AxisPreset::OpenOutward);
375
376 assert_eq!(cfg.bottom_x.min, before.bottom_x.min);
377 assert_eq!(cfg.bottom_x.max, before.bottom_x.max);
378 assert_eq!(cfg.bottom_x.out_margin, before.bottom_x.out_margin);
379 assert_eq!(
380 cfg.left_y.major_tick_length,
381 before.left_y.major_tick_length
382 );
383 assert_eq!(cfg.chart_title, before.chart_title);
384 assert_eq!(cfg.grid, before.grid);
385 assert_eq!(cfg.data_area().unwrap(), before.data_area().unwrap());
386 }
387
388 #[test]
389 fn cycle_color_wraps_around() {
390 for cycle in [
391 ColorCycle::Classic,
392 ColorCycle::Vivid,
393 ColorCycle::Balanced,
394 ColorCycle::ColorblindSafe,
395 ColorCycle::Monochrome,
396 ] {
397 let n = cycle.len();
398 assert!(n >= 4, "{cycle:?} palette too small");
399 assert_eq!(cycle.color(0), cycle.color(n));
400 assert_eq!(cycle.color(3), cycle.color(3 + n * 2));
401 assert_ne!(cycle.color(0), cycle.color(1));
403 }
404 }
405
406 fn full_series() -> SeriesConfig {
407 SeriesConfig {
408 series_id: "s".into(),
409 source_id: None,
410 label: None,
411 x_column: "x".into(),
412 y_column: "y".into(),
413 render_type: DataRenderType::LineScatterErrorbarXY {
414 scatter: DataScatterStyleConfig {
415 point_color: Color::BLACK,
416 point_shape: ScatterShape::Circle,
417 point_size: 4.0,
418 point_style_table: None,
419 point_style_index_column: None,
420 point_style_overrides: None,
421 },
422 line: DataLineStyleConfig {
423 line_style: LineStylePreset::Solid,
424 line_color: Color::BLACK,
425 line_width: 1.0,
426 },
427 err_x: ErrorRef::Symmetric {
428 column: "ex".into(),
429 },
430 err_y: ErrorRef::Symmetric {
431 column: "ey".into(),
432 },
433 err_style: DataErrorBarStyleConfig {
434 error_bar_color: Color::BLACK,
435 error_bar_width: 1.0,
436 error_bar_cap_size: 3.0,
437 cap_width: 1.0,
438 error_bar_style_table: None,
439 error_bar_style_index_column: None,
440 error_bar_style_overrides: None,
441 },
442 },
443 }
444 }
445
446 #[test]
447 fn apply_to_series_recolors_every_sub_style() {
448 let mut s = full_series();
449 ColorCycle::Balanced.apply_to_series(&mut s, 2);
450 let expected = ColorCycle::Balanced.color(2);
451 let DataRenderType::LineScatterErrorbarXY {
452 scatter,
453 line,
454 err_style,
455 ..
456 } = &s.render_type
457 else {
458 unreachable!()
459 };
460 assert_eq!(scatter.point_color, expected);
461 assert_eq!(line.line_color, expected);
462 assert_eq!(err_style.error_bar_color, expected);
463 }
464
465 #[test]
466 fn apply_to_all_assigns_sequential_rotation() {
467 let mut list = vec![full_series(), full_series(), full_series()];
468 ColorCycle::Classic.apply_to_all(&mut list);
469 for (i, s) in list.iter().enumerate() {
470 let DataRenderType::LineScatterErrorbarXY { line, .. } = &s.render_type else {
471 unreachable!()
472 };
473 assert_eq!(line.line_color, ColorCycle::Classic.color(i));
474 }
475 }
476
477 #[test]
480 fn the_cycle_recolors_bars_and_contour_lines_but_never_a_field() {
481 use crate::data_config::{
482 BarOrientation, ContourConfig, DataBarStyleConfig, FieldFillConfig, FillMode,
483 GridLayout, MatrixOrientation, MatrixRef, Shading,
484 };
485
486 let matrix = || MatrixRef {
487 columns: vec!["z0".into()],
488 orientation: MatrixOrientation::ColumnsAreX,
489 grid_layout: GridLayout::Centers,
490 };
491 let fill = || FieldFillConfig {
492 mode: FillMode::Continuous,
493 shading: Shading::Flat,
494 opacity: 1.0,
495 };
496 let contour = || ContourConfig {
497 levels: vec![1.0],
498 line: DataLineStyleConfig {
499 line_style: LineStylePreset::Solid,
500 line_color: Color::BLACK,
501 line_width: 1.0,
502 },
503 per_level_color: None,
504 labels: None,
505 };
506 let series = |render_type| SeriesConfig {
507 series_id: "s".into(),
508 source_id: None,
509 label: None,
510 x_column: "x".into(),
511 y_column: "y".into(),
512 render_type,
513 };
514 let expected = ColorCycle::Classic.color(2);
515
516 let mut histogram = series(DataRenderType::Histogram {
517 bar: DataBarStyleConfig {
518 fill_color: Color::BLACK,
519 border_color: Color::BLACK,
520 border_width: 1.0,
521 baseline: 0.0,
522 gap_px: 1.0,
523 width_ratio: 1.0,
524 orientation: BarOrientation::Vertical,
525 bar_style_overrides: None,
526 },
527 });
528 ColorCycle::Classic.apply_to_series(&mut histogram, 2);
529 let DataRenderType::Histogram { bar } = &histogram.render_type else {
530 unreachable!()
531 };
532 assert_eq!(bar.fill_color, expected);
533
534 for render_type in [
535 DataRenderType::Contour {
536 matrix: matrix(),
537 contour: contour(),
538 },
539 DataRenderType::HeatmapContour {
540 matrix: matrix(),
541 fill: fill(),
542 contour: contour(),
543 },
544 ] {
545 let mut item = series(render_type);
546 ColorCycle::Classic.apply_to_series(&mut item, 2);
547 let line_color = match &item.render_type {
548 DataRenderType::Contour { contour, .. }
549 | DataRenderType::HeatmapContour { contour, .. } => contour.line.line_color,
550 _ => unreachable!(),
551 };
552 assert_eq!(line_color, expected);
553 }
554
555 let mut heatmap = series(DataRenderType::Heatmap {
558 matrix: matrix(),
559 fill: fill(),
560 });
561 let before = heatmap.clone();
562 ColorCycle::Classic.apply_to_series(&mut heatmap, 2);
563 assert_eq!(heatmap, before);
564 }
565}