├── .cargo └── config ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── general-questions.md └── workflows │ ├── push-check.yml │ └── wasm.yml ├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── benches │ ├── data.rs │ ├── mod.rs │ ├── parallel.rs │ └── rasterizer.rs └── main.rs ├── doc-template ├── examples │ ├── chart.rs │ ├── composable_elements.rs │ ├── drawing_area.rs │ ├── drawing_backends.rs │ ├── elements.rs │ └── quick_start.rs ├── gen-toc.sh ├── latest_version ├── readme.template.md ├── readme │ ├── examples │ ├── gallery │ └── style ├── render_readme.sh ├── rustdoc │ ├── examples │ ├── gallery │ └── style └── update_readme.sh ├── examples ├── animation.rs ├── area-chart.rs ├── blit-bitmap.rs ├── boxplot.rs ├── chart.rs ├── console.rs ├── errorbar.rs ├── gtk-demo │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── histogram.rs ├── mandelbrot.rs ├── matshow.rs ├── minifb-demo │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── normal-dist.rs ├── normal-dist2.rs ├── piston-demo │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── relative_size.rs ├── sierpinski.rs ├── slc-temp.rs ├── snowflake.rs ├── stock.rs ├── two-scales.rs └── wasm-demo │ ├── Cargo.toml │ ├── README.md │ ├── src │ ├── func_plot.rs │ ├── lib.rs │ └── mandelbrot.rs │ ├── start-server.bat │ ├── start-server.sh │ └── www │ ├── .gitignore │ ├── bootstrap.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── style.css │ └── webpack.config.js ├── publish.sh └── src ├── .DS_Store ├── chart ├── builder.rs ├── context.rs ├── dual_coord.rs ├── mesh.rs ├── mod.rs └── series.rs ├── coord ├── category.rs ├── datetime.rs ├── logarithmic.rs ├── mod.rs ├── numeric.rs └── ranged.rs ├── data ├── data_range.rs ├── float.rs ├── mod.rs └── quartiles.rs ├── drawing ├── area.rs ├── backend.rs ├── backend_impl │ ├── bitmap.rs │ ├── cairo.rs │ ├── canvas.rs │ ├── mocked.rs │ ├── mod.rs │ ├── piston.rs │ └── svg.rs ├── mod.rs └── rasterizer │ ├── circle.rs │ ├── line.rs │ ├── mod.rs │ ├── path.rs │ ├── polygon.rs │ └── rect.rs ├── element ├── basic_shapes.rs ├── boxplot.rs ├── candlestick.rs ├── composable.rs ├── dynelem.rs ├── errorbar.rs ├── image.rs ├── mod.rs ├── points.rs └── text.rs ├── evcxr.rs ├── lib.rs ├── series ├── area_series.rs ├── histogram.rs ├── line_series.rs ├── mod.rs └── point_series.rs └── style ├── color.rs ├── colors.rs ├── font ├── font_desc.rs ├── mod.rs ├── naive.rs ├── ttf.rs └── web.rs ├── mod.rs ├── palette.rs ├── palette_ext.rs ├── shape.rs ├── size.rs └── text.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | runner = 'wasm-bindgen-test-runner' 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: About unexpected behaviors 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | Describe what is expected, what you actually get. 12 | It would be nice to have screenshot or result image uploaded 13 | 14 | **To Reproduce** 15 | Some minimal reproduce code is highly recommended 16 | 17 | **Version Information** 18 | Please give us what version you are using. If you are pulling `Plotters` directly from git repo, please mention this as well 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea to Plotter maintainers 4 | title: "[Feature Request]" 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### What is the feature ? 11 | *Detailed feature descrption* 12 | 13 | ### (Optional) Why this feature is useful and how people would use the feature ? 14 | *Explain why this feature is important* 15 | 16 | ### (Optional) Additional Information 17 | *More details are appreciated:)* 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general-questions.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General Questions 3 | about: Any other issues 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/push-check.yml: -------------------------------------------------------------------------------- 1 | name: Push Check 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build_and_test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macos-latest] 11 | steps: 12 | - uses: actions/checkout@v1 13 | with: 14 | submodules: recursive 15 | - uses: actions-rs/toolchain@v1 16 | with: 17 | toolchain: stable 18 | override: true 19 | - uses: actions-rs/cargo@v1 20 | with: 21 | command: test 22 | args: --verbose 23 | - uses: actions-rs/cargo@v1 24 | with: 25 | command: test 26 | args: --verbose --no-default-features --lib 27 | test_all_features: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v1 31 | with: 32 | submodules: recursive 33 | - uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: stable 36 | override: true 37 | - uses: actions-rs/cargo@v1 38 | with: 39 | command: test 40 | args: --verbose --all-features 41 | run_all_examples: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v1 45 | with: 46 | submodules: recursive 47 | - uses: actions-rs/cargo@v1 48 | with: 49 | command: build 50 | args: --verbose --release --examples 51 | - name: Run all the examples 52 | run: for example in examples/*.rs; do ./target/release/examples/$(basename ${example} .rs); done && tar -czvf example-outputs.tar.gz plotters-doc-data 53 | - uses: actions/upload-artifact@v1 54 | with: 55 | name: example-outputs 56 | path: example-outputs.tar.gz 57 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yml: -------------------------------------------------------------------------------- 1 | name: WASM Target 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | with: 13 | submodules: recursive 14 | - name: Install WASM tool chain 15 | run: rustup target add wasm32-unknown-unknown 16 | - name: Check WASM Target Compiles 17 | run: cargo build --verbose --target=wasm32-unknown-unknown 18 | 19 | -------------------------------------------------------------------------------- /.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/* 12 | .DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "plotters-doc-data"] 2 | path = plotters-doc-data 3 | url = https://github.com/38/plotters-doc-data 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | language: rust 4 | 5 | rust: 6 | - stable 7 | - nightly 8 | - beta 9 | 10 | os: 11 | - linux 12 | 13 | script: 14 | - env 15 | - cargo test --all-features && rustup target add wasm32-unknown-unknown && cargo build --target=wasm32-unknown-unknown 16 | 17 | after_success: | 18 | cargo install cargo-tarpaulin 19 | export CODECOV_TOKEN="8622abe0-9579-4cea-93d3-9707969ef6c2" 20 | cargo tarpaulin --all-features --out Xml 21 | bash <(curl -s https://codecov.io/bash) 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for contributing to `Plotters`! 2 | 3 | Here's the useful information about contributing to Plotters. 4 | 5 | # License 6 | 7 | The `Plotters` project is under MIT license. 8 | You may interested in reading [the full text of the license](https://github.com/38/plotters/blob/master/LICENSE). 9 | If you have any questions or concerns please contact us at . 10 | 11 | # Contributing to Plotters codebase 12 | 13 | You are warmly welcomed to contribute code and make Plotters better. Here's a few things that may be helpful to you. 14 | 15 | ## How to make sure my code works 16 | 17 | *Help wanted:* You may realize that `Plotters` doesn't have a high testing coverage, but we are trying hard to improve. It would be nice if you add more test cases for newly added code, or contribute new test cases directly. 18 | 19 | Before you finalize your PR, please check the following thing: 20 | 21 | - Please make sure all test case passes. If any case fails, we need to dig into that. For more details about testing, please read the [Testing notes](#testing-notes). 22 | 23 | - Please run the benchmark to check if the performance changed compare to the master branch. 24 | 25 | - Please run the following command to check if the example output changes. (There shouldn't be any change if you are not modifying the layout) 26 | 27 | ```bash 28 | cargo test --doc 29 | cargo build --release --examples 30 | for i in examples/*.rs 31 | do 32 | ./target/release/examples/$(basename $i .rs) 33 | done 34 | cd plotters-doc-data 35 | git status 36 | ``` 37 | 38 | - Please make sure the WASM target works as well. The easiest way to do that is try to run our WASM demo under [examples/wasm-demo](https://github.com/38/plotters/blob/master/examples/wasm-demo) directory and follow the instruction in the `README.md` file under that directory. 39 | 40 | ## Is my code meets the styling guideline 41 | 42 | Although there's no strictly enforced rules for the style, but please read the following recommendations before you start work. 43 | 44 | - In general, the only guide line is we need to make sure `cargo fmt` doesn't change anything. So it's recommended use `cargo fmt` to fix the code styling issues before you wrap up the work. (Such as submit a PR) 45 | - For naming, acronyms or initials aren't normally used in the code base. Descriptive identifier is highly recommended. 46 | - Documentation is highly recommended. (But there are still a lot of undocumented code unfortunately). 47 | - For API documentation, we normally follows Doxygen's style, which looks like 48 | 49 | ```rust 50 | /// Some description to this API 51 | /// - `param_1`: What param_1 do 52 | /// - `param_2`: What param_2 do 53 | /// - **returns**: The return value description 54 | fn foo(param_1: u32, param_2: u32) -> u32{ 0 } 55 | ``` 56 | 57 | ## Top Level Documentation and Readme 58 | 59 | Please notice we put almost same content for top level `rustdoc` and `README.md`. Thus the both part are generated by script. 60 | If you need to modify the readme and documentation, please change the template at [doc-template/readme.template.md](https://github.com/38/plotters/blob/master/doc-template/readme.template.md) and 61 | use the following command to synchronize the doc to both `src/lib.rs` and `README.md`. 62 | 63 | ```bash 64 | bash doc-template/update-readme.sh 65 | ``` 66 | 67 | ## Testing Notes 68 | 69 | As the project is intended to work in various environments, it's important to test its all features and different feature combinations. The notes below may help you with that task. 70 | 71 | ### Native 72 | 73 | Testing all features: 74 | 75 | ```bash 76 | cargo test --all-features 77 | ``` 78 | 79 | Testing no features at all: 80 | 81 | ```bash 82 | cargo test --no-default-features --lib 83 | ``` 84 | 85 | Since all examples and most doc-test requires `bitmap` features, so we don't test examples and doc test in this case. 86 | 87 | ### WebAssembly 88 | 89 | Wasm target is not tested by default, and you may want to use [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/wasm-bindgen-test/usage.html) CLI tool. 90 | 91 | Installation: 92 | 93 | ```bash 94 | rustup target add wasm32-unknown-unknown 95 | cargo install wasm-bindgen-cli 96 | ``` 97 | 98 | Additionally, the web browser and its driver should be available, please see [Configuring Which Browser is Used](https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/browsers.html#configuring-which-browser-is-used-1). For example, to use Firefox, its binary (`firefox`) and [geckodriver](https://github.com/mozilla/geckodriver/releases) must be on your `$PATH`. 99 | 100 | Usage (only library tests are supported for now): 101 | 102 | ```bash 103 | cargo test --lib --target wasm32-unknown-unknown 104 | ``` 105 | 106 | For the debugging you could set the `NO_HEADLESS=1` environment variable to run the tests using the local server instead of the headless browser. 107 | 108 | ### Code Coverage 109 | 110 | For for the code coverage information you may want to use [cargo-tarpaulin](https://crates.io/crates/cargo-tarpaulin). Please note that it works with x86_64 GNU/Linux only, and the doc tests coverage require nightly Rust. 111 | 112 | Installation ([pycobertura](https://pypi.python.org/pypi/pycobertura) is used to get the detailed report about the coverage): 113 | 114 | ```bash 115 | cargo install cargo-tarpaulin 116 | pip install pycobertura 117 | ``` 118 | 119 | Usage: 120 | 121 | ```bash 122 | cargo tarpaulin --all-features --run-types Tests Doctests -o Xml --output-dir target/test 123 | pycobertura show target/test/cobertura.xml 124 | ``` 125 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plotchart" 3 | version = "0.2.12" 4 | authors = ["hugcoday"] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "A Rust drawing library focus on data plotting for both WASM and native applications" 8 | repository = "https://github.com/hugcoday/plotchart" 9 | keywords = ["WebAssembly", "Visualization", "Plotting", "Drawing"] 10 | categories = ["visualization", "wasm"] 11 | readme = "README.md" 12 | exclude = ["doc-template/*"] 13 | 14 | [dependencies] 15 | num-traits = "0.2.10" 16 | chrono = { version = "0.4.10", optional = true } 17 | gif = { version = "0.10.3", optional = true } 18 | 19 | [dependencies.palette] 20 | version = "0.5.0" 21 | default-features = false 22 | optional = true 23 | features = ["std"] 24 | 25 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 26 | rusttype = { version = "0.8.2", optional = true } 27 | lazy_static = { version = "1.4.0", optional = true } 28 | font-kit = { version = "0.5.0", optional = true } 29 | piston_window = { version = "0.105.0", optional = true } 30 | 31 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.image] 32 | version = "0.22.3" 33 | optional = true 34 | default-features = false 35 | features = ["jpeg", "png_codec", "bmp"] 36 | 37 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies.cairo-rs] 38 | version = "0.8.0" 39 | optional = true 40 | features = ["ps"] 41 | 42 | [target.'cfg(target_arch = "wasm32")'.dependencies] 43 | js-sys= "0.3.32" 44 | wasm-bindgen = "0.2.55" 45 | 46 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] 47 | version = "0.3.32" 48 | features = ['Document', 'DomRect', 'Element', 'HtmlElement', 'Node', 'Window', 'HtmlCanvasElement', 'CanvasRenderingContext2d'] 49 | 50 | [features] 51 | default = ["image_encoder", "svg", "chrono", "palette_ext", "gif_backend", 52 | "deprecated_items", "bitmap", "ttf", "errorbar", "candlestick", 53 | "boxplot", "histogram", "area_series", "line_series", "point_series"] 54 | ttf = ["font-kit", "rusttype", "lazy_static"] 55 | image_encoder = ["image", "bitmap"] 56 | palette_ext = ["palette"] 57 | gif_backend = ["gif", "bitmap"] 58 | datetime = ["chrono"] 59 | svg = [] 60 | evcxr = ["svg"] 61 | piston = ["piston_window", "ttf"] 62 | cairo = ["cairo-rs", "ttf"] 63 | bitmap = ["ttf"] 64 | deprecated_items = [] # Keep some of the deprecated items for backward compatibility 65 | debug = [] # Enable debugging code 66 | errorbar = [] 67 | candlestick = [] 68 | boxplot = [] 69 | histogram = [] 70 | area_series = [] 71 | line_series = [] 72 | point_series = [] 73 | 74 | 75 | [dev-dependencies] 76 | rand = "0.7.2" 77 | itertools = "0.8.2" 78 | rand_distr = "0.2.2" 79 | criterion = "0.3.0" 80 | rayon = "1.2.1" 81 | rand_xorshift = "0.2.0" 82 | 83 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 84 | wasm-bindgen-test = "0.3.5" 85 | 86 | [[bench]] 87 | name = "benchmark" 88 | harness = false 89 | path = "benches/main.rs" 90 | 91 | [profile.bench] 92 | debug = true 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /benches/benches/data.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, Criterion}; 2 | use plotters::data::Quartiles; 3 | 4 | struct Lcg { 5 | state: u32, 6 | } 7 | 8 | impl Lcg { 9 | fn new() -> Lcg { 10 | Lcg { state: 0 } 11 | } 12 | } 13 | 14 | impl Iterator for Lcg { 15 | type Item = u32; 16 | 17 | fn next(&mut self) -> Option { 18 | self.state = self.state.wrapping_mul(1_103_515_245).wrapping_add(12_345); 19 | self.state %= 1 << 31; 20 | Some(self.state) 21 | } 22 | } 23 | 24 | fn quartiles_calc(c: &mut Criterion) { 25 | let src: Vec = Lcg::new().take(100000).collect(); 26 | c.bench_function("data::quartiles_calc", |b| { 27 | b.iter(|| { 28 | Quartiles::new(&src); 29 | }) 30 | }); 31 | } 32 | 33 | criterion_group! { 34 | name = quartiles_group; 35 | config = Criterion::default().sample_size(10); 36 | targets = quartiles_calc 37 | } 38 | -------------------------------------------------------------------------------- /benches/benches/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | pub mod parallel; 3 | pub mod rasterizer; 4 | -------------------------------------------------------------------------------- /benches/benches/parallel.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, Criterion, ParameterizedBenchmark}; 2 | 3 | use plotters::coord::Shift; 4 | use plotters::prelude::*; 5 | use rayon::prelude::*; 6 | 7 | const SIZES: &'static [u32] = &[100, 400, 800, 1000, 2000]; 8 | 9 | fn draw_plot(root: &DrawingArea, pow: f64) { 10 | let mut chart = ChartBuilder::on(root) 11 | .caption(format!("y = x^{}", pow), ("Arial", 30)) 12 | .build_ranged(-1.0..1.0, -1.0..1.0) 13 | .unwrap(); 14 | 15 | chart.configure_mesh().draw().unwrap(); 16 | 17 | chart 18 | .draw_series(LineSeries::new( 19 | (-50..=50) 20 | .map(|x| x as f64 / 50.0) 21 | .map(|x| (x, x.powf(pow))), 22 | &RED, 23 | )) 24 | .unwrap() 25 | .label(format!("y = x^{}", pow)) 26 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED)); 27 | chart 28 | .configure_series_labels() 29 | .background_style(&WHITE.mix(0.8)) 30 | .border_style(&BLACK) 31 | .draw() 32 | .unwrap(); 33 | } 34 | 35 | fn draw_func_1x1_seq(c: &mut Criterion) { 36 | c.bench( 37 | "draw_func_1x1", 38 | ParameterizedBenchmark::new( 39 | "sequential", 40 | |b, &&s| { 41 | let mut buffer = vec![0; (s * s * 3) as usize]; 42 | b.iter(|| { 43 | let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area(); 44 | root.fill(&WHITE).unwrap(); 45 | draw_plot(&root, 2.0); 46 | }) 47 | }, 48 | SIZES.clone(), 49 | ), 50 | ); 51 | } 52 | 53 | fn draw_func_4x4(c: &mut Criterion) { 54 | c.bench( 55 | "draw_func_4x4", 56 | ParameterizedBenchmark::new( 57 | "sequential", 58 | |b, &&s| { 59 | let mut buffer = vec![0; (s * s * 3) as usize]; 60 | b.iter(|| { 61 | let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area(); 62 | let areas = root.split_evenly((4, 4)); 63 | areas.iter().for_each(|area| draw_plot(&area, 2.0)); 64 | }) 65 | }, 66 | SIZES.clone(), 67 | ) 68 | .with_function("blit", |b, &&s| { 69 | let mut buffer = vec![0; (s * s * 3) as usize]; 70 | let mut element_buffer = vec![vec![0; (s * s / 4 * 3) as usize]; 4]; 71 | b.iter(|| { 72 | let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area(); 73 | let areas = root.split_evenly((4, 4)); 74 | let elements: Vec<_> = element_buffer 75 | .par_iter_mut() 76 | .map(|b| { 77 | let mut e = BitMapElement::with_mut((0, 0), (s / 2, s / 2), b).unwrap(); 78 | draw_plot(&e.as_bitmap_backend().into_drawing_area(), 2.0); 79 | e 80 | }) 81 | .collect(); 82 | 83 | areas 84 | .into_iter() 85 | .zip(elements.into_iter()) 86 | .for_each(|(a, e)| a.draw(&e).unwrap()); 87 | }) 88 | }) 89 | .with_function("inplace-blit", |b, &&s| { 90 | let mut buffer = vec![0; (s * s * 3) as usize]; 91 | let mut element_buffer = vec![vec![vec![0; (s * s / 4 * 3) as usize]; 2]; 2]; 92 | b.iter(|| { 93 | let mut back = BitMapBackend::with_buffer(&mut buffer, (s, s)); 94 | back.split(&[s / 2]) 95 | .into_iter() 96 | .zip(element_buffer.iter_mut()) 97 | .collect::>() 98 | .into_par_iter() 99 | .for_each(|(back, buffer)| { 100 | let root = back.into_drawing_area(); 101 | let areas = root.split_evenly((1, 2)); 102 | 103 | let elements: Vec<_> = buffer 104 | .par_iter_mut() 105 | .map(|b| { 106 | let mut e = 107 | BitMapElement::with_mut((0, 0), (s / 2, s / 2), b).unwrap(); 108 | draw_plot(&e.as_bitmap_backend().into_drawing_area(), 2.0); 109 | e 110 | }) 111 | .collect(); 112 | 113 | areas 114 | .into_iter() 115 | .zip(elements.into_iter()) 116 | .for_each(|(a, e)| a.draw(&e).unwrap()) 117 | }); 118 | }) 119 | }), 120 | ); 121 | } 122 | 123 | fn draw_func_2x1(c: &mut Criterion) { 124 | c.bench( 125 | "draw_func_2x1", 126 | ParameterizedBenchmark::new( 127 | "blit", 128 | |b, &&s| { 129 | let mut buffer = vec![0; (s * s * 3) as usize]; 130 | let mut element_buffer = vec![vec![0; (s * s / 2 * 3) as usize]; 2]; 131 | b.iter(|| { 132 | let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area(); 133 | let areas = root.split_evenly((2, 1)); 134 | let elements: Vec<_> = element_buffer 135 | .par_iter_mut() 136 | .map(|buf| { 137 | let mut element = 138 | BitMapElement::with_mut((0, 0), (s, s / 2), buf).unwrap(); 139 | draw_plot(&element.as_bitmap_backend().into_drawing_area(), 2.0); 140 | element 141 | }) 142 | .collect(); 143 | 144 | areas 145 | .into_iter() 146 | .zip(elements.into_iter()) 147 | .for_each(|(a, e)| a.draw(&e).unwrap()); 148 | }) 149 | }, 150 | SIZES.clone(), 151 | ) 152 | .with_function("inplace", |b, &&s| { 153 | let mut buffer = vec![0; (s * s * 3) as usize]; 154 | b.iter(|| { 155 | let mut back = BitMapBackend::with_buffer(&mut buffer, (s, s)); 156 | back.split(&[s / 2]) 157 | .into_par_iter() 158 | .for_each(|b| draw_plot(&b.into_drawing_area(), 2.0)); 159 | }) 160 | }) 161 | .with_function("sequential", |b, &&s| { 162 | let mut buffer = vec![0; (s * s * 3) as usize]; 163 | b.iter(|| { 164 | let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area(); 165 | root.split_evenly((2, 1)) 166 | .iter_mut() 167 | .for_each(|area| draw_plot(area, 2.0)); 168 | }) 169 | }), 170 | ); 171 | } 172 | 173 | criterion_group! { 174 | name = parallel_group; 175 | config = Criterion::default().sample_size(10); 176 | targets = 177 | draw_func_1x1_seq, 178 | draw_func_4x4, 179 | draw_func_2x1, 180 | } 181 | -------------------------------------------------------------------------------- /benches/main.rs: -------------------------------------------------------------------------------- 1 | use criterion::criterion_main; 2 | 3 | mod benches; 4 | 5 | criterion_main! { 6 | benches::parallel::parallel_group, 7 | benches::rasterizer::rasterizer_group, 8 | benches::data::quartiles_group 9 | } 10 | -------------------------------------------------------------------------------- /doc-template/examples/chart.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | fn main() -> Result<(), Box> { 3 | let root = BitMapBackend::new("plotters-doc-data/5.png", (640, 480)).into_drawing_area(); 4 | root.fill(&WHITE); 5 | let root = root.margin(10, 10, 10, 10); 6 | // After this point, we should be able to draw construct a chart context 7 | let mut chart = ChartBuilder::on(&root) 8 | // Set the caption of the chart 9 | .caption("This is our first plot", ("sans-serif", 40).into_font()) 10 | // Set the size of the label region 11 | .x_label_area_size(20) 12 | .y_label_area_size(40) 13 | // Finally attach a coordinate on the drawing area and make a chart context 14 | .build_ranged(0f32..10f32, 0f32..10f32)?; 15 | 16 | // Then we can draw a mesh 17 | chart 18 | .configure_mesh() 19 | // We can customize the maximum number of labels allowed for each axis 20 | .x_labels(5) 21 | .y_labels(5) 22 | // We can also change the format of the label text 23 | .y_label_formatter(&|x| format!("{:.3}", x)) 24 | .draw()?; 25 | 26 | // And we can draw something in the drawing area 27 | chart.draw_series(LineSeries::new( 28 | vec![(0.0, 0.0), (5.0, 5.0), (8.0, 7.0)], 29 | &RED, 30 | ))?; 31 | // Similarly, we can draw point series 32 | chart.draw_series(PointSeries::of_element( 33 | vec![(0.0, 0.0), (5.0, 5.0), (8.0, 7.0)], 34 | 5, 35 | &RED, 36 | &|c, s, st| { 37 | return EmptyElement::at(c) // We want to construct a composed element on-the-fly 38 | + Circle::new((0,0),s,st.filled()) // At this point, the new pixel coordinate is established 39 | + Text::new(format!("{:?}", c), (10, 0), ("sans-serif", 10).into_font()); 40 | }, 41 | ))?; 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /doc-template/examples/composable_elements.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | fn main() -> Result<(), Box> { 4 | let root = BitMapBackend::new("plotters-doc-data/4.png", (640, 480)).into_drawing_area(); 5 | 6 | root.fill(&RGBColor(240, 200, 200))?; 7 | 8 | let root = root.apply_coord_spec(RangedCoord::::new( 9 | 0f32..1f32, 10 | 0f32..1f32, 11 | (0..640, 0..480), 12 | )); 13 | 14 | let dot_and_label = |x: f32, y: f32| { 15 | return EmptyElement::at((x, y)) 16 | + Circle::new((0, 0), 3, ShapeStyle::from(&BLACK).filled()) 17 | + Text::new( 18 | format!("({:.2},{:.2})", x, y), 19 | (10, 0), 20 | ("sans-serif", 15.0).into_font(), 21 | ); 22 | }; 23 | 24 | root.draw(&dot_and_label(0.5, 0.6))?; 25 | root.draw(&dot_and_label(0.25, 0.33))?; 26 | root.draw(&dot_and_label(0.8, 0.8))?; 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /doc-template/examples/drawing_area.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | fn main() -> Result<(), Box> { 3 | let root_drawing_area = 4 | BitMapBackend::new("plotters-doc-data/2.png", (300, 200)).into_drawing_area(); 5 | // And we can split the drawing area into 3x3 grid 6 | let child_drawing_areas = root_drawing_area.split_evenly((3, 3)); 7 | // Then we fill the drawing area with different color 8 | for (area, color) in child_drawing_areas.into_iter().zip(0..) { 9 | area.fill(&Palette99::pick(color))?; 10 | } 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /doc-template/examples/drawing_backends.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | fn main() -> Result<(), Box> { 3 | // Create a 800*600 bitmap and start drawing 4 | let mut backend = BitMapBackend::new("plotters-doc-data/1.png", (300, 200)); 5 | // And if we want SVG backend 6 | // let backend = SVGBackend::new("output.svg", (800, 600)); 7 | backend.draw_rect((50, 50), (200, 150), &RED, true)?; 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /doc-template/examples/elements.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | fn main() -> Result<(), Box> { 3 | let root = BitMapBackend::new("plotters-doc-data/3.png", (300, 200)).into_drawing_area(); 4 | root.fill(&WHITE)?; 5 | // Draw an circle on the drawing area 6 | root.draw(&Circle::new( 7 | (100, 100), 8 | 50, 9 | Into::::into(&GREEN).filled(), 10 | ))?; 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /doc-template/examples/quick_start.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | fn main() -> Result<(), Box> { 3 | let root = BitMapBackend::new("plotters-doc-data/0.png", (640, 480)).into_drawing_area(); 4 | root.fill(&WHITE)?; 5 | let mut chart = ChartBuilder::on(&root) 6 | .caption("y=x^2", ("sans-serif", 50).into_font()) 7 | .margin(5) 8 | .x_label_area_size(30) 9 | .y_label_area_size(30) 10 | .build_ranged(-1f32..1f32, -0.1f32..1f32)?; 11 | 12 | chart.configure_mesh().draw()?; 13 | 14 | chart 15 | .draw_series(LineSeries::new( 16 | (-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)), 17 | &RED, 18 | ))? 19 | .label("y = x^2") 20 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED)); 21 | 22 | chart 23 | .configure_series_labels() 24 | .background_style(&WHITE.mix(0.8)) 25 | .border_style(&BLACK) 26 | .draw()?; 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /doc-template/gen-toc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sed -n 's/^##\(#* .*\)$/\1/gp' $1 \ 3 | | sed -e 's/^ /1\t/g' -e 's/^# /2\t/g' \ 4 | | awk -F'\t' ' 5 | BEGIN { 6 | level[1] = "* "; 7 | level[2] = " + "; 8 | print "## Table of Contents" 9 | } 10 | { 11 | link=tolower($2) 12 | gsub(/[^a-z0-9 ]/, "", link); 13 | gsub(/[^a-z0-9]/, "-", link); 14 | print " "level[$1]"["$2"](#"link")" 15 | }' 16 | -------------------------------------------------------------------------------- /doc-template/latest_version: -------------------------------------------------------------------------------- 1 | 0.2.12 2 | -------------------------------------------------------------------------------- /doc-template/readme/examples: -------------------------------------------------------------------------------- 1 | ../examples -------------------------------------------------------------------------------- /doc-template/readme/gallery: -------------------------------------------------------------------------------- 1 | To view the source code for each example, please click on the example image. 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /doc-template/readme/style: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugcoday/plotchart/860ccbc0e8677b23e8291af6d84187fc59907f85/doc-template/readme/style -------------------------------------------------------------------------------- /doc-template/render_readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | DOC_ROOT=$(readlink -f $(dirname $0)) 3 | DIR=$2 4 | TOC_FILENAME=$(mktemp) 5 | 6 | ${DOC_ROOT}/gen-toc.sh $1 > ${TOC_FILENAME} 7 | 8 | awk '{ 9 | if($0 ~ /\$\$.*\$\$/) { 10 | filename = substr($0,3,length($0)-4); 11 | if(filename == "[TOC]") { 12 | filename ="'${TOC_FILENAME}'" 13 | } else { 14 | filename = "'${DIR}/'"filename 15 | } 16 | while((getline content < filename) > 0) { 17 | print content; 18 | } 19 | } else { 20 | print $0; 21 | } 22 | } 23 | ' $1 | sed 's/\$LATEST_VERSION/'$(cat ${DOC_ROOT}/latest_version)'/g' 24 | 25 | rm -f ${TOC_FILENAME} 26 | -------------------------------------------------------------------------------- /doc-template/rustdoc/examples: -------------------------------------------------------------------------------- 1 | ../examples -------------------------------------------------------------------------------- /doc-template/rustdoc/style: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | -------------------------------------------------------------------------------- /doc-template/update_readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | REPO_BASE=`readlink -f $(dirname $(readlink -f $0))/../` 3 | ${REPO_BASE}/doc-template/render_readme.sh ${REPO_BASE}/doc-template/readme.template.md ${REPO_BASE}/doc-template/readme > ${REPO_BASE}/README.md 4 | 5 | awk ' 6 | NR == FNR { 7 | doc = doc"\n"$0; 8 | } 9 | NR != FNR{ 10 | if($0 == "/*!") { 11 | in_doc = 1; 12 | } 13 | if(!in_doc) { 14 | print $0 15 | } 16 | if($0 == "*/") { 17 | print "/*!" 18 | print doc 19 | print "*/" 20 | in_doc = 0; 21 | } 22 | }' <(${REPO_BASE}/doc-template/render_readme.sh ${REPO_BASE}/doc-template/readme.template.md ${REPO_BASE}/doc-template/rustdoc) ${REPO_BASE}/src/lib.rs > ${REPO_BASE}/src/lib.rs.tmp 23 | 24 | mv ${REPO_BASE}/src/lib.rs.tmp ${REPO_BASE}/src/lib.rs 25 | cargo fmt 26 | -------------------------------------------------------------------------------- /examples/animation.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | fn snowflake_iter(points: &[(f64, f64)]) -> Vec<(f64, f64)> { 4 | let mut ret = vec![]; 5 | for i in 0..points.len() { 6 | let (start, end) = (points[i], points[(i + 1) % points.len()]); 7 | let t = ((end.0 - start.0) / 3.0, (end.1 - start.1) / 3.0); 8 | let s = ( 9 | t.0 * 0.5 - t.1 * (0.75f64).sqrt(), 10 | t.1 * 0.5 + (0.75f64).sqrt() * t.0, 11 | ); 12 | ret.push(start); 13 | ret.push((start.0 + t.0, start.1 + t.1)); 14 | ret.push((start.0 + t.0 + s.0, start.1 + t.1 + s.1)); 15 | ret.push((start.0 + t.0 * 2.0, start.1 + t.1 * 2.0)); 16 | } 17 | ret 18 | } 19 | 20 | fn main() -> Result<(), Box> { 21 | let root = BitMapBackend::gif("plotters-doc-data/animation.gif", (800, 600), 1_000)? 22 | .into_drawing_area(); 23 | 24 | for i in 0..8 { 25 | root.fill(&WHITE)?; 26 | 27 | let mut chart = ChartBuilder::on(&root) 28 | .caption( 29 | format!("Koch's Snowflake (n_iter = {})", i), 30 | ("sans-serif", 50), 31 | ) 32 | .build_ranged(-2.0..2.0, -1.5..1.5)?; 33 | 34 | let mut snowflake_vertices = { 35 | let mut current: Vec<(f64, f64)> = vec![ 36 | (0.0, 1.0), 37 | ((3.0f64).sqrt() / 2.0, -0.5), 38 | (-(3.0f64).sqrt() / 2.0, -0.5), 39 | ]; 40 | for _ in 0..i { 41 | current = snowflake_iter(¤t[..]); 42 | } 43 | current 44 | }; 45 | 46 | chart.draw_series(std::iter::once(Polygon::new( 47 | snowflake_vertices.clone(), 48 | &RED.mix(0.2), 49 | )))?; 50 | 51 | snowflake_vertices.push(snowflake_vertices[0]); 52 | chart.draw_series(std::iter::once(PathElement::new(snowflake_vertices, &RED)))?; 53 | 54 | root.present()?; 55 | } 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /examples/area-chart.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | use rand::SeedableRng; 4 | use rand_distr::{Distribution, Normal}; 5 | use rand_xorshift::XorShiftRng; 6 | 7 | fn main() -> Result<(), Box> { 8 | let data: Vec<_> = { 9 | let norm_dist = Normal::new(500.0, 100.0).unwrap(); 10 | let mut x_rand = XorShiftRng::from_seed(*b"MyFragileSeed123"); 11 | let x_iter = norm_dist.sample_iter(&mut x_rand); 12 | x_iter 13 | .filter(|x| *x < 1500.0) 14 | .take(100) 15 | .zip(0..) 16 | .map(|(x, b)| x + (b as f64).powf(1.2)) 17 | .collect() 18 | }; 19 | 20 | let root = 21 | BitMapBackend::new("plotters-doc-data/area-chart.png", (1024, 768)).into_drawing_area(); 22 | 23 | root.fill(&WHITE)?; 24 | 25 | let mut chart = ChartBuilder::on(&root) 26 | .set_label_area_size(LabelAreaPosition::Left, 60) 27 | .set_label_area_size(LabelAreaPosition::Bottom, 60) 28 | .caption("Area Chart Demo", ("sans-serif", 40)) 29 | .build_ranged(0..(data.len() - 1), 0.0..1500.0)?; 30 | 31 | chart 32 | .configure_mesh() 33 | .disable_x_mesh() 34 | .disable_y_mesh() 35 | .draw()?; 36 | 37 | chart.draw_series( 38 | AreaSeries::new( 39 | (0..).zip(data.iter()).map(|(x, y)| (x, *y)), 40 | 0.0, 41 | &RED.mix(0.2), 42 | ) 43 | .border_style(&RED), 44 | )?; 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /examples/blit-bitmap.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | use image::{FilterType, ImageFormat}; 4 | 5 | use std::fs::File; 6 | use std::io::BufReader; 7 | 8 | fn main() -> Result<(), Box> { 9 | let root = 10 | BitMapBackend::new("plotters-doc-data/blit-bitmap.png", (1024, 768)).into_drawing_area(); 11 | root.fill(&WHITE)?; 12 | 13 | let mut chart = ChartBuilder::on(&root) 14 | .caption("Bitmap Example", ("sans-serif", 30)) 15 | .margin(5) 16 | .set_label_area_size(LabelAreaPosition::Left, 40) 17 | .set_label_area_size(LabelAreaPosition::Bottom, 40) 18 | .build_ranged(0.0..1.0, 0.0..1.0)?; 19 | 20 | chart.configure_mesh().disable_mesh().draw()?; 21 | 22 | let (w, h) = chart.plotting_area().dim_in_pixel(); 23 | let image = image::load( 24 | BufReader::new(File::open("plotters-doc-data/cat.png")?), 25 | ImageFormat::PNG, 26 | )? 27 | .resize_exact(w - w / 10, h - h / 10, FilterType::Nearest); 28 | 29 | let elem: BitMapElement<_> = ((0.05, 0.95), image).into(); 30 | 31 | chart.draw_series(std::iter::once(elem))?; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /examples/chart.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | fn main() -> Result<(), Box> { 4 | let root_area = 5 | BitMapBackend::new("plotters-doc-data/sample.png", (1024, 768)).into_drawing_area(); 6 | 7 | root_area.fill(&WHITE)?; 8 | 9 | let root_area = root_area.titled("Image Title", ("sans-serif", 60).into_font())?; 10 | 11 | let (upper, lower) = root_area.split_vertically(512); 12 | 13 | let mut cc = ChartBuilder::on(&upper) 14 | .margin(5) 15 | .set_all_label_area_size(50) 16 | .caption("Sine and Cosine", ("sans-serif", 40).into_font()) 17 | .build_ranged(-3.4f32..3.4f32, -1.2f32..1.2f32)?; 18 | 19 | cc.configure_mesh() 20 | .x_labels(20) 21 | .y_labels(10) 22 | .disable_mesh() 23 | .x_label_formatter(&|v| format!("{:.1}", v)) 24 | .y_label_formatter(&|v| format!("{:.1}", v)) 25 | .draw()?; 26 | 27 | cc.draw_series(LineSeries::new( 28 | (0..12).map(|x| ((x - 6) as f32 / 2.0, ((x - 6) as f32 / 2.0).sin())), 29 | &RED, 30 | ))? 31 | .label("Sine") 32 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED)); 33 | 34 | cc.draw_series(LineSeries::new( 35 | (0..6800).map(|x| { 36 | ( 37 | (x - 3400) as f32 / 1000.0, 38 | ((x - 3400) as f32 / 1000.0).cos(), 39 | ) 40 | }), 41 | &BLUE, 42 | ))? 43 | .label("Cosine") 44 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE)); 45 | 46 | cc.configure_series_labels().border_style(&BLACK).draw()?; 47 | 48 | /* 49 | // It's possible to use a existing pointing element 50 | cc.draw_series(PointSeries::<_, _, Circle<_>>::new( 51 | (0..6).map(|x| ((x - 3) as f32 / 1.0, ((x - 3) as f32 / 1.0).sin())), 52 | 5, 53 | Into::::into(&RGBColor(255,0,0)).filled(), 54 | ))?;*/ 55 | 56 | // Otherwise you can use a function to construct your pointing element yourself 57 | cc.draw_series(PointSeries::of_element( 58 | (0..6).map(|x| ((x - 3) as f32 / 1.0, ((x - 3) as f32 / 1.0).sin())), 59 | 5, 60 | ShapeStyle::from(&RED).filled(), 61 | &|coord, size, style| { 62 | EmptyElement::at(coord) 63 | + Circle::new((0, 0), size, style) 64 | + Text::new( 65 | format!("{:?}", coord), 66 | (0, 15), 67 | ("sans-serif", 15).into_font(), 68 | ) 69 | }, 70 | ))?; 71 | 72 | let drawing_areas = lower.split_evenly((1, 2)); 73 | 74 | for (drawing_area, idx) in drawing_areas.iter().zip(1..) { 75 | let mut cc = ChartBuilder::on(&drawing_area) 76 | .x_label_area_size(30) 77 | .y_label_area_size(30) 78 | .margin_right(20) 79 | .caption( 80 | format!("y = x^{}", 1 + 2 * idx), 81 | ("sans-serif", 40).into_font(), 82 | ) 83 | .build_ranged(-1f32..1f32, -1f32..1f32)?; 84 | cc.configure_mesh().x_labels(5).y_labels(3).draw()?; 85 | 86 | cc.draw_series(LineSeries::new( 87 | (-100..100).map(|x| { 88 | ( 89 | x as f32 / 100.0, 90 | (x as f32 / 100.0).powf(idx as f32 * 2.0 + 1.0), 91 | ) 92 | }), 93 | &BLUE, 94 | ))?; 95 | } 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /examples/console.rs: -------------------------------------------------------------------------------- 1 | use plotters::drawing::{ 2 | backend::{BackendStyle, DrawingErrorKind}, 3 | DrawingBackend, 4 | }; 5 | use plotters::prelude::*; 6 | use plotters::style::text_anchor::{HPos, VPos}; 7 | use plotters::style::RGBAColor; 8 | use std::error::Error; 9 | 10 | #[derive(Copy, Clone)] 11 | enum PixelState { 12 | Empty, 13 | HLine, 14 | VLine, 15 | Cross, 16 | Pixel, 17 | Text(char), 18 | Circle(bool), 19 | } 20 | 21 | impl PixelState { 22 | fn to_char(self) -> char { 23 | match self { 24 | Self::Empty => ' ', 25 | Self::HLine => '-', 26 | Self::VLine => '|', 27 | Self::Cross => '+', 28 | Self::Pixel => '.', 29 | Self::Text(c) => c, 30 | Self::Circle(filled) => { 31 | if filled { 32 | '@' 33 | } else { 34 | 'O' 35 | } 36 | } 37 | } 38 | } 39 | 40 | fn update(&mut self, new_state: PixelState) { 41 | let next_state = match (*self, new_state) { 42 | (Self::HLine, Self::VLine) => Self::Cross, 43 | (Self::VLine, Self::HLine) => Self::Cross, 44 | (_, Self::Circle(what)) => Self::Circle(what), 45 | (Self::Circle(what), _) => Self::Circle(what), 46 | (_, Self::Pixel) => Self::Pixel, 47 | (Self::Pixel, _) => Self::Pixel, 48 | (_, new) => new, 49 | }; 50 | 51 | *self = next_state; 52 | } 53 | } 54 | 55 | pub struct TextDrawingBackend(Vec); 56 | 57 | impl DrawingBackend for TextDrawingBackend { 58 | type ErrorType = std::io::Error; 59 | 60 | fn get_size(&self) -> (u32, u32) { 61 | (100, 30) 62 | } 63 | 64 | fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { 65 | Ok(()) 66 | } 67 | 68 | fn present(&mut self) -> Result<(), DrawingErrorKind> { 69 | for r in 0..30 { 70 | let mut buf = String::new(); 71 | for c in 0..100 { 72 | buf.push(self.0[r * 100 + c].to_char()); 73 | } 74 | println!("{}", buf); 75 | } 76 | 77 | Ok(()) 78 | } 79 | 80 | fn draw_pixel( 81 | &mut self, 82 | pos: (i32, i32), 83 | color: &RGBAColor, 84 | ) -> Result<(), DrawingErrorKind> { 85 | if color.alpha() > 0.3 { 86 | self.0[(pos.1 * 100 + pos.0) as usize].update(PixelState::Pixel); 87 | } 88 | Ok(()) 89 | } 90 | 91 | fn draw_line( 92 | &mut self, 93 | from: (i32, i32), 94 | to: (i32, i32), 95 | style: &S, 96 | ) -> Result<(), DrawingErrorKind> { 97 | if from.0 == to.0 { 98 | let x = from.0; 99 | let y0 = from.1.min(to.1); 100 | let y1 = from.1.max(to.1); 101 | for y in y0..y1 { 102 | self.0[(y * 100 + x) as usize].update(PixelState::VLine); 103 | } 104 | return Ok(()); 105 | } 106 | 107 | if from.1 == to.1 { 108 | let y = from.1; 109 | let x0 = from.0.min(to.0); 110 | let x1 = from.0.max(to.0); 111 | for x in x0..x1 { 112 | self.0[(y * 100 + x) as usize].update(PixelState::HLine); 113 | } 114 | return Ok(()); 115 | } 116 | 117 | plotters::drawing::rasterizer::draw_line(self, from, to, style) 118 | } 119 | 120 | fn estimate_text_size<'a>( 121 | &self, 122 | text: &str, 123 | _font: &FontDesc<'a>, 124 | ) -> Result<(u32, u32), DrawingErrorKind> { 125 | Ok((text.len() as u32, 1)) 126 | } 127 | 128 | fn draw_text( 129 | &mut self, 130 | text: &str, 131 | style: &TextStyle, 132 | pos: (i32, i32), 133 | ) -> Result<(), DrawingErrorKind> { 134 | let (width, height) = self.estimate_text_size(text, &style.font)?; 135 | let (width, height) = (width as i32, height as i32); 136 | let dx = match style.pos.h_pos { 137 | HPos::Left => 0, 138 | HPos::Right => -width, 139 | HPos::Center => -width / 2, 140 | }; 141 | let dy = match style.pos.v_pos { 142 | VPos::Top => 0, 143 | VPos::Center => -height / 2, 144 | VPos::Bottom => -height, 145 | }; 146 | let offset = (pos.1 + dy).max(0) * 100 + (pos.0 + dx).max(0); 147 | for (idx, chr) in (offset..).zip(text.chars()) { 148 | self.0[idx as usize].update(PixelState::Text(chr)); 149 | } 150 | Ok(()) 151 | } 152 | } 153 | 154 | fn draw_chart( 155 | b: DrawingArea, 156 | ) -> Result<(), Box> 157 | where 158 | DB::ErrorType: 'static, 159 | { 160 | let mut chart = ChartBuilder::on(&b) 161 | .margin(1) 162 | .caption("Sine and Cosine", ("sans-serif", (10).percent_height())) 163 | .set_label_area_size(LabelAreaPosition::Left, (5i32).percent_width()) 164 | .set_label_area_size(LabelAreaPosition::Bottom, (10i32).percent_height()) 165 | .build_ranged(-std::f64::consts::PI..std::f64::consts::PI, -1.2..1.2)?; 166 | 167 | chart 168 | .configure_mesh() 169 | .disable_x_mesh() 170 | .disable_y_mesh() 171 | .draw()?; 172 | 173 | chart.draw_series(LineSeries::new( 174 | (-314..314).map(|x| x as f64 / 100.0).map(|x| (x, x.sin())), 175 | &RED, 176 | ))?; 177 | 178 | chart.draw_series(LineSeries::new( 179 | (-314..314).map(|x| x as f64 / 100.0).map(|x| (x, x.cos())), 180 | &RED, 181 | ))?; 182 | 183 | b.present()?; 184 | 185 | Ok(()) 186 | } 187 | 188 | fn main() -> Result<(), Box> { 189 | draw_chart(TextDrawingBackend(vec![PixelState::Empty; 5000]).into_drawing_area())?; 190 | let b = BitMapBackend::new("plotters-doc-data/console-example.png", (1024, 768)) 191 | .into_drawing_area(); 192 | b.fill(&WHITE)?; 193 | draw_chart(b)?; 194 | Ok(()) 195 | } 196 | -------------------------------------------------------------------------------- /examples/errorbar.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | use rand::SeedableRng; 4 | use rand_distr::{Distribution, Normal}; 5 | use rand_xorshift::XorShiftRng; 6 | 7 | use itertools::Itertools; 8 | 9 | use num_traits::sign::Signed; 10 | 11 | fn main() -> Result<(), Box> { 12 | let data = generate_random_data(); 13 | let down_sampled = down_sample(&data[..]); 14 | 15 | let root = 16 | BitMapBackend::new("plotters-doc-data/errorbar.png", (1024, 768)).into_drawing_area(); 17 | 18 | root.fill(&WHITE)?; 19 | 20 | let mut chart = ChartBuilder::on(&root) 21 | .caption("Linear Function with Noise", ("sans-serif", 60)) 22 | .margin(10) 23 | .set_label_area_size(LabelAreaPosition::Left, 40) 24 | .set_label_area_size(LabelAreaPosition::Bottom, 40) 25 | .build_ranged(-10f64..10f64, -10f64..10f64)?; 26 | 27 | chart.configure_mesh().draw()?; 28 | 29 | chart 30 | .draw_series(LineSeries::new(data, &GREEN.mix(0.3)))? 31 | .label("Raw Data") 32 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &GREEN)); 33 | 34 | chart.draw_series(LineSeries::new( 35 | down_sampled.iter().map(|(x, _, y, _)| (*x, *y)), 36 | &BLUE, 37 | ))?; 38 | 39 | chart 40 | .draw_series( 41 | down_sampled.iter().map(|(x, yl, ym, yh)| { 42 | ErrorBar::new_vertical(*x, *yl, *ym, *yh, BLUE.filled(), 20) 43 | }), 44 | )? 45 | .label("Down-sampled") 46 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE)); 47 | 48 | chart 49 | .configure_series_labels() 50 | .background_style(WHITE.filled()) 51 | .draw()?; 52 | 53 | Ok(()) 54 | } 55 | 56 | fn generate_random_data() -> Vec<(f64, f64)> { 57 | let norm_dist = Normal::new(0.0, 1.0).unwrap(); 58 | let mut x_rand = XorShiftRng::from_seed(*b"MyFragileSeed123"); 59 | let x_iter = norm_dist.sample_iter(&mut x_rand); 60 | x_iter 61 | .take(20000) 62 | .filter(|x| x.abs() <= 4.0) 63 | .zip(-10000..10000) 64 | .map(|(yn, x)| { 65 | ( 66 | x as f64 / 1000.0, 67 | x as f64 / 1000.0 + yn * x as f64 / 10000.0, 68 | ) 69 | }) 70 | .collect() 71 | } 72 | 73 | fn down_sample(data: &[(f64, f64)]) -> Vec<(f64, f64, f64, f64)> { 74 | let down_sampled: Vec<_> = data 75 | .iter() 76 | .group_by(|x| (x.0 * 1.0).round() / 1.0) 77 | .into_iter() 78 | .map(|(x, g)| { 79 | let mut g: Vec<_> = g.map(|(_, y)| *y).collect(); 80 | g.sort_by(|a, b| a.partial_cmp(b).unwrap()); 81 | ( 82 | x, 83 | g[0], 84 | g.iter().sum::() / g.len() as f64, 85 | g[g.len() - 1], 86 | ) 87 | }) 88 | .collect(); 89 | down_sampled 90 | } 91 | -------------------------------------------------------------------------------- /examples/gtk-demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gtk-demo" 3 | version = "0.1.0" 4 | authors = ["Hao Hou "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | plotters = {path = "../..", features = ["cairo"]} 11 | cairo-rs = "0.8.0" 12 | gtk = "0.8.0" 13 | gio = "0.8.0" 14 | -------------------------------------------------------------------------------- /examples/gtk-demo/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | 3 | use gio::prelude::*; 4 | use gtk::prelude::*; 5 | use gtk::DrawingArea; 6 | 7 | use cairo::Context; 8 | use plotters::prelude::*; 9 | 10 | fn build_ui(app: >k::Application) { 11 | drawable(app, 500, 500, |_, cr| { 12 | let root = CairoBackend::new(cr, (500, 500)).unwrap().into_drawing_area(); 13 | 14 | root.fill(&WHITE).unwrap(); 15 | 16 | let mut chart = ChartBuilder::on(&root) 17 | .caption("This is a test", ("sans-serif", 20)) 18 | .x_label_area_size(40) 19 | .y_label_area_size(40) 20 | .build_ranged(0..100, 0..100) 21 | .unwrap(); 22 | 23 | chart.configure_mesh() 24 | .draw() 25 | .unwrap(); 26 | 27 | Inhibit(false) 28 | }) 29 | } 30 | 31 | fn main() { 32 | let application = gtk::Application::new( 33 | Some("io.github.plotters-rs.plotters-gtk-test"), 34 | Default::default(), 35 | ) 36 | .expect("Initialization failed..."); 37 | 38 | application.connect_activate(|app| { 39 | build_ui(app); 40 | }); 41 | 42 | application.run(&args().collect::>()); 43 | } 44 | 45 | pub fn drawable(application: >k::Application, width: i32, height: i32, draw_fn: F) 46 | where 47 | F: Fn(&DrawingArea, &Context) -> Inhibit + 'static, 48 | { 49 | let window = gtk::ApplicationWindow::new(application); 50 | let drawing_area = Box::new(DrawingArea::new)(); 51 | 52 | drawing_area.connect_draw(draw_fn); 53 | 54 | window.set_default_size(width, height); 55 | 56 | window.add(&drawing_area); 57 | window.show_all(); 58 | } 59 | -------------------------------------------------------------------------------- /examples/histogram.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | fn main() -> Result<(), Box> { 3 | let root = 4 | BitMapBackend::new("plotters-doc-data/histogram.png", (640, 480)).into_drawing_area(); 5 | 6 | root.fill(&WHITE)?; 7 | 8 | let mut chart = ChartBuilder::on(&root) 9 | .x_label_area_size(35) 10 | .y_label_area_size(40) 11 | .margin(5) 12 | .caption("Histogram Test", ("sans-serif", 50.0).into_font()) 13 | .build_ranged(0u32..10u32, 0u32..10u32)?; 14 | 15 | chart 16 | .configure_mesh() 17 | .disable_x_mesh() 18 | .line_style_1(&WHITE.mix(0.3)) 19 | .x_label_offset(30) 20 | .y_desc("Count") 21 | .x_desc("Bucket") 22 | .axis_desc_style(("sans-serif", 15).into_font()) 23 | .draw()?; 24 | 25 | let data = [ 26 | 0u32, 1, 1, 1, 4, 2, 5, 7, 8, 6, 4, 2, 1, 8, 3, 3, 3, 4, 4, 3, 3, 3, 27 | ]; 28 | 29 | chart.draw_series( 30 | Histogram::vertical(&chart) 31 | .style(RED.mix(0.5).filled()) 32 | .data(data.iter().map(|x: &u32| (*x, 1))), 33 | )?; 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/mandelbrot.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | use std::ops::Range; 3 | 4 | fn main() -> Result<(), Box> { 5 | let root = 6 | BitMapBackend::new("plotters-doc-data/mandelbrot.png", (800, 600)).into_drawing_area(); 7 | 8 | root.fill(&WHITE)?; 9 | 10 | let mut chart = ChartBuilder::on(&root) 11 | .margin(20) 12 | .x_label_area_size(10) 13 | .y_label_area_size(10) 14 | .build_ranged(-2.1f64..0.6f64, -1.2f64..1.2f64)?; 15 | 16 | chart 17 | .configure_mesh() 18 | .disable_x_mesh() 19 | .disable_y_mesh() 20 | .draw()?; 21 | 22 | let plotting_area = chart.plotting_area(); 23 | 24 | let range = plotting_area.get_pixel_range(); 25 | 26 | let (pw, ph) = (range.0.end - range.0.start, range.1.end - range.1.start); 27 | let (xr, yr) = (chart.x_range(), chart.y_range()); 28 | 29 | for (x, y, c) in mandelbrot_set(xr, yr, (pw as usize, ph as usize), 100) { 30 | if c != 100 { 31 | plotting_area.draw_pixel((x, y), &HSLColor(c as f64 / 100.0, 1.0, 0.5))?; 32 | } else { 33 | plotting_area.draw_pixel((x, y), &BLACK)?; 34 | } 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | fn mandelbrot_set( 41 | real: Range, 42 | complex: Range, 43 | samples: (usize, usize), 44 | max_iter: usize, 45 | ) -> impl Iterator { 46 | let step = ( 47 | (real.end - real.start) / samples.0 as f64, 48 | (complex.end - complex.start) / samples.1 as f64, 49 | ); 50 | return (0..(samples.0 * samples.1)).map(move |k| { 51 | let c = ( 52 | real.start + step.0 * (k % samples.0) as f64, 53 | complex.start + step.1 * (k / samples.0) as f64, 54 | ); 55 | let mut z = (0.0, 0.0); 56 | let mut cnt = 0; 57 | while cnt < max_iter && z.0 * z.0 + z.1 * z.1 <= 1e10 { 58 | z = (z.0 * z.0 - z.1 * z.1 + c.0, 2.0 * z.0 * z.1 + c.1); 59 | cnt += 1; 60 | } 61 | return (c.0, c.1, cnt); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /examples/matshow.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | fn main() -> Result<(), Box> { 4 | let root = BitMapBackend::new("plotters-doc-data/matshow.png", (1024, 768)).into_drawing_area(); 5 | 6 | root.fill(&WHITE)?; 7 | 8 | let mut chart = ChartBuilder::on(&root) 9 | .caption("Matshow Example", ("sans-serif", 80)) 10 | .margin(5) 11 | .top_x_label_area_size(40) 12 | .y_label_area_size(40) 13 | .build_ranged(0i32..15i32, 15i32..0i32)?; 14 | 15 | chart 16 | .configure_mesh() 17 | .x_labels(15) 18 | .y_labels(15) 19 | .x_label_offset(35) 20 | .y_label_offset(25) 21 | .disable_x_mesh() 22 | .disable_y_mesh() 23 | .label_style(("sans-serif", 20)) 24 | .draw()?; 25 | 26 | let mut matrix = [[0; 15]; 15]; 27 | 28 | for i in 0..15 { 29 | matrix[i][i] = i + 4; 30 | } 31 | 32 | chart.draw_series( 33 | matrix 34 | .iter() 35 | .zip(0..) 36 | .map(|(l, y)| l.iter().zip(0..).map(move |(v, x)| (x as i32, y as i32, v))) 37 | .flatten() 38 | .map(|(x, y, v)| { 39 | Rectangle::new( 40 | [(x, y), (x + 1, y + 1)], 41 | HSLColor( 42 | 240.0 / 360.0 - 240.0 / 360.0 * (*v as f64 / 20.0), 43 | 0.7, 44 | 0.1 + 0.4 * *v as f64 / 20.0, 45 | ) 46 | .filled(), 47 | ) 48 | }), 49 | )?; 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/minifb-demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minifb-demo" 3 | version = "0.1.0" 4 | authors = ["Hao Hou "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | minifb = "0.13.0" 11 | plotters = { path = "../.." , default_features = false, features = ["bitmap"]} 12 | -------------------------------------------------------------------------------- /examples/minifb-demo/src/main.rs: -------------------------------------------------------------------------------- 1 | use minifb::{Key, KeyRepeat, Window, WindowOptions}; 2 | use plotters::drawing::bitmap_pixel::BGRXPixel; 3 | use plotters::prelude::*; 4 | use std::collections::VecDeque; 5 | use std::error::Error; 6 | use std::time::SystemTime; 7 | const W: usize = 480; 8 | const H: usize = 320; 9 | 10 | const SAMPLE_RATE: f64 = 10_000.0; 11 | const FREAME_RATE: f64 = 30.0; 12 | 13 | fn get_window_title(fx: f64, fy: f64, iphase: f64) -> String { 14 | format!( 15 | "x={:.1}Hz, y={:.1}Hz, phase={:.1} +/-=Adjust y 9/0=Adjust x =Exit", 16 | fx, fy, iphase 17 | ) 18 | } 19 | 20 | fn main() -> Result<(), Box> { 21 | let mut buf = vec![0u8; W * H * 4]; 22 | 23 | let mut fx: f64 = 1.0; 24 | let mut fy: f64 = 1.1; 25 | let mut xphase: f64 = 0.0; 26 | let mut yphase: f64 = 0.1; 27 | 28 | let mut window = Window::new( 29 | &get_window_title(fx, fy, yphase - xphase), 30 | W, 31 | H, 32 | WindowOptions::default(), 33 | )?; 34 | let root = 35 | BitMapBackend::::with_buffer_and_format(&mut buf[..], (W as u32, H as u32))? 36 | .into_drawing_area(); 37 | root.fill(&BLACK)?; 38 | 39 | let mut chart = ChartBuilder::on(&root) 40 | .margin(10) 41 | .set_all_label_area_size(30) 42 | .build_ranged(-1.2..1.2, -1.2..1.2)?; 43 | 44 | chart 45 | .configure_mesh() 46 | .label_style(("sans-serif", 15).into_font().color(&GREEN)) 47 | .axis_style(&GREEN) 48 | .draw()?; 49 | 50 | let cs = chart.into_chart_state(); 51 | drop(root); 52 | 53 | let mut data = VecDeque::new(); 54 | let start_ts = SystemTime::now(); 55 | let mut last_flushed = 0.0; 56 | 57 | while window.is_open() && !window.is_key_down(Key::Escape) { 58 | let epoch = SystemTime::now() 59 | .duration_since(start_ts) 60 | .unwrap() 61 | .as_secs_f64(); 62 | 63 | if let Some((ts, _, _)) = data.back() { 64 | if epoch - ts < 1.0 / SAMPLE_RATE { 65 | std::thread::sleep(std::time::Duration::from_secs_f64(epoch - ts)); 66 | continue; 67 | } 68 | let mut ts = *ts; 69 | while ts < epoch { 70 | ts += 1.0 / SAMPLE_RATE; 71 | let phase_x: f64 = 2.0 * ts * std::f64::consts::PI * fx + xphase; 72 | let phase_y: f64 = 2.0 * ts * std::f64::consts::PI * fy + yphase; 73 | data.push_back((ts, phase_x.sin(), phase_y.sin())); 74 | } 75 | } 76 | 77 | let phase_x = 2.0 * epoch * std::f64::consts::PI * fx + xphase; 78 | let phase_y = 2.0 * epoch * std::f64::consts::PI * fy + yphase; 79 | data.push_back((epoch, phase_x.sin(), phase_y.sin())); 80 | 81 | if epoch - last_flushed > 1.0 / FREAME_RATE { 82 | let root = BitMapBackend::::with_buffer_and_format( 83 | &mut buf[..], 84 | (W as u32, H as u32), 85 | )? 86 | .into_drawing_area(); 87 | let mut chart = cs.clone().restore(&root); 88 | chart.plotting_area().fill(&BLACK)?; 89 | 90 | chart 91 | .configure_mesh() 92 | .line_style_1(&GREEN.mix(0.2)) 93 | .line_style_2(&TRANSPARENT) 94 | .draw()?; 95 | 96 | chart.draw_series(data.iter().zip(data.iter().skip(1)).map( 97 | |(&(e, x0, y0), &(_, x1, y1))| { 98 | PathElement::new( 99 | vec![(x0, y0), (x1, y1)], 100 | &GREEN.mix(((e - epoch) * 20.0).exp()), 101 | ) 102 | }, 103 | ))?; 104 | 105 | drop(root); 106 | drop(chart); 107 | 108 | if let Some(keys) = window.get_keys_pressed(KeyRepeat::Yes) { 109 | for key in keys { 110 | let old_fx = fx; 111 | let old_fy = fy; 112 | match key { 113 | Key::Equal => { 114 | fy += 0.1; 115 | } 116 | Key::Minus => { 117 | fy -= 0.1; 118 | } 119 | Key::Key0 => { 120 | fx += 0.1; 121 | } 122 | Key::Key9 => { 123 | fx -= 0.1; 124 | } 125 | _ => { 126 | continue; 127 | } 128 | } 129 | xphase += 2.0 * epoch * std::f64::consts::PI * (old_fx - fx); 130 | yphase += 2.0 * epoch * std::f64::consts::PI * (old_fy - fy); 131 | 132 | window.set_title(&get_window_title(fx, fy, yphase - xphase)); 133 | break; 134 | } 135 | } 136 | window.update_with_buffer(unsafe { std::mem::transmute(&buf[..]) })?; 137 | last_flushed = epoch; 138 | } 139 | 140 | while let Some((e, _, _)) = data.front() { 141 | if ((e - epoch) * 20.0).exp() > 0.1 { 142 | break; 143 | } 144 | data.pop_front(); 145 | } 146 | } 147 | Ok(()) 148 | } 149 | -------------------------------------------------------------------------------- /examples/normal-dist.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | use rand::SeedableRng; 4 | use rand_distr::{Distribution, Normal}; 5 | use rand_xorshift::XorShiftRng; 6 | 7 | fn main() -> Result<(), Box> { 8 | let root = 9 | BitMapBackend::new("plotters-doc-data/normal-dist.png", (1024, 768)).into_drawing_area(); 10 | 11 | root.fill(&WHITE)?; 12 | 13 | let sd = 0.13; 14 | 15 | let random_points: Vec<(f64, f64)> = { 16 | let norm_dist = Normal::new(0.5, sd).unwrap(); 17 | let mut x_rand = XorShiftRng::from_seed(*b"MyFragileSeed123"); 18 | let mut y_rand = XorShiftRng::from_seed(*b"MyFragileSeed321"); 19 | let x_iter = norm_dist.sample_iter(&mut x_rand); 20 | let y_iter = norm_dist.sample_iter(&mut y_rand); 21 | x_iter.zip(y_iter).take(5000).collect() 22 | }; 23 | 24 | let areas = root.split_by_breakpoints([944], [80]); 25 | 26 | let mut x_hist_ctx = ChartBuilder::on(&areas[0]) 27 | .y_label_area_size(40) 28 | .build_ranged(0u32..100u32, 0f64..0.5f64)?; 29 | let mut y_hist_ctx = ChartBuilder::on(&areas[3]) 30 | .x_label_area_size(40) 31 | .build_ranged(0f64..0.5f64, 0..100u32)?; 32 | let mut scatter_ctx = ChartBuilder::on(&areas[2]) 33 | .x_label_area_size(40) 34 | .y_label_area_size(40) 35 | .build_ranged(0f64..1f64, 0f64..1f64)?; 36 | scatter_ctx 37 | .configure_mesh() 38 | .disable_x_mesh() 39 | .disable_y_mesh() 40 | .draw()?; 41 | scatter_ctx.draw_series( 42 | random_points 43 | .iter() 44 | .map(|(x, y)| Circle::new((*x, *y), 2, GREEN.filled())), 45 | )?; 46 | let x_hist = Histogram::vertical(&x_hist_ctx) 47 | .style(GREEN.filled()) 48 | .margin(0) 49 | .data( 50 | random_points 51 | .iter() 52 | .map(|(x, _)| ((x * 100.0) as u32, 0.002)), 53 | ); 54 | let y_hist = Histogram::horizontal(&y_hist_ctx) 55 | .style(GREEN.filled()) 56 | .margin(0) 57 | .data( 58 | random_points 59 | .iter() 60 | .map(|(_, y)| ((y * 100.0) as u32, 0.002)), 61 | ); 62 | x_hist_ctx.draw_series(x_hist)?; 63 | y_hist_ctx.draw_series(y_hist)?; 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /examples/normal-dist2.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | use rand::SeedableRng; 4 | use rand_distr::{Distribution, Normal}; 5 | use rand_xorshift::XorShiftRng; 6 | 7 | use num_traits::sign::Signed; 8 | 9 | fn main() -> Result<(), Box> { 10 | let sd = 0.60; 11 | 12 | let random_points: Vec = { 13 | let norm_dist = Normal::new(0.0, sd).unwrap(); 14 | let mut x_rand = XorShiftRng::from_seed(*b"MyFragileSeed123"); 15 | let x_iter = norm_dist.sample_iter(&mut x_rand); 16 | x_iter.take(5000).filter(|x| x.abs() <= 4.0).collect() 17 | }; 18 | 19 | let root = 20 | BitMapBackend::new("plotters-doc-data/normal-dist2.png", (1024, 768)).into_drawing_area(); 21 | 22 | root.fill(&WHITE)?; 23 | 24 | let mut chart = ChartBuilder::on(&root) 25 | .margin(5) 26 | .caption("1D Gaussian Distribution Demo", ("sans-serif", 30)) 27 | .set_label_area_size(LabelAreaPosition::Left, 60) 28 | .set_label_area_size(LabelAreaPosition::Bottom, 60) 29 | .set_label_area_size(LabelAreaPosition::Right, 60) 30 | .build_ranged(-4f64..4f64, 0f64..0.1)? 31 | .set_secondary_coord((-40i32..40i32).into_centric(), 0u32..500u32); 32 | 33 | chart 34 | .configure_mesh() 35 | .disable_x_mesh() 36 | .disable_y_mesh() 37 | .y_label_formatter(&|y| format!("{:.0}%", *y * 100.0)) 38 | .y_desc("Percentage") 39 | .draw()?; 40 | 41 | chart.configure_secondary_axes().y_desc("Count").draw()?; 42 | 43 | let actual = Histogram::vertical(chart.borrow_secondary()) 44 | .style(GREEN.filled()) 45 | .margin(3) 46 | .data( 47 | random_points 48 | .iter() 49 | .map(|x| ((x * 10.0).round() as i32, 1u32)), 50 | ); 51 | 52 | chart 53 | .draw_secondary_series(actual)? 54 | .label("Observed") 55 | .legend(|(x, y)| Rectangle::new([(x, y - 5), (x + 10, y + 5)], GREEN.filled())); 56 | 57 | let pdf = LineSeries::new( 58 | (-400..400).map(|x| x as f64 / 100.0).map(|x| { 59 | ( 60 | x, 61 | (-x * x / 2.0 / sd / sd).exp() / (2.0 * std::f64::consts::PI * sd * sd).sqrt() 62 | * 0.1, 63 | ) 64 | }), 65 | &RED, 66 | ); 67 | 68 | chart 69 | .draw_series(pdf)? 70 | .label("PDF") 71 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED.filled())); 72 | 73 | chart.configure_series_labels().draw()?; 74 | 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /examples/piston-demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "piston-demo" 3 | version = "0.1.0" 4 | authors = ["Hao Hou "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | piston_window = "0.105.0" 9 | plotters = {path = "../..", default_features = false, features = ["piston", "line_series"]} 10 | systemstat = "0.1.4" 11 | -------------------------------------------------------------------------------- /examples/piston-demo/README.md: -------------------------------------------------------------------------------- 1 | # Realtime CPU Usage by Plotters + Piston 2 | 3 | This is a demo that demonstrate using Plotters along with Piston for dynamic rendering. 4 | 5 | To try the demo use 6 | 7 | ``` 8 | cargo run --release 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/piston-demo/src/main.rs: -------------------------------------------------------------------------------- 1 | use piston_window::{EventLoop, PistonWindow, WindowSettings}; 2 | use plotters::prelude::*; 3 | use systemstat::platform::common::Platform; 4 | use systemstat::System; 5 | 6 | use std::collections::vec_deque::VecDeque; 7 | 8 | const FPS: u32 = 10; 9 | const LENGTH: u32 = 20; 10 | const N_DATA_POINTS: usize = (FPS * LENGTH) as usize; 11 | fn main() { 12 | let mut window: PistonWindow = WindowSettings::new("Real Time CPU Usage", [450, 300]) 13 | .samples(4) 14 | .build() 15 | .unwrap(); 16 | let sys = System::new(); 17 | window.set_max_fps(FPS as u64); 18 | let mut load_measurement: Vec<_> = (0..FPS).map(|_| sys.cpu_load().unwrap()).collect(); 19 | let mut epoch = 0; 20 | let mut data = vec![]; 21 | while let Some(_) = draw_piston_window(&mut window, |b| { 22 | let cpu_loads = load_measurement[epoch % FPS as usize].done()?; 23 | 24 | let root = b.into_drawing_area(); 25 | root.fill(&WHITE)?; 26 | 27 | if data.len() < cpu_loads.len() { 28 | for _ in data.len()..cpu_loads.len() { 29 | data.push(VecDeque::from(vec![0f32; N_DATA_POINTS + 1])); 30 | } 31 | } 32 | 33 | for (core_load, target) in cpu_loads.into_iter().zip(data.iter_mut()) { 34 | if target.len() == N_DATA_POINTS + 1 { 35 | target.pop_front(); 36 | } 37 | target.push_back(1.0 - core_load.idle); 38 | } 39 | 40 | let mut cc = ChartBuilder::on(&root) 41 | .margin(10) 42 | .caption("Real Time CPU Usage", ("sans-serif", 30).into_font()) 43 | .x_label_area_size(40) 44 | .y_label_area_size(50) 45 | .build_ranged(0..N_DATA_POINTS as u32, 0f32..1f32)?; 46 | 47 | cc.configure_mesh() 48 | .x_label_formatter(&|x| format!("{}", -(LENGTH as f32) + (*x as f32 / FPS as f32))) 49 | .y_label_formatter(&|y| format!("{}%", (*y * 100.0) as u32)) 50 | .x_labels(15) 51 | .y_labels(5) 52 | .x_desc("Seconds") 53 | .y_desc("% Busy") 54 | .axis_desc_style(("sans-serif", 15).into_font()) 55 | .draw()?; 56 | 57 | for (idx, data) in (0..).zip(data.iter()) { 58 | cc.draw_series(LineSeries::new( 59 | (0..).zip(data.iter()).map(|(a, b)| (a, *b)), 60 | &Palette99::pick(idx), 61 | ))? 62 | .label(format!("CPU {}", idx)) 63 | .legend(move |(x, y)| { 64 | Rectangle::new([(x - 5, y - 5), (x + 5, y + 5)], &Palette99::pick(idx)) 65 | }); 66 | } 67 | 68 | cc.configure_series_labels() 69 | .background_style(&WHITE.mix(0.8)) 70 | .border_style(&BLACK) 71 | .draw()?; 72 | 73 | load_measurement[epoch % FPS as usize] = sys.cpu_load()?; 74 | epoch += 1; 75 | Ok(()) 76 | }) {} 77 | } 78 | -------------------------------------------------------------------------------- /examples/relative_size.rs: -------------------------------------------------------------------------------- 1 | use plotters::coord::Shift; 2 | use plotters::prelude::*; 3 | 4 | fn draw_chart(root: &DrawingArea) -> DrawResult<(), B> { 5 | let mut chart = ChartBuilder::on(root) 6 | .caption( 7 | "Relative Size Example", 8 | ("sans-serif", (5).percent_height()), 9 | ) 10 | .x_label_area_size((10).percent_height()) 11 | .y_label_area_size((10).percent_width()) 12 | .margin(5) 13 | .build_ranged(-5.0..5.0, -1.0..1.0)?; 14 | 15 | chart 16 | .configure_mesh() 17 | .disable_x_mesh() 18 | .disable_y_mesh() 19 | .label_style(("sans-serif", (3).percent_height())) 20 | .draw()?; 21 | 22 | chart.draw_series(LineSeries::new( 23 | (0..1000) 24 | .map(|x| x as f64 / 100.0 - 5.0) 25 | .map(|x| (x, x.sin())), 26 | &RED, 27 | ))?; 28 | Ok(()) 29 | } 30 | 31 | fn main() -> Result<(), Box> { 32 | let root = 33 | BitMapBackend::new("plotters-doc-data/relative_size.png", (1024, 768)).into_drawing_area(); 34 | 35 | root.fill(&WHITE)?; 36 | 37 | let (left, right) = root.split_horizontally((70).percent_width()); 38 | 39 | draw_chart(&left)?; 40 | 41 | let (upper, lower) = right.split_vertically(300); 42 | 43 | draw_chart(&upper)?; 44 | draw_chart(&lower)?; 45 | 46 | draw_chart(&root.shrink((200, 200), (150, 100)))?; 47 | 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /examples/sierpinski.rs: -------------------------------------------------------------------------------- 1 | use plotters::coord::Shift; 2 | use plotters::prelude::*; 3 | 4 | pub fn sierpinski_carpet( 5 | depth: u32, 6 | drawing_area: &DrawingArea, 7 | ) -> Result<(), Box> { 8 | if depth > 0 { 9 | let sub_areas = drawing_area.split_evenly((3, 3)); 10 | for (idx, sub_area) in (0..).zip(sub_areas.iter()) { 11 | if idx != 4 { 12 | sub_area.fill(&BLUE)?; 13 | sierpinski_carpet(depth - 1, sub_area)?; 14 | } else { 15 | sub_area.fill(&WHITE)?; 16 | } 17 | } 18 | } 19 | Ok(()) 20 | } 21 | 22 | fn main() -> Result<(), Box> { 23 | let root = 24 | BitMapBackend::new("plotters-doc-data/sierpinski.png", (1024, 768)).into_drawing_area(); 25 | 26 | root.fill(&WHITE)?; 27 | 28 | let root = root 29 | .titled("Sierpinski Carpet Demo", ("sans-serif", 60))? 30 | .shrink(((1024 - 700) / 2, 0), (700, 700)); 31 | 32 | sierpinski_carpet(5, &root) 33 | } 34 | -------------------------------------------------------------------------------- /examples/slc-temp.rs: -------------------------------------------------------------------------------- 1 | use plotters::coord::IntoMonthly; 2 | use plotters::prelude::*; 3 | 4 | use chrono::{Datelike, TimeZone, Utc}; 5 | 6 | use std::error::Error; 7 | 8 | fn main() -> Result<(), Box> { 9 | let root = 10 | BitMapBackend::new("plotters-doc-data/slc-temp.png", (1024, 768)).into_drawing_area(); 11 | 12 | root.fill(&WHITE)?; 13 | 14 | let mut chart = ChartBuilder::on(&root) 15 | .margin(10) 16 | .caption( 17 | "Monthly Average Temperate in Salt Lake City, UT", 18 | ("sans-serif", 40), 19 | ) 20 | .set_label_area_size(LabelAreaPosition::Left, 60) 21 | .set_label_area_size(LabelAreaPosition::Right, 60) 22 | .set_label_area_size(LabelAreaPosition::Bottom, 40) 23 | .build_ranged( 24 | (Utc.ymd(2010, 1, 1)..Utc.ymd(2018, 12, 1)).monthly(), 25 | 14.0..104.0, 26 | )? 27 | .set_secondary_coord( 28 | (Utc.ymd(2010, 1, 1)..Utc.ymd(2018, 12, 1)).monthly(), 29 | -10.0..40.0, 30 | ); 31 | 32 | chart 33 | .configure_mesh() 34 | .disable_x_mesh() 35 | .disable_y_mesh() 36 | .x_labels(30) 37 | .x_label_formatter(&|d| format!("{}-{}", d.year(), d.month())) 38 | .y_desc("Average Temp (F)") 39 | .draw()?; 40 | chart 41 | .configure_secondary_axes() 42 | .y_desc("Average Temp (C)") 43 | .draw()?; 44 | 45 | chart.draw_series(LineSeries::new( 46 | DATA.iter().map(|(y, m, t)| (Utc.ymd(*y, *m, 1), *t)), 47 | &BLUE, 48 | ))?; 49 | 50 | chart.draw_series( 51 | DATA.iter() 52 | .map(|(y, m, t)| Circle::new((Utc.ymd(*y, *m, 1), *t), 3, BLUE.filled())), 53 | )?; 54 | 55 | Ok(()) 56 | } 57 | 58 | const DATA: [(i32, u32, f64); 12 * 9] = [ 59 | (2010, 1, 32.4), 60 | (2010, 2, 37.5), 61 | (2010, 3, 44.5), 62 | (2010, 4, 50.3), 63 | (2010, 5, 55.0), 64 | (2010, 6, 70.0), 65 | (2010, 7, 78.7), 66 | (2010, 8, 76.5), 67 | (2010, 9, 68.9), 68 | (2010, 10, 56.3), 69 | (2010, 11, 40.3), 70 | (2010, 12, 36.5), 71 | (2011, 1, 28.8), 72 | (2011, 2, 35.1), 73 | (2011, 3, 45.5), 74 | (2011, 4, 48.9), 75 | (2011, 5, 55.1), 76 | (2011, 6, 68.8), 77 | (2011, 7, 77.9), 78 | (2011, 8, 78.4), 79 | (2011, 9, 68.2), 80 | (2011, 10, 55.0), 81 | (2011, 11, 41.5), 82 | (2011, 12, 31.0), 83 | (2012, 1, 35.6), 84 | (2012, 2, 38.1), 85 | (2012, 3, 49.1), 86 | (2012, 4, 56.1), 87 | (2012, 5, 63.4), 88 | (2012, 6, 73.0), 89 | (2012, 7, 79.0), 90 | (2012, 8, 79.0), 91 | (2012, 9, 68.8), 92 | (2012, 10, 54.9), 93 | (2012, 11, 45.2), 94 | (2012, 12, 34.9), 95 | (2013, 1, 19.7), 96 | (2013, 2, 31.1), 97 | (2013, 3, 46.2), 98 | (2013, 4, 49.8), 99 | (2013, 5, 61.3), 100 | (2013, 6, 73.3), 101 | (2013, 7, 80.3), 102 | (2013, 8, 77.2), 103 | (2013, 9, 68.3), 104 | (2013, 10, 52.0), 105 | (2013, 11, 43.2), 106 | (2013, 12, 25.7), 107 | (2014, 1, 31.5), 108 | (2014, 2, 39.3), 109 | (2014, 3, 46.4), 110 | (2014, 4, 52.5), 111 | (2014, 5, 63.0), 112 | (2014, 6, 71.3), 113 | (2014, 7, 81.0), 114 | (2014, 8, 75.3), 115 | (2014, 9, 70.0), 116 | (2014, 10, 58.6), 117 | (2014, 11, 42.1), 118 | (2014, 12, 38.0), 119 | (2015, 1, 35.3), 120 | (2015, 2, 45.2), 121 | (2015, 3, 50.9), 122 | (2015, 4, 54.3), 123 | (2015, 5, 60.5), 124 | (2015, 6, 77.1), 125 | (2015, 7, 76.2), 126 | (2015, 8, 77.3), 127 | (2015, 9, 70.4), 128 | (2015, 10, 60.6), 129 | (2015, 11, 40.9), 130 | (2015, 12, 32.4), 131 | (2016, 1, 31.5), 132 | (2016, 2, 35.1), 133 | (2016, 3, 49.1), 134 | (2016, 4, 55.1), 135 | (2016, 5, 60.9), 136 | (2016, 6, 76.9), 137 | (2016, 7, 80.0), 138 | (2016, 8, 77.0), 139 | (2016, 9, 67.1), 140 | (2016, 10, 59.1), 141 | (2016, 11, 47.4), 142 | (2016, 12, 31.8), 143 | (2017, 1, 29.4), 144 | (2017, 2, 42.4), 145 | (2017, 3, 51.7), 146 | (2017, 4, 51.7), 147 | (2017, 5, 62.5), 148 | (2017, 6, 74.8), 149 | (2017, 7, 81.3), 150 | (2017, 8, 78.1), 151 | (2017, 9, 65.7), 152 | (2017, 10, 52.5), 153 | (2017, 11, 49.0), 154 | (2017, 12, 34.4), 155 | (2018, 1, 38.1), 156 | (2018, 2, 37.5), 157 | (2018, 3, 45.4), 158 | (2018, 4, 54.6), 159 | (2018, 5, 64.0), 160 | (2018, 6, 74.9), 161 | (2018, 7, 82.5), 162 | (2018, 8, 78.1), 163 | (2018, 9, 71.9), 164 | (2018, 10, 53.2), 165 | (2018, 11, 39.7), 166 | (2018, 12, 33.6), 167 | ]; 168 | -------------------------------------------------------------------------------- /examples/snowflake.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | fn snowflake_iter(points: &[(f64, f64)]) -> Vec<(f64, f64)> { 4 | let mut ret = vec![]; 5 | for i in 0..points.len() { 6 | let (start, end) = (points[i], points[(i + 1) % points.len()]); 7 | let t = ((end.0 - start.0) / 3.0, (end.1 - start.1) / 3.0); 8 | let s = ( 9 | t.0 * 0.5 - t.1 * (0.75f64).sqrt(), 10 | t.1 * 0.5 + (0.75f64).sqrt() * t.0, 11 | ); 12 | ret.push(start); 13 | ret.push((start.0 + t.0, start.1 + t.1)); 14 | ret.push((start.0 + t.0 + s.0, start.1 + t.1 + s.1)); 15 | ret.push((start.0 + t.0 * 2.0, start.1 + t.1 * 2.0)); 16 | } 17 | ret 18 | } 19 | 20 | fn main() -> Result<(), Box> { 21 | let root = 22 | BitMapBackend::new("plotters-doc-data/snowflake.png", (1024, 768)).into_drawing_area(); 23 | 24 | root.fill(&WHITE)?; 25 | 26 | let mut chart = ChartBuilder::on(&root) 27 | .caption("Koch's Snowflake", ("sans-serif", 50)) 28 | .build_ranged(-2.0..2.0, -1.5..1.5)?; 29 | 30 | let mut snowflake_vertices = { 31 | let mut current: Vec<(f64, f64)> = vec![ 32 | (0.0, 1.0), 33 | ((3.0f64).sqrt() / 2.0, -0.5), 34 | (-(3.0f64).sqrt() / 2.0, -0.5), 35 | ]; 36 | for _ in 0..6 { 37 | current = snowflake_iter(¤t[..]); 38 | } 39 | current 40 | }; 41 | 42 | chart.draw_series(std::iter::once(Polygon::new( 43 | snowflake_vertices.clone(), 44 | &RED.mix(0.2), 45 | )))?; 46 | snowflake_vertices.push(snowflake_vertices[0]); 47 | chart.draw_series(std::iter::once(PathElement::new(snowflake_vertices, &RED)))?; 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /examples/stock.rs: -------------------------------------------------------------------------------- 1 | use chrono::offset::{Local, TimeZone}; 2 | use chrono::{Date, Duration}; 3 | use plotters::prelude::*; 4 | fn parse_time(t: &str) -> Date { 5 | Local 6 | .datetime_from_str(&format!("{} 0:0", t), "%Y-%m-%d %H:%M") 7 | .unwrap() 8 | .date() 9 | } 10 | fn main() -> Result<(), Box> { 11 | let data = get_data(); 12 | let root = BitMapBackend::new("plotters-doc-data/stock.png", (1024, 768)).into_drawing_area(); 13 | root.fill(&WHITE)?; 14 | 15 | let (to_date, from_date) = ( 16 | parse_time(&data[0].0) + Duration::days(1), 17 | parse_time(&data[29].0) - Duration::days(1), 18 | ); 19 | 20 | let mut chart = ChartBuilder::on(&root) 21 | .x_label_area_size(40) 22 | .y_label_area_size(40) 23 | .caption("MSFT Stock Price", ("sans-serif", 50.0).into_font()) 24 | .build_ranged(from_date..to_date, 110f32..135f32)?; 25 | 26 | chart.configure_mesh().line_style_2(&WHITE).draw()?; 27 | 28 | chart.draw_series( 29 | data.iter() 30 | .map(|x| CandleStick::new(parse_time(x.0), x.1, x.2, x.3, x.4, &GREEN, &RED, 15)), 31 | )?; 32 | 33 | Ok(()) 34 | } 35 | 36 | fn get_data() -> Vec<(&'static str, f32, f32, f32, f32)> { 37 | return vec![ 38 | ("2019-04-25", 130.0600, 131.3700, 128.8300, 129.1500), 39 | ("2019-04-24", 125.7900, 125.8500, 124.5200, 125.0100), 40 | ("2019-04-23", 124.1000, 125.5800, 123.8300, 125.4400), 41 | ("2019-04-22", 122.6200, 124.0000, 122.5700, 123.7600), 42 | ("2019-04-18", 122.1900, 123.5200, 121.3018, 123.3700), 43 | ("2019-04-17", 121.2400, 121.8500, 120.5400, 121.7700), 44 | ("2019-04-16", 121.6400, 121.6500, 120.1000, 120.7700), 45 | ("2019-04-15", 120.9400, 121.5800, 120.5700, 121.0500), 46 | ("2019-04-12", 120.6400, 120.9800, 120.3700, 120.9500), 47 | ("2019-04-11", 120.5400, 120.8500, 119.9200, 120.3300), 48 | ("2019-04-10", 119.7600, 120.3500, 119.5400, 120.1900), 49 | ("2019-04-09", 118.6300, 119.5400, 118.5800, 119.2800), 50 | ("2019-04-08", 119.8100, 120.0200, 118.6400, 119.9300), 51 | ("2019-04-05", 119.3900, 120.2300, 119.3700, 119.8900), 52 | ("2019-04-04", 120.1000, 120.2300, 118.3800, 119.3600), 53 | ("2019-04-03", 119.8600, 120.4300, 119.1500, 119.9700), 54 | ("2019-04-02", 119.0600, 119.4800, 118.5200, 119.1900), 55 | ("2019-04-01", 118.9500, 119.1085, 118.1000, 119.0200), 56 | ("2019-03-29", 118.0700, 118.3200, 116.9600, 117.9400), 57 | ("2019-03-28", 117.4400, 117.5800, 116.1300, 116.9300), 58 | ("2019-03-27", 117.8750, 118.2100, 115.5215, 116.7700), 59 | ("2019-03-26", 118.6200, 118.7050, 116.8500, 117.9100), 60 | ("2019-03-25", 116.5600, 118.0100, 116.3224, 117.6600), 61 | ("2019-03-22", 119.5000, 119.5900, 117.0400, 117.0500), 62 | ("2019-03-21", 117.1350, 120.8200, 117.0900, 120.2200), 63 | ("2019-03-20", 117.3900, 118.7500, 116.7100, 117.5200), 64 | ("2019-03-19", 118.0900, 118.4400, 116.9900, 117.6500), 65 | ("2019-03-18", 116.1700, 117.6100, 116.0500, 117.5700), 66 | ("2019-03-15", 115.3400, 117.2500, 114.5900, 115.9100), 67 | ("2019-03-14", 114.5400, 115.2000, 114.3300, 114.5900), 68 | ]; 69 | } 70 | -------------------------------------------------------------------------------- /examples/two-scales.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | fn main() -> Result<(), Box> { 4 | let root = 5 | BitMapBackend::new("plotters-doc-data/twoscale.png", (1024, 768)).into_drawing_area(); 6 | root.fill(&WHITE)?; 7 | 8 | let mut chart = ChartBuilder::on(&root) 9 | .x_label_area_size(35) 10 | .y_label_area_size(40) 11 | .right_y_label_area_size(40) 12 | .margin(5) 13 | .caption("Dual Y-Axis Example", ("sans-serif", 50.0).into_font()) 14 | .build_ranged(0f32..10f32, LogRange(0.1f32..1e10f32))? 15 | .set_secondary_coord(0f32..10f32, -1.0f32..1.0f32); 16 | 17 | chart 18 | .configure_mesh() 19 | .disable_x_mesh() 20 | .disable_y_mesh() 21 | .y_desc("Log Scale") 22 | .y_label_formatter(&|x| format!("{:e}", x)) 23 | .draw()?; 24 | 25 | chart 26 | .configure_secondary_axes() 27 | .y_desc("Linear Scale") 28 | .draw()?; 29 | 30 | chart 31 | .draw_series(LineSeries::new( 32 | (0..=100).map(|x| (x as f32 / 10.0, (1.02f32).powf(x as f32 * x as f32 / 10.0))), 33 | &BLUE, 34 | ))? 35 | .label("y = 1.02^x^2") 36 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE)); 37 | 38 | chart 39 | .draw_secondary_series(LineSeries::new( 40 | (0..=100).map(|x| (x as f32 / 10.0, (x as f32 / 5.0).sin())), 41 | &RED, 42 | ))? 43 | .label("y = sin(2x)") 44 | .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED)); 45 | 46 | chart 47 | .configure_series_labels() 48 | .background_style(&RGBColor(128, 128, 128)) 49 | .draw()?; 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /examples/wasm-demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-demo" 3 | version = "0.1.0" 4 | authors = ["Hao Hou "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type=["cdylib"] 9 | 10 | [dependencies] 11 | plotters = {path = "../.."} 12 | wasm-bindgen = "0.2" 13 | wee_alloc = "*" 14 | web-sys = { version = "0.3.4", features = ["HtmlCanvasElement"] } 15 | 16 | [profile.release] 17 | lto = true 18 | -------------------------------------------------------------------------------- /examples/wasm-demo/README.md: -------------------------------------------------------------------------------- 1 | # The Minimal Demo for Plotters + WASM 2 | 3 | To build the demo you need [wasm-pack](https://rustwasm.github.io/docs/book/game-of-life/setup.html). 4 | 5 | Then you can run it locally either using `npm` and `webpack-dev-server` or 6 | just with static web server. 7 | 8 | The following script will install needed software and run the server via `npm`. 9 | ``` 10 | ./start-server.sh 11 | ``` 12 | 13 | For Windows users without Bash, `start-server.bat` can be used to 14 | launch the server. 15 | 16 | ``` 17 | start-server.bat 18 | ``` 19 | 20 | ## Developing with NPM 21 | Please use [rust-wasm guide](https://rustwasm.github.io/docs/book/game-of-life/setup.html) for initial setup . 22 | Then you can run the demo locally using `npm`: 23 | ```bash 24 | wasm-pack build 25 | cd www 26 | npm install 27 | npm start 28 | ``` 29 | 30 | This will start a dev server which will automatically reload your page 31 | whenever you change anything in `www` directory. To update `rust` code 32 | call `wasm-pack build` manually. 33 | 34 | ## Developing without dependenices 35 | If you don't want to use `npm` here's how you can run the example 36 | using any web server. We are using rust [basic-http-server](https://github.com/brson/basic-http-server), but 37 | any web server will do. 38 | 39 | ```bash 40 | # Install web server (instead you can use your local nginx for example) 41 | cargo install basic-http-server 42 | wasm-pack build --target web # Note `--target web` 43 | basic-http-server 44 | ``` 45 | 46 | Then open http://127.0.0.1:4000/www 47 | -------------------------------------------------------------------------------- /examples/wasm-demo/src/func_plot.rs: -------------------------------------------------------------------------------- 1 | use crate::DrawResult; 2 | use plotters::prelude::*; 3 | 4 | /// Draw power function f(x) = x^power. 5 | pub fn draw(canvas_id: &str, power: i32) -> DrawResult Option<(f32, f32)>> { 6 | let backend = CanvasBackend::new(canvas_id).expect("cannot find canvas"); 7 | let root = backend.into_drawing_area(); 8 | let font: FontDesc = ("sans-serif", 20.0).into(); 9 | 10 | root.fill(&WHITE)?; 11 | 12 | let mut chart = ChartBuilder::on(&root) 13 | .caption(format!("y=x^{}", power), font) 14 | .x_label_area_size(30) 15 | .y_label_area_size(30) 16 | .build_ranged(-1f32..1f32, -1.2f32..1.2f32)?; 17 | 18 | chart.configure_mesh().x_labels(3).y_labels(3).draw()?; 19 | 20 | chart.draw_series(LineSeries::new( 21 | (-50..=50) 22 | .map(|x| x as f32 / 50.0) 23 | .map(|x| (x, x.powf(power as f32))), 24 | &RED, 25 | ))?; 26 | 27 | root.present()?; 28 | return Ok(chart.into_coord_trans()); 29 | } 30 | -------------------------------------------------------------------------------- /examples/wasm-demo/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use web_sys::HtmlCanvasElement; 3 | 4 | mod func_plot; 5 | mod mandelbrot; 6 | 7 | #[global_allocator] 8 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 9 | 10 | /// Type alias for the result of a drawing function. 11 | pub type DrawResult = Result>; 12 | 13 | /// Type used on the JS side to convert screen coordinates to chart 14 | /// coordinates. 15 | #[wasm_bindgen] 16 | pub struct Chart { 17 | convert: Box Option<(f64, f64)>>, 18 | } 19 | 20 | /// Result of screen to chart coordinates conversion. 21 | #[wasm_bindgen] 22 | pub struct Point { 23 | pub x: f64, 24 | pub y: f64, 25 | } 26 | 27 | #[wasm_bindgen] 28 | impl Chart { 29 | /// Draw provided power function on the canvas element using it's id. 30 | /// Return `Chart` struct suitable for coordinate conversion. 31 | pub fn power(canvas_id: &str, power: i32) -> Result { 32 | let map_coord = func_plot::draw(canvas_id, power).map_err(|err| err.to_string())?; 33 | Ok(Chart { 34 | convert: Box::new(move |coord| map_coord(coord).map(|(x, y)| (x.into(), y.into()))), 35 | }) 36 | } 37 | 38 | /// Draw Mandelbrot set on the provided canvas element. 39 | /// Return `Chart` struct suitable for coordinate conversion. 40 | pub fn mandelbrot(canvas: HtmlCanvasElement) -> Result { 41 | let map_coord = mandelbrot::draw(canvas).map_err(|err| err.to_string())?; 42 | Ok(Chart { 43 | convert: Box::new(map_coord), 44 | }) 45 | } 46 | 47 | /// This function can be used to convert screen coordinates to 48 | /// chart coordinates. 49 | pub fn coord(&self, x: i32, y: i32) -> Option { 50 | (self.convert)((x, y)).map(|(x, y)| Point { x, y }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/wasm-demo/src/mandelbrot.rs: -------------------------------------------------------------------------------- 1 | use crate::DrawResult; 2 | use plotters::prelude::*; 3 | use std::ops::Range; 4 | use web_sys::HtmlCanvasElement; 5 | 6 | /// Draw Mandelbrot set 7 | pub fn draw(element: HtmlCanvasElement) -> DrawResult Option<(f64, f64)>> { 8 | let backend = CanvasBackend::with_canvas_object(element).unwrap(); 9 | 10 | let root = backend.into_drawing_area(); 11 | root.fill(&WHITE)?; 12 | 13 | let mut chart = ChartBuilder::on(&root) 14 | .margin(20) 15 | .x_label_area_size(10) 16 | .y_label_area_size(10) 17 | .build_ranged(-2.1..0.6, -1.2..1.2)?; 18 | 19 | chart 20 | .configure_mesh() 21 | .disable_x_mesh() 22 | .disable_y_mesh() 23 | .draw()?; 24 | 25 | let plotting_area = chart.plotting_area(); 26 | 27 | let range = plotting_area.get_pixel_range(); 28 | let (pw, ph) = (range.0.end - range.0.start, range.1.end - range.1.start); 29 | let (xr, yr) = (chart.x_range(), chart.y_range()); 30 | 31 | for (x, y, c) in mandelbrot_set(xr, yr, (pw as usize, ph as usize), 100) { 32 | if c != 100 { 33 | plotting_area.draw_pixel((x, y), &HSLColor(c as f64 / 100.0, 1.0, 0.5))?; 34 | } else { 35 | plotting_area.draw_pixel((x, y), &BLACK)?; 36 | } 37 | } 38 | 39 | root.present()?; 40 | return Ok(Box::new(chart.into_coord_trans())); 41 | } 42 | 43 | fn mandelbrot_set( 44 | real: Range, 45 | complex: Range, 46 | samples: (usize, usize), 47 | max_iter: usize, 48 | ) -> impl Iterator { 49 | let step = ( 50 | (real.end - real.start) / samples.0 as f64, 51 | (complex.end - complex.start) / samples.1 as f64, 52 | ); 53 | return (0..(samples.0 * samples.1)).map(move |k| { 54 | let c = ( 55 | real.start + step.0 * (k % samples.0) as f64, 56 | complex.start + step.1 * (k / samples.0) as f64, 57 | ); 58 | let mut z = (0.0, 0.0); 59 | let mut cnt = 0; 60 | while cnt < max_iter && z.0 * z.0 + z.1 * z.1 <= 1e10 { 61 | z = (z.0 * z.0 - z.1 * z.1 + c.0, 2.0 * z.0 * z.1 + c.1); 62 | cnt += 1; 63 | } 64 | return (c.0, c.1, cnt); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /examples/wasm-demo/start-server.bat: -------------------------------------------------------------------------------- 1 | if not exist "www\pkg" mkdir www\pkg 2 | rustup target add wasm32-unknown-unknown 3 | wasm-pack build --release 4 | if errorlevel 1 cargo install wasm-pack 5 | wasm-pack build --release 6 | cd www 7 | npm install 8 | npm start 9 | -------------------------------------------------------------------------------- /examples/wasm-demo/start-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | CONFIG=release 5 | mkdir -p www/pkg 6 | 7 | rustup target add wasm32-unknown-unknown 8 | 9 | if [ -z "$(cargo install --list | grep wasm-pack)" ] 10 | then 11 | cargo install wasm-pack 12 | fi 13 | 14 | if [ "${CONFIG}" = "release" ] 15 | then 16 | wasm-pack build 17 | else 18 | wasm-pack build --release 19 | fi 20 | 21 | cd www 22 | npm install 23 | npm start 24 | -------------------------------------------------------------------------------- /examples/wasm-demo/www/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /examples/wasm-demo/www/bootstrap.js: -------------------------------------------------------------------------------- 1 | init(); 2 | 3 | async function init() { 4 | if (typeof process == "object") { 5 | // We run in the npm/webpack environment. 6 | const [{Chart}, {main, setup}] = await Promise.all([ 7 | import("wasm-demo"), 8 | import("./index.js"), 9 | ]); 10 | setup(Chart); 11 | main(); 12 | } else { 13 | const [{Chart, default: init}, {main, setup}] = await Promise.all([ 14 | import("../pkg/wasm_demo.js"), 15 | import("./index.js"), 16 | ]); 17 | await init(); 18 | setup(Chart); 19 | main(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/wasm-demo/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Plotters WebAssembly Demo 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Plotters WebAssembly Demo

14 |
15 | 16 |
Loading WebAssembly...
17 |
18 | 19 | 28 |
29 |
30 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/wasm-demo/www/index.js: -------------------------------------------------------------------------------- 1 | // If you only use `npm` you can simply 2 | // import { Chart } from "wasm-demo" and remove `setup` call from `bootstrap.js`. 3 | class Chart {} 4 | 5 | const canvas = document.getElementById("canvas"); 6 | const coord = document.getElementById("coord"); 7 | const plotType = document.getElementById("plot-type"); 8 | const status = document.getElementById("status"); 9 | 10 | let chart = null; 11 | 12 | /** Main entry point */ 13 | export function main() { 14 | setupUI(); 15 | setupCanvas(); 16 | } 17 | 18 | /** This function is used in `bootstrap.js` to setup imports. */ 19 | export function setup(WasmChart) { 20 | Chart = WasmChart; 21 | } 22 | 23 | /** Add event listeners. */ 24 | function setupUI() { 25 | status.innerText = "WebAssembly loaded!"; 26 | plotType.addEventListener("change", updatePlot); 27 | window.addEventListener("resize", setupCanvas); 28 | window.addEventListener("mousemove", onMouseMove); 29 | } 30 | 31 | /** Setup canvas to properly handle high DPI and redraw current plot. */ 32 | function setupCanvas() { 33 | const dpr = window.devicePixelRatio || 1; 34 | const aspectRatio = canvas.width / canvas.height; 35 | const size = Math.min(canvas.width, canvas.parentNode.offsetWidth); 36 | canvas.style.width = size + "px"; 37 | canvas.style.height = size / aspectRatio + "px"; 38 | canvas.width = size * dpr; 39 | canvas.height = size / aspectRatio * dpr; 40 | canvas.getContext("2d").scale(dpr, dpr); 41 | updatePlot(); 42 | } 43 | 44 | /** Update displayed coordinates. */ 45 | function onMouseMove(event) { 46 | if (chart) { 47 | const point = chart.coord(event.offsetX, event.offsetY); 48 | coord.innerText = (point) 49 | ? `(${point.x.toFixed(3)}, ${point.y.toFixed(3)})` 50 | : "Mouse pointer is out of range"; 51 | } 52 | } 53 | 54 | /** Redraw currently selected plot. */ 55 | function updatePlot() { 56 | const selected = plotType.selectedOptions[0]; 57 | status.innerText = `Rendering ${selected.innerText}...`; 58 | chart = null; 59 | const start = performance.now(); 60 | chart = (selected.value == "mandelbrot") 61 | ? Chart.mandelbrot(canvas) 62 | : Chart.power("canvas", Number(selected.value)); 63 | const end = performance.now(); 64 | status.innerText = `Rendered ${selected.innerText} in ${Math.ceil(end - start)}ms`; 65 | } 66 | -------------------------------------------------------------------------------- /examples/wasm-demo/www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plotters-wasm-demo", 3 | "version": "0.1.0", 4 | "description": "Plotters WASM Demo", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.config.js", 8 | "start": "webpack-dev-server" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/38/plotters.git" 13 | }, 14 | "keywords": [ 15 | "webassembly", 16 | "wasm", 17 | "rust", 18 | "webpack", 19 | "visualization" 20 | ], 21 | "author": "Plotters Developers", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/38/plotters/issues" 25 | }, 26 | "homepage": "https://github.com/38/plotters", 27 | "dependencies": { 28 | "wasm-demo": "file:../pkg" 29 | }, 30 | "devDependencies": { 31 | "webpack": "^4.29.3", 32 | "webpack-cli": "^3.1.0", 33 | "webpack-dev-server": "^3.1.5", 34 | "copy-webpack-plugin": "^5.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/wasm-demo/www/style.css: -------------------------------------------------------------------------------- 1 | html, body, main { 2 | width: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | margin: auto; 9 | max-width: 800px; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | @media (max-width: 800px) { 15 | body { 16 | padding: 10px; 17 | box-sizing: border-box; 18 | } 19 | } 20 | 21 | main { 22 | display: flex; 23 | flex-direction: column; 24 | align-items: center; 25 | } 26 | 27 | #coord, #status { 28 | color: grey; 29 | font-size: 10px; 30 | height: 15px 31 | } 32 | 33 | #control { 34 | margin-top: 1em; 35 | } 36 | 37 | #control label { 38 | font-weight: bold; 39 | margin-right: 1em; 40 | } 41 | 42 | #control select { 43 | padding: 0.25em 0.5em; 44 | } 45 | 46 | footer { 47 | margin-top: 2em; 48 | font-size: 12px; 49 | text-align: center; 50 | } 51 | -------------------------------------------------------------------------------- /examples/wasm-demo/www/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: "./bootstrap.js", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "bootstrap.js", 9 | }, 10 | mode: "development", 11 | plugins: [ 12 | new CopyWebpackPlugin(['index.html']) 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ $(git diff | wc -l) != 0 ] 5 | then 6 | echo "Commit your change before publish!" 7 | exit 1 8 | fi 9 | 10 | if [ -z "$1" ] 11 | then 12 | PART_ID=0 13 | else 14 | PART_ID=$1 15 | fi 16 | ROOT=$(dirname $(readlink -f $0)) 17 | OLD_VERSION=$(cat ${ROOT}/doc-template/latest_version) 18 | NEW_VERSION=$(awk -F. 'BEGIN{idx=3-'${PART_ID}'}{$idx+=1; for(i=idx+1; i <= 3; i++) $i=0; print $1"."$2"."$3}' ${ROOT}/doc-template/latest_version) 19 | echo "Publishing new version ${OLD_VERSION} -> ${NEW_VERSION}" 20 | echo ${NEW_VERSION} > doc-template/latest_version 21 | doc-template/update_readme.sh 22 | echo ${OLD_VERSION} > doc-template/latest_version 23 | cargo fmt 24 | 25 | PATTERN=$(echo ^version = \"${OLD_VERSION}\"\$ | sed 's/\./\\./g') 26 | DATE=$(date "+%Y-%m-%d") 27 | sed -i "s/${PATTERN}/version = \"${NEW_VERSION}\"/g" Cargo.toml 28 | PATTERN=$(echo ${NEW_VERSION} | sed 's/\./\\./g') 29 | sed -i "s/^## Plotters .* (?) *\$/## Plotters ${NEW_VERSION} ($DATE)/g" CHANGELOG.md 30 | 31 | echo ${NEW_VERSION} > doc-template/latest_version 32 | 33 | git add -u . 34 | git commit -m "Bump version number from ${OLD_VERSION} to ${NEW_VERSION}" 35 | git tag -a "v${NEW_VERSION}" -m "Plotters ${NEW_VERSION} release" 36 | 37 | cargo publish 38 | git push origin 39 | git push origin "v${NEW_VERSION}" 40 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugcoday/plotchart/860ccbc0e8677b23e8291af6d84187fc59907f85/src/.DS_Store -------------------------------------------------------------------------------- /src/chart/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The high-level plotting abstractions. 3 | 4 | Plotters uses `ChartContext`, a thin layer on the top of `DrawingArea`, to provide 5 | high-level chart specific drawing functionalities, like, mesh line, coordinate label 6 | and other common components for the data chart. 7 | 8 | To draw a series, `ChartContext::draw_series` is used to draw a series on the chart. 9 | In Plotters, a series is abstracted as an iterator of elements. 10 | 11 | `ChartBuilder` is used to construct a chart. To learn more detailed information, check the 12 | detailed description for each struct. 13 | */ 14 | 15 | mod builder; 16 | mod context; 17 | mod dual_coord; 18 | mod mesh; 19 | mod series; 20 | 21 | pub use builder::{ChartBuilder, LabelAreaPosition}; 22 | pub use context::{ChartContext, ChartState, SeriesAnno}; 23 | pub use dual_coord::{DualCoordChartContext, DualCoordChartState}; 24 | pub use mesh::MeshStyle; 25 | pub use series::{SeriesLabelPosition, SeriesLabelStyle}; 26 | -------------------------------------------------------------------------------- /src/chart/series.rs: -------------------------------------------------------------------------------- 1 | use super::ChartContext; 2 | use crate::coord::CoordTranslate; 3 | use crate::drawing::backend::{BackendCoord, DrawingErrorKind}; 4 | use crate::drawing::{DrawingAreaErrorKind, DrawingBackend}; 5 | use crate::element::{EmptyElement, IntoDynElement, MultiLineText, Rectangle}; 6 | use crate::style::{IntoFont, IntoTextStyle, ShapeStyle, SizeDesc, TextStyle, TRANSPARENT}; 7 | 8 | /// Describes where we want to put the series label 9 | pub enum SeriesLabelPosition { 10 | UpperLeft, 11 | MiddleLeft, 12 | LowerLeft, 13 | UpperMiddle, 14 | MiddleMiddle, 15 | LowerMiddle, 16 | UpperRight, 17 | MiddleRight, 18 | LowerRight, 19 | /// Force the series label drawn at the specific location 20 | Coordinate(i32, i32), 21 | } 22 | 23 | impl SeriesLabelPosition { 24 | fn layout_label_area(&self, label_dim: (i32, i32), area_dim: (u32, u32)) -> (i32, i32) { 25 | use SeriesLabelPosition::*; 26 | ( 27 | match self { 28 | UpperLeft | MiddleLeft | LowerLeft => 5, 29 | UpperMiddle | MiddleMiddle | LowerMiddle => { 30 | (area_dim.0 as i32 - label_dim.0 as i32) / 2 31 | } 32 | UpperRight | MiddleRight | LowerRight => area_dim.0 as i32 - label_dim.0 as i32 - 5, 33 | Coordinate(x, _) => *x, 34 | }, 35 | match self { 36 | UpperLeft | UpperMiddle | UpperRight => 5, 37 | MiddleLeft | MiddleMiddle | MiddleRight => { 38 | (area_dim.1 as i32 - label_dim.1 as i32) / 2 39 | } 40 | LowerLeft | LowerMiddle | LowerRight => area_dim.1 as i32 - label_dim.1 as i32 - 5, 41 | Coordinate(_, y) => *y, 42 | }, 43 | ) 44 | } 45 | } 46 | 47 | /// The struct to specify the series label of a target chart context 48 | pub struct SeriesLabelStyle<'a, 'b, DB: DrawingBackend, CT: CoordTranslate> { 49 | target: &'b mut ChartContext<'a, DB, CT>, 50 | position: SeriesLabelPosition, 51 | legend_area_size: u32, 52 | border_style: ShapeStyle, 53 | background: ShapeStyle, 54 | label_font: Option>, 55 | margin: u32, 56 | } 57 | 58 | impl<'a, 'b, DB: DrawingBackend + 'a, CT: CoordTranslate> SeriesLabelStyle<'a, 'b, DB, CT> { 59 | pub(super) fn new(target: &'b mut ChartContext<'a, DB, CT>) -> Self { 60 | Self { 61 | target, 62 | position: SeriesLabelPosition::MiddleRight, 63 | legend_area_size: 30, 64 | border_style: (&TRANSPARENT).into(), 65 | background: (&TRANSPARENT).into(), 66 | label_font: None, 67 | margin: 10, 68 | } 69 | } 70 | 71 | /// Set the series label positioning style 72 | /// `pos` - The positioning style 73 | pub fn position(&mut self, pos: SeriesLabelPosition) -> &mut Self { 74 | self.position = pos; 75 | self 76 | } 77 | 78 | /// Set the margin of the series label drawing are 79 | /// 80 | /// - `value`: The size specification 81 | pub fn margin(&mut self, value: S) -> &mut Self { 82 | self.margin = value 83 | .in_pixels(&self.target.plotting_area().dim_in_pixel()) 84 | .max(0) as u32; 85 | self 86 | } 87 | 88 | /// Set the size of legend area 89 | /// `size` - The size of legend area in pixel 90 | pub fn legend_area_size(&mut self, size: S) -> &mut Self { 91 | let size = size 92 | .in_pixels(&self.target.plotting_area().dim_in_pixel()) 93 | .max(0) as u32; 94 | self.legend_area_size = size; 95 | self 96 | } 97 | 98 | /// Set the style of the label series area 99 | /// `style` - The style of the border 100 | pub fn border_style>(&mut self, style: S) -> &mut Self { 101 | self.border_style = style.into(); 102 | self 103 | } 104 | 105 | /// Set the background style 106 | /// `style` - The style of the border 107 | pub fn background_style>(&mut self, style: S) -> &mut Self { 108 | self.background = style.into(); 109 | self 110 | } 111 | 112 | /// Set the series label font 113 | /// `font` - The font 114 | pub fn label_font>(&mut self, font: F) -> &mut Self { 115 | self.label_font = Some(font.into_text_style(&self.target.plotting_area().dim_in_pixel())); 116 | self 117 | } 118 | 119 | /// Draw the series label area 120 | pub fn draw(&mut self) -> Result<(), DrawingAreaErrorKind> { 121 | let drawing_area = self.target.plotting_area().strip_coord_spec(); 122 | 123 | // TODO: Issue #68 Currently generic font family doesn't load on OSX, change this after the issue 124 | // resolved 125 | let default_font = ("宋体", 12).into_font(); 126 | let default_style: TextStyle = default_font.into(); 127 | 128 | let font = { 129 | let mut temp = None; 130 | std::mem::swap(&mut self.label_font, &mut temp); 131 | temp.unwrap_or(default_style) 132 | }; 133 | 134 | let mut label_element = MultiLineText::<_, &str>::new((0, 0), &font); 135 | let mut funcs = vec![]; 136 | 137 | for anno in self.target.series_anno.iter() { 138 | let label_text = anno.get_label(); 139 | let draw_func = anno.get_draw_func(); 140 | 141 | if label_text == "" && draw_func.is_none() { 142 | continue; 143 | } 144 | 145 | funcs.push( 146 | draw_func.unwrap_or_else(|| &|p: BackendCoord| EmptyElement::at(p).into_dyn()), 147 | ); 148 | label_element.push_line(label_text); 149 | } 150 | 151 | let (mut w, mut h) = label_element 152 | .estimate_dimension() 153 | .map_err(|e| DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(e)))?; 154 | 155 | let margin = self.margin as i32; 156 | 157 | w += self.legend_area_size as i32 + margin * 2; 158 | h += margin * 2; 159 | 160 | let (area_w, area_h) = drawing_area.dim_in_pixel(); 161 | 162 | let (label_x, label_y) = self.position.layout_label_area((w, h), (area_w, area_h)); 163 | 164 | label_element.relocate(( 165 | label_x + self.legend_area_size as i32 + margin, 166 | label_y + margin, 167 | )); 168 | 169 | drawing_area.draw(&Rectangle::new( 170 | [(label_x, label_y), (label_x + w, label_y + h)], 171 | self.background.filled(), 172 | ))?; 173 | drawing_area.draw(&Rectangle::new( 174 | [(label_x, label_y), (label_x + w, label_y + h)], 175 | self.border_style.clone(), 176 | ))?; 177 | drawing_area.draw(&label_element)?; 178 | 179 | for (((_, y0), (_, y1)), make_elem) in label_element 180 | .compute_line_layout() 181 | .map_err(|e| DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(e)))? 182 | .into_iter() 183 | .zip(funcs.into_iter()) 184 | { 185 | let legend_element = make_elem((label_x + margin, (y0 + y1) / 2)); 186 | drawing_area.draw(&legend_element)?; 187 | } 188 | 189 | Ok(()) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/coord/category.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::ops::Range; 3 | use std::rc::Rc; 4 | 5 | use super::{AsRangedCoord, Ranged}; 6 | 7 | /// The category coordinate 8 | pub struct Category { 9 | name: String, 10 | elements: Rc>, 11 | // i32 type is required for the empty ref (having -1 value) 12 | idx: i32, 13 | } 14 | 15 | impl Clone for Category { 16 | fn clone(&self) -> Self { 17 | Category { 18 | name: self.name.clone(), 19 | elements: Rc::clone(&self.elements), 20 | idx: self.idx, 21 | } 22 | } 23 | } 24 | 25 | impl fmt::Debug for Category { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | let element = &self.elements[self.idx as usize]; 28 | write!(f, "{}", element) 29 | } 30 | } 31 | 32 | impl Category { 33 | /// Create a new category coordinate. 34 | /// 35 | /// - `name`: The name of the category 36 | /// - `elements`: The vector of category elements 37 | /// - **returns** The newly created category coordinate 38 | /// 39 | /// ```rust 40 | /// use plotters::prelude::*; 41 | /// 42 | /// let category = Category::new("color", vec!["red", "green", "blue"]); 43 | /// ``` 44 | pub fn new>(name: S, elements: Vec) -> Self { 45 | Self { 46 | name: name.into(), 47 | elements: Rc::new(elements), 48 | idx: -1, 49 | } 50 | } 51 | 52 | /// Get an element reference (tick) by its value. 53 | /// 54 | /// - `val`: The value of the element 55 | /// - **returns** The optional reference 56 | /// 57 | /// ```rust 58 | /// use plotters::prelude::*; 59 | /// 60 | /// let category = Category::new("color", vec!["red", "green", "blue"]); 61 | /// let red = category.get(&"red"); 62 | /// assert!(red.is_some()); 63 | /// let unknown = category.get(&"unknown"); 64 | /// assert!(unknown.is_none()); 65 | /// ``` 66 | pub fn get(&self, val: &T) -> Option> { 67 | match self.elements.iter().position(|x| x == val) { 68 | Some(pos) => { 69 | let element_ref = Category { 70 | name: self.name.clone(), 71 | elements: Rc::clone(&self.elements), 72 | idx: pos as i32, 73 | }; 74 | Some(element_ref) 75 | } 76 | _ => None, 77 | } 78 | } 79 | 80 | /// Create a full range over the category elements. 81 | /// 82 | /// - **returns** The range including all category elements 83 | /// 84 | /// ```rust 85 | /// use plotters::prelude::*; 86 | /// 87 | /// let category = Category::new("color", vec!["red", "green", "blue"]); 88 | /// let range = category.range(); 89 | /// ``` 90 | pub fn range(&self) -> Self { 91 | self.clone() 92 | } 93 | 94 | /// Get the number of elements in the category. 95 | /// 96 | /// - **returns** The number of elements 97 | /// 98 | /// ```rust 99 | /// use plotters::prelude::*; 100 | /// 101 | /// let category = Category::new("color", vec!["red", "green", "blue"]); 102 | /// assert_eq!(category.len(), 3); 103 | /// ``` 104 | pub fn len(&self) -> usize { 105 | self.elements.len() 106 | } 107 | 108 | /// Returns `true` if the category contains no elements. 109 | /// 110 | /// - **returns** `true` is no elements, otherwise - `false` 111 | /// 112 | /// ```rust 113 | /// use plotters::prelude::*; 114 | /// 115 | /// let category = Category::new("color", vec!["red", "green", "blue"]); 116 | /// assert_eq!(category.is_empty(), false); 117 | /// 118 | /// let category = Category::new("empty", Vec::<&str>::new()); 119 | /// assert_eq!(category.is_empty(), true); 120 | /// ``` 121 | pub fn is_empty(&self) -> bool { 122 | self.elements.is_empty() 123 | } 124 | 125 | /// Get the category name. 126 | /// 127 | /// - **returns** The name of the category 128 | /// 129 | /// ```rust 130 | /// use plotters::prelude::*; 131 | /// 132 | /// let category = Category::new("color", vec!["red", "green", "blue"]); 133 | /// assert_eq!(category.name(), "color"); 134 | /// ``` 135 | pub fn name(&self) -> String { 136 | self.name.clone() 137 | } 138 | } 139 | 140 | impl Ranged for Category { 141 | type ValueType = Category; 142 | 143 | fn range(&self) -> Range> { 144 | let mut left = self.clone(); 145 | let mut right = self.clone(); 146 | left.idx = 0; 147 | right.idx = right.len() as i32 - 1; 148 | left..right 149 | } 150 | 151 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 { 152 | // Add margins to spans as edge values are not applicable to category 153 | let total_span = (self.len() + 1) as f64; 154 | let value_span = f64::from(value.idx + 1); 155 | (f64::from(limit.1 - limit.0) * value_span / total_span) as i32 + limit.0 156 | } 157 | 158 | fn key_points(&self, max_points: usize) -> Vec { 159 | let mut ret = vec![]; 160 | let intervals = (self.len() - 1) as f64; 161 | let elements = &self.elements; 162 | let name = &self.name; 163 | let step = (intervals / max_points as f64 + 1.0) as usize; 164 | for idx in (0..self.len()).step_by(step) { 165 | ret.push(Category { 166 | name: name.clone(), 167 | elements: Rc::clone(&elements), 168 | idx: idx as i32, 169 | }); 170 | } 171 | ret 172 | } 173 | } 174 | 175 | impl AsRangedCoord for Category { 176 | type CoordDescType = Self; 177 | type Value = Category; 178 | } 179 | 180 | #[cfg(test)] 181 | mod test { 182 | use super::*; 183 | 184 | #[test] 185 | fn test_clone_trait() { 186 | let category = Category::new("color", vec!["red", "green", "blue"]); 187 | let red = category.get(&"red").unwrap(); 188 | assert_eq!(red.idx, 0); 189 | let clone = red.clone(); 190 | assert_eq!(clone.idx, 0); 191 | } 192 | 193 | #[test] 194 | fn test_debug_trait() { 195 | let category = Category::new("color", vec!["red", "green", "blue"]); 196 | let red = category.get(&"red").unwrap(); 197 | assert_eq!(format!("{:?}", red), "red"); 198 | } 199 | 200 | #[test] 201 | fn test_ranged_trait() { 202 | let category = Category::new("color", vec!["red", "green", "blue"]); 203 | assert_eq!(category.map(&category.get(&"red").unwrap(), (0, 8)), 2); 204 | assert_eq!(category.map(&category.get(&"green").unwrap(), (0, 8)), 4); 205 | assert_eq!(category.map(&category.get(&"blue").unwrap(), (0, 8)), 6); 206 | assert_eq!(category.key_points(3).len(), 3); 207 | assert_eq!(category.key_points(5).len(), 3); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/coord/logarithmic.rs: -------------------------------------------------------------------------------- 1 | use super::{AsRangedCoord, Ranged, RangedCoordf64}; 2 | use std::marker::PhantomData; 3 | use std::ops::Range; 4 | 5 | /// The trait for the type that is able to be presented in the log scale 6 | pub trait LogScalable: Clone { 7 | /// Make the conversion from the type to the floating point number 8 | fn as_f64(&self) -> f64; 9 | /// Convert a floating point number to the scale 10 | fn from_f64(f: f64) -> Self; 11 | } 12 | 13 | macro_rules! impl_log_scalable { 14 | (i, $t:ty) => { 15 | impl LogScalable for $t { 16 | fn as_f64(&self) -> f64 { 17 | if *self != 0 { 18 | return *self as f64; 19 | } 20 | // If this is an integer, we should allow zero point to be shown 21 | // on the chart, thus we can't map the zero point to inf. 22 | // So we just assigning a value smaller than 1 as the alternative 23 | // of the zero point. 24 | return 0.5; 25 | } 26 | fn from_f64(f: f64) -> $t { 27 | f.round() as $t 28 | } 29 | } 30 | }; 31 | (f, $t:ty) => { 32 | impl LogScalable for $t { 33 | fn as_f64(&self) -> f64 { 34 | *self as f64 35 | } 36 | fn from_f64(f: f64) -> $t { 37 | f as $t 38 | } 39 | } 40 | }; 41 | } 42 | 43 | impl_log_scalable!(i, u8); 44 | impl_log_scalable!(i, u16); 45 | impl_log_scalable!(i, u32); 46 | impl_log_scalable!(i, u64); 47 | impl_log_scalable!(f, f32); 48 | impl_log_scalable!(f, f64); 49 | 50 | /// The decorator type for a range of a log-scaled value 51 | pub struct LogRange(pub Range); 52 | 53 | impl Clone for LogRange { 54 | fn clone(&self) -> Self { 55 | Self(self.0.clone()) 56 | } 57 | } 58 | 59 | impl From> for LogCoord { 60 | fn from(range: LogRange) -> LogCoord { 61 | LogCoord { 62 | linear: (range.0.start.as_f64().ln()..range.0.end.as_f64().ln()).into(), 63 | logic: range.0, 64 | marker: PhantomData, 65 | } 66 | } 67 | } 68 | 69 | impl AsRangedCoord for LogRange { 70 | type CoordDescType = LogCoord; 71 | type Value = V; 72 | } 73 | 74 | /// A log scaled coordinate axis 75 | pub struct LogCoord { 76 | linear: RangedCoordf64, 77 | logic: Range, 78 | marker: PhantomData, 79 | } 80 | 81 | impl Ranged for LogCoord { 82 | type ValueType = V; 83 | 84 | fn map(&self, value: &V, limit: (i32, i32)) -> i32 { 85 | let value = value.as_f64(); 86 | let value = value.max(self.logic.start.as_f64()).ln(); 87 | self.linear.map(&value, limit) 88 | } 89 | 90 | fn key_points(&self, max_points: usize) -> Vec { 91 | let tier_1 = (self.logic.end.as_f64() / self.logic.start.as_f64()) 92 | .log10() 93 | .abs() 94 | .floor() as usize; 95 | let tier_2_density = if max_points < tier_1 { 96 | 0 97 | } else { 98 | let density = 1 + (max_points - tier_1) / tier_1; 99 | let mut exp = 1; 100 | while exp * 10 <= density { 101 | exp *= 10; 102 | } 103 | exp - 1 104 | }; 105 | 106 | let mut multiplier = 10.0; 107 | let mut cnt = 1; 108 | while max_points < tier_1 / cnt { 109 | multiplier *= 10.0; 110 | cnt += 1; 111 | } 112 | 113 | let mut ret = vec![]; 114 | let mut val = (10f64).powf(self.logic.start.as_f64().log10().ceil()); 115 | 116 | while val <= self.logic.end.as_f64() { 117 | ret.push(V::from_f64(val)); 118 | for i in 1..=tier_2_density { 119 | let v = val 120 | * (1.0 121 | + multiplier / f64::from(tier_2_density as u32 + 1) * f64::from(i as u32)); 122 | if v > self.logic.end.as_f64() { 123 | break; 124 | } 125 | ret.push(V::from_f64(v)); 126 | } 127 | val *= multiplier; 128 | } 129 | 130 | ret 131 | } 132 | 133 | fn range(&self) -> Range { 134 | self.logic.clone() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/coord/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Coordinate system abstractions. 3 | 4 | Coordinate systems can be attached to drawing areas. By doing so, 5 | the drawing area can draw elements in the guest coordinate system. 6 | `DrawingArea::apply_coord_spec` is used to attach new coordinate system 7 | to the drawing area. 8 | 9 | `CoordTranslate` is the trait required by `DrawingArea::apply_coord_spec`. It provides 10 | the forward coordinate translation: from the logic coordinate to the pixel-based absolute 11 | backend coordinate system. 12 | 13 | When the coordinate type implements `ReverseCoordTranslate`, 14 | the backward translation is possible, which allows mapping pixel-based coordinate into 15 | the logic coordinate. It's not usually used for static figure rendering, but may be useful 16 | for a interactive figure. 17 | 18 | `RangedCoord` is the 2D cartesian coordinate system that has two `Ranged` axis. 19 | A ranged axis can be logarithmic and by applying an logarithmic axis, the figure is logarithmic scale. 20 | Also, the ranged axis can be deserted, and this is required by the histogram series. 21 | 22 | */ 23 | use crate::drawing::backend::BackendCoord; 24 | 25 | mod category; 26 | #[cfg(feature = "chrono")] 27 | mod datetime; 28 | mod logarithmic; 29 | mod numeric; 30 | mod ranged; 31 | 32 | #[cfg(feature = "chrono")] 33 | pub use datetime::{IntoMonthly, IntoYearly, RangedDate, RangedDateTime, RangedDuration}; 34 | pub use numeric::{ 35 | RangedCoordf32, RangedCoordf64, RangedCoordi128, RangedCoordi32, RangedCoordi64, 36 | RangedCoordu128, RangedCoordu32, RangedCoordu64, 37 | }; 38 | pub use ranged::{ 39 | AsRangedCoord, DiscreteRanged, IntoCentric, IntoPartialAxis, MeshLine, Ranged, RangedCoord, 40 | ReversibleRanged, 41 | }; 42 | 43 | pub use ranged::make_partial_axis; 44 | 45 | pub use logarithmic::{LogCoord, LogRange, LogScalable}; 46 | 47 | pub use numeric::group_integer_by::{GroupBy, ToGroupByRange}; 48 | use std::rc::Rc; 49 | use std::sync::Arc; 50 | 51 | pub use category::Category; 52 | 53 | /// The trait that translates some customized object to the backend coordinate 54 | pub trait CoordTranslate { 55 | type From; 56 | 57 | /// Translate the guest coordinate to the guest coordinate 58 | fn translate(&self, from: &Self::From) -> BackendCoord; 59 | } 60 | 61 | impl CoordTranslate for Rc { 62 | type From = T::From; 63 | 64 | fn translate(&self, from: &Self::From) -> BackendCoord { 65 | self.as_ref().translate(from) 66 | } 67 | } 68 | 69 | impl CoordTranslate for Arc { 70 | type From = T::From; 71 | 72 | fn translate(&self, from: &Self::From) -> BackendCoord { 73 | self.as_ref().translate(from) 74 | } 75 | } 76 | 77 | /// The trait indicates that the coordinate system supports reverse transform 78 | /// This is useful when we need an interactive plot, thus we need to map the event 79 | /// from the backend coordinate to the logical coordinate 80 | pub trait ReverseCoordTranslate: CoordTranslate { 81 | /// Reverse translate the coordinate from the drawing coordinate to the 82 | /// logic coordinate. 83 | /// Note: the return value is an option, because it's possible that the drawing 84 | /// coordinate isn't able to be represented in te guest coordinate system 85 | fn reverse_translate(&self, input: BackendCoord) -> Option; 86 | } 87 | 88 | /// The coordinate translation that only impose shift 89 | #[derive(Debug, Clone)] 90 | pub struct Shift(pub BackendCoord); 91 | 92 | impl CoordTranslate for Shift { 93 | type From = BackendCoord; 94 | fn translate(&self, from: &Self::From) -> BackendCoord { 95 | (from.0 + (self.0).0, from.1 + (self.0).1) 96 | } 97 | } 98 | 99 | impl ReverseCoordTranslate for Shift { 100 | fn reverse_translate(&self, input: BackendCoord) -> Option { 101 | Some((input.0 - (self.0).0, input.1 - (self.0).1)) 102 | } 103 | } 104 | 105 | /// We can compose an arbitrary transformation with a shift 106 | pub struct ShiftAndTrans(Shift, T); 107 | 108 | impl CoordTranslate for ShiftAndTrans { 109 | type From = T::From; 110 | fn translate(&self, from: &Self::From) -> BackendCoord { 111 | let temp = self.1.translate(from); 112 | self.0.translate(&temp) 113 | } 114 | } 115 | 116 | impl ReverseCoordTranslate for ShiftAndTrans { 117 | fn reverse_translate(&self, input: BackendCoord) -> Option { 118 | Some(self.1.reverse_translate(self.0.reverse_translate(input)?)?) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/data/data_range.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::{Ordering, PartialOrd}; 2 | use std::iter::IntoIterator; 3 | use std::ops::Range; 4 | 5 | use num_traits::{One, Zero}; 6 | 7 | /// Build a range that fits the data 8 | /// 9 | /// - `iter`: the iterator over the data 10 | /// - **returns** The resulting range 11 | /// 12 | /// ```rust 13 | /// use plotters::data::fitting_range; 14 | /// 15 | /// let data = [4, 14, -2, 2, 5]; 16 | /// let range = fitting_range(&data); 17 | /// assert_eq!(range, std::ops::Range { start: -2, end: 14 }); 18 | /// ``` 19 | pub fn fitting_range<'a, T: 'a, I: IntoIterator>(iter: I) -> Range 20 | where 21 | T: Zero + One + PartialOrd + Clone, 22 | { 23 | let (mut lb, mut ub) = (None, None); 24 | 25 | for value in iter.into_iter() { 26 | if let Some(Ordering::Greater) = lb 27 | .as_ref() 28 | .map_or(Some(Ordering::Greater), |lbv: &T| lbv.partial_cmp(value)) 29 | { 30 | lb = Some(value.clone()); 31 | } 32 | 33 | if let Some(Ordering::Less) = ub 34 | .as_ref() 35 | .map_or(Some(Ordering::Less), |ubv: &T| ubv.partial_cmp(value)) 36 | { 37 | ub = Some(value.clone()); 38 | } 39 | } 40 | 41 | lb.unwrap_or_else(Zero::zero)..ub.unwrap_or_else(One::one) 42 | } 43 | -------------------------------------------------------------------------------- /src/data/float.rs: -------------------------------------------------------------------------------- 1 | // The code that is related to float number handling 2 | 3 | fn find_minimal_repr(n: f64, eps: f64) -> (f64, usize) { 4 | if eps >= 1.0 { 5 | return (n, 0); 6 | } 7 | if n - n.floor() < eps { 8 | (n.floor(), 0) 9 | } else if n.ceil() - n < eps { 10 | (n.ceil(), 0) 11 | } else { 12 | let (rem, pre) = find_minimal_repr((n - n.floor()) * 10.0, eps * 10.0); 13 | (n.floor() + rem / 10.0, pre + 1) 14 | } 15 | } 16 | 17 | fn float_to_string(n: f64, max_precision: usize) -> String { 18 | let (sign, n) = if n < 0.0 { ("-", -n) } else { ("", n) }; 19 | let int_part = n.floor(); 20 | 21 | let dec_part = 22 | ((n.abs() - int_part.abs()) * (10.0f64).powf(max_precision as f64)).round() as u64; 23 | 24 | if dec_part == 0 || max_precision == 0 { 25 | return format!("{}{:.0}", sign, int_part); 26 | } 27 | 28 | let mut leading = "".to_string(); 29 | let mut dec_result = format!("{}", dec_part); 30 | 31 | for _ in 0..(max_precision - dec_result.len()) { 32 | leading.push('0'); 33 | } 34 | 35 | while let Some(c) = dec_result.pop() { 36 | if c != '0' { 37 | dec_result.push(c); 38 | break; 39 | } 40 | } 41 | 42 | format!("{}{:.0}.{}{}", sign, int_part, leading, dec_result) 43 | } 44 | 45 | /// The function that pretty prints the floating number 46 | /// Since rust doesn't have anything that can format a float with out appearance, so we just 47 | /// implemnet a float pretty printing function, which finds the shortest representation of a 48 | /// floating point number within the allowed error range. 49 | /// 50 | /// - `n`: The float number to pretty-print 51 | /// - `allow_sn`: Should we use scientific notation when possible 52 | /// - **returns**: The pretty printed string 53 | pub fn pretty_print_float(n: f64, allow_sn: bool) -> String { 54 | let (n, p) = find_minimal_repr(n, 1e-10); 55 | let d_repr = float_to_string(n, p); 56 | if !allow_sn { 57 | d_repr 58 | } else { 59 | if n == 0.0 { 60 | return "0".to_string(); 61 | } 62 | 63 | let mut idx = n.abs().log10().floor(); 64 | let mut exp = (10.0f64).powf(idx); 65 | 66 | if n.abs() / exp + 1e-5 >= 10.0 { 67 | idx += 1.0; 68 | exp *= 10.0; 69 | } 70 | 71 | if idx.abs() < 3.0 { 72 | return d_repr; 73 | } 74 | 75 | let (sn, sp) = find_minimal_repr(n / exp, 1e-5); 76 | let s_repr = format!("{}e{}", float_to_string(sn, sp), float_to_string(idx, 0)); 77 | if s_repr.len() + 1 < d_repr.len() { 78 | s_repr 79 | } else { 80 | d_repr 81 | } 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod test { 87 | use super::*; 88 | #[test] 89 | fn test_pretty_printing() { 90 | assert_eq!(pretty_print_float(0.99999999999999999999, false), "1"); 91 | assert_eq!(pretty_print_float(0.9999, false), "0.9999"); 92 | assert_eq!( 93 | pretty_print_float(-1e-5 - 0.00000000000000001, true), 94 | "-1e-5" 95 | ); 96 | assert_eq!( 97 | pretty_print_float(-1e-5 - 0.00000000000000001, false), 98 | "-0.00001" 99 | ); 100 | assert_eq!(pretty_print_float(1e100, true), "1e100"); 101 | assert_eq!(pretty_print_float(1234567890f64, true), "1234567890"); 102 | assert_eq!(pretty_print_float(1000000001f64, true), "1e9"); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The data processing module, which implements algorithms related to visualization of data. 3 | Such as, down-sampling, etc. 4 | */ 5 | 6 | mod data_range; 7 | pub use data_range::fitting_range; 8 | 9 | mod quartiles; 10 | pub use quartiles::Quartiles; 11 | 12 | pub mod float; 13 | -------------------------------------------------------------------------------- /src/data/quartiles.rs: -------------------------------------------------------------------------------- 1 | /// The quartiles 2 | #[derive(Clone, Debug)] 3 | pub struct Quartiles { 4 | lower_fence: f64, 5 | lower: f64, 6 | median: f64, 7 | upper: f64, 8 | upper_fence: f64, 9 | } 10 | 11 | impl Quartiles { 12 | // Extract a value representing the `pct` percentile of a 13 | // sorted `s`, using linear interpolation. 14 | fn percentile_of_sorted + Copy>(s: &[T], pct: f64) -> f64 { 15 | assert!(!s.is_empty()); 16 | if s.len() == 1 { 17 | return s[0].into(); 18 | } 19 | assert!(0_f64 <= pct); 20 | let hundred = 100_f64; 21 | assert!(pct <= hundred); 22 | if (pct - hundred).abs() < std::f64::EPSILON { 23 | return s[s.len() - 1].into(); 24 | } 25 | let length = (s.len() - 1) as f64; 26 | let rank = (pct / hundred) * length; 27 | let lower_rank = rank.floor(); 28 | let d = rank - lower_rank; 29 | let n = lower_rank as usize; 30 | let lo = s[n].into(); 31 | let hi = s[n + 1].into(); 32 | lo + (hi - lo) * d 33 | } 34 | 35 | /// Create a new quartiles struct with the values calculated from the argument. 36 | /// 37 | /// - `s`: The array of the original values 38 | /// - **returns** The newly created quartiles 39 | /// 40 | /// ```rust 41 | /// use plotters::prelude::*; 42 | /// 43 | /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); 44 | /// assert_eq!(quartiles.median(), 37.5); 45 | /// ``` 46 | pub fn new + Copy + PartialOrd>(s: &[T]) -> Self { 47 | let mut s = s.to_owned(); 48 | s.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); 49 | 50 | let lower = Quartiles::percentile_of_sorted(&s, 25_f64); 51 | let median = lower; 52 | let upper = Quartiles::percentile_of_sorted(&s, 75_f64); 53 | let iqr = upper - lower; 54 | let lower_fence = lower; 55 | let upper_fence = upper; 56 | Self { 57 | lower_fence, 58 | lower, 59 | median, 60 | upper, 61 | upper_fence, 62 | } 63 | } 64 | 65 | /// Get the quartiles values. 66 | /// 67 | /// - **returns** The array [lower fence, lower quartile, median, upper quartile, upper fence] 68 | /// 69 | /// ```rust 70 | /// use plotters::prelude::*; 71 | /// 72 | /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); 73 | /// let values = quartiles.values(); 74 | /// assert_eq!(values, [-9.0, 20.25, 37.5, 39.75, 69.0]); 75 | /// ``` 76 | pub fn values(&self) -> [f32; 5] { 77 | [ 78 | self.lower_fence as f32, 79 | self.lower as f32, 80 | self.median as f32, 81 | self.upper as f32, 82 | self.upper_fence as f32, 83 | ] 84 | } 85 | 86 | /// Get the quartiles median. 87 | /// 88 | /// - **returns** The median 89 | /// 90 | /// ```rust 91 | /// use plotters::prelude::*; 92 | /// 93 | /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]); 94 | /// assert_eq!(quartiles.median(), 37.5); 95 | /// ``` 96 | pub fn median(&self) -> f64 { 97 | self.median 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod test { 103 | use super::*; 104 | 105 | #[test] 106 | #[should_panic] 107 | fn test_empty_input() { 108 | let empty_array: [i32; 0] = []; 109 | Quartiles::new(&empty_array); 110 | } 111 | 112 | #[test] 113 | fn test_low_inputs() { 114 | assert_eq!( 115 | Quartiles::new(&[15.0]).values(), 116 | [15.0, 15.0, 15.0, 15.0, 15.0] 117 | ); 118 | assert_eq!( 119 | Quartiles::new(&[10, 20]).values(), 120 | [5.0, 12.5, 15.0, 17.5, 25.0] 121 | ); 122 | assert_eq!( 123 | Quartiles::new(&[10, 20, 30]).values(), 124 | [0.0, 15.0, 20.0, 25.0, 40.0] 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/drawing/backend_impl/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "svg")] 2 | mod svg; 3 | #[cfg(feature = "svg")] 4 | pub use self::svg::SVGBackend; 5 | 6 | #[cfg(feature = "bitmap")] 7 | mod bitmap; 8 | #[cfg(feature = "bitmap")] 9 | pub use bitmap::BitMapBackend; 10 | 11 | #[cfg(feature = "bitmap")] 12 | pub mod bitmap_pixel { 13 | pub use super::bitmap::{BGRXPixel, PixelFormat, RGBPixel}; 14 | } 15 | 16 | #[cfg(target_arch = "wasm32")] 17 | mod canvas; 18 | #[cfg(target_arch = "wasm32")] 19 | pub use canvas::CanvasBackend; 20 | 21 | #[cfg(test)] 22 | mod mocked; 23 | #[cfg(test)] 24 | pub use mocked::{create_mocked_drawing_area, MockedBackend}; 25 | 26 | #[cfg(all(not(target_arch = "wasm32"), feature = "piston"))] 27 | mod piston; 28 | #[cfg(all(not(target_arch = "wasm32"), feature = "piston"))] 29 | pub use piston::{draw_piston_window, PistonBackend}; 30 | 31 | #[cfg(all(not(target_arch = "wasm32"), feature = "cairo-rs"))] 32 | mod cairo; 33 | #[cfg(all(not(target_arch = "wasm32"), feature = "cairo-rs"))] 34 | pub use self::cairo::CairoBackend; 35 | 36 | /// This is the dummy backend placeholder for the backend that never fails 37 | #[derive(Debug)] 38 | pub struct DummyBackendError; 39 | 40 | impl std::fmt::Display for DummyBackendError { 41 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 42 | write!(fmt, "{:?}", self) 43 | } 44 | } 45 | 46 | impl std::error::Error for DummyBackendError {} 47 | -------------------------------------------------------------------------------- /src/drawing/backend_impl/piston.rs: -------------------------------------------------------------------------------- 1 | use piston_window::context::Context; 2 | use piston_window::ellipse::circle; 3 | use piston_window::{circle_arc, ellipse, line, rectangle, Event, Loop}; 4 | use piston_window::{G2d, PistonWindow}; 5 | 6 | use super::DummyBackendError; 7 | use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind}; 8 | use crate::style::{Color, RGBAColor}; 9 | 10 | pub struct PistonBackend<'a, 'b> { 11 | size: (u32, u32), 12 | scale: f64, 13 | context: Context, 14 | graphics: &'b mut G2d<'a>, 15 | } 16 | 17 | fn make_piston_rgba(color: &RGBAColor) -> [f32; 4] { 18 | let (r, g, b) = color.rgb(); 19 | let a = color.alpha(); 20 | 21 | [ 22 | r as f32 / 255.0, 23 | g as f32 / 255.0, 24 | b as f32 / 255.0, 25 | a as f32, 26 | ] 27 | } 28 | fn make_point_pair(a: BackendCoord, b: BackendCoord, scale: f64) -> [f64; 4] { 29 | [ 30 | a.0 as f64 * scale, 31 | a.1 as f64 * scale, 32 | b.0 as f64 * scale, 33 | b.1 as f64 * scale, 34 | ] 35 | } 36 | 37 | impl<'a, 'b> PistonBackend<'a, 'b> { 38 | pub fn new(size: (u32, u32), scale: f64, context: Context, graphics: &'b mut G2d<'a>) -> Self { 39 | Self { 40 | size, 41 | context, 42 | graphics, 43 | scale, 44 | } 45 | } 46 | } 47 | 48 | impl<'a, 'b> DrawingBackend for PistonBackend<'a, 'b> { 49 | type ErrorType = DummyBackendError; 50 | 51 | fn get_size(&self) -> (u32, u32) { 52 | self.size 53 | } 54 | 55 | fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { 56 | Ok(()) 57 | } 58 | 59 | fn present(&mut self) -> Result<(), DrawingErrorKind> { 60 | Ok(()) 61 | } 62 | 63 | fn draw_pixel( 64 | &mut self, 65 | point: BackendCoord, 66 | color: &RGBAColor, 67 | ) -> Result<(), DrawingErrorKind> { 68 | piston_window::rectangle( 69 | make_piston_rgba(color), 70 | make_point_pair(point, (1, 1), self.scale), 71 | self.context.transform, 72 | self.graphics, 73 | ); 74 | Ok(()) 75 | } 76 | 77 | fn draw_line( 78 | &mut self, 79 | from: BackendCoord, 80 | to: BackendCoord, 81 | style: &S, 82 | ) -> Result<(), DrawingErrorKind> { 83 | line( 84 | make_piston_rgba(&style.as_color()), 85 | self.scale, 86 | make_point_pair(from, to, self.scale), 87 | self.context.transform, 88 | self.graphics, 89 | ); 90 | Ok(()) 91 | } 92 | 93 | fn draw_rect( 94 | &mut self, 95 | upper_left: BackendCoord, 96 | bottom_right: BackendCoord, 97 | style: &S, 98 | fill: bool, 99 | ) -> Result<(), DrawingErrorKind> { 100 | if fill { 101 | rectangle( 102 | make_piston_rgba(&style.as_color()), 103 | make_point_pair( 104 | upper_left, 105 | (bottom_right.0 - upper_left.0, bottom_right.1 - upper_left.1), 106 | self.scale, 107 | ), 108 | self.context.transform, 109 | self.graphics, 110 | ); 111 | } else { 112 | let color = make_piston_rgba(&style.as_color()); 113 | let [x0, y0, x1, y1] = make_point_pair(upper_left, bottom_right, self.scale); 114 | line( 115 | color, 116 | self.scale, 117 | [x0, y0, x0, y1], 118 | self.context.transform, 119 | self.graphics, 120 | ); 121 | line( 122 | color, 123 | self.scale, 124 | [x0, y1, x1, y1], 125 | self.context.transform, 126 | self.graphics, 127 | ); 128 | line( 129 | color, 130 | self.scale, 131 | [x1, y1, x1, y0], 132 | self.context.transform, 133 | self.graphics, 134 | ); 135 | line( 136 | color, 137 | self.scale, 138 | [x1, y0, x0, y0], 139 | self.context.transform, 140 | self.graphics, 141 | ); 142 | } 143 | Ok(()) 144 | } 145 | 146 | fn draw_circle( 147 | &mut self, 148 | center: BackendCoord, 149 | radius: u32, 150 | style: &S, 151 | fill: bool, 152 | ) -> Result<(), DrawingErrorKind> { 153 | let rect = circle(center.0 as f64, center.1 as f64, radius as f64); 154 | if fill { 155 | ellipse( 156 | make_piston_rgba(&style.as_color()), 157 | rect, 158 | self.context.transform, 159 | self.graphics, 160 | ); 161 | } else { 162 | circle_arc( 163 | make_piston_rgba(&style.as_color()), 164 | self.scale, 165 | std::f64::consts::PI, 166 | 0.0, 167 | rect, 168 | self.context.transform, 169 | self.graphics, 170 | ); 171 | circle_arc( 172 | make_piston_rgba(&style.as_color()), 173 | self.scale, 174 | 0.0, 175 | std::f64::consts::PI, 176 | rect, 177 | self.context.transform, 178 | self.graphics, 179 | ); 180 | } 181 | Ok(()) 182 | } 183 | } 184 | 185 | #[allow(clippy::single_match)] 186 | pub fn draw_piston_window Result<(), Box>>( 187 | window: &mut PistonWindow, 188 | draw: F, 189 | ) -> Option { 190 | if let Some(event) = window.next() { 191 | window.draw_2d(&event, |c, g, _| match event { 192 | Event::Loop(Loop::Render(arg)) => { 193 | draw(PistonBackend::new( 194 | (arg.draw_size[0], arg.draw_size[1]), 195 | arg.window_size[0] / arg.draw_size[0] as f64, 196 | c, 197 | g, 198 | )) 199 | .ok(); 200 | } 201 | _ => {} 202 | }); 203 | return Some(event); 204 | } 205 | None 206 | } 207 | -------------------------------------------------------------------------------- /src/drawing/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The drawing utils for Plotter. Which handles the both low-level and high-level 3 | drawing. 4 | 5 | For the low-level drawing abstraction, the module defines the `DrawingBackend` trait, 6 | which handles low-level drawing of different shapes, such as, pixels, lines, rectangles, etc. 7 | 8 | On the top of drawing backend, one or more drawing area can be defined and different coordinate 9 | system can be applied to the drawing areas. And the drawing area implement the high-level drawing 10 | interface, which draws an element. 11 | 12 | Currently we have following backend implemented: 13 | 14 | - `BitMapBackend`: The backend that creates bitmap, this is based on `image` crate 15 | - `SVGBackend`: The backend that creates SVG image, based on `svg` crate. 16 | - `PistonBackend`: The backend that uses Piston Window for real time rendering. Disabled by default, use feature `piston` to turn on. 17 | - `CanvasBackend`: The backend that operates HTML5 Canvas, this is available when `Plotters` is targeting WASM. 18 | 19 | */ 20 | mod area; 21 | mod backend_impl; 22 | 23 | pub mod rasterizer; 24 | 25 | pub mod backend; 26 | 27 | pub use area::{DrawingArea, DrawingAreaErrorKind, IntoDrawingArea}; 28 | 29 | pub use backend_impl::*; 30 | 31 | pub use backend::DrawingBackend; 32 | -------------------------------------------------------------------------------- /src/drawing/rasterizer/circle.rs: -------------------------------------------------------------------------------- 1 | use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind}; 2 | use crate::drawing::DrawingBackend; 3 | 4 | use crate::style::Color; 5 | 6 | pub fn draw_circle( 7 | b: &mut B, 8 | center: BackendCoord, 9 | radius: u32, 10 | style: &S, 11 | fill: bool, 12 | ) -> Result<(), DrawingErrorKind> { 13 | if style.as_color().alpha() == 0.0 { 14 | return Ok(()); 15 | } 16 | 17 | if !fill && style.stroke_width() != 1 { 18 | // FIXME: We are currently ignore the stroke width for circles 19 | } 20 | 21 | let min = (f64::from(radius) * (1.0 - (2f64).sqrt() / 2.0)).ceil() as i32; 22 | let max = (f64::from(radius) * (1.0 + (2f64).sqrt() / 2.0)).floor() as i32; 23 | 24 | let range = min..=max; 25 | 26 | let (up, down) = ( 27 | range.start() + center.1 - radius as i32, 28 | range.end() + center.1 - radius as i32, 29 | ); 30 | 31 | for dy in range { 32 | let dy = dy - radius as i32; 33 | let y = center.1 + dy; 34 | 35 | let lx = (f64::from(radius) * f64::from(radius) 36 | - (f64::from(dy) * f64::from(dy)).max(1e-5)) 37 | .sqrt(); 38 | 39 | let left = center.0 - lx.floor() as i32; 40 | let right = center.0 + lx.floor() as i32; 41 | 42 | let v = lx - lx.floor(); 43 | 44 | let x = center.0 + dy; 45 | let top = center.1 - lx.floor() as i32; 46 | let bottom = center.1 + lx.floor() as i32; 47 | 48 | if fill { 49 | check_result!(b.draw_line((left, y), (right, y), &style.as_color())); 50 | check_result!(b.draw_line((x, top), (x, up), &style.as_color())); 51 | check_result!(b.draw_line((x, down), (x, bottom), &style.as_color())); 52 | } else { 53 | check_result!(b.draw_pixel((left, y), &style.as_color().mix(1.0 - v))); 54 | check_result!(b.draw_pixel((right, y), &style.as_color().mix(1.0 - v))); 55 | 56 | check_result!(b.draw_pixel((x, top), &style.as_color().mix(1.0 - v))); 57 | check_result!(b.draw_pixel((x, bottom), &style.as_color().mix(1.0 - v))); 58 | } 59 | 60 | check_result!(b.draw_pixel((left - 1, y), &style.as_color().mix(v))); 61 | check_result!(b.draw_pixel((right + 1, y), &style.as_color().mix(v))); 62 | check_result!(b.draw_pixel((x, top - 1), &style.as_color().mix(v))); 63 | check_result!(b.draw_pixel((x, bottom + 1), &style.as_color().mix(v))); 64 | } 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /src/drawing/rasterizer/line.rs: -------------------------------------------------------------------------------- 1 | use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind}; 2 | use crate::drawing::DrawingBackend; 3 | 4 | use crate::style::Color; 5 | 6 | pub fn draw_line( 7 | back: &mut DB, 8 | mut from: BackendCoord, 9 | mut to: BackendCoord, 10 | style: &S, 11 | ) -> Result<(), DrawingErrorKind> { 12 | if style.as_color().alpha() == 0.0 { 13 | return Ok(()); 14 | } 15 | 16 | if style.stroke_width() != 1 { 17 | // If the line is wider than 1px, then we need to make it a polygon 18 | let v = (i64::from(to.0 - from.0), i64::from(to.1 - from.1)); 19 | let l = ((v.0 * v.0 + v.1 * v.1) as f64).sqrt(); 20 | 21 | if l < 1e-5 { 22 | return Ok(()); 23 | } 24 | 25 | let v = (v.0 as f64 / l, v.1 as f64 / l); 26 | 27 | let r = f64::from(style.stroke_width()) / 2.0; 28 | let mut trans = [(v.1 * r, -v.0 * r), (-v.1 * r, v.0 * r)]; 29 | let mut vertices = vec![]; 30 | 31 | for point in [from, to].iter() { 32 | for t in trans.iter() { 33 | vertices.push(( 34 | (f64::from(point.0) + t.0) as i32, 35 | (f64::from(point.1) + t.1) as i32, 36 | )) 37 | } 38 | 39 | trans.swap(0, 1); 40 | } 41 | 42 | return back.fill_polygon(vertices, &style.as_color()); 43 | } 44 | 45 | if from.0 == to.0 { 46 | if from.1 > to.1 { 47 | std::mem::swap(&mut from, &mut to); 48 | } 49 | for y in from.1..=to.1 { 50 | check_result!(back.draw_pixel((from.0, y), &style.as_color())); 51 | } 52 | return Ok(()); 53 | } 54 | 55 | if from.1 == to.1 { 56 | if from.0 > to.0 { 57 | std::mem::swap(&mut from, &mut to); 58 | } 59 | for x in from.0..=to.0 { 60 | check_result!(back.draw_pixel((x, from.1), &style.as_color())); 61 | } 62 | return Ok(()); 63 | } 64 | 65 | let steep = (from.0 - to.0).abs() < (from.1 - to.1).abs(); 66 | 67 | if steep { 68 | from = (from.1, from.0); 69 | to = (to.1, to.0); 70 | } 71 | 72 | let (from, to) = if from.0 > to.0 { 73 | (to, from) 74 | } else { 75 | (from, to) 76 | }; 77 | 78 | let grad = f64::from(to.1 - from.1) / f64::from(to.0 - from.0); 79 | 80 | let mut put_pixel = |(x, y): BackendCoord, b: f64| { 81 | if steep { 82 | back.draw_pixel((y, x), &style.as_color().mix(b)) 83 | } else { 84 | back.draw_pixel((x, y), &style.as_color().mix(b)) 85 | } 86 | }; 87 | 88 | let mut y = f64::from(from.1); 89 | 90 | for x in from.0..=to.0 { 91 | check_result!(put_pixel((x, y as i32), 1.0 + y.floor() - y)); 92 | check_result!(put_pixel((x, y as i32 + 1), y - y.floor())); 93 | 94 | y += grad; 95 | } 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /src/drawing/rasterizer/mod.rs: -------------------------------------------------------------------------------- 1 | // TODO: ? operator is very slow. See issue #58 for details 2 | macro_rules! check_result { 3 | ($e:expr) => { 4 | let result = $e; 5 | if result.is_err() { 6 | return result; 7 | } 8 | }; 9 | } 10 | 11 | mod line; 12 | pub use line::draw_line; 13 | 14 | mod rect; 15 | pub use rect::draw_rect; 16 | 17 | mod circle; 18 | pub use circle::draw_circle; 19 | 20 | mod polygon; 21 | pub use polygon::fill_polygon; 22 | 23 | mod path; 24 | pub use path::polygonize; 25 | -------------------------------------------------------------------------------- /src/drawing/rasterizer/path.rs: -------------------------------------------------------------------------------- 1 | use crate::drawing::backend::BackendCoord; 2 | 3 | fn get_dir_vector(from: BackendCoord, to: BackendCoord, flag: bool) -> ((f64, f64), (f64, f64)) { 4 | let v = (i64::from(to.0 - from.0), i64::from(to.1 - from.1)); 5 | let l = ((v.0 * v.0 + v.1 * v.1) as f64).sqrt(); 6 | 7 | let v = (v.0 as f64 / l, v.1 as f64 / l); 8 | 9 | if flag { 10 | (v, (v.1, -v.0)) 11 | } else { 12 | (v, (-v.1, v.0)) 13 | } 14 | } 15 | 16 | fn compute_polygon_vertex(triple: &[BackendCoord; 3], d: f64) -> BackendCoord { 17 | let (a_t, a_n) = get_dir_vector(triple[0], triple[1], false); 18 | let (b_t, b_n) = get_dir_vector(triple[2], triple[1], true); 19 | 20 | let a_p = ( 21 | f64::from(triple[1].0) + d * a_n.0, 22 | f64::from(triple[1].1) + d * a_n.1, 23 | ); 24 | let b_p = ( 25 | f64::from(triple[1].0) + d * b_n.0, 26 | f64::from(triple[1].1) + d * b_n.1, 27 | ); 28 | 29 | // u * a_t + a_p = v * b_t + b_p 30 | // u * a_t.0 - v * b_t.0 = b_p.0 - a_p.0 31 | // u * a_t.1 - v * b_t.1 = b_p.1 - a_p.1 32 | if a_p.0 as i32 == b_p.0 as i32 && a_p.1 as i32 == b_p.1 as i32 { 33 | return (a_p.0 as i32, a_p.1 as i32); 34 | } 35 | 36 | let a0 = a_t.0; 37 | let b0 = -b_t.0; 38 | let c0 = b_p.0 - a_p.0; 39 | let a1 = a_t.1; 40 | let b1 = -b_t.1; 41 | let c1 = b_p.1 - a_p.1; 42 | 43 | let u = (c0 * b1 - c1 * b0) / (a0 * b1 - a1 * b0); 44 | 45 | let x = a_p.0 + u * a_t.0; 46 | let y = a_p.1 + u * a_t.1; 47 | 48 | (x.round() as i32, y.round() as i32) 49 | } 50 | 51 | fn traverse_vertices<'a>( 52 | mut vertices: impl Iterator, 53 | width: u32, 54 | mut op: impl FnMut(BackendCoord), 55 | ) { 56 | let mut a = vertices.next().unwrap(); 57 | let mut b = vertices.next().unwrap(); 58 | 59 | while a == b { 60 | a = b; 61 | if let Some(new_b) = vertices.next() { 62 | b = new_b; 63 | } else { 64 | return; 65 | } 66 | } 67 | 68 | let (_, n) = get_dir_vector(*a, *b, false); 69 | 70 | op(( 71 | (f64::from(a.0) + n.0 * f64::from(width) / 2.0).round() as i32, 72 | (f64::from(a.1) + n.1 * f64::from(width) / 2.0).round() as i32, 73 | )); 74 | 75 | let mut recent = [(0, 0), *a, *b]; 76 | 77 | for p in vertices { 78 | if *p == recent[2] { 79 | continue; 80 | } 81 | recent.swap(0, 1); 82 | recent.swap(1, 2); 83 | recent[2] = *p; 84 | op(compute_polygon_vertex(&recent, f64::from(width) / 2.0)); 85 | } 86 | 87 | let b = recent[1]; 88 | let a = recent[2]; 89 | 90 | let (_, n) = get_dir_vector(a, b, true); 91 | 92 | op(( 93 | (f64::from(a.0) + n.0 * f64::from(width) / 2.0).round() as i32, 94 | (f64::from(a.1) + n.1 * f64::from(width) / 2.0).round() as i32, 95 | )); 96 | } 97 | 98 | pub fn polygonize(vertices: &[BackendCoord], stroke_width: u32) -> Vec { 99 | if vertices.len() < 2 { 100 | return vec![]; 101 | } 102 | 103 | let mut ret = vec![]; 104 | 105 | traverse_vertices(vertices.iter(), stroke_width, |v| ret.push(v)); 106 | traverse_vertices(vertices.iter().rev(), stroke_width, |v| ret.push(v)); 107 | 108 | ret 109 | } 110 | -------------------------------------------------------------------------------- /src/drawing/rasterizer/rect.rs: -------------------------------------------------------------------------------- 1 | use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind}; 2 | use crate::drawing::DrawingBackend; 3 | 4 | use crate::style::Color; 5 | 6 | pub fn draw_rect( 7 | b: &mut B, 8 | upper_left: BackendCoord, 9 | bottom_right: BackendCoord, 10 | style: &S, 11 | fill: bool, 12 | ) -> Result<(), DrawingErrorKind> { 13 | if style.as_color().alpha() == 0.0 { 14 | return Ok(()); 15 | } 16 | let (upper_left, bottom_right) = ( 17 | ( 18 | upper_left.0.min(bottom_right.0), 19 | upper_left.1.min(bottom_right.1), 20 | ), 21 | ( 22 | upper_left.0.max(bottom_right.0), 23 | upper_left.1.max(bottom_right.1), 24 | ), 25 | ); 26 | 27 | if fill { 28 | if bottom_right.0 - upper_left.0 < bottom_right.1 - upper_left.1 { 29 | for x in upper_left.0..=bottom_right.0 { 30 | check_result!(b.draw_line((x, upper_left.1), (x, bottom_right.1), style)); 31 | } 32 | } else { 33 | for y in upper_left.1..=bottom_right.1 { 34 | check_result!(b.draw_line((upper_left.0, y), (bottom_right.0, y), style)); 35 | } 36 | } 37 | } else { 38 | b.draw_line( 39 | (upper_left.0, upper_left.1), 40 | (upper_left.0, bottom_right.1), 41 | style, 42 | )?; 43 | b.draw_line( 44 | (upper_left.0, upper_left.1), 45 | (bottom_right.0, upper_left.1), 46 | style, 47 | )?; 48 | b.draw_line( 49 | (bottom_right.0, bottom_right.1), 50 | (upper_left.0, bottom_right.1), 51 | style, 52 | )?; 53 | b.draw_line( 54 | (bottom_right.0, bottom_right.1), 55 | (bottom_right.0, upper_left.1), 56 | style, 57 | )?; 58 | } 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /src/element/candlestick.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The candlestick element, which showing the high/low/open/close price 3 | */ 4 | 5 | use std::cmp::Ordering; 6 | 7 | use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; 8 | use crate::element::{Drawable, PointCollection}; 9 | use crate::style::ShapeStyle; 10 | 11 | /// The candlestick data point element 12 | pub struct CandleStick { 13 | style: ShapeStyle, 14 | width: u32, 15 | points: [(X, Y); 4], 16 | } 17 | 18 | impl CandleStick { 19 | /// Create a new candlestick element, which requires the Y coordinate can be compared 20 | /// 21 | /// - `x`: The x coordinate 22 | /// - `open`: The open value 23 | /// - `high`: The high value 24 | /// - `low`: The low value 25 | /// - `close`: The close value 26 | /// - `gain_style`: The style for gain 27 | /// - `loss_style`: The style for loss 28 | /// - `width`: The width 29 | /// - **returns** The newly created candlestick element 30 | /// 31 | /// ```rust 32 | /// use chrono::prelude::*; 33 | /// use plotters::prelude::*; 34 | /// 35 | /// let candlestick = CandleStick::new(Local::now(), 130.0600, 131.3700, 128.8300, 129.1500, &GREEN, &RED, 15); 36 | /// ``` 37 | #[allow(clippy::too_many_arguments)] 38 | pub fn new, LS: Into>( 39 | x: X, 40 | open: Y, 41 | high: Y, 42 | low: Y, 43 | close: Y, 44 | gain_style: GS, 45 | loss_style: LS, 46 | width: u32, 47 | ) -> Self { 48 | Self { 49 | style: match open.partial_cmp(&close) { 50 | Some(Ordering::Less) => gain_style.into(), 51 | _ => loss_style.into(), 52 | }, 53 | width, 54 | points: [ 55 | (x.clone(), open), 56 | (x.clone(), high), 57 | (x.clone(), low), 58 | (x, close), 59 | ], 60 | } 61 | } 62 | } 63 | 64 | impl<'a, X: 'a, Y: PartialOrd + 'a> PointCollection<'a, (X, Y)> for &'a CandleStick { 65 | type Borrow = &'a (X, Y); 66 | type IntoIter = &'a [(X, Y)]; 67 | fn point_iter(self) -> &'a [(X, Y)] { 68 | &self.points 69 | } 70 | } 71 | 72 | impl Drawable for CandleStick { 73 | fn draw>( 74 | &self, 75 | points: I, 76 | backend: &mut DB, 77 | _: (u32, u32), 78 | ) -> Result<(), DrawingErrorKind> { 79 | let mut points: Vec<_> = points.take(4).collect(); 80 | if points.len() == 4 { 81 | let fill = false; 82 | if points[0].1 > points[3].1 { 83 | points.swap(0, 3); 84 | } 85 | let (l, r) = ( 86 | self.width as i32 / 2, 87 | self.width as i32 - self.width as i32 / 2, 88 | ); 89 | 90 | backend.draw_line(points[0], points[1], &self.style.color)?; 91 | backend.draw_line(points[2], points[3], &self.style.color)?; 92 | 93 | points[0].0 -= l; 94 | points[3].0 += r; 95 | 96 | backend.draw_rect(points[0], points[3], &self.style.color, fill)?; 97 | } 98 | Ok(()) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/element/composable.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::drawing::backend::DrawingBackend; 3 | use std::borrow::Borrow; 4 | use std::iter::{once, Once}; 5 | use std::marker::PhantomData; 6 | use std::ops::Add; 7 | 8 | /// An empty composable element, which is the start point of an ad-hoc composable element 9 | pub struct EmptyElement { 10 | coord: Coord, 11 | phantom: PhantomData, 12 | } 13 | 14 | impl EmptyElement { 15 | pub fn at(coord: Coord) -> Self { 16 | Self { 17 | coord, 18 | phantom: PhantomData, 19 | } 20 | } 21 | } 22 | 23 | impl Add for EmptyElement 24 | where 25 | Other: Drawable, 26 | for<'a> &'a Other: PointCollection<'a, BackendCoord>, 27 | { 28 | type Output = BoxedElement; 29 | fn add(self, other: Other) -> Self::Output { 30 | BoxedElement { 31 | offset: self.coord, 32 | inner: other, 33 | phantom: PhantomData, 34 | } 35 | } 36 | } 37 | 38 | impl<'a, Coord, DB: DrawingBackend> PointCollection<'a, Coord> for &'a EmptyElement { 39 | type Borrow = &'a Coord; 40 | type IntoIter = Once<&'a Coord>; 41 | fn point_iter(self) -> Self::IntoIter { 42 | once(&self.coord) 43 | } 44 | } 45 | 46 | impl Drawable for EmptyElement { 47 | fn draw>( 48 | &self, 49 | _pos: I, 50 | _backend: &mut DB, 51 | _: (u32, u32), 52 | ) -> Result<(), DrawingErrorKind> { 53 | Ok(()) 54 | } 55 | } 56 | 57 | /// An composed element has only one component 58 | pub struct BoxedElement> { 59 | inner: A, 60 | offset: Coord, 61 | phantom: PhantomData, 62 | } 63 | 64 | impl<'b, Coord, DB: DrawingBackend, A: Drawable> PointCollection<'b, Coord> 65 | for &'b BoxedElement 66 | { 67 | type Borrow = &'b Coord; 68 | type IntoIter = Once<&'b Coord>; 69 | fn point_iter(self) -> Self::IntoIter { 70 | once(&self.offset) 71 | } 72 | } 73 | 74 | impl Drawable for BoxedElement 75 | where 76 | for<'a> &'a A: PointCollection<'a, BackendCoord>, 77 | A: Drawable, 78 | { 79 | fn draw>( 80 | &self, 81 | mut pos: I, 82 | backend: &mut DB, 83 | ps: (u32, u32), 84 | ) -> Result<(), DrawingErrorKind> { 85 | if let Some((x0, y0)) = pos.next() { 86 | self.inner.draw( 87 | self.inner.point_iter().into_iter().map(|p| { 88 | let p = p.borrow(); 89 | (p.0 + x0, p.1 + y0) 90 | }), 91 | backend, 92 | ps, 93 | )?; 94 | } 95 | Ok(()) 96 | } 97 | } 98 | 99 | impl Add for BoxedElement 100 | where 101 | My: Drawable, 102 | for<'a> &'a My: PointCollection<'a, BackendCoord>, 103 | Yours: Drawable, 104 | for<'a> &'a Yours: PointCollection<'a, BackendCoord>, 105 | { 106 | type Output = ComposedElement; 107 | fn add(self, yours: Yours) -> Self::Output { 108 | ComposedElement { 109 | offset: self.offset, 110 | first: self.inner, 111 | second: yours, 112 | phantom: PhantomData, 113 | } 114 | } 115 | } 116 | 117 | /// The composed element which has at least two components 118 | pub struct ComposedElement 119 | where 120 | A: Drawable, 121 | B: Drawable, 122 | { 123 | first: A, 124 | second: B, 125 | offset: Coord, 126 | phantom: PhantomData, 127 | } 128 | 129 | impl<'b, Coord, DB: DrawingBackend, A, B> PointCollection<'b, Coord> 130 | for &'b ComposedElement 131 | where 132 | A: Drawable, 133 | B: Drawable, 134 | { 135 | type Borrow = &'b Coord; 136 | type IntoIter = Once<&'b Coord>; 137 | fn point_iter(self) -> Self::IntoIter { 138 | once(&self.offset) 139 | } 140 | } 141 | 142 | impl Drawable for ComposedElement 143 | where 144 | for<'a> &'a A: PointCollection<'a, BackendCoord>, 145 | for<'b> &'b B: PointCollection<'b, BackendCoord>, 146 | A: Drawable, 147 | B: Drawable, 148 | { 149 | fn draw>( 150 | &self, 151 | mut pos: I, 152 | backend: &mut DB, 153 | ps: (u32, u32), 154 | ) -> Result<(), DrawingErrorKind> { 155 | if let Some((x0, y0)) = pos.next() { 156 | self.first.draw( 157 | self.first.point_iter().into_iter().map(|p| { 158 | let p = p.borrow(); 159 | (p.0 + x0, p.1 + y0) 160 | }), 161 | backend, 162 | ps, 163 | )?; 164 | self.second.draw( 165 | self.second.point_iter().into_iter().map(|p| { 166 | let p = p.borrow(); 167 | (p.0 + x0, p.1 + y0) 168 | }), 169 | backend, 170 | ps, 171 | )?; 172 | } 173 | Ok(()) 174 | } 175 | } 176 | 177 | impl Add for ComposedElement 178 | where 179 | A: Drawable, 180 | for<'a> &'a A: PointCollection<'a, BackendCoord>, 181 | B: Drawable, 182 | for<'a> &'a B: PointCollection<'a, BackendCoord>, 183 | C: Drawable, 184 | for<'a> &'a C: PointCollection<'a, BackendCoord>, 185 | { 186 | type Output = ComposedElement>; 187 | fn add(self, rhs: C) -> Self::Output { 188 | ComposedElement { 189 | offset: self.offset, 190 | first: self.first, 191 | second: ComposedElement { 192 | offset: (0, 0), 193 | first: self.second, 194 | second: rhs, 195 | phantom: PhantomData, 196 | }, 197 | phantom: PhantomData, 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/element/dynelem.rs: -------------------------------------------------------------------------------- 1 | use super::{Drawable, PointCollection}; 2 | use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; 3 | 4 | use std::borrow::Borrow; 5 | 6 | trait DynDrawable { 7 | fn draw_dyn( 8 | &self, 9 | points: &mut dyn Iterator, 10 | backend: &mut DB, 11 | parent_dim: (u32, u32), 12 | ) -> Result<(), DrawingErrorKind>; 13 | } 14 | 15 | impl> DynDrawable for T { 16 | fn draw_dyn( 17 | &self, 18 | points: &mut dyn Iterator, 19 | backend: &mut DB, 20 | parent_dim: (u32, u32), 21 | ) -> Result<(), DrawingErrorKind> { 22 | T::draw(self, points, backend, parent_dim) 23 | } 24 | } 25 | 26 | /// The container for a dynamically dispatched element 27 | pub struct DynElement<'a, DB, Coord> 28 | where 29 | DB: DrawingBackend, 30 | Coord: Clone, 31 | { 32 | points: Vec, 33 | drawable: Box + 'a>, 34 | } 35 | 36 | impl<'a, 'b: 'a, DB: DrawingBackend, Coord: Clone> PointCollection<'a, Coord> 37 | for &'a DynElement<'b, DB, Coord> 38 | { 39 | type Borrow = &'a Coord; 40 | type IntoIter = &'a Vec; 41 | fn point_iter(self) -> Self::IntoIter { 42 | &self.points 43 | } 44 | } 45 | 46 | impl<'a, DB: DrawingBackend, Coord: Clone> Drawable for DynElement<'a, DB, Coord> { 47 | fn draw>( 48 | &self, 49 | mut pos: I, 50 | backend: &mut DB, 51 | parent_dim: (u32, u32), 52 | ) -> Result<(), DrawingErrorKind> { 53 | self.drawable.draw_dyn(&mut pos, backend, parent_dim) 54 | } 55 | } 56 | 57 | /// The trait that makes the conversion from the statically dispatched element 58 | /// to the dynamically dispatched element 59 | pub trait IntoDynElement<'a, DB: DrawingBackend, Coord: Clone> 60 | where 61 | Self: 'a, 62 | { 63 | /// Make the conversion 64 | fn into_dyn(self) -> DynElement<'a, DB, Coord>; 65 | } 66 | 67 | impl<'b, T, DB, Coord> IntoDynElement<'b, DB, Coord> for T 68 | where 69 | T: Drawable + 'b, 70 | for<'a> &'a T: PointCollection<'a, Coord>, 71 | Coord: Clone, 72 | DB: DrawingBackend, 73 | { 74 | fn into_dyn(self) -> DynElement<'b, DB, Coord> { 75 | DynElement { 76 | points: self 77 | .point_iter() 78 | .into_iter() 79 | .map(|x| x.borrow().clone()) 80 | .collect(), 81 | drawable: Box::new(self), 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/element/errorbar.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; 4 | use crate::element::{Drawable, PointCollection}; 5 | use crate::style::ShapeStyle; 6 | 7 | pub trait ErrorBarOrient { 8 | type XType; 9 | type YType; 10 | 11 | fn make_coord(key: K, val: V) -> (Self::XType, Self::YType); 12 | fn ending_coord(coord: BackendCoord, w: u32) -> (BackendCoord, BackendCoord); 13 | } 14 | 15 | pub struct ErrorBarOrientH(PhantomData<(K, V)>); 16 | 17 | pub struct ErrorBarOrientV(PhantomData<(K, V)>); 18 | 19 | impl ErrorBarOrient for ErrorBarOrientH { 20 | type XType = V; 21 | type YType = K; 22 | 23 | fn make_coord(key: K, val: V) -> (V, K) { 24 | (val, key) 25 | } 26 | 27 | fn ending_coord(coord: BackendCoord, w: u32) -> (BackendCoord, BackendCoord) { 28 | ( 29 | (coord.0, coord.1 - w as i32 / 2), 30 | (coord.0, coord.1 + w as i32 / 2), 31 | ) 32 | } 33 | } 34 | 35 | impl ErrorBarOrient for ErrorBarOrientV { 36 | type XType = K; 37 | type YType = V; 38 | 39 | fn make_coord(key: K, val: V) -> (K, V) { 40 | (key, val) 41 | } 42 | 43 | fn ending_coord(coord: BackendCoord, w: u32) -> (BackendCoord, BackendCoord) { 44 | ( 45 | (coord.0 - w as i32 / 2, coord.1), 46 | (coord.0 + w as i32 / 2, coord.1), 47 | ) 48 | } 49 | } 50 | 51 | pub struct ErrorBar> { 52 | style: ShapeStyle, 53 | width: u32, 54 | key: K, 55 | values: [V; 3], 56 | _p: PhantomData, 57 | } 58 | 59 | impl ErrorBar> { 60 | pub fn new_vertical>( 61 | key: K, 62 | min: V, 63 | avg: V, 64 | max: V, 65 | style: S, 66 | width: u32, 67 | ) -> Self { 68 | Self { 69 | style: style.into(), 70 | width, 71 | key, 72 | values: [min, avg, max], 73 | _p: PhantomData, 74 | } 75 | } 76 | } 77 | 78 | impl ErrorBar> { 79 | pub fn new_horizontal>( 80 | key: K, 81 | min: V, 82 | avg: V, 83 | max: V, 84 | style: S, 85 | width: u32, 86 | ) -> Self { 87 | Self { 88 | style: style.into(), 89 | width, 90 | key, 91 | values: [min, avg, max], 92 | _p: PhantomData, 93 | } 94 | } 95 | } 96 | 97 | impl<'a, K: 'a + Clone, V: 'a + Clone, O: ErrorBarOrient> 98 | PointCollection<'a, (O::XType, O::YType)> for &'a ErrorBar 99 | { 100 | type Borrow = (O::XType, O::YType); 101 | type IntoIter = Vec; 102 | fn point_iter(self) -> Self::IntoIter { 103 | self.values 104 | .iter() 105 | .map(|v| O::make_coord(self.key.clone(), v.clone())) 106 | .collect() 107 | } 108 | } 109 | 110 | impl, DB: DrawingBackend> Drawable for ErrorBar { 111 | fn draw>( 112 | &self, 113 | points: I, 114 | backend: &mut DB, 115 | _: (u32, u32), 116 | ) -> Result<(), DrawingErrorKind> { 117 | let points: Vec<_> = points.take(3).collect(); 118 | 119 | let (from, to) = O::ending_coord(points[0], self.width); 120 | backend.draw_line(from, to, &self.style.color)?; 121 | 122 | let (from, to) = O::ending_coord(points[2], self.width); 123 | backend.draw_line(from, to, &self.style.color)?; 124 | 125 | backend.draw_line(points[0], points[2], &self.style.color)?; 126 | 127 | backend.draw_circle( 128 | points[1], 129 | self.width / 2, 130 | &self.style.color, 131 | self.style.filled, 132 | )?; 133 | 134 | Ok(()) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/element/points.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use super::{Drawable, PointCollection}; 3 | use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind}; 4 | use crate::style::{ShapeStyle, SizeDesc}; 5 | 6 | /// The element that used to describe a point 7 | pub trait PointElement { 8 | fn make_point(pos: Coord, size: Size, style: ShapeStyle) -> Self; 9 | } 10 | 11 | /// Describe a cross 12 | pub struct Cross { 13 | center: Coord, 14 | size: Size, 15 | style: ShapeStyle, 16 | } 17 | 18 | impl Cross { 19 | pub fn new>(coord: Coord, size: Size, style: T) -> Self { 20 | Self { 21 | center: coord, 22 | size, 23 | style: style.into(), 24 | } 25 | } 26 | } 27 | 28 | impl<'a, Coord: 'a, Size: SizeDesc> PointCollection<'a, Coord> for &'a Cross { 29 | type Borrow = &'a Coord; 30 | type IntoIter = std::iter::Once<&'a Coord>; 31 | fn point_iter(self) -> std::iter::Once<&'a Coord> { 32 | std::iter::once(&self.center) 33 | } 34 | } 35 | 36 | impl Drawable for Cross { 37 | fn draw>( 38 | &self, 39 | mut points: I, 40 | backend: &mut DB, 41 | ps: (u32, u32), 42 | ) -> Result<(), DrawingErrorKind> { 43 | if let Some((x, y)) = points.next() { 44 | let size = self.size.in_pixels(&ps); 45 | let (x0, y0) = (x - size, y - size); 46 | let (x1, y1) = (x + size, y + size); 47 | backend.draw_line((x0, y0), (x1, y1), &self.style.color)?; 48 | backend.draw_line((x0, y1), (x1, y0), &self.style.color)?; 49 | } 50 | Ok(()) 51 | } 52 | } 53 | 54 | /// Describe a triangle marker 55 | pub struct TriangleMarker { 56 | center: Coord, 57 | size: Size, 58 | style: ShapeStyle, 59 | } 60 | 61 | impl TriangleMarker { 62 | pub fn new>(coord: Coord, size: Size, style: T) -> Self { 63 | Self { 64 | center: coord, 65 | size, 66 | style: style.into(), 67 | } 68 | } 69 | } 70 | 71 | impl<'a, Coord: 'a, Size: SizeDesc> PointCollection<'a, Coord> for &'a TriangleMarker { 72 | type Borrow = &'a Coord; 73 | type IntoIter = std::iter::Once<&'a Coord>; 74 | fn point_iter(self) -> std::iter::Once<&'a Coord> { 75 | std::iter::once(&self.center) 76 | } 77 | } 78 | 79 | impl Drawable for TriangleMarker { 80 | fn draw>( 81 | &self, 82 | mut points: I, 83 | backend: &mut DB, 84 | ps: (u32, u32), 85 | ) -> Result<(), DrawingErrorKind> { 86 | if let Some((x, y)) = points.next() { 87 | let size = self.size.in_pixels(&ps); 88 | let points = [-90, -210, -330] 89 | .iter() 90 | .map(|deg| f64::from(*deg) * std::f64::consts::PI / 180.0) 91 | .map(|rad| { 92 | ( 93 | (rad.cos() * f64::from(size) + f64::from(x)).ceil() as i32, 94 | (rad.sin() * f64::from(size) + f64::from(y)).ceil() as i32, 95 | ) 96 | }); 97 | backend.fill_polygon(points, &self.style.color)?; 98 | } 99 | Ok(()) 100 | } 101 | } 102 | 103 | impl PointElement for Cross { 104 | fn make_point(pos: Coord, size: Size, style: ShapeStyle) -> Self { 105 | Self::new(pos, size, style) 106 | } 107 | } 108 | 109 | impl PointElement for TriangleMarker { 110 | fn make_point(pos: Coord, size: Size, style: ShapeStyle) -> Self { 111 | Self::new(pos, size, style) 112 | } 113 | } 114 | 115 | impl PointElement for Circle { 116 | fn make_point(pos: Coord, size: Size, style: ShapeStyle) -> Self { 117 | Self::new(pos, size, style) 118 | } 119 | } 120 | 121 | impl PointElement for Pixel { 122 | fn make_point(pos: Coord, _: Size, style: ShapeStyle) -> Self { 123 | Self::new(pos, style) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/evcxr.rs: -------------------------------------------------------------------------------- 1 | use crate::coord::Shift; 2 | use crate::drawing::{DrawingArea, IntoDrawingArea, SVGBackend}; 3 | 4 | /// The wrapper for the generated SVG 5 | pub struct SVGWrapper(String, String); 6 | 7 | impl SVGWrapper { 8 | pub fn evcxr_display(&self) { 9 | println!("{:?}", self); 10 | } 11 | 12 | pub fn style>(mut self, style: S) -> Self { 13 | self.1 = style.into(); 14 | self 15 | } 16 | } 17 | 18 | impl std::fmt::Debug for SVGWrapper { 19 | fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 20 | let svg = self.0.as_str(); 21 | write!( 22 | formatter, 23 | "EVCXR_BEGIN_CONTENT text/html\n
{}
\nEVCXR_END_CONTENT", 24 | self.1, svg 25 | ) 26 | } 27 | } 28 | 29 | /// Start drawing an evcxr figure 30 | pub fn evcxr_figure< 31 | Draw: FnOnce(DrawingArea) -> Result<(), Box>, 32 | >( 33 | size: (u32, u32), 34 | draw: Draw, 35 | ) -> SVGWrapper { 36 | let mut buffer = "".to_string(); 37 | let root = SVGBackend::with_string(&mut buffer, size).into_drawing_area(); 38 | draw(root).expect("Drawing failure"); 39 | SVGWrapper(buffer, "".to_string()) 40 | } 41 | -------------------------------------------------------------------------------- /src/series/area_series.rs: -------------------------------------------------------------------------------- 1 | use crate::drawing::DrawingBackend; 2 | use crate::element::{DynElement, IntoDynElement, PathElement, Polygon}; 3 | use crate::style::colors::TRANSPARENT; 4 | use crate::style::ShapeStyle; 5 | 6 | /// An area series is similar to a line series but use a filled polygon 7 | pub struct AreaSeries { 8 | area_style: ShapeStyle, 9 | border_style: ShapeStyle, 10 | baseline: Y, 11 | data: Vec<(X, Y)>, 12 | state: u32, 13 | _p: std::marker::PhantomData, 14 | } 15 | 16 | impl AreaSeries { 17 | pub fn new, I: IntoIterator>( 18 | iter: I, 19 | baseline: Y, 20 | area_style: S, 21 | ) -> Self { 22 | Self { 23 | area_style: area_style.into(), 24 | baseline, 25 | data: iter.into_iter().collect(), 26 | state: 0, 27 | border_style: (&TRANSPARENT).into(), 28 | _p: std::marker::PhantomData, 29 | } 30 | } 31 | 32 | pub fn border_style>(mut self, style: S) -> Self { 33 | self.border_style = style.into(); 34 | self 35 | } 36 | } 37 | 38 | impl Iterator for AreaSeries { 39 | type Item = DynElement<'static, DB, (X, Y)>; 40 | fn next(&mut self) -> Option { 41 | if self.state == 0 { 42 | let mut data: Vec<_> = self.data.clone(); 43 | 44 | if !data.is_empty() { 45 | data.push((data[data.len() - 1].0.clone(), self.baseline.clone())); 46 | data.push((data[0].0.clone(), self.baseline.clone())); 47 | } 48 | 49 | self.state = 1; 50 | 51 | Some(Polygon::new(data, self.area_style.clone()).into_dyn()) 52 | } else if self.state == 1 { 53 | let data: Vec<_> = self.data.clone(); 54 | 55 | self.state = 2; 56 | 57 | Some(PathElement::new(data, self.border_style.clone()).into_dyn()) 58 | } else { 59 | None 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/series/histogram.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{hash_map::IntoIter as HashMapIter, HashMap}; 2 | use std::hash::Hash; 3 | use std::marker::PhantomData; 4 | use std::ops::AddAssign; 5 | 6 | use crate::chart::ChartContext; 7 | use crate::coord::{DiscreteRanged, Ranged, RangedCoord}; 8 | use crate::drawing::DrawingBackend; 9 | use crate::element::Rectangle; 10 | use crate::style::{Color, ShapeStyle, GREEN}; 11 | 12 | pub trait HistogramType {} 13 | pub struct Vertical; 14 | pub struct Horizontal; 15 | 16 | impl HistogramType for Vertical {} 17 | impl HistogramType for Horizontal {} 18 | 19 | /// The series that aggregate data into a histogram 20 | pub struct Histogram<'a, BR, A, Tag = Vertical> 21 | where 22 | BR: DiscreteRanged, 23 | BR::ValueType: Eq + Hash, 24 | A: AddAssign + Default, 25 | Tag: HistogramType, 26 | { 27 | style: Box ShapeStyle + 'a>, 28 | margin: u32, 29 | iter: HashMapIter, 30 | baseline: Box A + 'a>, 31 | br_param: BR::RangeParameter, 32 | _p: PhantomData<(BR, Tag)>, 33 | } 34 | 35 | impl<'a, BR, A, Tag> Histogram<'a, BR, A, Tag> 36 | where 37 | BR: DiscreteRanged, 38 | BR::ValueType: Eq + Hash, 39 | A: AddAssign + Default + 'a, 40 | Tag: HistogramType, 41 | { 42 | fn empty(br_param: BR::RangeParameter) -> Self { 43 | Self { 44 | style: Box::new(|_, _| GREEN.filled()), 45 | margin: 5, 46 | iter: HashMap::new().into_iter(), 47 | baseline: Box::new(|_| A::default()), 48 | br_param, 49 | _p: PhantomData, 50 | } 51 | } 52 | /// Set the style of the histogram 53 | pub fn style>(mut self, style: S) -> Self { 54 | let style = style.into(); 55 | self.style = Box::new(move |_, _| style.clone()); 56 | self 57 | } 58 | 59 | /// Set the style of histogram using a lambda function 60 | pub fn style_func( 61 | mut self, 62 | style_func: impl Fn(&BR::ValueType, &A) -> ShapeStyle + 'a, 63 | ) -> Self { 64 | self.style = Box::new(style_func); 65 | self 66 | } 67 | 68 | /// Set the baseline of the histogram 69 | pub fn baseline(mut self, baseline: A) -> Self 70 | where 71 | A: Clone, 72 | { 73 | self.baseline = Box::new(move |_| baseline.clone()); 74 | self 75 | } 76 | 77 | /// Set a function that defines variant baseline 78 | pub fn baseline_func(mut self, func: impl Fn(BR::ValueType) -> A + 'a) -> Self { 79 | self.baseline = Box::new(func); 80 | self 81 | } 82 | 83 | /// Set the margin for each bar 84 | pub fn margin(mut self, value: u32) -> Self { 85 | self.margin = value; 86 | self 87 | } 88 | 89 | /// Set the data iterator 90 | pub fn data>(mut self, iter: I) -> Self { 91 | let mut buffer = HashMap::::new(); 92 | for (x, y) in iter.into_iter() { 93 | *buffer.entry(x).or_insert_with(Default::default) += y; 94 | } 95 | self.iter = buffer.into_iter(); 96 | self 97 | } 98 | } 99 | 100 | pub trait UseDefaultParameter: Default { 101 | fn new() -> Self { 102 | Default::default() 103 | } 104 | } 105 | 106 | impl UseDefaultParameter for () {} 107 | 108 | impl<'a, BR, A> Histogram<'a, BR, A, Vertical> 109 | where 110 | BR: DiscreteRanged, 111 | BR::ValueType: Eq + Hash, 112 | A: AddAssign + Default + 'a, 113 | { 114 | /// Create a new histogram series. 115 | /// 116 | /// - `iter`: The data iterator 117 | /// - `margin`: The margin between bars 118 | /// - `style`: The style of bars 119 | /// 120 | /// Returns the newly created histogram series 121 | #[allow(clippy::redundant_closure)] 122 | pub fn new, I: IntoIterator>( 123 | iter: I, 124 | margin: u32, 125 | style: S, 126 | ) -> Self 127 | where 128 | BR::RangeParameter: UseDefaultParameter, 129 | { 130 | let mut buffer = HashMap::::new(); 131 | for (x, y) in iter.into_iter() { 132 | *buffer.entry(x).or_insert_with(Default::default) += y; 133 | } 134 | let style = style.into(); 135 | Self { 136 | style: Box::new(move |_, _| style.clone()), 137 | margin, 138 | iter: buffer.into_iter(), 139 | baseline: Box::new(|_| A::default()), 140 | br_param: BR::RangeParameter::new(), 141 | _p: PhantomData, 142 | } 143 | } 144 | 145 | pub fn vertical( 146 | parent: &ChartContext>, 147 | ) -> Self 148 | where 149 | ACoord: Ranged, 150 | { 151 | let dp = parent.as_coord_spec().x_spec().get_range_parameter(); 152 | 153 | Self::empty(dp) 154 | } 155 | } 156 | 157 | impl<'a, BR, A> Histogram<'a, BR, A, Horizontal> 158 | where 159 | BR: DiscreteRanged, 160 | BR::ValueType: Eq + Hash, 161 | A: AddAssign + Default + 'a, 162 | { 163 | pub fn horizontal( 164 | parent: &ChartContext>, 165 | ) -> Self 166 | where 167 | ACoord: Ranged, 168 | { 169 | let dp = parent.as_coord_spec().y_spec().get_range_parameter(); 170 | Self::empty(dp) 171 | } 172 | } 173 | 174 | impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Vertical> 175 | where 176 | BR: DiscreteRanged, 177 | BR::ValueType: Eq + Hash, 178 | A: AddAssign + Default, 179 | { 180 | type Item = Rectangle<(BR::ValueType, A)>; 181 | fn next(&mut self) -> Option { 182 | if let Some((x, y)) = self.iter.next() { 183 | let nx = BR::next_value(&x, &self.br_param); 184 | let base = (self.baseline)(BR::previous_value(&nx, &self.br_param)); 185 | let style = (self.style)(&x, &y); 186 | let mut rect = Rectangle::new([(x, y), (nx, base)], style); 187 | rect.set_margin(0, 0, self.margin, self.margin); 188 | return Some(rect); 189 | } 190 | None 191 | } 192 | } 193 | 194 | impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Horizontal> 195 | where 196 | BR: DiscreteRanged, 197 | BR::ValueType: Eq + Hash, 198 | A: AddAssign + Default, 199 | { 200 | type Item = Rectangle<(A, BR::ValueType)>; 201 | fn next(&mut self) -> Option { 202 | if let Some((y, x)) = self.iter.next() { 203 | let ny = BR::next_value(&y, &self.br_param); 204 | // With this trick we can avoid the clone trait bound 205 | let base = (self.baseline)(BR::previous_value(&ny, &self.br_param)); 206 | let style = (self.style)(&y, &x); 207 | let mut rect = Rectangle::new([(x, y), (base, ny)], style); 208 | rect.set_margin(self.margin, self.margin, 0, 0); 209 | return Some(rect); 210 | } 211 | None 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/series/line_series.rs: -------------------------------------------------------------------------------- 1 | use crate::drawing::DrawingBackend; 2 | use crate::element::{Circle, DynElement, IntoDynElement, PathElement}; 3 | use crate::style::ShapeStyle; 4 | use std::marker::PhantomData; 5 | 6 | /// The line series object, which takes an iterator of points in guest coordinate system 7 | /// and creates the element rendering the line plot 8 | pub struct LineSeries { 9 | style: ShapeStyle, 10 | data: Vec, 11 | point_idx: usize, 12 | point_size: u32, 13 | phantom: PhantomData, 14 | } 15 | 16 | impl Iterator for LineSeries { 17 | type Item = DynElement<'static, DB, Coord>; 18 | fn next(&mut self) -> Option { 19 | if !self.data.is_empty() { 20 | if self.point_size > 0 && self.point_idx < self.data.len() { 21 | let idx = self.point_idx; 22 | self.point_idx += 1; 23 | return Some( 24 | Circle::new(self.data[idx].clone(), self.point_size, self.style.clone()) 25 | .into_dyn(), 26 | ); 27 | } 28 | let mut data = vec![]; 29 | std::mem::swap(&mut self.data, &mut data); 30 | Some(PathElement::new(data, self.style.clone()).into_dyn()) 31 | } else { 32 | None 33 | } 34 | } 35 | } 36 | 37 | impl LineSeries { 38 | pub fn new, S: Into>(iter: I, style: S) -> Self { 39 | Self { 40 | style: style.into(), 41 | data: iter.into_iter().collect(), 42 | point_size: 0, 43 | point_idx: 0, 44 | phantom: PhantomData, 45 | } 46 | } 47 | 48 | pub fn point_size(mut self, size: u32) -> Self { 49 | self.point_size = size; 50 | self 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod test { 56 | use crate::prelude::*; 57 | 58 | #[test] 59 | fn test_line_series() { 60 | let drawing_area = create_mocked_drawing_area(200, 200, |m| { 61 | m.check_draw_path(|c, s, path| { 62 | assert_eq!(c, RED.to_rgba()); 63 | assert_eq!(s, 3); 64 | for i in 0..100 { 65 | assert_eq!(path[i], (i as i32 * 2, 200 - i as i32 * 2 - 1)); 66 | } 67 | }); 68 | 69 | m.drop_check(|b| { 70 | assert_eq!(b.num_draw_path_call, 1); 71 | assert_eq!(b.draw_count, 1); 72 | }); 73 | }); 74 | 75 | let mut chart = ChartBuilder::on(&drawing_area) 76 | .build_ranged(0..100, 0..100) 77 | .expect("Build chart error"); 78 | 79 | chart 80 | .draw_series(LineSeries::new( 81 | (0..100).map(|x| (x, x)), 82 | Into::::into(&RED).stroke_width(3), 83 | )) 84 | .expect("Drawing Error"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/series/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This module contains predefined types of series. 3 | The series in Plotters is actually an iterator of elements, which 4 | can be taken by `ChartContext::draw_series` function. 5 | 6 | This module defines some "iterator transformer", which transform the data 7 | iterator to the element iterator. 8 | 9 | Any type that implements iterator emitting drawable elements are acceptable series. 10 | So iterator combinator such as `map`, `zip`, etc can also be used. 11 | */ 12 | 13 | #[cfg(feature = "area_series")] 14 | mod area_series; 15 | #[cfg(feature = "histogram")] 16 | mod histogram; 17 | #[cfg(feature = "line_series")] 18 | mod line_series; 19 | #[cfg(feature = "point_series")] 20 | mod point_series; 21 | 22 | #[cfg(feature = "area_series")] 23 | pub use area_series::AreaSeries; 24 | #[cfg(feature = "histogram")] 25 | pub use histogram::Histogram; 26 | #[cfg(feature = "line_series")] 27 | pub use line_series::LineSeries; 28 | #[cfg(feature = "point_series")] 29 | pub use point_series::PointSeries; 30 | -------------------------------------------------------------------------------- /src/series/point_series.rs: -------------------------------------------------------------------------------- 1 | use crate::element::PointElement; 2 | use crate::style::{ShapeStyle, SizeDesc}; 3 | 4 | /// The point plot object, which takes an iterator of points in guest coordinate system 5 | /// and create an element for each point 6 | pub struct PointSeries<'a, Coord, I: IntoIterator, E, Size: SizeDesc + Clone> { 7 | style: ShapeStyle, 8 | size: Size, 9 | data_iter: I::IntoIter, 10 | make_point: &'a dyn Fn(Coord, Size, ShapeStyle) -> E, 11 | } 12 | 13 | impl<'a, Coord, I: IntoIterator, E, Size: SizeDesc + Clone> Iterator 14 | for PointSeries<'a, Coord, I, E, Size> 15 | { 16 | type Item = E; 17 | fn next(&mut self) -> Option { 18 | self.data_iter 19 | .next() 20 | .map(|x| (self.make_point)(x, self.size.clone(), self.style.clone())) 21 | } 22 | } 23 | 24 | impl<'a, Coord, I: IntoIterator, E, Size: SizeDesc + Clone> 25 | PointSeries<'a, Coord, I, E, Size> 26 | where 27 | E: PointElement, 28 | { 29 | /// Create a new point series with the element that implements point trait. 30 | /// You may also use a more general way to create a point series with `of_element` 31 | /// function which allows a customized element construction function 32 | pub fn new>(iter: I, size: Size, style: S) -> Self { 33 | Self { 34 | data_iter: iter.into_iter(), 35 | size, 36 | style: style.into(), 37 | make_point: &|a, b, c| E::make_point(a, b, c), 38 | } 39 | } 40 | } 41 | 42 | impl<'a, Coord, I: IntoIterator, E, Size: SizeDesc + Clone> 43 | PointSeries<'a, Coord, I, E, Size> 44 | { 45 | /// Create a new point series. Similar to `PointSeries::new` but it doesn't 46 | /// requires the element implements point trait. So instead of using the point 47 | /// constructor, it uses the customized function for element creation 48 | pub fn of_element, F: Fn(Coord, Size, ShapeStyle) -> E>( 49 | iter: I, 50 | size: Size, 51 | style: S, 52 | cons: &'a F, 53 | ) -> Self { 54 | Self { 55 | data_iter: iter.into_iter(), 56 | size, 57 | style: style.into(), 58 | make_point: cons, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/style/color.rs: -------------------------------------------------------------------------------- 1 | use super::palette::Palette; 2 | use super::ShapeStyle; 3 | 4 | use std::marker::PhantomData; 5 | 6 | /// Any color representation 7 | pub trait Color { 8 | /// Convert the RGB representation to the standard RGB tuple 9 | fn rgb(&self) -> (u8, u8, u8); 10 | 11 | /// Get the alpha channel of the color 12 | fn alpha(&self) -> f64; 13 | 14 | /// Mix the color with given opacity 15 | fn mix(&self, value: f64) -> RGBAColor { 16 | let (r, g, b) = self.rgb(); 17 | let a = self.alpha() * value; 18 | RGBAColor(r, g, b, a) 19 | } 20 | 21 | /// Convert the color into the RGBA color which is internally used by Plotters 22 | fn to_rgba(&self) -> RGBAColor { 23 | let (r, g, b) = self.rgb(); 24 | let a = self.alpha(); 25 | RGBAColor(r, g, b, a) 26 | } 27 | 28 | /// Make a filled style form the color 29 | fn filled(&self) -> ShapeStyle 30 | where 31 | Self: Sized, 32 | { 33 | Into::::into(self).filled() 34 | } 35 | 36 | /// Make a shape style with stroke width from a color 37 | fn stroke_width(&self, width: u32) -> ShapeStyle 38 | where 39 | Self: Sized, 40 | { 41 | Into::::into(self).stroke_width(width) 42 | } 43 | } 44 | 45 | /// The RGBA representation of the color, Plotters use RGBA as the internal representation 46 | /// of color 47 | #[derive(Clone, PartialEq, Debug)] 48 | pub struct RGBAColor(pub(super) u8, pub(super) u8, pub(super) u8, pub(super) f64); 49 | 50 | impl Color for RGBAColor { 51 | #[inline(always)] 52 | fn rgb(&self) -> (u8, u8, u8) { 53 | (self.0, self.1, self.2) 54 | } 55 | 56 | #[inline(always)] 57 | fn alpha(&self) -> f64 { 58 | self.3 59 | } 60 | 61 | fn to_rgba(&self) -> RGBAColor { 62 | self.clone() 63 | } 64 | } 65 | 66 | /// Color without alpha channel 67 | pub trait SimpleColor { 68 | fn rgb(&self) -> (u8, u8, u8); 69 | } 70 | 71 | impl Color for T { 72 | fn rgb(&self) -> (u8, u8, u8) { 73 | SimpleColor::rgb(self) 74 | } 75 | 76 | fn alpha(&self) -> f64 { 77 | 1.0 78 | } 79 | } 80 | 81 | /// A color in the given palette 82 | pub struct PaletteColor(usize, PhantomData

); 83 | 84 | impl PaletteColor

{ 85 | /// Pick a color from the palette 86 | pub fn pick(idx: usize) -> PaletteColor

{ 87 | PaletteColor(idx % P::COLORS.len(), PhantomData) 88 | } 89 | } 90 | 91 | impl SimpleColor for PaletteColor

{ 92 | fn rgb(&self) -> (u8, u8, u8) { 93 | P::COLORS[self.0] 94 | } 95 | } 96 | 97 | /// The color described by its RGB value 98 | #[derive(Debug)] 99 | pub struct RGBColor(pub u8, pub u8, pub u8); 100 | 101 | impl SimpleColor for RGBColor { 102 | fn rgb(&self) -> (u8, u8, u8) { 103 | (self.0, self.1, self.2) 104 | } 105 | } 106 | 107 | /// The color described by HSL color space 108 | pub struct HSLColor(pub f64, pub f64, pub f64); 109 | 110 | impl SimpleColor for HSLColor { 111 | #[allow(clippy::many_single_char_names)] 112 | fn rgb(&self) -> (u8, u8, u8) { 113 | let (h, s, l) = ( 114 | self.0.min(1.0).max(0.0), 115 | self.1.min(1.0).max(0.0), 116 | self.2.min(1.0).max(0.0), 117 | ); 118 | 119 | if s == 0.0 { 120 | let value = (l * 255.0).round() as u8; 121 | return (value, value, value); 122 | } 123 | 124 | let q = if l < 0.5 { 125 | l * (1.0 + s) 126 | } else { 127 | l + s - l * s 128 | }; 129 | let p = 2.0 * l - q; 130 | 131 | let cvt = |mut t| { 132 | if t < 0.0 { 133 | t += 1.0; 134 | } 135 | if t > 1.0 { 136 | t -= 1.0; 137 | } 138 | let value = if t < 1.0 / 6.0 { 139 | p + (q - p) * 6.0 * t 140 | } else if t < 1.0 / 2.0 { 141 | q 142 | } else if t < 2.0 / 3.0 { 143 | p + (q - p) * (2.0 / 3.0 - t) * 6.0 144 | } else { 145 | p 146 | }; 147 | (value * 255.0).round() as u8 148 | }; 149 | 150 | (cvt(h + 1.0 / 3.0), cvt(h), cvt(h - 1.0 / 3.0)) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/style/colors.rs: -------------------------------------------------------------------------------- 1 | //! Basic predefined colors. 2 | use super::{RGBAColor, RGBColor}; 3 | 4 | macro_rules! predefined_color { 5 | ($name:ident, $r:expr, $g:expr, $b:expr, $doc:expr) => { 6 | #[doc = $doc] 7 | pub const $name: RGBColor = RGBColor($r, $g, $b); 8 | }; 9 | 10 | ($name:ident, $r:expr, $g:expr, $b:expr, $a: expr, $doc:expr) => { 11 | #[doc = $doc] 12 | pub const $name: RGBAColor = RGBAColor($r, $g, $b, $a); 13 | } 14 | } 15 | 16 | predefined_color!(WHITE, 255, 255, 255, "The predefined white color"); 17 | predefined_color!(BLACK, 0, 0, 0, "The predefined black color"); 18 | predefined_color!(RED, 255, 0, 0, "The predefined red color"); 19 | predefined_color!(GREEN, 0, 255, 0, "The predefined green color"); 20 | predefined_color!(BLUE, 0, 0, 255, "The predefined blue color"); 21 | predefined_color!(YELLOW, 255, 255, 0, "The predefined yellow color"); 22 | predefined_color!(CYAN, 0, 255, 255, "The predefined cyan color"); 23 | predefined_color!(MAGENTA, 255, 0, 255, "The predefined magenta color"); 24 | predefined_color!(TRANSPARENT, 0, 0, 0, 0.0, "The predefined transparent"); 25 | 26 | /// Predefined Color definitions using the [palette](https://docs.rs/palette/) color types 27 | #[cfg(feature = "palette_ext")] 28 | pub mod palette_ext { 29 | use palette::rgb::Srgb; 30 | use palette::Alpha; 31 | 32 | use std::marker::PhantomData; 33 | 34 | macro_rules! predefined_color_pal { 35 | ($name:ident, $r:expr, $g:expr, $b:expr, $doc:expr) => { 36 | #[doc = $doc] 37 | pub const $name: Srgb = predefined_color_pal!(@gen_c $r, $g, $b); 38 | }; 39 | ($name:ident, $r:expr, $g:expr, $b:expr, $a:expr, $doc:expr) => { 40 | #[doc = $doc] 41 | pub const $name: Alpha, f64> = Alpha{ alpha: $a, color: predefined_color_pal!(@gen_c $r, $g, $b) }; 42 | }; 43 | (@gen_c $r:expr, $g:expr, $b:expr) => { 44 | Srgb { red: $r, green: $g, blue: $b, standard: PhantomData } 45 | }; 46 | } 47 | 48 | predefined_color_pal!(WHITE, 255, 255, 255, "The predefined white color"); 49 | predefined_color_pal!(BLACK, 0, 0, 0, "The predefined black color"); 50 | predefined_color_pal!(RED, 255, 0, 0, "The predefined red color"); 51 | predefined_color_pal!(GREEN, 0, 255, 0, "The predefined green color"); 52 | predefined_color_pal!(BLUE, 0, 0, 255, "The predefined blue color"); 53 | predefined_color_pal!(YELLOW, 255, 255, 0, "The predefined yellow color"); 54 | predefined_color_pal!(CYAN, 0, 255, 255, "The predefined cyan color"); 55 | predefined_color_pal!(MAGENTA, 255, 0, 255, "The predefined magenta color"); 56 | predefined_color_pal!(TRANSPARENT, 0, 0, 0, 0.0, "The predefined transparent"); 57 | } 58 | -------------------------------------------------------------------------------- /src/style/font/mod.rs: -------------------------------------------------------------------------------- 1 | /// The implementation of an actual font implementation 2 | /// 3 | /// This exists since for the image rendering task, we want to use 4 | /// the system font. But in wasm application, we want the browser 5 | /// to handle all the font issue. 6 | /// 7 | /// Thus we need different mechanism for the font implementation 8 | 9 | #[cfg(all(not(target_arch = "wasm32"), feature = "ttf"))] 10 | mod ttf; 11 | #[cfg(all(not(target_arch = "wasm32"), feature = "ttf"))] 12 | use ttf::FontDataInternal; 13 | 14 | #[cfg(all(not(target_arch = "wasm32"), not(feature = "ttf")))] 15 | mod naive; 16 | #[cfg(all(not(target_arch = "wasm32"), not(feature = "ttf")))] 17 | use naive::FontDataInternal; 18 | 19 | #[cfg(target_arch = "wasm32")] 20 | mod web; 21 | #[cfg(target_arch = "wasm32")] 22 | use web::FontDataInternal; 23 | 24 | mod font_desc; 25 | pub use font_desc::*; 26 | 27 | pub type LayoutBox = ((i32, i32), (i32, i32)); 28 | 29 | pub trait FontData: Clone { 30 | type ErrorType: Sized + std::error::Error + Clone; 31 | fn new(family: FontFamily, style: FontStyle) -> Result; 32 | fn estimate_layout(&self, size: f64, text: &str) -> Result; 33 | fn draw Result<(), E>>( 34 | &self, 35 | _pos: (i32, i32), 36 | _size: f64, 37 | _text: &str, 38 | _draw: DrawFunc, 39 | ) -> Result, Self::ErrorType> { 40 | panic!("The font implementation is unable to draw text"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/style/font/naive.rs: -------------------------------------------------------------------------------- 1 | use super::{FontData, FontFamily, FontStyle, LayoutBox}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct FontError; 5 | 6 | impl std::fmt::Display for FontError { 7 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 8 | write!(fmt, "General Error")?; 9 | Ok(()) 10 | } 11 | } 12 | 13 | impl std::error::Error for FontError {} 14 | 15 | #[derive(Clone)] 16 | pub struct FontDataInternal(String, String); 17 | 18 | impl FontData for FontDataInternal { 19 | type ErrorType = FontError; 20 | fn new(family: FontFamily, style: FontStyle) -> Result { 21 | Ok(FontDataInternal( 22 | family.as_str().into(), 23 | style.as_str().into(), 24 | )) 25 | } 26 | 27 | /// Note: This is only a crude estimatation, since for some backend such as SVG, we have no way to 28 | /// know the real size of the text anyway. Thus using font-kit is an overkill and doesn't helps 29 | /// the layout. 30 | fn estimate_layout(&self, size: f64, text: &str) -> Result { 31 | let em = size / 1.24 / 1.24; 32 | Ok(( 33 | (0, -em.round() as i32), 34 | ( 35 | (em * 0.7 * text.len() as f64).round() as i32, 36 | (em * 0.24).round() as i32, 37 | ), 38 | )) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/style/font/web.rs: -------------------------------------------------------------------------------- 1 | use super::{FontData, FontFamily, FontStyle, LayoutBox}; 2 | use wasm_bindgen::JsCast; 3 | use web_sys::{window, HtmlElement}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub enum FontError { 7 | UnknownError, 8 | } 9 | 10 | impl std::fmt::Display for FontError { 11 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 12 | match self { 13 | _ => write!(fmt, "Unknown error"), 14 | } 15 | } 16 | } 17 | 18 | impl std::error::Error for FontError {} 19 | 20 | #[derive(Clone)] 21 | pub struct FontDataInternal(String, String); 22 | 23 | impl FontData for FontDataInternal { 24 | type ErrorType = FontError; 25 | fn new(family: FontFamily, style: FontStyle) -> Result { 26 | Ok(FontDataInternal( 27 | family.as_str().into(), 28 | style.as_str().into(), 29 | )) 30 | } 31 | fn estimate_layout(&self, size: f64, text: &str) -> Result { 32 | let window = window().unwrap(); 33 | let document = window.document().unwrap(); 34 | let body = document.body().unwrap(); 35 | let span = document.create_element("span").unwrap(); 36 | span.set_text_content(Some(text)); 37 | span.set_attribute("style", &format!("display: inline-block; font-family:{}; font-size: {}px; position: fixed; top: 100%", self.0, size)).unwrap(); 38 | let span = span.into(); 39 | body.append_with_node_1(&span).unwrap(); 40 | let elem = JsCast::dyn_into::(span).unwrap(); 41 | let height = elem.offset_height() as i32; 42 | let width = elem.offset_width() as i32; 43 | elem.remove(); 44 | Ok(((0, 0), (width, height))) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/style/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | The style for shapes and text, font, color, etc. 3 | */ 4 | pub mod color; 5 | pub mod colors; 6 | mod font; 7 | mod palette; 8 | mod shape; 9 | mod size; 10 | mod text; 11 | 12 | #[cfg(feature = "palette_ext")] 13 | mod palette_ext; 14 | 15 | /// Definitions of palettes of accessibility 16 | pub use self::palette::*; 17 | pub use color::{Color, HSLColor, PaletteColor, RGBAColor, RGBColor, SimpleColor}; 18 | pub use colors::{BLACK, BLUE, CYAN, GREEN, MAGENTA, RED, TRANSPARENT, WHITE, YELLOW}; 19 | pub use font::{ 20 | FontDesc, FontError, FontFamily, FontResult, FontStyle, FontTransform, IntoFont, LayoutBox, 21 | }; 22 | pub use shape::ShapeStyle; 23 | pub use size::{AsRelative, RelativeSize, SizeDesc}; 24 | pub use text::text_anchor; 25 | pub use text::{IntoTextStyle, TextStyle}; 26 | -------------------------------------------------------------------------------- /src/style/palette.rs: -------------------------------------------------------------------------------- 1 | use super::color::PaletteColor; 2 | 3 | pub trait Palette { 4 | const COLORS: &'static [(u8, u8, u8)]; 5 | fn pick(idx: usize) -> PaletteColor 6 | where 7 | Self: Sized, 8 | { 9 | PaletteColor::::pick(idx) 10 | } 11 | } 12 | 13 | /// The palette of 99% accessibility 14 | pub struct Palette99; 15 | /// The palette of 99.99% accessibility 16 | pub struct Palette9999; 17 | /// The palette of 100% accessibility 18 | pub struct Palette100; 19 | 20 | impl Palette for Palette99 { 21 | const COLORS: &'static [(u8, u8, u8)] = &[ 22 | (34, 195, 46), 23 | (255, 255, 0), 24 | (0, 128, 128), 25 | (255, 0, 0), 26 | (70, 240, 240), 27 | (230, 25, 75), 28 | (60, 180, 75), 29 | (245, 130, 48), 30 | (240, 50, 230), 31 | (210, 245, 60), 32 | (250, 190, 190), 33 | (0, 128, 128), 34 | (230, 190, 255), 35 | (170, 110, 40), 36 | (255, 250, 200), 37 | (128, 0, 0), 38 | (170, 255, 195), 39 | (128, 128, 0), 40 | (255, 215, 180), 41 | (0, 0, 128), 42 | (128, 128, 128), 43 | (0, 0, 0), 44 | ]; 45 | } 46 | 47 | impl Palette for Palette9999 { 48 | const COLORS: &'static [(u8, u8, u8)] = &[ 49 | (255, 225, 25), 50 | (0, 130, 200), 51 | (245, 130, 48), 52 | (250, 190, 190), 53 | (230, 190, 255), 54 | (128, 0, 0), 55 | (0, 0, 128), 56 | (128, 128, 128), 57 | (0, 0, 0), 58 | ]; 59 | } 60 | 61 | impl Palette for Palette100 { 62 | const COLORS: &'static [(u8, u8, u8)] = 63 | &[(255, 225, 25), (0, 130, 200), (128, 128, 128), (0, 0, 0)]; 64 | } 65 | -------------------------------------------------------------------------------- /src/style/palette_ext.rs: -------------------------------------------------------------------------------- 1 | use num_traits::Float; 2 | 3 | use palette::encoding::Linear; 4 | use palette::luma::{Luma, LumaStandard}; 5 | use palette::rgb::RgbStandard; 6 | use palette::rgb::{Rgb, RgbSpace}; 7 | use palette::white_point::D65; 8 | use palette::{Alpha, Component, Hsl, Hsv, Hwb, Lab, Lch, LinSrgb, Xyz, Yxy}; 9 | 10 | use super::color::Color; 11 | 12 | impl Color for Rgb { 13 | fn rgb(&self) -> (u8, u8, u8) { 14 | self.into_format::().into_components() 15 | } 16 | 17 | #[inline] 18 | fn alpha(&self) -> f64 { 19 | 1.0 20 | } 21 | } 22 | 23 | impl Color for Luma { 24 | fn rgb(&self) -> (u8, u8, u8) { 25 | let (luma,) = self.into_format::().into_components(); 26 | (luma, luma, luma) 27 | } 28 | 29 | #[inline] 30 | fn alpha(&self) -> f64 { 31 | 1.0 32 | } 33 | } 34 | 35 | impl Color for Hsl { 36 | fn rgb(&self) -> (u8, u8, u8) { 37 | Rgb::, T>::from(*self) 38 | .into_format::() 39 | .into_components() 40 | } 41 | 42 | #[inline] 43 | fn alpha(&self) -> f64 { 44 | 1.0 45 | } 46 | } 47 | 48 | impl Color for Hsv { 49 | fn rgb(&self) -> (u8, u8, u8) { 50 | Rgb::, T>::from(*self) 51 | .into_format::() 52 | .into_components() 53 | } 54 | 55 | #[inline] 56 | fn alpha(&self) -> f64 { 57 | 1.0 58 | } 59 | } 60 | 61 | impl Color for Hwb { 62 | fn rgb(&self) -> (u8, u8, u8) { 63 | Rgb::, T>::from(*self) 64 | .into_format::() 65 | .into_components() 66 | } 67 | 68 | #[inline] 69 | fn alpha(&self) -> f64 { 70 | 1.0 71 | } 72 | } 73 | 74 | impl Color for Lab { 75 | fn rgb(&self) -> (u8, u8, u8) { 76 | LinSrgb::::from(*self) 77 | .into_format::() 78 | .into_components() 79 | } 80 | 81 | #[inline] 82 | fn alpha(&self) -> f64 { 83 | 1.0 84 | } 85 | } 86 | 87 | impl Color for Lch { 88 | fn rgb(&self) -> (u8, u8, u8) { 89 | LinSrgb::::from(*self) 90 | .into_format::() 91 | .into_components() 92 | } 93 | 94 | #[inline] 95 | fn alpha(&self) -> f64 { 96 | 1.0 97 | } 98 | } 99 | 100 | impl Color for Xyz { 101 | fn rgb(&self) -> (u8, u8, u8) { 102 | LinSrgb::::from(*self) 103 | .into_format::() 104 | .into_components() 105 | } 106 | 107 | #[inline] 108 | fn alpha(&self) -> f64 { 109 | 1.0 110 | } 111 | } 112 | 113 | impl Color for Yxy { 114 | fn rgb(&self) -> (u8, u8, u8) { 115 | LinSrgb::::from(*self) 116 | .into_format::() 117 | .into_components() 118 | } 119 | 120 | #[inline] 121 | fn alpha(&self) -> f64 { 122 | 1.0 123 | } 124 | } 125 | 126 | impl Color for Alpha { 127 | #[inline] 128 | fn rgb(&self) -> (u8, u8, u8) { 129 | self.color.rgb() 130 | } 131 | 132 | #[inline] 133 | fn alpha(&self) -> f64 { 134 | self.alpha.convert() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/style/shape.rs: -------------------------------------------------------------------------------- 1 | use super::color::{Color, RGBAColor}; 2 | 3 | /// Style for any of shape 4 | #[derive(Clone)] 5 | pub struct ShapeStyle { 6 | pub color: RGBAColor, 7 | pub filled: bool, 8 | pub stroke_width: u32, 9 | } 10 | 11 | impl ShapeStyle { 12 | /// Make a filled shape style 13 | pub fn filled(&self) -> Self { 14 | Self { 15 | color: self.color.to_rgba(), 16 | filled: true, 17 | stroke_width: self.stroke_width, 18 | } 19 | } 20 | 21 | pub fn stroke_width(&self, width: u32) -> Self { 22 | Self { 23 | color: self.color.to_rgba(), 24 | filled: self.filled, 25 | stroke_width: width, 26 | } 27 | } 28 | } 29 | 30 | impl<'a, T: Color> From<&'a T> for ShapeStyle { 31 | fn from(f: &'a T) -> Self { 32 | ShapeStyle { 33 | color: f.to_rgba(), 34 | filled: false, 35 | stroke_width: 1, 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/style/size.rs: -------------------------------------------------------------------------------- 1 | use crate::coord::CoordTranslate; 2 | use crate::drawing::DrawingArea; 3 | use crate::drawing::DrawingBackend; 4 | 5 | /// The trait indicates that the type has a dimensional data. 6 | /// This is the abstraction for the relative sizing model. 7 | /// A relative sizing value is able to be converted into a concrete size 8 | /// when coupling with a type with `HasDimension` type. 9 | pub trait HasDimension { 10 | /// Get the dimensional data for this object 11 | fn dim(&self) -> (u32, u32); 12 | } 13 | 14 | impl HasDimension for T { 15 | fn dim(&self) -> (u32, u32) { 16 | self.get_size() 17 | } 18 | } 19 | 20 | impl HasDimension for DrawingArea { 21 | fn dim(&self) -> (u32, u32) { 22 | self.dim_in_pixel() 23 | } 24 | } 25 | 26 | impl HasDimension for (u32, u32) { 27 | fn dim(&self) -> (u32, u32) { 28 | *self 29 | } 30 | } 31 | 32 | /// The trait that describes a size, it may be a relative size which the 33 | /// size is determined by the parent size, e.g., 10% of the parent width 34 | pub trait SizeDesc { 35 | /// Convert the size into the number of pixels 36 | /// 37 | /// - `parent`: The reference to the parent container of this size 38 | /// - **returns**: The number of pixels 39 | fn in_pixels(&self, parent: &T) -> i32; 40 | } 41 | 42 | impl SizeDesc for i32 { 43 | fn in_pixels(&self, _parent: &D) -> i32 { 44 | *self 45 | } 46 | } 47 | 48 | impl SizeDesc for u32 { 49 | fn in_pixels(&self, _parent: &D) -> i32 { 50 | *self as i32 51 | } 52 | } 53 | 54 | /// Describes a relative size, might be 55 | /// 1. portion of height 56 | /// 2. portion of width 57 | /// 3. portion of the minimal of height and weight 58 | pub enum RelativeSize { 59 | /// Percentage height 60 | Height(f64), 61 | /// Percentage width 62 | Width(f64), 63 | /// Percentage of either height or width, which is smaller 64 | Smaller(f64), 65 | } 66 | 67 | impl RelativeSize { 68 | /// Set the lower bound of the relative size. 69 | /// 70 | /// - `min_sz`: The minimal size the relative size can be in pixels 71 | /// - **returns**: The relative size with the bound 72 | pub fn min(self, min_sz: i32) -> RelativeSizeWithBound { 73 | RelativeSizeWithBound { 74 | size: self, 75 | min: Some(min_sz), 76 | max: None, 77 | } 78 | } 79 | 80 | /// Set the upper bound of the relative size 81 | /// 82 | /// - `max_size`: The maximum size in pixels for this relative size 83 | /// - **returns** The relative size with the upper bound 84 | pub fn max(self, max_sz: i32) -> RelativeSizeWithBound { 85 | RelativeSizeWithBound { 86 | size: self, 87 | max: Some(max_sz), 88 | min: None, 89 | } 90 | } 91 | } 92 | 93 | impl SizeDesc for RelativeSize { 94 | fn in_pixels(&self, parent: &D) -> i32 { 95 | let (w, h) = parent.dim(); 96 | match self { 97 | RelativeSize::Width(p) => *p * f64::from(w), 98 | RelativeSize::Height(p) => *p * f64::from(h), 99 | RelativeSize::Smaller(p) => *p * f64::from(w.min(h)), 100 | } 101 | .round() as i32 102 | } 103 | } 104 | 105 | /// Allows a value turns into a relative size 106 | pub trait AsRelative: Into { 107 | /// Make the value a relative size of percentage of width 108 | fn percent_width(self) -> RelativeSize { 109 | RelativeSize::Width(self.into() / 100.0) 110 | } 111 | /// Make the value a relative size of percentage of height 112 | fn percent_height(self) -> RelativeSize { 113 | RelativeSize::Height(self.into() / 100.0) 114 | } 115 | /// Make the value a relative size of percentage of minimal of height and width 116 | fn percent(self) -> RelativeSize { 117 | RelativeSize::Smaller(self.into() / 100.0) 118 | } 119 | } 120 | 121 | impl> AsRelative for T {} 122 | 123 | /// The struct describes a relative size with upper bound and lower bound 124 | pub struct RelativeSizeWithBound { 125 | size: RelativeSize, 126 | min: Option, 127 | max: Option, 128 | } 129 | 130 | impl RelativeSizeWithBound { 131 | /// Set the lower bound of the bounded relative size 132 | /// 133 | /// - `min_sz`: The lower bound of this size description 134 | /// - **returns**: The newly created size description with the bound 135 | pub fn min(mut self, min_sz: i32) -> RelativeSizeWithBound { 136 | self.min = Some(min_sz); 137 | self 138 | } 139 | 140 | /// Set the upper bound of the bounded relative size 141 | /// 142 | /// - `min_sz`: The upper bound of this size description 143 | /// - **returns**: The newly created size description with the bound 144 | pub fn max(mut self, max_sz: i32) -> RelativeSizeWithBound { 145 | self.max = Some(max_sz); 146 | self 147 | } 148 | } 149 | 150 | impl SizeDesc for RelativeSizeWithBound { 151 | fn in_pixels(&self, parent: &D) -> i32 { 152 | let size = self.size.in_pixels(parent); 153 | let size_lower_capped = self.min.map_or(size, |x| x.max(size)); 154 | self.max.map_or(size_lower_capped, |x| x.min(size)) 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod test { 160 | use super::*; 161 | #[test] 162 | fn test_relative_size() { 163 | let size = (10).percent_height(); 164 | assert_eq!(size.in_pixels(&(100, 200)), 20); 165 | 166 | let size = (10).percent_width(); 167 | assert_eq!(size.in_pixels(&(100, 200)), 10); 168 | 169 | let size = (-10).percent_width(); 170 | assert_eq!(size.in_pixels(&(100, 200)), -10); 171 | 172 | let size = (10).percent_width().min(30); 173 | assert_eq!(size.in_pixels(&(100, 200)), 30); 174 | assert_eq!(size.in_pixels(&(400, 200)), 40); 175 | 176 | let size = (10).percent(); 177 | assert_eq!(size.in_pixels(&(100, 200)), 10); 178 | assert_eq!(size.in_pixels(&(400, 200)), 20); 179 | } 180 | } 181 | --------------------------------------------------------------------------------