1pub 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#[derive(Clone, Debug, Default)]
89struct EdgeExtents {
90 pub top: Option<TextExtents>,
92
93 pub bottom: Option<TextExtents>,
95
96 pub left: Option<TextExtents>,
98
99 pub right: Option<TextExtents>,
101}
102
103fn round_to(n: f64, k: f64) -> f64 {
105 (n / k).round() * k
106}
107
108fn choose_major_grid_spacing(mut scale: f64) -> (f64, u8) {
113 scale /= 4.0;
114
115 if scale >= 1.0 {
118 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 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#[derive(Clone, Debug, Default)]
146pub struct Graph {
147 pub expressions: Vec<AnalyzedExpr>,
149
150 pub points: Vec<Point<f64>>,
152
153 pub options: GraphOptions,
155}
156
157impl Graph {
158 pub fn new() -> Graph {
160 Graph::default()
161 }
162
163 pub fn with_opts(options: GraphOptions) -> Graph {
165 Graph {
166 options,
167 ..Graph::default()
168 }
169 }
170
171 pub fn add_expr(&mut self, expr: Expr) -> &mut Self {
175 self.expressions.push(AnalyzedExpr::new(expr));
176 self
177 }
178
179 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 pub fn add_analyzed_expr(&mut self, expr: AnalyzedExpr) -> &mut Self {
191 self.expressions.push(expr);
192 self
193 }
194
195 pub fn add_point(&mut self, point: impl Into<Point<f64>>) -> &mut Self {
199 self.points.push(point.into());
200 self
201 }
202
203 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 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 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 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 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 fn draw_grid_lines(
315 &self,
316 context: &Context,
317 ) -> Result<(), Error> {
318 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 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 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 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 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 fn draw_grid_line_numbers(
388 &self,
389 context: &Context,
390 origin_canvas: CanvasPoint<f64>,
391 edges: EdgeExtents,
392 ) -> Result<(), Error> {
393 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 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 if x == 0.0 {
413 x += self.options.major_grid_spacing.0;
414 continue;
415 }
416
417 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 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 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 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 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 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 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 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 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 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 let x = origin_canvas.0;
578 let top = if origin_canvas.1 >= 0.0 {
579 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 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 None
602 };
603
604 let bottom = if origin_canvas.1 <= canvas_height {
605 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 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 None
628 };
629
630 let y = origin_canvas.1;
632 let left = if origin_canvas.0 >= 0.0 {
633 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 fn draw_expressions(
678 &self,
679 context: &Context,
680 ) -> Result<(), Error> {
681 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 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 context.show_text_align(
723 label,
724 (canvas.0, canvas.1),
725 (-0.1, -0.1),
726 )?;
727 } else {
728 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}