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
11pub 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 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 pub fn with_canvas_object(canvas: HtmlCanvasElement) -> Option<Self> {
62 Self::init_backend(canvas)
63 }
64
65 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
303fn 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 assert_eq!(
558 ((10.0, 10.0), (20.0, 20.0)),
559 fine_hor_ver_lines((10, 10), (20, 20))
560 );
561
562 assert_eq!(
564 ((9.5, 10.0), (19.5, 10.0)),
565 fine_hor_ver_lines((10, 10), (20, 10))
566 );
567
568 assert_eq!(
570 ((10.0, 9.5), (10.0, 19.5)),
571 fine_hor_ver_lines((10, 10), (10, 20))
572 );
573 }
574}