├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ └── wasm.yml ├── .gitignore ├── src ├── lib.rs └── canvas.rs ├── Cargo.toml ├── README.md └── LICENSE /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .*.sw* 5 | backup/* 6 | **/Cargo.lock 7 | **/target 8 | examples/wasm-demo/www/pkg 9 | examples/.ipynb_checkpoints/ 10 | tarpaulin-report.html 11 | .vscode/* -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The Plotters HTML5 Canvas backend. 3 | 4 | This backend allows Plotters operates the HTML5 Canvas when targeted to Webassembly. 5 | 6 | See the documentation for [CanvasBackend](struct.CanvasBackend.html) for more details. 7 | */ 8 | mod canvas; 9 | 10 | pub use canvas::CanvasBackend; 11 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yml: -------------------------------------------------------------------------------- 1 | name: WASM Target 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Install wasm-pack 11 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 12 | - name: Run WASM test cases 13 | run: wasm-pack test --headless --chrome -- --lib 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plotters-canvas" 3 | version = "0.3.1" 4 | authors = ["Hao Hou "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Plotters HTML5 Canvas Backend" 8 | homepage = "https://plotters-rs.github.io" 9 | repository = "https://github.com/plotters-rs/plotters-canvas" 10 | readme = "README.md" 11 | resolver = "2" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies.plotters-backend] 16 | version = "0.3" 17 | 18 | [dependencies] 19 | js-sys= "0.3.32" 20 | wasm-bindgen = "0.2.55" 21 | 22 | [dependencies.web-sys] 23 | version = "0.3.32" 24 | features = ['Document', 'DomRect', 'Element', 'HtmlElement', 'Node', 'Window', 'HtmlCanvasElement', 'CanvasRenderingContext2d'] 25 | 26 | [dev-dependencies] 27 | wasm-bindgen-test = "^0.3.17" 28 | 29 | [dev-dependencies.plotters] 30 | default-features = false 31 | version = "0.3" 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plotters-canvas - The HTML5 canvas backend for Plotters 2 | 3 | This is a part of plotters project. For more details, please check the following links: 4 | 5 | - For high-level intro of Plotters, see: [Plotters on crates.io](https://crates.io/crates/plotters) 6 | - Check the main repo at [Plotters repo](https://github.com/38/plotters.git) 7 | - For detailed documentation about this crate, check [plotters-backend on docs.rs](https://docs.rs/plotters-backend/) 8 | - You can also visit Plotters [Homepage](https://plotters-rs.github.io) 9 | 10 | ## Testing 11 | 12 | This crate needs to be tested in a browser environment. In order to test, you should install `wasm-pack` first. 13 | 14 | ```bash 15 | cargo install wasm-pack 16 | ``` 17 | 18 | To run the test case with wasm-pack. You need choose what browser you want to run the test cases. You should also be able 19 | to use `--chrome` or `--safari` as well. 20 | 21 | ```bash 22 | wasm-pack test --firefox 23 | ``` 24 | 25 | Also you should be able to run it headlessly by adding the headless param to the testing command. 26 | 27 | ```bash 28 | wasm-pack test --firefox --headless 29 | ``` 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hao Hou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/canvas.rs: -------------------------------------------------------------------------------- 1 | use js_sys::JSON; 2 | use wasm_bindgen::{JsCast, JsValue}; 3 | use web_sys::{window, CanvasRenderingContext2d, HtmlCanvasElement}; 4 | 5 | use plotters_backend::text_anchor::{HPos, VPos}; 6 | use 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 13 | pub struct CanvasBackend { 14 | canvas: HtmlCanvasElement, 15 | context: CanvasRenderingContext2d, 16 | } 17 | 18 | pub struct CanvasError(String); 19 | 20 | impl 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 | 26 | impl 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 | 32 | fn error_cast(e: JsValue) -> DrawingErrorKind { 33 | DrawingErrorKind::DrawingError(CanvasError( 34 | JSON::stringify(&e) 35 | .map(|s| Into::::into(&s)) 36 | .unwrap_or_else(|_| "Unknown".to_string()), 37 | )) 38 | } 39 | 40 | impl std::error::Error for CanvasError {} 41 | 42 | impl CanvasBackend { 43 | fn init_backend(canvas: HtmlCanvasElement) -> Option { 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 { 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 { 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_str(&make_canvas_color(style.color())); 69 | self.context.set_line_width(style.stroke_width() as f64); 70 | } 71 | } 72 | 73 | fn make_canvas_color(color: BackendColor) -> String { 74 | let (r, g, b) = color.rgb; 75 | let a = color.alpha; 76 | format!("rgba({},{},{},{})", r, g, b, a) 77 | } 78 | 79 | impl 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> { 87 | Ok(()) 88 | } 89 | 90 | fn present(&mut self) -> Result<(), DrawingErrorKind> { 91 | Ok(()) 92 | } 93 | 94 | fn draw_pixel( 95 | &mut self, 96 | point: BackendCoord, 97 | style: BackendColor, 98 | ) -> Result<(), DrawingErrorKind> { 99 | if style.color().alpha == 0.0 { 100 | return Ok(()); 101 | } 102 | 103 | self.context 104 | .set_fill_style_str(&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( 111 | &mut self, 112 | from: BackendCoord, 113 | to: BackendCoord, 114 | style: &S, 115 | ) -> Result<(), DrawingErrorKind> { 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( 130 | &mut self, 131 | upper_left: BackendCoord, 132 | bottom_right: BackendCoord, 133 | style: &S, 134 | fill: bool, 135 | ) -> Result<(), DrawingErrorKind> { 136 | if style.color().alpha == 0.0 { 137 | return Ok(()); 138 | } 139 | if fill { 140 | self.context 141 | .set_fill_style_str(&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>( 161 | &mut self, 162 | path: I, 163 | style: &S, 164 | ) -> Result<(), DrawingErrorKind> { 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>( 182 | &mut self, 183 | path: I, 184 | style: &S, 185 | ) -> Result<(), DrawingErrorKind> { 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_str(&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( 205 | &mut self, 206 | center: BackendCoord, 207 | radius: u32, 208 | style: &S, 209 | fill: bool, 210 | ) -> Result<(), DrawingErrorKind> { 211 | if style.color().alpha == 0.0 { 212 | return Ok(()); 213 | } 214 | if fill { 215 | self.context 216 | .set_fill_style_str(&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( 239 | &mut self, 240 | text: &str, 241 | style: &S, 242 | pos: BackendCoord, 243 | ) -> Result<(), DrawingErrorKind> { 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_str(&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 306 | fn 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)] 321 | mod 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::() 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::() 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 | } 575 | --------------------------------------------------------------------------------