1use crate::coord::Coord;
2use crate::render::backend::{DrawBackend, LineStyle, TextAnchor, TextStyle};
3use crate::render::text::{axis_label_priority, label_bbox, text_width};
4use crate::render::{Rect, RenderError};
5use crate::scale::Scale;
6use crate::theme::{AxisLabelFit, Theme};
7
8fn along_x(coord: &dyn Coord, pos: f64, area: &Rect) -> f64 {
12 let p = if coord.is_flipped() {
13 (0.0, pos)
14 } else {
15 (pos, 0.0)
16 };
17 coord.transform(p, area).0
18}
19
20fn along_y(coord: &dyn Coord, pos: f64, area: &Rect) -> f64 {
22 let p = if coord.is_flipped() {
23 (pos, 0.0)
24 } else {
25 (0.0, pos)
26 };
27 coord.transform(p, area).1
28}
29
30pub fn draw_x_axis(
38 scale: &dyn Scale,
39 coord: &dyn Coord,
40 theme: &Theme,
41 plot_area: &Rect,
42 canvas: &Rect,
43 backend: &mut dyn DrawBackend,
44) -> Result<(), RenderError> {
45 draw_x_axis_with(scale, coord, theme, plot_area, canvas, true, backend)
46}
47
48pub(crate) fn draw_x_axis_with(
51 scale: &dyn Scale,
52 coord: &dyn Coord,
53 theme: &Theme,
54 plot_area: &Rect,
55 canvas: &Rect,
56 with_title: bool,
57 backend: &mut dyn DrawBackend,
58) -> Result<(), RenderError> {
59 let top = scale.axis_position_opposite();
61 let top_theme;
63 let theme = match (&theme.axis_text_x_top, top) {
64 (Some(el), true) => {
65 top_theme = Theme {
66 axis_text_x: el.clone(),
67 ..theme.clone()
68 };
69 &top_theme
70 }
71 _ => theme,
72 };
73 let breaks = scale.breaks();
74 let tick_len = theme.ticks_len_x();
75 let side = if top {
76 crate::theme::AxisSide::Top
77 } else {
78 crate::theme::AxisSide::Bottom
79 };
80 let axis_line = theme.axis_line_at(side);
81 let axis_ticks = theme.axis_ticks_at(side);
82
83 let edge_y = if top {
84 plot_area.y
85 } else {
86 plot_area.y + plot_area.height
87 };
88 let dir = if top { -1.0 } else { 1.0 };
89
90 if axis_line.visible {
92 backend.set_element_class(Some("gg-axis gg-axis-x gg-axis-line"));
93 backend.draw_line(
94 &[
95 (plot_area.x, edge_y),
96 (plot_area.x + plot_area.width, edge_y),
97 ],
98 &LineStyle {
99 color: axis_line.color,
100 width: axis_line.width,
101 alpha: 1.0,
102 linetype: axis_line.linetype.clone(),
103 ..Default::default()
104 },
105 )?;
106 }
107
108 let label_draw: Vec<Option<f64>> = if theme.axis_text_x.visible {
113 let angle = theme.axis_text_x.angle;
114 let rotated = angle.abs() > 10.0;
115 let anchor_frac = if rotated {
116 crate::render::text::rotated_anchor_frac(angle, dir)
117 } else {
118 0.5
119 };
120 let fit = theme.axis_text_x_fit;
121 let canvas_lo = canvas.x;
122 let canvas_hi = canvas.x + canvas.width;
123 const EDGE: f64 = 1.0;
124 let size = theme.axis_text_x.size;
125
126 let onscreen: Vec<(usize, f64, &str)> = breaks
130 .iter()
131 .enumerate()
132 .map(|(i, (pos, label))| (i, along_x(coord, *pos, plot_area), label.as_str()))
133 .filter(|(_, px, _)| (canvas_lo..=canvas_hi).contains(px))
134 .collect();
135
136 let mut keep = vec![true; onscreen.len()];
140 if theme.axis_text_x_check_overlap && onscreen.len() > 1 {
141 let n = onscreen.len();
142 let mut kept: Vec<(f64, f64, f64)> = Vec::with_capacity(n); keep = vec![false; n];
144 let min_gap = text_width(" ", size);
158 for p in axis_label_priority(n) {
159 let idx = p - 1; let (_, px, label) = onscreen[idx];
161 let bbox = label_bbox(label, size, px, 0.0, angle, anchor_frac);
162 let overlaps = kept.iter().any(|&(kx0, kx2, kpx)| {
163 if !rotated {
164 bbox[0] < kx2 + min_gap && bbox[2] > kx0 - min_gap
165 } else {
166 (px - kpx).abs() * angle.to_radians().sin().abs() < size
170 }
171 });
172 if !overlaps {
173 keep[idx] = true;
174 kept.push((bbox[0], bbox[2], px));
175 }
176 }
177 }
178
179 let mut draw = vec![None; breaks.len()];
180 for (k, &(i, px, label)) in onscreen.iter().enumerate() {
181 if !keep[k] {
182 continue;
183 }
184 let lx = if fit == AxisLabelFit::Clip {
187 px
188 } else {
189 let bbox = label_bbox(label, size, px, 0.0, angle, anchor_frac);
191 let room_hi = canvas_hi - EDGE;
192 let room_lo = canvas_lo + EDGE;
193 if bbox[0] < room_lo {
194 px + (room_lo - bbox[0])
195 } else if bbox[2] > room_hi {
196 px - (bbox[2] - room_hi).min(bbox[0] - room_lo)
197 } else {
198 px
199 }
200 };
201 draw[i] = Some(lx);
202 }
203 draw
204 } else {
205 Vec::new()
206 };
207
208 let dodge = theme.axis_text_x_dodge.max(1);
210 let family = if theme.axis_text_x.family.is_empty() {
211 None
212 } else {
213 Some(theme.axis_text_x.family.clone())
214 };
215 let rotated = theme.axis_text_x.angle.abs() > 10.0;
216 let draw_anchor_frac = if rotated {
221 crate::render::text::rotated_anchor_frac(theme.axis_text_x.angle, dir)
222 } else {
223 0.5
224 };
225 let anchor = if !rotated {
226 TextAnchor::Middle
227 } else if draw_anchor_frac >= 1.0 {
228 TextAnchor::End
229 } else {
230 TextAnchor::Start
231 };
232 let rotation_margin = if rotated {
242 let (rise, fall) = crate::render::text::rotated_label_rise_fall(
243 breaks
244 .iter()
245 .filter(|(_, l)| !l.is_empty())
246 .map(|(_, l)| l.as_str()),
247 theme.axis_text_x.size,
248 theme.axis_text_x.angle,
249 draw_anchor_frac,
250 );
251 if dir > 0.0 {
252 rise
253 } else {
254 fall
255 }
256 } else {
257 0.0
258 };
259 for (i, (pos, label)) in breaks.iter().enumerate() {
260 let px = along_x(coord, *pos, plot_area);
261
262 if axis_ticks.visible {
263 backend.set_element_class(Some("gg-axis gg-axis-x gg-axis-tick"));
264 backend.draw_line(
265 &[(px, edge_y), (px, edge_y + dir * tick_len)],
266 &LineStyle {
267 color: axis_ticks.color,
268 width: axis_ticks.width,
269 alpha: 1.0,
270 linetype: axis_ticks.linetype.clone(),
271 ..Default::default()
272 },
273 )?;
274 }
275
276 if let Some(Some(lx)) = label_draw.get(i) {
277 let row_offset = (i % dodge) as f64 * (theme.axis_text_x.size + 2.0);
279 let ly = edge_y
280 + dir
281 * (tick_len
282 + theme.legend_spacing / 2.0
283 + theme.axis_text_x.size / 2.0
284 + row_offset
285 + rotation_margin);
286 backend.set_element_class(Some("gg-axis gg-axis-x gg-axis-text"));
287 backend.draw_text(
288 label,
289 (*lx, ly),
290 &TextStyle {
291 color: theme.axis_text_x.color,
292 size: theme.axis_text_x.size,
293 anchor: anchor.clone(),
294 angle: theme.axis_text_x.angle,
295 family: family.clone(),
296 face: theme.axis_text_x.face,
297 bg_colour: None,
298 bg_r: 0.0,
299 },
300 )?;
301 }
302 }
303
304 if theme.axis_minor_ticks && axis_ticks.visible {
306 for pos in minor_breaks(&breaks) {
307 let px = along_x(coord, pos, plot_area);
308 backend.set_element_class(Some("gg-axis gg-axis-x gg-axis-tick"));
309 backend.draw_line(
310 &[(px, edge_y), (px, edge_y + dir * tick_len * 0.5)],
311 &LineStyle {
312 color: axis_ticks.color,
313 width: axis_ticks.width,
314 alpha: 1.0,
315 linetype: axis_ticks.linetype.clone(),
316 ..Default::default()
317 },
318 )?;
319 }
320 }
321
322 if axis_ticks.visible {
324 for (pos, kind) in scale.log_ticks() {
325 let px = along_x(coord, pos, plot_area);
326 let len = tick_len * [2.25, 1.5, 0.75][kind as usize];
327 backend.set_element_class(Some("gg-axis gg-axis-x gg-axis-tick"));
328 backend.draw_line(
329 &[(px, edge_y), (px, edge_y + dir * len)],
330 &LineStyle {
331 color: axis_ticks.color,
332 width: axis_ticks.width,
333 alpha: 1.0,
334 linetype: axis_ticks.linetype.clone(),
335 ..Default::default()
336 },
337 )?;
338 }
339 }
340
341 if with_title {
342 draw_x_axis_title(scale, theme, plot_area, backend)?;
343 }
344
345 Ok(())
346}
347
348pub(crate) fn draw_x_axis_title(
351 scale: &dyn Scale,
352 theme: &Theme,
353 area: &Rect,
354 backend: &mut dyn DrawBackend,
355) -> Result<(), RenderError> {
356 let plot_area = area;
357 let tick_len = theme.ticks_len_x();
358 let top = scale.axis_position_opposite();
359 let edge_y = if top {
360 plot_area.y
361 } else {
362 plot_area.y + plot_area.height
363 };
364 let dir = if top { -1.0 } else { 1.0 };
365 let title = scale.name();
366 if !title.is_empty() && theme.axis_title_x.visible {
367 let hjust = theme.axis_title_x.hjust.clamp(0.0, 1.0);
368 let center_x = plot_area.x + hjust * plot_area.width;
369 let anchor = if hjust <= 0.02 {
370 TextAnchor::Start
371 } else if hjust >= 0.98 {
372 TextAnchor::End
373 } else {
374 TextAnchor::Middle
375 };
376 let angle = theme.axis_text_x.angle;
380 let labels_h = if theme.axis_text_x.visible && angle.abs() > 10.0 {
381 let breaks = scale.breaks();
382 let (rise, fall) = crate::render::text::rotated_label_rise_fall(
383 breaks.iter().map(|(_, l)| l.as_str()),
384 theme.axis_text_x.size,
385 angle,
386 crate::render::text::rotated_anchor_frac(angle, dir),
387 );
388 (rise + fall).max(theme.axis_text_x.size)
389 } else {
390 theme.axis_text_x.size
391 };
392 let title_y = edge_y + dir * (tick_len + labels_h + 8.0 + theme.axis_title_x.size / 2.0);
393 let family = if theme.axis_title_x.family.is_empty() {
394 None
395 } else {
396 Some(theme.axis_title_x.family.clone())
397 };
398 backend.set_element_class(Some("gg-axis-title gg-axis-title-x"));
399 crate::render::rich::draw(
400 backend,
401 theme.rich_text,
402 title,
403 (center_x, title_y),
404 &TextStyle {
405 color: theme.axis_title_x.color,
406 size: theme.axis_title_x.size,
407 anchor,
408 angle: 0.0,
409 family,
410 face: theme.axis_title_x.face,
411 bg_colour: None,
412 bg_r: 0.0,
413 },
414 )?;
415 }
416
417 Ok(())
418}
419
420pub fn draw_y_axis(
422 scale: &dyn Scale,
423 coord: &dyn Coord,
424 theme: &Theme,
425 plot_area: &Rect,
426 backend: &mut dyn DrawBackend,
427) -> Result<(), RenderError> {
428 draw_y_axis_with(scale, coord, theme, plot_area, true, backend)
429}
430
431pub(crate) fn draw_y_axis_with(
434 scale: &dyn Scale,
435 coord: &dyn Coord,
436 theme: &Theme,
437 plot_area: &Rect,
438 with_title: bool,
439 backend: &mut dyn DrawBackend,
440) -> Result<(), RenderError> {
441 let breaks = scale.breaks();
442 let tick_len = theme.ticks_len_y();
443 let axis_line = theme.axis_line_at(crate::theme::AxisSide::Left);
444 let axis_ticks = theme.axis_ticks_at(crate::theme::AxisSide::Left);
445
446 if axis_line.visible {
448 let top = (plot_area.x, plot_area.y);
449 let bottom = (plot_area.x, plot_area.y + plot_area.height);
450 backend.set_element_class(Some("gg-axis gg-axis-y gg-axis-line"));
451 backend.draw_line(
452 &[top, bottom],
453 &LineStyle {
454 color: axis_line.color,
455 width: axis_line.width,
456 alpha: 1.0,
457 linetype: axis_line.linetype.clone(),
458 ..Default::default()
459 },
460 )?;
461 }
462
463 for (pos, label) in &breaks {
465 let py = along_y(coord, *pos, plot_area);
466
467 if axis_ticks.visible {
468 backend.set_element_class(Some("gg-axis gg-axis-y gg-axis-tick"));
469 backend.draw_line(
470 &[(plot_area.x - tick_len, py), (plot_area.x, py)],
471 &LineStyle {
472 color: axis_ticks.color,
473 width: axis_ticks.width,
474 alpha: 1.0,
475 linetype: axis_ticks.linetype.clone(),
476 ..Default::default()
477 },
478 )?;
479 }
480
481 if theme.axis_text_y.visible {
482 let family = if theme.axis_text_y.family.is_empty() {
483 None
484 } else {
485 Some(theme.axis_text_y.family.clone())
486 };
487 backend.set_element_class(Some("gg-axis gg-axis-y gg-axis-text"));
488 backend.draw_text(
489 label,
490 (plot_area.x - tick_len - theme.legend_spacing, py),
491 &TextStyle {
492 color: theme.axis_text_y.color,
493 size: theme.axis_text_y.size,
494 anchor: TextAnchor::End,
495 angle: theme.axis_text_y.angle,
496 family,
497 face: theme.axis_text_y.face,
498 bg_colour: None,
499 bg_r: 0.0,
500 },
501 )?;
502 }
503 }
504
505 if theme.axis_minor_ticks && axis_ticks.visible {
507 for pos in minor_breaks(&breaks) {
508 let py = along_y(coord, pos, plot_area);
509 backend.set_element_class(Some("gg-axis gg-axis-y gg-axis-tick"));
510 backend.draw_line(
511 &[(plot_area.x - tick_len * 0.5, py), (plot_area.x, py)],
512 &LineStyle {
513 color: axis_ticks.color,
514 width: axis_ticks.width,
515 alpha: 1.0,
516 linetype: axis_ticks.linetype.clone(),
517 ..Default::default()
518 },
519 )?;
520 }
521 }
522
523 if axis_ticks.visible {
525 for (pos, kind) in scale.log_ticks() {
526 let py = along_y(coord, pos, plot_area);
527 let len = tick_len * [2.25, 1.5, 0.75][kind as usize];
528 backend.set_element_class(Some("gg-axis gg-axis-y gg-axis-tick"));
529 backend.draw_line(
530 &[(plot_area.x - len, py), (plot_area.x, py)],
531 &LineStyle {
532 color: axis_ticks.color,
533 width: axis_ticks.width,
534 alpha: 1.0,
535 linetype: axis_ticks.linetype.clone(),
536 ..Default::default()
537 },
538 )?;
539 }
540 }
541
542 if with_title {
543 let reserve = y_label_reserve(theme, &breaks);
544 draw_y_axis_title(scale, theme, plot_area, reserve, backend)?;
545 }
546
547 Ok(())
548}
549
550const Y_TITLE_GAP: f64 = 4.0;
552
553pub(crate) fn y_label_reserve(theme: &Theme, breaks: &[(f64, String)]) -> f64 {
556 let max_ch = breaks
557 .iter()
558 .map(|(_, l)| l.chars().count())
559 .max()
560 .unwrap_or(0);
561 let measured = max_ch as f64 * theme.axis_text_y.size * 0.6 + 4.0;
562 let default = theme.axis_text_y.size * 3.5 + 4.0;
563 measured.max(default)
564}
565
566pub(crate) fn draw_y_axis_title(
569 scale: &dyn Scale,
570 theme: &Theme,
571 area: &Rect,
572 label_reserve: f64,
573 backend: &mut dyn DrawBackend,
574) -> Result<(), RenderError> {
575 let title = scale.name();
576 if !title.is_empty() && theme.axis_title_y.visible {
577 let size = theme.axis_title_y.size;
582 let right = area.x - theme.ticks_len_y() - label_reserve - Y_TITLE_GAP;
583 let min_x = backend.total_area().x + 0.75 * size;
584 let title_x = (right - 0.25 * size).max(min_x);
585 let center_y = area.y + area.height / 2.0;
586 let family = if theme.axis_title_y.family.is_empty() {
587 None
588 } else {
589 Some(theme.axis_title_y.family.clone())
590 };
591 backend.set_element_class(Some("gg-axis-title gg-axis-title-y"));
592 crate::render::rich::draw(
593 backend,
594 theme.rich_text,
595 title,
596 (title_x, center_y),
597 &TextStyle {
598 color: theme.axis_title_y.color,
599 size: theme.axis_title_y.size,
600 anchor: TextAnchor::Middle,
601 angle: 270.0,
602 family,
603 face: theme.axis_title_y.face,
604 bg_colour: None,
605 bg_r: 0.0,
606 },
607 )?;
608 }
609
610 Ok(())
611}
612
613pub fn draw_sec_y_axis(
615 primary_scale: &dyn Scale,
616 sec_axis: &crate::scale::sec_axis::SecAxis,
617 coord: &dyn Coord,
618 theme: &Theme,
619 plot_area: &Rect,
620 backend: &mut dyn DrawBackend,
621) -> Result<(), RenderError> {
622 let right_theme;
624 let theme = match &theme.axis_text_y_right {
625 Some(el) => {
626 right_theme = Theme {
627 axis_text_y: el.clone(),
628 ..theme.clone()
629 };
630 &right_theme
631 }
632 None => theme,
633 };
634 let own_ticks = sec_axis.own_ticks(primary_scale);
635 let breaks = own_ticks.clone().unwrap_or_else(|| primary_scale.breaks());
636 let tick_len = theme.ticks_len_y();
637 let axis_line = theme.axis_line_at(crate::theme::AxisSide::Right);
638 let axis_ticks = theme.axis_ticks_at(crate::theme::AxisSide::Right);
639
640 let right_x = plot_area.x + plot_area.width;
641
642 if axis_line.visible {
644 backend.set_element_class(Some("gg-axis gg-axis-y gg-axis-line"));
645 backend.draw_line(
646 &[
647 (right_x, plot_area.y),
648 (right_x, plot_area.y + plot_area.height),
649 ],
650 &LineStyle {
651 color: axis_line.color,
652 width: axis_line.width,
653 alpha: 1.0,
654 linetype: axis_line.linetype.clone(),
655 ..Default::default()
656 },
657 )?;
658 }
659
660 for (pos, label) in &breaks {
662 let py = along_y(coord, *pos, plot_area);
663
664 if axis_ticks.visible {
665 backend.set_element_class(Some("gg-axis gg-axis-y gg-axis-tick"));
666 backend.draw_line(
667 &[(right_x, py), (right_x + tick_len, py)],
668 &LineStyle {
669 color: axis_ticks.color,
670 width: axis_ticks.width,
671 alpha: 1.0,
672 linetype: axis_ticks.linetype.clone(),
673 ..Default::default()
674 },
675 )?;
676 }
677
678 if theme.axis_text_y.visible {
679 let locale = primary_scale.locale();
681 let parsed = match locale {
682 Some(l) => l.parse_number(label),
683 None => label.parse::<f64>().ok(),
684 };
685 let sec_label = if own_ticks.is_some() {
686 label.clone()
687 } else if let Some(v) = parsed {
688 sec_axis.label(sec_axis.transform_value(v), locale)
689 } else {
690 label.clone()
691 };
692
693 let family = if theme.axis_text_y.family.is_empty() {
694 None
695 } else {
696 Some(theme.axis_text_y.family.clone())
697 };
698 backend.set_element_class(Some("gg-axis gg-axis-y gg-axis-text"));
699 backend.draw_text(
700 &sec_label,
701 (right_x + tick_len + theme.legend_spacing, py),
702 &TextStyle {
703 color: theme.axis_text_y.color,
704 size: theme.axis_text_y.size,
705 anchor: TextAnchor::Start,
706 angle: theme.axis_text_y.angle,
707 family,
708 face: theme.axis_text_y.face,
709 bg_colour: None,
710 bg_r: 0.0,
711 },
712 )?;
713 }
714 }
715
716 if !sec_axis.name.is_empty() && theme.axis_title_y.visible {
718 let max_ch = own_ticks
721 .as_ref()
722 .and_then(|t| t.iter().map(|(_, l)| l.chars().count()).max())
723 .unwrap_or(0);
724 let text_w =
725 (max_ch as f64 * theme.axis_text_y.size * 0.6).max(theme.axis_text_y.size * 3.5);
726 let title_x = right_x + tick_len + text_w + theme.legend_spacing;
727 let center_y = plot_area.y + plot_area.height / 2.0;
728 let family = if theme.axis_title_y.family.is_empty() {
729 None
730 } else {
731 Some(theme.axis_title_y.family.clone())
732 };
733 backend.set_element_class(Some("gg-axis-title gg-axis-title-y"));
734 crate::render::rich::draw(
735 backend,
736 theme.rich_text,
737 &sec_axis.name,
738 (title_x, center_y),
739 &TextStyle {
740 color: theme.axis_title_y.color,
741 size: theme.axis_title_y.size,
742 anchor: TextAnchor::Middle,
743 angle: 90.0,
744 family,
745 face: theme.axis_title_y.face,
746 bg_colour: None,
747 bg_r: 0.0,
748 },
749 )?;
750 }
751
752 Ok(())
753}
754
755fn sec_labels(
758 primary_scale: &dyn Scale,
759 sec_axis: &crate::scale::sec_axis::SecAxis,
760 breaks: &[(f64, String)],
761 own: bool,
762) -> Vec<String> {
763 let locale = primary_scale.locale();
764 breaks
765 .iter()
766 .map(|(_, label)| {
767 if own {
768 return label.clone();
769 }
770 let parsed = match locale {
771 Some(l) => l.parse_number(label),
772 None => label.parse::<f64>().ok(),
773 };
774 match parsed {
775 Some(v) => sec_axis.label(sec_axis.transform_value(v), locale),
776 None => label.clone(),
777 }
778 })
779 .collect()
780}
781
782pub(crate) fn sec_x_axis_height(theme: &Theme, sec_axis: &crate::scale::sec_axis::SecAxis) -> f64 {
786 theme.ticks_len_x()
787 + if theme.axis_text_x.visible {
788 theme.axis_text_x.size + 4.0
789 } else {
790 0.0
791 }
792 + if !sec_axis.name.is_empty() && theme.axis_title_x.visible {
793 theme.axis_title_x.size + 6.0
794 } else {
795 0.0
796 }
797}
798
799pub fn draw_sec_x_axis(
802 primary_scale: &dyn Scale,
803 sec_axis: &crate::scale::sec_axis::SecAxis,
804 coord: &dyn Coord,
805 theme: &Theme,
806 plot_area: &Rect,
807 backend: &mut dyn DrawBackend,
808) -> Result<(), RenderError> {
809 let own_ticks = sec_axis.own_ticks(primary_scale);
810 let breaks = own_ticks.clone().unwrap_or_else(|| primary_scale.breaks());
811 let labels = sec_labels(primary_scale, sec_axis, &breaks, own_ticks.is_some());
812 let tick_len = theme.ticks_len_x();
813 let axis_line = theme.axis_line_at(crate::theme::AxisSide::Top);
814 let axis_ticks = theme.axis_ticks_at(crate::theme::AxisSide::Top);
815 let top_y = plot_area.y;
816 let line = |el: &crate::theme::elements::ElementLine| LineStyle {
817 color: el.color,
818 width: el.width,
819 alpha: 1.0,
820 linetype: el.linetype.clone(),
821 ..Default::default()
822 };
823 if axis_line.visible {
824 backend.set_element_class(Some("gg-axis gg-axis-x gg-axis-line"));
825 backend.draw_line(
826 &[(plot_area.x, top_y), (plot_area.x + plot_area.width, top_y)],
827 &line(axis_line),
828 )?;
829 }
830 let family = |f: &str| (!f.is_empty()).then(|| f.to_string());
831 for ((pos, _), label) in breaks.iter().zip(&labels) {
832 let px = along_x(coord, *pos, plot_area);
833 if axis_ticks.visible {
834 backend.set_element_class(Some("gg-axis gg-axis-x gg-axis-tick"));
835 backend.draw_line(&[(px, top_y), (px, top_y - tick_len)], &line(axis_ticks))?;
836 }
837 if theme.axis_text_x.visible {
838 backend.set_element_class(Some("gg-axis gg-axis-x gg-axis-text"));
839 backend.draw_text(
840 label,
841 (px, top_y - tick_len - 2.0 - theme.axis_text_x.size / 2.0),
842 &TextStyle {
843 color: theme.axis_text_x.color,
844 size: theme.axis_text_x.size,
845 anchor: TextAnchor::Middle,
846 angle: 0.0,
847 family: family(&theme.axis_text_x.family),
848 face: theme.axis_text_x.face,
849 bg_colour: None,
850 bg_r: 0.0,
851 },
852 )?;
853 }
854 }
855 if !sec_axis.name.is_empty() && theme.axis_title_x.visible {
856 let y = top_y - sec_x_axis_height(theme, sec_axis) + theme.axis_title_x.size / 2.0;
857 backend.set_element_class(Some("gg-axis-title gg-axis-title-x"));
858 crate::render::rich::draw(
859 backend,
860 theme.rich_text,
861 &sec_axis.name,
862 (plot_area.x + plot_area.width / 2.0, y),
863 &TextStyle {
864 color: theme.axis_title_x.color,
865 size: theme.axis_title_x.size,
866 anchor: TextAnchor::Middle,
867 angle: 0.0,
868 family: family(&theme.axis_title_x.family),
869 face: theme.axis_title_x.face,
870 bg_colour: None,
871 bg_r: 0.0,
872 },
873 )?;
874 }
875 Ok(())
876}
877
878fn minor_breaks(major: &[(f64, String)]) -> Vec<f64> {
880 if major.len() < 2 {
881 return vec![];
882 }
883 let mut minors = Vec::with_capacity(major.len() - 1);
884 for pair in major.windows(2) {
885 minors.push((pair[0].0 + pair[1].0) / 2.0);
886 }
887 minors
888}
889
890pub fn draw_gridlines(
892 x_scale: &dyn Scale,
893 y_scale: &dyn Scale,
894 coord: &dyn Coord,
895 theme: &Theme,
896 plot_area: &Rect,
897 backend: &mut dyn DrawBackend,
898) -> Result<(), RenderError> {
899 let major_x = theme.get_panel_grid_major_x();
900 let major_y = theme.get_panel_grid_major_y();
901 let minor_x = theme.get_panel_grid_minor_x();
902 let minor_y = theme.get_panel_grid_minor_y();
903
904 let x_breaks = x_scale.breaks();
905 let y_breaks = y_scale.breaks();
906
907 if minor_x.visible {
909 for pos in minor_breaks(&x_breaks) {
910 let px = along_x(coord, pos, plot_area);
911 backend.set_element_class(Some("gg-grid gg-grid-minor gg-grid-x"));
912 backend.draw_line(
913 &[(px, plot_area.y), (px, plot_area.y + plot_area.height)],
914 &LineStyle {
915 color: minor_x.color,
916 width: minor_x.width,
917 alpha: 1.0,
918 linetype: minor_x.linetype.clone(),
919 ..Default::default()
920 },
921 )?;
922 }
923 }
924
925 if minor_y.visible {
927 for pos in minor_breaks(&y_breaks) {
928 let py = along_y(coord, pos, plot_area);
929 backend.set_element_class(Some("gg-grid gg-grid-minor gg-grid-y"));
930 backend.draw_line(
931 &[(plot_area.x, py), (plot_area.x + plot_area.width, py)],
932 &LineStyle {
933 color: minor_y.color,
934 width: minor_y.width,
935 alpha: 1.0,
936 linetype: minor_y.linetype.clone(),
937 ..Default::default()
938 },
939 )?;
940 }
941 }
942
943 if major_x.visible {
945 for (pos, _) in &x_breaks {
946 let px = along_x(coord, *pos, plot_area);
947 backend.set_element_class(Some("gg-grid gg-grid-major gg-grid-x"));
948 backend.draw_line(
949 &[(px, plot_area.y), (px, plot_area.y + plot_area.height)],
950 &LineStyle {
951 color: major_x.color,
952 width: major_x.width,
953 alpha: 1.0,
954 linetype: major_x.linetype.clone(),
955 ..Default::default()
956 },
957 )?;
958 }
959 }
960
961 if major_y.visible {
963 for (pos, _) in &y_breaks {
964 let py = along_y(coord, *pos, plot_area);
965 backend.set_element_class(Some("gg-grid gg-grid-major gg-grid-y"));
966 backend.draw_line(
967 &[(plot_area.x, py), (plot_area.x + plot_area.width, py)],
968 &LineStyle {
969 color: major_y.color,
970 width: major_y.width,
971 alpha: 1.0,
972 linetype: major_y.linetype.clone(),
973 ..Default::default()
974 },
975 )?;
976 }
977 }
978
979 Ok(())
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985 use crate::aes::Aesthetic;
986 use crate::coord::cartesian::CoordCartesian;
987 use crate::data::Value;
988 use crate::render::svg_backend::SvgBackend;
989 use crate::theme::presets::theme_gray;
990
991 struct FixedBreaksScale(Vec<(f64, String)>);
994
995 impl Scale for FixedBreaksScale {
996 fn aesthetic(&self) -> Aesthetic {
997 Aesthetic::X
998 }
999 fn train(&mut self, _values: &[Value]) {}
1000 fn map(&self, _value: &Value) -> f64 {
1001 0.0
1002 }
1003 fn breaks(&self) -> Vec<(f64, String)> {
1004 self.0.clone()
1005 }
1006 fn name(&self) -> &str {
1007 ""
1008 }
1009 fn set_name(&mut self, _name: &str) {}
1010 fn clone_box(&self) -> Box<dyn Scale> {
1011 Box::new(FixedBreaksScale(self.0.clone()))
1012 }
1013 }
1014
1015 fn backend(w: u32, h: u32, plot_area: Rect) -> SvgBackend {
1016 SvgBackend::new(w, h, plot_area)
1017 }
1018
1019 #[test]
1020 fn a_tick_outside_the_svg_is_dropped() {
1021 let scale = FixedBreaksScale(vec![
1024 (0.2, "kept".to_string()),
1025 (5.0, "off_canvas".to_string()),
1026 ]);
1027 let coord = CoordCartesian::new();
1028 let theme = theme_gray();
1029 let plot_area = Rect {
1030 x: 10.0,
1031 y: 10.0,
1032 width: 180.0,
1033 height: 70.0,
1034 };
1035 let canvas = Rect {
1036 x: 0.0,
1037 y: 0.0,
1038 width: 200.0,
1039 height: 100.0,
1040 };
1041 let mut backend = backend(200, 100, plot_area.clone());
1042 draw_x_axis(&scale, &coord, &theme, &plot_area, &canvas, &mut backend).unwrap();
1043 let svg = backend.finish();
1044 assert!(svg.contains(">kept<"), "on-canvas label dropped: {svg}");
1045 assert!(
1046 !svg.contains(">off_canvas<"),
1047 "off-canvas label was drawn: {svg}"
1048 );
1049 }
1050
1051 #[test]
1052 fn clip_draws_the_label_exactly_at_the_tick_even_past_the_edge() {
1053 let scale = FixedBreaksScale(vec![
1055 (0.0, "a".to_string()),
1056 (1.0, "2021-01-01".to_string()),
1057 ]);
1058 let coord = CoordCartesian::new();
1059 let mut theme = theme_gray();
1060 theme.axis_text_x_fit = crate::theme::AxisLabelFit::Clip;
1061 let plot_area = Rect {
1062 x: 0.0,
1063 y: 0.0,
1064 width: 60.0,
1065 height: 50.0,
1066 };
1067 let canvas = plot_area.clone();
1068 let mut backend = backend(60, 50, plot_area.clone());
1069 draw_x_axis(&scale, &coord, &theme, &plot_area, &canvas, &mut backend).unwrap();
1070 let svg = backend.finish();
1071 assert!(
1074 svg.contains("x=\"60.00\" y=\"") && svg.contains(">2021-01-01<"),
1075 "clip mode should not shift the label: {svg}"
1076 );
1077 }
1078
1079 #[test]
1080 fn shift_moves_the_same_label_inside_the_canvas() {
1081 let scale = FixedBreaksScale(vec![
1082 (0.0, "a".to_string()),
1083 (1.0, "2021-01-01".to_string()),
1084 ]);
1085 let coord = CoordCartesian::new();
1086 let mut theme = theme_gray();
1087 theme.axis_text_x_fit = crate::theme::AxisLabelFit::Shift;
1088 let plot_area = Rect {
1089 x: 0.0,
1090 y: 0.0,
1091 width: 60.0,
1092 height: 50.0,
1093 };
1094 let canvas = plot_area.clone();
1095 let mut backend = backend(60, 50, plot_area.clone());
1096 draw_x_axis(&scale, &coord, &theme, &plot_area, &canvas, &mut backend).unwrap();
1097 let svg = backend.finish();
1098 assert!(
1099 !svg.contains("x=\"60.00\" y=\""),
1100 "shift mode should move the label off the bare tick: {svg}"
1101 );
1102 let w = crate::render::text::text_width("2021-01-01", theme.axis_text_x.size);
1103 let needle = ">2021-01-01<";
1105 let tag_end = svg.find(needle).unwrap();
1106 let tag_start = svg[..tag_end].rfind("<text ").unwrap();
1107 let tag = &svg[tag_start..tag_end];
1108 let x_attr = tag.rsplit("x=\"").next().unwrap();
1109 let x: f64 = x_attr.split('"').next().unwrap().parse().unwrap();
1110 assert!(
1113 x + w / 2.0 <= 60.0 - 1.0 + 0.01,
1114 "right edge still overflows: x={x} w={w}"
1115 );
1116 }
1117}