cas_graph/graph/
mod.rs

1//! Customizable graphing calculator.
2//!
3//! This module provides basic graphing functionality, such as plotting expressions and points,
4//! then rendering the result to an image.
5//!
6//! To build an image of a graph, create a [`Graph`] and add expressions and / or points to it
7//! using [`Graph::add_expr`] / [`Graph::try_add_expr`] and [`Graph::add_point`]. Then, call
8//! [`Graph::draw()`] to render the graph to an image. This crate uses the [`cairo`] crate to
9//! render the graph, and thus can render to any format supported by [`cairo`], including PNG and
10//! SVG.
11//!
12//! # Adding expressions
13//!
14//! The argument to [`Graph::try_add_expr()`] is any expression that can be parsed by
15//! [`cas_parser`] as an [`Expr`]. If you have an [`Expr`] already, you can use
16//! [`Graph::add_expr()`].
17//!
18//! The given expression should be one defined in terms of the variables `x` (horizontal axis), `y`
19//! (vertical axis), or both, with an optional `y ==` or `x ==` prefix / suffix to clearly indicate
20//! the dependent variable. For example, the following are all valid expressions:
21//!
22//! - `y == 0.8214285714x^2 + 4.3785714286x + 7`
23//! - `0.8214285714x^2 + 4.3785714286x + 7`
24//! - `sin(x) == y`
25//! - `sin(y)`
26//! - `x == sin(y)`
27//! - `x^2 + y^2 == 1` TODO: relations are not yet supported
28//! - etc.
29//!
30//! # Customizing visuals
31//!
32//! The graph can be configured by passing a custom [`GraphOptions`] to [`Graph::with_opts`] to
33//! customize the viewport size, scale, and grid divisions. See [`GraphOptions`] for a description
34//! of each option along with their defaults.
35//!
36//! # Example
37//!
38//! The following example creates a graph with the expression
39//! `y == 0.8214285714x^2 + 4.3785714286x + 7` and a few points with the viewport centered on the
40//! added points. The graph is then rendered to a PNG file.
41//!
42//! ```no_run
43//! use cas_graph::{Graph, GraphOptions};
44//! use std::fs::File;
45//!
46//! fn main() -> Result<(), Box<dyn std::error::Error>> {
47//!     let opts = GraphOptions::default()
48//!         .square_scale(true); // scales the x- and y-axes together, looks nicer in my opinion
49//!     let surface = Graph::with_opts(opts)
50//!         .try_add_expr("y == 0.8214285714x^2 + 4.3785714286x + 7").unwrap()
51//!     //  .try_add_expr("0.8214285714x^2 + 4.3785714286x + 7").unwrap() // "y==" can be omitted
52//!         .add_point((-5.0, 5.0))
53//!         .add_point((-4.0, 4.0))
54//!         .add_point((-3.0, 1.0))
55//!         .add_point((-2.0, 0.5))
56//!         .add_point((-1.0, 4.0))
57//!         .center_on_points()
58//!         .draw()?;
59//!
60//!     let mut file = File::create("output.png")?;
61//!     surface.write_to_png(&mut file)?;
62//!
63//!     Ok(())
64//! }
65//! ```
66//!
67//! Output (note: colors were randomly chosen; random color selection is not included in the
68//! example code):
69//!
70//! <img src="https://raw.githubusercontent.com/ElectrifyPro/cas-rs/main/cas-graph/img/output.png" width="500" height="500"/>
71
72pub mod analyzed;
73mod eval;
74pub mod opts;
75pub mod point;
76
77use analyzed::AnalyzedExpr;
78use cairo::{Context, Error, FontSlant, Format, ImageSurface, FontWeight, TextExtents};
79use cas_parser::parser::{ast::expr::Expr, Parser};
80use eval::evaluate_expr;
81pub use point::{CanvasPoint, GraphPoint, Point};
82use rayon::prelude::*;
83use super::text_align::ShowTextAlign;
84pub use opts::GraphOptions;
85
86/// The extents of the edge labels. The corresponding field of each label can be `None` if the
87/// label if it is not visible / drawn.
88#[derive(Clone, Debug, Default)]
89struct EdgeExtents {
90    /// The extents of the top edge label.
91    pub top: Option<TextExtents>,
92
93    /// The extents of the bottom edge label.
94    pub bottom: Option<TextExtents>,
95
96    /// The extents of the left edge label.
97    pub left: Option<TextExtents>,
98
99    /// The extents of the right edge label.
100    pub right: Option<TextExtents>,
101}
102
103/// Round `n` to the nearest `k`.
104fn round_to(n: f64, k: f64) -> f64 {
105    (n / k).round() * k
106}
107
108/// Choose a major grid spacing for the given scale of an axis.
109///
110/// Returns a 2-tuple where the first element is the major grid spacing for the axis, and the
111/// second is the major grid divisions for the axis.
112fn choose_major_grid_spacing(mut scale: f64) -> (f64, u8) {
113    scale /= 4.0;
114
115    // to make the grid lines look nice and easier to read,
116    // only choose the closest scale:
117    if scale >= 1.0 {
118        // whose first digit is 1, 2, or 5
119        let num_digits = scale.log10().floor() as i32;
120        let scientific = scale / 10.0_f64.powi(num_digits);
121        if scientific >= 2.5 {
122            (5.0 * 10.0_f64.powi(num_digits), 5)
123        } else if scientific >= 1.25 {
124            (2.0 * 10.0_f64.powi(num_digits), 4)
125        } else {
126            (10.0_f64.powi(num_digits), 4)
127        }
128    } else {
129        // whose last decimal digit is 1, 2, or 5
130        let num_digits = -scale.log10().ceil() as i32 + 1;
131        let scientific = scale * 10.0_f64.powi(num_digits);
132        if scientific >= 0.25 {
133            (5.0 * 10.0_f64.powi(-num_digits), 5)
134        } else if scientific >= 0.125 {
135            (2.0 * 10.0_f64.powi(-num_digits), 4)
136        } else {
137            (10.0_f64.powi(-num_digits), 4)
138        }
139    }
140}
141
142/// A graph containing expressions and points to draw.
143///
144/// See the [module-level documentation](self) for more information.
145#[derive(Clone, Debug, Default)]
146pub struct Graph {
147    /// The expressions to draw.
148    pub expressions: Vec<AnalyzedExpr>,
149
150    /// The points to draw.
151    pub points: Vec<Point<f64>>,
152
153    /// The rendering options for the graph.
154    pub options: GraphOptions,
155}
156
157impl Graph {
158    /// Create a new, empty graph.
159    pub fn new() -> Graph {
160        Graph::default()
161    }
162
163    /// Create a new graph with the given options.
164    pub fn with_opts(options: GraphOptions) -> Graph {
165        Graph {
166            options,
167            ..Graph::default()
168        }
169    }
170
171    /// Add an expression to the graph.
172    ///
173    /// Returns a mutable reference to the graph to allow chaining.
174    pub fn add_expr(&mut self, expr: Expr) -> &mut Self {
175        self.expressions.push(AnalyzedExpr::new(expr));
176        self
177    }
178
179    /// Tries to parse the given expression and add it to the graph.
180    ///
181    /// Returns a mutable reference to the graph to allow chaining.
182    pub fn try_add_expr(&mut self, expr: &str) -> Result<&mut Self, Vec<cas_error::Error>> {
183        self.expressions.push(AnalyzedExpr::new(Parser::new(expr).try_parse_full()?));
184        Ok(self)
185    }
186
187    /// Adds an expression that has already been analyzed to the graph.
188    ///
189    /// Returns a mutable reference to the graph to allow chaining.
190    pub fn add_analyzed_expr(&mut self, expr: AnalyzedExpr) -> &mut Self {
191        self.expressions.push(expr);
192        self
193    }
194
195    /// Add a point to the graph.
196    ///
197    /// Returns a mutable reference to the graph to allow chaining.
198    pub fn add_point(&mut self, point: impl Into<Point<f64>>) -> &mut Self {
199        self.points.push(point.into());
200        self
201    }
202
203    /// Center the graph on the points in the graph and scale it so that all points are visible.
204    ///
205    /// Returns a mutable reference to the graph to allow chaining.
206    pub fn center_on_points(&mut self) -> &mut Self {
207        if self.points.is_empty() {
208            return self;
209        } else if self.points.len() == 1 {
210            self.options = GraphOptions {
211                canvas_size: self.options.canvas_size,
212                center: self.points[0].coordinates,
213                square_scale: self.options.square_scale,
214                ..Default::default()
215            };
216            return self;
217        }
218
219        let mut sum = GraphPoint(0.0, 0.0);
220
221        // find the average of the points and center on that
222        for point in self.points.iter() {
223            sum.0 += point.coordinates.0;
224            sum.1 += point.coordinates.1;
225        }
226
227        self.options.center = GraphPoint(
228            sum.0 / self.points.len() as f64,
229            sum.1 / self.points.len() as f64,
230        );
231
232        if self.options.square_scale {
233            // find the point furthest from the center and scale so that is is visible
234            let mut max_dist = 0.0;
235            for point in self.points.iter() {
236                let dist = point.coordinates.distance(self.options.center);
237                if dist > max_dist {
238                    max_dist = dist;
239                }
240            }
241            self.options.scale = GraphPoint(
242                max_dist * 1.5,
243                max_dist * 1.5,
244            );
245        } else {
246            // find the point furthest from the center in each direction and scale so that is is
247            // visible
248            let mut max_dist_x = 0.0;
249            let mut max_dist_y = 0.0;
250            for point in self.points.iter() {
251                let dist_x = (point.coordinates.0 - self.options.center.0).abs();
252                let dist_y = (point.coordinates.1 - self.options.center.1).abs();
253                if dist_x > max_dist_x {
254                    max_dist_x = dist_x;
255                }
256                if dist_y > max_dist_y {
257                    max_dist_y = dist_y;
258                }
259            }
260            self.options.scale = GraphPoint(
261                max_dist_x * 1.5,
262                max_dist_y * 1.5,
263            );
264        }
265
266        let (major_grid_spacing_x, major_grid_divisions_x) = choose_major_grid_spacing(self.options.scale.0);
267        let (major_grid_spacing_y, major_grid_divisions_y) = choose_major_grid_spacing(self.options.scale.1);
268        self.options.major_grid_spacing = GraphPoint(
269            major_grid_spacing_x,
270            major_grid_spacing_y,
271        );
272        self.options.major_grid_divisions = (
273            major_grid_divisions_x,
274            major_grid_divisions_y,
275        );
276
277        self
278    }
279
280    /// Creates an [`ImageSurface`] with the graph's canvas size and draws the graph to it.
281    ///
282    /// The resulting [`ImageSurface`] can be written to a file or manipulated further.
283    pub fn draw(&self) -> Result<ImageSurface, Error> {
284        let surface = ImageSurface::create(
285            Format::ARgb32,
286            self.options.canvas_size.0 as i32,
287            self.options.canvas_size.1 as i32,
288        )?;
289        let context = Context::new(&surface)?;
290
291        context.set_source_rgb(0.0, 0.0, 0.0);
292        context.paint()?;
293
294        context.select_font_face("sans-serif", FontSlant::Oblique, FontWeight::Normal);
295
296        let origin_canvas = self.options.to_canvas(GraphPoint(0.0, 0.0));
297        self.draw_grid_lines(&context)?;
298        self.draw_origin_axes(&context, origin_canvas)?;
299
300        let edges = if self.options.label_canvas_boundaries {
301            self.draw_boundary_labels(&context, origin_canvas)?
302        } else {
303            EdgeExtents::default()
304        };
305        self.draw_grid_line_numbers(&context, origin_canvas, edges)?;
306
307        self.draw_expressions(&context)?;
308        self.draw_points(&context)?;
309
310        Ok(surface)
311    }
312
313    /// Draw major and minor grid lines.
314    fn draw_grid_lines(
315        &self,
316        context: &Context,
317    ) -> Result<(), Error> {
318        // vertical grid lines (x = ...)
319        let mut count = 0;
320        let vert_bounds = (
321            round_to(self.options.center.0 - self.options.scale.0, self.options.major_grid_spacing.0) - self.options.major_grid_spacing.0,
322            round_to(self.options.center.0 + self.options.scale.0, self.options.major_grid_spacing.0) + self.options.major_grid_spacing.0,
323        );
324        let mut x = vert_bounds.0;
325        while x <= vert_bounds.1 {
326            if count == 0 {
327                // major line
328                context.set_source_rgba(0.4, 0.4, 0.4, self.options.major_grid_opacity);
329                context.set_line_width(2.0);
330            } else {
331                // minor line
332                context.set_source_rgba(0.4, 0.4, 0.4, self.options.minor_grid_opacity);
333                context.set_line_width(1.0);
334            }
335
336            count = (count + 1) % self.options.major_grid_divisions.0;
337
338            // is this grid line within the canvas bounds?
339            let x_canvas = self.options.x_to_canvas(x);
340            if x_canvas < 0.0 || x_canvas > self.options.canvas_size.0 as f64 {
341                x += self.options.major_grid_spacing.0 / self.options.major_grid_divisions.0 as f64;
342                continue;
343            }
344
345            context.move_to(x_canvas, 0.0);
346            context.line_to(x_canvas, self.options.canvas_size.1 as f64);
347            context.stroke()?;
348
349            x += self.options.major_grid_spacing.0 / self.options.major_grid_divisions.0 as f64;
350        }
351
352        // horizontal grid lines (y = ...)
353        let mut count = 0;
354        let hor_bounds = (
355            round_to(self.options.center.1 - self.options.scale.1, self.options.major_grid_spacing.1) - self.options.major_grid_spacing.1,
356            round_to(self.options.center.1 + self.options.scale.1, self.options.major_grid_spacing.1) + self.options.major_grid_spacing.1,
357        );
358        let mut y = hor_bounds.0;
359        while y <= hor_bounds.1 {
360            if count == 0 {
361                context.set_source_rgba(0.4, 0.4, 0.4, self.options.major_grid_opacity);
362                context.set_line_width(2.0);
363            } else {
364                context.set_source_rgba(0.4, 0.4, 0.4, self.options.minor_grid_opacity);
365                context.set_line_width(1.0);
366            }
367
368            count = (count + 1) % self.options.major_grid_divisions.1;
369
370            let y_canvas = self.options.y_to_canvas(y);
371            if y_canvas < 0.0 || y_canvas > self.options.canvas_size.1 as f64 {
372                y += self.options.major_grid_spacing.1 / self.options.major_grid_divisions.1 as f64;
373                continue;
374            }
375
376            context.move_to(0.0, y_canvas);
377            context.line_to(self.options.canvas_size.0 as f64, y_canvas);
378            context.stroke()?;
379
380            y += self.options.major_grid_spacing.1 / self.options.major_grid_divisions.1 as f64;
381        }
382
383        Ok(())
384    }
385
386    /// Draw major grid line numbers.
387    fn draw_grid_line_numbers(
388        &self,
389        context: &Context,
390        origin_canvas: CanvasPoint<f64>,
391        edges: EdgeExtents,
392    ) -> Result<(), Error> {
393        // TODO: check collisions between grid line numbers themselves
394        context.set_source_rgba(1.0, 1.0, 1.0, self.options.major_grid_opacity);
395        context.set_font_size(30.0);
396
397        let padding = 10.0;
398        let (canvas_width, canvas_height) = (
399            self.options.canvas_size.0 as f64,
400            self.options.canvas_size.1 as f64,
401        );
402
403        // vertical grid line numbers
404        let vert_bounds = (
405            round_to(self.options.center.0 - self.options.scale.0, self.options.major_grid_spacing.0),
406            round_to(self.options.center.0 + self.options.scale.0, self.options.major_grid_spacing.0),
407        );
408        let mut x = vert_bounds.0;
409        while x <= vert_bounds.1 {
410            // skip 0.0, as the origin axes will be drawn later
411            // this can be missed if floating point
412            if x == 0.0 {
413                x += self.options.major_grid_spacing.0;
414                continue;
415            }
416
417            // is this grid line number within the canvas bounds?
418            let x_canvas = self.options.x_to_canvas(x);
419            if x_canvas < 0.0 || x_canvas > self.options.canvas_size.0 as f64 {
420                x += self.options.major_grid_spacing.0;
421                continue;
422            }
423
424            let x_value_str = format!("{:.3}", x);
425            let x_value_str_trimmed = x_value_str.trim_end_matches('0').trim_end_matches('.');
426
427            // last check for 0.0
428            if x_value_str_trimmed == "0" || x_value_str_trimmed == "-0" {
429                x += self.options.major_grid_spacing.0;
430                continue;
431            }
432
433            let x_value_extents = context.text_extents(x_value_str_trimmed)?;
434
435            // will this grid line number collide with the left / right edge labels?
436            if let Some(left) = edges.left {
437                let text_left_bound = x_canvas - x_value_extents.width() / 2.0;
438                if text_left_bound < left.width() + padding {
439                    x += self.options.major_grid_spacing.0;
440                    continue;
441                }
442            }
443
444            if let Some(right) = edges.right {
445                let text_right_bound = x_canvas + x_value_extents.width() / 2.0;
446                if text_right_bound > self.options.canvas_size.0 as f64 - right.width() - padding {
447                    x += self.options.major_grid_spacing.0;
448                    continue;
449                }
450            }
451
452            // will the horizontal axis (y = 0) intersect with this grid line number?
453            let (y, anchor) = if origin_canvas.1 >= canvas_height - x_value_extents.height() - 2.0 * padding {
454                (origin_canvas.1.min(canvas_height) - padding, (0.5, 0.0))
455            } else {
456                (origin_canvas.1.max(0.0) + padding, (0.5, 1.0))
457            };
458
459            context.show_text_align_with_extents(
460                x_value_str_trimmed,
461                (x_canvas, y),
462                anchor,
463                &x_value_extents,
464            )?;
465
466            x += self.options.major_grid_spacing.0;
467        }
468
469        // horizontal grid line numbers
470        let hor_bounds = (
471            round_to(self.options.center.1 - self.options.scale.1, self.options.major_grid_spacing.1),
472            round_to(self.options.center.1 + self.options.scale.1, self.options.major_grid_spacing.1),
473        );
474        let mut y = hor_bounds.0;
475        while y <= hor_bounds.1 {
476            // same as above, but for the y-axis
477            if y == 0.0 {
478                y += self.options.major_grid_spacing.1;
479                continue;
480            }
481
482            let y_canvas = self.options.y_to_canvas(y);
483            if y_canvas < 0.0 || y_canvas > self.options.canvas_size.1 as f64 {
484                y += self.options.major_grid_spacing.1;
485                continue;
486            }
487
488            let y_value_str_raw = format!("{:.3}", y);
489            let y_value_str = y_value_str_raw.trim_end_matches('0').trim_end_matches('.');
490
491            if y_value_str == "0" || y_value_str == "-0" {
492                y += self.options.major_grid_spacing.0;
493                continue;
494            }
495
496            let y_value_extents = context.text_extents(y_value_str)?;
497
498            if let Some(top) = edges.top {
499                let text_top_bound = y_canvas - y_value_extents.height() / 2.0;
500                if text_top_bound < top.height() + padding {
501                    y += self.options.major_grid_spacing.1;
502                    continue;
503                }
504            }
505
506            if let Some(bottom) = edges.bottom {
507                let text_bottom_bound = y_canvas + y_value_extents.height() / 2.0;
508                if text_bottom_bound > self.options.canvas_size.1 as f64 - bottom.height() - padding {
509                    y += self.options.major_grid_spacing.1;
510                    continue;
511                }
512            }
513
514            let (x, anchor) = if origin_canvas.0 >= canvas_width - y_value_extents.width() - 2.0 * padding {
515                (origin_canvas.0.min(canvas_width) - padding, (1.0, 0.5))
516            } else {
517                (origin_canvas.0.max(0.0) + padding, (0.0, 0.5))
518            };
519
520            context.show_text_align_with_extents(
521                y_value_str,
522                (x, y_canvas),
523                anchor,
524                &y_value_extents,
525            )?;
526
527            y += self.options.major_grid_spacing.1;
528        }
529
530        Ok(())
531    }
532
533    /// Draw the origin axes if applicable.
534    fn draw_origin_axes(
535        &self,
536        context: &Context,
537        origin_canvas: CanvasPoint<f64>,
538    ) -> Result<(), Error> {
539        context.set_source_rgb(1.0, 1.0, 1.0);
540        context.set_line_width(5.0);
541
542        // vertical axis (x = 0)
543        if origin_canvas.0 >= 0.0 && origin_canvas.0 <= self.options.canvas_size.0 as f64 {
544            context.move_to(origin_canvas.0, 0.0);
545            context.line_to(origin_canvas.0, self.options.canvas_size.1 as f64);
546            context.stroke()?;
547        }
548
549        // horizontal axis (y = 0)
550        if origin_canvas.1 >= 0.0 && origin_canvas.1 <= self.options.canvas_size.1 as f64 {
551            context.move_to(0.0, origin_canvas.1);
552            context.line_to(self.options.canvas_size.0 as f64, origin_canvas.1);
553            context.stroke()?;
554        }
555
556        Ok(())
557    }
558
559    /// Draw the canvas boundary labels (the values at the edge of the canvas).
560    ///
561    /// Returns the extents of each edge label, which is used to mask major grid line numbers.
562    fn draw_boundary_labels(
563        &self,
564        context: &Context,
565        origin_canvas: CanvasPoint<f64>,
566    ) -> Result<EdgeExtents, Error> {
567        context.set_source_rgb(1.0, 1.0, 1.0);
568        context.set_font_size(40.0);
569
570        let padding = 10.0;
571        let (canvas_width, canvas_height) = (
572            self.options.canvas_size.0 as f64,
573            self.options.canvas_size.1 as f64,
574        );
575
576        // top edge, bottom edge
577        let x = origin_canvas.0;
578        let top = if origin_canvas.1 >= 0.0 {
579            // if the origin is visible or below the bottom edge of the canvas, draw the top edge
580            // label
581            let top_value = format!("{:.3}", self.options.center.1 + self.options.scale.1);
582            let top_value_trimmed = top_value.trim_end_matches('0').trim_end_matches('.');
583            let text_width = context.text_extents(top_value_trimmed)?.width();
584
585            // if the vertical axis (x = 0) intersects with the top edge label (it's too far to the
586            // right of the canvas), move the label to the left side of the vertical axis
587            let (x, anchor) = if x >= canvas_width - text_width - 2.0 * padding {
588                (x.min(canvas_width) - padding, (1.0, 1.0))
589            } else {
590                (x.max(0.0) + padding, (0.0, 1.0))
591            };
592
593            Some(context.show_text_align(
594                top_value_trimmed,
595                (x, padding),
596                anchor,
597            )?)
598        } else {
599            // otherwise, the top edge label might intersect with the numbers on the x-axis or the
600            // x-axis itself, so don't draw it
601            None
602        };
603
604        let bottom = if origin_canvas.1 <= canvas_height {
605            // if the origin is visible or above the top edge of the canvas, draw the bottom edge
606            // label
607            let bottom_value = format!("{:.3}", self.options.center.1 - self.options.scale.1);
608            let bottom_value_trimmed = bottom_value.trim_end_matches('0').trim_end_matches('.');
609            let text_width = context.text_extents(bottom_value_trimmed)?.width();
610
611            // if the vertical axis (x = 0) intersects with the bottom edge label (it's too far to
612            // the right of the canvas), move the label to the left side of the vertical axis
613            let (x, anchor) = if x >= canvas_width - text_width - 2.0 * padding {
614                (x.min(canvas_width) - padding, (1.0, 0.0))
615            } else {
616                (x.max(0.0) + padding, (0.0, 0.0))
617            };
618
619            Some(context.show_text_align(
620                bottom_value_trimmed,
621                (x, canvas_height - padding),
622                anchor,
623            )?)
624        } else {
625            // otherwise, the bottom edge label might intersect with the numbers on the x-axis or
626            // the x-axis itself, so don't draw it
627            None
628        };
629
630        // left edge, right edge
631        let y = origin_canvas.1;
632        let left = if origin_canvas.0 >= 0.0 {
633            // same as above, but for the left edge
634            let left_value = format!("{:.3}", self.options.center.0 - self.options.scale.0);
635            let left_value_trimmed = left_value.trim_end_matches('0').trim_end_matches('.');
636            let text_height = context.text_extents(left_value_trimmed)?.height();
637
638            let (y, anchor) = if y >= canvas_height - text_height - 2.0 * padding {
639                (y.min(canvas_height) - padding, (0.0, 0.0))
640            } else {
641                (y.max(0.0) + padding, (0.0, 1.0))
642            };
643
644            Some(context.show_text_align(
645                left_value.trim_end_matches('0').trim_end_matches('.'),
646                (padding, y),
647                anchor,
648            )?)
649        } else {
650            None
651        };
652
653        let right = if origin_canvas.0 <= canvas_width {
654            let right_value = format!("{:.3}", self.options.center.0 + self.options.scale.0);
655            let right_value_trimmed = right_value.trim_end_matches('0').trim_end_matches('.');
656            let text_height = context.text_extents(right_value_trimmed)?.height();
657
658            let (y, anchor) = if y >= canvas_height - text_height - 2.0 * padding {
659                (y.min(canvas_height) - padding, (1.0, 0.0))
660            } else {
661                (y.max(0.0) + padding, (1.0, 1.0))
662            };
663
664            Some(context.show_text_align(
665                right_value.trim_end_matches('0').trim_end_matches('.'),
666                (canvas_width - padding, y),
667                anchor,
668            )?)
669        } else {
670            None
671        };
672
673        Ok(EdgeExtents { top, bottom, left, right })
674    }
675
676    /// Draw the expressions in the graph.
677    fn draw_expressions(
678        &self,
679        context: &Context,
680    ) -> Result<(), Error> {
681        // evaluate expressions and draw as we go
682        context.set_line_width(5.0);
683
684        let expr_points = self.expressions.par_iter()
685            .map(|expr| (expr, evaluate_expr(expr, self.options)))
686            .collect::<Vec<_>>();
687        for (expr, points) in expr_points {
688            context.set_source_rgb(expr.color.0, expr.color.1, expr.color.2);
689
690            let mut first_eval = true;
691            for point in points {
692                let canvas = self.options.to_canvas(point);
693                if first_eval {
694                    context.move_to(canvas.0, canvas.1);
695                    first_eval = false;
696                } else {
697                    context.line_to(canvas.0, canvas.1);
698                }
699            }
700            context.stroke()?;
701        }
702
703        Ok(())
704    }
705
706    /// Draw the points in the graph.
707    fn draw_points(
708        &self,
709        context: &Context,
710    ) -> Result<(), Error> {
711        context.set_font_size(30.0);
712
713        for point in self.points.iter() {
714            context.set_source_rgb(point.color.0, point.color.1, point.color.2);
715
716            let canvas = self.options.to_canvas(point.coordinates);
717            context.arc(canvas.0, canvas.1, 10.0, 0.0, 2.0 * std::f64::consts::PI);
718            context.fill()?;
719
720            if let Some(label) = &point.label {
721                // draw the point's label
722                context.show_text_align(
723                    label,
724                    (canvas.0, canvas.1),
725                    (-0.1, -0.1),
726                )?;
727            } else {
728                // draw the point's coordinates
729                let point_value = (
730                    format!("{:.3}", point.coordinates.0),
731                    format!("{:.3}", point.coordinates.1),
732                );
733                context.show_text_align(
734                    &format!(
735                        "({}, {})",
736                        point_value.0.trim_end_matches('0').trim_end_matches('.'),
737                        point_value.1.trim_end_matches('0').trim_end_matches('.')
738                    ),
739                    (canvas.0, canvas.1),
740                    (-0.1, -0.1),
741                )?;
742            }
743        }
744
745        Ok(())
746    }
747}