cas_graph/graph/
opts.rs

1use super::point::{CanvasPoint, GraphPoint};
2
3/// Options to use when drawing a graph.
4#[derive(Clone, Copy, Debug)]
5pub struct GraphOptions {
6    /// The width and height of the canvas, in pixels.
7    ///
8    /// The default value is `(1000, 1000)`.
9    pub canvas_size: CanvasPoint<u16>,
10
11    /// The `(x, y)` point at which to center the graph.
12    ///
13    /// For example, to place the origin at the center of the output image, set this to `(0.0,
14    /// 0.0)`.
15    ///
16    /// This field will automatically be computed when calling
17    /// [`Graph::center_on_points`](super::Graph::center_on_points).
18    ///
19    /// The default value is `(0.0, 0.0)`.
20    pub center: GraphPoint<f64>,
21
22    /// The `(x, y)` scale of the graph.
23    ///
24    /// The scale indicates the distance, in graph units, from the center of the canvas to the edge
25    /// of the canvas. For example, when the graph is centered at `(0.0, 0.0)` with a scale of
26    /// `(10.0, 10.0)`, the visible graph will be from `(x, y): (-10.0, -10.0)` to `(x, y): (10.0,
27    /// 10.0)`.
28    ///
29    /// This field will automatically be computed when calling
30    /// [`Graph::center_on_points`](super::Graph::center_on_points).
31    ///
32    /// The default value is `(10.0, 10.0)`.
33    pub scale: GraphPoint<f64>,
34
35    /// When calling [`Graph::center_on_points`](super::Graph::center_on_points), determines
36    /// whether to scale the x- and y-axes together (resulting in a square graph) or independently
37    /// (resulting in a rectangular graph).
38    ///
39    /// The default value is `false`.
40    pub square_scale: bool,
41
42    /// Whether to label the canvas boundaries with their corresponding graph values.
43    ///
44    /// The default value is `false`.
45    pub label_canvas_boundaries: bool,
46
47    /// The number of graph units between each major grid line, given as a pair of `(x, y)` units.
48    ///
49    /// For example, to have a major grid line every `3.0` units on the x-axis and every `2.0`
50    /// units on the y-axis, set this to `(3.0, 2.0)`.
51    ///
52    /// This field will automatically be computed when calling
53    /// [`Graph::center_on_points`](super::Graph::center_on_points).
54    ///
55    /// The default value is `(2.0, 2.0)`.
56    pub major_grid_spacing: GraphPoint<f64>,
57
58    /// The number of spaces to divide each major grid line into, given as a pair of `(x, y)`
59    /// units. The number of minor grid lines between each major grid line will then be `x - 1` on
60    /// the x-axis, and `y - 1` on the y-axis.
61    ///
62    /// For example, to divide each major grid line into `5` spaces on the x-axis (4 minor grid
63    /// lines) and `4` spaces on the y-axis (3 minor grid lines), set this to `(5, 4)`.
64    ///
65    /// This field will automatically be computed when calling
66    /// [`Graph::center_on_points`](super::Graph::center_on_points).
67    ///
68    /// The default value is `(4, 4)`.
69    pub major_grid_divisions: (u8, u8),
70
71    /// The opacity of the major grid lines, given as a value in the range `0.0` to `1.0`, where
72    /// `0.0` is fully transparent and `1.0` is fully opaque. This also affects the opacity of the
73    /// major grid line numbers.
74    ///
75    /// The default value is `0.8`.
76    pub major_grid_opacity: f64,
77
78    /// The opacity of the minor grid lines, given as a value in the range `0.0` to `1.0`, where
79    /// `0.0` is fully transparent and `1.0` is fully opaque.
80    ///
81    /// The default value is `0.5`.
82    pub minor_grid_opacity: f64,
83}
84
85/// The default options for a graph. Returns a [`GraphOptions`] with the following values:
86///
87/// - [`canvas_size`](GraphOptions::canvas_size): `(1000, 1000)`
88/// - [`center`](GraphOptions::center): `(0.0, 0.0)`
89/// - [`scale`](GraphOptions::scale): `(10.0, 10.0)`
90/// - [`square_scale`](GraphOptions::square_scale): `false`
91/// - [`label_canvas_boundaries`](GraphOptions::label_canvas_boundaries): `false`
92/// - [`major_grid_spacing`](GraphOptions::major_grid_spacing): `(2.0, 2.0)`
93/// - [`major_grid_divisions`](GraphOptions::major_grid_divisions): `(4, 4)`
94impl Default for GraphOptions {
95    fn default() -> GraphOptions {
96        GraphOptions {
97            canvas_size: CanvasPoint(1000, 1000),
98            center: GraphPoint(0.0, 0.0),
99            scale: GraphPoint(10.0, 10.0),
100            square_scale: false,
101            label_canvas_boundaries: false,
102            major_grid_spacing: GraphPoint(2.0, 2.0),
103            major_grid_divisions: (4, 4),
104            major_grid_opacity: 0.8,
105            minor_grid_opacity: 0.4,
106        }
107    }
108}
109
110impl GraphOptions {
111    /// Set the canvas size. Returns an updated [`GraphOptions`] for chaining.
112    pub fn canvas_size(mut self, width: u16, height: u16) -> Self {
113        self.canvas_size = CanvasPoint(width, height);
114        self
115    }
116
117    /// Set the center of the graph. Returns an updated [`GraphOptions`] for chaining.
118    pub fn center(mut self, x: f64, y: f64) -> Self {
119        self.center = GraphPoint(x, y);
120        self
121    }
122
123    /// Set the scale of the graph. Returns an updated [`GraphOptions`] for chaining.
124    pub fn scale(mut self, x: f64, y: f64) -> Self {
125        self.scale = GraphPoint(x, y);
126        self
127    }
128
129    /// Set whether to scale the x- and y-axes together. Returns an updated [`GraphOptions`] for
130    /// chaining.
131    pub fn square_scale(mut self, square_scale: bool) -> Self {
132        self.square_scale = square_scale;
133        self
134    }
135
136    /// Set whether to label the canvas boundaries with their corresponding graph values. Returns
137    /// an updated [`GraphOptions`] for chaining.
138    pub fn label_canvas_boundaries(mut self, label_canvas_boundaries: bool) -> Self {
139        self.label_canvas_boundaries = label_canvas_boundaries;
140        self
141    }
142
143    /// Set the number of graph units between each major grid line. Returns an updated
144    /// [`GraphOptions`] for chaining.
145    pub fn major_grid_spacing(mut self, x: f64, y: f64) -> Self {
146        self.major_grid_spacing = GraphPoint(x, y);
147        self
148    }
149
150    /// Set the number of spaces to divide each major grid line into. Returns an updated
151    /// [`GraphOptions`] for chaining.
152    pub fn major_grid_divisions(mut self, x: u8, y: u8) -> Self {
153        self.major_grid_divisions = (x, y);
154        self
155    }
156
157    /// Set the opacity of the major grid lines. Returns an updated [`GraphOptions`] for chaining.
158    pub fn major_grid_opacity(mut self, major_grid_opacity: f64) -> Self {
159        self.major_grid_opacity = major_grid_opacity;
160        self
161    }
162
163    /// Set the opacity of the minor grid lines. Returns an updated [`GraphOptions`] for chaining.
164    pub fn minor_grid_opacity(mut self, minor_grid_opacity: f64) -> Self {
165        self.minor_grid_opacity = minor_grid_opacity;
166        self
167    }
168}
169
170impl GraphOptions {
171    /// Converts an x-value in **graph** space to an x-value in **canvas** space.
172    pub(crate) fn x_to_canvas(&self, x: f64) -> f64 {
173        let graph_space_range = self.scale.0 * 2.0;
174
175        // normalize x-value to [0.0, 1.0], where 0.0 indicates left-edge of visible graph, 1.0
176        // indicates right-edge of visible graph
177        let normalized = (x - self.center.0) / graph_space_range + 0.5;
178
179        // convert normalized x-value to canvas space
180        normalized * self.canvas_size.0 as f64
181    }
182
183    /// Converts a y-value in **graph** space to a y-value in **canvas** space.
184    pub(crate) fn y_to_canvas(&self, y: f64) -> f64 {
185        let graph_space_range = self.scale.1 * 2.0;
186
187        // normalize y-value to [0.0, 1.0], then flip the normalized value so, 0.0 is top, 1.0 is bottom
188        // this is because the y-axis is flipped in graph space
189        let normalized = 0.5 - (y - self.center.1) / graph_space_range;
190
191        // convert normalized y-value to canvas space
192        normalized * self.canvas_size.1 as f64
193    }
194
195    /// Converts a point in **graph** space to **canvas** space.
196    pub fn to_canvas(&self, point: GraphPoint<f64>) -> CanvasPoint<f64> {
197        CanvasPoint(
198            self.x_to_canvas(point.0),
199            self.y_to_canvas(point.1),
200        )
201    }
202
203    /// Converts an x-value in **canvas** space to an x-value in **graph** space.
204    pub(crate) fn x_to_graph(&self, x: f64) -> f64 {
205        // normalize x-value to [0.0, 1.0], where 0.0 indicates left-edge of canvas, 1.0 indicates
206        // right-edge of canvas (x should always be positive)
207        let normalized = x / self.canvas_size.0 as f64;
208
209        // convert normalized x-value to graph space
210        let graph_space_range = self.scale.0 * 2.0;
211        let left_edge_graph_space = self.center.0 - self.scale.0;
212
213        normalized * graph_space_range + left_edge_graph_space
214    }
215
216    /// Converts a y-value in **canvas** space to a y-value in **graph** space.
217    pub(crate) fn y_to_graph(&self, y: f64) -> f64 {
218        // normalize y-value to [0.0, 1.0], then flip the normalized value so, 0.0 is bottom, 1.0 is top
219        // this is because the y-axis is flipped in canvas space
220        let normalized = 1.0 - y / self.canvas_size.1 as f64;
221
222        // convert normalized y-value to graph space
223        let graph_space_range = self.scale.1 * 2.0;
224        let bottom_edge_graph_space = self.center.1 - self.scale.1;
225
226        normalized * graph_space_range + bottom_edge_graph_space
227    }
228
229    /// Converts a point in **canvas** space to **graph** space.
230    pub fn to_graph(&self, point: CanvasPoint<f64>) -> GraphPoint<f64> {
231        GraphPoint(
232            self.x_to_graph(point.0),
233            self.y_to_graph(point.1),
234        )
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use crate::graph::round_to;
241    use super::*;
242
243    /// Test the conversion functions from canvas to graph space.
244    #[test]
245    fn canvas_to_graph() {
246        let options = GraphOptions {
247            canvas_size: CanvasPoint(465, 917),
248            center: GraphPoint(-3.0, 2.41),
249            scale: GraphPoint(3.59, 5.69),
250            ..Default::default()
251        };
252
253        assert_eq!(
254            options.x_to_graph(0.0),
255            options.center.0 - options.scale.0,
256        );
257        assert_eq!(
258            options.x_to_graph(options.canvas_size.0 as f64),
259            options.center.0 + options.scale.0,
260        );
261        assert_eq!(
262            options.y_to_graph(0.0),
263            options.center.1 + options.scale.1,
264        );
265        assert_eq!(
266            options.y_to_graph(options.canvas_size.1 as f64),
267            options.center.1 - options.scale.1,
268        );
269    }
270
271    /// Test the conversion functions from graph to canvas space.
272    #[test]
273    fn graph_to_canvas() {
274        let options = GraphOptions {
275            canvas_size: CanvasPoint(465, 917),
276            center: GraphPoint(-3.0, 2.41),
277            scale: GraphPoint(3.59, 5.69),
278            ..Default::default()
279        };
280
281        assert_eq!(
282            options.x_to_canvas(options.center.0 - options.scale.0),
283            0.0,
284        );
285        assert_eq!(
286            options.x_to_canvas(options.center.0 + options.scale.0),
287            options.canvas_size.0 as f64,
288        );
289        // precision is wonky with this one
290        assert_eq!(
291            round_to(options.y_to_canvas(options.center.1 + options.scale.1), 1e-6),
292            0.0,
293        );
294        assert_eq!(
295            options.y_to_canvas(options.center.1 - options.scale.1),
296            options.canvas_size.1 as f64,
297        );
298    }
299}