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
66fn make_canvas_color(color: BackendColor) -> JsValue {
67 let (r, g, b) = color.rgb;
68 let a = color.alpha;
69 format!("rgba({},{},{},{})", r, g, b, a).into()
70}
71
72impl DrawingBackend for CanvasBackend {
73 type ErrorType = CanvasError;
74
75 fn get_size(&self) -> (u32, u32) {
76 (self.canvas.width(), self.canvas.height())
77 }
78
79 fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
80 Ok(())
81 }
82
83 fn present(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
84 Ok(())
85 }
86
87 fn draw_pixel(
88 &mut self,
89 point: BackendCoord,
90 style: BackendColor,
91 ) -> Result<(), DrawingErrorKind<CanvasError>> {
92 if style.color().alpha == 0.0 {
93 return Ok(());
94 }
95
96 self.context
97 .set_fill_style(&make_canvas_color(style.color()));
98 self.context
99 .fill_rect(f64::from(point.0), f64::from(point.1), 1.0, 1.0);
100 Ok(())
101 }
102
103 fn draw_line<S: BackendStyle>(
104 &mut self,
105 from: BackendCoord,
106 to: BackendCoord,
107 style: &S,
108 ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
109 if style.color().alpha == 0.0 {
110 return Ok(());
111 }
112
113 self.context
114 .set_stroke_style(&make_canvas_color(style.color()));
115 self.context.set_line_width(style.stroke_width() as f64);
116 self.context.begin_path();
117 self.context.move_to(f64::from(from.0), f64::from(from.1));
118 self.context.line_to(f64::from(to.0), f64::from(to.1));
119 self.context.stroke();
120 Ok(())
121 }
122
123 fn draw_rect<S: BackendStyle>(
124 &mut self,
125 upper_left: BackendCoord,
126 bottom_right: BackendCoord,
127 style: &S,
128 fill: bool,
129 ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
130 if style.color().alpha == 0.0 {
131 return Ok(());
132 }
133 if fill {
134 self.context
135 .set_fill_style(&make_canvas_color(style.color()));
136 self.context.fill_rect(
137 f64::from(upper_left.0),
138 f64::from(upper_left.1),
139 f64::from(bottom_right.0 - upper_left.0),
140 f64::from(bottom_right.1 - upper_left.1),
141 );
142 } else {
143 self.context
144 .set_stroke_style(&make_canvas_color(style.color()));
145 self.context.stroke_rect(
146 f64::from(upper_left.0),
147 f64::from(upper_left.1),
148 f64::from(bottom_right.0 - upper_left.0),
149 f64::from(bottom_right.1 - upper_left.1),
150 );
151 }
152 Ok(())
153 }
154
155 fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
156 &mut self,
157 path: I,
158 style: &S,
159 ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
160 if style.color().alpha == 0.0 {
161 return Ok(());
162 }
163 let mut path = path.into_iter();
164 self.context.begin_path();
165 if let Some(start) = path.next() {
166 self.context
167 .set_stroke_style(&make_canvas_color(style.color()));
168 self.context.move_to(f64::from(start.0), f64::from(start.1));
169 for next in path {
170 self.context.line_to(f64::from(next.0), f64::from(next.1));
171 }
172 }
173 self.context.stroke();
174 Ok(())
175 }
176
177 fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
178 &mut self,
179 path: I,
180 style: &S,
181 ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
182 if style.color().alpha == 0.0 {
183 return Ok(());
184 }
185 let mut path = path.into_iter();
186 self.context.begin_path();
187 if let Some(start) = path.next() {
188 self.context
189 .set_fill_style(&make_canvas_color(style.color()));
190 self.context.move_to(f64::from(start.0), f64::from(start.1));
191 for next in path {
192 self.context.line_to(f64::from(next.0), f64::from(next.1));
193 }
194 self.context.close_path();
195 }
196 self.context.fill();
197 Ok(())
198 }
199
200 fn draw_circle<S: BackendStyle>(
201 &mut self,
202 center: BackendCoord,
203 radius: u32,
204 style: &S,
205 fill: bool,
206 ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
207 if style.color().alpha == 0.0 {
208 return Ok(());
209 }
210 if fill {
211 self.context
212 .set_fill_style(&make_canvas_color(style.color()));
213 } else {
214 self.context
215 .set_stroke_style(&make_canvas_color(style.color()));
216 }
217 self.context.begin_path();
218 self.context
219 .arc(
220 f64::from(center.0),
221 f64::from(center.1),
222 f64::from(radius),
223 0.0,
224 std::f64::consts::PI * 2.0,
225 )
226 .map_err(error_cast)?;
227 if fill {
228 self.context.fill();
229 } else {
230 self.context.stroke();
231 }
232 Ok(())
233 }
234
235 fn draw_text<S: BackendTextStyle>(
236 &mut self,
237 text: &str,
238 style: &S,
239 pos: BackendCoord,
240 ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
241 let color = style.color();
242 if color.alpha == 0.0 {
243 return Ok(());
244 }
245
246 let (mut x, mut y) = (pos.0, pos.1);
247
248 let degree = match style.transform() {
249 FontTransform::None => 0.0,
250 FontTransform::Rotate90 => 90.0,
251 FontTransform::Rotate180 => 180.0,
252 FontTransform::Rotate270 => 270.0,
253 } / 180.0
254 * std::f64::consts::PI;
255
256 if degree != 0.0 {
257 self.context.save();
258 self.context
259 .translate(f64::from(x), f64::from(y))
260 .map_err(error_cast)?;
261 self.context.rotate(degree).map_err(error_cast)?;
262 x = 0;
263 y = 0;
264 }
265
266 let text_baseline = match style.anchor().v_pos {
267 VPos::Top => "top",
268 VPos::Center => "middle",
269 VPos::Bottom => "bottom",
270 };
271 self.context.set_text_baseline(text_baseline);
272
273 let text_align = match style.anchor().h_pos {
274 HPos::Left => "start",
275 HPos::Right => "end",
276 HPos::Center => "center",
277 };
278 self.context.set_text_align(text_align);
279
280 self.context
281 .set_fill_style(&make_canvas_color(color.clone()));
282 self.context.set_font(&format!(
283 "{} {}px {}",
284 style.style().as_str(),
285 style.size(),
286 style.family().as_str(),
287 ));
288 self.context
289 .fill_text(text, f64::from(x), f64::from(y))
290 .map_err(error_cast)?;
291
292 if degree != 0.0 {
293 self.context.restore();
294 }
295
296 Ok(())
297 }
298}
299
300#[cfg(test)]
301mod test {
302 use super::*;
303 use plotters::element::Circle;
304 use plotters::prelude::*;
305 use plotters_backend::text_anchor::Pos;
306 use wasm_bindgen_test::wasm_bindgen_test_configure;
307 use wasm_bindgen_test::*;
308 use web_sys::Document;
309
310 wasm_bindgen_test_configure!(run_in_browser);
311
312 fn create_canvas(document: &Document, id: &str, width: u32, height: u32) -> HtmlCanvasElement {
313 let canvas = document
314 .create_element("canvas")
315 .unwrap()
316 .dyn_into::<HtmlCanvasElement>()
317 .unwrap();
318 let div = document.create_element("div").unwrap();
319 div.append_child(&canvas).unwrap();
320 document.body().unwrap().append_child(&div).unwrap();
321 canvas.set_attribute("id", id).unwrap();
322 canvas.set_width(width);
323 canvas.set_height(height);
324 canvas
325 }
326
327 fn check_content(document: &Document, id: &str) {
328 let canvas = document
329 .get_element_by_id(id)
330 .unwrap()
331 .dyn_into::<HtmlCanvasElement>()
332 .unwrap();
333 let data_uri = canvas.to_data_url().unwrap();
334 let prefix = "data:image/png;base64,";
335 assert!(&data_uri.starts_with(prefix));
336 }
337
338 fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
339 let document = window().unwrap().document().unwrap();
340 let canvas = create_canvas(&document, test_name, 500, 500);
341 let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
342 let root = backend.into_drawing_area();
343
344 let mut chart = ChartBuilder::on(&root)
345 .caption("This is a test", ("sans-serif", 20))
346 .set_all_label_area_size(40)
347 .build_ranged(0..10, 0..10)
348 .unwrap();
349
350 chart
351 .configure_mesh()
352 .set_all_tick_mark_size(tick_size)
353 .draw()
354 .unwrap();
355
356 check_content(&document, test_name);
357 }
358
359 #[wasm_bindgen_test]
360 fn test_draw_mesh_no_ticks() {
361 draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
362 }
363
364 #[wasm_bindgen_test]
365 fn test_draw_mesh_negative_ticks() {
366 draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
367 }
368
369 #[wasm_bindgen_test]
370 fn test_text_draw() {
371 let document = window().unwrap().document().unwrap();
372 let canvas = create_canvas(&document, "test_text_draw", 1500, 800);
373 let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
374 let root = backend.into_drawing_area();
375 let root = root
376 .titled("Image Title", ("sans-serif", 60).into_font())
377 .unwrap();
378
379 let mut chart = ChartBuilder::on(&root)
380 .caption("All anchor point positions", ("sans-serif", 20))
381 .set_all_label_area_size(40)
382 .build_ranged(0..100, 0..50)
383 .unwrap();
384
385 chart
386 .configure_mesh()
387 .disable_x_mesh()
388 .disable_y_mesh()
389 .x_desc("X Axis")
390 .y_desc("Y Axis")
391 .draw()
392 .unwrap();
393
394 let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30));
395
396 for (dy, trans) in [
397 FontTransform::None,
398 FontTransform::Rotate90,
399 FontTransform::Rotate180,
400 FontTransform::Rotate270,
401 ]
402 .iter()
403 .enumerate()
404 {
405 for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() {
406 for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() {
407 let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150;
408 let y = 120 + dy as i32 * 150;
409 let draw = |x, y, text| {
410 root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap();
411 let style = TextStyle::from(("sans-serif", 20).into_font())
412 .pos(Pos::new(*h_pos, *v_pos))
413 .transform(trans.clone());
414 root.draw_text(text, &style, (x, y)).unwrap();
415 };
416 draw(x + x1, y + y1, "dood");
417 draw(x + x2, y + y2, "dog");
418 draw(x + x3, y + y3, "goog");
419 }
420 }
421 }
422 check_content(&document, "test_text_draw");
423 }
424
425 #[wasm_bindgen_test]
426 fn test_text_clipping() {
427 let (width, height) = (500_i32, 500_i32);
428 let document = window().unwrap().document().unwrap();
429 let canvas = create_canvas(&document, "test_text_clipping", width as u32, height as u32);
430 let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
431 let root = backend.into_drawing_area();
432
433 let style = TextStyle::from(("sans-serif", 20).into_font())
434 .pos(Pos::new(HPos::Center, VPos::Center));
435 root.draw_text("TOP LEFT", &style, (0, 0)).unwrap();
436 root.draw_text("TOP CENTER", &style, (width / 2, 0))
437 .unwrap();
438 root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap();
439
440 root.draw_text("MIDDLE LEFT", &style, (0, height / 2))
441 .unwrap();
442 root.draw_text("MIDDLE RIGHT", &style, (width, height / 2))
443 .unwrap();
444
445 root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap();
446 root.draw_text("BOTTOM CENTER", &style, (width / 2, height))
447 .unwrap();
448 root.draw_text("BOTTOM RIGHT", &style, (width, height))
449 .unwrap();
450
451 check_content(&document, "test_text_clipping");
452 }
453
454 #[wasm_bindgen_test]
455 fn test_series_labels() {
456 let (width, height) = (500, 500);
457 let document = window().unwrap().document().unwrap();
458 let canvas = create_canvas(&document, "test_series_labels", width, height);
459 let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
460 let root = backend.into_drawing_area();
461
462 let mut chart = ChartBuilder::on(&root)
463 .caption("All series label positions", ("sans-serif", 20))
464 .set_all_label_area_size(40)
465 .build_ranged(0..50, 0..50)
466 .unwrap();
467
468 chart
469 .configure_mesh()
470 .disable_x_mesh()
471 .disable_y_mesh()
472 .draw()
473 .unwrap();
474
475 chart
476 .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
477 .expect("Drawing error")
478 .label("Series 1")
479 .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
480
481 chart
482 .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
483 .expect("Drawing error")
484 .label("Series 2")
485 .legend(|(x, y)| Circle::new((x, y), 3, BLUE.filled()));
486
487 for pos in vec![
488 SeriesLabelPosition::UpperLeft,
489 SeriesLabelPosition::MiddleLeft,
490 SeriesLabelPosition::LowerLeft,
491 SeriesLabelPosition::UpperMiddle,
492 SeriesLabelPosition::MiddleMiddle,
493 SeriesLabelPosition::LowerMiddle,
494 SeriesLabelPosition::UpperRight,
495 SeriesLabelPosition::MiddleRight,
496 SeriesLabelPosition::LowerRight,
497 SeriesLabelPosition::Coordinate(70, 70),
498 ]
499 .into_iter()
500 {
501 chart
502 .configure_series_labels()
503 .border_style(&BLACK.mix(0.5))
504 .position(pos)
505 .draw()
506 .expect("Drawing error");
507 }
508
509 check_content(&document, "test_series_labels");
510 }
511
512 #[wasm_bindgen_test]
513 fn test_draw_pixel_alphas() {
514 let (width, height) = (100_i32, 100_i32);
515 let document = window().unwrap().document().unwrap();
516 let canvas = create_canvas(
517 &document,
518 "test_draw_pixel_alphas",
519 width as u32,
520 height as u32,
521 );
522 let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
523 let root = backend.into_drawing_area();
524
525 for i in -20..20 {
526 let alpha = i as f64 * 0.1;
527 root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
528 .unwrap();
529 }
530
531 check_content(&document, "test_draw_pixel_alphas");
532 }
533}