Skip to main content

ggplot_rs/guide/
polar.rs

1//! Guides for radar coordinates: rings at the y breaks, spokes at the x
2//! breaks, and labels around the outside.
3
4use crate::coord::Coord;
5use crate::render::backend::{DrawBackend, LineStyle, TextAnchor, TextStyle};
6use crate::render::{Rect, RenderError};
7use crate::scale::Scale;
8use crate::theme::Theme;
9
10/// Draw radar rings, spokes and labels (instead of cartesian gridlines/axes).
11pub fn draw_radar_guides(
12    x_scale: &dyn Scale,
13    y_scale: &dyn Scale,
14    coord: &dyn Coord,
15    theme: &Theme,
16    area: &Rect,
17    backend: &mut dyn DrawBackend,
18) -> Result<(), RenderError> {
19    let x_breaks = x_scale.breaks();
20    let y_breaks = y_scale.breaks();
21    // Spoke positions: the x breaks; a continuous x gets a smooth ring.
22    let ring_pos: Vec<f64> = if x_scale.is_discrete() && x_breaks.len() >= 3 {
23        x_breaks.iter().map(|(p, _)| *p).collect()
24    } else {
25        (0..72).map(|k| k as f64 / 72.0).collect()
26    };
27    let centre = coord.transform((0.0, f64::NEG_INFINITY), area);
28
29    let grid = theme.get_panel_grid_major_y();
30    if grid.visible {
31        let style = LineStyle {
32            color: grid.color,
33            width: grid.width,
34            alpha: 1.0,
35            linetype: grid.linetype.clone(),
36            ..Default::default()
37        };
38        for (ny, _) in &y_breaks {
39            if !(0.0..=1.0 + 1e-9).contains(ny) {
40                continue;
41            }
42            let mut ring: Vec<(f64, f64)> = ring_pos
43                .iter()
44                .map(|&p| coord.transform((p, *ny), area))
45                .collect();
46            if let Some(&first) = ring.first() {
47                ring.push(first);
48            }
49            backend.draw_line(&ring, &style)?;
50        }
51    }
52    let spoke = theme.get_panel_grid_major_x();
53    if spoke.visible {
54        let style = LineStyle {
55            color: spoke.color,
56            width: spoke.width,
57            alpha: 1.0,
58            linetype: spoke.linetype.clone(),
59            ..Default::default()
60        };
61        for (p, _) in &x_breaks {
62            backend.draw_line(&[centre, coord.transform((*p, 1.0), area)], &style)?;
63        }
64    }
65
66    let family = |f: &str| (!f.is_empty()).then(|| f.to_string());
67    if theme.axis_text_x.visible {
68        for (p, label) in &x_breaks {
69            let outer = coord.transform((*p, 1.0), area);
70            let (dx, dy) = (outer.0 - centre.0, outer.1 - centre.1);
71            let len = (dx * dx + dy * dy).sqrt().max(1e-9);
72            let pad = theme.axis_text_x.size * 0.9;
73            let pos = (outer.0 + dx / len * pad, outer.1 + dy / len * pad);
74            let anchor = if dx.abs() < len * 0.1 {
75                TextAnchor::Middle
76            } else if dx > 0.0 {
77                TextAnchor::Start
78            } else {
79                TextAnchor::End
80            };
81            backend.draw_text(
82                label,
83                pos,
84                &TextStyle {
85                    color: theme.axis_text_x.color,
86                    size: theme.axis_text_x.size,
87                    anchor,
88                    angle: 0.0,
89                    family: family(&theme.axis_text_x.family),
90                    face: theme.axis_text_x.face,
91                    bg_colour: None,
92                    bg_r: 0.0,
93                },
94            )?;
95        }
96    }
97    if theme.axis_text_y.visible {
98        // Ring values along the first spoke, nudged right of it.
99        let p0 = x_breaks.first().map(|(p, _)| *p).unwrap_or(0.0);
100        for (ny, label) in &y_breaks {
101            if !(0.0..=1.0 + 1e-9).contains(ny) {
102                continue;
103            }
104            let (x, y) = coord.transform((p0, *ny), area);
105            backend.draw_text(
106                label,
107                (x + 3.0, y),
108                &TextStyle {
109                    color: theme.axis_text_y.color,
110                    size: theme.axis_text_y.size * 0.85,
111                    anchor: TextAnchor::Start,
112                    angle: 0.0,
113                    family: family(&theme.axis_text_y.family),
114                    face: theme.axis_text_y.face,
115                    bg_colour: None,
116                    bg_r: 0.0,
117                },
118            )?;
119        }
120    }
121    Ok(())
122}