Skip to main content

ggplot_rs/scale/
sec_axis.rs

1/// A secondary axis specification — defines a transformation from the primary axis domain.
2///
3/// # Example
4/// ```
5/// use ggplot_rs::prelude::*;
6///
7/// // Temperature in Celsius on primary, Fahrenheit on secondary
8/// let sec = SecAxis::new(|c| c * 9.0 / 5.0 + 32.0).with_name("Fahrenheit");
9/// ```
10#[derive(Clone)]
11pub struct SecAxis {
12    /// Transformation from primary axis values to secondary axis values.
13    pub transform: fn(f64) -> f64,
14    /// Name / title for the secondary axis.
15    pub name: String,
16    /// Custom break positions in primary axis coordinates.
17    pub breaks: Option<Vec<f64>>,
18    /// Label the axis as a percentage of the primary scale's trained data
19    /// maximum ([`SecAxis::percent_of_max`]) instead of through `transform`.
20    pub percent_of_max: bool,
21}
22
23impl SecAxis {
24    /// Create a secondary axis with the given transformation function.
25    pub fn new(transform: fn(f64) -> f64) -> Self {
26        SecAxis {
27            transform,
28            name: String::new(),
29            breaks: None,
30            percent_of_max: false,
31        }
32    }
33
34    /// A 0–100 % axis relative to the primary scale's trained data maximum
35    /// (the value drawn at the right axis' 100 % tick) — e.g. a Pareto
36    /// chart's cumulative share, where the maximum is the grand total. The
37    /// ticks sit at their own nice percentages (0, 25, …, 100 %) rather than
38    /// at the primary breaks.
39    pub fn percent_of_max() -> Self {
40        SecAxis {
41            percent_of_max: true,
42            ..SecAxis::new(|v| v)
43        }
44    }
45
46    pub fn with_name(mut self, name: &str) -> Self {
47        self.name = name.to_string();
48        self
49    }
50
51    pub fn with_breaks(mut self, breaks: Vec<f64>) -> Self {
52        self.breaks = Some(breaks);
53        self
54    }
55
56    /// Transform a primary axis value to the secondary axis value.
57    pub fn transform_value(&self, v: f64) -> f64 {
58        (self.transform)(v)
59    }
60
61    /// The axis' own ticks as `(normalized position, label)` when it does not
62    /// simply relabel the primary breaks (the [`percent_of_max`](Self::percent_of_max)
63    /// mode); `None` otherwise.
64    pub(crate) fn own_ticks(&self, primary: &dyn super::Scale) -> Option<Vec<(f64, String)>> {
65        if !self.percent_of_max {
66            return None;
67        }
68        let (_, max) = primary.domain()?;
69        let (lo, hi) = primary.expanded_domain()?;
70        if !(max > 0.0 && max.is_finite()) {
71            return Some(vec![]);
72        }
73        let (lo_pct, hi_pct) = (lo / max * 100.0, hi / max * 100.0);
74        let eps = 1e-9 * (hi_pct - lo_pct).abs().max(1.0);
75        Some(
76            super::util::extended_breaks(0.0, 100.0, 5)
77                .into_iter()
78                .filter(|b| *b >= lo_pct - eps && *b <= hi_pct + eps)
79                .map(|b| {
80                    let pos = primary.map(&crate::data::Value::Float(b / 100.0 * max));
81                    let label = crate::scale::util::format_number_opt(b, primary.locale());
82                    (pos, format!("{label}%"))
83                })
84                .collect(),
85        )
86    }
87}
88
89impl std::fmt::Debug for SecAxis {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.debug_struct("SecAxis")
92            .field("name", &self.name)
93            .field("breaks", &self.breaks)
94            .field("percent_of_max", &self.percent_of_max)
95            .finish()
96    }
97}