Skip to main content

rust_ppm/plot/
axes.rs

1/// Axis limits, ticks, labels, and title for a plot.
2#[derive(Clone, Debug, PartialEq)]
3pub struct Axes {
4    /// Lower and upper x-axis limits.
5    pub xlim: (f64, f64),
6    /// Lower and upper y-axis limits.
7    pub ylim: (f64, f64),
8    /// Tick values shown on the x axis.
9    pub xticks: Vec<f64>,
10    /// Tick values shown on the y axis.
11    pub yticks: Vec<f64>,
12    /// Label drawn beneath the plot.
13    pub xlabel: String,
14    /// Label drawn beside the plot.
15    pub ylabel: String,
16    /// Plot title.
17    pub title: String,
18    auto_xlim: bool,
19    auto_ylim: bool,
20}
21
22impl Axes {
23    /// Creates a new axis configuration with automatic limits and default labels.
24    pub fn new() -> Self {
25        Self {
26            xlim: (0.0, 1.0),
27            ylim: (0.0, 1.0),
28            xticks: evenly_spaced_ticks((0.0, 1.0), 4),
29            yticks: evenly_spaced_ticks((0.0, 1.0), 4),
30            xlabel: "x".to_owned(),
31            ylabel: "y".to_owned(),
32            title: "plot".to_owned(),
33            auto_xlim: true,
34            auto_ylim: true,
35        }
36    }
37
38    /// Creates a new axes configuration from explicit x/y limits.
39    pub fn from_limits(xlim: (f64, f64), ylim: (f64, f64)) -> Self {
40        Self::new().with_xlim(xlim).with_ylim(ylim)
41    }
42
43    /// Sets the x-axis limits while keeping the builder pattern.
44    pub fn x_limits(self, limits: (f64, f64)) -> Self {
45        self.with_xlim(limits)
46    }
47
48    /// Sets the y-axis limits while keeping the builder pattern.
49    pub fn y_limits(self, limits: (f64, f64)) -> Self {
50        self.with_ylim(limits)
51    }
52
53    /// Sets the x and y axis labels while keeping the builder pattern.
54    pub fn labels(self, x: impl Into<String>, y: impl Into<String>) -> Self {
55        self.with_labels(x, y)
56    }
57
58    /// Sets the plot title while keeping the builder pattern.
59    pub fn title(self, title: impl Into<String>) -> Self {
60        self.with_title(title)
61    }
62
63    /// Replaces the x-axis tick positions with explicit values.
64    pub fn x_ticks(mut self, ticks: impl IntoIterator<Item = f64>) -> Self {
65        self.xticks = ticks.into_iter().collect();
66        self
67    }
68
69    /// Replaces the y-axis tick positions with explicit values.
70    pub fn y_ticks(mut self, ticks: impl IntoIterator<Item = f64>) -> Self {
71        self.yticks = ticks.into_iter().collect();
72        self
73    }
74
75    /// Sets the x-axis limits and disables automatic rescaling for that axis.
76    pub fn with_xlim(mut self, xlim: (f64, f64)) -> Self {
77        self.set_xlim(xlim);
78        self
79    }
80
81    /// Sets the y-axis limits and disables automatic rescaling for that axis.
82    pub fn with_ylim(mut self, ylim: (f64, f64)) -> Self {
83        self.set_ylim(ylim);
84        self
85    }
86
87    /// Sets the x-axis limits and disables automatic rescaling for that axis.
88    pub fn set_xlim(&mut self, xlim: (f64, f64)) {
89        self.xlim = xlim;
90        self.xticks = evenly_spaced_ticks(xlim, 4);
91        self.auto_xlim = false;
92    }
93
94    /// Sets the y-axis limits and disables automatic rescaling for that axis.
95    pub fn set_ylim(&mut self, ylim: (f64, f64)) {
96        self.ylim = ylim;
97        self.yticks = evenly_spaced_ticks(ylim, 4);
98        self.auto_ylim = false;
99    }
100
101    /// Re-enables automatic fitting for the x-axis.
102    pub fn use_auto_xlim(&mut self) {
103        self.auto_xlim = true;
104    }
105
106    /// Re-enables automatic fitting for the y-axis.
107    pub fn use_auto_ylim(&mut self) {
108        self.auto_ylim = true;
109    }
110
111    /// Sets the x and y axis labels.
112    pub fn with_labels(mut self, xlabel: impl Into<String>, ylabel: impl Into<String>) -> Self {
113        self.xlabel = xlabel.into();
114        self.ylabel = ylabel.into();
115        self
116    }
117
118    /// Sets the plot title.
119    pub fn with_title(mut self, title: impl Into<String>) -> Self {
120        self.title = title.into();
121        self
122    }
123
124    pub(crate) fn update_auto_limits(
125        &mut self,
126        xlim: Option<(f64, f64)>,
127        ylim: Option<(f64, f64)>,
128    ) -> bool {
129        let mut changed = false;
130
131        if self.auto_xlim
132            && let Some(xlim) = xlim
133        {
134            changed |= self.xlim != xlim;
135            self.xlim = xlim;
136            self.xticks = evenly_spaced_ticks(xlim, 4);
137        }
138        if self.auto_ylim
139            && let Some(ylim) = ylim
140        {
141            changed |= self.ylim != ylim;
142            self.ylim = ylim;
143            self.yticks = evenly_spaced_ticks(ylim, 4);
144        }
145
146        changed
147    }
148}
149
150impl Default for Axes {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156fn evenly_spaced_ticks(limits: (f64, f64), intervals: usize) -> Vec<f64> {
157    if intervals == 0 {
158        return vec![limits.0];
159    }
160
161    (0..=intervals)
162        .map(|index| limits.0 + (limits.1 - limits.0) * index as f64 / intervals as f64)
163        .collect()
164}