plotters_canvas/
canvas.rs

1use js_sys::JSON;
2use wasm_bindgen::{JsCast, JsValue};
3use web_sys::{window, CanvasRenderingContext2d, HtmlCanvasElement};
4
5use plotters_backend::text_anchor::{HPos, VPos};
6use plotters_backend::{
7    BackendColor, BackendCoord, BackendStyle, BackendTextStyle, DrawingBackend, DrawingErrorKind,
8    FontTransform,
9};
10
11/// The backend that is drawing on the HTML canvas
12/// TODO: Support double buffering
13pub struct CanvasBackend {
14    canvas: HtmlCanvasElement,
15    context: CanvasRenderingContext2d,
16}
17
18pub struct CanvasError(String);
19
20impl std::fmt::Display for CanvasError {
21    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
22        return write!(fmt, "Canvas Error: {}", self.0);
23    }
24}
25
26impl std::fmt::Debug for CanvasError {
27    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
28        return write!(fmt, "CanvasError({})", self.0);
29    }
30}
31
32fn error_cast(e: JsValue) -> DrawingErrorKind<CanvasError> {
33    DrawingErrorKind::DrawingError(CanvasError(
34        JSON::stringify(&e)
35            .map(|s| Into::<String>::into(&s))
36            .unwrap_or_else(|_| "Unknown".to_string()),
37    ))
38}
39
40impl std::error::Error for CanvasError {}
41
42impl CanvasBackend {
43    fn init_backend(canvas: HtmlCanvasElement) -> Option<Self> {
44        let context: CanvasRenderingContext2d = canvas.get_context("2d").ok()??.dyn_into().ok()?;
45        Some(CanvasBackend { canvas, context })
46    }
47
48    /// Create a new drawing backend backed with an HTML5 canvas object with given Id
49    /// - `elem_id` The element id for the canvas
50    /// - Return either some drawing backend has been created, or none in error case
51    pub fn new(elem_id: &str) -> Option<Self> {
52        let document = window()?.document()?;
53        let canvas = document.get_element_by_id(elem_id)?;
54        let canvas: HtmlCanvasElement = canvas.dyn_into().ok()?;
55        Self::init_backend(canvas)
56    }
57
58    /// Create a new drawing backend backend with a HTML5 canvas object passed in
59    /// - `canvas` The object we want to use as backend
60    /// - Return either the drawing backend or None for error
61    pub fn with_canvas_object(canvas: HtmlCanvasElement) -> Option<Self> {
62        Self::init_backend(canvas)
63    }
64
65    /// Sets the stroke style and line width in the underlying context.
66    fn set_line_style(&mut self, style: &impl BackendStyle) {
67        self.context
68            .set_stroke_style(&make_canvas_color(style.color()));
69        self.context.set_line_width(style.stroke_width() as f64);
70    }
71}
72
73fn make_canvas_color(color: BackendColor) -> JsValue {
74    let (r, g, b) = color.rgb;
75    let a = color.alpha;
76    format!("rgba({},{},{},{})", r, g, b, a).into()
77}
78
79impl DrawingBackend for CanvasBackend {
80    type ErrorType = CanvasError;
81
82    fn get_size(&self) -> (u32, u32) {
83        (self.canvas.width(), self.canvas.height())
84    }
85
86    fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
87        Ok(())
88    }
89
90    fn present(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
91        Ok(())
92    }
93
94    fn draw_pixel(
95        &mut self,
96        point: BackendCoord,
97        style: BackendColor,
98    ) -> Result<(), DrawingErrorKind<CanvasError>> {
99        if style.color().alpha == 0.0 {
100            return Ok(());
101        }
102
103        self.context
104            .set_fill_style(&make_canvas_color(style.color()));
105        self.context
106            .fill_rect(f64::from(point.0), f64::from(point.1), 1.0, 1.0);
107        Ok(())
108    }
109
110    fn draw_line<S: BackendStyle>(
111        &mut self,
112        from: BackendCoord,
113        to: BackendCoord,
114        style: &S,
115    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
116        if style.color().alpha == 0.0 {
117            return Ok(());
118        }
119
120        let (from, to) = fine_hor_ver_lines(from, to);
121        self.set_line_style(style);
122        self.context.begin_path();
123        self.context.move_to(from.0, from.1);
124        self.context.line_to(to.0, to.1);
125        self.context.stroke();
126        Ok(())
127    }
128
129    fn draw_rect<S: BackendStyle>(
130        &mut self,
131        upper_left: BackendCoord,
132        bottom_right: BackendCoord,
133        style: &S,
134        fill: bool,
135    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
136        if style.color().alpha == 0.0 {
137            return Ok(());
138        }
139        if fill {
140            self.context
141                .set_fill_style(&make_canvas_color(style.color()));
142            self.context.fill_rect(
143                f64::from(upper_left.0),
144                f64::from(upper_left.1),
145                f64::from(bottom_right.0 - upper_left.0),
146                f64::from(bottom_right.1 - upper_left.1),
147            );
148        } else {
149            self.set_line_style(style);
150            self.context.stroke_rect(
151                f64::from(upper_left.0),
152                f64::from(upper_left.1),
153                f64::from(bottom_right.0 - upper_left.0),
154                f64::from(bottom_right.1 - upper_left.1),
155            );
156        }
157        Ok(())
158    }
159
160    fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
161        &mut self,
162        path: I,
163        style: &S,
164    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
165        if style.color().alpha == 0.0 {
166            return Ok(());
167        }
168        let mut path = path.into_iter();
169        self.context.begin_path();
170        if let Some(start) = path.next() {
171            self.set_line_style(style);
172            self.context.move_to(f64::from(start.0), f64::from(start.1));
173            for next in path {
174                self.context.line_to(f64::from(next.0), f64::from(next.1));
175            }
176        }
177        self.context.stroke();
178        Ok(())
179    }
180
181    fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
182        &mut self,
183        path: I,
184        style: &S,
185    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
186        if style.color().alpha == 0.0 {
187            return Ok(());
188        }
189        let mut path = path.into_iter();
190        self.context.begin_path();
191        if let Some(start) = path.next() {
192            self.context
193                .set_fill_style(&make_canvas_color(style.color()));
194            self.context.move_to(f64::from(start.0), f64::from(start.1));
195            for next in path {
196                self.context.line_to(f64::from(next.0), f64::from(next.1));
197            }
198            self.context.close_path();
199        }
200        self.context.fill();
201        Ok(())
202    }
203
204    fn draw_circle<S: BackendStyle>(
205        &mut self,
206        center: BackendCoord,
207        radius: u32,
208        style: &S,
209        fill: bool,
210    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
211        if style.color().alpha == 0.0 {
212            return Ok(());
213        }
214        if fill {
215            self.context
216                .set_fill_style(&make_canvas_color(style.color()));
217        } else {
218            self.set_line_style(style);
219        }
220        self.context.begin_path();
221        self.context
222            .arc(
223                f64::from(center.0),
224                f64::from(center.1),
225                f64::from(radius),
226                0.0,
227                std::f64::consts::PI * 2.0,
228            )
229            .map_err(error_cast)?;
230        if fill {
231            self.context.fill();
232        } else {
233            self.context.stroke();
234        }
235        Ok(())
236    }
237
238    fn draw_text<S: BackendTextStyle>(
239        &mut self,
240        text: &str,
241        style: &S,
242        pos: BackendCoord,
243    ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
244        let color = style.color();
245        if color.alpha == 0.0 {
246            return Ok(());
247        }
248
249        let (mut x, mut y) = (pos.0, pos.1);
250
251        let degree = match style.transform() {
252            FontTransform::None => 0.0,
253            FontTransform::Rotate90 => 90.0,
254            FontTransform::Rotate180 => 180.0,
255            FontTransform::Rotate270 => 270.0,
256        } / 180.0
257            * std::f64::consts::PI;
258
259        if degree != 0.0 {
260            self.context.save();
261            self.context
262                .translate(f64::from(x), f64::from(y))
263                .map_err(error_cast)?;
264            self.context.rotate(degree).map_err(error_cast)?;
265            x = 0;
266            y = 0;
267        }
268
269        let text_baseline = match style.anchor().v_pos {
270            VPos::Top => "top",
271            VPos::Center => "middle",
272            VPos::Bottom => "bottom",
273        };
274        self.context.set_text_baseline(text_baseline);
275
276        let text_align = match style.anchor().h_pos {
277            HPos::Left => "start",
278            HPos::Right => "end",
279            HPos::Center => "center",
280        };
281        self.context.set_text_align(text_align);
282
283        self.context
284            .set_fill_style(&make_canvas_color(color.clone()));
285        self.context.set_font(&format!(
286            "{} {}px {}",
287            style.style().as_str(),
288            style.size(),
289            style.family().as_str(),
290        ));
291        self.context
292            .fill_text(text, f64::from(x), f64::from(y))
293            .map_err(error_cast)?;
294
295        if degree != 0.0 {
296            self.context.restore();
297        }
298
299        Ok(())
300    }
301}
302
303/// Move line coord to left/right half pixel if the line is vertical/horizontal
304/// to prevent line blurry in canvas, see https://stackoverflow.com/a/13879322/10651567
305/// and https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#a_linewidth_example
306fn fine_hor_ver_lines(from: BackendCoord, end: BackendCoord) -> ((f64, f64), (f64, f64)) {
307    let mut from = (from.0 as f64, from.1 as f64);
308    let mut end = (end.0 as f64, end.1 as f64);
309    if from.0 == end.0 {
310        from.0 -= 0.5;
311        end.0 -= 0.5;
312    }
313    if from.1 == end.1 {
314        from.1 -= 0.5;
315        end.1 -= 0.5;
316    }
317    (from, end)
318}
319
320#[cfg(test)]
321mod test {
322    use super::*;
323    use plotters::element::Circle;
324    use plotters::prelude::*;
325    use plotters_backend::text_anchor::Pos;
326    use wasm_bindgen_test::wasm_bindgen_test_configure;
327    use wasm_bindgen_test::*;
328    use web_sys::Document;
329
330    wasm_bindgen_test_configure!(run_in_browser);
331
332    fn create_canvas(document: &Document, id: &str, width: u32, height: u32) -> HtmlCanvasElement {
333        let canvas = document
334            .create_element("canvas")
335            .unwrap()
336            .dyn_into::<HtmlCanvasElement>()
337            .unwrap();
338        let div = document.create_element("div").unwrap();
339        div.append_child(&canvas).unwrap();
340        document.body().unwrap().append_child(&div).unwrap();
341        canvas.set_attribute("id", id).unwrap();
342        canvas.set_width(width);
343        canvas.set_height(height);
344        canvas
345    }
346
347    fn check_content(document: &Document, id: &str) {
348        let canvas = document
349            .get_element_by_id(id)
350            .unwrap()
351            .dyn_into::<HtmlCanvasElement>()
352            .unwrap();
353        let data_uri = canvas.to_data_url().unwrap();
354        let prefix = "data:image/png;base64,";
355        assert!(&data_uri.starts_with(prefix));
356    }
357
358    fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
359        let document = window().unwrap().document().unwrap();
360        let canvas = create_canvas(&document, test_name, 500, 500);
361        let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
362        let root = backend.into_drawing_area();
363
364        let mut chart = ChartBuilder::on(&root)
365            .caption("This is a test", ("sans-serif", 20))
366            .set_all_label_area_size(40)
367            .build_cartesian_2d(0..10, 0..10)
368            .unwrap();
369
370        chart
371            .configure_mesh()
372            .set_all_tick_mark_size(tick_size)
373            .draw()
374            .unwrap();
375
376        check_content(&document, test_name);
377    }
378
379    #[wasm_bindgen_test]
380    fn test_draw_mesh_no_ticks() {
381        draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
382    }
383
384    #[wasm_bindgen_test]
385    fn test_draw_mesh_negative_ticks() {
386        draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
387    }
388
389    #[wasm_bindgen_test]
390    fn test_text_draw() {
391        let document = window().unwrap().document().unwrap();
392        let canvas = create_canvas(&document, "test_text_draw", 1500, 800);
393        let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
394        let root = backend.into_drawing_area();
395        let root = root
396            .titled("Image Title", ("sans-serif", 60).into_font())
397            .unwrap();
398
399        let mut chart = ChartBuilder::on(&root)
400            .caption("All anchor point positions", ("sans-serif", 20))
401            .set_all_label_area_size(40)
402            .build_cartesian_2d(0..100, 0..50)
403            .unwrap();
404
405        chart
406            .configure_mesh()
407            .disable_x_mesh()
408            .disable_y_mesh()
409            .x_desc("X Axis")
410            .y_desc("Y Axis")
411            .draw()
412            .unwrap();
413
414        let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30));
415
416        for (dy, trans) in [
417            FontTransform::None,
418            FontTransform::Rotate90,
419            FontTransform::Rotate180,
420            FontTransform::Rotate270,
421        ]
422        .iter()
423        .enumerate()
424        {
425            for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() {
426                for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() {
427                    let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150;
428                    let y = 120 + dy as i32 * 150;
429                    let draw = |x, y, text| {
430                        root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap();
431                        let style = TextStyle::from(("sans-serif", 20).into_font())
432                            .pos(Pos::new(*h_pos, *v_pos))
433                            .transform(trans.clone());
434                        root.draw_text(text, &style, (x, y)).unwrap();
435                    };
436                    draw(x + x1, y + y1, "dood");
437                    draw(x + x2, y + y2, "dog");
438                    draw(x + x3, y + y3, "goog");
439                }
440            }
441        }
442        check_content(&document, "test_text_draw");
443    }
444
445    #[wasm_bindgen_test]
446    fn test_text_clipping() {
447        let (width, height) = (500_i32, 500_i32);
448        let document = window().unwrap().document().unwrap();
449        let canvas = create_canvas(&document, "test_text_clipping", width as u32, height as u32);
450        let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
451        let root = backend.into_drawing_area();
452
453        let style = TextStyle::from(("sans-serif", 20).into_font())
454            .pos(Pos::new(HPos::Center, VPos::Center));
455        root.draw_text("TOP LEFT", &style, (0, 0)).unwrap();
456        root.draw_text("TOP CENTER", &style, (width / 2, 0))
457            .unwrap();
458        root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap();
459
460        root.draw_text("MIDDLE LEFT", &style, (0, height / 2))
461            .unwrap();
462        root.draw_text("MIDDLE RIGHT", &style, (width, height / 2))
463            .unwrap();
464
465        root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap();
466        root.draw_text("BOTTOM CENTER", &style, (width / 2, height))
467            .unwrap();
468        root.draw_text("BOTTOM RIGHT", &style, (width, height))
469            .unwrap();
470
471        check_content(&document, "test_text_clipping");
472    }
473
474    #[wasm_bindgen_test]
475    fn test_series_labels() {
476        let (width, height) = (500, 500);
477        let document = window().unwrap().document().unwrap();
478        let canvas = create_canvas(&document, "test_series_labels", width, height);
479        let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
480        let root = backend.into_drawing_area();
481
482        let mut chart = ChartBuilder::on(&root)
483            .caption("All series label positions", ("sans-serif", 20))
484            .set_all_label_area_size(40)
485            .build_cartesian_2d(0..50, 0..50)
486            .unwrap();
487
488        chart
489            .configure_mesh()
490            .disable_x_mesh()
491            .disable_y_mesh()
492            .draw()
493            .unwrap();
494
495        chart
496            .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
497            .expect("Drawing error")
498            .label("Series 1")
499            .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
500
501        chart
502            .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
503            .expect("Drawing error")
504            .label("Series 2")
505            .legend(|(x, y)| Circle::new((x, y), 3, BLUE.filled()));
506
507        for pos in vec![
508            SeriesLabelPosition::UpperLeft,
509            SeriesLabelPosition::MiddleLeft,
510            SeriesLabelPosition::LowerLeft,
511            SeriesLabelPosition::UpperMiddle,
512            SeriesLabelPosition::MiddleMiddle,
513            SeriesLabelPosition::LowerMiddle,
514            SeriesLabelPosition::UpperRight,
515            SeriesLabelPosition::MiddleRight,
516            SeriesLabelPosition::LowerRight,
517            SeriesLabelPosition::Coordinate(70, 70),
518        ]
519        .into_iter()
520        {
521            chart
522                .configure_series_labels()
523                .border_style(&BLACK.mix(0.5))
524                .position(pos)
525                .draw()
526                .expect("Drawing error");
527        }
528
529        check_content(&document, "test_series_labels");
530    }
531
532    #[wasm_bindgen_test]
533    fn test_draw_pixel_alphas() {
534        let (width, height) = (100_i32, 100_i32);
535        let document = window().unwrap().document().unwrap();
536        let canvas = create_canvas(
537            &document,
538            "test_draw_pixel_alphas",
539            width as u32,
540            height as u32,
541        );
542        let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
543        let root = backend.into_drawing_area();
544
545        for i in -20..20 {
546            let alpha = i as f64 * 0.1;
547            root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
548                .unwrap();
549        }
550
551        check_content(&document, "test_draw_pixel_alphas");
552    }
553
554    #[test]
555    fn test_fine_hor_ver_lines() {
556        // not horizontal nor vertical
557        assert_eq!(
558            ((10.0, 10.0), (20.0, 20.0)),
559            fine_hor_ver_lines((10, 10), (20, 20))
560        );
561
562        // vertical
563        assert_eq!(
564            ((9.5, 10.0), (19.5, 10.0)),
565            fine_hor_ver_lines((10, 10), (20, 10))
566        );
567
568        // horizontal
569        assert_eq!(
570            ((10.0, 9.5), (10.0, 19.5)),
571            fine_hor_ver_lines((10, 10), (10, 20))
572        );
573    }
574}