1use crate::config::{AxisOptions, BarAlign, ColorBarOptions, LegendCorner, TickVisibility};
2use crate::text::TextExtents;
3
4use super::{ChartArea, DataArea, RectF, Side};
5
6pub const LABEL_GAP: f32 = 4.0;
8
9pub const LEGEND_INSET: f32 = 6.0;
11
12pub fn axis_anchor(side: Side, data_area: &DataArea) -> ((f32, f32), (f32, f32)) {
15 let x0 = data_area.x as f32 + 0.5;
16 let y0 = data_area.y as f32 + 0.5;
17 let x1 = (data_area.x + data_area.width) as f32 - 0.5;
18 let y1 = (data_area.y + data_area.height) as f32 - 0.5;
19 match side {
20 Side::Top => ((x0, y0), (x1, y0)),
21 Side::Bottom => ((x0, y1), (x1, y1)),
22 Side::Left => ((x0, y0), (x0, y1)),
23 Side::Right => ((x1, y0), (x1, y1)),
24 }
25}
26
27pub fn axis_offset(side: Side, line_offset: f32) -> (f32, f32) {
29 match side {
30 Side::Left | Side::Right => (line_offset, 0.0),
31 Side::Top | Side::Bottom => (0.0, line_offset),
32 }
33}
34
35pub fn axis_visibility_extent(axis: &AxisOptions) -> (f32, f32) {
37 let tick = axis.major_tick_length;
38 let (inward, outward) = match axis.tick {
39 TickVisibility::None => (0.0, 0.0),
40 TickVisibility::Outside => (0.0, tick),
41 TickVisibility::Inside => (tick, 0.0),
42 TickVisibility::Both => (tick, tick),
43 };
44 let half_line = if axis.line_visible {
45 axis.line_width.max(1.0) * 0.5
46 } else {
47 0.0
48 };
49 (inward.max(half_line), outward.max(half_line))
50}
51
52pub fn axis_visibility_rect(side: Side, data_area: &DataArea, axis: &AxisOptions) -> RectF {
54 let (inward, outward) = axis_visibility_extent(axis);
55 let rect = match side.clone() {
56 Side::Top => RectF {
57 x: data_area.x as f32,
58 y: data_area.y as f32 - outward,
59 width: data_area.width as f32,
60 height: inward + outward,
61 },
62 Side::Bottom => RectF {
63 x: data_area.x as f32,
64 y: (data_area.y + data_area.height) as f32 - inward,
65 width: data_area.width as f32,
66 height: inward + outward,
67 },
68 Side::Left => RectF {
69 x: data_area.x as f32 - outward,
70 y: data_area.y as f32,
71 width: inward + outward,
72 height: data_area.height as f32,
73 },
74 Side::Right => RectF {
75 x: (data_area.x + data_area.width) as f32 - inward,
76 y: data_area.y as f32,
77 width: inward + outward,
78 height: data_area.height as f32,
79 },
80 };
81 let (dx, dy) = axis_offset(side, axis.line_offset);
82 rect.translated(dx, dy)
83}
84
85pub fn rect_axis_visibility_rect(side: Side, rect: &RectF, axis: &AxisOptions) -> RectF {
90 let (inward, outward) = axis_visibility_extent(axis);
91 let bounds = match side.clone() {
92 Side::Top => RectF {
93 x: rect.x,
94 y: rect.y - outward,
95 width: rect.width,
96 height: inward + outward,
97 },
98 Side::Bottom => RectF {
99 x: rect.x,
100 y: rect.y + rect.height - inward,
101 width: rect.width,
102 height: inward + outward,
103 },
104 Side::Left => RectF {
105 x: rect.x - outward,
106 y: rect.y,
107 width: inward + outward,
108 height: rect.height,
109 },
110 Side::Right => RectF {
111 x: rect.x + rect.width - inward,
112 y: rect.y,
113 width: inward + outward,
114 height: rect.height,
115 },
116 };
117 let (dx, dy) = axis_offset(side, axis.line_offset);
118 bounds.translated(dx, dy)
119}
120
121pub fn point_on_rect_side(t: f32, side: &Side, rect: &RectF) -> (f32, f32) {
126 match side {
127 Side::Top => (rect.x + t * rect.width, rect.y),
128 Side::Bottom => (rect.x + t * rect.width, rect.y + rect.height),
129 Side::Left => (rect.x, rect.y + rect.height - t * rect.height),
130 Side::Right => (rect.x + rect.width, rect.y + rect.height - t * rect.height),
131 }
132}
133
134pub fn label_origin(
136 side: Side,
137 tick_position: (f32, f32),
138 outward: f32,
139 offset: (f32, f32),
140 extents: TextExtents,
141) -> (f32, f32) {
142 let (x, y) = match side {
143 Side::Top => (
144 tick_position.0 - extents.width * 0.5,
145 tick_position.1 - outward - LABEL_GAP - extents.descent,
146 ),
147 Side::Bottom => (
148 tick_position.0 - extents.width * 0.5,
149 tick_position.1 + outward + LABEL_GAP + extents.ascent,
150 ),
151 Side::Left => (
152 tick_position.0 - outward - LABEL_GAP - extents.width,
153 tick_position.1 + (extents.ascent - extents.descent) * 0.5,
154 ),
155 Side::Right => (
156 tick_position.0 + outward + LABEL_GAP,
157 tick_position.1 + (extents.ascent - extents.descent) * 0.5,
158 ),
159 };
160 (x + offset.0, y + offset.1)
161}
162
163pub fn label_rect(origin: (f32, f32), extents: TextExtents) -> RectF {
165 RectF {
166 x: origin.0,
167 y: origin.1 - extents.ascent,
168 width: extents.width,
169 height: extents.height(),
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq)]
175pub struct TitlePlacement {
176 pub origin: (f32, f32),
177 pub rotation_center: (f32, f32),
178 pub rotation_degrees: f32,
179}
180
181impl TitlePlacement {
182 pub fn rect(self, extents: TextExtents) -> RectF {
184 let local = label_rect(self.origin, extents);
185 match self.rotation_degrees {
186 -90.0 => RectF {
187 x: self.rotation_center.0 + (local.y - self.rotation_center.1),
188 y: self.rotation_center.1 - (local.x + local.width - self.rotation_center.0),
189 width: local.height,
190 height: local.width,
191 },
192 90.0 => RectF {
193 x: self.rotation_center.0 - (local.y + local.height - self.rotation_center.1),
194 y: self.rotation_center.1 + (local.x - self.rotation_center.0),
195 width: local.height,
196 height: local.width,
197 },
198 _ => local,
199 }
200 }
201}
202
203pub fn chart_title_placement(
204 chart_area: &ChartArea,
205 top_margin: f32,
206 offset: (f32, f32),
207 extents: TextExtents,
208) -> TitlePlacement {
209 let origin = (
210 chart_area.x as f32 + chart_area.width as f32 * 0.5 - extents.width * 0.5 + offset.0,
211 chart_area.y as f32 + (top_margin - extents.height()) * 0.5 + extents.ascent + offset.1,
212 );
213 TitlePlacement {
214 origin,
215 rotation_center: origin,
216 rotation_degrees: 0.0,
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq)]
226pub struct TitleBand {
227 pub out_margin: f32,
229 pub chart_title_margin: f32,
232 pub edge_inset: f32,
239}
240
241pub fn axis_title_placement(
243 side: Side,
244 chart_area: &ChartArea,
245 data_area: &DataArea,
246 band: TitleBand,
247 offset: (f32, f32),
248 extents: TextExtents,
249) -> TitlePlacement {
250 let TitleBand {
251 out_margin,
252 chart_title_margin,
253 edge_inset,
254 } = band;
255 match side {
256 Side::Top | Side::Bottom => {
257 let band_top = if matches!(side, Side::Top) {
258 chart_area.y as f32 + chart_title_margin + edge_inset
259 } else {
260 (chart_area.y + chart_area.height) as f32 - out_margin - edge_inset
261 };
262 let origin = (
263 data_area.x as f32 + data_area.width as f32 * 0.5 - extents.width * 0.5 + offset.0,
264 band_top + (out_margin - extents.height()) * 0.5 + extents.ascent + offset.1,
265 );
266 TitlePlacement {
267 origin,
268 rotation_center: origin,
269 rotation_degrees: 0.0,
270 }
271 }
272 Side::Left | Side::Right => {
273 let center = (
274 if matches!(side, Side::Left) {
275 chart_area.x as f32 + edge_inset + out_margin * 0.5
276 } else {
277 (chart_area.x + chart_area.width) as f32 - edge_inset - out_margin * 0.5
278 },
279 data_area.y as f32 + data_area.height as f32 * 0.5,
280 );
281 TitlePlacement {
282 origin: (
283 center.0 - extents.width * 0.5 + offset.0,
284 center.1 + (extents.ascent - extents.descent) * 0.5 + offset.1,
285 ),
286 rotation_center: center,
287 rotation_degrees: if matches!(side, Side::Left) {
288 -90.0
289 } else {
290 90.0
291 },
292 }
293 }
294 }
295}
296
297pub fn colorbar_title_placement(
304 side: Side,
305 strip: &RectF,
306 axis: &AxisOptions,
307 offset: (f32, f32),
308 extents: TextExtents,
309) -> TitlePlacement {
310 let tick = axis.major_tick_length.max(0.0);
311 let out_margin = axis.out_margin.max(0.0);
312 match side {
313 Side::Top | Side::Bottom => {
314 let band_top = if matches!(side, Side::Top) {
315 strip.y - tick - out_margin
316 } else {
317 strip.y + strip.height + tick
318 };
319 let origin = (
320 strip.x + strip.width * 0.5 - extents.width * 0.5 + offset.0,
321 band_top + (out_margin - extents.height()) * 0.5 + extents.ascent + offset.1,
322 );
323 TitlePlacement {
324 origin,
325 rotation_center: origin,
326 rotation_degrees: 0.0,
327 }
328 }
329 Side::Left | Side::Right => {
330 let center = (
331 if matches!(side, Side::Left) {
332 strip.x - tick - out_margin * 0.5
333 } else {
334 strip.x + strip.width + tick + out_margin * 0.5
335 },
336 strip.y + strip.height * 0.5,
337 );
338 TitlePlacement {
339 origin: (
340 center.0 - extents.width * 0.5 + offset.0,
341 center.1 + (extents.ascent - extents.descent) * 0.5 + offset.1,
342 ),
343 rotation_center: center,
344 rotation_degrees: if matches!(side, Side::Left) {
345 -90.0
346 } else {
347 90.0
348 },
349 }
350 }
351 }
352}
353
354pub fn axis_title_offset_to_screen(side: Side, offset: (f32, f32)) -> (f32, f32) {
356 match side {
357 Side::Left => (offset.1, -offset.0),
358 Side::Right => (-offset.1, offset.0),
359 Side::Top | Side::Bottom => offset,
360 }
361}
362
363pub fn screen_offset_to_axis_title(side: Side, offset: (f32, f32)) -> (f32, f32) {
365 match side {
366 Side::Left => (-offset.1, offset.0),
367 Side::Right => (offset.1, -offset.0),
368 Side::Top | Side::Bottom => offset,
369 }
370}
371
372pub fn legend_rect(
373 data_area: &DataArea,
374 corner: LegendCorner,
375 padding: f32,
376 offset: (f32, f32),
377 extents: TextExtents,
378) -> RectF {
379 let width = extents.width + padding * 2.0;
380 let height = extents.height() + padding * 2.0;
381 let x = match corner {
382 LegendCorner::TopLeft | LegendCorner::BottomLeft => data_area.x as f32 + LEGEND_INSET,
383 LegendCorner::TopRight | LegendCorner::BottomRight => {
384 (data_area.x + data_area.width) as f32 - width - LEGEND_INSET
385 }
386 };
387 let y = match corner {
388 LegendCorner::TopLeft | LegendCorner::TopRight => data_area.y as f32 + LEGEND_INSET,
389 LegendCorner::BottomLeft | LegendCorner::BottomRight => {
390 (data_area.y + data_area.height) as f32 - height - LEGEND_INSET
391 }
392 };
393 RectF {
394 x: x + offset.0,
395 y: y + offset.1,
396 width,
397 height,
398 }
399}
400
401pub fn colorbar_rect(
430 chart_area: &ChartArea,
431 data_area: &DataArea,
432 chart_title_margin: f32,
433 bar: &ColorBarOptions,
434) -> RectF {
435 let frac = if bar.length_frac.is_finite() {
436 bar.length_frac.clamp(0.0, 1.0)
437 } else {
438 0.0
439 };
440 let thickness = bar.thickness_px.max(0.0);
441 let outer = bar.axis.out_margin.max(0.0) + bar.axis.major_tick_length.max(0.0) + thickness;
444 let (dx, dy, dw, dh) = (
445 data_area.x as f32,
446 data_area.y as f32,
447 data_area.width as f32,
448 data_area.height as f32,
449 );
450 let (cx, cy, cw, ch) = (
451 chart_area.x as f32,
452 chart_area.y as f32,
453 chart_area.width as f32,
454 chart_area.height as f32,
455 );
456
457 let vertical_length = dh * frac;
460 let horizontal_length = dw * frac;
461 let anchored = match bar.side {
462 Side::Left => RectF {
463 x: cx + outer - thickness,
464 y: dy + align_offset(dh, vertical_length, &bar.align),
465 width: thickness,
466 height: vertical_length,
467 },
468 Side::Right => RectF {
469 x: cx + cw - outer,
470 y: dy + align_offset(dh, vertical_length, &bar.align),
471 width: thickness,
472 height: vertical_length,
473 },
474 Side::Top => RectF {
478 x: dx + align_offset(dw, horizontal_length, &bar.align),
479 y: cy + chart_title_margin + outer - thickness,
480 width: horizontal_length,
481 height: thickness,
482 },
483 Side::Bottom => RectF {
484 x: dx + align_offset(dw, horizontal_length, &bar.align),
485 y: cy + ch - outer,
486 width: horizontal_length,
487 height: thickness,
488 },
489 };
490 anchored.translated(bar.offset_x, bar.offset_y)
491}
492
493fn align_offset(available: f32, length: f32, align: &BarAlign) -> f32 {
496 match align {
497 BarAlign::Start => 0.0,
498 BarAlign::Center => (available - length) * 0.5,
499 BarAlign::End => available - length,
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use crate::default::default_config;
507 use crate::layout::Rect;
508
509 fn areas() -> (ChartArea, DataArea) {
510 (
511 ChartArea(Rect {
512 x: 10,
513 y: 20,
514 width: 300,
515 height: 200,
516 }),
517 DataArea(Rect {
518 x: 50,
519 y: 60,
520 width: 200,
521 height: 100,
522 }),
523 )
524 }
525
526 fn band(out_margin: f32, chart_title_margin: f32, edge_inset: f32) -> TitleBand {
527 TitleBand {
528 out_margin,
529 chart_title_margin,
530 edge_inset,
531 }
532 }
533
534 fn extents() -> TextExtents {
535 TextExtents {
536 width: 20.0,
537 ascent: 7.0,
538 descent: 3.0,
539 }
540 }
541
542 #[test]
543 fn axis_anchors_and_visibility_preserve_half_pixels() {
544 let (_, data) = areas();
545 assert_eq!(axis_anchor(Side::Top, &data), ((50.5, 60.5), (249.5, 60.5)));
546 assert_eq!(
547 axis_anchor(Side::Right, &data),
548 ((249.5, 60.5), (249.5, 159.5))
549 );
550
551 let mut axis = default_config().left_y;
552 axis.tick = TickVisibility::Both;
553 axis.major_tick_length = 5.0;
554 axis.line_width = 1.0;
555 axis.line_offset = 2.0;
556 assert_eq!(axis_visibility_extent(&axis), (5.0, 5.0));
557 assert_eq!(
558 axis_visibility_rect(Side::Left, &data, &axis),
559 RectF {
560 x: 47.0,
561 y: 60.0,
562 width: 10.0,
563 height: 100.0,
564 }
565 );
566 }
567
568 #[test]
569 fn label_origin_and_rect_cover_all_sides_and_offsets() {
570 let tick = (100.0, 200.0);
571 let offset = (2.0, 4.0);
572 let m = extents();
573 let expected = [
574 (Side::Top, (92.0, 192.0)),
575 (Side::Bottom, (92.0, 220.0)),
576 (Side::Left, (73.0, 206.0)),
577 (Side::Right, (111.0, 206.0)),
578 ];
579 for (side, origin) in expected {
580 let actual = label_origin(side, tick, 5.0, offset, m);
581 assert_eq!(actual, origin);
582 assert_eq!(label_rect(actual, m).height, 10.0);
583 }
584 }
585
586 #[test]
587 fn title_placement_keeps_rotation_signs_and_offset_frames() {
588 let (chart, data) = areas();
589 let m = extents();
590 let left = axis_title_placement(
591 Side::Left,
592 &chart,
593 &data,
594 band(40.0, 32.0, 0.0),
595 (2.0, 3.0),
596 m,
597 );
598 let right = axis_title_placement(
599 Side::Right,
600 &chart,
601 &data,
602 band(40.0, 32.0, 0.0),
603 (2.0, 3.0),
604 m,
605 );
606 assert_eq!(left.rotation_degrees, -90.0);
607 assert_eq!(right.rotation_degrees, 90.0);
608 assert_eq!(
609 left.rect(m),
610 RectF {
611 x: 28.0,
612 y: 98.0,
613 width: 10.0,
614 height: 20.0
615 }
616 );
617 assert_eq!(
618 right.rect(m),
619 RectF {
620 x: 282.0,
621 y: 102.0,
622 width: 10.0,
623 height: 20.0
624 }
625 );
626
627 for side in [Side::Top, Side::Bottom, Side::Left, Side::Right] {
628 let local = (7.0, -4.0);
629 assert_eq!(
630 screen_offset_to_axis_title(side.clone(), axis_title_offset_to_screen(side, local)),
631 local
632 );
633 }
634 }
635
636 #[test]
637 fn colorbar_title_tracks_the_actual_strip_on_every_side() {
638 let strip = RectF {
639 x: 70.0,
640 y: 80.0,
641 width: 40.0,
642 height: 100.0,
643 };
644 let moved = strip.translated(13.0, -9.0);
645 let axis = crate::default::default_axis_options_colorbar();
646 let m = extents();
647
648 for side in [Side::Top, Side::Bottom, Side::Left, Side::Right] {
649 let before =
650 colorbar_title_placement(side.clone(), &strip, &axis, (0.0, 0.0), m).rect(m);
651 let after = colorbar_title_placement(side, &moved, &axis, (0.0, 0.0), m).rect(m);
652 assert_eq!(after, before.translated(13.0, -9.0));
653 }
654
655 let right = colorbar_title_placement(Side::Right, &strip, &axis, (0.0, 0.0), m).rect(m);
656 assert_eq!(right.y + right.height * 0.5, strip.y + strip.height * 0.5);
657
658 let top = colorbar_title_placement(Side::Top, &strip, &axis, (0.0, 0.0), m).rect(m);
659 assert_eq!(top.x + top.width * 0.5, strip.x + strip.width * 0.5);
660 }
661
662 #[test]
663 fn rect_axis_visibility_uses_tick_direction_and_line_offset() {
664 let strip = RectF {
665 x: 10.0,
666 y: 20.0,
667 width: 40.0,
668 height: 100.0,
669 };
670 let mut axis = crate::default::default_axis_options_colorbar();
671 axis.line_visible = false;
672 axis.major_tick_length = 5.0;
673 axis.line_offset = 3.0;
674
675 axis.tick = TickVisibility::Outside;
676 assert_eq!(
677 rect_axis_visibility_rect(Side::Right, &strip, &axis),
678 RectF {
679 x: 53.0,
680 y: 20.0,
681 width: 5.0,
682 height: 100.0,
683 }
684 );
685
686 axis.tick = TickVisibility::Inside;
687 assert_eq!(
688 rect_axis_visibility_rect(Side::Right, &strip, &axis),
689 RectF {
690 x: 48.0,
691 y: 20.0,
692 width: 5.0,
693 height: 100.0,
694 }
695 );
696 }
697
698 #[test]
699 fn legend_rect_covers_all_corners_and_offsets() {
700 let (_, data) = areas();
701 let m = extents();
702 let expected = [
703 (LegendCorner::TopLeft, (57.0, 68.0)),
704 (LegendCorner::TopRight, (215.0, 68.0)),
705 (LegendCorner::BottomLeft, (57.0, 136.0)),
706 (LegendCorner::BottomRight, (215.0, 136.0)),
707 ];
708 for (corner, (x, y)) in expected {
709 let rect = legend_rect(&data, corner, 5.0, (1.0, 2.0), m);
710 assert_eq!(
711 rect,
712 RectF {
713 x,
714 y,
715 width: 30.0,
716 height: 20.0
717 }
718 );
719 }
720 }
721
722 fn colorbar_config(side: Side) -> (crate::config::Config, DataArea) {
727 let mut cfg = crate::default::default_config();
728 let mut bar = crate::default::default_colorbar_options();
729 bar.side = side;
730 bar.thickness_px = 20.0;
731 bar.gap_px = 10.0;
732 bar.length_frac = 0.5;
733 bar.align = BarAlign::Center;
734 cfg.colorbar = Some(bar);
735 let da = cfg.data_area().expect("data area");
736 (cfg, da)
737 }
738
739 fn rect_of(cfg: &crate::config::Config, da: &DataArea) -> RectF {
740 colorbar_rect(
741 &cfg.chart_area,
742 da,
743 cfg.chart_title.top_margin,
744 cfg.colorbar.as_ref().expect("colourbar"),
745 )
746 }
747
748 #[test]
753 fn the_colorbar_strip_sits_outside_the_axis_band_not_on_top_of_it() {
754 for side in [Side::Top, Side::Bottom, Side::Left, Side::Right] {
755 let (cfg, da) = colorbar_config(side.clone());
756 let bar = cfg.colorbar.as_ref().expect("colourbar");
757 let rect = rect_of(&cfg, &da);
758 let axis = crate::layout::axis_ref(&cfg, &side);
759 let axis_band = axis.out_margin + axis.major_tick_length;
760 let outer_band = bar.axis.out_margin + bar.axis.major_tick_length;
761 let ca = &cfg.chart_area;
762
763 let (from_data_area, to_chart_edge) = match side {
764 Side::Left => (da.x as f32 - (rect.x + rect.width), rect.x - ca.x as f32),
765 Side::Right => (
766 rect.x - (da.x + da.width) as f32,
767 (ca.x + ca.width) as f32 - (rect.x + rect.width),
768 ),
769 Side::Top => (
770 da.y as f32 - (rect.y + rect.height),
771 rect.y - (ca.y as f32 + cfg.chart_title.top_margin),
772 ),
773 Side::Bottom => (
774 rect.y - (da.y + da.height) as f32,
775 (ca.y + ca.height) as f32 - (rect.y + rect.height),
776 ),
777 };
778 assert!(
781 (from_data_area - (axis_band + bar.gap_px)).abs() <= 1.0,
782 "{side:?}: {from_data_area} px from the data area, expected axis band \
783 {axis_band} + gap {}",
784 bar.gap_px
785 );
786 assert!(
788 (to_chart_edge - outer_band).abs() <= 1.0,
789 "{side:?}: {to_chart_edge} px to the chart edge, expected {outer_band}"
790 );
791 }
792 }
793
794 #[test]
796 fn colorbar_orientation_and_length_follow_side_and_length_frac() {
797 let (cfg, da) = colorbar_config(Side::Right);
798 let vertical = rect_of(&cfg, &da);
799 assert_eq!(vertical.width, 20.0);
800 assert_eq!(vertical.height, da.height as f32 * 0.5);
801 assert_eq!(vertical.y, da.y as f32 + da.height as f32 * 0.25);
803
804 let (cfg, da) = colorbar_config(Side::Bottom);
805 let horizontal = rect_of(&cfg, &da);
806 assert_eq!(horizontal.height, 20.0);
807 assert_eq!(horizontal.width, da.width as f32 * 0.5);
808 assert_eq!(horizontal.x, da.x as f32 + da.width as f32 * 0.25);
809 }
810
811 #[test]
814 fn colorbar_align_places_the_strip_along_its_side() {
815 let (mut cfg, da) = colorbar_config(Side::Right);
816 let quarter = da.height as f32 * 0.5;
817
818 cfg.colorbar.as_mut().unwrap().align = BarAlign::Start;
819 assert_eq!(rect_of(&cfg, &da).y, da.y as f32);
820 cfg.colorbar.as_mut().unwrap().align = BarAlign::End;
821 assert_eq!(rect_of(&cfg, &da).y, (da.y + da.height) as f32 - quarter);
822
823 let (mut cfg, da) = colorbar_config(Side::Bottom);
824 let half = da.width as f32 * 0.5;
825 cfg.colorbar.as_mut().unwrap().align = BarAlign::Start;
826 assert_eq!(rect_of(&cfg, &da).x, da.x as f32);
827 cfg.colorbar.as_mut().unwrap().align = BarAlign::End;
828 assert_eq!(rect_of(&cfg, &da).x, (da.x + da.width) as f32 - half);
829 }
830
831 #[test]
833 fn a_full_length_colorbar_spans_its_side() {
834 for align in [BarAlign::Start, BarAlign::Center, BarAlign::End] {
835 let (mut cfg, da) = colorbar_config(Side::Right);
836 {
837 let bar = cfg.colorbar.as_mut().unwrap();
838 bar.length_frac = 1.0;
839 bar.align = align.clone();
840 }
841 let rect = rect_of(&cfg, &da);
842 assert_eq!(rect.y, da.y as f32, "{align:?}");
843 assert_eq!(rect.height, da.height as f32, "{align:?}");
844 }
845 }
846
847 #[test]
850 fn colorbar_rect_survives_values_validate_would_reject() {
851 let (mut cfg, da) = colorbar_config(Side::Right);
852 for frac in [0.0, -1.0, f32::NAN, 2.0] {
853 cfg.colorbar.as_mut().unwrap().length_frac = frac;
854 let rect = rect_of(&cfg, &da);
855 assert!(
856 rect.height >= 0.0,
857 "frac {frac} gave height {}",
858 rect.height
859 );
860 assert!(
861 rect.height <= da.height as f32,
862 "frac {frac} overran its side"
863 );
864 }
865 cfg.colorbar.as_mut().unwrap().length_frac = 0.5;
866 cfg.colorbar.as_mut().unwrap().thickness_px = -5.0;
867 let rect = rect_of(&cfg, &da);
868 assert_eq!(rect.width, 0.0);
869 }
870
871 #[test]
875 fn axis_title_placement_is_pushed_in_by_the_colorbar_band() {
876 let (cfg, da) = colorbar_config(Side::Right);
877 let colorbar_band = cfg.colorbar_band(&Side::Right);
878 assert!(colorbar_band > 0.0);
879
880 let placement = |inset: f32| {
881 axis_title_placement(
882 Side::Right,
883 &cfg.chart_area,
884 &da,
885 band(cfg.right_y.out_margin, cfg.chart_title.top_margin, inset),
886 (0.0, 0.0),
887 extents(),
888 )
889 };
890 let with_band = placement(colorbar_band);
891 let without = placement(0.0);
892 assert!(
893 (without.rotation_center.0 - with_band.rotation_center.0 - colorbar_band).abs() < 1e-3,
894 "the inset must move the title inward by exactly the band"
895 );
896
897 for side in [Side::Top, Side::Bottom, Side::Left] {
899 assert_eq!(cfg.colorbar_band(&side), 0.0);
900 }
901 }
902}