1use crate::{Image, Pixel};
2
3use super::Axes;
4use super::raster::{Bounds, draw_axis_arrows, draw_box, draw_line, draw_marker, draw_thick_line};
5use super::style::{LineStyle, MarkerStyle};
6use super::text::{self, GLYPH_HEIGHT};
7
8const TICK_LENGTH: usize = 4;
9const TEXT_GAP: usize = 2;
10
11enum Series {
12 Markers(Vec<(f64, f64)>, MarkerStyle),
13 Line(Vec<(f64, f64)>, LineStyle),
14}
15
16impl Series {
17 fn points(&self) -> &[(f64, f64)] {
18 match self {
19 Self::Markers(points, _) | Self::Line(points, _) => points,
20 }
21 }
22}
23
24pub struct Canvas {
30 pub axes: Axes,
32 pub image: Image,
34 inset_x: usize,
35 inset_y: usize,
36 render_scale: usize,
37 series: Vec<Series>,
38}
39
40impl Canvas {
41 pub fn new(width: usize, height: usize, axes: Axes) -> Self {
43 Self::with_inset(width, height, axes, 0, 0)
44 }
45
46 pub fn with_inset(
48 width: usize,
49 height: usize,
50 axes: Axes,
51 inset_x: usize,
52 inset_y: usize,
53 ) -> Self {
54 Self {
55 axes,
56 image: Image::new_white(width, height),
57 inset_x,
58 inset_y,
59 render_scale: 1,
60 series: Vec::new(),
61 }
62 }
63
64 pub fn width(&self) -> usize {
66 self.image.width
67 }
68
69 pub fn height(&self) -> usize {
71 self.image.height
72 }
73
74 pub fn insets(&self) -> (usize, usize) {
76 (self.inset_x, self.inset_y)
77 }
78
79 pub fn set_inset(&mut self, inset_x: usize, inset_y: usize) {
81 self.inset_x = inset_x;
82 self.inset_y = inset_y;
83 }
84
85 pub fn set_render_scale(&mut self, scale: usize) {
87 assert!(scale > 0, "render scale must be greater than zero");
88 self.render_scale = scale;
89 self.redraw();
90 }
91
92 pub fn render(&mut self) {
94 let Some(bounds) = self.bounds() else {
95 return;
96 };
97 self.draw_axes(bounds);
98 }
99
100 pub fn scatter(&mut self, points: &[(f64, f64)], size: usize) {
102 self.markers(points, MarkerStyle::new().size(size));
103 }
104
105 pub fn markers(&mut self, points: &[(f64, f64)], style: MarkerStyle) {
107 self.series.push(Series::Markers(points.to_vec(), style));
108 if self.refresh_auto_limits() {
109 self.redraw();
110 } else {
111 self.draw_markers(points, style);
112 }
113 }
114
115 pub fn plot(&mut self, points: &[(f64, f64)], line_width: usize) {
117 self.line(points, LineStyle::new().width(line_width));
118 }
119
120 pub fn line(&mut self, points: &[(f64, f64)], style: LineStyle) {
122 self.series.push(Series::Line(points.to_vec(), style));
123 if self.refresh_auto_limits() {
124 self.redraw();
125 } else {
126 self.draw_line_series(points, style);
127 }
128 }
129
130 pub fn set_xlim(&mut self, xlim: (f64, f64)) {
132 self.axes.set_xlim(xlim);
133 self.redraw();
134 }
135
136 pub fn set_ylim(&mut self, ylim: (f64, f64)) {
138 self.axes.set_ylim(ylim);
139 self.redraw();
140 }
141
142 pub fn use_auto_xlim(&mut self) {
144 self.axes.use_auto_xlim();
145 self.refresh_auto_limits();
146 self.redraw();
147 }
148
149 pub fn use_auto_ylim(&mut self) {
151 self.axes.use_auto_ylim();
152 self.refresh_auto_limits();
153 self.redraw();
154 }
155
156 fn draw_markers(&mut self, points: &[(f64, f64)], style: MarkerStyle) {
157 let Some(bounds) = self.bounds() else {
158 return;
159 };
160
161 for &(x, y) in points {
162 if !self.contains(x, y) {
163 continue;
164 }
165
166 if let Some(pixel) = self.map_point((x, y), bounds) {
167 draw_marker(&mut self.image, pixel, style.size, bounds, style.color);
168 }
169 }
170 }
171
172 fn draw_line_series(&mut self, points: &[(f64, f64)], style: LineStyle) {
173 let Some(bounds) = self.bounds() else {
174 return;
175 };
176
177 let mut previous = None;
178 for &(x, y) in points {
179 if !self.contains(x, y) {
180 previous = None;
181 continue;
182 }
183
184 if let Some(pixel) = self.map_point((x, y), bounds) {
185 if let Some(start) = previous {
186 draw_thick_line(
187 &mut self.image,
188 start,
189 pixel,
190 style.width,
191 bounds,
192 style.color,
193 );
194 } else {
195 draw_marker(&mut self.image, pixel, style.width, bounds, style.color);
196 }
197 previous = Some(pixel);
198 }
199 }
200 }
201
202 fn refresh_auto_limits(&mut self) -> bool {
203 let limits = data_limits(self.series.iter().flat_map(|series| series.points()));
204 let (xlim, ylim) = limits.unzip();
205 self.axes.update_auto_limits(xlim, ylim)
206 }
207
208 fn redraw(&mut self) {
209 self.image = Image::new_white(self.width(), self.height());
210 self.render();
211
212 let series = std::mem::take(&mut self.series);
213 for item in &series {
214 match item {
215 Series::Markers(points, style) => self.draw_markers(points, *style),
216 Series::Line(points, style) => self.draw_line_series(points, *style),
217 }
218 }
219 self.series = series;
220 }
221
222 pub fn into_image(self) -> Image {
224 self.image
225 }
226
227 fn bounds(&self) -> Option<Bounds> {
228 Bounds::from_insets(self.width(), self.height(), self.inset_x, self.inset_y)
229 }
230
231 fn contains(&self, x: f64, y: f64) -> bool {
232 x >= self.axes.xlim.0
233 && x <= self.axes.xlim.1
234 && y >= self.axes.ylim.0
235 && y <= self.axes.ylim.1
236 }
237
238 fn map_point(&self, point: (f64, f64), bounds: Bounds) -> Option<(usize, usize)> {
239 Some((
240 map_x(point.0, self.axes.xlim, bounds)?,
241 map_y(point.1, self.axes.ylim, bounds)?,
242 ))
243 }
244
245 fn draw_axes(&mut self, bounds: Bounds) {
246 draw_box(&mut self.image, bounds, Pixel::BLACK);
247
248 let y_axis_x = if contains_zero(self.axes.xlim) {
249 map_x(0.0, self.axes.xlim, bounds)
250 } else {
251 Some(bounds.left + (bounds.right - bounds.left) / 2)
252 };
253 let x_axis_y = if contains_zero(self.axes.ylim) {
254 map_y(0.0, self.axes.ylim, bounds)
255 } else {
256 Some(bounds.top + (bounds.bottom - bounds.top) / 2)
257 };
258
259 if let Some(x) = y_axis_x {
260 for y in bounds.top..=bounds.bottom {
261 self.image.set_pixel(x, y, Pixel::BLACK);
262 }
263 }
264 if let Some(y) = x_axis_y {
265 for x in bounds.left..=bounds.right {
266 self.image.set_pixel(x, y, Pixel::BLACK);
267 }
268 }
269
270 self.draw_ticks(bounds);
271
272 if let (Some(y_axis_x), Some(x_axis_y)) = (y_axis_x, x_axis_y) {
273 draw_axis_arrows(&mut self.image, y_axis_x, x_axis_y, bounds, Pixel::BLACK);
274 }
275
276 self.draw_labels(bounds);
277 }
278
279 fn draw_ticks(&mut self, bounds: Bounds) {
280 let bottom_margin = self.height() - 1 - bounds.bottom;
281 let tick_length = TICK_LENGTH * self.render_scale;
282 let text_gap = TEXT_GAP * self.render_scale;
283 let glyph_height = GLYPH_HEIGHT * self.render_scale;
284
285 for &tick in &self.axes.xticks {
286 let Some(x) = map_x(tick, self.axes.xlim, bounds) else {
287 continue;
288 };
289 let tick_end = (bounds.bottom + tick_length).min(self.height() - 1);
290 draw_line(
291 &mut self.image,
292 (x, bounds.bottom),
293 (x, tick_end),
294 Pixel::BLACK,
295 );
296
297 if bottom_margin >= tick_length + text_gap + glyph_height {
298 let label = format_tick(tick);
299 let (label_width, _) = text::measure(&label, self.render_scale);
300 let label_x = centered_start(x, label_width, self.width());
301 text::draw(
302 &mut self.image,
303 (label_x, bounds.bottom + tick_length + text_gap),
304 &label,
305 self.render_scale,
306 Pixel::BLACK,
307 );
308 }
309 }
310
311 for &tick in &self.axes.yticks {
312 let Some(y) = map_y(tick, self.axes.ylim, bounds) else {
313 continue;
314 };
315 let tick_start = bounds.left.saturating_sub(tick_length);
316 draw_line(
317 &mut self.image,
318 (tick_start, y),
319 (bounds.left, y),
320 Pixel::BLACK,
321 );
322
323 let label = format_tick(tick);
324 let (label_width, label_height) = text::measure(&label, self.render_scale);
325 if self.height() >= label_height && bounds.left >= tick_length + text_gap + label_width
326 {
327 let label_x = tick_start - text_gap - label_width;
328 let label_y = y
329 .saturating_sub(label_height / 2)
330 .min(self.height() - label_height);
331 text::draw(
332 &mut self.image,
333 (label_x, label_y),
334 &label,
335 self.render_scale,
336 Pixel::BLACK,
337 );
338 }
339 }
340 }
341
342 fn draw_labels(&mut self, bounds: Bounds) {
343 let bottom_margin = self.height() - 1 - bounds.bottom;
344 let tick_length = TICK_LENGTH * self.render_scale;
345 let text_gap = TEXT_GAP * self.render_scale;
346 let glyph_height = GLYPH_HEIGHT * self.render_scale;
347
348 if !self.axes.title.is_empty() && bounds.top >= glyph_height + text_gap {
349 let (width, _) = text::measure(&self.axes.title, self.render_scale);
350 let x = centered_start((bounds.left + bounds.right) / 2, width, self.width());
351 text::draw(
352 &mut self.image,
353 (x, bounds.top - glyph_height - text_gap),
354 &self.axes.title,
355 self.render_scale,
356 Pixel::BLACK,
357 );
358 }
359
360 let xlabel_y = bounds.bottom + tick_length + text_gap + glyph_height + text_gap;
361 if !self.axes.xlabel.is_empty() && bottom_margin >= xlabel_y - bounds.bottom + glyph_height
362 {
363 let (width, _) = text::measure(&self.axes.xlabel, self.render_scale);
364 let x = centered_start((bounds.left + bounds.right) / 2, width, self.width());
365 text::draw(
366 &mut self.image,
367 (x, xlabel_y),
368 &self.axes.xlabel,
369 self.render_scale,
370 Pixel::BLACK,
371 );
372 }
373
374 let max_tick_width = self
375 .axes
376 .yticks
377 .iter()
378 .map(|&tick| text::measure(&format_tick(tick), self.render_scale).0)
379 .max()
380 .unwrap_or(0);
381 let required_left = tick_length + text_gap + max_tick_width + text_gap + glyph_height;
382 if !self.axes.ylabel.is_empty() && bounds.left >= required_left {
383 let (rotated_height, _) = text::measure(&self.axes.ylabel, self.render_scale);
384 let x = bounds.left - required_left;
385 let y = centered_start(
386 (bounds.top + bounds.bottom) / 2,
387 rotated_height,
388 self.height(),
389 );
390 text::draw_rotated_counterclockwise(
391 &mut self.image,
392 (x, y),
393 &self.axes.ylabel,
394 self.render_scale,
395 Pixel::BLACK,
396 );
397 }
398 }
399}
400
401fn map_x(value: f64, limits: (f64, f64), bounds: Bounds) -> Option<usize> {
402 map_value(value, limits, bounds.width()).map(|pixel| bounds.left + pixel)
403}
404
405fn map_y(value: f64, limits: (f64, f64), bounds: Bounds) -> Option<usize> {
406 map_value(value, limits, bounds.height()).map(|pixel| bounds.bottom - pixel)
407}
408
409fn map_value(value: f64, limits: (f64, f64), size: usize) -> Option<usize> {
410 if limits.1 <= limits.0 || size == 0 || value < limits.0 || value > limits.1 {
411 return None;
412 }
413 let scaled = (value - limits.0) / (limits.1 - limits.0);
414 Some((scaled * (size - 1) as f64).round() as usize)
415}
416
417fn contains_zero(limits: (f64, f64)) -> bool {
418 limits.0 <= 0.0 && 0.0 <= limits.1
419}
420
421fn centered_start(center: usize, length: usize, extent: usize) -> usize {
422 center
423 .saturating_sub(length / 2)
424 .min(extent.saturating_sub(length))
425}
426
427fn format_tick(value: f64) -> String {
428 if value == 0.0 {
429 return "0".to_owned();
430 }
431
432 let magnitude = value.abs();
433 if !(0.001..10_000.0).contains(&magnitude) {
434 return format!("{value:.1e}");
435 }
436
437 format!("{value:.2}")
438 .trim_end_matches('0')
439 .trim_end_matches('.')
440 .to_owned()
441}
442
443fn data_limits<'a>(
444 points: impl Iterator<Item = &'a (f64, f64)>,
445) -> Option<((f64, f64), (f64, f64))> {
446 let mut limits: Option<((f64, f64), (f64, f64))> = None;
447
448 for &(x, y) in points.filter(|(x, y)| x.is_finite() && y.is_finite()) {
449 limits = Some(match limits {
450 None => ((x, x), (y, y)),
451 Some(((xmin, xmax), (ymin, ymax))) => {
452 ((xmin.min(x), xmax.max(x)), (ymin.min(y), ymax.max(y)))
453 }
454 });
455 }
456
457 limits.map(|(xlim, ylim)| (expand_degenerate(xlim), expand_degenerate(ylim)))
458}
459
460fn expand_degenerate(limits: (f64, f64)) -> (f64, f64) {
461 if limits.0 != limits.1 {
462 return limits;
463 }
464
465 let padding = (limits.0.abs() * 0.05).max(1.0);
466 (limits.0 - padding, limits.1 + padding)
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn coordinates_start_at_bottom_left() {
475 let bounds = Bounds::from_insets(7, 7, 0, 0).unwrap();
476 assert_eq!(map_x(-2.0, (-2.0, 4.0), bounds), Some(0));
477 assert_eq!(map_x(4.0, (-2.0, 4.0), bounds), Some(6));
478 assert_eq!(map_y(-2.0, (-2.0, 16.0), bounds), Some(6));
479 assert_eq!(map_y(16.0, (-2.0, 16.0), bounds), Some(0));
480 }
481
482 #[test]
483 fn canvas_respects_horizontal_and_vertical_insets() {
484 let mut canvas =
485 Canvas::with_inset(11, 11, Axes::from_limits((0.0, 1.0), (0.0, 1.0)), 2, 3);
486 canvas.render();
487
488 assert_eq!(canvas.image.get_pixel(0, 0), Some(&Pixel::WHITE));
489 assert_eq!(canvas.image.get_pixel(2, 3), Some(&Pixel::BLACK));
490 assert_eq!(canvas.image.get_pixel(8, 7), Some(&Pixel::BLACK));
491 }
492
493 #[test]
494 fn axes_render_origin() {
495 let mut canvas = Canvas::new(7, 7, Axes::from_limits((0.0, 2.0), (0.0, 2.0)));
496 canvas.render();
497
498 assert_eq!(canvas.image.get_pixel(0, 3), Some(&Pixel::BLACK));
499 assert_eq!(canvas.image.get_pixel(3, 6), Some(&Pixel::BLACK));
500 }
501
502 #[test]
503 fn labels_and_tick_values_render_inside_margins() {
504 let axes = Axes::from_limits((0.0, 1.0), (0.0, 1.0))
505 .with_labels("time", "value")
506 .with_title("demo");
507 let mut canvas = Canvas::with_inset(128, 128, axes, 40, 32);
508 canvas.render();
509
510 let width = canvas.width();
511 let margin_has_text = canvas
512 .image
513 .pixels()
514 .iter()
515 .enumerate()
516 .any(|(index, pixel)| {
517 let x = index % width;
518 let y = index / width;
519 *pixel == Pixel::BLACK && (x < 36 || !(28..=100).contains(&y))
520 });
521 assert!(margin_has_text);
522 }
523
524 #[test]
525 fn tiny_canvas_does_not_panic_while_laying_out_ticks() {
526 let mut canvas =
527 Canvas::with_inset(64, 1, Axes::from_limits((0.0, 1.0), (0.0, 1.0)), 20, 0);
528
529 canvas.render();
530 assert_eq!(canvas.height(), 1);
531 }
532
533 #[test]
534 fn axes_automatically_fit_all_added_series() {
535 let mut canvas = Canvas::new(11, 11, Axes::new());
536 canvas.render();
537 canvas.scatter(&[(0.0, 0.0)], 1);
538 canvas.scatter(&[(3.0, 4.0)], 1);
539
540 assert_eq!(canvas.axes.xlim, (0.0, 3.0));
541 assert_eq!(canvas.axes.ylim, (0.0, 4.0));
542 assert_eq!(canvas.image.get_pixel(0, 10), Some(&Pixel::rgb(255, 0, 0)));
543 assert_eq!(canvas.image.get_pixel(10, 0), Some(&Pixel::rgb(255, 0, 0)));
544 }
545
546 #[test]
547 fn manual_limits_override_automatic_limits_per_axis() {
548 let axes = Axes::new().with_xlim((-10.0, 10.0));
549 let mut canvas = Canvas::new(11, 11, axes);
550 canvas.scatter(&[(2.0, 3.0), (4.0, 7.0)], 1);
551
552 assert_eq!(canvas.axes.xlim, (-10.0, 10.0));
553 assert_eq!(canvas.axes.ylim, (3.0, 7.0));
554 }
555}