aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Kotur <qtr@google.com>2021-03-16 20:53:15 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-03-16 20:53:15 +0000
commit59b25e0dc5d4ca639c8d46b53a90417f37976169 (patch)
treea2b9db8395d7a587819e58039f5a575290b1a427
parent97853433079c3108a7c92b68a671575f945dd5a9 (diff)
parent15d57486a2b5313937d3dda5f0b74f3d7c4675b7 (diff)
downloadplotters-59b25e0dc5d4ca639c8d46b53a90417f37976169.tar.gz
Initial import of plotters-0.2.15. am: 00e4d270b2 am: fa31b59d73 am: 92238a6cd1 am: 15d57486a2
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/plotters/+/1621407 Change-Id: I1c0edc0a5d6d263892cf85d4ff5adf9d484fe22b
-rw-r--r--.cargo/config2
-rw-r--r--.cargo_vcs_info.json5
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md18
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md17
-rw-r--r--.github/ISSUE_TEMPLATE/general-questions.md10
-rw-r--r--.github/workflows/push-check.yml70
-rw-r--r--.github/workflows/wasm.yml18
-rw-r--r--.gitignore11
-rw-r--r--.gitmodules3
-rw-r--r--.travis.yml21
-rw-r--r--CHANGELOG.md219
-rw-r--r--CONTRIBUTING.md131
-rw-r--r--Cargo.toml123
-rw-r--r--Cargo.toml.orig92
-rw-r--r--README.md545
-rw-r--r--benches/benches/data.rs37
-rw-r--r--benches/benches/mod.rs3
-rw-r--r--benches/benches/parallel.rs180
-rw-r--r--benches/benches/rasterizer.rs204
-rw-r--r--benches/main.rs9
-rw-r--r--examples/README.md11
-rw-r--r--examples/animation.rs58
-rw-r--r--examples/area-chart.rs47
-rw-r--r--examples/blit-bitmap.rs33
-rw-r--r--examples/boxplot.rs222
-rw-r--r--examples/chart.rs98
-rw-r--r--examples/console.rs195
-rw-r--r--examples/errorbar.rs90
-rw-r--r--examples/histogram.rs36
-rw-r--r--examples/mandelbrot.rs63
-rw-r--r--examples/matshow.rs52
-rw-r--r--examples/normal-dist.rs66
-rw-r--r--examples/normal-dist2.rs76
-rw-r--r--examples/relative_size.rs49
-rw-r--r--examples/sierpinski.rs33
-rw-r--r--examples/slc-temp.rs167
-rw-r--r--examples/snowflake.rs50
-rw-r--r--examples/stock.rs69
-rw-r--r--examples/two-scales.rs52
-rw-r--r--plotters-doc-data/0.pngbin0 -> 27453 bytes
-rw-r--r--plotters-doc-data/1.pngbin0 -> 1192 bytes
-rw-r--r--plotters-doc-data/2.pngbin0 -> 1433 bytes
-rw-r--r--plotters-doc-data/3.pngbin0 -> 2204 bytes
-rw-r--r--plotters-doc-data/4.pngbin0 -> 7844 bytes
-rw-r--r--plotters-doc-data/5.pngbin0 -> 38309 bytes
-rw-r--r--plotters-doc-data/animation.gifbin0 -> 83710 bytes
-rw-r--r--plotters-doc-data/area-chart.pngbin0 -> 56891 bytes
-rw-r--r--plotters-doc-data/blit-bitmap.pngbin0 -> 16368 bytes
-rw-r--r--plotters-doc-data/boxplot.svg432
-rw-r--r--plotters-doc-data/console-example.pngbin0 -> 28667 bytes
-rw-r--r--plotters-doc-data/element-0.pngbin0 -> 2261 bytes
-rw-r--r--plotters-doc-data/element-1.pngbin0 -> 2261 bytes
-rw-r--r--plotters-doc-data/element-3.pngbin0 -> 8263 bytes
-rw-r--r--plotters-doc-data/errorbar.pngbin0 -> 216780 bytes
-rw-r--r--plotters-doc-data/histogram.pngbin0 -> 16391 bytes
-rw-r--r--plotters-doc-data/mandelbrot.pngbin0 -> 83791 bytes
-rw-r--r--plotters-doc-data/matshow.pngbin0 -> 29739 bytes
-rw-r--r--plotters-doc-data/normal-dist.pngbin0 -> 76806 bytes
-rw-r--r--plotters-doc-data/normal-dist2.pngbin0 -> 41320 bytes
-rw-r--r--plotters-doc-data/relative_size.pngbin0 -> 56086 bytes
-rw-r--r--plotters-doc-data/sample.pngbin0 -> 68038 bytes
-rw-r--r--plotters-doc-data/sierpinski.pngbin0 -> 62541 bytes
-rw-r--r--plotters-doc-data/slc-temp.pngbin0 -> 86845 bytes
-rw-r--r--plotters-doc-data/snowflake.pngbin0 -> 24585 bytes
-rw-r--r--plotters-doc-data/stock.pngbin0 -> 33306 bytes
-rw-r--r--plotters-doc-data/twoscale.pngbin0 -> 68012 bytes
-rwxr-xr-xpublish.sh44
-rw-r--r--src/chart/builder.rs345
-rw-r--r--src/chart/context.rs792
-rw-r--r--src/chart/dual_coord.rs231
-rw-r--r--src/chart/mesh.rs431
-rw-r--r--src/chart/mod.rs25
-rw-r--r--src/chart/series.rs191
-rw-r--r--src/coord/category.rs209
-rw-r--r--src/coord/datetime.rs954
-rw-r--r--src/coord/logarithmic.rs148
-rw-r--r--src/coord/mod.rs120
-rw-r--r--src/coord/numeric.rs395
-rw-r--r--src/coord/ranged.rs397
-rw-r--r--src/data/data_range.rs42
-rw-r--r--src/data/float.rs104
-rw-r--r--src/data/mod.rs12
-rw-r--r--src/data/quartiles.rs127
-rw-r--r--src/drawing/area.rs848
-rw-r--r--src/drawing/backend.rs284
-rw-r--r--src/drawing/backend_impl/bitmap.rs1607
-rw-r--r--src/drawing/backend_impl/cairo.rs567
-rw-r--r--src/drawing/backend_impl/canvas.rs530
-rw-r--r--src/drawing/backend_impl/mocked.rs276
-rw-r--r--src/drawing/backend_impl/mod.rs46
-rw-r--r--src/drawing/backend_impl/piston.rs206
-rw-r--r--src/drawing/backend_impl/svg.rs832
-rw-r--r--src/drawing/mod.rs31
-rw-r--r--src/drawing/rasterizer/circle.rs67
-rw-r--r--src/drawing/rasterizer/line.rs126
-rw-r--r--src/drawing/rasterizer/mod.rs24
-rw-r--r--src/drawing/rasterizer/path.rs115
-rw-r--r--src/drawing/rasterizer/polygon.rs245
-rw-r--r--src/drawing/rasterizer/rect.rs60
-rw-r--r--src/element/basic_shapes.rs348
-rw-r--r--src/element/boxplot.rs283
-rw-r--r--src/element/candlestick.rs100
-rw-r--r--src/element/composable.rs200
-rw-r--r--src/element/dynelem.rs84
-rw-r--r--src/element/errorbar.rs147
-rw-r--r--src/element/image.rs213
-rw-r--r--src/element/mod.rs218
-rw-r--r--src/element/points.rs125
-rw-r--r--src/element/text.rs242
-rw-r--r--src/evcxr.rs40
-rw-r--r--src/lib.rs764
-rw-r--r--src/series/area_series.rs62
-rw-r--r--src/series/histogram.rs213
-rw-r--r--src/series/line_series.rs86
-rw-r--r--src/series/mod.rs29
-rw-r--r--src/series/point_series.rs61
-rw-r--r--src/style/color.rs152
-rw-r--r--src/style/colors.rs57
-rw-r--r--src/style/font/font_desc.rs296
-rw-r--r--src/style/font/mod.rs42
-rw-r--r--src/style/font/naive.rs40
-rw-r--r--src/style/font/ttf.rs209
-rw-r--r--src/style/font/web.rs46
-rw-r--r--src/style/mod.rs25
-rw-r--r--src/style/palette.rs63
-rw-r--r--src/style/palette_ext.rs136
-rw-r--r--src/style/shape.rs38
-rw-r--r--src/style/size.rs180
-rw-r--r--src/style/text.rs220
129 files changed, 18767 insertions, 0 deletions
diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 0000000..4ec2f3b
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,2 @@
+[target.wasm32-unknown-unknown]
+runner = 'wasm-bindgen-test-runner'
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..116e0de
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,5 @@
+{
+ "git": {
+ "sha1": "42dc17dea6d73974b906a8523658db5d6ef11761"
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..af039b6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,18 @@
+---
+name: Bug report
+about: About unexpected behaviors
+title: "[BUG] "
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+Describe what is expected, what you actually get.
+It would be nice to have screenshot or result image uploaded
+
+**To Reproduce**
+Some minimal reproduce code is highly recommended
+
+**Version Information**
+Please give us what version you are using. If you are pulling `Plotters` directly from git repo, please mention this as well
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..cbf5a68
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Suggest an idea to Plotter maintainers
+title: "[Feature Request]"
+labels: feature request
+assignees: ''
+
+---
+
+### What is the feature ?
+*Detailed feature descrption*
+
+### (Optional) Why this feature is useful and how people would use the feature ?
+*Explain why this feature is important*
+
+### (Optional) Additional Information
+*More details are appreciated:)*
diff --git a/.github/ISSUE_TEMPLATE/general-questions.md b/.github/ISSUE_TEMPLATE/general-questions.md
new file mode 100644
index 0000000..96d4e89
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/general-questions.md
@@ -0,0 +1,10 @@
+---
+name: General Questions
+about: Any other issues
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+
diff --git a/.github/workflows/push-check.yml b/.github/workflows/push-check.yml
new file mode 100644
index 0000000..afce1c6
--- /dev/null
+++ b/.github/workflows/push-check.yml
@@ -0,0 +1,70 @@
+name: Push Check
+
+on: [push]
+
+jobs:
+ msrv:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ submodules: recursive
+ - uses: actions-rs/toolchain@v1
+ with:
+ toolchain: 1.36.0
+ override: true
+ - uses: actions-rs/cargo@v1
+ with:
+ command: build
+ args: --verbose
+ build_and_test:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ submodules: recursive
+ - uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ - uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --verbose
+ - uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --verbose --no-default-features --lib
+ test_all_features:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ submodules: recursive
+ - uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ - uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --verbose --all-features
+ run_all_examples:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ submodules: recursive
+ - uses: actions-rs/cargo@v1
+ with:
+ command: build
+ args: --verbose --release --examples
+ - name: Run all the examples
+ run: for example in examples/*.rs; do ./target/release/examples/$(basename ${example} .rs); done && tar -czvf example-outputs.tar.gz plotters-doc-data
+ - uses: actions/upload-artifact@v1
+ with:
+ name: example-outputs
+ path: example-outputs.tar.gz
diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml
new file mode 100644
index 0000000..35166ec
--- /dev/null
+++ b/.github/workflows/wasm.yml
@@ -0,0 +1,18 @@
+name: WASM Target
+
+on: [push]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ with:
+ submodules: recursive
+ - name: Install WASM tool chain
+ run: rustup target add wasm32-unknown-unknown
+ - name: Check WASM Target Compiles
+ run: cargo build --verbose --target=wasm32-unknown-unknown
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0f28dff
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+/target
+**/*.rs.bk
+Cargo.lock
+.*.sw*
+backup/*
+**/Cargo.lock
+**/target
+examples/wasm-demo/www/pkg
+examples/.ipynb_checkpoints/
+tarpaulin-report.html
+.vscode/* \ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..960b83b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "plotters-doc-data"]
+ path = plotters-doc-data
+ url = https://github.com/38/plotters-doc-data
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b3dd6f7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,21 @@
+language: rust
+
+language: rust
+
+rust:
+ - stable
+ - nightly
+ - beta
+
+os:
+ - linux
+
+script:
+ - env
+ - cargo test --all-features && rustup target add wasm32-unknown-unknown && cargo build --target=wasm32-unknown-unknown
+
+after_success: |
+ cargo install cargo-tarpaulin
+ export CODECOV_TOKEN="8622abe0-9579-4cea-93d3-9707969ef6c2"
+ cargo tarpaulin --all-features --out Xml
+ bash <(curl -s https://codecov.io/bash)
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..2fc77c2
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,219 @@
+# Changelog
+
+## Plotters 0.2.15 (2020-05-26)
+### Fixed
+
+- Division by zero with logarithmic coord (issue #143)
+- Update dependencies
+
+## Plotters 0.2.14 (2020-05-05)
+### Fixed
+
+- Compile error with older rustc which causing breaks
+
+## Plotters 0.2.13 (2020-05-04)
+### Added
+
+- Line width is supported for SVG
+
+### Fixed
+
+- Updated dependencies
+- Default rasterizer causing bitmap backend draw out-of-range pixels
+- Depdendicy fix
+
+## Plotters 0.2.12 (2019-12-06)
+### Added
+
+- BitMapBackend now is able to support different pixel format natively. Check our new minifb demo for details.
+- Incremental Rendering by saving the previous chart context into a state and restore it on a different drawing area.
+- BoxPlot support (See boxplot example for more details) (Thanks to @nuald)
+- Category coordinate spec which allows use a list of given values as coordinate (Thanks to @nuald)
+- New text positioning model which allows develvoper sepecify the anchor point. This is critical for layouting SVG correctly. See `plotters::style::text::text_anchor` for details. (Thanks to @nuald)
+
+### Improved
+
+- Faster bitmap blending algorithm, which is 5x faster than the original one.
+- Text alignment improvement, now we can suggest the invariant point by giving `TextAlignment` to the text style (Thanks to @nauld)
+- More controls on features, allows opt in and out series types
+- Remove dependency to svg crate, since it doesn't provide more feature than a plain string.
+
+## Plotters 0.2.11 (2019-10-27)
+
+### Added
+
+- Add font style support, now we are able to set font variations: normal, oblique, italic or bold.
+
+### Improved
+
+- Font description is greatly improved, general font family is supported: `serif`, `serif-sans`, `monospace` (Thanks to @Tatrix)
+- Tested the font loading on Linux, OSX and Windowns. Make font loading more reliable.
+- `BitMapBackend` isn't depdends on `image` crate now. Only the image encoding part relies on the `image` crate
+- Refactored WASM demo use ES6 and `wasm-pack` (Thanks to @Tatrix)
+- Label style for X axis and Y axis can be set seperately now using `x\_label\_style` and `y\_label\_style`. (Thanks to @zhiburt)
+
+## Plotters 0.2.10 (2019-10-23)
+
+### Improved
+
+- Refactored and simplified TTF font cache, use RwLock instead of mutex which may benefit for parallel rendering. (Thanks to @Tatrix)
+
+### Bug Fix
+
+- The fast bitmap filling algorithm may overflow the framebuffer and cause segfault
+
+## Plotters 0.2.9 (2019-10-21)
+
+### Improvement
+
+- Avoid copying image buffer when manipulate the image. (Thanks to @ralfbiedert)
+- Bitmap element which allows blit the image to another drawing area.
+- Performance improvement: now the bitmap backend is 8 times faster
+- Added benchmarks to monitor the performance change
+
+### Bug Fix
+
+- Performance fix: '?' operator is very slow
+- Dynamic Element lifetime bound: Fix a bug that prevents area series draws on non-static lifetime backend
+
+## Plotters 0.2.8 (2019-10-12)
+
+### Added
+
+- Cairo backend, which supports using Plotters draw a GTK surface.
+- Allow secondary axis to be configured with different label style.
+- Relative Sizing, now font and size can use relative scale: `(10).percent\_height()` means we want the size is 10% of parent's height.
+- Allow the axis overlapping with the plotting area with `ChartBuilder::set\_\label\_area\_overlap`.
+- Allow label area is on the top of the drawing area by setting the label area size to negative (Thanks to @serzhiio).
+- Allow configure the tick mark size, when the tick mark size is negative the axis becomes inward (Thanks to @serzhiio).
+
+### Bug Fix
+
+- `FontError` from rusttype isn't `Sync` and `Send`. We don't have trait bound to ensure this. (Thanks to @dalance)
+
+### Improvement
+
+- New convenient functions: `disable_mesh` and `disable_axes`
+
+## Plotters 0.2.7 (2019-10-1)
+
+### Added
+
+- GIF Support, now bitmap backend is able to render gif animation
+
+### Bug Fix
+
+- Fixed several polygon filling bugs.
+- Completely DateTime coordinate system support
+
+## Plotters 0.2.6 (2019-09-19)
+
+### Added
+
+- Allowing axis be placed on top or right by setting `right_y_label_area` and `top_x_label_area`
+- Dual-coord system chart support: Now we are able to attach a secondary coord system to the chart using `ChartContext::set_secondary_coord`. And `draw_secondary_axes` to configure the style of secondary axes. Use `draw_secondary axis` to draw series under the secondary coordinate system.
+- Add support for `y_label_offset`. Previously only X axis label supports offset attribute.
+- New label area size API `set_label_area_size` can be used for all 4 label area
+- Added new error bar element
+- New axis specification type `PartialAxis` which allows the partially rendered axis. For example, we can define the chart's axis range as `0..1`, but only `0.3..0.7` is rendered on axis. This can be done by `(0.0..1.0).partial_axis(0.3..0.7)`
+- Drawing backend now support fill polygon and introduce polygon element
+- Area Chart Support
+
+### Improvement
+
+- More examples are included
+- Date coordinate now support using monthly or yearly axis. This is useful when plotting some data in monthly or yearly basis.
+- Make margin on different side of a chart can be configured separately.
+- Better test coverage
+
+## Plotters 0.2.5 (2019-09-07)
+
+### Bug Fix
+
+- Key points algorithm for numeric coordinate might not respect the constraint
+
+## Plotters 0.2.4 (2019-09-05)
+
+### Improvement
+
+- Add `i128` and `u128` as coordinate type (Credit to @Geemili)
+
+### Bug Fix
+
+- Date coordinate is working for a long time span now
+
+## Plotters 0.2.3 (2019-08-19)
+
+### Improvement
+
+- Color system now is based on `palette` crate (Credit to @Veykril)
+
+## Plotters 0.2.2 (2019-06-25)
+
+### Added
+
+- More documentation: a Jupyter interactive notebook of Plotters tutorial
+- Add more quadrants to the `SeriesLabelPosition` (Credit to @wolfjagger).
+
+### Improvement
+
+- Histogram improvements, horizontal bar is supported, new creation API which compiler can infer the type
+- Supporting split the drawing area with a list of breakpoints using `DrawingArea::split_by_breakpoints`
+- Enable SVG support for WASM
+- Make the `BitMapBackend` takes an in memory mutable buffer
+
+### Fix
+
+- Rectangle drawing bug when the axis is reversed
+
+## Plotters 0.2.1 (2019-06-10)
+
+### Improvement
+
+- Move the sample images and other documentation data out of this repository.
+
+### Fix
+- Make drawing errors shareable across threads. Otherwise, it causes compile error in some cases. (Thanks to @rkarp)
+
+## Plotters 0.2.0 (2019-06-08)
+
+### Added
+- Add relative sizing by added function `DrawingArea::relative_to_height` and `DrawingArea::relative_to_width`.
+- Added piston backend, now we can render plot on a window and dynamically render the plot
+
+### Improved
+- Creating drawing area with `&Rc<RefCell<DrawingBackend>>`. Previously, the drawing area creation requires take over the drawing backend ownership. But sometimes the drawing backend may have additional options. With new API, this can be done by putting the backend drawing area into smart pointers, thus, the drawing backend is accessible after creates the root drawing area.
+
+## Plotters 0.1.14 (2019-06-06)
+
+### Added
+- Font is now support rotation transformation. Use `FontDesc::transform` to apply an rotation to transformation. For example, `font.transform(FontTransform::Rotate90)`.
+- ChartContext now support drawing axis description. Use `MeshStyle::x_desc` and `MeshStyle::y_desc` to specify the axis description text.
+- Add series label support. `ChartContext::draw_series` now returns a struct `SeriesAnno` that collects the additional information for series labeling. `ChartContext::draw_series_labels` are used to actually draw the series label. (See `examples/chart.rs` for detailed examples)
+- Mocking drawing backend.
+- evcxr Support
+
+### Improvement
+- Unify `OwnedText` and `Text` into `Text`. Previously, `OwnedText` and `Text` are two separate types, one holds a `String` another holds a `&str`. Now `OwnedText` is removed.
+use `Text::new("text".to_string(),...)` for owned text element and `Text::new("text", ...)` for borrowed text.
+- Refactor the color representation code, since previously it's heavily relies on the trait object and hard to use
+- More test cases
+
+## Plotters 0.1.13 (2019-05-31)
+
+### Added
+- New abstraction of backend style with `BackendStyle` trait which should be able to extend easier in the future
+- Backend support features, now feature options can be used to control which backend should be supported
+- Add new trait `IntoDrawingArea`, now we can use `backend.into_drawing_area()` to convert the backend into a raw drawing area
+- Now elements support dynamic dispatch, use `element.into_dyn()` to convert the element into a runtime dispatching element
+
+### Improvement
+- Improved the overall code quality
+- Documentation polish
+- Stabilized APIs
+- New conversion traits implementations
+- Now transparent color is ignored by SVG, bitmap and HTML Canvas backend
+
+### Fix
+- Changed the open-close pattern to a `present` function which indicates the end of drawing one frame
+- Fix the but that `ChartBuilder::title` and `ChartBuilder::margin` cannot be called at the same time && `build_ranged` now returning a result.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6271c2c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,131 @@
+Thanks for contributing to `Plotters`!
+
+Here's the useful information about contributing to Plotters.
+
+# License
+
+The `Plotters` project is under MIT license.
+You may interested in reading [the full text of the license](https://github.com/38/plotters/blob/master/LICENSE).
+If you have any questions or concerns please contact us at <haohou302@gmail.com>.
+
+# Contributing to Plotters codebase
+
+You are warmly welcomed to contribute code and make Plotters better. Here's a few things that may be helpful to you.
+
+## How to make sure my code works
+
+*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.
+
+Before you finalize your PR, please check the following thing:
+
+- 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).
+
+- Please run the benchmark to check if the performance changed compare to the master branch.
+
+- 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)
+
+ ```bash
+ cargo test --doc
+ cargo build --release --examples
+ for i in examples/*.rs
+ do
+ ./target/release/examples/$(basename $i .rs)
+ done
+ cd plotters-doc-data
+ git status
+ ```
+
+- 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.
+
+## Is my code meets the styling guideline
+
+Although there's no strictly enforced rules for the style, but please read the following recommendations before you start work.
+
+- 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)
+- For naming, acronyms or initials aren't normally used in the code base. Descriptive identifier is highly recommended.
+- Documentation is highly recommended. (But there are still a lot of undocumented code unfortunately).
+- For API documentation, we normally follows Doxygen's style, which looks like
+
+```rust
+/// Some description to this API
+/// - `param_1`: What param_1 do
+/// - `param_2`: What param_2 do
+/// - **returns**: The return value description
+fn foo(param_1: u32, param_2: u32) -> u32{ 0 }
+```
+
+## Top Level Documentation and Readme
+
+Please notice we put almost same content for top level `rustdoc` and `README.md`. Thus the both part are generated by script.
+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
+use the following command to synchronize the doc to both `src/lib.rs` and `README.md`.
+
+```bash
+bash doc-template/update-readme.sh
+```
+
+## Testing Notes
+
+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.
+
+### Native
+
+Testing all features:
+
+```bash
+cargo test --all-features
+```
+
+Testing no features at all:
+
+```bash
+cargo test --no-default-features --lib
+```
+
+Since all examples and most doc-test requires `bitmap` features, so we don't test examples and doc test in this case.
+
+### WebAssembly
+
+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.
+
+Installation:
+
+```bash
+rustup target add wasm32-unknown-unknown
+cargo install wasm-bindgen-cli
+```
+
+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`.
+
+Usage (only library tests are supported for now):
+
+```bash
+cargo test --lib --target wasm32-unknown-unknown
+```
+
+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.
+
+### Minimal Supported Compiler Version
+
+Currently we should make sure Plotters is compatible with rustc 1.36.0.
+Before making a PR, please check if the code compile with 1.36.0 (with default features).
+
+### Code Coverage
+
+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.
+
+Installation ([pycobertura](https://pypi.python.org/pypi/pycobertura) is used to get the detailed report about the coverage):
+
+```bash
+cargo install cargo-tarpaulin
+pip install pycobertura
+```
+
+Usage:
+
+```bash
+cargo tarpaulin --all-features --run-types Tests Doctests -o Xml --output-dir target/test
+pycobertura show target/test/cobertura.xml
+```
+
+
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..68fedf5
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,123 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+edition = "2018"
+name = "plotters"
+version = "0.2.15"
+authors = ["Hao Hou <haohou302@gmail.com>"]
+exclude = ["doc-template/*"]
+description = "A Rust drawing library focus on data plotting for both WASM and native applications"
+readme = "README.md"
+keywords = ["WebAssembly", "Visualization", "Plotting", "Drawing"]
+categories = ["visualization", "wasm"]
+license = "MIT"
+repository = "https://github.com/38/plotters"
+[profile.bench]
+debug = true
+
+[[bench]]
+name = "benchmark"
+path = "benches/main.rs"
+harness = false
+[dependencies.chrono]
+version = "0.4.11"
+optional = true
+
+[dependencies.gif]
+version = "0.10.3"
+optional = true
+
+[dependencies.num-traits]
+version = "0.2.11"
+
+[dependencies.palette]
+version = "0.5.0"
+features = ["std"]
+optional = true
+default-features = false
+[dev-dependencies.criterion]
+version = "0.3.2"
+
+[dev-dependencies.itertools]
+version = "0.9.0"
+
+[dev-dependencies.rand]
+version = "0.7.3"
+
+[dev-dependencies.rand_distr]
+version = "0.2.2"
+
+[dev-dependencies.rand_xorshift]
+version = "0.2.0"
+
+[dev-dependencies.rayon]
+version = "1.3.0"
+
+[features]
+area_series = []
+bitmap = ["ttf"]
+boxplot = []
+cairo = ["cairo-rs", "ttf"]
+candlestick = []
+datetime = ["chrono"]
+debug = []
+default = ["image_encoder", "svg", "chrono", "palette_ext", "gif_backend", "deprecated_items", "bitmap", "ttf", "errorbar", "candlestick", "boxplot", "histogram", "area_series", "line_series", "point_series"]
+deprecated_items = []
+errorbar = []
+evcxr = ["svg"]
+gif_backend = ["gif", "bitmap"]
+histogram = []
+image_encoder = ["image", "bitmap"]
+line_series = []
+palette_ext = ["palette"]
+piston = ["piston_window", "ttf"]
+point_series = []
+svg = []
+ttf = ["font-kit", "rusttype", "lazy_static"]
+[target."cfg(not(target_arch = \"wasm32\"))".dependencies.cairo-rs]
+version = "0.8.1"
+features = ["ps"]
+optional = true
+
+[target."cfg(not(target_arch = \"wasm32\"))".dependencies.font-kit]
+version = "0.7.0"
+optional = true
+
+[target."cfg(not(target_arch = \"wasm32\"))".dependencies.image]
+version = "0.23.4"
+features = ["jpeg", "png", "bmp"]
+optional = true
+default-features = false
+
+[target."cfg(not(target_arch = \"wasm32\"))".dependencies.lazy_static]
+version = "1.4.0"
+optional = true
+
+[target."cfg(not(target_arch = \"wasm32\"))".dependencies.piston_window]
+version = "0.108.0"
+optional = true
+
+[target."cfg(not(target_arch = \"wasm32\"))".dependencies.rusttype]
+version = "0.8.2"
+optional = true
+[target."cfg(target_arch = \"wasm32\")".dependencies.js-sys]
+version = "0.3.39"
+
+[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
+version = "0.2.62"
+
+[target."cfg(target_arch = \"wasm32\")".dependencies.web-sys]
+version = "0.3.39"
+features = ["Document", "DomRect", "Element", "HtmlElement", "Node", "Window", "HtmlCanvasElement", "CanvasRenderingContext2d"]
+[target."cfg(target_arch = \"wasm32\")".dev-dependencies.wasm-bindgen-test]
+version = "0.3.12"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..395c8f9
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,92 @@
+[package]
+name = "plotters"
+version = "0.2.15"
+authors = ["Hao Hou <haohou302@gmail.com>"]
+edition = "2018"
+license = "MIT"
+description = "A Rust drawing library focus on data plotting for both WASM and native applications"
+repository = "https://github.com/38/plotters"
+keywords = ["WebAssembly", "Visualization", "Plotting", "Drawing"]
+categories = ["visualization", "wasm"]
+readme = "README.md"
+exclude = ["doc-template/*"]
+
+[dependencies]
+num-traits = "0.2.11"
+chrono = { version = "0.4.11", optional = true }
+gif = { version = "0.10.3", optional = true }
+
+[dependencies.palette]
+version = "0.5.0"
+default-features = false
+optional = true
+features = ["std"]
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+rusttype = { version = "0.8.2", optional = true }
+lazy_static = { version = "1.4.0", optional = true }
+font-kit = { version = "0.7.0", optional = true }
+piston_window = { version = "0.108.0", optional = true }
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.image]
+version = "0.23.4"
+optional = true
+default-features = false
+features = ["jpeg", "png", "bmp"]
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.cairo-rs]
+version = "0.8.1"
+optional = true
+features = ["ps"]
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+js-sys= "0.3.39"
+wasm-bindgen = "0.2.62"
+
+[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
+version = "0.3.39"
+features = ['Document', 'DomRect', 'Element', 'HtmlElement', 'Node', 'Window', 'HtmlCanvasElement', 'CanvasRenderingContext2d']
+
+[features]
+default = ["image_encoder", "svg", "chrono", "palette_ext", "gif_backend",
+ "deprecated_items", "bitmap", "ttf", "errorbar", "candlestick",
+ "boxplot", "histogram", "area_series", "line_series", "point_series"]
+ttf = ["font-kit", "rusttype", "lazy_static"]
+image_encoder = ["image", "bitmap"]
+palette_ext = ["palette"]
+gif_backend = ["gif", "bitmap"]
+datetime = ["chrono"]
+svg = []
+evcxr = ["svg"]
+piston = ["piston_window", "ttf"]
+cairo = ["cairo-rs", "ttf"]
+bitmap = ["ttf"]
+deprecated_items = [] # Keep some of the deprecated items for backward compatibility
+debug = [] # Enable debugging code
+errorbar = []
+candlestick = []
+boxplot = []
+histogram = []
+area_series = []
+line_series = []
+point_series = []
+
+
+[dev-dependencies]
+rand = "0.7.3"
+itertools = "0.9.0"
+rand_distr = "0.2.2"
+criterion = "0.3.2"
+rayon = "1.3.0"
+rand_xorshift = "0.2.0"
+
+[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
+wasm-bindgen-test = "0.3.12"
+
+[[bench]]
+name = "benchmark"
+harness = false
+path = "benches/main.rs"
+
+[profile.bench]
+debug = true
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3836960
--- /dev/null
+++ b/README.md
@@ -0,0 +1,545 @@
+# Plotters - A Rust drawing library focus on data plotting for both WASM and native applications 🦀📈🚀
+
+<a href="https://crates.io/crates/plotters">
+ <img style="display: inline!important" src="https://img.shields.io/crates/v/plotters.svg"></img>
+</a>
+<a href="https://docs.rs/plotters">
+ <img style="display: inline!important" src="https://docs.rs/plotters/badge.svg"></img>
+</a>
+<a href="https://plotters-rs.github.io/rustdoc/plotters/">
+ <img style="display: inline! important" src="https://img.shields.io/badge/docs-development-lightgrey.svg"></img>
+</a>
+<a href="https://travis-ci.org/38/plotters">
+ <img style="display: inline! important" src="https://travis-ci.org/38/plotters.svg?branch=master"></img>
+</a>
+<a href="https://codecov.io/gh/38/plotters">
+ <img style="display: inline! important" src="https://codecov.io/gh/38/plotters/branch/master/graph/badge.svg" />
+</a>
+
+Plotters is drawing library designed for rendering figures, plots, and charts, in pure rust. Plotters supports various types of back-ends,
+including bitmap, vector graph, piston window, GTK/Cairo and WebAssembly.
+
+- A new Plotters Developer's Guide is working in progress. The preview version is available at [here](https://plotters-rs.github.io/book).
+- To try Plotters with interactive Jupyter notebook, or view [here](https://plotters-rs.github.io/plotters-doc-data/evcxr-jupyter-integration.html) for the static HTML version.
+- To view the WASM example, go to this [link](https://plumberserver.com/plotters-wasm-demo/index.html)
+- Currently we have all the internal code ready for console plotting, but a console based backend is still not ready. See [this example](https://github.com/38/plotters/blob/master/examples/console.rs) for how to plotting on Console with a customized backend.
+
+## Gallery
+
+To view the source code for each example, please click on the example image.
+
+<a href="https://github.com/38/plotters/blob/master/examples/chart.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/sample.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/stock.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/stock.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/histogram.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/histogram.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters#quick-start">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/0.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="#">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/console-2.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/mandelbrot.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/mandelbrot.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters#trying-with-jupyter-evcxr-kernel-interactively">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/evcxr_animation.gif" class="galleryItem" width=200px></img>
+</a>
+
+
+<a href="https://github.com/38/plotters/tree/master/examples/piston-demo">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/plotters-piston.gif" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/normal-dist.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/normal-dist.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/two-scales.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/twoscale.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/matshow.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/matshow.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/sierpinski.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/sierpinski.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/normal-dist2.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/normal-dist2.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/errorbar.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/errorbar.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/slc-temp.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/slc-temp.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/area-chart.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/area-chart.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/snowflake.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/snowflake.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/animation.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/animation.gif" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/console.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/console-example.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/console.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/console.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/blit-bitmap.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/blit-bitmap.png" class="galleryItem" width=200px></img>
+</a>
+
+<a href="https://github.com/38/plotters/blob/master/examples/boxplot.rs">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/boxplot.svg" class="galleryItem" width=200px></img>
+</a>
+
+
+## Table of Contents
+ * [Gallery](#gallery)
+ * [Quick Start](#quick-start)
+ * [Trying with Jupyter evcxr Kernel Interactively](#trying-with-jupyter-evcxr-kernel-interactively)
+ * [Interactive Tutorial with Jupyter Notebook](#interactive-tutorial-with-jupyter-notebook)
+ * [Plotting in Rust](#plotting-in-rust)
+ * [Plotting on HTML5 canvas with WASM Backend](#plotting-on-html5-canvas-with-wasm-backend)
+ * [What types of figure are supported?](#what-types-of-figure-are-supported)
+ * [Concepts by examples](#concepts-by-examples)
+ + [Drawing Back-ends](#drawing-backends)
+ + [Drawing Area](#drawing-area)
+ + [Elements](#elements)
+ + [Composable Elements](#composable-elements)
+ + [Chart Context](#chart-context)
+ * [Misc](#misc)
+ + [Development Version](#development-version)
+ + [Reducing Depending Libraries && Turning Off Backends](#reducing-depending-libraries--turning-off-backends)
+ + [List of Features](#list-of-features)
+ * [FAQ List](#faq-list)
+
+## Quick Start
+
+To use Plotters, you can simply add Plotters into your `Cargo.toml`
+```toml
+[dependencies]
+plotters = "^0.2.15"
+```
+
+And the following code draws a quadratic function. `src/main.rs`,
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new("plotters-doc-data/0.png", (640, 480)).into_drawing_area();
+ root.fill(&WHITE)?;
+ let mut chart = ChartBuilder::on(&root)
+ .caption("y=x^2", ("sans-serif", 50).into_font())
+ .margin(5)
+ .x_label_area_size(30)
+ .y_label_area_size(30)
+ .build_ranged(-1f32..1f32, -0.1f32..1f32)?;
+
+ chart.configure_mesh().draw()?;
+
+ chart
+ .draw_series(LineSeries::new(
+ (-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
+ &RED,
+ ))?
+ .label("y = x^2")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));
+
+ chart
+ .configure_series_labels()
+ .background_style(&WHITE.mix(0.8))
+ .border_style(&BLACK)
+ .draw()?;
+
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/0.png)
+
+
+## Trying with Jupyter evcxr Kernel Interactively
+
+Plotters now supports integrate with `evcxr` and is able to interactively drawing plots in Jupyter Notebook.
+The feature `evcxr` should be enabled when including Plotters to Jupyter Notebook.
+
+The following code shows a minimal example of this.
+
+```text
+:dep plotters = { git = "https://github.com/38/plotters", default_features = false, features = ["evcxr"] }
+extern crate plotters;
+use plotters::prelude::*;
+
+let figure = evcxr_figure((640, 480), |root| {
+ root.fill(&WHITE);
+ let mut chart = ChartBuilder::on(&root)
+ .caption("y=x^2", ("Arial", 50).into_font())
+ .margin(5)
+ .x_label_area_size(30)
+ .y_label_area_size(30)
+ .build_ranged(-1f32..1f32, -0.1f32..1f32)?;
+
+ chart.configure_mesh().draw()?;
+
+ chart.draw_series(LineSeries::new(
+ (-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
+ &RED,
+ )).unwrap()
+ .label("y = x^2")
+ .legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &RED));
+
+ chart.configure_series_labels()
+ .background_style(&WHITE.mix(0.8))
+ .border_style(&BLACK)
+ .draw()?;
+ Ok(())
+});
+figure
+```
+
+<img src="https://plotters-rs.github.io/plotters-doc-data/evcxr_animation.gif" width="450px"></img>
+
+## Interactive Tutorial with Jupyter Notebook
+
+*This tutorial is now working in progress and isn't complete*
+
+Thanks to the evcxr, now we have an interactive tutorial for Plotters!
+To use the interactive notebook, you must have Jupyter and evcxr installed on your computer.
+Follow the instruction on [this page](https://github.com/google/evcxr/tree/master/evcxr_jupyter) below to install it.
+
+After that, you should be able to start your Jupyter server locally and load the tutorial!
+
+```bash
+git clone https://github.com/38/plotters-doc-data
+cd plotteres-doc-data
+jupyter notebook
+```
+
+And select the notebook called `evcxr-jupyter-integration.ipynb`.
+
+Also, there's a static HTML version of this notebook available at the [this location](https://plumberserver.com/plotters-docs/evcxr-jupyter-integration.html)
+
+## Plotting in Rust
+
+Rust is a perfect language for data visualization. Although there are many mature visualization libraries in many different languages.
+But Rust is one of the best languages fits the need.
+
+* **Easy to use** Rust has a very good iterator system built into the standard library. With the help of iterators,
+Plotting in Rust can be as easy as most of the high-level programming languages. The Rust based plotting library
+can be very easy to use.
+
+* **Fast** If you need rendering a figure with trillions of data points,
+Rust is a good choice. Rust's performance allows you to combine data processing step
+and rendering step into a single application. When plotting in high-level programming languages,
+e.g. Javascript or Python, data points must be down-sampled before feeding into the plotting
+program because of the performance considerations. Rust is fast enough to do the data processing and visualization
+within a single program. You can also integrate the
+figure rendering code into your application handling a huge amount of data and visualize it in real-time.
+
+* **WebAssembly Support** Rust is one of few the language with the best WASM support. Plotting in Rust could be
+very useful for visualization on a web page and would have a huge performance improvement comparing to Javascript.
+
+## Plotting on HTML5 canvas with WASM Backend
+
+Plotters currently supports backend that uses the HTML5 canvas. To use the WASM support, you can simply use
+`CanvasBackend` instead of other backend and all other API remains the same!
+
+There's a small demo for Plotters + WASM under `examples/wasm-demo` directory of this repo.
+To play with the deployed version, follow this [link](https://plumberserver.com/plotters-wasm-demo/index.html).
+
+
+## What types of figure are supported?
+
+Plotters is not limited to any specific type of figure.
+You can create your own types of figures easily with the Plotters API.
+
+But Plotters provides some builtin figure types for convenience.
+Currently, we support line series, point series, candlestick series, and histogram.
+And the library is designed to be able to render multiple figure into a single image.
+But Plotter is aimed to be a platform that is fully extendable to support any other types of figure.
+
+## Concepts by examples
+
+### Drawing Back-ends
+Plotters can use different drawing back-ends, including SVG, BitMap, and even real-time rendering. For example, a bitmap drawing backend.
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ // Create a 800*600 bitmap and start drawing
+ let mut backend = BitMapBackend::new("plotters-doc-data/1.png", (300, 200));
+ // And if we want SVG backend
+ // let backend = SVGBackend::new("output.svg", (800, 600));
+ backend.draw_rect((50, 50), (200, 150), &RED, true)?;
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/1.png)
+
+### Drawing Area
+Plotters uses a concept called drawing area for layout purpose.
+Plotters support multiple integrating into a single image.
+This is done by creating sub-drawing-areas.
+
+Besides that, the drawing area also allows the customized coordinate system, by doing so, the coordinate mapping is done by the drawing area automatically.
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root_drawing_area =
+ BitMapBackend::new("plotters-doc-data/2.png", (300, 200)).into_drawing_area();
+ // And we can split the drawing area into 3x3 grid
+ let child_drawing_areas = root_drawing_area.split_evenly((3, 3));
+ // Then we fill the drawing area with different color
+ for (area, color) in child_drawing_areas.into_iter().zip(0..) {
+ area.fill(&Palette99::pick(color))?;
+ }
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/2.png)
+
+### Elements
+
+In Plotters, elements are build blocks of figures. All elements are able to draw on a drawing area.
+There are different types of built-in elements, like lines, texts, circles, etc.
+You can also define your own element in the application code.
+
+You may also combine existing elements to build a complex element.
+
+To learn more about the element system, please read the [element module documentation](./element/index.html).
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new("plotters-doc-data/3.png", (300, 200)).into_drawing_area();
+ root.fill(&WHITE)?;
+ // Draw an circle on the drawing area
+ root.draw(&Circle::new(
+ (100, 100),
+ 50,
+ Into::<ShapeStyle>::into(&GREEN).filled(),
+ ))?;
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/3.png)
+
+### Composable Elements
+
+Besides the built-in elements, elements can be composed into a logic group we called composed elements.
+When composing new elements, the upper-left corner is given in the target coordinate, and a new pixel-based
+coordinate which has the upper-left corner defined as `(0,0)` is used for further element composition purpose.
+
+For example, we can have an element which includes a dot and its coordinate.
+
+```rust
+use plotters::prelude::*;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new("plotters-doc-data/4.png", (640, 480)).into_drawing_area();
+
+ root.fill(&RGBColor(240, 200, 200))?;
+
+ let root = root.apply_coord_spec(RangedCoord::<RangedCoordf32, RangedCoordf32>::new(
+ 0f32..1f32,
+ 0f32..1f32,
+ (0..640, 0..480),
+ ));
+
+ let dot_and_label = |x: f32, y: f32| {
+ return EmptyElement::at((x, y))
+ + Circle::new((0, 0), 3, ShapeStyle::from(&BLACK).filled())
+ + Text::new(
+ format!("({:.2},{:.2})", x, y),
+ (10, 0),
+ ("sans-serif", 15.0).into_font(),
+ );
+ };
+
+ root.draw(&dot_and_label(0.5, 0.6))?;
+ root.draw(&dot_and_label(0.25, 0.33))?;
+ root.draw(&dot_and_label(0.8, 0.8))?;
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/4.png)
+
+### Chart Context
+
+In order to draw a chart, Plotters need a data object built on top of the drawing area called `ChartContext`.
+The chart context defines even higher level constructs compare to the drawing area.
+For example, you can define the label areas, meshes, and put a data series onto the drawing area with the help
+of the chart context object.
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new("plotters-doc-data/5.png", (640, 480)).into_drawing_area();
+ root.fill(&WHITE);
+ let root = root.margin(10, 10, 10, 10);
+ // After this point, we should be able to draw construct a chart context
+ let mut chart = ChartBuilder::on(&root)
+ // Set the caption of the chart
+ .caption("This is our first plot", ("sans-serif", 40).into_font())
+ // Set the size of the label region
+ .x_label_area_size(20)
+ .y_label_area_size(40)
+ // Finally attach a coordinate on the drawing area and make a chart context
+ .build_ranged(0f32..10f32, 0f32..10f32)?;
+
+ // Then we can draw a mesh
+ chart
+ .configure_mesh()
+ // We can customize the maximum number of labels allowed for each axis
+ .x_labels(5)
+ .y_labels(5)
+ // We can also change the format of the label text
+ .y_label_formatter(&|x| format!("{:.3}", x))
+ .draw()?;
+
+ // And we can draw something in the drawing area
+ chart.draw_series(LineSeries::new(
+ vec![(0.0, 0.0), (5.0, 5.0), (8.0, 7.0)],
+ &RED,
+ ))?;
+ // Similarly, we can draw point series
+ chart.draw_series(PointSeries::of_element(
+ vec![(0.0, 0.0), (5.0, 5.0), (8.0, 7.0)],
+ 5,
+ &RED,
+ &|c, s, st| {
+ return EmptyElement::at(c) // We want to construct a composed element on-the-fly
+ + Circle::new((0,0),s,st.filled()) // At this point, the new pixel coordinate is established
+ + Text::new(format!("{:?}", c), (10, 0), ("sans-serif", 10).into_font());
+ },
+ ))?;
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/5.png)
+
+## Misc
+
+### Development Version
+
+To use the latest development version, pull https://github.com/38/plotters.git. In `Cargo.toml`
+
+```toml
+[dependencies]
+plotters = { git = "https://github.com/38/plotters.git" }
+```
+
+### Reducing Depending Libraries && Turning Off Backends
+Plotters now supports use features to control the backend dependencies. By default, `BitMapBackend` and `SVGBackend` are supported,
+use `default_features = false` in the dependency description in `Cargo.toml` and you can cherry-pick the backend implementations.
+
+- `svg` Enable the `SVGBackend`
+- `bitmap` Enable the `BitMapBackend`
+
+For example, the following dependency description would avoid compiling with bitmap support:
+
+```toml
+[dependencies]
+plotters = { git = "https://github.com/38/plotters.git", default_features = false, features = ["svg"] }
+```
+
+The library also allows consumers to make use of the [`Palette`](https://crates.io/crates/palette/) crate's color types by default.
+This behavior can also be turned off by setting `default_features = false`.
+
+### List of Features
+
+This is the full list of features that is defined by `Plotters` crate.
+Use `default_features = false` to disable those default enabled features,
+and then you should be able to cherry-pick what features you want to include into `Plotters` crate.
+By doing so, you can minimize the number of dependencies down to only `itertools` and compile time is less than 6s.
+
+The following list is a complete list of features that can be opt in and out.
+
+- Drawing backends related features
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| image\_encoder | Allow `BitMapBackend` save the result to bitmap files | image, rusttype, font-kit | Yes |
+| svg | Enable `SVGBackend` Support | None | Yes |
+| gif\_backend| Opt-in GIF animation Rendering support for `BitMapBackend`, implies `bitmap` enabled | gif | Yes |
+| piston | Enable `PistonWindowBackend` | piston\_window, rusttype, font-kit | No |
+| cairo | Enable `CairoBackend` | cairo-rs, rusttype, font-kit | No |
+
+- Font manipulation features
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| ttf | Allows TrueType font support | rusttype, font-kit | Yes |
+
+- Coordinate features
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| datetime | Eanble the date and time coordinate support | chrono | Yes |
+
+- Element, series and util functions
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| errorbar | The errorbar element support | None | Yes |
+| candlestick | The candlestick element support | None | Yes |
+| boxplot | The boxplot element support | None | Yes |
+| area\_series | The area series support | None | Yes |
+| line\_series | The line series support | None | Yes |
+| histogram | The histogram series support | None | Yes |
+| point\_series| The point series support | None | Yes |
+
+- Misc
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| deprecated\_items | This feature allows use of deprecated items which is going to be removed in the future | None | Yes |
+| debug | Enable the code used for debugging | None | No |
+
+
+## FAQ List
+
+* Why does the WASM example break on my machine ?
+
+ The WASM example requires using `wasm32` target to build. Using `cargo build` is likely to use the default target
+ which in most of the case is any of the x86 target. Thus you need add `--target=wasm32-unknown-unknown` in the cargo
+ parameter list to build it.
+
+* How to draw text/circle/point/rectangle/... on the top of chart ?
+
+ As you may realized, Plotters is a drawing library rather than a traditional data plotting library,
+ you have the freedom to draw anything you want on the drawing area.
+ Use `DrawingArea::draw` to draw any element on the drawing area.
+
+
diff --git a/benches/benches/data.rs b/benches/benches/data.rs
new file mode 100644
index 0000000..22dc9f4
--- /dev/null
+++ b/benches/benches/data.rs
@@ -0,0 +1,37 @@
+use criterion::{criterion_group, Criterion};
+use plotters::data::Quartiles;
+
+struct Lcg {
+ state: u32,
+}
+
+impl Lcg {
+ fn new() -> Lcg {
+ Lcg { state: 0 }
+ }
+}
+
+impl Iterator for Lcg {
+ type Item = u32;
+
+ fn next(&mut self) -> Option<u32> {
+ self.state = self.state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
+ self.state %= 1 << 31;
+ Some(self.state)
+ }
+}
+
+fn quartiles_calc(c: &mut Criterion) {
+ let src: Vec<u32> = Lcg::new().take(100000).collect();
+ c.bench_function("data::quartiles_calc", |b| {
+ b.iter(|| {
+ Quartiles::new(&src);
+ })
+ });
+}
+
+criterion_group! {
+ name = quartiles_group;
+ config = Criterion::default().sample_size(10);
+ targets = quartiles_calc
+}
diff --git a/benches/benches/mod.rs b/benches/benches/mod.rs
new file mode 100644
index 0000000..0967b2d
--- /dev/null
+++ b/benches/benches/mod.rs
@@ -0,0 +1,3 @@
+pub mod data;
+pub mod parallel;
+pub mod rasterizer;
diff --git a/benches/benches/parallel.rs b/benches/benches/parallel.rs
new file mode 100644
index 0000000..7bd37d0
--- /dev/null
+++ b/benches/benches/parallel.rs
@@ -0,0 +1,180 @@
+use criterion::{criterion_group, Criterion, ParameterizedBenchmark};
+
+use plotters::coord::Shift;
+use plotters::prelude::*;
+use rayon::prelude::*;
+
+const SIZES: &'static [u32] = &[100, 400, 800, 1000, 2000];
+
+fn draw_plot(root: &DrawingArea<BitMapBackend, Shift>, pow: f64) {
+ let mut chart = ChartBuilder::on(root)
+ .caption(format!("y = x^{}", pow), ("Arial", 30))
+ .build_ranged(-1.0..1.0, -1.0..1.0)
+ .unwrap();
+
+ chart.configure_mesh().draw().unwrap();
+
+ chart
+ .draw_series(LineSeries::new(
+ (-50..=50)
+ .map(|x| x as f64 / 50.0)
+ .map(|x| (x, x.powf(pow))),
+ &RED,
+ ))
+ .unwrap()
+ .label(format!("y = x^{}", pow))
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));
+ chart
+ .configure_series_labels()
+ .background_style(&WHITE.mix(0.8))
+ .border_style(&BLACK)
+ .draw()
+ .unwrap();
+}
+
+fn draw_func_1x1_seq(c: &mut Criterion) {
+ c.bench(
+ "draw_func_1x1",
+ ParameterizedBenchmark::new(
+ "sequential",
+ |b, &&s| {
+ let mut buffer = vec![0; (s * s * 3) as usize];
+ b.iter(|| {
+ let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area();
+ root.fill(&WHITE).unwrap();
+ draw_plot(&root, 2.0);
+ })
+ },
+ SIZES.clone(),
+ ),
+ );
+}
+
+fn draw_func_4x4(c: &mut Criterion) {
+ c.bench(
+ "draw_func_4x4",
+ ParameterizedBenchmark::new(
+ "sequential",
+ |b, &&s| {
+ let mut buffer = vec![0; (s * s * 3) as usize];
+ b.iter(|| {
+ let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area();
+ let areas = root.split_evenly((4, 4));
+ areas.iter().for_each(|area| draw_plot(&area, 2.0));
+ })
+ },
+ SIZES.clone(),
+ )
+ .with_function("blit", |b, &&s| {
+ let mut buffer = vec![0; (s * s * 3) as usize];
+ let mut element_buffer = vec![vec![0; (s * s / 4 * 3) as usize]; 4];
+ b.iter(|| {
+ let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area();
+ let areas = root.split_evenly((4, 4));
+ let elements: Vec<_> = element_buffer
+ .par_iter_mut()
+ .map(|b| {
+ let mut e = BitMapElement::with_mut((0, 0), (s / 2, s / 2), b).unwrap();
+ draw_plot(&e.as_bitmap_backend().into_drawing_area(), 2.0);
+ e
+ })
+ .collect();
+
+ areas
+ .into_iter()
+ .zip(elements.into_iter())
+ .for_each(|(a, e)| a.draw(&e).unwrap());
+ })
+ })
+ .with_function("inplace-blit", |b, &&s| {
+ let mut buffer = vec![0; (s * s * 3) as usize];
+ let mut element_buffer = vec![vec![vec![0; (s * s / 4 * 3) as usize]; 2]; 2];
+ b.iter(|| {
+ let mut back = BitMapBackend::with_buffer(&mut buffer, (s, s));
+ back.split(&[s / 2])
+ .into_iter()
+ .zip(element_buffer.iter_mut())
+ .collect::<Vec<_>>()
+ .into_par_iter()
+ .for_each(|(back, buffer)| {
+ let root = back.into_drawing_area();
+ let areas = root.split_evenly((1, 2));
+
+ let elements: Vec<_> = buffer
+ .par_iter_mut()
+ .map(|b| {
+ let mut e =
+ BitMapElement::with_mut((0, 0), (s / 2, s / 2), b).unwrap();
+ draw_plot(&e.as_bitmap_backend().into_drawing_area(), 2.0);
+ e
+ })
+ .collect();
+
+ areas
+ .into_iter()
+ .zip(elements.into_iter())
+ .for_each(|(a, e)| a.draw(&e).unwrap())
+ });
+ })
+ }),
+ );
+}
+
+fn draw_func_2x1(c: &mut Criterion) {
+ c.bench(
+ "draw_func_2x1",
+ ParameterizedBenchmark::new(
+ "blit",
+ |b, &&s| {
+ let mut buffer = vec![0; (s * s * 3) as usize];
+ let mut element_buffer = vec![vec![0; (s * s / 2 * 3) as usize]; 2];
+ b.iter(|| {
+ let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area();
+ let areas = root.split_evenly((2, 1));
+ let elements: Vec<_> = element_buffer
+ .par_iter_mut()
+ .map(|buf| {
+ let mut element =
+ BitMapElement::with_mut((0, 0), (s, s / 2), buf).unwrap();
+ draw_plot(&element.as_bitmap_backend().into_drawing_area(), 2.0);
+ element
+ })
+ .collect();
+
+ areas
+ .into_iter()
+ .zip(elements.into_iter())
+ .for_each(|(a, e)| a.draw(&e).unwrap());
+ })
+ },
+ SIZES.clone(),
+ )
+ .with_function("inplace", |b, &&s| {
+ let mut buffer = vec![0; (s * s * 3) as usize];
+ b.iter(|| {
+ let mut back = BitMapBackend::with_buffer(&mut buffer, (s, s));
+ back.split(&[s / 2])
+ .into_par_iter()
+ .for_each(|b| draw_plot(&b.into_drawing_area(), 2.0));
+ })
+ })
+ .with_function("sequential", |b, &&s| {
+ let mut buffer = vec![0; (s * s * 3) as usize];
+ b.iter(|| {
+ let root = BitMapBackend::with_buffer(&mut buffer, (s, s)).into_drawing_area();
+ root.split_evenly((2, 1))
+ .iter_mut()
+ .for_each(|area| draw_plot(area, 2.0));
+ })
+ }),
+ );
+}
+
+criterion_group! {
+ name = parallel_group;
+ config = Criterion::default().sample_size(10);
+ targets =
+ draw_func_1x1_seq,
+ draw_func_4x4,
+ draw_func_2x1,
+}
diff --git a/benches/benches/rasterizer.rs b/benches/benches/rasterizer.rs
new file mode 100644
index 0000000..405acb1
--- /dev/null
+++ b/benches/benches/rasterizer.rs
@@ -0,0 +1,204 @@
+use criterion::{criterion_group, Criterion};
+use plotters::drawing::bitmap_pixel::BGRXPixel;
+use plotters::prelude::*;
+
+const W: u32 = 1000;
+const H: u32 = 1000;
+
+fn draw_pixel(c: &mut Criterion) {
+ let mut group = c.benchmark_group("rasterizer::draw_pixel");
+
+ group.bench_function("rgb", |b| {
+ let mut buffer = vec![0; (W * H * 3) as usize];
+ b.iter(|| {
+ let mut root = BitMapBackend::with_buffer(&mut buffer, (W, H));
+ for x in 0..W / 10 {
+ for y in 0..H / 10 {
+ root.draw_pixel((x as i32, y as i32), &RGBColor(255, 0, 234).to_rgba())
+ .unwrap();
+ }
+ }
+ })
+ });
+
+ group.bench_function("xbgr", |b| {
+ let mut buffer = vec![0; (W * H * 4) as usize];
+ b.iter(|| {
+ let mut root =
+ BitMapBackend::<BGRXPixel>::with_buffer_and_format(&mut buffer, (W, H)).unwrap();
+ for x in 0..W / 10 {
+ for y in 0..H / 10 {
+ root.draw_pixel((x as i32, y as i32), &RGBColor(255, 0, 234).to_rgba())
+ .unwrap();
+ }
+ }
+ })
+ });
+}
+
+fn draw_line(c: &mut Criterion) {
+ let mut group = c.benchmark_group("rasterizer::draw_line");
+
+ group.bench_function("rgb", |b| {
+ let mut buffer = vec![0; (W * H * 3) as usize];
+ b.iter(|| {
+ let mut root = BitMapBackend::with_buffer(&mut buffer, (W, H));
+ for y in 0..10 {
+ root.draw_line(
+ (0, 0),
+ ((W / 2) as i32, (y * 100) as i32),
+ &RGBColor(255, 0, 234).to_rgba(),
+ )
+ .unwrap();
+ }
+ })
+ });
+
+ group.bench_function("bgrx", |b| {
+ let mut buffer = vec![0; (W * H * 4) as usize];
+ b.iter(|| {
+ let mut root =
+ BitMapBackend::<BGRXPixel>::with_buffer_and_format(&mut buffer, (W, H)).unwrap();
+ for y in 0..10 {
+ root.draw_line(
+ (0, 0),
+ ((W / 2) as i32, (y * 100) as i32),
+ &RGBColor(255, 0, 234).to_rgba(),
+ )
+ .unwrap();
+ }
+ })
+ });
+}
+
+fn fill_background(c: &mut Criterion) {
+ let mut group = c.benchmark_group("rasterizer::fill_background");
+
+ group.bench_function("rgb", |b| {
+ let mut buffer = vec![0; (W * H * 3) as usize];
+ b.iter(|| {
+ let root = BitMapBackend::with_buffer(&mut buffer, (W, H)).into_drawing_area();
+ root.fill(&WHITE).unwrap();
+ })
+ });
+
+ group.bench_function("bgrx", |b| {
+ let mut buffer = vec![0; (W * H * 4) as usize];
+ b.iter(|| {
+ let root = BitMapBackend::<BGRXPixel>::with_buffer_and_format(&mut buffer, (W, H))
+ .unwrap()
+ .into_drawing_area();
+ root.fill(&WHITE).unwrap();
+ })
+ });
+}
+
+fn blend_background(c: &mut Criterion) {
+ let mut g = c.benchmark_group("rasterizer::blend_background");
+
+ g.bench_function("rgb", |b| {
+ let mut buffer = vec![0; (W * H * 3) as usize];
+ b.iter(|| {
+ let root = BitMapBackend::with_buffer(&mut buffer, (W, H)).into_drawing_area();
+ root.fill(&WHITE.mix(0.1)).unwrap();
+ })
+ });
+
+ g.bench_function("bgrx", |b| {
+ let mut buffer = vec![0; (W * H * 4) as usize];
+ b.iter(|| {
+ let root = BitMapBackend::<BGRXPixel>::with_buffer_and_format(&mut buffer, (W, H))
+ .unwrap()
+ .into_drawing_area();
+ root.fill(&WHITE.mix(0.1)).unwrap();
+ })
+ });
+}
+
+fn fill_circle(c: &mut Criterion) {
+ let mut g = c.benchmark_group("rasterizer::fill_circle");
+
+ g.bench_function("rgb", |b| {
+ let mut buffer = vec![0; (W * H * 3) as usize];
+ b.iter(|| {
+ let mut root = BitMapBackend::with_buffer(&mut buffer, (W, H));
+ root.draw_circle((W as i32 / 2, H as i32 / 2), W / 2, &WHITE.to_rgba(), true)
+ .unwrap();
+ })
+ });
+
+ g.bench_function("bgrx", |b| {
+ let mut buffer = vec![0; (W * H * 4) as usize];
+ b.iter(|| {
+ let mut root =
+ BitMapBackend::<BGRXPixel>::with_buffer_and_format(&mut buffer, (W, H)).unwrap();
+ root.draw_circle((W as i32 / 2, H as i32 / 2), W / 2, &WHITE.to_rgba(), true)
+ .unwrap();
+ })
+ });
+}
+
+fn fill_background_red(c: &mut Criterion) {
+ let mut g = c.benchmark_group("rasterizer::fill_background_red");
+
+ g.bench_function("rgb", |b| {
+ let mut buffer = vec![0; (W * H * 3) as usize];
+ b.iter(|| {
+ let root = BitMapBackend::with_buffer(&mut buffer, (W, H)).into_drawing_area();
+ root.fill(&RED).unwrap();
+ })
+ });
+
+ g.bench_function("bgrx", |b| {
+ let mut buffer = vec![0; (W * H * 4) as usize];
+ b.iter(|| {
+ let root = BitMapBackend::<BGRXPixel>::with_buffer_and_format(&mut buffer, (W, H))
+ .unwrap()
+ .into_drawing_area();
+ root.fill(&RED).unwrap();
+ })
+ });
+}
+
+fn fill_hexagon(c: &mut Criterion) {
+ let mut g = c.benchmark_group("rasterizer::fill_hexagon");
+ let mut vert = vec![];
+
+ for i in 0..6 {
+ let x = (W as f64 / 5.0 * (std::f64::consts::PI * i as f64 / 3.0).cos()).ceil() as i32
+ + W as i32 / 2;
+ let y = (W as f64 / 5.0 * (std::f64::consts::PI * i as f64 / 3.0).sin()).ceil() as i32
+ + W as i32 / 2;
+ vert.push((x, y));
+ }
+
+ g.bench_function("rgb", |b| {
+ let mut buffer = vec![0; (W * H * 3) as usize];
+ b.iter(|| {
+ let mut root = BitMapBackend::with_buffer(&mut buffer, (W, H));
+ root.fill_polygon(vert.clone(), &RED).unwrap();
+ })
+ });
+
+ g.bench_function("bgrx", |b| {
+ let mut buffer = vec![0; (W * H * 4) as usize];
+ b.iter(|| {
+ let mut root =
+ BitMapBackend::<BGRXPixel>::with_buffer_and_format(&mut buffer, (W, H)).unwrap();
+ root.fill_polygon(vert.clone(), &RED).unwrap();
+ })
+ });
+}
+
+criterion_group! {
+ name = rasterizer_group;
+ config = Criterion::default();
+ targets =
+ blend_background,
+ draw_pixel,
+ draw_line,
+ fill_background,
+ fill_circle,
+ fill_background_red,
+ fill_hexagon,
+}
diff --git a/benches/main.rs b/benches/main.rs
new file mode 100644
index 0000000..3ebf8e8
--- /dev/null
+++ b/benches/main.rs
@@ -0,0 +1,9 @@
+use criterion::criterion_main;
+
+mod benches;
+
+criterion_main! {
+ benches::parallel::parallel_group,
+ benches::rasterizer::rasterizer_group,
+ benches::data::quartiles_group
+}
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..956de7a
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,11 @@
+# plotters examples
+
+To run any example, from within the repo, run `cargo run --example <example_name>` where `<example name>` is the name of the file without the `.rs` extension.
+
+All the examples assumes the directory [plotters-doc-data](https://github.com/38/plotters-doc-data) exists, otherwise those example crashs.
+
+The output of these example files are used to generate the [plotters-doc-data](https://github.com/38/plotters-doc-data) repo that populates the sample images in the main README.
+We also relies on the output of examples to detect protential layout changes.
+For that reason, **they must be run with `cargo` from within the repo, or you must change the output filename in the example code to a directory that exists.**
+
+The examples that have their own directories and `Cargo.toml` files work differently. They are run the same way you would a standalone project.
diff --git a/examples/animation.rs b/examples/animation.rs
new file mode 100644
index 0000000..7f20bca
--- /dev/null
+++ b/examples/animation.rs
@@ -0,0 +1,58 @@
+use plotters::prelude::*;
+
+fn snowflake_iter(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
+ let mut ret = vec![];
+ for i in 0..points.len() {
+ let (start, end) = (points[i], points[(i + 1) % points.len()]);
+ let t = ((end.0 - start.0) / 3.0, (end.1 - start.1) / 3.0);
+ let s = (
+ t.0 * 0.5 - t.1 * (0.75f64).sqrt(),
+ t.1 * 0.5 + (0.75f64).sqrt() * t.0,
+ );
+ ret.push(start);
+ ret.push((start.0 + t.0, start.1 + t.1));
+ ret.push((start.0 + t.0 + s.0, start.1 + t.1 + s.1));
+ ret.push((start.0 + t.0 * 2.0, start.1 + t.1 * 2.0));
+ }
+ ret
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::gif("plotters-doc-data/animation.gif", (800, 600), 1_000)?
+ .into_drawing_area();
+
+ for i in 0..8 {
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption(
+ format!("Koch's Snowflake (n_iter = {})", i),
+ ("sans-serif", 50),
+ )
+ .build_ranged(-2.0..2.0, -1.5..1.5)?;
+
+ let mut snowflake_vertices = {
+ let mut current: Vec<(f64, f64)> = vec![
+ (0.0, 1.0),
+ ((3.0f64).sqrt() / 2.0, -0.5),
+ (-(3.0f64).sqrt() / 2.0, -0.5),
+ ];
+ for _ in 0..i {
+ current = snowflake_iter(&current[..]);
+ }
+ current
+ };
+
+ chart.draw_series(std::iter::once(Polygon::new(
+ snowflake_vertices.clone(),
+ &RED.mix(0.2),
+ )))?;
+
+ snowflake_vertices.push(snowflake_vertices[0]);
+ chart.draw_series(std::iter::once(PathElement::new(snowflake_vertices, &RED)))?;
+
+ root.present()?;
+ }
+
+ Ok(())
+}
diff --git a/examples/area-chart.rs b/examples/area-chart.rs
new file mode 100644
index 0000000..707cc5a
--- /dev/null
+++ b/examples/area-chart.rs
@@ -0,0 +1,47 @@
+use plotters::prelude::*;
+
+use rand::SeedableRng;
+use rand_distr::{Distribution, Normal};
+use rand_xorshift::XorShiftRng;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let data: Vec<_> = {
+ let norm_dist = Normal::new(500.0, 100.0).unwrap();
+ let mut x_rand = XorShiftRng::from_seed(*b"MyFragileSeed123");
+ let x_iter = norm_dist.sample_iter(&mut x_rand);
+ x_iter
+ .filter(|x| *x < 1500.0)
+ .take(100)
+ .zip(0..)
+ .map(|(x, b)| x + (b as f64).powf(1.2))
+ .collect()
+ };
+
+ let root =
+ BitMapBackend::new("plotters-doc-data/area-chart.png", (1024, 768)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .set_label_area_size(LabelAreaPosition::Left, 60)
+ .set_label_area_size(LabelAreaPosition::Bottom, 60)
+ .caption("Area Chart Demo", ("sans-serif", 40))
+ .build_ranged(0..(data.len() - 1), 0.0..1500.0)?;
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .draw()?;
+
+ chart.draw_series(
+ AreaSeries::new(
+ (0..).zip(data.iter()).map(|(x, y)| (x, *y)),
+ 0.0,
+ &RED.mix(0.2),
+ )
+ .border_style(&RED),
+ )?;
+
+ Ok(())
+}
diff --git a/examples/blit-bitmap.rs b/examples/blit-bitmap.rs
new file mode 100644
index 0000000..1e0e47e
--- /dev/null
+++ b/examples/blit-bitmap.rs
@@ -0,0 +1,33 @@
+use plotters::prelude::*;
+
+use image::{imageops::FilterType, ImageFormat};
+
+use std::fs::File;
+use std::io::BufReader;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/blit-bitmap.png", (1024, 768)).into_drawing_area();
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("Bitmap Example", ("sans-serif", 30))
+ .margin(5)
+ .set_label_area_size(LabelAreaPosition::Left, 40)
+ .set_label_area_size(LabelAreaPosition::Bottom, 40)
+ .build_ranged(0.0..1.0, 0.0..1.0)?;
+
+ chart.configure_mesh().disable_mesh().draw()?;
+
+ let (w, h) = chart.plotting_area().dim_in_pixel();
+ let image = image::load(
+ BufReader::new(File::open("plotters-doc-data/cat.png")?),
+ ImageFormat::Png,
+ )?
+ .resize_exact(w - w / 10, h - h / 10, FilterType::Nearest);
+
+ let elem: BitMapElement<_> = ((0.05, 0.95), image).into();
+
+ chart.draw_series(std::iter::once(elem))?;
+ Ok(())
+}
diff --git a/examples/boxplot.rs b/examples/boxplot.rs
new file mode 100644
index 0000000..47f3b1a
--- /dev/null
+++ b/examples/boxplot.rs
@@ -0,0 +1,222 @@
+use itertools::Itertools;
+use plotters::data::fitting_range;
+use plotters::prelude::*;
+use std::collections::BTreeMap;
+use std::collections::HashMap;
+use std::env;
+use std::fs;
+use std::io::{self, prelude::*, BufReader};
+
+fn read_data<BR: BufRead>(reader: BR) -> HashMap<(String, String), Vec<f64>> {
+ let mut ds = HashMap::new();
+ for l in reader.lines() {
+ let line = l.unwrap();
+ let tuple: Vec<&str> = line.split('\t').collect();
+ if tuple.len() == 3 {
+ let key = (String::from(tuple[0]), String::from(tuple[1]));
+ let entry = ds.entry(key).or_insert_with(Vec::new);
+ entry.push(tuple[2].parse::<f64>().unwrap());
+ }
+ }
+ ds
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = SVGBackend::new("plotters-doc-data/boxplot.svg", (1024, 768)).into_drawing_area();
+ root.fill(&WHITE)?;
+
+ let root = root.margin(5, 5, 5, 5);
+
+ let (upper, lower) = root.split_vertically(512);
+
+ let args: Vec<String> = env::args().collect();
+
+ let ds = if args.len() < 2 {
+ read_data(io::Cursor::new(get_data()))
+ } else {
+ let file = fs::File::open(&args[1])?;
+ read_data(BufReader::new(file))
+ };
+ let dataset: Vec<(String, String, Quartiles)> = ds
+ .iter()
+ .map(|(k, v)| (k.0.clone(), k.1.clone(), Quartiles::new(&v)))
+ .collect();
+
+ let category = Category::new(
+ "Host",
+ dataset
+ .iter()
+ .unique_by(|x| x.0.clone())
+ .sorted_by(|a, b| b.2.median().partial_cmp(&a.2.median()).unwrap())
+ .map(|x| x.0.clone())
+ .collect(),
+ );
+
+ let mut colors = (0..).map(Palette99::pick);
+ let mut offsets = (-12..).step_by(24);
+ let mut series = BTreeMap::new();
+ for x in dataset.iter() {
+ let entry = series
+ .entry(x.1.clone())
+ .or_insert_with(|| (Vec::new(), colors.next().unwrap(), offsets.next().unwrap()));
+ entry.0.push((x.0.clone(), &x.2));
+ }
+
+ let values: Vec<f32> = dataset
+ .iter()
+ .map(|x| x.2.values().to_vec())
+ .flatten()
+ .collect();
+ let values_range = fitting_range(values.iter());
+
+ let mut chart = ChartBuilder::on(&upper)
+ .x_label_area_size(40)
+ .y_label_area_size(80)
+ .caption("Ping Boxplot", ("sans-serif", 20).into_font())
+ .build_ranged(
+ values_range.start - 1.0..values_range.end + 1.0,
+ category.range(),
+ )?;
+
+ chart
+ .configure_mesh()
+ .x_desc("Ping, ms")
+ .y_desc(category.name())
+ .y_labels(category.len())
+ .line_style_2(&WHITE)
+ .draw()?;
+
+ for (label, (values, style, offset)) in &series {
+ chart
+ .draw_series(values.iter().map(|x| {
+ Boxplot::new_horizontal(category.get(&x.0).unwrap(), &x.1)
+ .width(20)
+ .whisker_width(0.5)
+ .style(style)
+ .offset(*offset)
+ }))?
+ .label(label)
+ .legend(move |(x, y)| Rectangle::new([(x, y - 6), (x + 12, y + 6)], style.filled()));
+ }
+ chart
+ .configure_series_labels()
+ .position(SeriesLabelPosition::UpperRight)
+ .background_style(WHITE.filled())
+ .border_style(&BLACK.mix(0.5))
+ .legend_area_size(22)
+ .draw()?;
+
+ let drawing_areas = lower.split_evenly((1, 2));
+ let (left, right) = (&drawing_areas[0], &drawing_areas[1]);
+
+ let quartiles_a = Quartiles::new(&[
+ 6.0, 7.0, 15.9, 36.9, 39.0, 40.0, 41.0, 42.0, 43.0, 47.0, 49.0,
+ ]);
+ let quartiles_b = Quartiles::new(&[16.0, 17.0, 50.0, 60.0, 40.2, 41.3, 42.7, 43.3, 47.0]);
+ let category_ab = Category::new("", vec!["a", "b"]);
+ let values_range = fitting_range(
+ quartiles_a
+ .values()
+ .iter()
+ .chain(quartiles_b.values().iter()),
+ );
+ let mut chart = ChartBuilder::on(&left)
+ .x_label_area_size(40)
+ .y_label_area_size(40)
+ .caption("Vertical Boxplot", ("sans-serif", 20).into_font())
+ .build_ranged(
+ category_ab.clone(),
+ values_range.start - 10.0..values_range.end + 10.0,
+ )?;
+
+ chart.configure_mesh().line_style_2(&WHITE).draw()?;
+ chart.draw_series(vec![
+ Boxplot::new_vertical(category_ab.get(&"a").unwrap(), &quartiles_a),
+ Boxplot::new_vertical(category_ab.get(&"b").unwrap(), &quartiles_b),
+ ])?;
+
+ let mut chart = ChartBuilder::on(&right)
+ .x_label_area_size(40)
+ .y_label_area_size(40)
+ .caption("Horizontal Boxplot", ("sans-serif", 20).into_font())
+ .build_ranged(-30f32..90f32, 0..3)?;
+
+ chart.configure_mesh().line_style_2(&WHITE).draw()?;
+ chart.draw_series(vec![
+ Boxplot::new_horizontal(1, &quartiles_a),
+ Boxplot::new_horizontal(2, &Quartiles::new(&[30])),
+ ])?;
+
+ Ok(())
+}
+
+fn get_data() -> String {
+ String::from(
+ "
+ 1.1.1.1 wireless 41.6
+ 1.1.1.1 wireless 32.5
+ 1.1.1.1 wireless 33.1
+ 1.1.1.1 wireless 32.3
+ 1.1.1.1 wireless 36.7
+ 1.1.1.1 wireless 32.0
+ 1.1.1.1 wireless 33.1
+ 1.1.1.1 wireless 32.0
+ 1.1.1.1 wireless 32.9
+ 1.1.1.1 wireless 32.7
+ 1.1.1.1 wireless 34.5
+ 1.1.1.1 wireless 36.5
+ 1.1.1.1 wireless 31.9
+ 1.1.1.1 wireless 33.7
+ 1.1.1.1 wireless 32.6
+ 1.1.1.1 wireless 35.1
+ 8.8.8.8 wireless 42.3
+ 8.8.8.8 wireless 32.9
+ 8.8.8.8 wireless 32.9
+ 8.8.8.8 wireless 34.3
+ 8.8.8.8 wireless 32.0
+ 8.8.8.8 wireless 33.3
+ 8.8.8.8 wireless 31.5
+ 8.8.8.8 wireless 33.1
+ 8.8.8.8 wireless 33.2
+ 8.8.8.8 wireless 35.9
+ 8.8.8.8 wireless 42.3
+ 8.8.8.8 wireless 34.1
+ 8.8.8.8 wireless 34.2
+ 8.8.8.8 wireless 34.2
+ 8.8.8.8 wireless 32.4
+ 8.8.8.8 wireless 33.0
+ 1.1.1.1 wired 31.8
+ 1.1.1.1 wired 28.6
+ 1.1.1.1 wired 29.4
+ 1.1.1.1 wired 28.8
+ 1.1.1.1 wired 28.2
+ 1.1.1.1 wired 28.8
+ 1.1.1.1 wired 28.4
+ 1.1.1.1 wired 28.6
+ 1.1.1.1 wired 28.3
+ 1.1.1.1 wired 28.5
+ 1.1.1.1 wired 28.5
+ 1.1.1.1 wired 28.5
+ 1.1.1.1 wired 28.4
+ 1.1.1.1 wired 28.6
+ 1.1.1.1 wired 28.4
+ 1.1.1.1 wired 28.9
+ 8.8.8.8 wired 33.3
+ 8.8.8.8 wired 28.4
+ 8.8.8.8 wired 28.7
+ 8.8.8.8 wired 29.1
+ 8.8.8.8 wired 29.6
+ 8.8.8.8 wired 28.9
+ 8.8.8.8 wired 28.6
+ 8.8.8.8 wired 29.3
+ 8.8.8.8 wired 28.6
+ 8.8.8.8 wired 29.1
+ 8.8.8.8 wired 28.7
+ 8.8.8.8 wired 28.3
+ 8.8.8.8 wired 28.3
+ 8.8.8.8 wired 28.6
+ 8.8.8.8 wired 29.4
+ 8.8.8.8 wired 33.1
+",
+ )
+}
diff --git a/examples/chart.rs b/examples/chart.rs
new file mode 100644
index 0000000..15fb23d
--- /dev/null
+++ b/examples/chart.rs
@@ -0,0 +1,98 @@
+use plotters::prelude::*;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root_area =
+ BitMapBackend::new("plotters-doc-data/sample.png", (1024, 768)).into_drawing_area();
+
+ root_area.fill(&WHITE)?;
+
+ let root_area = root_area.titled("Image Title", ("sans-serif", 60).into_font())?;
+
+ let (upper, lower) = root_area.split_vertically(512);
+
+ let mut cc = ChartBuilder::on(&upper)
+ .margin(5)
+ .set_all_label_area_size(50)
+ .caption("Sine and Cosine", ("sans-serif", 40).into_font())
+ .build_ranged(-3.4f32..3.4f32, -1.2f32..1.2f32)?;
+
+ cc.configure_mesh()
+ .x_labels(20)
+ .y_labels(10)
+ .disable_mesh()
+ .x_label_formatter(&|v| format!("{:.1}", v))
+ .y_label_formatter(&|v| format!("{:.1}", v))
+ .draw()?;
+
+ cc.draw_series(LineSeries::new(
+ (0..12).map(|x| ((x - 6) as f32 / 2.0, ((x - 6) as f32 / 2.0).sin())),
+ &RED,
+ ))?
+ .label("Sine")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));
+
+ cc.draw_series(LineSeries::new(
+ (0..6800).map(|x| {
+ (
+ (x - 3400) as f32 / 1000.0,
+ ((x - 3400) as f32 / 1000.0).cos(),
+ )
+ }),
+ &BLUE,
+ ))?
+ .label("Cosine")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE));
+
+ cc.configure_series_labels().border_style(&BLACK).draw()?;
+
+ /*
+ // It's possible to use a existing pointing element
+ cc.draw_series(PointSeries::<_, _, Circle<_>>::new(
+ (0..6).map(|x| ((x - 3) as f32 / 1.0, ((x - 3) as f32 / 1.0).sin())),
+ 5,
+ Into::<ShapeStyle>::into(&RGBColor(255,0,0)).filled(),
+ ))?;*/
+
+ // Otherwise you can use a function to construct your pointing element yourself
+ cc.draw_series(PointSeries::of_element(
+ (0..6).map(|x| ((x - 3) as f32 / 1.0, ((x - 3) as f32 / 1.0).sin())),
+ 5,
+ ShapeStyle::from(&RED).filled(),
+ &|coord, size, style| {
+ EmptyElement::at(coord)
+ + Circle::new((0, 0), size, style)
+ + Text::new(
+ format!("{:?}", coord),
+ (0, 15),
+ ("sans-serif", 15).into_font(),
+ )
+ },
+ ))?;
+
+ let drawing_areas = lower.split_evenly((1, 2));
+
+ for (drawing_area, idx) in drawing_areas.iter().zip(1..) {
+ let mut cc = ChartBuilder::on(&drawing_area)
+ .x_label_area_size(30)
+ .y_label_area_size(30)
+ .margin_right(20)
+ .caption(
+ format!("y = x^{}", 1 + 2 * idx),
+ ("sans-serif", 40).into_font(),
+ )
+ .build_ranged(-1f32..1f32, -1f32..1f32)?;
+ cc.configure_mesh().x_labels(5).y_labels(3).draw()?;
+
+ cc.draw_series(LineSeries::new(
+ (-100..100).map(|x| {
+ (
+ x as f32 / 100.0,
+ (x as f32 / 100.0).powf(idx as f32 * 2.0 + 1.0),
+ )
+ }),
+ &BLUE,
+ ))?;
+ }
+
+ Ok(())
+}
diff --git a/examples/console.rs b/examples/console.rs
new file mode 100644
index 0000000..663b3ef
--- /dev/null
+++ b/examples/console.rs
@@ -0,0 +1,195 @@
+use plotters::drawing::{
+ backend::{BackendStyle, DrawingErrorKind},
+ DrawingBackend,
+};
+use plotters::prelude::*;
+use plotters::style::text_anchor::{HPos, VPos};
+use plotters::style::RGBAColor;
+use std::error::Error;
+
+#[derive(Copy, Clone)]
+enum PixelState {
+ Empty,
+ HLine,
+ VLine,
+ Cross,
+ Pixel,
+ Text(char),
+ Circle(bool),
+}
+
+impl PixelState {
+ fn to_char(self) -> char {
+ match self {
+ Self::Empty => ' ',
+ Self::HLine => '-',
+ Self::VLine => '|',
+ Self::Cross => '+',
+ Self::Pixel => '.',
+ Self::Text(c) => c,
+ Self::Circle(filled) => {
+ if filled {
+ '@'
+ } else {
+ 'O'
+ }
+ }
+ }
+ }
+
+ fn update(&mut self, new_state: PixelState) {
+ let next_state = match (*self, new_state) {
+ (Self::HLine, Self::VLine) => Self::Cross,
+ (Self::VLine, Self::HLine) => Self::Cross,
+ (_, Self::Circle(what)) => Self::Circle(what),
+ (Self::Circle(what), _) => Self::Circle(what),
+ (_, Self::Pixel) => Self::Pixel,
+ (Self::Pixel, _) => Self::Pixel,
+ (_, new) => new,
+ };
+
+ *self = next_state;
+ }
+}
+
+pub struct TextDrawingBackend(Vec<PixelState>);
+
+impl DrawingBackend for TextDrawingBackend {
+ type ErrorType = std::io::Error;
+
+ fn get_size(&self) -> (u32, u32) {
+ (100, 30)
+ }
+
+ fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<std::io::Error>> {
+ Ok(())
+ }
+
+ fn present(&mut self) -> Result<(), DrawingErrorKind<std::io::Error>> {
+ for r in 0..30 {
+ let mut buf = String::new();
+ for c in 0..100 {
+ buf.push(self.0[r * 100 + c].to_char());
+ }
+ println!("{}", buf);
+ }
+
+ Ok(())
+ }
+
+ fn draw_pixel(
+ &mut self,
+ pos: (i32, i32),
+ color: &RGBAColor,
+ ) -> Result<(), DrawingErrorKind<std::io::Error>> {
+ if color.alpha() > 0.3 {
+ self.0[(pos.1 * 100 + pos.0) as usize].update(PixelState::Pixel);
+ }
+ Ok(())
+ }
+
+ fn draw_line<S: BackendStyle>(
+ &mut self,
+ from: (i32, i32),
+ to: (i32, i32),
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if from.0 == to.0 {
+ let x = from.0;
+ let y0 = from.1.min(to.1);
+ let y1 = from.1.max(to.1);
+ for y in y0..y1 {
+ self.0[(y * 100 + x) as usize].update(PixelState::VLine);
+ }
+ return Ok(());
+ }
+
+ if from.1 == to.1 {
+ let y = from.1;
+ let x0 = from.0.min(to.0);
+ let x1 = from.0.max(to.0);
+ for x in x0..x1 {
+ self.0[(y * 100 + x) as usize].update(PixelState::HLine);
+ }
+ return Ok(());
+ }
+
+ plotters::drawing::rasterizer::draw_line(self, from, to, style)
+ }
+
+ fn estimate_text_size<'a>(
+ &self,
+ text: &str,
+ _font: &FontDesc<'a>,
+ ) -> Result<(u32, u32), DrawingErrorKind<Self::ErrorType>> {
+ Ok((text.len() as u32, 1))
+ }
+
+ fn draw_text(
+ &mut self,
+ text: &str,
+ style: &TextStyle,
+ pos: (i32, i32),
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let (width, height) = self.estimate_text_size(text, &style.font)?;
+ let (width, height) = (width as i32, height as i32);
+ let dx = match style.pos.h_pos {
+ HPos::Left => 0,
+ HPos::Right => -width,
+ HPos::Center => -width / 2,
+ };
+ let dy = match style.pos.v_pos {
+ VPos::Top => 0,
+ VPos::Center => -height / 2,
+ VPos::Bottom => -height,
+ };
+ let offset = (pos.1 + dy).max(0) * 100 + (pos.0 + dx).max(0);
+ for (idx, chr) in (offset..).zip(text.chars()) {
+ self.0[idx as usize].update(PixelState::Text(chr));
+ }
+ Ok(())
+ }
+}
+
+fn draw_chart<DB: DrawingBackend>(
+ b: DrawingArea<DB, plotters::coord::Shift>,
+) -> Result<(), Box<dyn Error>>
+where
+ DB::ErrorType: 'static,
+{
+ let mut chart = ChartBuilder::on(&b)
+ .margin(1)
+ .caption("Sine and Cosine", ("sans-serif", (10).percent_height()))
+ .set_label_area_size(LabelAreaPosition::Left, (5i32).percent_width())
+ .set_label_area_size(LabelAreaPosition::Bottom, (10i32).percent_height())
+ .build_ranged(-std::f64::consts::PI..std::f64::consts::PI, -1.2..1.2)?;
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .draw()?;
+
+ chart.draw_series(LineSeries::new(
+ (-314..314).map(|x| x as f64 / 100.0).map(|x| (x, x.sin())),
+ &RED,
+ ))?;
+
+ chart.draw_series(LineSeries::new(
+ (-314..314).map(|x| x as f64 / 100.0).map(|x| (x, x.cos())),
+ &RED,
+ ))?;
+
+ b.present()?;
+
+ Ok(())
+}
+
+fn main() -> Result<(), Box<dyn Error>> {
+ draw_chart(TextDrawingBackend(vec![PixelState::Empty; 5000]).into_drawing_area())?;
+ let b = BitMapBackend::new("plotters-doc-data/console-example.png", (1024, 768))
+ .into_drawing_area();
+ b.fill(&WHITE)?;
+ draw_chart(b)?;
+ Ok(())
+}
diff --git a/examples/errorbar.rs b/examples/errorbar.rs
new file mode 100644
index 0000000..4ef46d2
--- /dev/null
+++ b/examples/errorbar.rs
@@ -0,0 +1,90 @@
+use plotters::prelude::*;
+
+use rand::SeedableRng;
+use rand_distr::{Distribution, Normal};
+use rand_xorshift::XorShiftRng;
+
+use itertools::Itertools;
+
+use num_traits::sign::Signed;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let data = generate_random_data();
+ let down_sampled = down_sample(&data[..]);
+
+ let root =
+ BitMapBackend::new("plotters-doc-data/errorbar.png", (1024, 768)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("Linear Function with Noise", ("sans-serif", 60))
+ .margin(10)
+ .set_label_area_size(LabelAreaPosition::Left, 40)
+ .set_label_area_size(LabelAreaPosition::Bottom, 40)
+ .build_ranged(-10f64..10f64, -10f64..10f64)?;
+
+ chart.configure_mesh().draw()?;
+
+ chart
+ .draw_series(LineSeries::new(data, &GREEN.mix(0.3)))?
+ .label("Raw Data")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &GREEN));
+
+ chart.draw_series(LineSeries::new(
+ down_sampled.iter().map(|(x, _, y, _)| (*x, *y)),
+ &BLUE,
+ ))?;
+
+ chart
+ .draw_series(
+ down_sampled.iter().map(|(x, yl, ym, yh)| {
+ ErrorBar::new_vertical(*x, *yl, *ym, *yh, BLUE.filled(), 20)
+ }),
+ )?
+ .label("Down-sampled")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE));
+
+ chart
+ .configure_series_labels()
+ .background_style(WHITE.filled())
+ .draw()?;
+
+ Ok(())
+}
+
+fn generate_random_data() -> Vec<(f64, f64)> {
+ let norm_dist = Normal::new(0.0, 1.0).unwrap();
+ let mut x_rand = XorShiftRng::from_seed(*b"MyFragileSeed123");
+ let x_iter = norm_dist.sample_iter(&mut x_rand);
+ x_iter
+ .take(20000)
+ .filter(|x| x.abs() <= 4.0)
+ .zip(-10000..10000)
+ .map(|(yn, x)| {
+ (
+ x as f64 / 1000.0,
+ x as f64 / 1000.0 + yn * x as f64 / 10000.0,
+ )
+ })
+ .collect()
+}
+
+fn down_sample(data: &[(f64, f64)]) -> Vec<(f64, f64, f64, f64)> {
+ let down_sampled: Vec<_> = data
+ .iter()
+ .group_by(|x| (x.0 * 1.0).round() / 1.0)
+ .into_iter()
+ .map(|(x, g)| {
+ let mut g: Vec<_> = g.map(|(_, y)| *y).collect();
+ g.sort_by(|a, b| a.partial_cmp(b).unwrap());
+ (
+ x,
+ g[0],
+ g.iter().sum::<f64>() / g.len() as f64,
+ g[g.len() - 1],
+ )
+ })
+ .collect();
+ down_sampled
+}
diff --git a/examples/histogram.rs b/examples/histogram.rs
new file mode 100644
index 0000000..c33363f
--- /dev/null
+++ b/examples/histogram.rs
@@ -0,0 +1,36 @@
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/histogram.png", (640, 480)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .x_label_area_size(35)
+ .y_label_area_size(40)
+ .margin(5)
+ .caption("Histogram Test", ("sans-serif", 50.0).into_font())
+ .build_ranged(0u32..10u32, 0u32..10u32)?;
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .line_style_1(&WHITE.mix(0.3))
+ .x_label_offset(30)
+ .y_desc("Count")
+ .x_desc("Bucket")
+ .axis_desc_style(("sans-serif", 15).into_font())
+ .draw()?;
+
+ let data = [
+ 0u32, 1, 1, 1, 4, 2, 5, 7, 8, 6, 4, 2, 1, 8, 3, 3, 3, 4, 4, 3, 3, 3,
+ ];
+
+ chart.draw_series(
+ Histogram::vertical(&chart)
+ .style(RED.mix(0.5).filled())
+ .data(data.iter().map(|x: &u32| (*x, 1))),
+ )?;
+
+ Ok(())
+}
diff --git a/examples/mandelbrot.rs b/examples/mandelbrot.rs
new file mode 100644
index 0000000..81fa688
--- /dev/null
+++ b/examples/mandelbrot.rs
@@ -0,0 +1,63 @@
+use plotters::prelude::*;
+use std::ops::Range;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/mandelbrot.png", (800, 600)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .margin(20)
+ .x_label_area_size(10)
+ .y_label_area_size(10)
+ .build_ranged(-2.1f64..0.6f64, -1.2f64..1.2f64)?;
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .draw()?;
+
+ let plotting_area = chart.plotting_area();
+
+ let range = plotting_area.get_pixel_range();
+
+ let (pw, ph) = (range.0.end - range.0.start, range.1.end - range.1.start);
+ let (xr, yr) = (chart.x_range(), chart.y_range());
+
+ for (x, y, c) in mandelbrot_set(xr, yr, (pw as usize, ph as usize), 100) {
+ if c != 100 {
+ plotting_area.draw_pixel((x, y), &HSLColor(c as f64 / 100.0, 1.0, 0.5))?;
+ } else {
+ plotting_area.draw_pixel((x, y), &BLACK)?;
+ }
+ }
+
+ Ok(())
+}
+
+fn mandelbrot_set(
+ real: Range<f64>,
+ complex: Range<f64>,
+ samples: (usize, usize),
+ max_iter: usize,
+) -> impl Iterator<Item = (f64, f64, usize)> {
+ let step = (
+ (real.end - real.start) / samples.0 as f64,
+ (complex.end - complex.start) / samples.1 as f64,
+ );
+ return (0..(samples.0 * samples.1)).map(move |k| {
+ let c = (
+ real.start + step.0 * (k % samples.0) as f64,
+ complex.start + step.1 * (k / samples.0) as f64,
+ );
+ let mut z = (0.0, 0.0);
+ let mut cnt = 0;
+ while cnt < max_iter && z.0 * z.0 + z.1 * z.1 <= 1e10 {
+ z = (z.0 * z.0 - z.1 * z.1 + c.0, 2.0 * z.0 * z.1 + c.1);
+ cnt += 1;
+ }
+ return (c.0, c.1, cnt);
+ });
+}
diff --git a/examples/matshow.rs b/examples/matshow.rs
new file mode 100644
index 0000000..186fcba
--- /dev/null
+++ b/examples/matshow.rs
@@ -0,0 +1,52 @@
+use plotters::prelude::*;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new("plotters-doc-data/matshow.png", (1024, 768)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("Matshow Example", ("sans-serif", 80))
+ .margin(5)
+ .top_x_label_area_size(40)
+ .y_label_area_size(40)
+ .build_ranged(0i32..15i32, 15i32..0i32)?;
+
+ chart
+ .configure_mesh()
+ .x_labels(15)
+ .y_labels(15)
+ .x_label_offset(35)
+ .y_label_offset(25)
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .label_style(("sans-serif", 20))
+ .draw()?;
+
+ let mut matrix = [[0; 15]; 15];
+
+ for i in 0..15 {
+ matrix[i][i] = i + 4;
+ }
+
+ chart.draw_series(
+ matrix
+ .iter()
+ .zip(0..)
+ .map(|(l, y)| l.iter().zip(0..).map(move |(v, x)| (x as i32, y as i32, v)))
+ .flatten()
+ .map(|(x, y, v)| {
+ Rectangle::new(
+ [(x, y), (x + 1, y + 1)],
+ HSLColor(
+ 240.0 / 360.0 - 240.0 / 360.0 * (*v as f64 / 20.0),
+ 0.7,
+ 0.1 + 0.4 * *v as f64 / 20.0,
+ )
+ .filled(),
+ )
+ }),
+ )?;
+
+ Ok(())
+}
diff --git a/examples/normal-dist.rs b/examples/normal-dist.rs
new file mode 100644
index 0000000..65cf0e3
--- /dev/null
+++ b/examples/normal-dist.rs
@@ -0,0 +1,66 @@
+use plotters::prelude::*;
+
+use rand::SeedableRng;
+use rand_distr::{Distribution, Normal};
+use rand_xorshift::XorShiftRng;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/normal-dist.png", (1024, 768)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let sd = 0.13;
+
+ let random_points: Vec<(f64, f64)> = {
+ let norm_dist = Normal::new(0.5, sd).unwrap();
+ let mut x_rand = XorShiftRng::from_seed(*b"MyFragileSeed123");
+ let mut y_rand = XorShiftRng::from_seed(*b"MyFragileSeed321");
+ let x_iter = norm_dist.sample_iter(&mut x_rand);
+ let y_iter = norm_dist.sample_iter(&mut y_rand);
+ x_iter.zip(y_iter).take(5000).collect()
+ };
+
+ let areas = root.split_by_breakpoints([944], [80]);
+
+ let mut x_hist_ctx = ChartBuilder::on(&areas[0])
+ .y_label_area_size(40)
+ .build_ranged(0u32..100u32, 0f64..0.5f64)?;
+ let mut y_hist_ctx = ChartBuilder::on(&areas[3])
+ .x_label_area_size(40)
+ .build_ranged(0f64..0.5f64, 0..100u32)?;
+ let mut scatter_ctx = ChartBuilder::on(&areas[2])
+ .x_label_area_size(40)
+ .y_label_area_size(40)
+ .build_ranged(0f64..1f64, 0f64..1f64)?;
+ scatter_ctx
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .draw()?;
+ scatter_ctx.draw_series(
+ random_points
+ .iter()
+ .map(|(x, y)| Circle::new((*x, *y), 2, GREEN.filled())),
+ )?;
+ let x_hist = Histogram::vertical(&x_hist_ctx)
+ .style(GREEN.filled())
+ .margin(0)
+ .data(
+ random_points
+ .iter()
+ .map(|(x, _)| ((x * 100.0) as u32, 0.002)),
+ );
+ let y_hist = Histogram::horizontal(&y_hist_ctx)
+ .style(GREEN.filled())
+ .margin(0)
+ .data(
+ random_points
+ .iter()
+ .map(|(_, y)| ((y * 100.0) as u32, 0.002)),
+ );
+ x_hist_ctx.draw_series(x_hist)?;
+ y_hist_ctx.draw_series(y_hist)?;
+
+ Ok(())
+}
diff --git a/examples/normal-dist2.rs b/examples/normal-dist2.rs
new file mode 100644
index 0000000..6155ea6
--- /dev/null
+++ b/examples/normal-dist2.rs
@@ -0,0 +1,76 @@
+use plotters::prelude::*;
+
+use rand::SeedableRng;
+use rand_distr::{Distribution, Normal};
+use rand_xorshift::XorShiftRng;
+
+use num_traits::sign::Signed;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let sd = 0.60;
+
+ let random_points: Vec<f64> = {
+ let norm_dist = Normal::new(0.0, sd).unwrap();
+ let mut x_rand = XorShiftRng::from_seed(*b"MyFragileSeed123");
+ let x_iter = norm_dist.sample_iter(&mut x_rand);
+ x_iter.take(5000).filter(|x| x.abs() <= 4.0).collect()
+ };
+
+ let root =
+ BitMapBackend::new("plotters-doc-data/normal-dist2.png", (1024, 768)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .margin(5)
+ .caption("1D Gaussian Distribution Demo", ("sans-serif", 30))
+ .set_label_area_size(LabelAreaPosition::Left, 60)
+ .set_label_area_size(LabelAreaPosition::Bottom, 60)
+ .set_label_area_size(LabelAreaPosition::Right, 60)
+ .build_ranged(-4f64..4f64, 0f64..0.1)?
+ .set_secondary_coord((-40i32..40i32).into_centric(), 0u32..500u32);
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .y_label_formatter(&|y| format!("{:.0}%", *y * 100.0))
+ .y_desc("Percentage")
+ .draw()?;
+
+ chart.configure_secondary_axes().y_desc("Count").draw()?;
+
+ let actual = Histogram::vertical(chart.borrow_secondary())
+ .style(GREEN.filled())
+ .margin(3)
+ .data(
+ random_points
+ .iter()
+ .map(|x| ((x * 10.0).round() as i32, 1u32)),
+ );
+
+ chart
+ .draw_secondary_series(actual)?
+ .label("Observed")
+ .legend(|(x, y)| Rectangle::new([(x, y - 5), (x + 10, y + 5)], GREEN.filled()));
+
+ let pdf = LineSeries::new(
+ (-400..400).map(|x| x as f64 / 100.0).map(|x| {
+ (
+ x,
+ (-x * x / 2.0 / sd / sd).exp() / (2.0 * std::f64::consts::PI * sd * sd).sqrt()
+ * 0.1,
+ )
+ }),
+ &RED,
+ );
+
+ chart
+ .draw_series(pdf)?
+ .label("PDF")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED.filled()));
+
+ chart.configure_series_labels().draw()?;
+
+ Ok(())
+}
diff --git a/examples/relative_size.rs b/examples/relative_size.rs
new file mode 100644
index 0000000..a915d8a
--- /dev/null
+++ b/examples/relative_size.rs
@@ -0,0 +1,49 @@
+use plotters::coord::Shift;
+use plotters::prelude::*;
+
+fn draw_chart<B: DrawingBackend>(root: &DrawingArea<B, Shift>) -> DrawResult<(), B> {
+ let mut chart = ChartBuilder::on(root)
+ .caption(
+ "Relative Size Example",
+ ("sans-serif", (5).percent_height()),
+ )
+ .x_label_area_size((10).percent_height())
+ .y_label_area_size((10).percent_width())
+ .margin(5)
+ .build_ranged(-5.0..5.0, -1.0..1.0)?;
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .label_style(("sans-serif", (3).percent_height()))
+ .draw()?;
+
+ chart.draw_series(LineSeries::new(
+ (0..1000)
+ .map(|x| x as f64 / 100.0 - 5.0)
+ .map(|x| (x, x.sin())),
+ &RED,
+ ))?;
+ Ok(())
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/relative_size.png", (1024, 768)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let (left, right) = root.split_horizontally((70).percent_width());
+
+ draw_chart(&left)?;
+
+ let (upper, lower) = right.split_vertically(300);
+
+ draw_chart(&upper)?;
+ draw_chart(&lower)?;
+
+ draw_chart(&root.shrink((200, 200), (150, 100)))?;
+
+ Ok(())
+}
diff --git a/examples/sierpinski.rs b/examples/sierpinski.rs
new file mode 100644
index 0000000..9e68589
--- /dev/null
+++ b/examples/sierpinski.rs
@@ -0,0 +1,33 @@
+use plotters::coord::Shift;
+use plotters::prelude::*;
+
+pub fn sierpinski_carpet(
+ depth: u32,
+ drawing_area: &DrawingArea<BitMapBackend, Shift>,
+) -> Result<(), Box<dyn std::error::Error>> {
+ if depth > 0 {
+ let sub_areas = drawing_area.split_evenly((3, 3));
+ for (idx, sub_area) in (0..).zip(sub_areas.iter()) {
+ if idx != 4 {
+ sub_area.fill(&BLUE)?;
+ sierpinski_carpet(depth - 1, sub_area)?;
+ } else {
+ sub_area.fill(&WHITE)?;
+ }
+ }
+ }
+ Ok(())
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/sierpinski.png", (1024, 768)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let root = root
+ .titled("Sierpinski Carpet Demo", ("sans-serif", 60))?
+ .shrink(((1024 - 700) / 2, 0), (700, 700));
+
+ sierpinski_carpet(5, &root)
+}
diff --git a/examples/slc-temp.rs b/examples/slc-temp.rs
new file mode 100644
index 0000000..7a1b509
--- /dev/null
+++ b/examples/slc-temp.rs
@@ -0,0 +1,167 @@
+use plotters::coord::IntoMonthly;
+use plotters::prelude::*;
+
+use chrono::{Datelike, TimeZone, Utc};
+
+use std::error::Error;
+
+fn main() -> Result<(), Box<dyn Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/slc-temp.png", (1024, 768)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .margin(10)
+ .caption(
+ "Monthly Average Temperate in Salt Lake City, UT",
+ ("sans-serif", 40),
+ )
+ .set_label_area_size(LabelAreaPosition::Left, 60)
+ .set_label_area_size(LabelAreaPosition::Right, 60)
+ .set_label_area_size(LabelAreaPosition::Bottom, 40)
+ .build_ranged(
+ (Utc.ymd(2010, 1, 1)..Utc.ymd(2018, 12, 1)).monthly(),
+ 14.0..104.0,
+ )?
+ .set_secondary_coord(
+ (Utc.ymd(2010, 1, 1)..Utc.ymd(2018, 12, 1)).monthly(),
+ -10.0..40.0,
+ );
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .x_labels(30)
+ .x_label_formatter(&|d| format!("{}-{}", d.year(), d.month()))
+ .y_desc("Average Temp (F)")
+ .draw()?;
+ chart
+ .configure_secondary_axes()
+ .y_desc("Average Temp (C)")
+ .draw()?;
+
+ chart.draw_series(LineSeries::new(
+ DATA.iter().map(|(y, m, t)| (Utc.ymd(*y, *m, 1), *t)),
+ &BLUE,
+ ))?;
+
+ chart.draw_series(
+ DATA.iter()
+ .map(|(y, m, t)| Circle::new((Utc.ymd(*y, *m, 1), *t), 3, BLUE.filled())),
+ )?;
+
+ Ok(())
+}
+
+const DATA: [(i32, u32, f64); 12 * 9] = [
+ (2010, 1, 32.4),
+ (2010, 2, 37.5),
+ (2010, 3, 44.5),
+ (2010, 4, 50.3),
+ (2010, 5, 55.0),
+ (2010, 6, 70.0),
+ (2010, 7, 78.7),
+ (2010, 8, 76.5),
+ (2010, 9, 68.9),
+ (2010, 10, 56.3),
+ (2010, 11, 40.3),
+ (2010, 12, 36.5),
+ (2011, 1, 28.8),
+ (2011, 2, 35.1),
+ (2011, 3, 45.5),
+ (2011, 4, 48.9),
+ (2011, 5, 55.1),
+ (2011, 6, 68.8),
+ (2011, 7, 77.9),
+ (2011, 8, 78.4),
+ (2011, 9, 68.2),
+ (2011, 10, 55.0),
+ (2011, 11, 41.5),
+ (2011, 12, 31.0),
+ (2012, 1, 35.6),
+ (2012, 2, 38.1),
+ (2012, 3, 49.1),
+ (2012, 4, 56.1),
+ (2012, 5, 63.4),
+ (2012, 6, 73.0),
+ (2012, 7, 79.0),
+ (2012, 8, 79.0),
+ (2012, 9, 68.8),
+ (2012, 10, 54.9),
+ (2012, 11, 45.2),
+ (2012, 12, 34.9),
+ (2013, 1, 19.7),
+ (2013, 2, 31.1),
+ (2013, 3, 46.2),
+ (2013, 4, 49.8),
+ (2013, 5, 61.3),
+ (2013, 6, 73.3),
+ (2013, 7, 80.3),
+ (2013, 8, 77.2),
+ (2013, 9, 68.3),
+ (2013, 10, 52.0),
+ (2013, 11, 43.2),
+ (2013, 12, 25.7),
+ (2014, 1, 31.5),
+ (2014, 2, 39.3),
+ (2014, 3, 46.4),
+ (2014, 4, 52.5),
+ (2014, 5, 63.0),
+ (2014, 6, 71.3),
+ (2014, 7, 81.0),
+ (2014, 8, 75.3),
+ (2014, 9, 70.0),
+ (2014, 10, 58.6),
+ (2014, 11, 42.1),
+ (2014, 12, 38.0),
+ (2015, 1, 35.3),
+ (2015, 2, 45.2),
+ (2015, 3, 50.9),
+ (2015, 4, 54.3),
+ (2015, 5, 60.5),
+ (2015, 6, 77.1),
+ (2015, 7, 76.2),
+ (2015, 8, 77.3),
+ (2015, 9, 70.4),
+ (2015, 10, 60.6),
+ (2015, 11, 40.9),
+ (2015, 12, 32.4),
+ (2016, 1, 31.5),
+ (2016, 2, 35.1),
+ (2016, 3, 49.1),
+ (2016, 4, 55.1),
+ (2016, 5, 60.9),
+ (2016, 6, 76.9),
+ (2016, 7, 80.0),
+ (2016, 8, 77.0),
+ (2016, 9, 67.1),
+ (2016, 10, 59.1),
+ (2016, 11, 47.4),
+ (2016, 12, 31.8),
+ (2017, 1, 29.4),
+ (2017, 2, 42.4),
+ (2017, 3, 51.7),
+ (2017, 4, 51.7),
+ (2017, 5, 62.5),
+ (2017, 6, 74.8),
+ (2017, 7, 81.3),
+ (2017, 8, 78.1),
+ (2017, 9, 65.7),
+ (2017, 10, 52.5),
+ (2017, 11, 49.0),
+ (2017, 12, 34.4),
+ (2018, 1, 38.1),
+ (2018, 2, 37.5),
+ (2018, 3, 45.4),
+ (2018, 4, 54.6),
+ (2018, 5, 64.0),
+ (2018, 6, 74.9),
+ (2018, 7, 82.5),
+ (2018, 8, 78.1),
+ (2018, 9, 71.9),
+ (2018, 10, 53.2),
+ (2018, 11, 39.7),
+ (2018, 12, 33.6),
+];
diff --git a/examples/snowflake.rs b/examples/snowflake.rs
new file mode 100644
index 0000000..ccefb8e
--- /dev/null
+++ b/examples/snowflake.rs
@@ -0,0 +1,50 @@
+use plotters::prelude::*;
+
+fn snowflake_iter(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
+ let mut ret = vec![];
+ for i in 0..points.len() {
+ let (start, end) = (points[i], points[(i + 1) % points.len()]);
+ let t = ((end.0 - start.0) / 3.0, (end.1 - start.1) / 3.0);
+ let s = (
+ t.0 * 0.5 - t.1 * (0.75f64).sqrt(),
+ t.1 * 0.5 + (0.75f64).sqrt() * t.0,
+ );
+ ret.push(start);
+ ret.push((start.0 + t.0, start.1 + t.1));
+ ret.push((start.0 + t.0 + s.0, start.1 + t.1 + s.1));
+ ret.push((start.0 + t.0 * 2.0, start.1 + t.1 * 2.0));
+ }
+ ret
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/snowflake.png", (1024, 768)).into_drawing_area();
+
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("Koch's Snowflake", ("sans-serif", 50))
+ .build_ranged(-2.0..2.0, -1.5..1.5)?;
+
+ let mut snowflake_vertices = {
+ let mut current: Vec<(f64, f64)> = vec![
+ (0.0, 1.0),
+ ((3.0f64).sqrt() / 2.0, -0.5),
+ (-(3.0f64).sqrt() / 2.0, -0.5),
+ ];
+ for _ in 0..6 {
+ current = snowflake_iter(&current[..]);
+ }
+ current
+ };
+
+ chart.draw_series(std::iter::once(Polygon::new(
+ snowflake_vertices.clone(),
+ &RED.mix(0.2),
+ )))?;
+ snowflake_vertices.push(snowflake_vertices[0]);
+ chart.draw_series(std::iter::once(PathElement::new(snowflake_vertices, &RED)))?;
+
+ Ok(())
+}
diff --git a/examples/stock.rs b/examples/stock.rs
new file mode 100644
index 0000000..5c5c2b5
--- /dev/null
+++ b/examples/stock.rs
@@ -0,0 +1,69 @@
+use chrono::offset::{Local, TimeZone};
+use chrono::{Date, Duration};
+use plotters::prelude::*;
+fn parse_time(t: &str) -> Date<Local> {
+ Local
+ .datetime_from_str(&format!("{} 0:0", t), "%Y-%m-%d %H:%M")
+ .unwrap()
+ .date()
+}
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let data = get_data();
+ let root = BitMapBackend::new("plotters-doc-data/stock.png", (1024, 768)).into_drawing_area();
+ root.fill(&WHITE)?;
+
+ let (to_date, from_date) = (
+ parse_time(&data[0].0) + Duration::days(1),
+ parse_time(&data[29].0) - Duration::days(1),
+ );
+
+ let mut chart = ChartBuilder::on(&root)
+ .x_label_area_size(40)
+ .y_label_area_size(40)
+ .caption("MSFT Stock Price", ("sans-serif", 50.0).into_font())
+ .build_ranged(from_date..to_date, 110f32..135f32)?;
+
+ chart.configure_mesh().line_style_2(&WHITE).draw()?;
+
+ chart.draw_series(
+ data.iter()
+ .map(|x| CandleStick::new(parse_time(x.0), x.1, x.2, x.3, x.4, &GREEN, &RED, 15)),
+ )?;
+
+ Ok(())
+}
+
+fn get_data() -> Vec<(&'static str, f32, f32, f32, f32)> {
+ return vec![
+ ("2019-04-25", 130.0600, 131.3700, 128.8300, 129.1500),
+ ("2019-04-24", 125.7900, 125.8500, 124.5200, 125.0100),
+ ("2019-04-23", 124.1000, 125.5800, 123.8300, 125.4400),
+ ("2019-04-22", 122.6200, 124.0000, 122.5700, 123.7600),
+ ("2019-04-18", 122.1900, 123.5200, 121.3018, 123.3700),
+ ("2019-04-17", 121.2400, 121.8500, 120.5400, 121.7700),
+ ("2019-04-16", 121.6400, 121.6500, 120.1000, 120.7700),
+ ("2019-04-15", 120.9400, 121.5800, 120.5700, 121.0500),
+ ("2019-04-12", 120.6400, 120.9800, 120.3700, 120.9500),
+ ("2019-04-11", 120.5400, 120.8500, 119.9200, 120.3300),
+ ("2019-04-10", 119.7600, 120.3500, 119.5400, 120.1900),
+ ("2019-04-09", 118.6300, 119.5400, 118.5800, 119.2800),
+ ("2019-04-08", 119.8100, 120.0200, 118.6400, 119.9300),
+ ("2019-04-05", 119.3900, 120.2300, 119.3700, 119.8900),
+ ("2019-04-04", 120.1000, 120.2300, 118.3800, 119.3600),
+ ("2019-04-03", 119.8600, 120.4300, 119.1500, 119.9700),
+ ("2019-04-02", 119.0600, 119.4800, 118.5200, 119.1900),
+ ("2019-04-01", 118.9500, 119.1085, 118.1000, 119.0200),
+ ("2019-03-29", 118.0700, 118.3200, 116.9600, 117.9400),
+ ("2019-03-28", 117.4400, 117.5800, 116.1300, 116.9300),
+ ("2019-03-27", 117.8750, 118.2100, 115.5215, 116.7700),
+ ("2019-03-26", 118.6200, 118.7050, 116.8500, 117.9100),
+ ("2019-03-25", 116.5600, 118.0100, 116.3224, 117.6600),
+ ("2019-03-22", 119.5000, 119.5900, 117.0400, 117.0500),
+ ("2019-03-21", 117.1350, 120.8200, 117.0900, 120.2200),
+ ("2019-03-20", 117.3900, 118.7500, 116.7100, 117.5200),
+ ("2019-03-19", 118.0900, 118.4400, 116.9900, 117.6500),
+ ("2019-03-18", 116.1700, 117.6100, 116.0500, 117.5700),
+ ("2019-03-15", 115.3400, 117.2500, 114.5900, 115.9100),
+ ("2019-03-14", 114.5400, 115.2000, 114.3300, 114.5900),
+ ];
+}
diff --git a/examples/two-scales.rs b/examples/two-scales.rs
new file mode 100644
index 0000000..33939e6
--- /dev/null
+++ b/examples/two-scales.rs
@@ -0,0 +1,52 @@
+use plotters::prelude::*;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/twoscale.png", (1024, 768)).into_drawing_area();
+ root.fill(&WHITE)?;
+
+ let mut chart = ChartBuilder::on(&root)
+ .x_label_area_size(35)
+ .y_label_area_size(40)
+ .right_y_label_area_size(40)
+ .margin(5)
+ .caption("Dual Y-Axis Example", ("sans-serif", 50.0).into_font())
+ .build_ranged(0f32..10f32, LogRange(0.1f32..1e10f32))?
+ .set_secondary_coord(0f32..10f32, -1.0f32..1.0f32);
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .y_desc("Log Scale")
+ .y_label_formatter(&|x| format!("{:e}", x))
+ .draw()?;
+
+ chart
+ .configure_secondary_axes()
+ .y_desc("Linear Scale")
+ .draw()?;
+
+ chart
+ .draw_series(LineSeries::new(
+ (0..=100).map(|x| (x as f32 / 10.0, (1.02f32).powf(x as f32 * x as f32 / 10.0))),
+ &BLUE,
+ ))?
+ .label("y = 1.02^x^2")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &BLUE));
+
+ chart
+ .draw_secondary_series(LineSeries::new(
+ (0..=100).map(|x| (x as f32 / 10.0, (x as f32 / 5.0).sin())),
+ &RED,
+ ))?
+ .label("y = sin(2x)")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));
+
+ chart
+ .configure_series_labels()
+ .background_style(&RGBColor(128, 128, 128))
+ .draw()?;
+
+ Ok(())
+}
diff --git a/plotters-doc-data/0.png b/plotters-doc-data/0.png
new file mode 100644
index 0000000..3743c2a
--- /dev/null
+++ b/plotters-doc-data/0.png
Binary files differ
diff --git a/plotters-doc-data/1.png b/plotters-doc-data/1.png
new file mode 100644
index 0000000..d491fa6
--- /dev/null
+++ b/plotters-doc-data/1.png
Binary files differ
diff --git a/plotters-doc-data/2.png b/plotters-doc-data/2.png
new file mode 100644
index 0000000..ee0e408
--- /dev/null
+++ b/plotters-doc-data/2.png
Binary files differ
diff --git a/plotters-doc-data/3.png b/plotters-doc-data/3.png
new file mode 100644
index 0000000..222af87
--- /dev/null
+++ b/plotters-doc-data/3.png
Binary files differ
diff --git a/plotters-doc-data/4.png b/plotters-doc-data/4.png
new file mode 100644
index 0000000..f6dcc97
--- /dev/null
+++ b/plotters-doc-data/4.png
Binary files differ
diff --git a/plotters-doc-data/5.png b/plotters-doc-data/5.png
new file mode 100644
index 0000000..6a5b09d
--- /dev/null
+++ b/plotters-doc-data/5.png
Binary files differ
diff --git a/plotters-doc-data/animation.gif b/plotters-doc-data/animation.gif
new file mode 100644
index 0000000..f85cf17
--- /dev/null
+++ b/plotters-doc-data/animation.gif
Binary files differ
diff --git a/plotters-doc-data/area-chart.png b/plotters-doc-data/area-chart.png
new file mode 100644
index 0000000..86cb9de
--- /dev/null
+++ b/plotters-doc-data/area-chart.png
Binary files differ
diff --git a/plotters-doc-data/blit-bitmap.png b/plotters-doc-data/blit-bitmap.png
new file mode 100644
index 0000000..f2de0d7
--- /dev/null
+++ b/plotters-doc-data/blit-bitmap.png
Binary files differ
diff --git a/plotters-doc-data/boxplot.svg b/plotters-doc-data/boxplot.svg
new file mode 100644
index 0000000..708dcea
--- /dev/null
+++ b/plotters-doc-data/boxplot.svg
@@ -0,0 +1,432 @@
+<svg width="1024" height="768" viewBox="0 0 1024 768" xmlns="http://www.w3.org/2000/svg">
+<rect x="0" y="0" width="1023" height="767" opacity="1" fill="#FFFFFF" stroke="none"/>
+<text x="512" y="10" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="16.129032258064516" opacity="1" fill="#000000">
+Ping Boxplot
+</text>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="91" y1="476" x2="91" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="106" y1="476" x2="106" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="121" y1="476" x2="121" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="136" y1="476" x2="136" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="151" y1="476" x2="151" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="166" y1="476" x2="166" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="181" y1="476" x2="181" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="196" y1="476" x2="196" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="211" y1="476" x2="211" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="226" y1="476" x2="226" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="241" y1="476" x2="241" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="256" y1="476" x2="256" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="271" y1="476" x2="271" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="286" y1="476" x2="286" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="301" y1="476" x2="301" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="316" y1="476" x2="316" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="331" y1="476" x2="331" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="346" y1="476" x2="346" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="361" y1="476" x2="361" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="376" y1="476" x2="376" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="391" y1="476" x2="391" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="406" y1="476" x2="406" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="421" y1="476" x2="421" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="437" y1="476" x2="437" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="452" y1="476" x2="452" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="467" y1="476" x2="467" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="482" y1="476" x2="482" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="497" y1="476" x2="497" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="512" y1="476" x2="512" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="527" y1="476" x2="527" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="542" y1="476" x2="542" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="557" y1="476" x2="557" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="572" y1="476" x2="572" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="587" y1="476" x2="587" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="602" y1="476" x2="602" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="617" y1="476" x2="617" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="632" y1="476" x2="632" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="647" y1="476" x2="647" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="662" y1="476" x2="662" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="677" y1="476" x2="677" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="692" y1="476" x2="692" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="707" y1="476" x2="707" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="722" y1="476" x2="722" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="737" y1="476" x2="737" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="752" y1="476" x2="752" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="767" y1="476" x2="767" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="782" y1="476" x2="782" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="797" y1="476" x2="797" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="812" y1="476" x2="812" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="827" y1="476" x2="827" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="842" y1="476" x2="842" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="857" y1="476" x2="857" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="872" y1="476" x2="872" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="887" y1="476" x2="887" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="902" y1="476" x2="902" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="917" y1="476" x2="917" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="932" y1="476" x2="932" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="947" y1="476" x2="947" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="962" y1="476" x2="962" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="977" y1="476" x2="977" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="992" y1="476" x2="992" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="1007" y1="476" x2="1007" y2="32"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="85" y1="328" x2="1019" y2="328"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="85" y1="180" x2="1019" y2="180"/>
+<text x="5" y="255" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000" transform="rotate(270, 5, 255)">
+Host
+</text>
+<text x="552" y="517" dy="-0.5ex" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+Ping, ms
+</text>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="196" y1="476" x2="196" y2="32"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="346" y1="476" x2="346" y2="32"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="497" y1="476" x2="497" y2="32"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="647" y1="476" x2="647" y2="32"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="797" y1="476" x2="797" y2="32"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="947" y1="476" x2="947" y2="32"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="85" y1="328" x2="1019" y2="328"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="85" y1="180" x2="1019" y2="180"/>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="84,33 84,477 "/>
+<text x="75" y="328" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+ 8.8.8.8
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="79,328 84,328 "/>
+<text x="75" y="180" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+ 1.1.1.1
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="79,180 84,180 "/>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="85,477 1019,477 "/>
+<text x="196" y="487" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+28.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="196,477 196,482 "/>
+<text x="346" y="487" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+30.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="346,477 346,482 "/>
+<text x="497" y="487" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+32.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="497,477 497,482 "/>
+<text x="647" y="487" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+34.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="647,477 647,482 "/>
+<text x="797" y="487" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+36.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="797,477 797,482 "/>
+<text x="947" y="487" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+38.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="947,477 947,482 "/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="181" y1="187" x2="181" y2="197"/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="181" y1="192" x2="226" y2="192"/>
+<rect x="226" y="182" width="30" height="20" opacity="1" fill="none" stroke="#3CB44B"/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="238" y1="182" x2="238" y2="202"/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="256" y1="192" x2="301" y2="192"/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="301" y1="187" x2="301" y2="197"/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="160" y1="335" x2="160" y2="345"/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="160" y1="340" x2="241" y2="340"/>
+<rect x="241" y="330" width="55" height="20" opacity="1" fill="none" stroke="#3CB44B"/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="256" y1="330" x2="256" y2="350"/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="296" y1="340" x2="377" y2="340"/>
+<line opacity="1" stroke="#3CB44B" stroke-width="1" x1="377" y1="335" x2="377" y2="345"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="283" y1="163" x2="283" y2="173"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="283" y1="168" x2="530" y2="168"/>
+<rect x="530" y="158" width="166" height="20" opacity="1" fill="none" stroke="#E6194B"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="572" y1="158" x2="572" y2="178"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="696" y1="168" x2="943" y2="168"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="943" y1="163" x2="943" y2="173"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="415" y1="311" x2="415" y2="321"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="415" y1="316" x2="564" y2="316"/>
+<rect x="564" y="306" width="100" height="20" opacity="1" fill="none" stroke="#E6194B"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="590" y1="306" x2="590" y2="326"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="664" y1="316" x2="813" y2="316"/>
+<line opacity="1" stroke="#E6194B" stroke-width="1" x1="813" y1="311" x2="813" y2="321"/>
+<rect x="930" y="38" width="84" height="44" opacity="1" fill="#FFFFFF" stroke="none"/>
+<rect x="930" y="38" width="84" height="44" opacity="0.5" fill="none" stroke="#000000"/>
+<text x="962" y="48" dy="0.76em" text-anchor="start" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+wired
+</text>
+<text x="962" y="63" dy="0.76em" text-anchor="start" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+wireless
+</text>
+<rect x="940" y="46" width="12" height="12" opacity="1" fill="#3CB44B" stroke="none"/>
+<rect x="940" y="61" width="12" height="12" opacity="1" fill="#E6194B" stroke="none"/>
+<text x="258" y="522" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="16.129032258064516" opacity="1" fill="#000000">
+Vertical Boxplot
+</text>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="200" y1="722" x2="200" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="356" y1="722" x2="356" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="720" x2="512" y2="720"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="718" x2="512" y2="718"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="716" x2="512" y2="716"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="714" x2="512" y2="714"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="711" x2="512" y2="711"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="709" x2="512" y2="709"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="707" x2="512" y2="707"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="705" x2="512" y2="705"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="703" x2="512" y2="703"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="701" x2="512" y2="701"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="699" x2="512" y2="699"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="697" x2="512" y2="697"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="695" x2="512" y2="695"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="693" x2="512" y2="693"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="690" x2="512" y2="690"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="688" x2="512" y2="688"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="686" x2="512" y2="686"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="684" x2="512" y2="684"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="682" x2="512" y2="682"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="680" x2="512" y2="680"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="678" x2="512" y2="678"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="676" x2="512" y2="676"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="674" x2="512" y2="674"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="671" x2="512" y2="671"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="669" x2="512" y2="669"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="667" x2="512" y2="667"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="665" x2="512" y2="665"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="663" x2="512" y2="663"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="661" x2="512" y2="661"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="659" x2="512" y2="659"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="657" x2="512" y2="657"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="655" x2="512" y2="655"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="652" x2="512" y2="652"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="650" x2="512" y2="650"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="648" x2="512" y2="648"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="646" x2="512" y2="646"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="644" x2="512" y2="644"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="642" x2="512" y2="642"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="640" x2="512" y2="640"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="638" x2="512" y2="638"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="636" x2="512" y2="636"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="633" x2="512" y2="633"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="631" x2="512" y2="631"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="629" x2="512" y2="629"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="627" x2="512" y2="627"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="625" x2="512" y2="625"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="623" x2="512" y2="623"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="621" x2="512" y2="621"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="619" x2="512" y2="619"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="617" x2="512" y2="617"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="614" x2="512" y2="614"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="612" x2="512" y2="612"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="610" x2="512" y2="610"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="608" x2="512" y2="608"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="606" x2="512" y2="606"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="604" x2="512" y2="604"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="602" x2="512" y2="602"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="600" x2="512" y2="600"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="598" x2="512" y2="598"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="595" x2="512" y2="595"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="593" x2="512" y2="593"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="591" x2="512" y2="591"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="589" x2="512" y2="589"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="587" x2="512" y2="587"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="585" x2="512" y2="585"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="583" x2="512" y2="583"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="581" x2="512" y2="581"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="579" x2="512" y2="579"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="577" x2="512" y2="577"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="574" x2="512" y2="574"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="572" x2="512" y2="572"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="570" x2="512" y2="570"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="568" x2="512" y2="568"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="566" x2="512" y2="566"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="564" x2="512" y2="564"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="562" x2="512" y2="562"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="560" x2="512" y2="560"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="558" x2="512" y2="558"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="555" x2="512" y2="555"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="553" x2="512" y2="553"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="551" x2="512" y2="551"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="549" x2="512" y2="549"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="547" x2="512" y2="547"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="45" y1="545" x2="512" y2="545"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="200" y1="722" x2="200" y2="544"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="356" y1="722" x2="356" y2="544"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="45" y1="705" x2="512" y2="705"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="45" y1="684" x2="512" y2="684"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="45" y1="663" x2="512" y2="663"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="45" y1="642" x2="512" y2="642"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="45" y1="621" x2="512" y2="621"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="45" y1="600" x2="512" y2="600"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="45" y1="579" x2="512" y2="579"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="45" y1="558" x2="512" y2="558"/>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="44,545 44,723 "/>
+<text x="35" y="705" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+0.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="39,705 44,705 "/>
+<text x="35" y="684" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+10.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="39,684 44,684 "/>
+<text x="35" y="663" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+20.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="39,663 44,663 "/>
+<text x="35" y="642" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+30.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="39,642 44,642 "/>
+<text x="35" y="621" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+40.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="39,621 44,621 "/>
+<text x="35" y="600" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+50.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="39,600 44,600 "/>
+<text x="35" y="579" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+60.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="39,579 44,579 "/>
+<text x="35" y="558" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+70.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="39,558 44,558 "/>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="45,723 512,723 "/>
+<text x="200" y="733" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+a
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="200,723 200,728 "/>
+<text x="356" y="733" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+b
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="356,723 356,728 "/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="195" y1="700" x2="205" y2="700"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="200" y1="700" x2="200" y2="649"/>
+<rect x="195" y="616" width="10" height="33" opacity="1" fill="none" stroke="#000000"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="195" y1="621" x2="205" y2="621"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="200" y1="616" x2="200" y2="565"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="195" y1="565" x2="205" y2="565"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="351" y1="642" x2="361" y2="642"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="356" y1="642" x2="356" y2="620"/>
+<rect x="351" y="606" width="10" height="14" opacity="1" fill="none" stroke="#000000"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="351" y1="615" x2="361" y2="615"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="356" y1="606" x2="356" y2="585"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="351" y1="585" x2="361" y2="585"/>
+<text x="765" y="522" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="16.129032258064516" opacity="1" fill="#000000">
+Horizontal Boxplot
+</text>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="559" y1="722" x2="559" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="567" y1="722" x2="567" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="575" y1="722" x2="575" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="583" y1="722" x2="583" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="590" y1="722" x2="590" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="598" y1="722" x2="598" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="606" y1="722" x2="606" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="614" y1="722" x2="614" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="622" y1="722" x2="622" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="629" y1="722" x2="629" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="637" y1="722" x2="637" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="645" y1="722" x2="645" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="653" y1="722" x2="653" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="660" y1="722" x2="660" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="668" y1="722" x2="668" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="676" y1="722" x2="676" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="684" y1="722" x2="684" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="692" y1="722" x2="692" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="699" y1="722" x2="699" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="707" y1="722" x2="707" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="715" y1="722" x2="715" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="723" y1="722" x2="723" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="731" y1="722" x2="731" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="738" y1="722" x2="738" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="746" y1="722" x2="746" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="754" y1="722" x2="754" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="762" y1="722" x2="762" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="769" y1="722" x2="769" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="777" y1="722" x2="777" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="785" y1="722" x2="785" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="793" y1="722" x2="793" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="801" y1="722" x2="801" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="808" y1="722" x2="808" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="816" y1="722" x2="816" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="824" y1="722" x2="824" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="832" y1="722" x2="832" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="839" y1="722" x2="839" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="847" y1="722" x2="847" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="855" y1="722" x2="855" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="863" y1="722" x2="863" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="871" y1="722" x2="871" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="878" y1="722" x2="878" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="886" y1="722" x2="886" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="894" y1="722" x2="894" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="902" y1="722" x2="902" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="910" y1="722" x2="910" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="917" y1="722" x2="917" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="925" y1="722" x2="925" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="933" y1="722" x2="933" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="941" y1="722" x2="941" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="948" y1="722" x2="948" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="956" y1="722" x2="956" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="964" y1="722" x2="964" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="972" y1="722" x2="972" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="980" y1="722" x2="980" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="987" y1="722" x2="987" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="995" y1="722" x2="995" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="1003" y1="722" x2="1003" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="1011" y1="722" x2="1011" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="1019" y1="722" x2="1019" y2="544"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="552" y1="722" x2="1019" y2="722"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="552" y1="662" x2="1019" y2="662"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="552" y1="603" x2="1019" y2="603"/>
+<line opacity="1" stroke="#FFFFFF" stroke-width="1" x1="552" y1="544" x2="1019" y2="544"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="590" y1="722" x2="590" y2="544"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="668" y1="722" x2="668" y2="544"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="746" y1="722" x2="746" y2="544"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="824" y1="722" x2="824" y2="544"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="902" y1="722" x2="902" y2="544"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="980" y1="722" x2="980" y2="544"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="552" y1="722" x2="1019" y2="722"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="552" y1="662" x2="1019" y2="662"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="552" y1="603" x2="1019" y2="603"/>
+<line opacity="0.2" stroke="#000000" stroke-width="1" x1="552" y1="544" x2="1019" y2="544"/>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="551,545 551,723 "/>
+<text x="542" y="722" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="546,722 551,722 "/>
+<text x="542" y="662" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+1
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="546,662 551,662 "/>
+<text x="542" y="603" dy="0.5ex" text-anchor="end" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+2
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="546,603 551,603 "/>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="552,723 1019,723 "/>
+<text x="590" y="733" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+-20.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="590,723 590,728 "/>
+<text x="668" y="733" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+0.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="668,723 668,728 "/>
+<text x="746" y="733" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+20.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="746,723 746,728 "/>
+<text x="824" y="733" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+40.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="824,723 824,728 "/>
+<text x="902" y="733" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+60.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="902,723 902,728 "/>
+<text x="980" y="733" dy="0.76em" text-anchor="middle" font-family="sans-serif" font-size="9.67741935483871" opacity="1" fill="#000000">
+80.0
+</text>
+<polyline fill="none" opacity="1" stroke="#000000" stroke-width="1" points="980,723 980,728 "/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="677" y1="657" x2="677" y2="667"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="677" y1="662" x2="771" y2="662"/>
+<rect x="771" y="657" width="63" height="10" opacity="1" fill="none" stroke="#000000"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="824" y1="657" x2="824" y2="667"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="834" y1="662" x2="928" y2="662"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="928" y1="657" x2="928" y2="667"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="785" y1="598" x2="785" y2="608"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="785" y1="603" x2="785" y2="603"/>
+<rect x="785" y="598" width="0" height="10" opacity="1" fill="none" stroke="#000000"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="785" y1="598" x2="785" y2="608"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="785" y1="603" x2="785" y2="603"/>
+<line opacity="1" stroke="#000000" stroke-width="1" x1="785" y1="598" x2="785" y2="608"/>
+</svg>
diff --git a/plotters-doc-data/console-example.png b/plotters-doc-data/console-example.png
new file mode 100644
index 0000000..0a354dc
--- /dev/null
+++ b/plotters-doc-data/console-example.png
Binary files differ
diff --git a/plotters-doc-data/element-0.png b/plotters-doc-data/element-0.png
new file mode 100644
index 0000000..cdfba76
--- /dev/null
+++ b/plotters-doc-data/element-0.png
Binary files differ
diff --git a/plotters-doc-data/element-1.png b/plotters-doc-data/element-1.png
new file mode 100644
index 0000000..cdfba76
--- /dev/null
+++ b/plotters-doc-data/element-1.png
Binary files differ
diff --git a/plotters-doc-data/element-3.png b/plotters-doc-data/element-3.png
new file mode 100644
index 0000000..9890019
--- /dev/null
+++ b/plotters-doc-data/element-3.png
Binary files differ
diff --git a/plotters-doc-data/errorbar.png b/plotters-doc-data/errorbar.png
new file mode 100644
index 0000000..ed5482b
--- /dev/null
+++ b/plotters-doc-data/errorbar.png
Binary files differ
diff --git a/plotters-doc-data/histogram.png b/plotters-doc-data/histogram.png
new file mode 100644
index 0000000..4ffddb8
--- /dev/null
+++ b/plotters-doc-data/histogram.png
Binary files differ
diff --git a/plotters-doc-data/mandelbrot.png b/plotters-doc-data/mandelbrot.png
new file mode 100644
index 0000000..c5e8c25
--- /dev/null
+++ b/plotters-doc-data/mandelbrot.png
Binary files differ
diff --git a/plotters-doc-data/matshow.png b/plotters-doc-data/matshow.png
new file mode 100644
index 0000000..913b9aa
--- /dev/null
+++ b/plotters-doc-data/matshow.png
Binary files differ
diff --git a/plotters-doc-data/normal-dist.png b/plotters-doc-data/normal-dist.png
new file mode 100644
index 0000000..249f63a
--- /dev/null
+++ b/plotters-doc-data/normal-dist.png
Binary files differ
diff --git a/plotters-doc-data/normal-dist2.png b/plotters-doc-data/normal-dist2.png
new file mode 100644
index 0000000..a1ec8d7
--- /dev/null
+++ b/plotters-doc-data/normal-dist2.png
Binary files differ
diff --git a/plotters-doc-data/relative_size.png b/plotters-doc-data/relative_size.png
new file mode 100644
index 0000000..7dacc63
--- /dev/null
+++ b/plotters-doc-data/relative_size.png
Binary files differ
diff --git a/plotters-doc-data/sample.png b/plotters-doc-data/sample.png
new file mode 100644
index 0000000..20fee59
--- /dev/null
+++ b/plotters-doc-data/sample.png
Binary files differ
diff --git a/plotters-doc-data/sierpinski.png b/plotters-doc-data/sierpinski.png
new file mode 100644
index 0000000..901bad9
--- /dev/null
+++ b/plotters-doc-data/sierpinski.png
Binary files differ
diff --git a/plotters-doc-data/slc-temp.png b/plotters-doc-data/slc-temp.png
new file mode 100644
index 0000000..e6a8302
--- /dev/null
+++ b/plotters-doc-data/slc-temp.png
Binary files differ
diff --git a/plotters-doc-data/snowflake.png b/plotters-doc-data/snowflake.png
new file mode 100644
index 0000000..ae47fc5
--- /dev/null
+++ b/plotters-doc-data/snowflake.png
Binary files differ
diff --git a/plotters-doc-data/stock.png b/plotters-doc-data/stock.png
new file mode 100644
index 0000000..4658fff
--- /dev/null
+++ b/plotters-doc-data/stock.png
Binary files differ
diff --git a/plotters-doc-data/twoscale.png b/plotters-doc-data/twoscale.png
new file mode 100644
index 0000000..532a3af
--- /dev/null
+++ b/plotters-doc-data/twoscale.png
Binary files differ
diff --git a/publish.sh b/publish.sh
new file mode 100755
index 0000000..01c7999
--- /dev/null
+++ b/publish.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+set -e
+
+if [ $(git diff | wc -l) != 0 ]
+then
+ echo "Commit your change before publish!"
+ exit 1
+fi
+
+if [ -z "$1" ]
+then
+ PART_ID=0
+else
+ PART_ID=$1
+fi
+ROOT=$(dirname $(readlink -f $0))
+OLD_VERSION=$(cat ${ROOT}/doc-template/latest_version)
+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)
+echo "Publishing new version ${OLD_VERSION} -> ${NEW_VERSION}"
+echo ${NEW_VERSION} > doc-template/latest_version
+doc-template/update_readme.sh
+echo ${OLD_VERSION} > doc-template/latest_version
+cargo fmt
+
+PATTERN=$(echo ^version = \"${OLD_VERSION}\"\$ | sed 's/\./\\./g')
+DATE=$(date "+%Y-%m-%d")
+sed -i "s/${PATTERN}/version = \"${NEW_VERSION}\"/g" Cargo.toml
+PATTERN=$(echo ${NEW_VERSION} | sed 's/\./\\./g')
+sed -i "s/^## Plotters .* (?) *\$/## Plotters ${NEW_VERSION} ($DATE)/g" CHANGELOG.md
+
+echo ${NEW_VERSION} > doc-template/latest_version
+
+git add -u .
+git commit -m "Bump version number from ${OLD_VERSION} to ${NEW_VERSION}"
+git tag -a "v${NEW_VERSION}" -m "Plotters ${NEW_VERSION} release"
+
+# Verify MSRV
+MSRV=$(cat ${ROOT}/doc-template/msrv.txt)
+rustup install ${MSRV}
+cargo +${MSRV} build
+
+cargo publish
+git push origin
+git push origin "v${NEW_VERSION}"
diff --git a/src/chart/builder.rs b/src/chart/builder.rs
new file mode 100644
index 0000000..ee8ef12
--- /dev/null
+++ b/src/chart/builder.rs
@@ -0,0 +1,345 @@
+use super::context::ChartContext;
+
+use crate::coord::{AsRangedCoord, RangedCoord, Shift};
+use crate::drawing::backend::DrawingBackend;
+use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
+use crate::style::{IntoTextStyle, SizeDesc, TextStyle};
+
+/// The enum used to specify the position of label area.
+/// This is used when we configure the label area size with the API `set_label_area_size`
+#[derive(Copy, Clone)]
+pub enum LabelAreaPosition {
+ Top = 0,
+ Bottom = 1,
+ Left = 2,
+ Right = 3,
+}
+
+/// The helper object to create a chart context, which is used for the high-level figure drawing.
+/// With the help of this object, we can convert a basic drawing area into a chart context, which
+/// allows the high-level charting API being used on the drawing area.
+pub struct ChartBuilder<'a, 'b, DB: DrawingBackend> {
+ label_area_size: [u32; 4], // [upper, lower, left, right]
+ overlap_plotting_area: [bool; 4],
+ root_area: &'a DrawingArea<DB, Shift>,
+ title: Option<(String, TextStyle<'b>)>,
+ margin: [u32; 4],
+}
+
+impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> {
+ /// Create a chart builder on the given drawing area
+ /// - `root`: The root drawing area
+ /// - Returns: The chart builder object
+ pub fn on(root: &'a DrawingArea<DB, Shift>) -> Self {
+ Self {
+ label_area_size: [0; 4],
+ root_area: root,
+ title: None,
+ margin: [0; 4],
+ overlap_plotting_area: [false; 4],
+ }
+ }
+
+ /// Set the margin size of the chart (applied for top, bottom, left and right at the same time)
+ /// - `size`: The size of the chart margin.
+ pub fn margin<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ let size = size.in_pixels(self.root_area).max(0) as u32;
+ self.margin = [size, size, size, size];
+ self
+ }
+
+ /// Set the top margin of current chart
+ /// - `size`: The size of the top margin.
+ pub fn margin_top<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ let size = size.in_pixels(self.root_area).max(0) as u32;
+ self.margin[0] = size;
+ self
+ }
+
+ /// Set the bottom margin of current chart
+ /// - `size`: The size of the bottom margin.
+ pub fn margin_bottom<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ let size = size.in_pixels(self.root_area).max(0) as u32;
+ self.margin[1] = size;
+ self
+ }
+
+ /// Set the left margin of current chart
+ /// - `size`: The size of the left margin.
+ pub fn margin_left<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ let size = size.in_pixels(self.root_area).max(0) as u32;
+ self.margin[2] = size;
+ self
+ }
+
+ /// Set the right margin of current chart
+ /// - `size`: The size of the right margin.
+ pub fn margin_right<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ let size = size.in_pixels(self.root_area).max(0) as u32;
+ self.margin[3] = size;
+ self
+ }
+
+ /// Set all the label area size with the same value
+ pub fn set_all_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ let size = size.in_pixels(self.root_area);
+ self.set_label_area_size(LabelAreaPosition::Top, size)
+ .set_label_area_size(LabelAreaPosition::Bottom, size)
+ .set_label_area_size(LabelAreaPosition::Left, size)
+ .set_label_area_size(LabelAreaPosition::Right, size)
+ }
+
+ /// Set the most commonly used label area size to the same value
+ pub fn set_left_and_bottom_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ let size = size.in_pixels(self.root_area);
+ self.set_label_area_size(LabelAreaPosition::Left, size)
+ .set_label_area_size(LabelAreaPosition::Bottom, size)
+ }
+
+ /// Set the size of X label area
+ /// - `size`: The height of the x label area, if x is 0, the chart doesn't have the X label area
+ pub fn x_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ self.set_label_area_size(LabelAreaPosition::Bottom, size)
+ }
+
+ /// Set the size of the Y label area
+ /// - `size`: The width of the Y label area. If size is 0, the chart doesn't have Y label area
+ pub fn y_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ self.set_label_area_size(LabelAreaPosition::Left, size)
+ }
+
+ /// Set the size of X label area on the top of the chart
+ /// - `size`: The height of the x label area, if x is 0, the chart doesn't have the X label area
+ pub fn top_x_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ self.set_label_area_size(LabelAreaPosition::Top, size)
+ }
+
+ /// Set the size of the Y label area on the right side
+ /// - `size`: The width of the Y label area. If size is 0, the chart doesn't have Y label area
+ pub fn right_y_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ self.set_label_area_size(LabelAreaPosition::Right, size)
+ }
+
+ /// Set a label area size
+ /// - `pos`: THe position where the label area located
+ /// - `size`: The size of the label area size
+ pub fn set_label_area_size<S: SizeDesc>(
+ &mut self,
+ pos: LabelAreaPosition,
+ size: S,
+ ) -> &mut Self {
+ let size = size.in_pixels(self.root_area);
+ self.label_area_size[pos as usize] = size.abs() as u32;
+ self.overlap_plotting_area[pos as usize] = size < 0;
+ self
+ }
+
+ /// Set the caption of the chart
+ /// - `caption`: The caption of the chart
+ /// - `style`: The text style
+ /// - Note: If the caption is set, the margin option will be ignored
+ pub fn caption<S: AsRef<str>, Style: IntoTextStyle<'b>>(
+ &mut self,
+ caption: S,
+ style: Style,
+ ) -> &mut Self {
+ self.title = Some((
+ caption.as_ref().to_string(),
+ style.into_text_style(self.root_area),
+ ));
+ self
+ }
+
+ /// Build the chart with a 2D Cartesian coordinate system. The function will returns a chart
+ /// context, where data series can be rendered on.
+ /// - `x_spec`: The specification of X axis
+ /// - `y_spec`: The specification of Y axis
+ /// - Returns: A chart context
+ #[allow(clippy::type_complexity)]
+ pub fn build_ranged<X: AsRangedCoord, Y: AsRangedCoord>(
+ &mut self,
+ x_spec: X,
+ y_spec: Y,
+ ) -> Result<
+ ChartContext<'a, DB, RangedCoord<X::CoordDescType, Y::CoordDescType>>,
+ DrawingAreaErrorKind<DB::ErrorType>,
+ > {
+ let mut label_areas = [None, None, None, None];
+
+ let mut drawing_area = DrawingArea::clone(self.root_area);
+
+ if *self.margin.iter().max().unwrap_or(&0) > 0 {
+ drawing_area = drawing_area.margin(
+ self.margin[0] as i32,
+ self.margin[1] as i32,
+ self.margin[2] as i32,
+ self.margin[3] as i32,
+ );
+ }
+
+ let (title_dx, title_dy) = if let Some((ref title, ref style)) = self.title {
+ let (origin_dx, origin_dy) = drawing_area.get_base_pixel();
+ drawing_area = drawing_area.titled(title, style.clone())?;
+ let (current_dx, current_dy) = drawing_area.get_base_pixel();
+ (current_dx - origin_dx, current_dy - origin_dy)
+ } else {
+ (0, 0)
+ };
+
+ let (w, h) = drawing_area.dim_in_pixel();
+
+ let mut actual_drawing_area_pos = [0, h as i32, 0, w as i32];
+
+ const DIR: [(i16, i16); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)];
+
+ for (idx, (dx, dy)) in (0..4).map(|idx| (idx, DIR[idx])) {
+ if self.overlap_plotting_area[idx] {
+ continue;
+ }
+
+ let size = self.label_area_size[idx] as i32;
+
+ let split_point = if dx + dy < 0 { size } else { -size };
+
+ actual_drawing_area_pos[idx] += split_point;
+ }
+
+ let mut split: Vec<_> = drawing_area
+ .split_by_breakpoints(
+ &actual_drawing_area_pos[2..4],
+ &actual_drawing_area_pos[0..2],
+ )
+ .into_iter()
+ .map(Some)
+ .collect();
+
+ std::mem::swap(&mut drawing_area, split[4].as_mut().unwrap());
+
+ for (src_idx, dst_idx) in [1, 7, 3, 5].iter().zip(0..4) {
+ if !self.overlap_plotting_area[dst_idx] {
+ let (h, w) = split[*src_idx].as_ref().unwrap().dim_in_pixel();
+ if h > 0 && w > 0 {
+ std::mem::swap(&mut label_areas[dst_idx], &mut split[*src_idx]);
+ }
+ } else if self.label_area_size[dst_idx] != 0 {
+ let size = self.label_area_size[dst_idx] as i32;
+ let (dw, dh) = drawing_area.dim_in_pixel();
+ let x0 = if DIR[dst_idx].0 > 0 {
+ dw as i32 - size
+ } else {
+ 0
+ };
+ let y0 = if DIR[dst_idx].1 > 0 {
+ dh as i32 - size
+ } else {
+ 0
+ };
+ let x1 = if DIR[dst_idx].0 >= 0 { dw as i32 } else { size };
+ let y1 = if DIR[dst_idx].1 >= 0 { dh as i32 } else { size };
+
+ label_areas[dst_idx] = Some(
+ drawing_area
+ .clone()
+ .shrink((x0, y0), ((x1 - x0), (y1 - y0))),
+ );
+ }
+ }
+
+ let mut pixel_range = drawing_area.get_pixel_range();
+ pixel_range.1 = (pixel_range.1.end - 1)..(pixel_range.1.start - 1);
+
+ let mut x_label_area = [None, None];
+ let mut y_label_area = [None, None];
+
+ std::mem::swap(&mut x_label_area[0], &mut label_areas[0]);
+ std::mem::swap(&mut x_label_area[1], &mut label_areas[1]);
+ std::mem::swap(&mut y_label_area[0], &mut label_areas[2]);
+ std::mem::swap(&mut y_label_area[1], &mut label_areas[3]);
+
+ Ok(ChartContext {
+ x_label_area,
+ y_label_area,
+ drawing_area: drawing_area.apply_coord_spec(RangedCoord::new(
+ x_spec,
+ y_spec,
+ pixel_range,
+ )),
+ series_anno: vec![],
+ drawing_area_pos: (
+ actual_drawing_area_pos[2] + title_dx + self.margin[2] as i32,
+ actual_drawing_area_pos[0] + title_dy + self.margin[0] as i32,
+ ),
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::prelude::*;
+ #[test]
+ fn test_label_area_size() {
+ let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
+ let mut chart = ChartBuilder::on(&drawing_area);
+
+ chart
+ .x_label_area_size(10)
+ .y_label_area_size(20)
+ .top_x_label_area_size(30)
+ .right_y_label_area_size(40);
+ assert_eq!(chart.label_area_size[1], 10);
+ assert_eq!(chart.label_area_size[2], 20);
+ assert_eq!(chart.label_area_size[0], 30);
+ assert_eq!(chart.label_area_size[3], 40);
+
+ chart.set_label_area_size(LabelAreaPosition::Left, 100);
+ chart.set_label_area_size(LabelAreaPosition::Right, 200);
+ chart.set_label_area_size(LabelAreaPosition::Top, 300);
+ chart.set_label_area_size(LabelAreaPosition::Bottom, 400);
+
+ assert_eq!(chart.label_area_size[0], 300);
+ assert_eq!(chart.label_area_size[1], 400);
+ assert_eq!(chart.label_area_size[2], 100);
+ assert_eq!(chart.label_area_size[3], 200);
+ }
+
+ #[test]
+ fn test_margin_configure() {
+ let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
+ let mut chart = ChartBuilder::on(&drawing_area);
+
+ chart.margin(5);
+ assert_eq!(chart.margin[0], 5);
+ assert_eq!(chart.margin[1], 5);
+ assert_eq!(chart.margin[2], 5);
+ assert_eq!(chart.margin[3], 5);
+
+ chart.margin_top(10);
+ chart.margin_bottom(11);
+ chart.margin_left(12);
+ chart.margin_right(13);
+ assert_eq!(chart.margin[0], 10);
+ assert_eq!(chart.margin[1], 11);
+ assert_eq!(chart.margin[2], 12);
+ assert_eq!(chart.margin[3], 13);
+ }
+
+ #[test]
+ fn test_caption() {
+ let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
+ let mut chart = ChartBuilder::on(&drawing_area);
+
+ chart.caption("This is a test case", ("serif", 10));
+
+ assert_eq!(chart.title.as_ref().unwrap().0, "This is a test case");
+ assert_eq!(chart.title.as_ref().unwrap().1.font.get_name(), "serif");
+ assert_eq!(chart.title.as_ref().unwrap().1.font.get_size(), 10.0);
+ assert_eq!(
+ chart.title.as_ref().unwrap().1.color.to_rgba(),
+ BLACK.to_rgba()
+ );
+
+ chart.caption("This is a test case", ("serif", 10));
+ assert_eq!(chart.title.as_ref().unwrap().1.font.get_name(), "serif");
+ }
+}
diff --git a/src/chart/context.rs b/src/chart/context.rs
new file mode 100644
index 0000000..6f7d09e
--- /dev/null
+++ b/src/chart/context.rs
@@ -0,0 +1,792 @@
+use std::borrow::Borrow;
+use std::fmt::Debug;
+use std::marker::PhantomData;
+use std::ops::Range;
+use std::sync::Arc;
+
+use super::dual_coord::DualCoordChartContext;
+use super::mesh::MeshStyle;
+use super::series::SeriesLabelStyle;
+
+use crate::coord::{
+ AsRangedCoord, CoordTranslate, MeshLine, Ranged, RangedCoord, ReverseCoordTranslate, Shift,
+};
+use crate::drawing::backend::{BackendCoord, DrawingBackend};
+use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
+use crate::element::{Drawable, DynElement, IntoDynElement, PathElement, PointCollection};
+use crate::style::text_anchor::{HPos, Pos, VPos};
+use crate::style::{AsRelative, FontTransform, ShapeStyle, SizeDesc, TextStyle};
+
+/// The annotations (such as the label of the series, the legend element, etc)
+/// When a series is drawn onto a drawing area, an series annotation object
+/// is created and a mutable reference is returned.
+#[allow(clippy::type_complexity)]
+pub struct SeriesAnno<'a, DB: DrawingBackend> {
+ label: Option<String>,
+ draw_func: Option<Box<dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord> + 'a>>,
+ phantom_data: PhantomData<DB>,
+}
+
+impl<'a, DB: DrawingBackend> SeriesAnno<'a, DB> {
+ pub(crate) fn get_label(&self) -> &str {
+ self.label.as_ref().map(|x| x.as_str()).unwrap_or("")
+ }
+
+ pub(crate) fn get_draw_func(
+ &self,
+ ) -> Option<&dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord>> {
+ self.draw_func.as_ref().map(|x| x.borrow())
+ }
+
+ fn new() -> Self {
+ Self {
+ label: None,
+ draw_func: None,
+ phantom_data: PhantomData,
+ }
+ }
+
+ /// Set the series label
+ /// - `label`: The string would be use as label for current series
+ pub fn label<L: Into<String>>(&mut self, label: L) -> &mut Self {
+ self.label = Some(label.into());
+ self
+ }
+
+ /// Set the legend element creator function
+ /// - `func`: The function use to create the element
+ /// *Note*: The creation function uses a shifted pixel-based coordinate system. And place the
+ /// point (0,0) to the mid-right point of the shape
+ pub fn legend<E: IntoDynElement<'a, DB, BackendCoord>, T: Fn(BackendCoord) -> E + 'a>(
+ &mut self,
+ func: T,
+ ) -> &mut Self {
+ self.draw_func = Some(Box::new(move |p| func(p).into_dyn()));
+ self
+ }
+}
+
+/// The context of the chart. This is the core object of Plotters.
+/// Any plot/chart is abstracted as this type, and any data series can be placed to the chart
+/// context.
+pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
+ pub(super) x_label_area: [Option<DrawingArea<DB, Shift>>; 2],
+ pub(super) y_label_area: [Option<DrawingArea<DB, Shift>>; 2],
+ pub(super) drawing_area: DrawingArea<DB, CT>,
+ pub(super) series_anno: Vec<SeriesAnno<'a, DB>>,
+ pub(super) drawing_area_pos: (i32, i32),
+}
+
+/// A chart context state - This is the data that is needed to reconstruct the chart context
+/// without actually drawing the chart. This is useful when we want to do realtime rendering and
+/// want to incrementally update the chart.
+///
+/// For each frame, instead of updating the entire backend, we are able to keep the keep the figure
+/// component like axis, labels untouched and make updates only in the plotting drawing area.
+pub struct ChartState<CT: CoordTranslate> {
+ drawing_area_pos: (i32, i32),
+ drawing_area_size: (u32, u32),
+ coord: CT,
+}
+
+impl<'a, CT: CoordTranslate + Clone> Clone for ChartState<CT> {
+ fn clone(&self) -> Self {
+ Self {
+ drawing_area_size: self.drawing_area_size,
+ drawing_area_pos: self.drawing_area_pos,
+ coord: self.coord.clone(),
+ }
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT: CoordTranslate> From<ChartContext<'a, DB, CT>> for ChartState<CT> {
+ fn from(chart: ChartContext<'a, DB, CT>) -> ChartState<CT> {
+ ChartState {
+ drawing_area_pos: chart.drawing_area_pos,
+ drawing_area_size: chart.drawing_area.dim_in_pixel(),
+ coord: chart.drawing_area.into_coord_spec(),
+ }
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
+ /// Convert a chart context into a chart state, by doing so, the chart context is consumed and
+ /// a saved chart state is created for later use.
+ pub fn into_chart_state(self) -> ChartState<CT> {
+ self.into()
+ }
+
+ /// Convert the chart context into a sharable chart state.
+ /// Normally a chart state can not be clone, since the coordinate spec may not be able to be
+ /// cloned. In this case, we can use an `Arc` get the coordinate wrapped thus the state can be
+ /// cloned and shared by multiple chart context
+ pub fn into_shared_chart_state(self) -> ChartState<Arc<CT>> {
+ ChartState {
+ drawing_area_pos: self.drawing_area_pos,
+ drawing_area_size: self.drawing_area.dim_in_pixel(),
+ coord: Arc::new(self.drawing_area.into_coord_spec()),
+ }
+ }
+}
+
+impl<'a, 'b, DB, CT> From<&ChartContext<'a, DB, CT>> for ChartState<CT>
+where
+ DB: DrawingBackend,
+ CT: CoordTranslate + Clone,
+{
+ fn from(chart: &ChartContext<'a, DB, CT>) -> ChartState<CT> {
+ ChartState {
+ drawing_area_pos: chart.drawing_area_pos,
+ drawing_area_size: chart.drawing_area.dim_in_pixel(),
+ coord: chart.drawing_area.as_coord_spec().clone(),
+ }
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT: CoordTranslate + Clone> ChartContext<'a, DB, CT> {
+ /// Make the chart context, do not consume the chart context and clone the coordinate spec
+ pub fn to_chart_state(&self) -> ChartState<CT> {
+ self.into()
+ }
+}
+
+impl<CT: CoordTranslate> ChartState<CT> {
+ /// Restore the chart context on the given drawing area
+ ///
+ /// - `area`: The given drawing area where we want to restore the chart context
+ /// - **returns** The newly created chart context
+ pub fn restore<'a, DB: DrawingBackend>(
+ self,
+ area: &DrawingArea<DB, Shift>,
+ ) -> ChartContext<'a, DB, CT> {
+ let area = area
+ .clone()
+ .shrink(self.drawing_area_pos, self.drawing_area_size);
+ ChartContext {
+ x_label_area: [None, None],
+ y_label_area: [None, None],
+ drawing_area: area.apply_coord_spec(self.coord),
+ series_anno: vec![],
+ drawing_area_pos: self.drawing_area_pos,
+ }
+ }
+}
+
+impl<
+ 'a,
+ DB: DrawingBackend,
+ XT: Debug,
+ YT: Debug,
+ X: Ranged<ValueType = XT>,
+ Y: Ranged<ValueType = YT>,
+ > ChartContext<'a, DB, RangedCoord<X, Y>>
+{
+ fn is_overlapping_drawing_area(&self, area: Option<&DrawingArea<DB, Shift>>) -> bool {
+ if let Some(area) = area {
+ let (x0, y0) = area.get_base_pixel();
+ let (w, h) = area.dim_in_pixel();
+ let (x1, y1) = (x0 + w as i32, y0 + h as i32);
+ let (dx0, dy0) = self.drawing_area.get_base_pixel();
+ let (w, h) = self.drawing_area.dim_in_pixel();
+ let (dx1, dy1) = (dx0 + w as i32, dy0 + h as i32);
+
+ let (ox0, ox1) = (x0.max(dx0), x1.min(dx1));
+ let (oy0, oy1) = (y0.max(dy0), y1.min(dy1));
+
+ ox1 > ox0 && oy1 > oy0
+ } else {
+ false
+ }
+ }
+
+ /// Initialize a mesh configuration object and mesh drawing can be finalized by calling
+ /// the function `MeshStyle::draw`
+ pub fn configure_mesh<'b>(&'b mut self) -> MeshStyle<'a, 'b, X, Y, DB> {
+ let base_tick_size = (5u32).percent().max(5).in_pixels(&self.drawing_area);
+
+ let mut x_tick_size = [base_tick_size, base_tick_size];
+ let mut y_tick_size = [base_tick_size, base_tick_size];
+
+ for idx in 0..2 {
+ if self.is_overlapping_drawing_area(self.x_label_area[idx].as_ref()) {
+ x_tick_size[idx] = -x_tick_size[idx];
+ }
+ if self.is_overlapping_drawing_area(self.y_label_area[idx].as_ref()) {
+ y_tick_size[idx] = -y_tick_size[idx];
+ }
+ }
+
+ MeshStyle {
+ parent_size: self.drawing_area.dim_in_pixel(),
+ axis_style: None,
+ x_label_offset: 0,
+ y_label_offset: 0,
+ draw_x_mesh: true,
+ draw_y_mesh: true,
+ draw_x_axis: true,
+ draw_y_axis: true,
+ n_x_labels: 10,
+ n_y_labels: 10,
+ line_style_1: None,
+ line_style_2: None,
+ x_label_style: None,
+ y_label_style: None,
+ format_x: &|x| format!("{:?}", x),
+ format_y: &|y| format!("{:?}", y),
+ target: Some(self),
+ _phantom_data: PhantomData,
+ x_desc: None,
+ y_desc: None,
+ axis_desc_style: None,
+ x_tick_size,
+ y_tick_size,
+ }
+ }
+}
+
+impl<'a, DB: DrawingBackend + 'a, CT: CoordTranslate> ChartContext<'a, DB, CT> {
+ /// Configure the styles for drawing series labels in the chart
+ pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT> {
+ SeriesLabelStyle::new(self)
+ }
+
+ /// Get a reference of underlying plotting area
+ pub fn plotting_area(&self) -> &DrawingArea<DB, CT> {
+ &self.drawing_area
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
+ pub fn as_coord_spec(&self) -> &CT {
+ self.drawing_area.as_coord_spec()
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
+ /// Convert the chart context into an closure that can be used for coordinate translation
+ pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
+ let coord_spec = self.drawing_area.into_coord_spec();
+ move |coord| coord_spec.reverse_translate(coord)
+ }
+}
+
+impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Arc<RangedCoord<X, Y>>> {
+ pub(super) fn draw_series_impl<E, R, S>(
+ &mut self,
+ series: S,
+ ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
+ where
+ for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
+ E: Drawable<DB>,
+ R: Borrow<E>,
+ S: IntoIterator<Item = R>,
+ {
+ for element in series {
+ self.drawing_area.draw(element.borrow())?;
+ }
+ Ok(())
+ }
+
+ pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
+ let idx = self.series_anno.len();
+ self.series_anno.push(SeriesAnno::new());
+ &mut self.series_anno[idx]
+ }
+
+ /// Draw a data series. A data series in Plotters is abstracted as an iterator of elements
+ pub fn draw_series<E, R, S>(
+ &mut self,
+ series: S,
+ ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
+ where
+ for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
+ E: Drawable<DB>,
+ R: Borrow<E>,
+ S: IntoIterator<Item = R>,
+ {
+ self.draw_series_impl(series)?;
+ Ok(self.alloc_series_anno())
+ }
+}
+
+impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, RangedCoord<X, Y>> {
+ /// Get the range of X axis
+ pub fn x_range(&self) -> Range<X::ValueType> {
+ self.drawing_area.get_x_range()
+ }
+
+ /// Get range of the Y axis
+ pub fn y_range(&self) -> Range<Y::ValueType> {
+ self.drawing_area.get_y_range()
+ }
+
+ /// Maps the coordinate to the backend coordinate. This is typically used
+ /// with an interactive chart.
+ pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord {
+ self.drawing_area.map_coordinate(coord)
+ }
+
+ pub(super) fn draw_series_impl<E, R, S>(
+ &mut self,
+ series: S,
+ ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
+ where
+ for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
+ E: Drawable<DB>,
+ R: Borrow<E>,
+ S: IntoIterator<Item = R>,
+ {
+ for element in series {
+ self.drawing_area.draw(element.borrow())?;
+ }
+ Ok(())
+ }
+
+ pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
+ let idx = self.series_anno.len();
+ self.series_anno.push(SeriesAnno::new());
+ &mut self.series_anno[idx]
+ }
+
+ /// Draw a data series. A data series in Plotters is abstracted as an iterator of elements
+ pub fn draw_series<E, R, S>(
+ &mut self,
+ series: S,
+ ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
+ where
+ for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
+ E: Drawable<DB>,
+ R: Borrow<E>,
+ S: IntoIterator<Item = R>,
+ {
+ self.draw_series_impl(series)?;
+ Ok(self.alloc_series_anno())
+ }
+
+ /// The actual function that draws the mesh lines.
+ /// It also returns the label that suppose to be there.
+ #[allow(clippy::type_complexity)]
+ fn draw_mesh_lines<FmtLabel>(
+ &mut self,
+ (r, c): (usize, usize),
+ (x_mesh, y_mesh): (bool, bool),
+ mesh_line_style: &ShapeStyle,
+ mut fmt_label: FmtLabel,
+ ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>>
+ where
+ FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
+ {
+ let mut x_labels = vec![];
+ let mut y_labels = vec![];
+ self.drawing_area.draw_mesh(
+ |b, l| {
+ let draw;
+ match l {
+ MeshLine::XMesh((x, _), _, _) => {
+ if let Some(label_text) = fmt_label(&l) {
+ x_labels.push((x, label_text));
+ }
+ draw = x_mesh;
+ }
+ MeshLine::YMesh((_, y), _, _) => {
+ if let Some(label_text) = fmt_label(&l) {
+ y_labels.push((y, label_text));
+ }
+ draw = y_mesh;
+ }
+ };
+ if draw {
+ l.draw(b, mesh_line_style)
+ } else {
+ Ok(())
+ }
+ },
+ r,
+ c,
+ )?;
+ Ok((x_labels, y_labels))
+ }
+
+ fn draw_axis(
+ &self,
+ area: &DrawingArea<DB, Shift>,
+ axis_style: Option<&ShapeStyle>,
+ orientation: (i16, i16),
+ inward_labels: bool,
+ ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> {
+ let (x0, y0) = self.drawing_area.get_base_pixel();
+ let (tw, th) = area.dim_in_pixel();
+
+ let mut axis_range = if orientation.0 == 0 {
+ self.drawing_area.get_x_axis_pixel_range()
+ } else {
+ self.drawing_area.get_y_axis_pixel_range()
+ };
+
+ /* At this point, the coordinate system tells us the pixel range
+ * after the translation.
+ * However, we need to use the logic coordinate system for drawing. */
+ if orientation.0 == 0 {
+ axis_range.start -= x0;
+ axis_range.end -= x0;
+ } else {
+ axis_range.start -= y0;
+ axis_range.end -= y0;
+ }
+
+ if let Some(axis_style) = axis_style {
+ let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 };
+ let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 };
+ let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 };
+ let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 };
+
+ if inward_labels {
+ if orientation.0 == 0 {
+ if y0 == 0 {
+ y0 = th as i32 - 1;
+ y1 = th as i32 - 1;
+ } else {
+ y0 = 0;
+ y1 = 0;
+ }
+ } else if x0 == 0 {
+ x0 = tw as i32 - 1;
+ x1 = tw as i32 - 1;
+ } else {
+ x0 = 0;
+ x1 = 0;
+ }
+ }
+
+ if orientation.0 == 0 {
+ x0 = axis_range.start;
+ x1 = axis_range.end;
+ } else {
+ y0 = axis_range.start;
+ y1 = axis_range.end;
+ }
+
+ area.draw(&PathElement::new(
+ vec![(x0, y0), (x1, y1)],
+ axis_style.clone(),
+ ))?;
+ }
+
+ Ok(axis_range)
+ }
+
+ // TODO: consider make this function less complicated
+ #[allow(clippy::too_many_arguments)]
+ #[allow(clippy::cognitive_complexity)]
+ fn draw_axis_and_labels(
+ &self,
+ area: Option<&DrawingArea<DB, Shift>>,
+ axis_style: Option<&ShapeStyle>,
+ labels: &[(i32, String)],
+ label_style: &TextStyle,
+ label_offset: i32,
+ orientation: (i16, i16),
+ axis_desc: Option<(&str, &TextStyle)>,
+ tick_size: i32,
+ ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
+ let area = if let Some(target) = area {
+ target
+ } else {
+ return Ok(());
+ };
+
+ let (x0, y0) = self.drawing_area.get_base_pixel();
+ let (tw, th) = area.dim_in_pixel();
+
+ /* This is the minimal distance from the axis to the box of the labels */
+ let label_dist = tick_size.abs() * 2;
+
+ /* Draw the axis and get the axis range so that we can do further label
+ * and tick mark drawing */
+ let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?;
+
+ /* To make the right label area looks nice, it's a little bit tricky, since for a that is
+ * very long, we actually prefer left alignment instead of right alignment.
+ * Otherwise, the right alignment looks better. So we estimate the max and min label width
+ * So that we are able decide if we should apply right alignment for the text. */
+ let label_width: Vec<_> = labels
+ .iter()
+ .map(|(_, text)| {
+ if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 {
+ let ((x0, _), (x1, _)) = label_style
+ .font
+ .layout_box(text)
+ .unwrap_or(((0, 0), (0, 0)));
+ x1 - x0
+ } else {
+ // Don't ever do the layout estimationfor the drawing area that is either not
+ // the right one or the tick mark is inward.
+ 0
+ }
+ })
+ .collect();
+
+ let min_width = *label_width.iter().min().unwrap_or(&1).max(&1);
+ let max_width = *label_width
+ .iter()
+ .filter(|&&x| x < min_width * 2)
+ .max()
+ .unwrap_or(&min_width);
+ let right_align_width = (min_width * 2).min(max_width);
+
+ /* Then we need to draw the tick mark and the label */
+ for ((p, t), w) in labels.iter().zip(label_width.into_iter()) {
+ /* Make sure we are actually in the visible range */
+ let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 };
+
+ if rp < axis_range.start.min(axis_range.end)
+ || axis_range.end.max(axis_range.start) < rp
+ {
+ continue;
+ }
+
+ let (cx, cy, h_pos, v_pos) = if tick_size >= 0 {
+ match orientation {
+ // Right
+ (dx, dy) if dx > 0 && dy == 0 => {
+ if w >= right_align_width {
+ (label_dist, *p - y0, HPos::Left, VPos::Center)
+ } else {
+ (
+ label_dist + right_align_width,
+ *p - y0,
+ HPos::Right,
+ VPos::Center,
+ )
+ }
+ }
+ // Left
+ (dx, dy) if dx < 0 && dy == 0 => {
+ (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
+ }
+ // Bottom
+ (dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
+ // Top
+ (dx, dy) if dx == 0 && dy < 0 => {
+ (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
+ }
+ _ => panic!("Bug: Invalid orientation specification"),
+ }
+ } else {
+ match orientation {
+ // Right
+ (dx, dy) if dx > 0 && dy == 0 => {
+ (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
+ }
+ // Left
+ (dx, dy) if dx < 0 && dy == 0 => {
+ (label_dist, *p - y0, HPos::Left, VPos::Center)
+ }
+ // Bottom
+ (dx, dy) if dx == 0 && dy > 0 => {
+ (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
+ }
+ // Top
+ (dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
+ _ => panic!("Bug: Invalid orientation specification"),
+ }
+ };
+
+ let (text_x, text_y) = if orientation.0 == 0 {
+ (cx + label_offset, cy)
+ } else {
+ (cx, cy + label_offset)
+ };
+
+ let label_style = &label_style.pos(Pos::new(h_pos, v_pos));
+ area.draw_text(&t, label_style, (text_x, text_y))?;
+
+ if tick_size != 0 {
+ if let Some(style) = axis_style {
+ let xmax = tw as i32 - 1;
+ let ymax = th as i32 - 1;
+ let (kx0, ky0, kx1, ky1) = if tick_size > 0 {
+ match orientation {
+ (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0),
+ (dx, dy) if dx < 0 && dy == 0 => {
+ (xmax - tick_size, *p - y0, xmax, *p - y0)
+ }
+ (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size),
+ (dx, dy) if dx == 0 && dy < 0 => {
+ (*p - x0, ymax - tick_size, *p - x0, ymax)
+ }
+ _ => panic!("Bug: Invalid orientation specification"),
+ }
+ } else {
+ match orientation {
+ (dx, dy) if dx > 0 && dy == 0 => {
+ (xmax, *p - y0, xmax + tick_size, *p - y0)
+ }
+ (dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0),
+ (dx, dy) if dx == 0 && dy > 0 => {
+ (*p - x0, ymax, *p - x0, ymax + tick_size)
+ }
+ (dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size),
+ _ => panic!("Bug: Invalid orientation specification"),
+ }
+ };
+ let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], style.clone());
+ area.draw(&line)?;
+ }
+ }
+ }
+
+ if let Some((text, style)) = axis_desc {
+ let actual_style = if orientation.0 == 0 {
+ style.clone()
+ } else if orientation.0 == -1 {
+ style.transform(FontTransform::Rotate270)
+ } else {
+ style.transform(FontTransform::Rotate90)
+ };
+
+ let (x0, y0, h_pos, v_pos) = match orientation {
+ // Right
+ (dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top),
+ // Left
+ (dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top),
+ // Bottom
+ (dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom),
+ // Top
+ (dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top),
+ _ => panic!("Bug: Invalid orientation specification"),
+ };
+
+ let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos));
+ area.draw_text(&text, &actual_style, (x0 as i32, y0 as i32))?;
+ }
+
+ Ok(())
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ pub(super) fn draw_mesh<FmtLabel>(
+ &mut self,
+ (r, c): (usize, usize),
+ mesh_line_style: &ShapeStyle,
+ x_label_style: &TextStyle,
+ y_label_style: &TextStyle,
+ fmt_label: FmtLabel,
+ x_mesh: bool,
+ y_mesh: bool,
+ x_label_offset: i32,
+ y_label_offset: i32,
+ x_axis: bool,
+ y_axis: bool,
+ axis_style: &ShapeStyle,
+ axis_desc_style: &TextStyle,
+ x_desc: Option<String>,
+ y_desc: Option<String>,
+ x_tick_size: [i32; 2],
+ y_tick_size: [i32; 2],
+ ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
+ where
+ FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
+ {
+ let (x_labels, y_labels) =
+ self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?;
+
+ for idx in 0..2 {
+ self.draw_axis_and_labels(
+ self.x_label_area[idx].as_ref(),
+ if x_axis { Some(axis_style) } else { None },
+ &x_labels[..],
+ x_label_style,
+ x_label_offset,
+ (0, -1 + idx as i16 * 2),
+ x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
+ x_tick_size[idx],
+ )?;
+
+ self.draw_axis_and_labels(
+ self.y_label_area[idx].as_ref(),
+ if y_axis { Some(axis_style) } else { None },
+ &y_labels[..],
+ y_label_style,
+ y_label_offset,
+ (-1 + idx as i16 * 2, 0),
+ y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
+ y_tick_size[idx],
+ )?;
+ }
+
+ Ok(())
+ }
+
+ /// Convert this chart context into a dual axis chart context
+ ///
+ /// - `x_coord`: The coordinate spec for the X axis
+ /// - `y_coord`: The coordinate spec for the Y axis
+ /// - **returns** The newly created dual spec chart context
+ #[allow(clippy::type_complexity)]
+ pub fn set_secondary_coord<SX: AsRangedCoord, SY: AsRangedCoord>(
+ self,
+ x_coord: SX,
+ y_coord: SY,
+ ) -> DualCoordChartContext<
+ 'a,
+ DB,
+ RangedCoord<X, Y>,
+ RangedCoord<SX::CoordDescType, SY::CoordDescType>,
+ > {
+ let mut pixel_range = self.drawing_area.get_pixel_range();
+ pixel_range.1 = pixel_range.1.end..pixel_range.1.start;
+
+ DualCoordChartContext::new(self, RangedCoord::new(x_coord, y_coord, pixel_range))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::prelude::*;
+
+ #[test]
+ fn test_chart_context() {
+ let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
+
+ drawing_area.fill(&WHITE).expect("Fill");
+
+ let mut chart = ChartBuilder::on(&drawing_area)
+ .caption("Test Title", ("serif", 10))
+ .x_label_area_size(20)
+ .y_label_area_size(20)
+ .set_label_area_size(LabelAreaPosition::Top, 20)
+ .set_label_area_size(LabelAreaPosition::Right, 20)
+ .build_ranged(0..10, 0..10)
+ .expect("Create chart")
+ .set_secondary_coord(0.0..1.0, 0.0..1.0);
+
+ chart
+ .configure_mesh()
+ .x_desc("X")
+ .y_desc("Y")
+ .draw()
+ .expect("Draw mesh");
+ chart
+ .configure_secondary_axes()
+ .x_desc("X")
+ .y_desc("Y")
+ .draw()
+ .expect("Draw Secondary axes");
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 5), 5, &RED)))
+ .expect("Drawing error");
+ chart
+ .draw_secondary_series(std::iter::once(Circle::new((0.3, 0.8), 5, &GREEN)))
+ .expect("Drawing error")
+ .label("Test label")
+ .legend(|(x, y)| Rectangle::new([(x - 10, y - 5), (x, y + 5)], &GREEN));
+
+ chart
+ .configure_series_labels()
+ .position(SeriesLabelPosition::UpperMiddle)
+ .draw()
+ .expect("Drawing error");
+ }
+}
diff --git a/src/chart/dual_coord.rs b/src/chart/dual_coord.rs
new file mode 100644
index 0000000..dcc0ce8
--- /dev/null
+++ b/src/chart/dual_coord.rs
@@ -0,0 +1,231 @@
+/// The dual coordinate system support
+use std::borrow::{Borrow, BorrowMut};
+use std::fmt::Debug;
+use std::ops::{Deref, DerefMut};
+use std::sync::Arc;
+
+use super::context::{ChartContext, ChartState, SeriesAnno};
+use super::mesh::SecondaryMeshStyle;
+
+use crate::coord::{CoordTranslate, Ranged, RangedCoord, ReverseCoordTranslate, Shift};
+use crate::drawing::backend::{BackendCoord, DrawingBackend};
+use crate::drawing::DrawingArea;
+use crate::drawing::DrawingAreaErrorKind;
+use crate::element::{Drawable, PointCollection};
+
+/// The chart context that has two coordinate system attached
+pub struct DualCoordChartContext<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate> {
+ pub(super) primary: ChartContext<'a, DB, CT1>,
+ pub(super) secondary: ChartContext<'a, DB, CT2>,
+}
+
+/// The chart state for a dual coord chart, see the detailed description for `ChartState` for more
+/// information about the purpose of a chart state
+pub struct DualCoordChartState<CT1: CoordTranslate, CT2: CoordTranslate> {
+ primary: ChartState<CT1>,
+ secondary: ChartState<CT2>,
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
+ DualCoordChartContext<'a, DB, CT1, CT2>
+{
+ /// Convert the chart context into a chart state
+ pub fn into_chart_state(self) -> DualCoordChartState<CT1, CT2> {
+ DualCoordChartState {
+ primary: self.primary.into(),
+ secondary: self.secondary.into(),
+ }
+ }
+
+ /// Convert the chart context into a sharable chart state
+ pub fn into_shared_chart_state(self) -> DualCoordChartState<Arc<CT1>, Arc<CT2>> {
+ DualCoordChartState {
+ primary: self.primary.into_shared_chart_state(),
+ secondary: self.secondary.into_shared_chart_state(),
+ }
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
+ DualCoordChartContext<'a, DB, CT1, CT2>
+where
+ CT1: Clone,
+ CT2: Clone,
+{
+ /// Copy the coordinate specs and make the chart state
+ pub fn to_chart_state(&self) -> DualCoordChartState<CT1, CT2> {
+ DualCoordChartState {
+ primary: self.primary.to_chart_state(),
+ secondary: self.secondary.to_chart_state(),
+ }
+ }
+}
+
+impl<CT1: CoordTranslate, CT2: CoordTranslate> DualCoordChartState<CT1, CT2> {
+ /// Restore the chart state on the given drawing area
+ pub fn restore<'a, DB: DrawingBackend + 'a>(
+ self,
+ area: &DrawingArea<DB, Shift>,
+ ) -> DualCoordChartContext<'a, DB, CT1, CT2> {
+ let primary = self.primary.restore(area);
+ let secondary = self
+ .secondary
+ .restore(&primary.plotting_area().strip_coord_spec());
+ DualCoordChartContext { primary, secondary }
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
+ From<DualCoordChartContext<'a, DB, CT1, CT2>> for DualCoordChartState<CT1, CT2>
+{
+ fn from(chart: DualCoordChartContext<'a, DB, CT1, CT2>) -> DualCoordChartState<CT1, CT2> {
+ chart.into_chart_state()
+ }
+}
+
+impl<'a, 'b, DB: DrawingBackend, CT1: CoordTranslate + Clone, CT2: CoordTranslate + Clone>
+ From<&'b DualCoordChartContext<'a, DB, CT1, CT2>> for DualCoordChartState<CT1, CT2>
+{
+ fn from(chart: &'b DualCoordChartContext<'a, DB, CT1, CT2>) -> DualCoordChartState<CT1, CT2> {
+ chart.to_chart_state()
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
+ DualCoordChartContext<'a, DB, CT1, CT2>
+{
+ pub(super) fn new(mut primary: ChartContext<'a, DB, CT1>, secondary_coord: CT2) -> Self {
+ let secondary_drawing_area = primary
+ .drawing_area
+ .strip_coord_spec()
+ .apply_coord_spec(secondary_coord);
+ let mut secondary_x_label_area = [None, None];
+ let mut secondary_y_label_area = [None, None];
+
+ std::mem::swap(&mut primary.x_label_area[0], &mut secondary_x_label_area[0]);
+ std::mem::swap(&mut primary.y_label_area[1], &mut secondary_y_label_area[1]);
+
+ Self {
+ primary,
+ secondary: ChartContext {
+ x_label_area: secondary_x_label_area,
+ y_label_area: secondary_y_label_area,
+ drawing_area: secondary_drawing_area,
+ series_anno: vec![],
+ drawing_area_pos: (0, 0),
+ },
+ }
+ }
+
+ /// Get a reference to the drawing area that uses the secondary coordinate system
+ pub fn secondary_plotting_area(&self) -> &DrawingArea<DB, CT2> {
+ &self.secondary.drawing_area
+ }
+
+ /// Borrow a mutable reference to the chart context that uses the secondary
+ /// coordinate system
+ pub fn borrow_secondary(&self) -> &ChartContext<'a, DB, CT2> {
+ &self.secondary
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: ReverseCoordTranslate>
+ DualCoordChartContext<'a, DB, CT1, CT2>
+{
+ /// Convert the chart context into the secondary coordinate translation function
+ pub fn into_secondary_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT2::From> {
+ let coord_spec = self.secondary.drawing_area.into_coord_spec();
+ move |coord| coord_spec.reverse_translate(coord)
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: ReverseCoordTranslate, CT2: ReverseCoordTranslate>
+ DualCoordChartContext<'a, DB, CT1, CT2>
+{
+ /// Convert the chart context into a pair of closures that maps the pixel coordinate into the
+ /// logical coordinate for both primary coordinate system and secondary coordinate system.
+ pub fn into_coord_trans_pair(
+ self,
+ ) -> (
+ impl Fn(BackendCoord) -> Option<CT1::From>,
+ impl Fn(BackendCoord) -> Option<CT2::From>,
+ ) {
+ let coord_spec_1 = self.primary.drawing_area.into_coord_spec();
+ let coord_spec_2 = self.secondary.drawing_area.into_coord_spec();
+ (
+ move |coord| coord_spec_1.reverse_translate(coord),
+ move |coord| coord_spec_2.reverse_translate(coord),
+ )
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, SX: Ranged, SY: Ranged>
+ DualCoordChartContext<'a, DB, CT1, RangedCoord<SX, SY>>
+where
+ SX::ValueType: Debug,
+ SY::ValueType: Debug,
+{
+ /// Start configure the style for the secondary axes
+ pub fn configure_secondary_axes<'b>(&'b mut self) -> SecondaryMeshStyle<'a, 'b, SX, SY, DB> {
+ SecondaryMeshStyle::new(&mut self.secondary)
+ }
+}
+
+impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged, SX: Ranged, SY: Ranged>
+ DualCoordChartContext<'a, DB, RangedCoord<X, Y>, RangedCoord<SX, SY>>
+where
+ X::ValueType: Debug,
+ Y::ValueType: Debug,
+ SX::ValueType: Debug,
+ SY::ValueType: Debug,
+{
+ /// Draw a series use the secondary coordinate system
+ /// - `series`: The series to draw
+ /// - `Returns` the series annotation object or error code
+ pub fn draw_secondary_series<E, R, S>(
+ &mut self,
+ series: S,
+ ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
+ where
+ for<'b> &'b E: PointCollection<'b, (SX::ValueType, SY::ValueType)>,
+ E: Drawable<DB>,
+ R: Borrow<E>,
+ S: IntoIterator<Item = R>,
+ {
+ self.secondary.draw_series_impl(series)?;
+ Ok(self.primary.alloc_series_anno())
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
+ Borrow<ChartContext<'a, DB, CT1>> for DualCoordChartContext<'a, DB, CT1, CT2>
+{
+ fn borrow(&self) -> &ChartContext<'a, DB, CT1> {
+ &self.primary
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate>
+ BorrowMut<ChartContext<'a, DB, CT1>> for DualCoordChartContext<'a, DB, CT1, CT2>
+{
+ fn borrow_mut(&mut self) -> &mut ChartContext<'a, DB, CT1> {
+ &mut self.primary
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate> Deref
+ for DualCoordChartContext<'a, DB, CT1, CT2>
+{
+ type Target = ChartContext<'a, DB, CT1>;
+ fn deref(&self) -> &Self::Target {
+ self.borrow()
+ }
+}
+
+impl<'a, DB: DrawingBackend, CT1: CoordTranslate, CT2: CoordTranslate> DerefMut
+ for DualCoordChartContext<'a, DB, CT1, CT2>
+{
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.borrow_mut()
+ }
+}
diff --git a/src/chart/mesh.rs b/src/chart/mesh.rs
new file mode 100644
index 0000000..8f96168
--- /dev/null
+++ b/src/chart/mesh.rs
@@ -0,0 +1,431 @@
+use std::fmt::Debug;
+use std::marker::PhantomData;
+
+use super::builder::LabelAreaPosition;
+use super::context::ChartContext;
+use crate::coord::{MeshLine, Ranged, RangedCoord};
+use crate::drawing::backend::DrawingBackend;
+use crate::drawing::DrawingAreaErrorKind;
+use crate::style::{
+ AsRelative, Color, FontDesc, FontFamily, FontStyle, IntoTextStyle, RGBColor, ShapeStyle,
+ SizeDesc, TextStyle,
+};
+
+/// The style used to describe the mesh and axis for a secondary coordinate system.
+pub struct SecondaryMeshStyle<'a, 'b, X: Ranged, Y: Ranged, DB: DrawingBackend> {
+ style: MeshStyle<'a, 'b, X, Y, DB>,
+}
+
+impl<'a, 'b, X: Ranged, Y: Ranged, DB: DrawingBackend> SecondaryMeshStyle<'a, 'b, X, Y, DB>
+where
+ X::ValueType: Debug,
+ Y::ValueType: Debug,
+{
+ pub(super) fn new(target: &'b mut ChartContext<'a, DB, RangedCoord<X, Y>>) -> Self {
+ let mut style = target.configure_mesh();
+ style.draw_x_mesh = false;
+ style.draw_y_mesh = false;
+ Self { style }
+ }
+
+ /// Set the style definition for the axis
+ /// - `style`: The style for the axis
+ pub fn axis_style<T: Into<ShapeStyle>>(&mut self, style: T) -> &mut Self {
+ self.style.axis_style(style);
+ self
+ }
+
+ /// The offset of x labels. This is used when we want to place the label in the middle of
+ /// the grid. This is useful if we are drawing a histogram
+ /// - `value`: The offset in pixel
+ pub fn x_label_offset<S: SizeDesc>(&mut self, value: S) -> &mut Self {
+ self.style.x_label_offset(value);
+ self
+ }
+
+ /// The offset of y labels. This is used when we want to place the label in the middle of
+ /// the grid. This is useful if we are drawing a histogram
+ /// - `value`: The offset in pixel
+ pub fn y_label_offset<S: SizeDesc>(&mut self, value: S) -> &mut Self {
+ self.style.y_label_offset(value);
+ self
+ }
+
+ /// Set how many labels for the X axis at most
+ /// - `value`: The maximum desired number of labels in the X axis
+ pub fn x_labels(&mut self, value: usize) -> &mut Self {
+ self.style.x_labels(value);
+ self
+ }
+
+ /// Set how many label for the Y axis at most
+ /// - `value`: The maximum desired number of labels in the Y axis
+ pub fn y_labels(&mut self, value: usize) -> &mut Self {
+ self.style.y_labels(value);
+ self
+ }
+
+ /// Set the formatter function for the X label text
+ /// - `fmt`: The formatter function
+ pub fn x_label_formatter(&mut self, fmt: &'b dyn Fn(&X::ValueType) -> String) -> &mut Self {
+ self.style.x_label_formatter(fmt);
+ self
+ }
+
+ /// Set the formatter function for the Y label text
+ /// - `fmt`: The formatter function
+ pub fn y_label_formatter(&mut self, fmt: &'b dyn Fn(&Y::ValueType) -> String) -> &mut Self {
+ self.style.y_label_formatter(fmt);
+ self
+ }
+
+ /// Set the axis description's style. If not given, use label style instead.
+ /// - `style`: The text style that would be applied to descriptions
+ pub fn axis_desc_style<T: IntoTextStyle<'b>>(&mut self, style: T) -> &mut Self {
+ self.style
+ .axis_desc_style(style.into_text_style(&self.style.parent_size));
+ self
+ }
+
+ /// Set the X axis's description
+ /// - `desc`: The description of the X axis
+ pub fn x_desc<T: Into<String>>(&mut self, desc: T) -> &mut Self {
+ self.style.x_desc(desc);
+ self
+ }
+
+ /// Set the Y axis's description
+ /// - `desc`: The description of the Y axis
+ pub fn y_desc<T: Into<String>>(&mut self, desc: T) -> &mut Self {
+ self.style.y_desc(desc);
+ self
+ }
+
+ /// Draw the axes for the secondary coordinate system
+ pub fn draw(&mut self) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
+ self.style.draw()
+ }
+
+ /// Set the label style for the secondary axis
+ pub fn label_style<T: IntoTextStyle<'b>>(&mut self, style: T) -> &mut Self {
+ self.style.label_style(style);
+ self
+ }
+
+ /// Set all the tick mark to the same size
+ /// `value`: The new size
+ pub fn set_all_tick_mark_size<S: SizeDesc>(&mut self, value: S) -> &mut Self {
+ let size = value.in_pixels(&self.style.parent_size);
+ self.style.x_tick_size = [size, size];
+ self.style.y_tick_size = [size, size];
+ self
+ }
+
+ pub fn set_tick_mark_size<S: SizeDesc>(
+ &mut self,
+ pos: LabelAreaPosition,
+ value: S,
+ ) -> &mut Self {
+ *match pos {
+ LabelAreaPosition::Top => &mut self.style.x_tick_size[0],
+ LabelAreaPosition::Bottom => &mut self.style.x_tick_size[1],
+ LabelAreaPosition::Left => &mut self.style.y_tick_size[0],
+ LabelAreaPosition::Right => &mut self.style.y_tick_size[1],
+ } = value.in_pixels(&self.style.parent_size);
+ self
+ }
+}
+
+/// The struct that is used for tracking the configuration of a mesh of any chart
+pub struct MeshStyle<'a, 'b, X: Ranged, Y: Ranged, DB>
+where
+ DB: DrawingBackend,
+{
+ pub(super) parent_size: (u32, u32),
+ pub(super) draw_x_mesh: bool,
+ pub(super) draw_y_mesh: bool,
+ pub(super) draw_x_axis: bool,
+ pub(super) draw_y_axis: bool,
+ pub(super) x_label_offset: i32,
+ pub(super) y_label_offset: i32,
+ pub(super) n_x_labels: usize,
+ pub(super) n_y_labels: usize,
+ pub(super) axis_desc_style: Option<TextStyle<'b>>,
+ pub(super) x_desc: Option<String>,
+ pub(super) y_desc: Option<String>,
+ pub(super) line_style_1: Option<ShapeStyle>,
+ pub(super) line_style_2: Option<ShapeStyle>,
+ pub(super) axis_style: Option<ShapeStyle>,
+ pub(super) x_label_style: Option<TextStyle<'b>>,
+ pub(super) y_label_style: Option<TextStyle<'b>>,
+ pub(super) format_x: &'b dyn Fn(&X::ValueType) -> String,
+ pub(super) format_y: &'b dyn Fn(&Y::ValueType) -> String,
+ pub(super) target: Option<&'b mut ChartContext<'a, DB, RangedCoord<X, Y>>>,
+ pub(super) _phantom_data: PhantomData<(X, Y)>,
+ pub(super) x_tick_size: [i32; 2],
+ pub(super) y_tick_size: [i32; 2],
+}
+
+impl<'a, 'b, X, Y, DB> MeshStyle<'a, 'b, X, Y, DB>
+where
+ X: Ranged,
+ Y: Ranged,
+ DB: DrawingBackend,
+{
+ /// Set all the tick mark to the same size
+ /// `value`: The new size
+ pub fn set_all_tick_mark_size<S: SizeDesc>(&mut self, value: S) -> &mut Self {
+ let size = value.in_pixels(&self.parent_size);
+ self.x_tick_size = [size, size];
+ self.y_tick_size = [size, size];
+ self
+ }
+
+ /// Set the tick mark size on the axes. When this is set to negative, the axis value label will
+ /// become inward.
+ ///
+ /// - `pos`: The which label area we want to set
+ /// - `value`: The size specification
+ pub fn set_tick_mark_size<S: SizeDesc>(
+ &mut self,
+ pos: LabelAreaPosition,
+ value: S,
+ ) -> &mut Self {
+ *match pos {
+ LabelAreaPosition::Top => &mut self.x_tick_size[0],
+ LabelAreaPosition::Bottom => &mut self.x_tick_size[1],
+ LabelAreaPosition::Left => &mut self.y_tick_size[0],
+ LabelAreaPosition::Right => &mut self.y_tick_size[1],
+ } = value.in_pixels(&self.parent_size);
+ self
+ }
+
+ /// The offset of x labels. This is used when we want to place the label in the middle of
+ /// the grid. This is useful if we are drawing a histogram
+ /// - `value`: The offset in pixel
+ pub fn x_label_offset<S: SizeDesc>(&mut self, value: S) -> &mut Self {
+ self.x_label_offset = value.in_pixels(&self.parent_size);
+ self
+ }
+
+ /// The offset of y labels. This is used when we want to place the label in the middle of
+ /// the grid. This is useful if we are drawing a histogram
+ /// - `value`: The offset in pixel
+ pub fn y_label_offset<S: SizeDesc>(&mut self, value: S) -> &mut Self {
+ self.y_label_offset = value.in_pixels(&self.parent_size);
+ self
+ }
+
+ /// Disable the mesh for the x axis.
+ pub fn disable_x_mesh(&mut self) -> &mut Self {
+ self.draw_x_mesh = false;
+ self
+ }
+
+ /// Disable the mesh for the y axis
+ pub fn disable_y_mesh(&mut self) -> &mut Self {
+ self.draw_y_mesh = false;
+ self
+ }
+
+ /// Disable drawing the X axis
+ pub fn disable_x_axis(&mut self) -> &mut Self {
+ self.draw_x_axis = false;
+ self
+ }
+
+ /// Disable drawing the Y axis
+ pub fn disable_y_axis(&mut self) -> &mut Self {
+ self.draw_y_axis = false;
+ self
+ }
+
+ /// Disable drawing all meshes
+ pub fn disable_mesh(&mut self) -> &mut Self {
+ self.disable_x_mesh().disable_y_mesh()
+ }
+
+ /// Disable drawing all axes
+ pub fn disable_axes(&mut self) -> &mut Self {
+ self.disable_x_axis().disable_y_axis()
+ }
+
+ /// Set the style definition for the axis
+ /// - `style`: The style for the axis
+ pub fn axis_style<T: Into<ShapeStyle>>(&mut self, style: T) -> &mut Self {
+ self.axis_style = Some(style.into());
+ self
+ }
+ /// Set how many labels for the X axis at most
+ /// - `value`: The maximum desired number of labels in the X axis
+ pub fn x_labels(&mut self, value: usize) -> &mut Self {
+ self.n_x_labels = value;
+ self
+ }
+
+ /// Set how many label for the Y axis at most
+ /// - `value`: The maximum desired number of labels in the Y axis
+ pub fn y_labels(&mut self, value: usize) -> &mut Self {
+ self.n_y_labels = value;
+ self
+ }
+
+ /// Set the style for the coarse grind grid
+ /// - `style`: This is the coarse grind grid style
+ pub fn line_style_1<T: Into<ShapeStyle>>(&mut self, style: T) -> &mut Self {
+ self.line_style_1 = Some(style.into());
+ self
+ }
+
+ /// Set the style for the fine grind grid
+ /// - `style`: The fine grind grid style
+ pub fn line_style_2<T: Into<ShapeStyle>>(&mut self, style: T) -> &mut Self {
+ self.line_style_2 = Some(style.into());
+ self
+ }
+
+ /// Set the style of the label text
+ /// - `style`: The text style that would be applied to the labels
+ pub fn label_style<T: IntoTextStyle<'b>>(&mut self, style: T) -> &mut Self {
+ let style = style.into_text_style(&self.parent_size);
+ self.x_label_style = Some(style.clone());
+ self.y_label_style = Some(style);
+ self
+ }
+
+ /// Set the style of the label X axis text
+ /// - `style`: The text style that would be applied to the labels
+ pub fn x_label_style<T: IntoTextStyle<'b>>(&mut self, style: T) -> &mut Self {
+ self.x_label_style = Some(style.into_text_style(&self.parent_size));
+ self
+ }
+
+ /// Set the style of the label Y axis text
+ /// - `style`: The text style that would be applied to the labels
+ pub fn y_label_style<T: IntoTextStyle<'b>>(&mut self, style: T) -> &mut Self {
+ self.y_label_style = Some(style.into_text_style(&self.parent_size));
+ self
+ }
+
+ /// Set the formatter function for the X label text
+ /// - `fmt`: The formatter function
+ pub fn x_label_formatter(&mut self, fmt: &'b dyn Fn(&X::ValueType) -> String) -> &mut Self {
+ self.format_x = fmt;
+ self
+ }
+
+ /// Set the formatter function for the Y label text
+ /// - `fmt`: The formatter function
+ pub fn y_label_formatter(&mut self, fmt: &'b dyn Fn(&Y::ValueType) -> String) -> &mut Self {
+ self.format_y = fmt;
+ self
+ }
+
+ /// Set the axis description's style. If not given, use label style instead.
+ /// - `style`: The text style that would be applied to descriptions
+ pub fn axis_desc_style<T: IntoTextStyle<'b>>(&mut self, style: T) -> &mut Self {
+ self.axis_desc_style = Some(style.into_text_style(&self.parent_size));
+ self
+ }
+
+ /// Set the X axis's description
+ /// - `desc`: The description of the X axis
+ pub fn x_desc<T: Into<String>>(&mut self, desc: T) -> &mut Self {
+ self.x_desc = Some(desc.into());
+ self
+ }
+
+ /// Set the Y axis's description
+ /// - `desc`: The description of the Y axis
+ pub fn y_desc<T: Into<String>>(&mut self, desc: T) -> &mut Self {
+ self.y_desc = Some(desc.into());
+ self
+ }
+
+ /// Draw the configured mesh on the target plot
+ pub fn draw(&mut self) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
+ let mut target = None;
+ std::mem::swap(&mut target, &mut self.target);
+ let target = target.unwrap();
+
+ let default_mesh_color_1 = RGBColor(0, 0, 0).mix(0.2);
+ let default_mesh_color_2 = RGBColor(0, 0, 0).mix(0.1);
+ let default_axis_color = RGBColor(0, 0, 0);
+ let default_label_font = FontDesc::new(
+ FontFamily::SansSerif,
+ f64::from((12i32).percent().max(12).in_pixels(&self.parent_size)),
+ FontStyle::Normal,
+ );
+
+ let mesh_style_1 = self
+ .line_style_1
+ .clone()
+ .unwrap_or_else(|| (&default_mesh_color_1).into());
+ let mesh_style_2 = self
+ .line_style_2
+ .clone()
+ .unwrap_or_else(|| (&default_mesh_color_2).into());
+ let axis_style = self
+ .axis_style
+ .clone()
+ .unwrap_or_else(|| (&default_axis_color).into());
+
+ let x_label_style = self
+ .x_label_style
+ .clone()
+ .unwrap_or_else(|| default_label_font.clone().into());
+
+ let y_label_style = self
+ .y_label_style
+ .clone()
+ .unwrap_or_else(|| default_label_font.into());
+
+ let axis_desc_style = self
+ .axis_desc_style
+ .clone()
+ .unwrap_or_else(|| x_label_style.clone());
+
+ target.draw_mesh(
+ (self.n_y_labels * 10, self.n_x_labels * 10),
+ &mesh_style_2,
+ &x_label_style,
+ &y_label_style,
+ |_| None,
+ self.draw_x_mesh,
+ self.draw_y_mesh,
+ self.x_label_offset,
+ self.y_label_offset,
+ false,
+ false,
+ &axis_style,
+ &axis_desc_style,
+ self.x_desc.clone(),
+ self.y_desc.clone(),
+ self.x_tick_size,
+ self.y_tick_size,
+ )?;
+
+ target.draw_mesh(
+ (self.n_y_labels, self.n_x_labels),
+ &mesh_style_1,
+ &x_label_style,
+ &y_label_style,
+ |m| match m {
+ MeshLine::XMesh(_, _, v) => Some((self.format_x)(v)),
+ MeshLine::YMesh(_, _, v) => Some((self.format_y)(v)),
+ },
+ self.draw_x_mesh,
+ self.draw_y_mesh,
+ self.x_label_offset,
+ self.y_label_offset,
+ self.draw_x_axis,
+ self.draw_y_axis,
+ &axis_style,
+ &axis_desc_style,
+ None,
+ None,
+ self.x_tick_size,
+ self.y_tick_size,
+ )
+ }
+}
diff --git a/src/chart/mod.rs b/src/chart/mod.rs
new file mode 100644
index 0000000..edb0902
--- /dev/null
+++ b/src/chart/mod.rs
@@ -0,0 +1,25 @@
+/*!
+The high-level plotting abstractions.
+
+Plotters uses `ChartContext`, a thin layer on the top of `DrawingArea`, to provide
+high-level chart specific drawing functionalities, like, mesh line, coordinate label
+and other common components for the data chart.
+
+To draw a series, `ChartContext::draw_series` is used to draw a series on the chart.
+In Plotters, a series is abstracted as an iterator of elements.
+
+`ChartBuilder` is used to construct a chart. To learn more detailed information, check the
+detailed description for each struct.
+*/
+
+mod builder;
+mod context;
+mod dual_coord;
+mod mesh;
+mod series;
+
+pub use builder::{ChartBuilder, LabelAreaPosition};
+pub use context::{ChartContext, ChartState, SeriesAnno};
+pub use dual_coord::{DualCoordChartContext, DualCoordChartState};
+pub use mesh::MeshStyle;
+pub use series::{SeriesLabelPosition, SeriesLabelStyle};
diff --git a/src/chart/series.rs b/src/chart/series.rs
new file mode 100644
index 0000000..51fe97d
--- /dev/null
+++ b/src/chart/series.rs
@@ -0,0 +1,191 @@
+use super::ChartContext;
+use crate::coord::CoordTranslate;
+use crate::drawing::backend::{BackendCoord, DrawingErrorKind};
+use crate::drawing::{DrawingAreaErrorKind, DrawingBackend};
+use crate::element::{EmptyElement, IntoDynElement, MultiLineText, Rectangle};
+use crate::style::{IntoFont, IntoTextStyle, ShapeStyle, SizeDesc, TextStyle, TRANSPARENT};
+
+/// Describes where we want to put the series label
+pub enum SeriesLabelPosition {
+ UpperLeft,
+ MiddleLeft,
+ LowerLeft,
+ UpperMiddle,
+ MiddleMiddle,
+ LowerMiddle,
+ UpperRight,
+ MiddleRight,
+ LowerRight,
+ /// Force the series label drawn at the specific location
+ Coordinate(i32, i32),
+}
+
+impl SeriesLabelPosition {
+ fn layout_label_area(&self, label_dim: (i32, i32), area_dim: (u32, u32)) -> (i32, i32) {
+ use SeriesLabelPosition::*;
+ (
+ match self {
+ UpperLeft | MiddleLeft | LowerLeft => 5,
+ UpperMiddle | MiddleMiddle | LowerMiddle => {
+ (area_dim.0 as i32 - label_dim.0 as i32) / 2
+ }
+ UpperRight | MiddleRight | LowerRight => area_dim.0 as i32 - label_dim.0 as i32 - 5,
+ Coordinate(x, _) => *x,
+ },
+ match self {
+ UpperLeft | UpperMiddle | UpperRight => 5,
+ MiddleLeft | MiddleMiddle | MiddleRight => {
+ (area_dim.1 as i32 - label_dim.1 as i32) / 2
+ }
+ LowerLeft | LowerMiddle | LowerRight => area_dim.1 as i32 - label_dim.1 as i32 - 5,
+ Coordinate(_, y) => *y,
+ },
+ )
+ }
+}
+
+/// The struct to specify the series label of a target chart context
+pub struct SeriesLabelStyle<'a, 'b, DB: DrawingBackend, CT: CoordTranslate> {
+ target: &'b mut ChartContext<'a, DB, CT>,
+ position: SeriesLabelPosition,
+ legend_area_size: u32,
+ border_style: ShapeStyle,
+ background: ShapeStyle,
+ label_font: Option<TextStyle<'b>>,
+ margin: u32,
+}
+
+impl<'a, 'b, DB: DrawingBackend + 'a, CT: CoordTranslate> SeriesLabelStyle<'a, 'b, DB, CT> {
+ pub(super) fn new(target: &'b mut ChartContext<'a, DB, CT>) -> Self {
+ Self {
+ target,
+ position: SeriesLabelPosition::MiddleRight,
+ legend_area_size: 30,
+ border_style: (&TRANSPARENT).into(),
+ background: (&TRANSPARENT).into(),
+ label_font: None,
+ margin: 10,
+ }
+ }
+
+ /// Set the series label positioning style
+ /// `pos` - The positioning style
+ pub fn position(&mut self, pos: SeriesLabelPosition) -> &mut Self {
+ self.position = pos;
+ self
+ }
+
+ /// Set the margin of the series label drawing are
+ ///
+ /// - `value`: The size specification
+ pub fn margin<S: SizeDesc>(&mut self, value: S) -> &mut Self {
+ self.margin = value
+ .in_pixels(&self.target.plotting_area().dim_in_pixel())
+ .max(0) as u32;
+ self
+ }
+
+ /// Set the size of legend area
+ /// `size` - The size of legend area in pixel
+ pub fn legend_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
+ let size = size
+ .in_pixels(&self.target.plotting_area().dim_in_pixel())
+ .max(0) as u32;
+ self.legend_area_size = size;
+ self
+ }
+
+ /// Set the style of the label series area
+ /// `style` - The style of the border
+ pub fn border_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
+ self.border_style = style.into();
+ self
+ }
+
+ /// Set the background style
+ /// `style` - The style of the border
+ pub fn background_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
+ self.background = style.into();
+ self
+ }
+
+ /// Set the series label font
+ /// `font` - The font
+ pub fn label_font<F: IntoTextStyle<'b>>(&mut self, font: F) -> &mut Self {
+ self.label_font = Some(font.into_text_style(&self.target.plotting_area().dim_in_pixel()));
+ self
+ }
+
+ /// Draw the series label area
+ pub fn draw(&mut self) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
+ let drawing_area = self.target.plotting_area().strip_coord_spec();
+
+ // TODO: Issue #68 Currently generic font family doesn't load on OSX, change this after the issue
+ // resolved
+ let default_font = ("sans-serif", 12).into_font();
+ let default_style: TextStyle = default_font.into();
+
+ let font = {
+ let mut temp = None;
+ std::mem::swap(&mut self.label_font, &mut temp);
+ temp.unwrap_or(default_style)
+ };
+
+ let mut label_element = MultiLineText::<_, &str>::new((0, 0), &font);
+ let mut funcs = vec![];
+
+ for anno in self.target.series_anno.iter() {
+ let label_text = anno.get_label();
+ let draw_func = anno.get_draw_func();
+
+ if label_text == "" && draw_func.is_none() {
+ continue;
+ }
+
+ funcs.push(
+ draw_func.unwrap_or_else(|| &|p: BackendCoord| EmptyElement::at(p).into_dyn()),
+ );
+ label_element.push_line(label_text);
+ }
+
+ let (mut w, mut h) = label_element
+ .estimate_dimension()
+ .map_err(|e| DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(e)))?;
+
+ let margin = self.margin as i32;
+
+ w += self.legend_area_size as i32 + margin * 2;
+ h += margin * 2;
+
+ let (area_w, area_h) = drawing_area.dim_in_pixel();
+
+ let (label_x, label_y) = self.position.layout_label_area((w, h), (area_w, area_h));
+
+ label_element.relocate((
+ label_x + self.legend_area_size as i32 + margin,
+ label_y + margin,
+ ));
+
+ drawing_area.draw(&Rectangle::new(
+ [(label_x, label_y), (label_x + w, label_y + h)],
+ self.background.filled(),
+ ))?;
+ drawing_area.draw(&Rectangle::new(
+ [(label_x, label_y), (label_x + w, label_y + h)],
+ self.border_style.clone(),
+ ))?;
+ drawing_area.draw(&label_element)?;
+
+ for (((_, y0), (_, y1)), make_elem) in label_element
+ .compute_line_layout()
+ .map_err(|e| DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(e)))?
+ .into_iter()
+ .zip(funcs.into_iter())
+ {
+ let legend_element = make_elem((label_x + margin, (y0 + y1) / 2));
+ drawing_area.draw(&legend_element)?;
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/coord/category.rs b/src/coord/category.rs
new file mode 100644
index 0000000..805bad2
--- /dev/null
+++ b/src/coord/category.rs
@@ -0,0 +1,209 @@
+use std::fmt;
+use std::ops::Range;
+use std::rc::Rc;
+
+use super::{AsRangedCoord, Ranged};
+
+/// The category coordinate
+pub struct Category<T: PartialEq> {
+ name: String,
+ elements: Rc<Vec<T>>,
+ // i32 type is required for the empty ref (having -1 value)
+ idx: i32,
+}
+
+impl<T: PartialEq> Clone for Category<T> {
+ fn clone(&self) -> Self {
+ Category {
+ name: self.name.clone(),
+ elements: Rc::clone(&self.elements),
+ idx: self.idx,
+ }
+ }
+}
+
+impl<T: PartialEq + fmt::Display> fmt::Debug for Category<T> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let element = &self.elements[self.idx as usize];
+ write!(f, "{}", element)
+ }
+}
+
+impl<T: PartialEq> Category<T> {
+ /// Create a new category coordinate.
+ ///
+ /// - `name`: The name of the category
+ /// - `elements`: The vector of category elements
+ /// - **returns** The newly created category coordinate
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let category = Category::new("color", vec!["red", "green", "blue"]);
+ /// ```
+ pub fn new<S: Into<String>>(name: S, elements: Vec<T>) -> Self {
+ Self {
+ name: name.into(),
+ elements: Rc::new(elements),
+ idx: -1,
+ }
+ }
+
+ /// Get an element reference (tick) by its value.
+ ///
+ /// - `val`: The value of the element
+ /// - **returns** The optional reference
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let category = Category::new("color", vec!["red", "green", "blue"]);
+ /// let red = category.get(&"red");
+ /// assert!(red.is_some());
+ /// let unknown = category.get(&"unknown");
+ /// assert!(unknown.is_none());
+ /// ```
+ pub fn get(&self, val: &T) -> Option<Category<T>> {
+ match self.elements.iter().position(|x| x == val) {
+ Some(pos) => {
+ let element_ref = Category {
+ name: self.name.clone(),
+ elements: Rc::clone(&self.elements),
+ idx: pos as i32,
+ };
+ Some(element_ref)
+ }
+ _ => None,
+ }
+ }
+
+ /// Create a full range over the category elements.
+ ///
+ /// - **returns** The range including all category elements
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let category = Category::new("color", vec!["red", "green", "blue"]);
+ /// let range = category.range();
+ /// ```
+ pub fn range(&self) -> Self {
+ self.clone()
+ }
+
+ /// Get the number of elements in the category.
+ ///
+ /// - **returns** The number of elements
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let category = Category::new("color", vec!["red", "green", "blue"]);
+ /// assert_eq!(category.len(), 3);
+ /// ```
+ pub fn len(&self) -> usize {
+ self.elements.len()
+ }
+
+ /// Returns `true` if the category contains no elements.
+ ///
+ /// - **returns** `true` is no elements, otherwise - `false`
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let category = Category::new("color", vec!["red", "green", "blue"]);
+ /// assert_eq!(category.is_empty(), false);
+ ///
+ /// let category = Category::new("empty", Vec::<&str>::new());
+ /// assert_eq!(category.is_empty(), true);
+ /// ```
+ pub fn is_empty(&self) -> bool {
+ self.elements.is_empty()
+ }
+
+ /// Get the category name.
+ ///
+ /// - **returns** The name of the category
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let category = Category::new("color", vec!["red", "green", "blue"]);
+ /// assert_eq!(category.name(), "color");
+ /// ```
+ pub fn name(&self) -> String {
+ self.name.clone()
+ }
+}
+
+impl<T: PartialEq> Ranged for Category<T> {
+ type ValueType = Category<T>;
+
+ fn range(&self) -> Range<Category<T>> {
+ let mut left = self.clone();
+ let mut right = self.clone();
+ left.idx = 0;
+ right.idx = right.len() as i32 - 1;
+ left..right
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ // Add margins to spans as edge values are not applicable to category
+ let total_span = (self.len() + 1) as f64;
+ let value_span = f64::from(value.idx + 1);
+ (f64::from(limit.1 - limit.0) * value_span / total_span) as i32 + limit.0
+ }
+
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ let mut ret = vec![];
+ let intervals = (self.len() - 1) as f64;
+ let elements = &self.elements;
+ let name = &self.name;
+ let step = (intervals / max_points as f64 + 1.0) as usize;
+ for idx in (0..self.len()).step_by(step) {
+ ret.push(Category {
+ name: name.clone(),
+ elements: Rc::clone(&elements),
+ idx: idx as i32,
+ });
+ }
+ ret
+ }
+}
+
+impl<T: PartialEq> AsRangedCoord for Category<T> {
+ type CoordDescType = Self;
+ type Value = Category<T>;
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_clone_trait() {
+ let category = Category::new("color", vec!["red", "green", "blue"]);
+ let red = category.get(&"red").unwrap();
+ assert_eq!(red.idx, 0);
+ let clone = red.clone();
+ assert_eq!(clone.idx, 0);
+ }
+
+ #[test]
+ fn test_debug_trait() {
+ let category = Category::new("color", vec!["red", "green", "blue"]);
+ let red = category.get(&"red").unwrap();
+ assert_eq!(format!("{:?}", red), "red");
+ }
+
+ #[test]
+ fn test_ranged_trait() {
+ let category = Category::new("color", vec!["red", "green", "blue"]);
+ assert_eq!(category.map(&category.get(&"red").unwrap(), (0, 8)), 2);
+ assert_eq!(category.map(&category.get(&"green").unwrap(), (0, 8)), 4);
+ assert_eq!(category.map(&category.get(&"blue").unwrap(), (0, 8)), 6);
+ assert_eq!(category.key_points(3).len(), 3);
+ assert_eq!(category.key_points(5).len(), 3);
+ }
+}
diff --git a/src/coord/datetime.rs b/src/coord/datetime.rs
new file mode 100644
index 0000000..cb96f93
--- /dev/null
+++ b/src/coord/datetime.rs
@@ -0,0 +1,954 @@
+/// The datetime coordinates
+use chrono::{Date, DateTime, Datelike, Duration, NaiveTime, TimeZone, Timelike};
+use std::ops::Range;
+
+use super::{AsRangedCoord, DiscreteRanged, Ranged};
+
+/// The trait that describe some time value
+pub trait TimeValue: Eq {
+ type Tz: TimeZone;
+ /// Returns the date that is no later than the time
+ fn date_floor(&self) -> Date<Self::Tz>;
+ /// Returns the date that is no earlier than the time
+ fn date_ceil(&self) -> Date<Self::Tz>;
+ /// Returns the maximum value that is earlier than the given date
+ fn earliest_after_date(date: Date<Self::Tz>) -> Self;
+ /// Returns the duration between two time value
+ fn subtract(&self, other: &Self) -> Duration;
+ /// Get the timezone information for current value
+ fn timezone(&self) -> Self::Tz;
+
+ /// Map the coord
+ fn map_coord(value: &Self, begin: &Self, end: &Self, limit: (i32, i32)) -> i32 {
+ let total_span = end.subtract(begin);
+ let value_span = value.subtract(begin);
+
+ // First, lets try the nanoseconds precision
+ if let Some(total_ns) = total_span.num_nanoseconds() {
+ if let Some(value_ns) = value_span.num_nanoseconds() {
+ return (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64) as i32
+ + limit.0;
+ }
+ }
+
+ // If it overflows, it means we have a time span nearly 300 years, we are safe to ignore the
+ // portion less than 1 day.
+ let total_days = total_span.num_days() as f64;
+ let value_days = value_span.num_days() as f64;
+
+ (f64::from(limit.1 - limit.0) * value_days / total_days) as i32 + limit.0
+ }
+}
+
+impl<Z: TimeZone> TimeValue for Date<Z> {
+ type Tz = Z;
+ fn date_floor(&self) -> Date<Z> {
+ self.clone()
+ }
+ fn date_ceil(&self) -> Date<Z> {
+ self.clone()
+ }
+ fn earliest_after_date(date: Date<Z>) -> Self {
+ date
+ }
+ fn subtract(&self, other: &Date<Z>) -> Duration {
+ self.clone() - other.clone()
+ }
+ fn timezone(&self) -> Self::Tz {
+ self.timezone()
+ }
+}
+
+impl<Z: TimeZone> TimeValue for DateTime<Z> {
+ type Tz = Z;
+ fn date_floor(&self) -> Date<Z> {
+ self.date()
+ }
+ fn date_ceil(&self) -> Date<Z> {
+ if self.time().num_seconds_from_midnight() > 0 {
+ self.date() + Duration::days(1)
+ } else {
+ self.date()
+ }
+ }
+ fn earliest_after_date(date: Date<Z>) -> DateTime<Z> {
+ date.and_hms(0, 0, 0)
+ }
+
+ fn subtract(&self, other: &DateTime<Z>) -> Duration {
+ self.clone() - other.clone()
+ }
+ fn timezone(&self) -> Self::Tz {
+ self.timezone()
+ }
+}
+
+/// The ranged coordinate for date
+#[derive(Clone)]
+pub struct RangedDate<Z: TimeZone>(Date<Z>, Date<Z>);
+
+impl<Z: TimeZone> From<Range<Date<Z>>> for RangedDate<Z> {
+ fn from(range: Range<Date<Z>>) -> Self {
+ Self(range.start, range.end)
+ }
+}
+
+impl<Z: TimeZone> Ranged for RangedDate<Z> {
+ type ValueType = Date<Z>;
+
+ fn range(&self) -> Range<Date<Z>> {
+ self.0.clone()..self.1.clone()
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ TimeValue::map_coord(value, &self.0, &self.1, limit)
+ }
+
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ let mut ret = vec![];
+
+ let total_days = (self.1.clone() - self.0.clone()).num_days();
+ let total_weeks = (self.1.clone() - self.0.clone()).num_weeks();
+
+ if total_days > 0 && total_days as usize <= max_points {
+ for day_idx in 0..=total_days {
+ ret.push(self.0.clone() + Duration::days(day_idx));
+ }
+ return ret;
+ }
+
+ if total_weeks > 0 && total_weeks as usize <= max_points {
+ for day_idx in 0..=total_weeks {
+ ret.push(self.0.clone() + Duration::weeks(day_idx));
+ }
+ return ret;
+ }
+
+ let week_per_point = ((total_weeks as f64) / (max_points as f64)).ceil() as usize;
+
+ for idx in 0..=(total_weeks as usize / week_per_point) {
+ ret.push(self.0.clone() + Duration::weeks((idx * week_per_point) as i64));
+ }
+
+ ret
+ }
+}
+
+impl<Z: TimeZone> DiscreteRanged for RangedDate<Z> {
+ type RangeParameter = ();
+ fn get_range_parameter(&self) {}
+ fn next_value(this: &Date<Z>, _: &()) -> Date<Z> {
+ this.clone() + Duration::days(1)
+ }
+
+ fn previous_value(this: &Date<Z>, _: &()) -> Date<Z> {
+ this.clone() - Duration::days(1)
+ }
+}
+
+impl<Z: TimeZone> AsRangedCoord for Range<Date<Z>> {
+ type CoordDescType = RangedDate<Z>;
+ type Value = Date<Z>;
+}
+
+/// Indicates the coord has a monthly resolution
+///
+/// Note: since month doesn't have a constant duration.
+/// We can't use a simple granularity to describe it. Thus we have
+/// this axis decorator to make it yield monthly key-points.
+#[derive(Clone)]
+pub struct Monthly<T: TimeValue>(Range<T>);
+
+impl<T: TimeValue + Clone> AsRangedCoord for Monthly<T> {
+ type CoordDescType = Monthly<T>;
+ type Value = T;
+}
+
+impl<T: TimeValue + Clone> Ranged for Monthly<T> {
+ type ValueType = T;
+
+ fn range(&self) -> Range<T> {
+ self.0.start.clone()..self.0.end.clone()
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ T::map_coord(value, &self.0.start, &self.0.end, limit)
+ }
+
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ let start_date = self.0.start.date_ceil();
+ let end_date = self.0.end.date_floor();
+
+ let mut start_year = start_date.year();
+ let mut start_month = start_date.month();
+ let start_day = start_date.day();
+
+ let end_year = end_date.year();
+ let end_month = end_date.month();
+
+ if start_day != 1 {
+ start_month += 1;
+ if start_month == 13 {
+ start_month = 1;
+ start_year += 1;
+ }
+ }
+
+ let total_month = (end_year - start_year) * 12 + end_month as i32 - start_month as i32;
+
+ fn generate_key_points<T: TimeValue>(
+ mut start_year: i32,
+ mut start_month: i32,
+ end_year: i32,
+ end_month: i32,
+ step: u32,
+ tz: T::Tz,
+ ) -> Vec<T> {
+ let mut ret = vec![];
+ while end_year > start_year || (end_year == start_year && end_month >= start_month) {
+ ret.push(T::earliest_after_date(tz.ymd(
+ start_year,
+ start_month as u32,
+ 1,
+ )));
+ start_month += step as i32;
+
+ if start_month >= 13 {
+ start_year += start_month / 12;
+ start_month %= 12;
+ }
+ }
+
+ ret
+ }
+
+ if total_month as usize <= max_points {
+ // Monthly
+ return generate_key_points(
+ start_year,
+ start_month as i32,
+ end_year,
+ end_month as i32,
+ 1,
+ self.0.start.timezone(),
+ );
+ } else if total_month as usize <= max_points * 3 {
+ // Quarterly
+ return generate_key_points(
+ start_year,
+ start_month as i32,
+ end_year,
+ end_month as i32,
+ 3,
+ self.0.start.timezone(),
+ );
+ } else if total_month as usize <= max_points * 6 {
+ // Biyearly
+ return generate_key_points(
+ start_year,
+ start_month as i32,
+ end_year,
+ end_month as i32,
+ 6,
+ self.0.start.timezone(),
+ );
+ }
+
+ // Otherwise we could generate the yearly keypoints
+ generate_yearly_keypoints(
+ max_points,
+ start_year,
+ start_month,
+ end_year,
+ end_month,
+ self.0.start.timezone(),
+ )
+ }
+}
+
+impl<T: TimeValue + Clone> DiscreteRanged for Monthly<T> {
+ type RangeParameter = ();
+ fn get_range_parameter(&self) {}
+ fn next_value(this: &T, _: &()) -> T {
+ let mut year = this.date_ceil().year();
+ let mut month = this.date_ceil().month();
+ month += 1;
+ if month == 13 {
+ month = 1;
+ year += 1;
+ }
+ T::earliest_after_date(this.timezone().ymd(year, month, this.date_ceil().day()))
+ }
+
+ fn previous_value(this: &T, _: &()) -> T {
+ let mut year = this.clone().date_floor().year();
+ let mut month = this.clone().date_floor().month();
+ month -= 1;
+ if month == 0 {
+ month = 12;
+ year -= 1;
+ }
+ T::earliest_after_date(this.timezone().ymd(year, month, this.date_floor().day()))
+ }
+}
+
+/// Indicate the coord has a yearly granularity.
+#[derive(Clone)]
+pub struct Yearly<T: TimeValue>(Range<T>);
+
+impl<T: TimeValue + Clone> AsRangedCoord for Yearly<T> {
+ type CoordDescType = Yearly<T>;
+ type Value = T;
+}
+
+fn generate_yearly_keypoints<T: TimeValue>(
+ max_points: usize,
+ mut start_year: i32,
+ start_month: u32,
+ mut end_year: i32,
+ end_month: u32,
+ tz: T::Tz,
+) -> Vec<T> {
+ if start_month > end_month {
+ end_year -= 1;
+ }
+
+ let mut exp10 = 1;
+
+ while (end_year - start_year + 1) as usize / (exp10 * 10) > max_points {
+ exp10 *= 10;
+ }
+
+ let mut freq = exp10;
+
+ for try_freq in &[1, 2, 5, 10] {
+ freq = *try_freq * exp10;
+ if (end_year - start_year + 1) as usize / (exp10 * *try_freq) <= max_points {
+ break;
+ }
+ }
+
+ let mut ret = vec![];
+
+ while start_year <= end_year {
+ ret.push(T::earliest_after_date(tz.ymd(start_year, start_month, 1)));
+ start_year += freq as i32;
+ }
+
+ ret
+}
+
+impl<T: TimeValue + Clone> Ranged for Yearly<T> {
+ type ValueType = T;
+
+ fn range(&self) -> Range<T> {
+ self.0.start.clone()..self.0.end.clone()
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ T::map_coord(value, &self.0.start, &self.0.end, limit)
+ }
+
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ let start_date = self.0.start.date_ceil();
+ let end_date = self.0.end.date_floor();
+
+ let mut start_year = start_date.year();
+ let mut start_month = start_date.month();
+ let start_day = start_date.day();
+
+ let end_year = end_date.year();
+ let end_month = end_date.month();
+
+ if start_day != 1 {
+ start_month += 1;
+ if start_month == 13 {
+ start_month = 1;
+ start_year += 1;
+ }
+ }
+
+ generate_yearly_keypoints(
+ max_points,
+ start_year,
+ start_month,
+ end_year,
+ end_month,
+ self.0.start.timezone(),
+ )
+ }
+}
+
+impl<T: TimeValue + Clone> DiscreteRanged for Yearly<T> {
+ type RangeParameter = ();
+ fn get_range_parameter(&self) {}
+ fn next_value(this: &T, _: &()) -> T {
+ T::earliest_after_date(this.timezone().ymd(this.date_floor().year() + 1, 1, 1))
+ }
+
+ fn previous_value(this: &T, _: &()) -> T {
+ T::earliest_after_date(this.timezone().ymd(this.date_ceil().year() - 1, 1, 1))
+ }
+}
+
+/// The trait that converts a normal date coord into a yearly one
+pub trait IntoMonthly<T: TimeValue> {
+ fn monthly(self) -> Monthly<T>;
+}
+
+/// The trait that converts a normal date coord into a yearly one
+pub trait IntoYearly<T: TimeValue> {
+ fn yearly(self) -> Yearly<T>;
+}
+
+impl<T: TimeValue> IntoMonthly<T> for Range<T> {
+ fn monthly(self) -> Monthly<T> {
+ Monthly(self)
+ }
+}
+
+impl<T: TimeValue> IntoYearly<T> for Range<T> {
+ fn yearly(self) -> Yearly<T> {
+ Yearly(self)
+ }
+}
+
+/// The ranged coordinate for the date and time
+#[derive(Clone)]
+pub struct RangedDateTime<Z: TimeZone>(DateTime<Z>, DateTime<Z>);
+
+impl<Z: TimeZone> AsRangedCoord for Range<DateTime<Z>> {
+ type CoordDescType = RangedDateTime<Z>;
+ type Value = DateTime<Z>;
+}
+
+impl<Z: TimeZone> From<Range<DateTime<Z>>> for RangedDateTime<Z> {
+ fn from(range: Range<DateTime<Z>>) -> Self {
+ Self(range.start, range.end)
+ }
+}
+
+impl<Z: TimeZone> Ranged for RangedDateTime<Z> {
+ type ValueType = DateTime<Z>;
+
+ fn range(&self) -> Range<DateTime<Z>> {
+ self.0.clone()..self.1.clone()
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ TimeValue::map_coord(value, &self.0, &self.1, limit)
+ }
+
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ let total_span = self.1.clone() - self.0.clone();
+
+ if let Some(total_ns) = total_span.num_nanoseconds() {
+ if let Some(actual_ns_per_point) =
+ compute_period_per_point(total_ns as u64, max_points, true)
+ {
+ let start_time_ns = u64::from(self.0.time().num_seconds_from_midnight())
+ * 1_000_000_000
+ + u64::from(self.0.time().nanosecond());
+
+ let mut start_time = self
+ .0
+ .date_floor()
+ .and_time(
+ NaiveTime::from_hms(0, 0, 0)
+ + Duration::nanoseconds(if start_time_ns % actual_ns_per_point > 0 {
+ start_time_ns
+ + (actual_ns_per_point - start_time_ns % actual_ns_per_point)
+ } else {
+ start_time_ns
+ } as i64),
+ )
+ .unwrap();
+
+ let mut ret = vec![];
+
+ while start_time < self.1 {
+ ret.push(start_time.clone());
+ start_time = start_time + Duration::nanoseconds(actual_ns_per_point as i64);
+ }
+
+ return ret;
+ }
+ }
+
+ // Otherwise, it actually behaves like a date
+ let date_range = RangedDate(self.0.date_ceil(), self.1.date_floor());
+
+ date_range
+ .key_points(max_points)
+ .into_iter()
+ .map(|x| x.and_hms(0, 0, 0))
+ .collect()
+ }
+}
+
+/// The coordinate that for duration of time
+#[derive(Clone)]
+pub struct RangedDuration(Duration, Duration);
+
+impl AsRangedCoord for Range<Duration> {
+ type CoordDescType = RangedDuration;
+ type Value = Duration;
+}
+
+impl From<Range<Duration>> for RangedDuration {
+ fn from(range: Range<Duration>) -> Self {
+ Self(range.start, range.end)
+ }
+}
+
+impl Ranged for RangedDuration {
+ type ValueType = Duration;
+
+ fn range(&self) -> Range<Duration> {
+ self.0..self.1
+ }
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ let total_span = self.1 - self.0;
+ let value_span = *value - self.0;
+
+ if let Some(total_ns) = total_span.num_nanoseconds() {
+ if let Some(value_ns) = value_span.num_nanoseconds() {
+ return limit.0
+ + (f64::from(limit.1 - limit.0) * value_ns as f64 / total_ns as f64 + 1e-10)
+ as i32;
+ }
+ return limit.1;
+ }
+
+ let total_days = total_span.num_days();
+ let value_days = value_span.num_days();
+
+ limit.0
+ + (f64::from(limit.1 - limit.0) * value_days as f64 / total_days as f64 + 1e-10) as i32
+ }
+
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ let total_span = self.1 - self.0;
+
+ if let Some(total_ns) = total_span.num_nanoseconds() {
+ if let Some(period) = compute_period_per_point(total_ns as u64, max_points, false) {
+ let mut start_ns = self.0.num_nanoseconds().unwrap();
+
+ if start_ns as u64 % period > 0 {
+ if start_ns > 0 {
+ start_ns += period as i64 - (start_ns % period as i64);
+ } else {
+ start_ns -= start_ns % period as i64;
+ }
+ }
+
+ let mut current = Duration::nanoseconds(start_ns);
+ let mut ret = vec![];
+
+ while current < self.1 {
+ ret.push(current);
+ current = current + Duration::nanoseconds(period as i64);
+ }
+
+ return ret;
+ }
+ }
+
+ let begin_days = self.0.num_days();
+ let end_days = self.1.num_days();
+
+ let mut days_per_tick = 1;
+ let mut idx = 0;
+ const MULTIPLIER: &[i32] = &[1, 2, 5];
+
+ while (end_days - begin_days) / i64::from(days_per_tick * MULTIPLIER[idx])
+ > max_points as i64
+ {
+ idx += 1;
+ if idx == MULTIPLIER.len() {
+ idx = 0;
+ days_per_tick *= 10;
+ }
+ }
+
+ days_per_tick *= MULTIPLIER[idx];
+
+ let mut ret = vec![];
+
+ let mut current = Duration::days(
+ self.0.num_days()
+ + if Duration::days(self.0.num_days()) != self.0 {
+ 1
+ } else {
+ 0
+ },
+ );
+
+ while current < self.1 {
+ ret.push(current);
+ current = current + Duration::days(i64::from(days_per_tick));
+ }
+
+ ret
+ }
+}
+
+#[allow(clippy::inconsistent_digit_grouping)]
+fn compute_period_per_point(total_ns: u64, max_points: usize, sub_daily: bool) -> Option<u64> {
+ let min_ns_per_point = total_ns as f64 / max_points as f64;
+ let actual_ns_per_point: u64 = (10u64).pow((min_ns_per_point as f64).log10().floor() as u32);
+
+ fn determine_actual_ns_per_point(
+ total_ns: u64,
+ mut actual_ns_per_point: u64,
+ units: &[u64],
+ base: u64,
+ max_points: usize,
+ ) -> u64 {
+ let mut unit_per_point_idx = 0;
+ while total_ns / actual_ns_per_point > max_points as u64 * units[unit_per_point_idx] {
+ unit_per_point_idx += 1;
+ if unit_per_point_idx == units.len() {
+ unit_per_point_idx = 0;
+ actual_ns_per_point *= base;
+ }
+ }
+ units[unit_per_point_idx] * actual_ns_per_point
+ }
+
+ if actual_ns_per_point < 1_000_000_000 {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ actual_ns_per_point,
+ &[1, 2, 5],
+ 10,
+ max_points,
+ ))
+ } else if actual_ns_per_point < 3600_000_000_000 {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ 1_000_000_000,
+ &[1, 2, 5, 10, 15, 20, 30],
+ 60,
+ max_points,
+ ))
+ } else if actual_ns_per_point < 3600_000_000_000 * 24 {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ 3600_000_000_000,
+ &[1, 2, 4, 8, 12],
+ 24,
+ max_points,
+ ))
+ } else if !sub_daily {
+ if actual_ns_per_point < 3600_000_000_000 * 24 * 10 {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ 3600_000_000_000 * 24,
+ &[1, 2, 5, 7],
+ 10,
+ max_points,
+ ))
+ } else {
+ Some(determine_actual_ns_per_point(
+ total_ns as u64,
+ 3600_000_000_000 * 24 * 10,
+ &[1, 2, 5],
+ 10,
+ max_points,
+ ))
+ }
+ } else {
+ None
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use chrono::{TimeZone, Utc};
+
+ #[test]
+ fn test_date_range_long() {
+ let range = Utc.ymd(1000, 1, 1)..Utc.ymd(2999, 1, 1);
+
+ let ranged_coord = Into::<RangedDate<_>>::into(range);
+
+ assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0);
+ assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100);
+
+ let kps = ranged_coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .min()
+ .unwrap();
+ assert_eq!(max, min);
+ assert_eq!(max % 7, 0);
+ }
+
+ #[test]
+ fn test_date_range_short() {
+ let range = Utc.ymd(2019, 1, 1)..Utc.ymd(2019, 1, 21);
+ let ranged_coord = Into::<RangedDate<_>>::into(range);
+
+ let kps = ranged_coord.key_points(4);
+
+ assert_eq!(kps.len(), 3);
+
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .min()
+ .unwrap();
+ assert_eq!(max, min);
+ assert_eq!(max, 7);
+
+ let kps = ranged_coord.key_points(30);
+ assert_eq!(kps.len(), 21);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .min()
+ .unwrap();
+ assert_eq!(max, min);
+ assert_eq!(max, 1);
+ }
+
+ #[test]
+ fn test_yearly_date_range() {
+ let range = Utc.ymd(1000, 8, 5)..Utc.ymd(2999, 1, 1);
+ let ranged_coord = range.yearly();
+
+ assert_eq!(ranged_coord.map(&Utc.ymd(1000, 8, 10), (0, 100)), 0);
+ assert_eq!(ranged_coord.map(&Utc.ymd(2999, 8, 10), (0, 100)), 100);
+
+ let kps = ranged_coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_days())
+ .min()
+ .unwrap();
+ assert!(max != min);
+
+ assert!(kps.into_iter().all(|x| x.month() == 9 && x.day() == 1));
+
+ let range = Utc.ymd(2019, 8, 5)..Utc.ymd(2020, 1, 1);
+ let ranged_coord = range.yearly();
+ let kps = ranged_coord.key_points(23);
+ assert!(kps.len() == 1);
+ }
+
+ #[test]
+ fn test_monthly_date_range() {
+ let range = Utc.ymd(2019, 8, 5)..Utc.ymd(2020, 9, 1);
+ let ranged_coord = range.monthly();
+
+ let kps = ranged_coord.key_points(15);
+
+ assert!(kps.len() <= 15);
+ assert!(kps.iter().all(|x| x.day() == 1));
+ assert!(kps.into_iter().any(|x| x.month() != 9));
+
+ let kps = ranged_coord.key_points(5);
+ assert!(kps.len() <= 5);
+ assert!(kps.iter().all(|x| x.day() == 1));
+ let kps: Vec<_> = kps.into_iter().map(|x| x.month()).collect();
+ assert_eq!(kps, vec![9, 12, 3, 6, 9]);
+
+ // TODO: Investigate why max_point = 1 breaks the contract
+ let kps = ranged_coord.key_points(3);
+ assert!(kps.len() == 3);
+ assert!(kps.iter().all(|x| x.day() == 1));
+ let kps: Vec<_> = kps.into_iter().map(|x| x.month()).collect();
+ assert_eq!(kps, vec![9, 3, 9]);
+ }
+
+ #[test]
+ fn test_datetime_long_range() {
+ let coord: RangedDateTime<_> =
+ (Utc.ymd(1000, 1, 1).and_hms(0, 0, 0)..Utc.ymd(3000, 1, 1).and_hms(0, 0, 0)).into();
+
+ assert_eq!(
+ coord.map(&Utc.ymd(1000, 1, 1).and_hms(0, 0, 0), (0, 100)),
+ 0
+ );
+ assert_eq!(
+ coord.map(&Utc.ymd(3000, 1, 1).and_hms(0, 0, 0), (0, 100)),
+ 100
+ );
+
+ let kps = coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert!(max % (24 * 3600 * 7) == 0);
+ }
+
+ #[test]
+ fn test_datetime_medium_range() {
+ let coord: RangedDateTime<_> =
+ (Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)..Utc.ymd(2019, 1, 11).and_hms(0, 0, 0)).into();
+
+ let kps = coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert_eq!(max, 12 * 3600);
+ }
+
+ #[test]
+ fn test_datetime_short_range() {
+ let coord: RangedDateTime<_> =
+ (Utc.ymd(2019, 1, 1).and_hms(0, 0, 0)..Utc.ymd(2019, 1, 2).and_hms(0, 0, 0)).into();
+
+ let kps = coord.key_points(50);
+
+ assert!(kps.len() <= 50);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert_eq!(max, 1800);
+ }
+
+ #[test]
+ fn test_datetime_nano_range() {
+ let start = Utc.ymd(2019, 1, 1).and_hms(0, 0, 0);
+ let end = start.clone() + Duration::nanoseconds(100);
+ let coord: RangedDateTime<_> = (start..end).into();
+
+ let kps = coord.key_points(50);
+
+ assert!(kps.len() <= 50);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_nanoseconds().unwrap())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_nanoseconds().unwrap())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert_eq!(max, 2);
+ }
+
+ #[test]
+ fn test_duration_long_range() {
+ let coord: RangedDuration = (Duration::days(-1000000)..Duration::days(1000000)).into();
+
+ assert_eq!(coord.map(&Duration::days(-1000000), (0, 100)), 0);
+ assert_eq!(coord.map(&Duration::days(1000000), (0, 100)), 100);
+
+ let kps = coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert!(max % (24 * 3600 * 10000) == 0);
+ }
+
+ #[test]
+ fn test_duration_daily_range() {
+ let coord: RangedDuration = (Duration::days(0)..Duration::hours(25)).into();
+
+ let kps = coord.key_points(23);
+
+ assert!(kps.len() <= 23);
+ let max = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .max()
+ .unwrap();
+ let min = kps
+ .iter()
+ .zip(kps.iter().skip(1))
+ .map(|(p, n)| (*n - *p).num_seconds())
+ .min()
+ .unwrap();
+ assert!(max == min);
+ assert_eq!(max, 3600 * 2);
+ }
+}
diff --git a/src/coord/logarithmic.rs b/src/coord/logarithmic.rs
new file mode 100644
index 0000000..a651013
--- /dev/null
+++ b/src/coord/logarithmic.rs
@@ -0,0 +1,148 @@
+use super::{AsRangedCoord, Ranged, RangedCoordf64};
+use std::marker::PhantomData;
+use std::ops::Range;
+
+/// The trait for the type that is able to be presented in the log scale
+pub trait LogScalable: Clone {
+ /// Make the conversion from the type to the floating point number
+ fn as_f64(&self) -> f64;
+ /// Convert a floating point number to the scale
+ fn from_f64(f: f64) -> Self;
+}
+
+macro_rules! impl_log_scalable {
+ (i, $t:ty) => {
+ impl LogScalable for $t {
+ fn as_f64(&self) -> f64 {
+ if *self != 0 {
+ return *self as f64;
+ }
+ // If this is an integer, we should allow zero point to be shown
+ // on the chart, thus we can't map the zero point to inf.
+ // So we just assigning a value smaller than 1 as the alternative
+ // of the zero point.
+ return 0.5;
+ }
+ fn from_f64(f: f64) -> $t {
+ f.round() as $t
+ }
+ }
+ };
+ (f, $t:ty) => {
+ impl LogScalable for $t {
+ fn as_f64(&self) -> f64 {
+ *self as f64
+ }
+ fn from_f64(f: f64) -> $t {
+ f as $t
+ }
+ }
+ };
+}
+
+impl_log_scalable!(i, u8);
+impl_log_scalable!(i, u16);
+impl_log_scalable!(i, u32);
+impl_log_scalable!(i, u64);
+impl_log_scalable!(f, f32);
+impl_log_scalable!(f, f64);
+
+/// The decorator type for a range of a log-scaled value
+pub struct LogRange<V: LogScalable>(pub Range<V>);
+
+impl<V: LogScalable + Clone> Clone for LogRange<V> {
+ fn clone(&self) -> Self {
+ Self(self.0.clone())
+ }
+}
+
+impl<V: LogScalable> From<LogRange<V>> for LogCoord<V> {
+ fn from(range: LogRange<V>) -> LogCoord<V> {
+ LogCoord {
+ linear: (range.0.start.as_f64().ln()..range.0.end.as_f64().ln()).into(),
+ logic: range.0,
+ marker: PhantomData,
+ }
+ }
+}
+
+impl<V: LogScalable> AsRangedCoord for LogRange<V> {
+ type CoordDescType = LogCoord<V>;
+ type Value = V;
+}
+
+/// A log scaled coordinate axis
+pub struct LogCoord<V: LogScalable> {
+ linear: RangedCoordf64,
+ logic: Range<V>,
+ marker: PhantomData<V>,
+}
+
+impl<V: LogScalable> Ranged for LogCoord<V> {
+ type ValueType = V;
+
+ fn map(&self, value: &V, limit: (i32, i32)) -> i32 {
+ let value = value.as_f64();
+ let value = value.max(self.logic.start.as_f64()).ln();
+ self.linear.map(&value, limit)
+ }
+
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ let tier_1 = (self.logic.end.as_f64() / self.logic.start.as_f64())
+ .log10()
+ .abs()
+ .floor()
+ .max(1.0) as usize;
+
+ let tier_2_density = if max_points < tier_1 {
+ 0
+ } else {
+ let density = 1 + (max_points - tier_1) / tier_1;
+ let mut exp = 1;
+ while exp * 10 <= density {
+ exp *= 10;
+ }
+ exp - 1
+ };
+
+ let mut multiplier = 10.0;
+ let mut cnt = 1;
+ while max_points < tier_1 / cnt {
+ multiplier *= 10.0;
+ cnt += 1;
+ }
+
+ let mut ret = vec![];
+ let mut val = (10f64).powf(self.logic.start.as_f64().log10().ceil());
+
+ while val <= self.logic.end.as_f64() {
+ ret.push(V::from_f64(val));
+ for i in 1..=tier_2_density {
+ let v = val
+ * (1.0
+ + multiplier / f64::from(tier_2_density as u32 + 1) * f64::from(i as u32));
+ if v > self.logic.end.as_f64() {
+ break;
+ }
+ ret.push(V::from_f64(v));
+ }
+ val *= multiplier;
+ }
+
+ ret
+ }
+
+ fn range(&self) -> Range<V> {
+ self.logic.clone()
+ }
+}
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[test]
+ fn regression_test_issue_143() {
+ let range: LogCoord<f64> = LogRange(1.0..5.0).into();
+
+ range.key_points(100);
+ }
+}
diff --git a/src/coord/mod.rs b/src/coord/mod.rs
new file mode 100644
index 0000000..0afafa3
--- /dev/null
+++ b/src/coord/mod.rs
@@ -0,0 +1,120 @@
+/*!
+Coordinate system abstractions.
+
+Coordinate systems can be attached to drawing areas. By doing so,
+the drawing area can draw elements in the guest coordinate system.
+`DrawingArea::apply_coord_spec` is used to attach new coordinate system
+to the drawing area.
+
+`CoordTranslate` is the trait required by `DrawingArea::apply_coord_spec`. It provides
+the forward coordinate translation: from the logic coordinate to the pixel-based absolute
+backend coordinate system.
+
+When the coordinate type implements `ReverseCoordTranslate`,
+the backward translation is possible, which allows mapping pixel-based coordinate into
+the logic coordinate. It's not usually used for static figure rendering, but may be useful
+for a interactive figure.
+
+`RangedCoord` is the 2D cartesian coordinate system that has two `Ranged` axis.
+A ranged axis can be logarithmic and by applying an logarithmic axis, the figure is logarithmic scale.
+Also, the ranged axis can be deserted, and this is required by the histogram series.
+
+*/
+use crate::drawing::backend::BackendCoord;
+
+mod category;
+#[cfg(feature = "chrono")]
+mod datetime;
+mod logarithmic;
+mod numeric;
+mod ranged;
+
+#[cfg(feature = "chrono")]
+pub use datetime::{IntoMonthly, IntoYearly, RangedDate, RangedDateTime, RangedDuration};
+pub use numeric::{
+ RangedCoordf32, RangedCoordf64, RangedCoordi128, RangedCoordi32, RangedCoordi64,
+ RangedCoordu128, RangedCoordu32, RangedCoordu64,
+};
+pub use ranged::{
+ AsRangedCoord, DiscreteRanged, IntoCentric, IntoPartialAxis, MeshLine, Ranged, RangedCoord,
+ ReversibleRanged,
+};
+
+pub use ranged::make_partial_axis;
+
+pub use logarithmic::{LogCoord, LogRange, LogScalable};
+
+pub use numeric::group_integer_by::{GroupBy, ToGroupByRange};
+use std::rc::Rc;
+use std::sync::Arc;
+
+pub use category::Category;
+
+/// The trait that translates some customized object to the backend coordinate
+pub trait CoordTranslate {
+ type From;
+
+ /// Translate the guest coordinate to the guest coordinate
+ fn translate(&self, from: &Self::From) -> BackendCoord;
+}
+
+impl<T: CoordTranslate> CoordTranslate for Rc<T> {
+ type From = T::From;
+
+ fn translate(&self, from: &Self::From) -> BackendCoord {
+ self.as_ref().translate(from)
+ }
+}
+
+impl<T: CoordTranslate> CoordTranslate for Arc<T> {
+ type From = T::From;
+
+ fn translate(&self, from: &Self::From) -> BackendCoord {
+ self.as_ref().translate(from)
+ }
+}
+
+/// The trait indicates that the coordinate system supports reverse transform
+/// This is useful when we need an interactive plot, thus we need to map the event
+/// from the backend coordinate to the logical coordinate
+pub trait ReverseCoordTranslate: CoordTranslate {
+ /// Reverse translate the coordinate from the drawing coordinate to the
+ /// logic coordinate.
+ /// Note: the return value is an option, because it's possible that the drawing
+ /// coordinate isn't able to be represented in te guest coordinate system
+ fn reverse_translate(&self, input: BackendCoord) -> Option<Self::From>;
+}
+
+/// The coordinate translation that only impose shift
+#[derive(Debug, Clone)]
+pub struct Shift(pub BackendCoord);
+
+impl CoordTranslate for Shift {
+ type From = BackendCoord;
+ fn translate(&self, from: &Self::From) -> BackendCoord {
+ (from.0 + (self.0).0, from.1 + (self.0).1)
+ }
+}
+
+impl ReverseCoordTranslate for Shift {
+ fn reverse_translate(&self, input: BackendCoord) -> Option<BackendCoord> {
+ Some((input.0 - (self.0).0, input.1 - (self.0).1))
+ }
+}
+
+/// We can compose an arbitrary transformation with a shift
+pub struct ShiftAndTrans<T: CoordTranslate>(Shift, T);
+
+impl<T: CoordTranslate> CoordTranslate for ShiftAndTrans<T> {
+ type From = T::From;
+ fn translate(&self, from: &Self::From) -> BackendCoord {
+ let temp = self.1.translate(from);
+ self.0.translate(&temp)
+ }
+}
+
+impl<T: ReverseCoordTranslate> ReverseCoordTranslate for ShiftAndTrans<T> {
+ fn reverse_translate(&self, input: BackendCoord) -> Option<T::From> {
+ Some(self.1.reverse_translate(self.0.reverse_translate(input)?)?)
+ }
+}
diff --git a/src/coord/numeric.rs b/src/coord/numeric.rs
new file mode 100644
index 0000000..6a9f72d
--- /dev/null
+++ b/src/coord/numeric.rs
@@ -0,0 +1,395 @@
+use std::ops::Range;
+
+use super::{AsRangedCoord, DiscreteRanged, Ranged, ReversibleRanged};
+
+macro_rules! impl_discrete_trait {
+ ($name:ident) => {
+ impl DiscreteRanged for $name {
+ type RangeParameter = ();
+ fn get_range_parameter(&self) -> () {}
+ fn next_value(this: &Self::ValueType, _: &()) -> Self::ValueType {
+ return *this + 1;
+ }
+ fn previous_value(this: &Self::ValueType, _: &()) -> Self::ValueType {
+ return *this - 1;
+ }
+ }
+ };
+}
+
+macro_rules! impl_ranged_type_trait {
+ ($value:ty, $coord:ident) => {
+ impl AsRangedCoord for Range<$value> {
+ type CoordDescType = $coord;
+ type Value = $value;
+ }
+ };
+}
+
+macro_rules! make_numeric_coord {
+ ($type:ty, $name:ident, $key_points:ident, $doc: expr) => {
+ #[doc = $doc]
+ #[derive(Clone)]
+ pub struct $name($type, $type);
+ impl From<Range<$type>> for $name {
+ fn from(range: Range<$type>) -> Self {
+ return $name(range.start, range.end);
+ }
+ }
+ impl Ranged for $name {
+ type ValueType = $type;
+ fn map(&self, v: &$type, limit: (i32, i32)) -> i32 {
+ let logic_length = (*v - self.0) as f64 / (self.1 - self.0) as f64;
+ let actual_length = limit.1 - limit.0;
+
+ if actual_length == 0 {
+ return limit.1;
+ }
+
+ return limit.0 + (actual_length as f64 * logic_length + 1e-3).floor() as i32;
+ }
+ fn key_points(&self, max_points: usize) -> Vec<$type> {
+ $key_points((self.0, self.1), max_points)
+ }
+ fn range(&self) -> Range<$type> {
+ return self.0..self.1;
+ }
+ }
+
+ impl ReversibleRanged for $name {
+ fn unmap(&self, p:i32, (min,max): (i32, i32)) -> Option<$type> {
+ if p < min.min(max) || p > max.max(min) {
+ return None;
+ }
+
+ let logical_offset = (p - min) as f64 / (max - min) as f64;
+
+ return Some(((self.1 - self.0) as f64 * logical_offset + self.0 as f64) as $type);
+ }
+ }
+ };
+}
+
+macro_rules! gen_key_points_comp {
+ (float, $name:ident, $type:ty) => {
+ fn $name(range: ($type, $type), max_points: usize) -> Vec<$type> {
+ if max_points == 0 {
+ return vec![];
+ }
+
+ let range = (range.0 as f64, range.1 as f64);
+ let mut scale = (10f64).powf((range.1 - range.0).log(10.0).floor());
+ let mut digits = -(range.1 - range.0).log(10.0).floor() as i32 + 1;
+ fn rem_euclid(a: f64, b: f64) -> f64 {
+ if b > 0.0 {
+ a - (a / b).floor() * b
+ } else {
+ a - (a / b).ceil() * b
+ }
+ }
+
+ // At this point we need to make sure that the loop invariant:
+ // The scale must yield number of points than requested
+ if 1 + ((range.1 - range.0) / scale).floor() as usize > max_points {
+ scale *= 10.0;
+ }
+
+ 'outer: loop {
+ let old_scale = scale;
+ for nxt in [2.0, 5.0, 10.0].iter() {
+ let new_left = range.0 + scale / nxt - rem_euclid(range.0, scale / nxt);
+ let new_right = range.1 - rem_euclid(range.1, scale / nxt);
+
+ let npoints = 1 + ((new_right - new_left) / old_scale * nxt) as usize;
+
+ if npoints > max_points {
+ break 'outer;
+ }
+
+ scale = old_scale / nxt;
+ }
+ scale = old_scale / 10.0;
+ if scale < 1.0 {
+ digits += 1;
+ }
+ }
+
+ let mut ret = vec![];
+ let mut left = range.0 + scale - rem_euclid(range.0, scale);
+ let right = range.1 - rem_euclid(range.1, scale);
+ while left <= right {
+ let size = (10f64).powf(digits as f64 + 1.0);
+ let new_left = (left * size).abs() + 1e-3;
+ if left < 0.0 {
+ left = -new_left.round() / size;
+ } else {
+ left = new_left.round() / size;
+ }
+ ret.push(left as $type);
+ left += scale;
+ }
+ return ret;
+ }
+ };
+ (integer, $name:ident, $type:ty) => {
+ fn $name(range: ($type, $type), max_points: usize) -> Vec<$type> {
+ let mut scale: $type = 1;
+ let range = (range.0.min(range.1), range.0.max(range.1));
+ 'outer: while (range.1 - range.0 + scale - 1) as usize / (scale as usize) > max_points {
+ let next_scale = scale * 10;
+ for new_scale in [scale * 2, scale * 5, scale * 10].iter() {
+ scale = *new_scale;
+ if (range.1 - range.0 + *new_scale - 1) as usize / (*new_scale as usize)
+ < max_points
+ {
+ break 'outer;
+ }
+ }
+ scale = next_scale;
+ }
+
+ let (mut left, right) = (
+ range.0 + (scale - range.0 % scale) % scale,
+ range.1 - range.1 % scale,
+ );
+
+ let mut ret = vec![];
+ while left <= right {
+ ret.push(left as $type);
+ left += scale;
+ }
+
+ return ret;
+ }
+ };
+}
+
+gen_key_points_comp!(float, compute_f32_key_points, f32);
+gen_key_points_comp!(float, compute_f64_key_points, f64);
+gen_key_points_comp!(integer, compute_i32_key_points, i32);
+gen_key_points_comp!(integer, compute_u32_key_points, u32);
+gen_key_points_comp!(integer, compute_i64_key_points, i64);
+gen_key_points_comp!(integer, compute_u64_key_points, u64);
+gen_key_points_comp!(integer, compute_i128_key_points, i128);
+gen_key_points_comp!(integer, compute_u128_key_points, u128);
+gen_key_points_comp!(integer, compute_isize_key_points, isize);
+gen_key_points_comp!(integer, compute_usize_key_points, usize);
+
+make_numeric_coord!(
+ f32,
+ RangedCoordf32,
+ compute_f32_key_points,
+ "The ranged coordinate for type f32"
+);
+make_numeric_coord!(
+ f64,
+ RangedCoordf64,
+ compute_f64_key_points,
+ "The ranged coordinate for type f64"
+);
+make_numeric_coord!(
+ u32,
+ RangedCoordu32,
+ compute_u32_key_points,
+ "The ranged coordinate for type u32"
+);
+make_numeric_coord!(
+ i32,
+ RangedCoordi32,
+ compute_i32_key_points,
+ "The ranged coordinate for type i32"
+);
+make_numeric_coord!(
+ u64,
+ RangedCoordu64,
+ compute_u64_key_points,
+ "The ranged coordinate for type u64"
+);
+make_numeric_coord!(
+ i64,
+ RangedCoordi64,
+ compute_i64_key_points,
+ "The ranged coordinate for type i64"
+);
+make_numeric_coord!(
+ u128,
+ RangedCoordu128,
+ compute_u128_key_points,
+ "The ranged coordinate for type u128"
+);
+make_numeric_coord!(
+ i128,
+ RangedCoordi128,
+ compute_i128_key_points,
+ "The ranged coordinate for type i128"
+);
+make_numeric_coord!(
+ usize,
+ RangedCoordusize,
+ compute_usize_key_points,
+ "The ranged coordinate for type usize"
+);
+make_numeric_coord!(
+ isize,
+ RangedCoordisize,
+ compute_isize_key_points,
+ "The ranged coordinate for type isize"
+);
+
+impl_discrete_trait!(RangedCoordu32);
+impl_discrete_trait!(RangedCoordi32);
+impl_discrete_trait!(RangedCoordu64);
+impl_discrete_trait!(RangedCoordi64);
+impl_discrete_trait!(RangedCoordu128);
+impl_discrete_trait!(RangedCoordi128);
+impl_discrete_trait!(RangedCoordusize);
+impl_discrete_trait!(RangedCoordisize);
+
+impl_ranged_type_trait!(f32, RangedCoordf32);
+impl_ranged_type_trait!(f64, RangedCoordf64);
+impl_ranged_type_trait!(i32, RangedCoordi32);
+impl_ranged_type_trait!(u32, RangedCoordu32);
+impl_ranged_type_trait!(i64, RangedCoordi64);
+impl_ranged_type_trait!(u64, RangedCoordu64);
+impl_ranged_type_trait!(i128, RangedCoordi128);
+impl_ranged_type_trait!(u128, RangedCoordu128);
+impl_ranged_type_trait!(isize, RangedCoordisize);
+impl_ranged_type_trait!(usize, RangedCoordusize);
+
+// TODO: Think about how to re-organize this part
+pub mod group_integer_by {
+ use super::Ranged;
+ use super::{AsRangedCoord, DiscreteRanged};
+ use num_traits::{FromPrimitive, PrimInt, ToPrimitive};
+ use std::ops::{Mul, Range};
+
+ /// The ranged value spec that needs to be grouped.
+ /// This is useful, for example, when we have an X axis is a integer and denotes days.
+ /// And we are expecting the tick mark denotes weeks, in this way we can make the range
+ /// spec grouping by 7 elements.
+ pub struct GroupBy<T>(T, T::ValueType)
+ where
+ T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
+ T: Ranged;
+
+ /// The trait that provides method `Self::group_by` function which creates a
+ /// `GroupBy` decorated ranged value.
+ pub trait ToGroupByRange
+ where
+ Self: AsRangedCoord,
+ <Self as AsRangedCoord>::Value: PrimInt + ToPrimitive + FromPrimitive + Mul,
+ <<Self as AsRangedCoord>::CoordDescType as Ranged>::ValueType:
+ PrimInt + ToPrimitive + FromPrimitive + Mul,
+ {
+ /// Make a grouping ranged value, see the documentation for `GroupBy` for details.
+ ///
+ /// - `value`: The number of values we want to group it
+ /// - **return**: The newly created grouping range sepcification
+ fn group_by(
+ self,
+ value: <<Self as AsRangedCoord>::CoordDescType as Ranged>::ValueType,
+ ) -> GroupBy<<Self as AsRangedCoord>::CoordDescType> {
+ GroupBy(self.into(), value)
+ }
+ }
+
+ impl<T> ToGroupByRange for T
+ where
+ Self: AsRangedCoord,
+ <Self as AsRangedCoord>::Value: PrimInt + FromPrimitive + ToPrimitive + Mul,
+ <<Self as AsRangedCoord>::CoordDescType as Ranged>::ValueType:
+ PrimInt + FromPrimitive + ToPrimitive + Mul,
+ {
+ }
+
+ impl<T> AsRangedCoord for GroupBy<T>
+ where
+ T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
+ T: Ranged,
+ {
+ type Value = T::ValueType;
+ type CoordDescType = Self;
+ }
+
+ impl<T> DiscreteRanged for GroupBy<T>
+ where
+ T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
+ T: Ranged + DiscreteRanged,
+ {
+ type RangeParameter = <T as DiscreteRanged>::RangeParameter;
+ fn get_range_parameter(&self) -> Self::RangeParameter {
+ self.0.get_range_parameter()
+ }
+ fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
+ <T as DiscreteRanged>::previous_value(this, param)
+ }
+ fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
+ <T as DiscreteRanged>::next_value(this, param)
+ }
+ }
+
+ impl<T> Ranged for GroupBy<T>
+ where
+ T::ValueType: PrimInt + ToPrimitive + FromPrimitive + Mul,
+ T: Ranged,
+ {
+ type ValueType = T::ValueType;
+ fn map(&self, value: &T::ValueType, limit: (i32, i32)) -> i32 {
+ self.0.map(value, limit)
+ }
+ fn range(&self) -> Range<T::ValueType> {
+ self.0.range()
+ }
+ fn key_points(&self, max_points: usize) -> Vec<T::ValueType> {
+ let actual_range = self.0.range();
+ let from = ((actual_range.start + self.1 - T::ValueType::from_u8(1).unwrap()) / self.1)
+ .to_isize()
+ .unwrap();
+ let to = (actual_range.end / self.1).to_isize().unwrap();
+ let logic_range: super::RangedCoordisize = (from..to).into();
+
+ logic_range
+ .key_points(max_points)
+ .into_iter()
+ .map(|x| T::ValueType::from_isize(x).unwrap() * self.1)
+ .collect()
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::coord::*;
+ #[test]
+ fn test_key_points() {
+ let kp = compute_i32_key_points((0, 999), 28);
+
+ assert!(kp.len() > 0);
+ assert!(kp.len() <= 28);
+
+ let kp = compute_f64_key_points((-1.2, 1.2), 1);
+ assert!(kp.len() == 1);
+
+ let kp = compute_f64_key_points((-1.2, 1.2), 0);
+ assert!(kp.len() == 0);
+ }
+
+ #[test]
+ fn test_linear_coord_map() {
+ let coord: RangedCoordu32 = (0..20).into();
+ assert_eq!(coord.key_points(11).len(), 11);
+ assert_eq!(coord.key_points(11)[0], 0);
+ assert_eq!(coord.key_points(11)[10], 20);
+ assert_eq!(coord.map(&5, (0, 100)), 25);
+
+ let coord: RangedCoordf32 = (0f32..20f32).into();
+ assert_eq!(coord.map(&5.0, (0, 100)), 25);
+ }
+
+ #[test]
+ fn test_linear_coord_system() {
+ let _coord =
+ RangedCoord::<RangedCoordu32, RangedCoordu32>::new(0..10, 0..10, (0..1024, 0..768));
+ }
+}
diff --git a/src/coord/ranged.rs b/src/coord/ranged.rs
new file mode 100644
index 0000000..2291854
--- /dev/null
+++ b/src/coord/ranged.rs
@@ -0,0 +1,397 @@
+use super::{CoordTranslate, ReverseCoordTranslate};
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use crate::style::ShapeStyle;
+
+use std::ops::Range;
+
+/// The trait that indicates we have a ordered and ranged value
+/// Which is used to describe the axis
+pub trait Ranged {
+ /// The type of this value
+ type ValueType;
+
+ /// This function maps the value to i32, which is the drawing coordinate
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32;
+
+ /// This function gives the key points that we can draw a grid based on this
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType>;
+
+ /// Get the range of this value
+ fn range(&self) -> Range<Self::ValueType>;
+
+ /// This function provides the on-axis part of its range
+ #[allow(clippy::range_plus_one)]
+ fn axis_pixel_range(&self, limit: (i32, i32)) -> Range<i32> {
+ if limit.0 < limit.1 {
+ limit.0..limit.1
+ } else {
+ (limit.1 + 1)..(limit.0 + 1)
+ }
+ }
+}
+
+/// The trait indicates the ranged value can be map reversely, which means
+/// an pixel-based coordinate is given, it's possible to figure out the underlying
+/// logic value.
+pub trait ReversibleRanged: Ranged {
+ fn unmap(&self, input: i32, limit: (i32, i32)) -> Option<Self::ValueType>;
+}
+
+/// The coordinate described by two ranged value
+pub struct RangedCoord<X: Ranged, Y: Ranged> {
+ logic_x: X,
+ logic_y: Y,
+ back_x: (i32, i32),
+ back_y: (i32, i32),
+}
+
+impl<X: Ranged + Clone, Y: Ranged + Clone> Clone for RangedCoord<X, Y> {
+ fn clone(&self) -> Self {
+ Self {
+ logic_x: self.logic_x.clone(),
+ logic_y: self.logic_y.clone(),
+ back_x: self.back_x,
+ back_y: self.back_y,
+ }
+ }
+}
+
+impl<X: Ranged, Y: Ranged> RangedCoord<X, Y> {
+ /// Create a new ranged value coordinate system
+ pub fn new<IntoX: Into<X>, IntoY: Into<Y>>(
+ logic_x: IntoX,
+ logic_y: IntoY,
+ actual: (Range<i32>, Range<i32>),
+ ) -> Self {
+ Self {
+ logic_x: logic_x.into(),
+ logic_y: logic_y.into(),
+ back_x: (actual.0.start, actual.0.end),
+ back_y: (actual.1.start, actual.1.end),
+ }
+ }
+
+ /// Draw the mesh for the coordinate system
+ pub fn draw_mesh<E, DrawMesh: FnMut(MeshLine<X, Y>) -> Result<(), E>>(
+ &self,
+ h_limit: usize,
+ v_limit: usize,
+ mut draw_mesh: DrawMesh,
+ ) -> Result<(), E> {
+ let (xkp, ykp) = (
+ self.logic_x.key_points(v_limit),
+ self.logic_y.key_points(h_limit),
+ );
+
+ for logic_x in xkp {
+ let x = self.logic_x.map(&logic_x, self.back_x);
+ draw_mesh(MeshLine::XMesh(
+ (x, self.back_y.0),
+ (x, self.back_y.1),
+ &logic_x,
+ ))?;
+ }
+
+ for logic_y in ykp {
+ let y = self.logic_y.map(&logic_y, self.back_y);
+ draw_mesh(MeshLine::YMesh(
+ (self.back_x.0, y),
+ (self.back_x.1, y),
+ &logic_y,
+ ))?;
+ }
+
+ Ok(())
+ }
+
+ /// Get the range of X axis
+ pub fn get_x_range(&self) -> Range<X::ValueType> {
+ self.logic_x.range()
+ }
+
+ /// Get the range of Y axis
+ pub fn get_y_range(&self) -> Range<Y::ValueType> {
+ self.logic_y.range()
+ }
+
+ pub fn get_x_axis_pixel_range(&self) -> Range<i32> {
+ self.logic_x.axis_pixel_range(self.back_x)
+ }
+
+ pub fn get_y_axis_pixel_range(&self) -> Range<i32> {
+ self.logic_y.axis_pixel_range(self.back_y)
+ }
+
+ pub fn x_spec(&self) -> &X {
+ &self.logic_x
+ }
+
+ pub fn y_spec(&self) -> &Y {
+ &self.logic_y
+ }
+}
+
+impl<X: Ranged, Y: Ranged> CoordTranslate for RangedCoord<X, Y> {
+ type From = (X::ValueType, Y::ValueType);
+
+ fn translate(&self, from: &Self::From) -> BackendCoord {
+ (
+ self.logic_x.map(&from.0, self.back_x),
+ self.logic_y.map(&from.1, self.back_y),
+ )
+ }
+}
+
+impl<X: ReversibleRanged, Y: ReversibleRanged> ReverseCoordTranslate for RangedCoord<X, Y> {
+ fn reverse_translate(&self, input: BackendCoord) -> Option<Self::From> {
+ Some((
+ self.logic_x.unmap(input.0, self.back_x)?,
+ self.logic_y.unmap(input.1, self.back_y)?,
+ ))
+ }
+}
+
+/// Represent a coordinate mesh for the two ranged value coordinate system
+pub enum MeshLine<'a, X: Ranged, Y: Ranged> {
+ XMesh(BackendCoord, BackendCoord, &'a X::ValueType),
+ YMesh(BackendCoord, BackendCoord, &'a Y::ValueType),
+}
+
+impl<'a, X: Ranged, Y: Ranged> MeshLine<'a, X, Y> {
+ /// Draw a single mesh line onto the backend
+ pub fn draw<DB: DrawingBackend>(
+ &self,
+ backend: &mut DB,
+ style: &ShapeStyle,
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ let (&left, &right) = match self {
+ MeshLine::XMesh(a, b, _) => (a, b),
+ MeshLine::YMesh(a, b, _) => (a, b),
+ };
+ backend.draw_line(left, right, style)
+ }
+}
+
+/// The trait indicates the coordinate is discrete, so that we can draw histogram on it
+pub trait DiscreteRanged
+where
+ Self: Ranged,
+{
+ type RangeParameter;
+
+ fn get_range_parameter(&self) -> Self::RangeParameter;
+
+ /// Get the smallest value that is larger than the `this` value
+ fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType;
+
+ /// Get the largest value that is smaller than `this` value
+ fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType;
+}
+
+/// The trait for the type that can be converted into a ranged coordinate axis
+pub trait AsRangedCoord: Sized {
+ type CoordDescType: Ranged<ValueType = Self::Value> + From<Self>;
+ type Value;
+}
+
+impl<T> AsRangedCoord for T
+where
+ T: Ranged,
+ Range<T::ValueType>: Into<T>,
+{
+ type CoordDescType = T;
+ type Value = T::ValueType;
+}
+
+/// The axis decorator that makes key-point in the center of the value range
+/// This is useful when we draw a histogram, since we want the axis value label
+/// to be shown in the middle of the range rather than exactly the location where
+/// the value mapped to.
+pub struct CentricDiscreteRange<D: DiscreteRanged>(D)
+where
+ <D as Ranged>::ValueType: Eq;
+
+/// The trait for types that can decorated by `CentricDiscreteRange` decorator
+pub trait IntoCentric: AsRangedCoord
+where
+ Self::CoordDescType: DiscreteRanged,
+ <Self::CoordDescType as Ranged>::ValueType: Eq,
+{
+ /// Convert current ranged value into a centric ranged value
+ fn into_centric(self) -> CentricDiscreteRange<Self::CoordDescType> {
+ CentricDiscreteRange(self.into())
+ }
+}
+
+impl<T: AsRangedCoord> IntoCentric for T
+where
+ T::CoordDescType: DiscreteRanged,
+ <Self::CoordDescType as Ranged>::ValueType: Eq,
+{
+}
+
+impl<D: DiscreteRanged + Clone> Clone for CentricDiscreteRange<D>
+where
+ <D as Ranged>::ValueType: Eq,
+{
+ fn clone(&self) -> Self {
+ Self(self.0.clone())
+ }
+}
+
+impl<D: DiscreteRanged> Ranged for CentricDiscreteRange<D>
+where
+ <D as Ranged>::ValueType: Eq,
+{
+ type ValueType = <D as Ranged>::ValueType;
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ let prev = <D as DiscreteRanged>::previous_value(&value, &self.0.get_range_parameter());
+ (self.0.map(&prev, limit) + self.0.map(value, limit)) / 2
+ }
+
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ self.0.key_points(max_points)
+ }
+
+ fn range(&self) -> Range<Self::ValueType> {
+ self.0.range()
+ }
+}
+
+impl<D: DiscreteRanged> DiscreteRanged for CentricDiscreteRange<D>
+where
+ <D as Ranged>::ValueType: Eq,
+{
+ type RangeParameter = <D as DiscreteRanged>::RangeParameter;
+ fn get_range_parameter(&self) -> Self::RangeParameter {
+ self.0.get_range_parameter()
+ }
+ fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
+ <D as DiscreteRanged>::next_value(this, param)
+ }
+
+ fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
+ <D as DiscreteRanged>::previous_value(this, param)
+ }
+}
+
+impl<D: DiscreteRanged> AsRangedCoord for CentricDiscreteRange<D>
+where
+ <D as Ranged>::ValueType: Eq,
+{
+ type CoordDescType = Self;
+ type Value = <Self as Ranged>::ValueType;
+}
+
+/// This axis decorator will make the axis partially display on the axis.
+/// At some time, we want the axis only covers some part of the value.
+/// This decorator will have an additional display range defined.
+pub struct PartialAxis<R: Ranged>(R, Range<R::ValueType>);
+
+/// The trait for the types that can be converted into a partial axis
+pub trait IntoPartialAxis: AsRangedCoord {
+ /// Make the partial axis
+ ///
+ /// - `axis_range`: The range of the axis to be displayed
+ /// - **returns**: The converted range specification
+ fn partial_axis(
+ self,
+ axis_range: Range<<Self::CoordDescType as Ranged>::ValueType>,
+ ) -> PartialAxis<Self::CoordDescType> {
+ PartialAxis(self.into(), axis_range)
+ }
+}
+
+impl<R: AsRangedCoord> IntoPartialAxis for R {}
+
+impl<R: Ranged + Clone> Clone for PartialAxis<R>
+where
+ <R as Ranged>::ValueType: Clone,
+{
+ fn clone(&self) -> Self {
+ PartialAxis(self.0.clone(), self.1.clone())
+ }
+}
+
+impl<R: Ranged> Ranged for PartialAxis<R>
+where
+ R::ValueType: Clone,
+{
+ type ValueType = R::ValueType;
+
+ fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
+ self.0.map(value, limit)
+ }
+
+ fn key_points(&self, max_points: usize) -> Vec<Self::ValueType> {
+ self.0.key_points(max_points)
+ }
+
+ fn range(&self) -> Range<Self::ValueType> {
+ self.0.range()
+ }
+
+ fn axis_pixel_range(&self, limit: (i32, i32)) -> Range<i32> {
+ let left = self.map(&self.1.start, limit);
+ let right = self.map(&self.1.end, limit);
+
+ left.min(right)..left.max(right)
+ }
+}
+
+impl<R: DiscreteRanged> DiscreteRanged for PartialAxis<R>
+where
+ R: Ranged,
+ <R as Ranged>::ValueType: Eq + Clone,
+{
+ type RangeParameter = <R as DiscreteRanged>::RangeParameter;
+ fn get_range_parameter(&self) -> Self::RangeParameter {
+ self.0.get_range_parameter()
+ }
+ fn next_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
+ <R as DiscreteRanged>::next_value(this, param)
+ }
+
+ fn previous_value(this: &Self::ValueType, param: &Self::RangeParameter) -> Self::ValueType {
+ <R as DiscreteRanged>::previous_value(this, param)
+ }
+}
+
+impl<R: Ranged> AsRangedCoord for PartialAxis<R>
+where
+ <R as Ranged>::ValueType: Clone,
+{
+ type CoordDescType = Self;
+ type Value = <Self as Ranged>::ValueType;
+}
+
+/// Make a partial axis based on the percentage of visible portion.
+/// We can use `into_partial_axis` to create a partial axis range specification.
+/// But sometimes, we want to directly specify the percentage visible to the user.
+///
+/// - `axis_range`: The range specification
+/// - `part`: The visible part of the axis. Each value is from [0.0, 1.0]
+/// - **returns**: The partial axis created from the input, or `None` when not possible
+pub fn make_partial_axis<T>(
+ axis_range: Range<T>,
+ part: Range<f64>,
+) -> Option<PartialAxis<<Range<T> as AsRangedCoord>::CoordDescType>>
+where
+ Range<T>: AsRangedCoord,
+ T: num_traits::NumCast + Clone,
+{
+ let left: f64 = num_traits::cast(axis_range.start.clone())?;
+ let right: f64 = num_traits::cast(axis_range.end.clone())?;
+
+ let full_range_size = (right - left) / (part.end - part.start);
+
+ let full_left = left - full_range_size * part.start;
+ let full_right = right + full_range_size * (1.0 - part.end);
+
+ let full_range: Range<T> = num_traits::cast(full_left)?..num_traits::cast(full_right)?;
+
+ let axis_range: <Range<T> as AsRangedCoord>::CoordDescType = axis_range.into();
+
+ Some(PartialAxis(full_range.into(), axis_range.range()))
+}
diff --git a/src/data/data_range.rs b/src/data/data_range.rs
new file mode 100644
index 0000000..445260b
--- /dev/null
+++ b/src/data/data_range.rs
@@ -0,0 +1,42 @@
+use std::cmp::{Ordering, PartialOrd};
+use std::iter::IntoIterator;
+use std::ops::Range;
+
+use num_traits::{One, Zero};
+
+/// Build a range that fits the data
+///
+/// - `iter`: the iterator over the data
+/// - **returns** The resulting range
+///
+/// ```rust
+/// use plotters::data::fitting_range;
+///
+/// let data = [4, 14, -2, 2, 5];
+/// let range = fitting_range(&data);
+/// assert_eq!(range, std::ops::Range { start: -2, end: 14 });
+/// ```
+pub fn fitting_range<'a, T: 'a, I: IntoIterator<Item = &'a T>>(iter: I) -> Range<T>
+where
+ T: Zero + One + PartialOrd + Clone,
+{
+ let (mut lb, mut ub) = (None, None);
+
+ for value in iter.into_iter() {
+ if let Some(Ordering::Greater) = lb
+ .as_ref()
+ .map_or(Some(Ordering::Greater), |lbv: &T| lbv.partial_cmp(value))
+ {
+ lb = Some(value.clone());
+ }
+
+ if let Some(Ordering::Less) = ub
+ .as_ref()
+ .map_or(Some(Ordering::Less), |ubv: &T| ubv.partial_cmp(value))
+ {
+ ub = Some(value.clone());
+ }
+ }
+
+ lb.unwrap_or_else(Zero::zero)..ub.unwrap_or_else(One::one)
+}
diff --git a/src/data/float.rs b/src/data/float.rs
new file mode 100644
index 0000000..6115876
--- /dev/null
+++ b/src/data/float.rs
@@ -0,0 +1,104 @@
+// The code that is related to float number handling
+
+fn find_minimal_repr(n: f64, eps: f64) -> (f64, usize) {
+ if eps >= 1.0 {
+ return (n, 0);
+ }
+ if n - n.floor() < eps {
+ (n.floor(), 0)
+ } else if n.ceil() - n < eps {
+ (n.ceil(), 0)
+ } else {
+ let (rem, pre) = find_minimal_repr((n - n.floor()) * 10.0, eps * 10.0);
+ (n.floor() + rem / 10.0, pre + 1)
+ }
+}
+
+fn float_to_string(n: f64, max_precision: usize) -> String {
+ let (sign, n) = if n < 0.0 { ("-", -n) } else { ("", n) };
+ let int_part = n.floor();
+
+ let dec_part =
+ ((n.abs() - int_part.abs()) * (10.0f64).powf(max_precision as f64)).round() as u64;
+
+ if dec_part == 0 || max_precision == 0 {
+ return format!("{}{:.0}", sign, int_part);
+ }
+
+ let mut leading = "".to_string();
+ let mut dec_result = format!("{}", dec_part);
+
+ for _ in 0..(max_precision - dec_result.len()) {
+ leading.push('0');
+ }
+
+ while let Some(c) = dec_result.pop() {
+ if c != '0' {
+ dec_result.push(c);
+ break;
+ }
+ }
+
+ format!("{}{:.0}.{}{}", sign, int_part, leading, dec_result)
+}
+
+/// The function that pretty prints the floating number
+/// Since rust doesn't have anything that can format a float with out appearance, so we just
+/// implemnet a float pretty printing function, which finds the shortest representation of a
+/// floating point number within the allowed error range.
+///
+/// - `n`: The float number to pretty-print
+/// - `allow_sn`: Should we use scientific notation when possible
+/// - **returns**: The pretty printed string
+pub fn pretty_print_float(n: f64, allow_sn: bool) -> String {
+ let (n, p) = find_minimal_repr(n, 1e-10);
+ let d_repr = float_to_string(n, p);
+ if !allow_sn {
+ d_repr
+ } else {
+ if n == 0.0 {
+ return "0".to_string();
+ }
+
+ let mut idx = n.abs().log10().floor();
+ let mut exp = (10.0f64).powf(idx);
+
+ if n.abs() / exp + 1e-5 >= 10.0 {
+ idx += 1.0;
+ exp *= 10.0;
+ }
+
+ if idx.abs() < 3.0 {
+ return d_repr;
+ }
+
+ let (sn, sp) = find_minimal_repr(n / exp, 1e-5);
+ let s_repr = format!("{}e{}", float_to_string(sn, sp), float_to_string(idx, 0));
+ if s_repr.len() + 1 < d_repr.len() {
+ s_repr
+ } else {
+ d_repr
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[test]
+ fn test_pretty_printing() {
+ assert_eq!(pretty_print_float(0.99999999999999999999, false), "1");
+ assert_eq!(pretty_print_float(0.9999, false), "0.9999");
+ assert_eq!(
+ pretty_print_float(-1e-5 - 0.00000000000000001, true),
+ "-1e-5"
+ );
+ assert_eq!(
+ pretty_print_float(-1e-5 - 0.00000000000000001, false),
+ "-0.00001"
+ );
+ assert_eq!(pretty_print_float(1e100, true), "1e100");
+ assert_eq!(pretty_print_float(1234567890f64, true), "1234567890");
+ assert_eq!(pretty_print_float(1000000001f64, true), "1e9");
+ }
+}
diff --git a/src/data/mod.rs b/src/data/mod.rs
new file mode 100644
index 0000000..af2b3d4
--- /dev/null
+++ b/src/data/mod.rs
@@ -0,0 +1,12 @@
+/*!
+The data processing module, which implements algorithms related to visualization of data.
+Such as, down-sampling, etc.
+*/
+
+mod data_range;
+pub use data_range::fitting_range;
+
+mod quartiles;
+pub use quartiles::Quartiles;
+
+pub mod float;
diff --git a/src/data/quartiles.rs b/src/data/quartiles.rs
new file mode 100644
index 0000000..054f51d
--- /dev/null
+++ b/src/data/quartiles.rs
@@ -0,0 +1,127 @@
+/// The quartiles
+#[derive(Clone, Debug)]
+pub struct Quartiles {
+ lower_fence: f64,
+ lower: f64,
+ median: f64,
+ upper: f64,
+ upper_fence: f64,
+}
+
+impl Quartiles {
+ // Extract a value representing the `pct` percentile of a
+ // sorted `s`, using linear interpolation.
+ fn percentile_of_sorted<T: Into<f64> + Copy>(s: &[T], pct: f64) -> f64 {
+ assert!(!s.is_empty());
+ if s.len() == 1 {
+ return s[0].into();
+ }
+ assert!(0_f64 <= pct);
+ let hundred = 100_f64;
+ assert!(pct <= hundred);
+ if (pct - hundred).abs() < std::f64::EPSILON {
+ return s[s.len() - 1].into();
+ }
+ let length = (s.len() - 1) as f64;
+ let rank = (pct / hundred) * length;
+ let lower_rank = rank.floor();
+ let d = rank - lower_rank;
+ let n = lower_rank as usize;
+ let lo = s[n].into();
+ let hi = s[n + 1].into();
+ lo + (hi - lo) * d
+ }
+
+ /// Create a new quartiles struct with the values calculated from the argument.
+ ///
+ /// - `s`: The array of the original values
+ /// - **returns** The newly created quartiles
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]);
+ /// assert_eq!(quartiles.median(), 37.5);
+ /// ```
+ pub fn new<T: Into<f64> + Copy + PartialOrd>(s: &[T]) -> Self {
+ let mut s = s.to_owned();
+ s.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
+
+ let lower = Quartiles::percentile_of_sorted(&s, 25_f64);
+ let median = Quartiles::percentile_of_sorted(&s, 50_f64);
+ let upper = Quartiles::percentile_of_sorted(&s, 75_f64);
+ let iqr = upper - lower;
+ let lower_fence = lower - 1.5 * iqr;
+ let upper_fence = upper + 1.5 * iqr;
+ Self {
+ lower_fence,
+ lower,
+ median,
+ upper,
+ upper_fence,
+ }
+ }
+
+ /// Get the quartiles values.
+ ///
+ /// - **returns** The array [lower fence, lower quartile, median, upper quartile, upper fence]
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]);
+ /// let values = quartiles.values();
+ /// assert_eq!(values, [-9.0, 20.25, 37.5, 39.75, 69.0]);
+ /// ```
+ pub fn values(&self) -> [f32; 5] {
+ [
+ self.lower_fence as f32,
+ self.lower as f32,
+ self.median as f32,
+ self.upper as f32,
+ self.upper_fence as f32,
+ ]
+ }
+
+ /// Get the quartiles median.
+ ///
+ /// - **returns** The median
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]);
+ /// assert_eq!(quartiles.median(), 37.5);
+ /// ```
+ pub fn median(&self) -> f64 {
+ self.median
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ #[should_panic]
+ fn test_empty_input() {
+ let empty_array: [i32; 0] = [];
+ Quartiles::new(&empty_array);
+ }
+
+ #[test]
+ fn test_low_inputs() {
+ assert_eq!(
+ Quartiles::new(&[15.0]).values(),
+ [15.0, 15.0, 15.0, 15.0, 15.0]
+ );
+ assert_eq!(
+ Quartiles::new(&[10, 20]).values(),
+ [5.0, 12.5, 15.0, 17.5, 25.0]
+ );
+ assert_eq!(
+ Quartiles::new(&[10, 20, 30]).values(),
+ [0.0, 15.0, 20.0, 25.0, 40.0]
+ );
+ }
+}
diff --git a/src/drawing/area.rs b/src/drawing/area.rs
new file mode 100644
index 0000000..ae75087
--- /dev/null
+++ b/src/drawing/area.rs
@@ -0,0 +1,848 @@
+/// The abstraction of a drawing area
+use super::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use crate::coord::{CoordTranslate, MeshLine, Ranged, RangedCoord, Shift};
+use crate::element::{Drawable, PointCollection};
+use crate::style::text_anchor::{HPos, Pos, VPos};
+use crate::style::{Color, FontDesc, SizeDesc, TextStyle};
+
+use std::borrow::Borrow;
+use std::cell::RefCell;
+use std::error::Error;
+use std::iter::{once, repeat};
+use std::ops::Range;
+use std::rc::Rc;
+
+/// The representation of the rectangle in backend canvas
+#[derive(Clone, Debug)]
+struct Rect {
+ x0: i32,
+ y0: i32,
+ x1: i32,
+ y1: i32,
+}
+
+impl Rect {
+ /// Split the rectangle into a few smaller rectangles
+ fn split<'a, BPI: IntoIterator<Item = &'a i32> + 'a>(
+ &'a self,
+ break_points: BPI,
+ vertical: bool,
+ ) -> impl Iterator<Item = Rect> + 'a {
+ let (mut x0, mut y0) = (self.x0, self.y0);
+ let (full_x, full_y) = (self.x1, self.y1);
+ break_points
+ .into_iter()
+ .chain(once(if vertical { &self.y1 } else { &self.x1 }))
+ .map(move |&p| {
+ let x1 = if vertical { full_x } else { p };
+ let y1 = if vertical { p } else { full_y };
+ let ret = Rect { x0, y0, x1, y1 };
+
+ if vertical {
+ y0 = y1
+ } else {
+ x0 = x1;
+ }
+
+ ret
+ })
+ }
+
+ /// Evenly split the rectangle to a row * col mesh
+ fn split_evenly<'a>(&'a self, (row, col): (usize, usize)) -> impl Iterator<Item = Rect> + 'a {
+ fn compute_evenly_split(from: i32, to: i32, n: usize, idx: usize) -> i32 {
+ let size = (to - from) as usize;
+ from + idx as i32 * (size / n) as i32 + idx.min(size % n) as i32
+ }
+ (0..row)
+ .map(move |x| repeat(x).zip(0..col))
+ .flatten()
+ .map(move |(ri, ci)| Self {
+ y0: compute_evenly_split(self.y0, self.y1, row, ri),
+ y1: compute_evenly_split(self.y0, self.y1, row, ri + 1),
+ x0: compute_evenly_split(self.x0, self.x1, col, ci),
+ x1: compute_evenly_split(self.x0, self.x1, col, ci + 1),
+ })
+ }
+
+ fn split_grid(
+ &self,
+ x_breaks: impl Iterator<Item = i32>,
+ y_breaks: impl Iterator<Item = i32>,
+ ) -> impl Iterator<Item = Rect> {
+ let mut xs = vec![self.x0, self.x1];
+ let mut ys = vec![self.y0, self.y1];
+ xs.extend(x_breaks.map(|v| v + self.x0));
+ ys.extend(y_breaks.map(|v| v + self.y0));
+
+ xs.sort();
+ ys.sort();
+
+ let xsegs: Vec<_> = xs
+ .iter()
+ .zip(xs.iter().skip(1))
+ .map(|(a, b)| (*a, *b))
+ .collect();
+ let ysegs: Vec<_> = ys
+ .iter()
+ .zip(ys.iter().skip(1))
+ .map(|(a, b)| (*a, *b))
+ .collect();
+
+ ysegs
+ .into_iter()
+ .map(move |(y0, y1)| {
+ xsegs
+ .clone()
+ .into_iter()
+ .map(move |(x0, x1)| Self { x0, y0, x1, y1 })
+ })
+ .flatten()
+ }
+
+ /// Make the coordinate in the range of the rectangle
+ fn truncate(&self, p: (i32, i32)) -> (i32, i32) {
+ (p.0.min(self.x1).max(self.x0), p.1.min(self.y1).max(self.y0))
+ }
+}
+
+/// The abstraction of a drawing area. Plotters uses drawing area as the fundamental abstraction for the
+/// high level drawing API. The major functionality provided by the drawing area is
+/// 1. Layout specification - Split the parent drawing area into sub-drawing-areas
+/// 2. Coordinate Translation - Allows guest coordinate system attached and used for drawing.
+/// 3. Element based drawing - drawing area provides the environment the element can be drawn onto it.
+pub struct DrawingArea<DB: DrawingBackend, CT: CoordTranslate> {
+ backend: Rc<RefCell<DB>>,
+ rect: Rect,
+ coord: CT,
+}
+
+impl<DB: DrawingBackend, CT: CoordTranslate + Clone> Clone for DrawingArea<DB, CT> {
+ fn clone(&self) -> Self {
+ Self {
+ backend: self.copy_backend_ref(),
+ rect: self.rect.clone(),
+ coord: self.coord.clone(),
+ }
+ }
+}
+
+/// The error description of any drawing area API
+#[derive(Debug)]
+pub enum DrawingAreaErrorKind<E: Error + Send + Sync> {
+ /// The error is due to drawing backend failure
+ BackendError(DrawingErrorKind<E>),
+ /// We are not able to get the mutable reference of the backend,
+ /// which indicates the drawing backend is current used by other
+ /// drawing operation
+ SharingError,
+ /// The error caused by invalid layout
+ LayoutError,
+}
+
+impl<E: Error + Send + Sync> std::fmt::Display for DrawingAreaErrorKind<E> {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ match self {
+ DrawingAreaErrorKind::BackendError(e) => write!(fmt, "backend error: {}", e),
+ DrawingAreaErrorKind::SharingError => {
+ write!(fmt, "Multiple backend operation in progress")
+ }
+ DrawingAreaErrorKind::LayoutError => write!(fmt, "Bad layout"),
+ }
+ }
+}
+
+impl<E: Error + Send + Sync> Error for DrawingAreaErrorKind<E> {}
+
+#[allow(type_alias_bounds)]
+type DrawingAreaError<T: DrawingBackend> = DrawingAreaErrorKind<T::ErrorType>;
+
+impl<DB: DrawingBackend> From<DB> for DrawingArea<DB, Shift> {
+ fn from(backend: DB) -> Self {
+ Self::with_rc_cell(Rc::new(RefCell::new(backend)))
+ }
+}
+
+impl<'a, DB: DrawingBackend> From<&'a Rc<RefCell<DB>>> for DrawingArea<DB, Shift> {
+ fn from(backend: &'a Rc<RefCell<DB>>) -> Self {
+ Self::with_rc_cell(backend.clone())
+ }
+}
+
+/// A type which can be converted into a root drawing area
+pub trait IntoDrawingArea: DrawingBackend + Sized {
+ /// Convert the type into a root drawing area
+ fn into_drawing_area(self) -> DrawingArea<Self, Shift>;
+}
+
+impl<T: DrawingBackend> IntoDrawingArea for T {
+ fn into_drawing_area(self) -> DrawingArea<T, Shift> {
+ self.into()
+ }
+}
+
+impl<DB: DrawingBackend, X: Ranged, Y: Ranged> DrawingArea<DB, RangedCoord<X, Y>> {
+ /// Draw the mesh on a area
+ pub fn draw_mesh<DrawFunc>(
+ &self,
+ mut draw_func: DrawFunc,
+ y_count_max: usize,
+ x_count_max: usize,
+ ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
+ where
+ DrawFunc: FnMut(&mut DB, MeshLine<X, Y>) -> Result<(), DrawingErrorKind<DB::ErrorType>>,
+ {
+ self.backend_ops(move |b| {
+ self.coord
+ .draw_mesh(y_count_max, x_count_max, |line| draw_func(b, line))
+ })
+ }
+
+ /// Get the range of X of the guest coordinate for current drawing area
+ pub fn get_x_range(&self) -> Range<X::ValueType> {
+ self.coord.get_x_range()
+ }
+
+ /// Get the range of Y of the guest coordinate for current drawing area
+ pub fn get_y_range(&self) -> Range<Y::ValueType> {
+ self.coord.get_y_range()
+ }
+
+ pub fn get_x_axis_pixel_range(&self) -> Range<i32> {
+ self.coord.get_x_axis_pixel_range()
+ }
+
+ pub fn get_y_axis_pixel_range(&self) -> Range<i32> {
+ self.coord.get_y_axis_pixel_range()
+ }
+}
+
+impl<DB: DrawingBackend, CT: CoordTranslate> DrawingArea<DB, CT> {
+ /// Get the left upper conner of this area in the drawing backend
+ pub fn get_base_pixel(&self) -> BackendCoord {
+ (self.rect.x0, self.rect.y0)
+ }
+
+ /// Strip the applied coordinate specification and returns a shift-based drawing area
+ pub fn strip_coord_spec(&self) -> DrawingArea<DB, Shift> {
+ DrawingArea {
+ rect: self.rect.clone(),
+ backend: self.copy_backend_ref(),
+ coord: Shift((self.rect.x0, self.rect.y0)),
+ }
+ }
+
+ /// Get the area dimension in pixel
+ pub fn dim_in_pixel(&self) -> (u32, u32) {
+ (
+ (self.rect.x1 - self.rect.x0) as u32,
+ (self.rect.y1 - self.rect.y0) as u32,
+ )
+ }
+
+ /// Compute the relative size based on the drawing area's height
+ pub fn relative_to_height(&self, p: f64) -> f64 {
+ f64::from((self.rect.y1 - self.rect.y0).max(0)) * (p.min(1.0).max(0.0))
+ }
+
+ /// Compute the relative size based on the drawing area's width
+ pub fn relative_to_width(&self, p: f64) -> f64 {
+ f64::from((self.rect.x1 - self.rect.x0).max(0)) * (p.min(1.0).max(0.0))
+ }
+
+ /// Get the pixel range of this area
+ pub fn get_pixel_range(&self) -> (Range<i32>, Range<i32>) {
+ (self.rect.x0..self.rect.x1, self.rect.y0..self.rect.y1)
+ }
+
+ /// Copy the drawing context
+ fn copy_backend_ref(&self) -> Rc<RefCell<DB>> {
+ self.backend.clone()
+ }
+
+ /// Perform operation on the drawing backend
+ fn backend_ops<R, O: FnOnce(&mut DB) -> Result<R, DrawingErrorKind<DB::ErrorType>>>(
+ &self,
+ ops: O,
+ ) -> Result<R, DrawingAreaError<DB>> {
+ if let Ok(mut db) = self.backend.try_borrow_mut() {
+ db.ensure_prepared()
+ .map_err(DrawingAreaErrorKind::BackendError)?;
+ ops(&mut db).map_err(DrawingAreaErrorKind::BackendError)
+ } else {
+ Err(DrawingAreaErrorKind::SharingError)
+ }
+ }
+
+ /// Fill the entire drawing area with a color
+ pub fn fill<ColorType: Color>(&self, color: &ColorType) -> Result<(), DrawingAreaError<DB>> {
+ self.backend_ops(|backend| {
+ backend.draw_rect(
+ (self.rect.x0, self.rect.y0),
+ (self.rect.x1 - 1, self.rect.y1 - 1),
+ color,
+ true,
+ )
+ })
+ }
+
+ /// Draw a single pixel
+ pub fn draw_pixel<ColorType: Color>(
+ &self,
+ pos: CT::From,
+ color: &ColorType,
+ ) -> Result<(), DrawingAreaError<DB>> {
+ let pos = self.coord.translate(&pos);
+ self.backend_ops(|b| b.draw_pixel(pos, &color.to_rgba()))
+ }
+
+ /// Present all the pending changes to the backend
+ pub fn present(&self) -> Result<(), DrawingAreaError<DB>> {
+ self.backend_ops(|b| b.present())
+ }
+
+ /// Draw an high-level element
+ pub fn draw<'a, E>(&self, element: &'a E) -> Result<(), DrawingAreaError<DB>>
+ where
+ &'a E: PointCollection<'a, CT::From>,
+ E: Drawable<DB>,
+ {
+ let backend_coords = element.point_iter().into_iter().map(|p| {
+ let b = p.borrow();
+ self.rect.truncate(self.coord.translate(b))
+ });
+ self.backend_ops(move |b| element.draw(backend_coords, b, self.dim_in_pixel()))
+ }
+
+ /// Map coordinate to the backend coordinate
+ pub fn map_coordinate(&self, coord: &CT::From) -> BackendCoord {
+ self.coord.translate(coord)
+ }
+
+ /// Estimate the dimension of the text if drawn on this drawing area.
+ /// We can't get this directly from the font, since the drawing backend may or may not
+ /// follows the font configuration. In terminal, the font family will be dropped.
+ /// So the size of the text is drawing area related.
+ ///
+ /// - `text`: The text we want to estimate
+ /// - `font`: The font spec in which we want to draw the text
+ /// - **return**: The size of the text if drawn on this area
+ pub fn estimate_text_size(
+ &self,
+ text: &str,
+ font: &FontDesc,
+ ) -> Result<(u32, u32), DrawingAreaError<DB>> {
+ self.backend_ops(move |b| b.estimate_text_size(text, font))
+ }
+}
+
+impl<DB: DrawingBackend> DrawingArea<DB, Shift> {
+ fn with_rc_cell(backend: Rc<RefCell<DB>>) -> Self {
+ let (x1, y1) = RefCell::borrow(backend.borrow()).get_size();
+ Self {
+ rect: Rect {
+ x0: 0,
+ y0: 0,
+ x1: x1 as i32,
+ y1: y1 as i32,
+ },
+ backend,
+ coord: Shift((0, 0)),
+ }
+ }
+
+ /// Shrink the region, note all the locations are in guest coordinate
+ pub fn shrink<A: SizeDesc, B: SizeDesc, C: SizeDesc, D: SizeDesc>(
+ mut self,
+ left_upper: (A, B),
+ dimension: (C, D),
+ ) -> DrawingArea<DB, Shift> {
+ let left_upper = (left_upper.0.in_pixels(&self), left_upper.1.in_pixels(&self));
+ let dimension = (dimension.0.in_pixels(&self), dimension.1.in_pixels(&self));
+ self.rect.x0 = self.rect.x1.min(self.rect.x0 + left_upper.0);
+ self.rect.y0 = self.rect.y1.min(self.rect.y0 + left_upper.1);
+
+ self.rect.x1 = self.rect.x0.max(self.rect.x0 + dimension.0);
+ self.rect.y1 = self.rect.y0.max(self.rect.y0 + dimension.1);
+
+ self.coord = Shift((self.rect.x0, self.rect.y0));
+
+ self
+ }
+
+ /// Apply a new coord transformation object and returns a new drawing area
+ pub fn apply_coord_spec<CT: CoordTranslate>(&self, coord_spec: CT) -> DrawingArea<DB, CT> {
+ DrawingArea {
+ rect: self.rect.clone(),
+ backend: self.copy_backend_ref(),
+ coord: coord_spec,
+ }
+ }
+
+ /// Create a margin for the given drawing area and returns the new drawing area
+ pub fn margin<ST: SizeDesc, SB: SizeDesc, SL: SizeDesc, SR: SizeDesc>(
+ &self,
+ top: ST,
+ bottom: SB,
+ left: SL,
+ right: SR,
+ ) -> DrawingArea<DB, Shift> {
+ let left = left.in_pixels(self);
+ let right = right.in_pixels(self);
+ let top = top.in_pixels(self);
+ let bottom = bottom.in_pixels(self);
+ DrawingArea {
+ rect: Rect {
+ x0: self.rect.x0 + left,
+ y0: self.rect.y0 + top,
+ x1: self.rect.x1 - right,
+ y1: self.rect.y1 - bottom,
+ },
+ backend: self.copy_backend_ref(),
+ coord: Shift((self.rect.x0 + left, self.rect.y0 + top)),
+ }
+ }
+
+ /// Split the drawing area vertically
+ pub fn split_vertically<S: SizeDesc>(&self, y: S) -> (Self, Self) {
+ let y = y.in_pixels(self);
+ let split_point = [y + self.rect.y0];
+ let mut ret = self.rect.split(split_point.iter(), true).map(|rect| Self {
+ rect: rect.clone(),
+ backend: self.copy_backend_ref(),
+ coord: Shift((rect.x0, rect.y0)),
+ });
+
+ (ret.next().unwrap(), ret.next().unwrap())
+ }
+
+ /// Split the drawing area horizontally
+ pub fn split_horizontally<S: SizeDesc>(&self, x: S) -> (Self, Self) {
+ let x = x.in_pixels(self);
+ let split_point = [x + self.rect.x0];
+ let mut ret = self.rect.split(split_point.iter(), false).map(|rect| Self {
+ rect: rect.clone(),
+ backend: self.copy_backend_ref(),
+ coord: Shift((rect.x0, rect.y0)),
+ });
+
+ (ret.next().unwrap(), ret.next().unwrap())
+ }
+
+ /// Split the drawing area evenly
+ pub fn split_evenly(&self, (row, col): (usize, usize)) -> Vec<Self> {
+ self.rect
+ .split_evenly((row, col))
+ .map(|rect| Self {
+ rect: rect.clone(),
+ backend: self.copy_backend_ref(),
+ coord: Shift((rect.x0, rect.y0)),
+ })
+ .collect()
+ }
+
+ /// Split the drawing area into a grid with specified breakpoints on both X axis and Y axis
+ pub fn split_by_breakpoints<
+ XSize: SizeDesc,
+ YSize: SizeDesc,
+ XS: AsRef<[XSize]>,
+ YS: AsRef<[YSize]>,
+ >(
+ &self,
+ xs: XS,
+ ys: YS,
+ ) -> Vec<Self> {
+ self.rect
+ .split_grid(
+ xs.as_ref().iter().map(|x| x.in_pixels(self)),
+ ys.as_ref().iter().map(|x| x.in_pixels(self)),
+ )
+ .map(|rect| Self {
+ rect: rect.clone(),
+ backend: self.copy_backend_ref(),
+ coord: Shift((rect.x0, rect.y0)),
+ })
+ .collect()
+ }
+
+ /// Draw a title of the drawing area and return the remaining drawing area
+ pub fn titled<'a, S: Into<TextStyle<'a>>>(
+ &self,
+ text: &str,
+ style: S,
+ ) -> Result<Self, DrawingAreaError<DB>> {
+ let style = style.into();
+
+ let x_padding = (self.rect.x1 - self.rect.x0) / 2;
+
+ let (_, text_h) = self.estimate_text_size(text, &style.font)?;
+ let y_padding = (text_h / 2).min(5) as i32;
+
+ let style = &style.pos(Pos::new(HPos::Center, VPos::Top));
+
+ self.backend_ops(|b| {
+ b.draw_text(
+ text,
+ &style,
+ (self.rect.x0 + x_padding, self.rect.y0 + y_padding),
+ )
+ })?;
+
+ Ok(Self {
+ rect: Rect {
+ x0: self.rect.x0,
+ y0: self.rect.y0 + y_padding * 2 + text_h as i32,
+ x1: self.rect.x1,
+ y1: self.rect.y1,
+ },
+ backend: self.copy_backend_ref(),
+ coord: Shift((self.rect.x0, self.rect.y0 + y_padding * 2 + text_h as i32)),
+ })
+ }
+
+ /// Draw text on the drawing area
+ pub fn draw_text(
+ &self,
+ text: &str,
+ style: &TextStyle,
+ pos: BackendCoord,
+ ) -> Result<(), DrawingAreaError<DB>> {
+ self.backend_ops(|b| {
+ b.draw_text(text, &style, (pos.0 + self.rect.x0, pos.1 + self.rect.y0))
+ })
+ }
+}
+
+impl<DB: DrawingBackend, CT: CoordTranslate> DrawingArea<DB, CT> {
+ pub fn into_coord_spec(self) -> CT {
+ self.coord
+ }
+
+ pub fn as_coord_spec(&self) -> &CT {
+ &self.coord
+ }
+}
+
+#[cfg(test)]
+mod drawing_area_tests {
+ use crate::{create_mocked_drawing_area, prelude::*};
+ #[test]
+ fn test_filling() {
+ let drawing_area = create_mocked_drawing_area(1024, 768, |m| {
+ m.check_draw_rect(|c, _, f, u, d| {
+ assert_eq!(c, WHITE.to_rgba());
+ assert_eq!(f, true);
+ assert_eq!(u, (0, 0));
+ assert_eq!(d, (1023, 767));
+ });
+
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_rect_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ });
+
+ drawing_area.fill(&WHITE).expect("Drawing Failure");
+ }
+
+ #[test]
+ fn test_split_evenly() {
+ let colors = vec![
+ &RED, &BLUE, &YELLOW, &WHITE, &BLACK, &MAGENTA, &CYAN, &BLUE, &RED,
+ ];
+ let drawing_area = create_mocked_drawing_area(902, 900, |m| {
+ for col in 0..3 {
+ for row in 0..3 {
+ let colors = colors.clone();
+ m.check_draw_rect(move |c, _, f, u, d| {
+ assert_eq!(c, colors[col * 3 + row].to_rgba());
+ assert_eq!(f, true);
+ assert_eq!(u, (300 * row as i32 + 2.min(row) as i32, 300 * col as i32));
+ assert_eq!(
+ d,
+ (
+ 300 + 300 * row as i32 + 2.min(row + 1) as i32 - 1,
+ 300 + 300 * col as i32 - 1
+ )
+ );
+ });
+ }
+ }
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_rect_call, 9);
+ assert_eq!(b.draw_count, 9);
+ });
+ });
+
+ drawing_area
+ .split_evenly((3, 3))
+ .iter_mut()
+ .zip(colors.iter())
+ .for_each(|(d, c)| {
+ d.fill(*c).expect("Drawing Failure");
+ });
+ }
+
+ #[test]
+ fn test_split_horizontally() {
+ let drawing_area = create_mocked_drawing_area(1024, 768, |m| {
+ m.check_draw_rect(|c, _, f, u, d| {
+ assert_eq!(c, RED.to_rgba());
+ assert_eq!(f, true);
+ assert_eq!(u, (0, 0));
+ assert_eq!(d, (345 - 1, 768 - 1));
+ });
+
+ m.check_draw_rect(|c, _, f, u, d| {
+ assert_eq!(c, BLUE.to_rgba());
+ assert_eq!(f, true);
+ assert_eq!(u, (345, 0));
+ assert_eq!(d, (1024 - 1, 768 - 1));
+ });
+
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_rect_call, 2);
+ assert_eq!(b.draw_count, 2);
+ });
+ });
+
+ let (left, right) = drawing_area.split_horizontally(345);
+ left.fill(&RED).expect("Drawing Error");
+ right.fill(&BLUE).expect("Drawing Error");
+ }
+
+ #[test]
+ fn test_split_vertically() {
+ let drawing_area = create_mocked_drawing_area(1024, 768, |m| {
+ m.check_draw_rect(|c, _, f, u, d| {
+ assert_eq!(c, RED.to_rgba());
+ assert_eq!(f, true);
+ assert_eq!(u, (0, 0));
+ assert_eq!(d, (1024 - 1, 345 - 1));
+ });
+
+ m.check_draw_rect(|c, _, f, u, d| {
+ assert_eq!(c, BLUE.to_rgba());
+ assert_eq!(f, true);
+ assert_eq!(u, (0, 345));
+ assert_eq!(d, (1024 - 1, 768 - 1));
+ });
+
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_rect_call, 2);
+ assert_eq!(b.draw_count, 2);
+ });
+ });
+
+ let (left, right) = drawing_area.split_vertically(345);
+ left.fill(&RED).expect("Drawing Error");
+ right.fill(&BLUE).expect("Drawing Error");
+ }
+
+ #[test]
+ fn test_split_grid() {
+ let colors = vec![
+ &RED, &BLUE, &YELLOW, &WHITE, &BLACK, &MAGENTA, &CYAN, &BLUE, &RED,
+ ];
+ let breaks: [i32; 5] = [100, 200, 300, 400, 500];
+
+ for nxb in 0..=5 {
+ for nyb in 0..=5 {
+ let drawing_area = create_mocked_drawing_area(1024, 768, |m| {
+ for row in 0..=nyb {
+ for col in 0..=nxb {
+ let get_bp = |full, limit, id| {
+ (if id == 0 {
+ 0
+ } else if id > limit {
+ full
+ } else {
+ breaks[id as usize - 1]
+ }) as i32
+ };
+
+ let expected_u = (get_bp(1024, nxb, col), get_bp(768, nyb, row));
+ let expected_d = (
+ get_bp(1024, nxb, col + 1) - 1,
+ get_bp(768, nyb, row + 1) - 1,
+ );
+ let expected_color =
+ colors[(row * (nxb + 1) + col) as usize % colors.len()];
+
+ m.check_draw_rect(move |c, _, f, u, d| {
+ assert_eq!(c, expected_color.to_rgba());
+ assert_eq!(f, true);
+ assert_eq!(u, expected_u);
+ assert_eq!(d, expected_d);
+ });
+ }
+ }
+
+ m.drop_check(move |b| {
+ assert_eq!(b.num_draw_rect_call, ((nxb + 1) * (nyb + 1)) as u32);
+ assert_eq!(b.draw_count, ((nyb + 1) * (nxb + 1)) as u32);
+ });
+ });
+
+ let result = drawing_area
+ .split_by_breakpoints(&breaks[0..nxb as usize], &breaks[0..nyb as usize]);
+ for i in 0..result.len() {
+ result[i]
+ .fill(colors[i % colors.len()])
+ .expect("Drawing Error");
+ }
+ }
+ }
+ }
+ #[test]
+ fn test_titled() {
+ let drawing_area = create_mocked_drawing_area(1024, 768, |m| {
+ m.check_draw_text(|c, font, size, _pos, text| {
+ assert_eq!(c, BLACK.to_rgba());
+ assert_eq!(font, "serif");
+ assert_eq!(size, 30.0);
+ assert_eq!("This is the title", text);
+ });
+ m.check_draw_rect(|c, _, f, u, d| {
+ assert_eq!(c, WHITE.to_rgba());
+ assert_eq!(f, true);
+ assert_eq!(u.0, 0);
+ assert!(u.1 > 0);
+ assert_eq!(d, (1024 - 1, 768 - 1));
+ });
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_text_call, 1);
+ assert_eq!(b.num_draw_rect_call, 1);
+ assert_eq!(b.draw_count, 2);
+ });
+ });
+
+ drawing_area
+ .titled("This is the title", ("serif", 30))
+ .unwrap()
+ .fill(&WHITE)
+ .unwrap();
+ }
+
+ #[test]
+ fn test_margin() {
+ let drawing_area = create_mocked_drawing_area(1024, 768, |m| {
+ m.check_draw_rect(|c, _, f, u, d| {
+ assert_eq!(c, WHITE.to_rgba());
+ assert_eq!(f, true);
+ assert_eq!(u, (3, 1));
+ assert_eq!(d, (1024 - 4 - 1, 768 - 2 - 1));
+ });
+
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_rect_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ });
+
+ drawing_area
+ .margin(1, 2, 3, 4)
+ .fill(&WHITE)
+ .expect("Drawing Failure");
+ }
+
+ #[test]
+ fn test_ranges() {
+ let drawing_area =
+ create_mocked_drawing_area(1024, 768, |_m| {}).apply_coord_spec(RangedCoord::<
+ RangedCoordi32,
+ RangedCoordu32,
+ >::new(
+ -100..100,
+ 0..200,
+ (0..1024, 0..768),
+ ));
+
+ let x_range = drawing_area.get_x_range();
+ assert_eq!(x_range, -100..100);
+
+ let y_range = drawing_area.get_y_range();
+ assert_eq!(y_range, 0..200);
+ }
+
+ #[test]
+ fn test_relative_size() {
+ let drawing_area = create_mocked_drawing_area(1024, 768, |_m| {});
+
+ assert_eq!(102.4, drawing_area.relative_to_width(0.1));
+ assert_eq!(384.0, drawing_area.relative_to_height(0.5));
+
+ assert_eq!(1024.0, drawing_area.relative_to_width(1.3));
+ assert_eq!(768.0, drawing_area.relative_to_height(1.5));
+
+ assert_eq!(0.0, drawing_area.relative_to_width(-0.2));
+ assert_eq!(0.0, drawing_area.relative_to_height(-0.5));
+ }
+
+ #[test]
+ fn test_relative_split() {
+ let drawing_area = create_mocked_drawing_area(1000, 1200, |m| {
+ let mut counter = 0;
+ m.check_draw_rect(move |c, _, f, u, d| {
+ assert_eq!(f, true);
+
+ match counter {
+ 0 => {
+ assert_eq!(c, RED.to_rgba());
+ assert_eq!(u, (0, 0));
+ assert_eq!(d, (300 - 1, 600 - 1));
+ }
+ 1 => {
+ assert_eq!(c, BLUE.to_rgba());
+ assert_eq!(u, (300, 0));
+ assert_eq!(d, (1000 - 1, 600 - 1));
+ }
+ 2 => {
+ assert_eq!(c, GREEN.to_rgba());
+ assert_eq!(u, (0, 600));
+ assert_eq!(d, (300 - 1, 1200 - 1));
+ }
+ 3 => {
+ assert_eq!(c, WHITE.to_rgba());
+ assert_eq!(u, (300, 600));
+ assert_eq!(d, (1000 - 1, 1200 - 1));
+ }
+ _ => panic!("Too many draw rect"),
+ }
+
+ counter += 1;
+ });
+
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_rect_call, 4);
+ assert_eq!(b.draw_count, 4);
+ });
+ });
+
+ let split =
+ drawing_area.split_by_breakpoints([(30).percent_width()], [(50).percent_height()]);
+
+ split[0].fill(&RED).unwrap();
+ split[1].fill(&BLUE).unwrap();
+ split[2].fill(&GREEN).unwrap();
+ split[3].fill(&WHITE).unwrap();
+ }
+
+ #[test]
+ fn test_relative_shrink() {
+ let drawing_area = create_mocked_drawing_area(1000, 1200, |m| {
+ m.check_draw_rect(move |_, _, _, u, d| {
+ assert_eq!((100, 100), u);
+ assert_eq!((300 - 1, 700 - 1), d);
+ });
+
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_rect_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ })
+ .shrink(((10).percent_width(), 100), (200, (50).percent_height()));
+
+ drawing_area.fill(&RED).unwrap();
+ }
+}
diff --git a/src/drawing/backend.rs b/src/drawing/backend.rs
new file mode 100644
index 0000000..a5ba54b
--- /dev/null
+++ b/src/drawing/backend.rs
@@ -0,0 +1,284 @@
+use crate::style::text_anchor::{HPos, VPos};
+use crate::style::{Color, FontDesc, FontError, RGBAColor, ShapeStyle, TextStyle};
+use std::error::Error;
+
+/// A coordinate in the image
+pub type BackendCoord = (i32, i32);
+
+/// The error produced by a drawing backend
+#[derive(Debug)]
+pub enum DrawingErrorKind<E: Error + Send + Sync>
+where
+ FontError: Send + Sync,
+{
+ /// A drawing backend error
+ DrawingError(E),
+ /// A font rendering error
+ FontError(FontError),
+}
+
+impl<E: Error + Send + Sync> std::fmt::Display for DrawingErrorKind<E> {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ match self {
+ DrawingErrorKind::DrawingError(e) => write!(fmt, "Drawing backend error: {}", e),
+ DrawingErrorKind::FontError(e) => write!(fmt, "Font loading error: {}", e),
+ }
+ }
+}
+
+impl<E: Error + Send + Sync> Error for DrawingErrorKind<E> {}
+
+/// The style data for the backend drawing API
+pub trait BackendStyle {
+ /// The underlying type represents the color for this style
+ type ColorType: Color;
+
+ /// Convert the style into the underlying color
+ fn as_color(&self) -> RGBAColor;
+
+ // TODO: In the future we should support stroke width, line shape, etc....
+ fn stroke_width(&self) -> u32 {
+ 1
+ }
+}
+
+impl<T: Color> BackendStyle for T {
+ type ColorType = T;
+ fn as_color(&self) -> RGBAColor {
+ self.to_rgba()
+ }
+}
+
+impl BackendStyle for ShapeStyle {
+ type ColorType = RGBAColor;
+ fn as_color(&self) -> RGBAColor {
+ self.color.clone()
+ }
+ fn stroke_width(&self) -> u32 {
+ self.stroke_width
+ }
+}
+
+/// The drawing backend trait, which implements the low-level drawing APIs.
+/// This trait has a set of default implementation. And the minimal requirement of
+/// implementing a drawing backend is implementing the `draw_pixel` function.
+///
+/// If the drawing backend supports vector graphics, the other drawing APIs should be
+/// override by the backend specific implementation. Otherwise, the default implementation
+/// will use the pixel-based approach to draw other types of low-level shapes.
+pub trait DrawingBackend: Sized {
+ /// The error type reported by the backend
+ type ErrorType: Error + Send + Sync;
+
+ /// Get the dimension of the drawing backend in pixels
+ fn get_size(&self) -> (u32, u32);
+
+ /// Ensure the backend is ready to draw
+ fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>>;
+
+ /// Finalize the drawing step and present all the changes.
+ /// This is used as the real-time rendering support.
+ /// The backend may implement in the following way, when `ensure_prepared` is called
+ /// it checks if it needs a fresh buffer and `present` is called rendering all the
+ /// pending changes on the screen.
+ fn present(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>>;
+
+ /// Draw a pixel on the drawing backend
+ /// - `point`: The backend pixel-based coordinate to draw
+ /// - `color`: The color of the pixel
+ fn draw_pixel(
+ &mut self,
+ point: BackendCoord,
+ color: &RGBAColor,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>>;
+
+ /// Draw a line on the drawing backend
+ /// - `from`: The start point of the line
+ /// - `to`: The end point of the line
+ /// - `style`: The style of the line
+ fn draw_line<S: BackendStyle>(
+ &mut self,
+ from: BackendCoord,
+ to: BackendCoord,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ super::rasterizer::draw_line(self, from, to, style)
+ }
+
+ /// Draw a rectangle on the drawing backend
+ /// - `upper_left`: The coordinate of the upper-left corner of the rect
+ /// - `bottom_right`: The coordinate of the bottom-right corner of the rect
+ /// - `style`: The style
+ /// - `fill`: If the rectangle should be filled
+ fn draw_rect<S: BackendStyle>(
+ &mut self,
+ upper_left: BackendCoord,
+ bottom_right: BackendCoord,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ super::rasterizer::draw_rect(self, upper_left, bottom_right, style, fill)
+ }
+
+ /// Draw a path on the drawing backend
+ /// - `path`: The iterator of key points of the path
+ /// - `style`: The style of the path
+ fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ path: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+
+ if style.stroke_width() == 1 {
+ let mut begin: Option<BackendCoord> = None;
+ for end in path.into_iter() {
+ if let Some(begin) = begin {
+ let result = self.draw_line(begin, end, style);
+ if result.is_err() {
+ return result;
+ }
+ }
+ begin = Some(end);
+ }
+ } else {
+ let p: Vec<_> = path.into_iter().collect();
+ let v = super::rasterizer::polygonize(&p[..], style.stroke_width());
+ return self.fill_polygon(v, &style.as_color());
+ }
+ Ok(())
+ }
+
+ /// Draw a circle on the drawing backend
+ /// - `center`: The center coordinate of the circle
+ /// - `radius`: The radius of the circle
+ /// - `style`: The style of the shape
+ /// - `fill`: If the circle should be filled
+ fn draw_circle<S: BackendStyle>(
+ &mut self,
+ center: BackendCoord,
+ radius: u32,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ super::rasterizer::draw_circle(self, center, radius, style, fill)
+ }
+
+ fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ vert: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let vert_buf: Vec<_> = vert.into_iter().collect();
+
+ super::rasterizer::fill_polygon(self, &vert_buf[..], style)
+ }
+
+ /// Draw a text on the drawing backend
+ /// - `text`: The text to draw
+ /// - `style`: The text style
+ /// - `pos` : The text anchor point
+ fn draw_text(
+ &mut self,
+ text: &str,
+ style: &TextStyle,
+ pos: BackendCoord,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let font = &style.font;
+ let color = &style.color;
+ if color.alpha() == 0.0 {
+ return Ok(());
+ }
+
+ let layout = font.layout_box(text).map_err(DrawingErrorKind::FontError)?;
+ let ((min_x, min_y), (max_x, max_y)) = layout;
+ let width = (max_x - min_x) as i32;
+ let height = (max_y - min_y) as i32;
+ let dx = match style.pos.h_pos {
+ HPos::Left => 0,
+ HPos::Right => -width,
+ HPos::Center => -width / 2,
+ };
+ let dy = match style.pos.v_pos {
+ VPos::Top => 0,
+ VPos::Center => -height / 2,
+ VPos::Bottom => -height,
+ };
+ let trans = font.get_transform();
+ let (w, h) = self.get_size();
+ match font.draw(text, (0, 0), |x, y, v| {
+ let (x, y) = trans.transform(x + dx - min_x, y + dy - min_y);
+ let (x, y) = (pos.0 + x, pos.1 + y);
+ if x >= 0 && x < w as i32 && y >= 0 && y < h as i32 {
+ self.draw_pixel((x, y), &color.mix(f64::from(v)))
+ } else {
+ Ok(())
+ }
+ }) {
+ Ok(drawing_result) => drawing_result,
+ Err(font_error) => Err(DrawingErrorKind::FontError(font_error)),
+ }
+ }
+
+ /// Estimate the size of the horizontal text if rendered on this backend.
+ /// This is important because some of the backend may not have font ability.
+ /// Thus this allows those backend reports proper value rather than ask the
+ /// font rasterizer for that.
+ ///
+ /// - `text`: The text to estimate
+ /// - `font`: The font to estimate
+ /// - *Returns* The estimated text size
+ fn estimate_text_size<'a>(
+ &self,
+ text: &str,
+ font: &FontDesc<'a>,
+ ) -> Result<(u32, u32), DrawingErrorKind<Self::ErrorType>> {
+ let layout = font.layout_box(text).map_err(DrawingErrorKind::FontError)?;
+ Ok((
+ ((layout.1).0 - (layout.0).0) as u32,
+ ((layout.1).1 - (layout.0).1) as u32,
+ ))
+ }
+
+ /// Blit a bitmap on to the backend.
+ ///
+ /// - `text`: pos the left upper conner of the bitmap to blit
+ /// - `src`: The source of the image
+ ///
+ /// TODO: The default implementation of bitmap blitting assumes that the bitmap is RGB, but
+ /// this may not be the case. But for bitmap backend it's actually ok if we use the bitmap
+ /// element that matches the pixel format, but we need to fix this.
+ fn blit_bitmap<'a>(
+ &mut self,
+ pos: BackendCoord,
+ (iw, ih): (u32, u32),
+ src: &'a [u8],
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let (w, h) = self.get_size();
+
+ for dx in 0..iw {
+ if pos.0 + dx as i32 >= w as i32 {
+ break;
+ }
+ for dy in 0..ih {
+ if pos.1 + dy as i32 >= h as i32 {
+ break;
+ }
+ // FIXME: This assume we have RGB image buffer
+ let r = src[(dx + dy * iw) as usize * 3];
+ let g = src[(dx + dy * iw) as usize * 3 + 1];
+ let b = src[(dx + dy * iw) as usize * 3 + 2];
+ let color = crate::style::RGBColor(r, g, b);
+ let result =
+ self.draw_pixel((pos.0 + dx as i32, pos.1 + dy as i32), &color.to_rgba());
+ if result.is_err() {
+ return result;
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/drawing/backend_impl/bitmap.rs b/src/drawing/backend_impl/bitmap.rs
new file mode 100644
index 0000000..a4d776a
--- /dev/null
+++ b/src/drawing/backend_impl/bitmap.rs
@@ -0,0 +1,1607 @@
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
+use crate::style::{Color, RGBAColor};
+use std::marker::PhantomData;
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+mod image_encoding_support {
+ pub(super) use image::{ImageBuffer, ImageError, Rgb};
+ pub(super) use std::path::Path;
+ pub(super) type BorrowedImage<'a> = ImageBuffer<Rgb<u8>, &'a mut [u8]>;
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+use image_encoding_support::*;
+
+#[derive(Debug)]
+/// Indicates some error occurs within the bitmap backend
+pub enum BitMapBackendError {
+ /// The buffer provided is invalid, for example, wrong pixel buffer size
+ InvalidBuffer,
+ /// Some IO error occurs while the bitmap maniuplation
+ IOError(std::io::Error),
+ #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+ /// Image encoding error
+ ImageError(ImageError),
+}
+
+impl std::fmt::Display for BitMapBackendError {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl std::error::Error for BitMapBackendError {}
+
+#[inline(always)]
+fn blend(prev: &mut u8, new: u8, a: u64) {
+ if new > *prev {
+ *prev += (u64::from(new - *prev) * a / 256) as u8
+ } else {
+ *prev -= (u64::from(*prev - new) * a / 256) as u8
+ }
+}
+
+#[cfg(all(feature = "gif", not(target_arch = "wasm32"), feature = "image"))]
+mod gif_support {
+ use super::*;
+ use gif::{Encoder as GifEncoder, Frame as GifFrame, Repeat, SetParameter};
+ use std::fs::File;
+
+ pub(super) struct GifFile {
+ encoder: GifEncoder<File>,
+ height: u32,
+ width: u32,
+ delay: u32,
+ }
+
+ impl GifFile {
+ pub(super) fn new<T: AsRef<Path>>(
+ path: T,
+ dim: (u32, u32),
+ delay: u32,
+ ) -> Result<Self, BitMapBackendError> {
+ let mut encoder = GifEncoder::new(
+ File::create(path.as_ref()).map_err(BitMapBackendError::IOError)?,
+ dim.0 as u16,
+ dim.1 as u16,
+ &[],
+ )
+ .map_err(BitMapBackendError::IOError)?;
+
+ encoder
+ .set(Repeat::Infinite)
+ .map_err(BitMapBackendError::IOError)?;
+
+ Ok(Self {
+ encoder,
+ width: dim.0,
+ height: dim.1,
+ delay: (delay + 5) / 10,
+ })
+ }
+
+ pub(super) fn flush_frame(&mut self, buffer: &[u8]) -> Result<(), BitMapBackendError> {
+ let mut frame =
+ GifFrame::from_rgb_speed(self.width as u16, self.height as u16, buffer, 10);
+
+ frame.delay = self.delay as u16;
+
+ self.encoder
+ .write_frame(&frame)
+ .map_err(BitMapBackendError::IOError)?;
+
+ Ok(())
+ }
+ }
+}
+
+enum Target<'a> {
+ #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+ File(&'a Path),
+ Buffer(PhantomData<&'a u32>),
+ #[cfg(all(feature = "gif", not(target_arch = "wasm32"), feature = "image"))]
+ Gif(Box<gif_support::GifFile>),
+}
+
+enum Buffer<'a> {
+ #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+ Owned(Vec<u8>),
+ Borrowed(&'a mut [u8]),
+}
+
+impl<'a> Buffer<'a> {
+ #[inline(always)]
+ fn borrow_buffer(&mut self) -> &mut [u8] {
+ match self {
+ #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+ Buffer::Owned(buf) => &mut buf[..],
+ Buffer::Borrowed(buf) => *buf,
+ }
+ }
+}
+
+/// The trait that describes some details about a particular pixel format
+pub trait PixelFormat: Sized {
+ /// Number of bytes per pixel
+ const PIXEL_SIZE: usize;
+
+ /// Number of effective bytes per pixel, e.g. for BGRX pixel format, the size of pixel
+ /// is 4 but the effective size is 3, since the 4th byte isn't used
+ const EFFECTIVE_PIXEL_SIZE: usize;
+
+ /// Encoding a pixel and returns the idx-th byte for the pixel
+ fn byte_at(r: u8, g: u8, b: u8, a: u64, idx: usize) -> u8;
+
+ /// Decode a pixel at the given location
+ fn decode_pixel(data: &[u8]) -> (u8, u8, u8, u64);
+
+ /// The fast alpha blending algorithm for this pixel format
+ ///
+ /// - `target`: The target bitmap backend
+ /// - `upper_left`: The upper-left coord for the rect
+ /// - `bottom_right`: The bottom-right coord for the rect
+ /// - `r`, `g`, `b`, `a`: The blending color and alpha value
+ fn blend_rect_fast(
+ target: &mut BitMapBackend<'_, Self>,
+ upper_left: (i32, i32),
+ bottom_right: (i32, i32),
+ r: u8,
+ g: u8,
+ b: u8,
+ a: f64,
+ );
+
+ /// The fast vertical line filling algorithm
+ ///
+ /// - `target`: The target bitmap backend
+ /// - `x`: the X coordinate for the entire line
+ /// - `ys`: The range of y coord
+ /// - `r`, `g`, `b`: The blending color and alpha value
+ fn fill_vertical_line_fast(
+ target: &mut BitMapBackend<'_, Self>,
+ x: i32,
+ ys: (i32, i32),
+ r: u8,
+ g: u8,
+ b: u8,
+ ) {
+ let (w, h) = target.get_size();
+ let w = w as i32;
+ let h = h as i32;
+
+ // Make sure we are in the range
+ if x < 0 || x >= w {
+ return;
+ }
+
+ let dst = target.get_raw_pixel_buffer();
+ let (mut y0, mut y1) = ys;
+ if y0 > y1 {
+ std::mem::swap(&mut y0, &mut y1);
+ }
+ // And check the y axis isn't out of bound
+ y0 = y0.max(0);
+ y1 = y1.min(h - 1);
+ // This is ok because once y0 > y1, there won't be any iteration anymore
+ for y in y0..=y1 {
+ for idx in 0..Self::EFFECTIVE_PIXEL_SIZE {
+ dst[(y * w + x) as usize * Self::PIXEL_SIZE + idx] = Self::byte_at(r, g, b, 0, idx);
+ }
+ }
+ }
+
+ /// The fast rectangle filling algorithm
+ ///
+ /// - `target`: The target bitmap backend
+ /// - `upper_left`: The upper-left coord for the rect
+ /// - `bottom_right`: The bottom-right coord for the rect
+ /// - `r`, `g`, `b`: The filling color
+ fn fill_rect_fast(
+ target: &mut BitMapBackend<'_, Self>,
+ upper_left: (i32, i32),
+ bottom_right: (i32, i32),
+ r: u8,
+ g: u8,
+ b: u8,
+ );
+
+ #[inline(always)]
+ /// Drawing a single pixel in this format
+ ///
+ /// - `target`: The target bitmap backend
+ /// - `point`: The coord of the point
+ /// - `r`, `g`, `b`: The filling color
+ /// - `alpha`: The alpha value
+ fn draw_pixel(
+ target: &mut BitMapBackend<'_, Self>,
+ point: (i32, i32),
+ (r, g, b): (u8, u8, u8),
+ alpha: f64,
+ ) {
+ let (x, y) = (point.0 as usize, point.1 as usize);
+ let (w, _) = target.get_size();
+ let buf = target.get_raw_pixel_buffer();
+ let w = w as usize;
+ let base = (y * w + x) * Self::PIXEL_SIZE;
+
+ if base < buf.len() {
+ unsafe {
+ if alpha >= 1.0 - 1.0 / 256.0 {
+ for idx in 0..Self::EFFECTIVE_PIXEL_SIZE {
+ *buf.get_unchecked_mut(base + idx) = Self::byte_at(r, g, b, 0, idx);
+ }
+ } else {
+ if alpha <= 0.0 {
+ return;
+ }
+
+ let alpha = (alpha * 256.0).floor() as u64;
+ for idx in 0..Self::EFFECTIVE_PIXEL_SIZE {
+ blend(
+ buf.get_unchecked_mut(base + idx),
+ Self::byte_at(r, g, b, 0, idx),
+ alpha,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ /// Indicates if this pixel format can be saved as image.
+ /// Note: Currently we only using RGB pixel format in the image crate, but later we may lift
+ /// this restriction
+ ///
+ /// - `returns`: If the image can be saved as image file
+ fn can_be_saved() -> bool {
+ false
+ }
+}
+
+/// The marker type that indicates we are currently using a RGB888 pixel format
+pub struct RGBPixel;
+
+/// The marker type that indicates we are currently using a BGRX8888 pixel format
+pub struct BGRXPixel;
+
+impl PixelFormat for RGBPixel {
+ const PIXEL_SIZE: usize = 3;
+ const EFFECTIVE_PIXEL_SIZE: usize = 3;
+
+ #[inline(always)]
+ fn byte_at(r: u8, g: u8, b: u8, _a: u64, idx: usize) -> u8 {
+ match idx {
+ 0 => r,
+ 1 => g,
+ 2 => b,
+ _ => 0xff,
+ }
+ }
+
+ #[inline(always)]
+ fn decode_pixel(data: &[u8]) -> (u8, u8, u8, u64) {
+ (data[0], data[1], data[2], 0x255)
+ }
+
+ fn can_be_saved() -> bool {
+ true
+ }
+
+ #[allow(clippy::many_single_char_names, clippy::cast_ptr_alignment)]
+ fn blend_rect_fast(
+ target: &mut BitMapBackend<'_, Self>,
+ upper_left: (i32, i32),
+ bottom_right: (i32, i32),
+ r: u8,
+ g: u8,
+ b: u8,
+ a: f64,
+ ) {
+ let (w, h) = target.get_size();
+ let a = a.min(1.0).max(0.0);
+ if a == 0.0 {
+ return;
+ }
+
+ let (x0, y0) = (
+ upper_left.0.min(bottom_right.0).max(0),
+ upper_left.1.min(bottom_right.1).max(0),
+ );
+ let (x1, y1) = (
+ upper_left.0.max(bottom_right.0).min(w as i32 - 1),
+ upper_left.1.max(bottom_right.1).min(h as i32 - 1),
+ );
+
+ // This may happen when the minimal value is larger than the limit.
+ // Thus we just have something that is completely out-of-range
+ if x0 > x1 || y0 > y1 {
+ return;
+ }
+
+ let dst = target.get_raw_pixel_buffer();
+
+ let a = (256.0 * a).floor() as u64;
+
+ // Since we should always make sure the RGB payload occupies the logic lower bits
+ // thus, this type purning should work for both LE and BE CPUs
+ #[rustfmt::skip]
+ let (p1, p2, p3): (u64, u64, u64) = unsafe {
+ std::mem::transmute([
+ u16::from(r), u16::from(b), u16::from(g), u16::from(r), // QW1
+ u16::from(b), u16::from(g), u16::from(r), u16::from(b), // QW2
+ u16::from(g), u16::from(r), u16::from(b), u16::from(g), // QW3
+ ])
+ };
+
+ #[rustfmt::skip]
+ let (q1, q2, q3): (u64, u64, u64) = unsafe {
+ std::mem::transmute([
+ u16::from(g), u16::from(r), u16::from(b), u16::from(g), // QW1
+ u16::from(r), u16::from(b), u16::from(g), u16::from(r), // QW2
+ u16::from(b), u16::from(g), u16::from(r), u16::from(b), // QW3
+ ])
+ };
+
+ const N: u64 = 0xff00_ff00_ff00_ff00;
+ const M: u64 = 0x00ff_00ff_00ff_00ff;
+
+ for y in y0..=y1 {
+ let start = (y * w as i32 + x0) as usize;
+ let count = (x1 - x0 + 1) as usize;
+
+ let start_ptr = &mut dst[start * Self::PIXEL_SIZE] as *mut u8 as *mut [u8; 24];
+ let slice = unsafe { std::slice::from_raw_parts_mut(start_ptr, (count - 1) / 8) };
+ for p in slice.iter_mut() {
+ let ptr = p as *mut [u8; 24] as *mut (u64, u64, u64);
+ let (d1, d2, d3) = unsafe { *ptr };
+ let (mut h1, mut h2, mut h3) = ((d1 >> 8) & M, (d2 >> 8) & M, (d3 >> 8) & M);
+ let (mut l1, mut l2, mut l3) = (d1 & M, d2 & M, d3 & M);
+
+ #[cfg(target_endian = "little")]
+ {
+ h1 = (h1 * (256 - a) + q1 * a) & N;
+ h2 = (h2 * (256 - a) + q2 * a) & N;
+ h3 = (h3 * (256 - a) + q3 * a) & N;
+ l1 = ((l1 * (256 - a) + p1 * a) & N) >> 8;
+ l2 = ((l2 * (256 - a) + p2 * a) & N) >> 8;
+ l3 = ((l3 * (256 - a) + p3 * a) & N) >> 8;
+ }
+
+ #[cfg(target_endian = "big")]
+ {
+ h1 = (h1 * (256 - a) + p1 * a) & N;
+ h2 = (h2 * (256 - a) + p2 * a) & N;
+ h3 = (h3 * (256 - a) + p3 * a) & N;
+ l1 = ((l1 * (256 - a) + q1 * a) & N) >> 8;
+ l2 = ((l2 * (256 - a) + q2 * a) & N) >> 8;
+ l3 = ((l3 * (256 - a) + q3 * a) & N) >> 8;
+ }
+
+ unsafe {
+ *ptr = (h1 | l1, h2 | l2, h3 | l3);
+ }
+ }
+
+ let mut iter = dst[((start + slice.len() * 8) * Self::PIXEL_SIZE)
+ ..((start + count) * Self::PIXEL_SIZE)]
+ .iter_mut();
+ for _ in (slice.len() * 8)..count {
+ blend(iter.next().unwrap(), r, a);
+ blend(iter.next().unwrap(), g, a);
+ blend(iter.next().unwrap(), b, a);
+ }
+ }
+ }
+
+ #[allow(clippy::many_single_char_names, clippy::cast_ptr_alignment)]
+ fn fill_rect_fast(
+ target: &mut BitMapBackend<'_, Self>,
+ upper_left: (i32, i32),
+ bottom_right: (i32, i32),
+ r: u8,
+ g: u8,
+ b: u8,
+ ) {
+ let (w, h) = target.get_size();
+ let (x0, y0) = (
+ upper_left.0.min(bottom_right.0).max(0),
+ upper_left.1.min(bottom_right.1).max(0),
+ );
+ let (x1, y1) = (
+ upper_left.0.max(bottom_right.0).min(w as i32 - 1),
+ upper_left.1.max(bottom_right.1).min(h as i32 - 1),
+ );
+
+ // This may happen when the minimal value is larger than the limit.
+ // Thus we just have something that is completely out-of-range
+ if x0 > x1 || y0 > y1 {
+ return;
+ }
+
+ let dst = target.get_raw_pixel_buffer();
+
+ if r == g && g == b {
+ // If r == g == b, then we can use memset
+ if x0 != 0 || x1 != w as i32 - 1 {
+ // If it's not the entire row is filled, we can only do
+ // memset per row
+ for y in y0..=y1 {
+ let start = (y * w as i32 + x0) as usize;
+ let count = (x1 - x0 + 1) as usize;
+ dst[(start * Self::PIXEL_SIZE)..((start + count) * Self::PIXEL_SIZE)]
+ .iter_mut()
+ .for_each(|e| *e = r);
+ }
+ } else {
+ // If the entire memory block is going to be filled, just use single memset
+ dst[Self::PIXEL_SIZE * (y0 * w as i32) as usize
+ ..((y1 + 1) * w as i32) as usize * Self::PIXEL_SIZE]
+ .iter_mut()
+ .for_each(|e| *e = r);
+ }
+ } else {
+ let count = (x1 - x0 + 1) as usize;
+ if count < 8 {
+ for y in y0..=y1 {
+ let start = (y * w as i32 + x0) as usize;
+ let mut iter = dst
+ [(start * Self::PIXEL_SIZE)..((start + count) * Self::PIXEL_SIZE)]
+ .iter_mut();
+ for _ in 0..=(x1 - x0) {
+ *iter.next().unwrap() = r;
+ *iter.next().unwrap() = g;
+ *iter.next().unwrap() = b;
+ }
+ }
+ } else {
+ for y in y0..=y1 {
+ let start = (y * w as i32 + x0) as usize;
+ let start_ptr = &mut dst[start * Self::PIXEL_SIZE] as *mut u8 as *mut [u8; 24];
+ let slice =
+ unsafe { std::slice::from_raw_parts_mut(start_ptr, (count - 1) / 8) };
+ for p in slice.iter_mut() {
+ // In this case, we can actually fill 8 pixels in one iteration with
+ // only 3 movq instructions.
+ // TODO: Consider using AVX instructions when possible
+ let ptr = p as *mut [u8; 24] as *mut u64;
+ unsafe {
+ let (d1, d2, d3): (u64, u64, u64) = std::mem::transmute([
+ r, g, b, r, g, b, r, g, // QW1
+ b, r, g, b, r, g, b, r, // QW2
+ g, b, r, g, b, r, g, b, // QW3
+ ]);
+ *ptr = d1;
+ *ptr.offset(1) = d2;
+ *ptr.offset(2) = d3;
+ }
+ }
+
+ for idx in (slice.len() * 8)..count {
+ dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE] = r;
+ dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE + 1] = g;
+ dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE + 2] = b;
+ }
+ }
+ }
+ }
+ }
+}
+
+impl PixelFormat for BGRXPixel {
+ const PIXEL_SIZE: usize = 4;
+ const EFFECTIVE_PIXEL_SIZE: usize = 3;
+
+ #[inline(always)]
+ fn byte_at(r: u8, g: u8, b: u8, _a: u64, idx: usize) -> u8 {
+ match idx {
+ 0 => b,
+ 1 => g,
+ 2 => r,
+ _ => 0xff,
+ }
+ }
+
+ #[inline(always)]
+ fn decode_pixel(data: &[u8]) -> (u8, u8, u8, u64) {
+ (data[2], data[1], data[0], 0x255)
+ }
+
+ #[allow(clippy::many_single_char_names, clippy::cast_ptr_alignment)]
+ fn blend_rect_fast(
+ target: &mut BitMapBackend<'_, Self>,
+ upper_left: (i32, i32),
+ bottom_right: (i32, i32),
+ r: u8,
+ g: u8,
+ b: u8,
+ a: f64,
+ ) {
+ let (w, h) = target.get_size();
+ let a = a.min(1.0).max(0.0);
+ if a == 0.0 {
+ return;
+ }
+
+ let (x0, y0) = (
+ upper_left.0.min(bottom_right.0).max(0),
+ upper_left.1.min(bottom_right.1).max(0),
+ );
+ let (x1, y1) = (
+ upper_left.0.max(bottom_right.0).min(w as i32 - 1),
+ upper_left.1.max(bottom_right.1).min(h as i32 - 1),
+ );
+
+ // This may happen when the minimal value is larger than the limit.
+ // Thus we just have something that is completely out-of-range
+ if x0 > x1 || y0 > y1 {
+ return;
+ }
+
+ let dst = target.get_raw_pixel_buffer();
+
+ let a = (256.0 * a).floor() as u64;
+
+ // Since we should always make sure the RGB payload occupies the logic lower bits
+ // thus, this type purning should work for both LE and BE CPUs
+ #[rustfmt::skip]
+ let p: u64 = unsafe {
+ std::mem::transmute([
+ u16::from(b), u16::from(r), u16::from(b), u16::from(r), // QW1
+ ])
+ };
+
+ #[rustfmt::skip]
+ let q: u64 = unsafe {
+ std::mem::transmute([
+ u16::from(g), 0u16, u16::from(g), 0u16, // QW1
+ ])
+ };
+
+ const N: u64 = 0xff00_ff00_ff00_ff00;
+ const M: u64 = 0x00ff_00ff_00ff_00ff;
+
+ for y in y0..=y1 {
+ let start = (y * w as i32 + x0) as usize;
+ let count = (x1 - x0 + 1) as usize;
+
+ let start_ptr = &mut dst[start * Self::PIXEL_SIZE] as *mut u8 as *mut [u8; 8];
+ let slice = unsafe { std::slice::from_raw_parts_mut(start_ptr, (count - 1) / 2) };
+ for rp in slice.iter_mut() {
+ let ptr = rp as *mut [u8; 8] as *mut u64;
+ let d1 = unsafe { *ptr };
+ let mut h = (d1 >> 8) & M;
+ let mut l = d1 & M;
+
+ #[cfg(target_endian = "little")]
+ {
+ h = (h * (256 - a) + q * a) & N;
+ l = ((l * (256 - a) + p * a) & N) >> 8;
+ }
+
+ #[cfg(target_endian = "big")]
+ {
+ h = (h * (256 - a) + p * a) & N;
+ l = ((l * (256 - a) + q * a) & N) >> 8;
+ }
+
+ unsafe {
+ *ptr = h | l;
+ }
+ }
+
+ let mut iter = dst[((start + slice.len() * 2) * Self::PIXEL_SIZE)
+ ..((start + count) * Self::PIXEL_SIZE)]
+ .iter_mut();
+ for _ in (slice.len() * 2)..count {
+ blend(iter.next().unwrap(), b, a);
+ blend(iter.next().unwrap(), g, a);
+ blend(iter.next().unwrap(), r, a);
+ iter.next();
+ }
+ }
+ }
+
+ #[allow(clippy::many_single_char_names, clippy::cast_ptr_alignment)]
+ fn fill_rect_fast(
+ target: &mut BitMapBackend<'_, Self>,
+ upper_left: (i32, i32),
+ bottom_right: (i32, i32),
+ r: u8,
+ g: u8,
+ b: u8,
+ ) {
+ let (w, h) = target.get_size();
+ let (x0, y0) = (
+ upper_left.0.min(bottom_right.0).max(0),
+ upper_left.1.min(bottom_right.1).max(0),
+ );
+ let (x1, y1) = (
+ upper_left.0.max(bottom_right.0).min(w as i32 - 1),
+ upper_left.1.max(bottom_right.1).min(h as i32 - 1),
+ );
+
+ // This may happen when the minimal value is larger than the limit.
+ // Thus we just have something that is completely out-of-range
+ if x0 > x1 || y0 > y1 {
+ return;
+ }
+
+ let dst = target.get_raw_pixel_buffer();
+
+ if r == g && g == b {
+ // If r == g == b, then we can use memset
+ if x0 != 0 || x1 != w as i32 - 1 {
+ // If it's not the entire row is filled, we can only do
+ // memset per row
+ for y in y0..=y1 {
+ let start = (y * w as i32 + x0) as usize;
+ let count = (x1 - x0 + 1) as usize;
+ dst[(start * Self::PIXEL_SIZE)..((start + count) * Self::PIXEL_SIZE)]
+ .iter_mut()
+ .for_each(|e| *e = r);
+ }
+ } else {
+ // If the entire memory block is going to be filled, just use single memset
+ dst[Self::PIXEL_SIZE * (y0 * w as i32) as usize
+ ..((y1 + 1) * w as i32) as usize * Self::PIXEL_SIZE]
+ .iter_mut()
+ .for_each(|e| *e = r);
+ }
+ } else {
+ let count = (x1 - x0 + 1) as usize;
+ if count < 8 {
+ for y in y0..=y1 {
+ let start = (y * w as i32 + x0) as usize;
+ let mut iter = dst
+ [(start * Self::PIXEL_SIZE)..((start + count) * Self::PIXEL_SIZE)]
+ .iter_mut();
+ for _ in 0..=(x1 - x0) {
+ *iter.next().unwrap() = b;
+ *iter.next().unwrap() = g;
+ *iter.next().unwrap() = r;
+ iter.next();
+ }
+ }
+ } else {
+ for y in y0..=y1 {
+ let start = (y * w as i32 + x0) as usize;
+ let start_ptr = &mut dst[start * Self::PIXEL_SIZE] as *mut u8 as *mut [u8; 8];
+ let slice =
+ unsafe { std::slice::from_raw_parts_mut(start_ptr, (count - 1) / 2) };
+ for p in slice.iter_mut() {
+ // In this case, we can actually fill 8 pixels in one iteration with
+ // only 3 movq instructions.
+ // TODO: Consider using AVX instructions when possible
+ let ptr = p as *mut [u8; 8] as *mut u64;
+ unsafe {
+ let d: u64 = std::mem::transmute([
+ b, g, r, 0, b, g, r, 0, // QW1
+ ]);
+ *ptr = d;
+ }
+ }
+
+ for idx in (slice.len() * 2)..count {
+ dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE] = b;
+ dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE + 1] = g;
+ dst[start * Self::PIXEL_SIZE + idx * Self::PIXEL_SIZE + 2] = r;
+ }
+ }
+ }
+ }
+ }
+}
+
+/// The backend that drawing a bitmap
+pub struct BitMapBackend<'a, P: PixelFormat = RGBPixel> {
+ /// The path to the image
+ #[allow(dead_code)]
+ target: Target<'a>,
+ /// The size of the image
+ size: (u32, u32),
+ /// The data buffer of the image
+ buffer: Buffer<'a>,
+ /// Flag indicates if the bitmap has been saved
+ saved: bool,
+ _pantomdata: PhantomData<P>,
+}
+
+impl<'a, P: PixelFormat> BitMapBackend<'a, P> {
+ /// The number of bytes per pixel
+ const PIXEL_SIZE: usize = P::PIXEL_SIZE;
+}
+
+impl<'a> BitMapBackend<'a, RGBPixel> {
+ /// Create a new bitmap backend
+ #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+ pub fn new<T: AsRef<Path> + ?Sized>(path: &'a T, (w, h): (u32, u32)) -> Self {
+ Self {
+ target: Target::File(path.as_ref()),
+ size: (w, h),
+ buffer: Buffer::Owned(vec![0; Self::PIXEL_SIZE * (w * h) as usize]),
+ saved: false,
+ _pantomdata: PhantomData,
+ }
+ }
+
+ /// Create a new bitmap backend that generate GIF animation
+ ///
+ /// When this is used, the bitmap backend acts similar to a real-time rendering backend.
+ /// When the program finished drawing one frame, use `present` function to flush the frame
+ /// into the GIF file.
+ ///
+ /// - `path`: The path to the GIF file to create
+ /// - `dimension`: The size of the GIF image
+ /// - `speed`: The amount of time for each frame to display
+ #[cfg(all(feature = "gif", not(target_arch = "wasm32"), feature = "image"))]
+ pub fn gif<T: AsRef<Path>>(
+ path: T,
+ (w, h): (u32, u32),
+ frame_delay: u32,
+ ) -> Result<Self, BitMapBackendError> {
+ Ok(Self {
+ target: Target::Gif(Box::new(gif_support::GifFile::new(
+ path,
+ (w, h),
+ frame_delay,
+ )?)),
+ size: (w, h),
+ buffer: Buffer::Owned(vec![0; Self::PIXEL_SIZE * (w * h) as usize]),
+ saved: false,
+ _pantomdata: PhantomData,
+ })
+ }
+
+ /// Create a new bitmap backend which only lives in-memory
+ ///
+ /// When this is used, the bitmap backend will write to a user provided [u8] array (or Vec<u8>)
+ /// in RGB pixel format.
+ ///
+ /// Note: This function provides backward compatibility for those code that assumes Plotters
+ /// uses RGB pixel format and maniuplates the in-memory framebuffer.
+ /// For more pixel format option, use `with_buffer_and_format` instead.
+ ///
+ /// - `buf`: The buffer to operate
+ /// - `dimension`: The size of the image in pixels
+ /// - **returns**: The newly created bitmap backend
+ pub fn with_buffer(buf: &'a mut [u8], (w, h): (u32, u32)) -> Self {
+ Self::with_buffer_and_format(buf, (w, h)).expect("Wrong buffer size")
+ }
+}
+
+impl<'a, P: PixelFormat> BitMapBackend<'a, P> {
+ /// Create a new bitmap backend with a in-memory buffer with specific pixel format.
+ ///
+ /// Note: This can be used as a way to manipulate framebuffer, `mmap` can be used on the top of this
+ /// as well.
+ ///
+ /// - `buf`: The buffer to operate
+ /// - `dimension`: The size of the image in pixels
+ /// - **returns**: The newly created bitmap backend
+ pub fn with_buffer_and_format(
+ buf: &'a mut [u8],
+ (w, h): (u32, u32),
+ ) -> Result<Self, BitMapBackendError> {
+ if (w * h) as usize * Self::PIXEL_SIZE > buf.len() {
+ return Err(BitMapBackendError::InvalidBuffer);
+ }
+
+ Ok(Self {
+ target: Target::Buffer(PhantomData),
+ size: (w, h),
+ buffer: Buffer::Borrowed(buf),
+ saved: false,
+ _pantomdata: PhantomData,
+ })
+ }
+
+ #[inline(always)]
+ fn get_raw_pixel_buffer(&mut self) -> &mut [u8] {
+ self.buffer.borrow_buffer()
+ }
+
+ /// Split a bitmap backend vertically into several sub drawing area which allows
+ /// multi-threading rendering.
+ ///
+ /// - `area_size`: The size of the area
+ /// - **returns**: The splitted backends that can be rendered in parallel
+ pub fn split(&mut self, area_size: &[u32]) -> Vec<BitMapBackend<P>> {
+ let (w, h) = self.get_size();
+ let buf = self.get_raw_pixel_buffer();
+
+ let base_addr = &mut buf[0] as *mut u8;
+ let mut split_points = vec![0];
+ for size in area_size {
+ let next = split_points.last().unwrap() + size;
+ if next >= h {
+ break;
+ }
+ split_points.push(next);
+ }
+ split_points.push(h);
+
+ split_points
+ .iter()
+ .zip(split_points.iter().skip(1))
+ .map(|(begin, end)| {
+ let actual_buf = unsafe {
+ std::slice::from_raw_parts_mut(
+ base_addr.offset((begin * w) as isize * Self::PIXEL_SIZE as isize),
+ ((end - begin) * w) as usize * Self::PIXEL_SIZE,
+ )
+ };
+ Self::with_buffer_and_format(actual_buf, (w, end - begin)).unwrap()
+ })
+ .collect()
+ }
+}
+
+impl<'a, P: PixelFormat> DrawingBackend for BitMapBackend<'a, P> {
+ type ErrorType = BitMapBackendError;
+
+ fn get_size(&self) -> (u32, u32) {
+ self.size
+ }
+
+ fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<BitMapBackendError>> {
+ self.saved = false;
+ Ok(())
+ }
+
+ #[cfg(any(target_arch = "wasm32", not(feature = "image")))]
+ fn present(&mut self) -> Result<(), DrawingErrorKind<BitMapBackendError>> {
+ Ok(())
+ }
+
+ #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+ fn present(&mut self) -> Result<(), DrawingErrorKind<BitMapBackendError>> {
+ if !P::can_be_saved() {
+ return Ok(());
+ }
+ let (w, h) = self.get_size();
+ match &mut self.target {
+ Target::File(path) => {
+ if let Some(img) = BorrowedImage::from_raw(w, h, self.buffer.borrow_buffer()) {
+ img.save(&path).map_err(|x| {
+ DrawingErrorKind::DrawingError(match x {
+ ImageError::IoError(x) => BitMapBackendError::IOError(x),
+ whatever => BitMapBackendError::IOError(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ format!("{}", whatever),
+ )),
+ })
+ })?;
+ self.saved = true;
+ Ok(())
+ } else {
+ Err(DrawingErrorKind::DrawingError(
+ BitMapBackendError::InvalidBuffer,
+ ))
+ }
+ }
+ Target::Buffer(_) => Ok(()),
+
+ #[cfg(all(feature = "gif", not(target_arch = "wasm32"), feature = "image"))]
+ Target::Gif(target) => {
+ target
+ .flush_frame(self.buffer.borrow_buffer())
+ .map_err(DrawingErrorKind::DrawingError)?;
+ self.saved = true;
+ Ok(())
+ }
+ }
+ }
+
+ fn draw_pixel(
+ &mut self,
+ point: BackendCoord,
+ color: &RGBAColor,
+ ) -> Result<(), DrawingErrorKind<BitMapBackendError>> {
+ if point.0 < 0 || point.1 < 0 {
+ return Ok(());
+ }
+
+ let alpha = color.alpha();
+ let rgb = color.rgb();
+
+ P::draw_pixel(self, point, rgb, alpha);
+
+ Ok(())
+ }
+
+ fn draw_line<S: BackendStyle>(
+ &mut self,
+ from: (i32, i32),
+ to: (i32, i32),
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let alpha = style.as_color().alpha();
+ let (r, g, b) = style.as_color().rgb();
+
+ if (from.0 == to.0 || from.1 == to.1) && style.stroke_width() == 1 {
+ if alpha >= 1.0 {
+ if from.1 == to.1 {
+ P::fill_rect_fast(self, from, to, r, g, b);
+ } else {
+ P::fill_vertical_line_fast(self, from.0, (from.1, to.1), r, g, b);
+ }
+ } else {
+ P::blend_rect_fast(self, from, to, r, g, b, alpha);
+ }
+ return Ok(());
+ }
+
+ crate::drawing::rasterizer::draw_line(self, from, to, style)
+ }
+
+ fn draw_rect<S: BackendStyle>(
+ &mut self,
+ upper_left: (i32, i32),
+ bottom_right: (i32, i32),
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let alpha = style.as_color().alpha();
+ let (r, g, b) = style.as_color().rgb();
+ if fill {
+ if alpha >= 1.0 {
+ P::fill_rect_fast(self, upper_left, bottom_right, r, g, b);
+ } else {
+ P::blend_rect_fast(self, upper_left, bottom_right, r, g, b, alpha);
+ }
+ return Ok(());
+ }
+ crate::drawing::rasterizer::draw_rect(self, upper_left, bottom_right, style, fill)
+ }
+
+ fn blit_bitmap<'b>(
+ &mut self,
+ pos: BackendCoord,
+ (sw, sh): (u32, u32),
+ src: &'b [u8],
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let (dw, dh) = self.get_size();
+
+ let (x0, y0) = pos;
+ let (x1, y1) = (x0 + sw as i32, y0 + sh as i32);
+
+ let (x0, y0, x1, y1) = (x0.max(0), y0.max(0), x1.min(dw as i32), y1.min(dh as i32));
+
+ if x0 == x1 || y0 == y1 {
+ return Ok(());
+ }
+
+ let mut chunk_size = (x1 - x0) as usize;
+ let mut num_chunks = (y1 - y0) as usize;
+ let dst_gap = dw as usize - chunk_size;
+ let src_gap = sw as usize - chunk_size;
+
+ let dst_start = Self::PIXEL_SIZE * (y0 as usize * dw as usize + x0 as usize);
+
+ let mut dst = &mut self.get_raw_pixel_buffer()[dst_start..];
+
+ let src_start =
+ Self::PIXEL_SIZE * ((sh as i32 + y0 - y1) * sw as i32 + (sw as i32 + x0 - x1)) as usize;
+ let mut src = &src[src_start..];
+
+ if src_gap == 0 && dst_gap == 0 {
+ chunk_size *= num_chunks;
+ num_chunks = 1;
+ }
+ for i in 0..num_chunks {
+ dst[0..(chunk_size * Self::PIXEL_SIZE)]
+ .copy_from_slice(&src[0..(chunk_size * Self::PIXEL_SIZE)]);
+ if i != num_chunks - 1 {
+ dst = &mut dst[((chunk_size + dst_gap) * Self::PIXEL_SIZE)..];
+ src = &src[((chunk_size + src_gap) * Self::PIXEL_SIZE)..];
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl<P: PixelFormat> Drop for BitMapBackend<'_, P> {
+ fn drop(&mut self) {
+ if !self.saved {
+ // drop should not panic, so we ignore a failed present
+ let _ = self.present();
+ }
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_bitmap_backend() {
+ use crate::prelude::*;
+ let mut buffer = vec![0; 10 * 10 * 3];
+
+ {
+ let back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
+
+ let area = back.into_drawing_area();
+ area.fill(&WHITE).unwrap();
+ area.draw(&PathElement::new(vec![(0, 0), (10, 10)], RED.filled()))
+ .unwrap();
+ area.present().unwrap();
+ }
+
+ for i in 0..10 {
+ assert_eq!(buffer[i * 33], 255);
+ assert_eq!(buffer[i * 33 + 1], 0);
+ assert_eq!(buffer[i * 33 + 2], 0);
+ buffer[i * 33] = 255;
+ buffer[i * 33 + 1] = 255;
+ buffer[i * 33 + 2] = 255;
+ }
+
+ assert!(buffer.into_iter().all(|x| x == 255));
+}
+
+#[cfg(test)]
+#[test]
+fn test_bitmap_backend_fill_half() {
+ use crate::prelude::*;
+ let mut buffer = vec![0; 10 * 10 * 3];
+
+ {
+ let back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
+
+ let area = back.into_drawing_area();
+ area.draw(&Rectangle::new([(0, 0), (5, 10)], RED.filled()))
+ .unwrap();
+ area.present().unwrap();
+ }
+ for x in 0..10 {
+ for y in 0..10 {
+ assert_eq!(
+ buffer[(y * 10 + x) as usize * 3 + 0],
+ if x <= 5 { 255 } else { 0 }
+ );
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 1], 0);
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 2], 0);
+ }
+ }
+
+ let mut buffer = vec![0; 10 * 10 * 3];
+
+ {
+ let back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
+
+ let area = back.into_drawing_area();
+ area.draw(&Rectangle::new([(0, 0), (10, 5)], RED.filled()))
+ .unwrap();
+ area.present().unwrap();
+ }
+ for x in 0..10 {
+ for y in 0..10 {
+ assert_eq!(
+ buffer[(y * 10 + x) as usize * 3 + 0],
+ if y <= 5 { 255 } else { 0 }
+ );
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 1], 0);
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 2], 0);
+ }
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_bitmap_backend_blend() {
+ use crate::prelude::*;
+ let mut buffer = vec![255; 10 * 10 * 3];
+
+ {
+ let back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
+
+ let area = back.into_drawing_area();
+ area.draw(&Rectangle::new(
+ [(0, 0), (5, 10)],
+ RGBColor(0, 100, 200).mix(0.2).filled(),
+ ))
+ .unwrap();
+ area.present().unwrap();
+ }
+
+ for x in 0..10 {
+ for y in 0..10 {
+ let (r, g, b) = if x <= 5 {
+ (205, 225, 245)
+ } else {
+ (255, 255, 255)
+ };
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 0], r);
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 1], g);
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 2], b);
+ }
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_bitmap_backend_split_and_fill() {
+ use crate::prelude::*;
+ let mut buffer = vec![255; 10 * 10 * 3];
+
+ {
+ let mut back = BitMapBackend::with_buffer(&mut buffer, (10, 10));
+
+ for (sub_backend, color) in back.split(&[5]).into_iter().zip([&RED, &GREEN].iter()) {
+ sub_backend.into_drawing_area().fill(*color).unwrap();
+ }
+ }
+
+ for x in 0..10 {
+ for y in 0..10 {
+ let (r, g, b) = if y < 5 { (255, 0, 0) } else { (0, 255, 0) };
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 0], r);
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 1], g);
+ assert_eq!(buffer[(y * 10 + x) as usize * 3 + 2], b);
+ }
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_draw_rect_out_of_range() {
+ use crate::prelude::*;
+ let mut buffer = vec![0; 1099 * 1000 * 3];
+
+ {
+ let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
+
+ back.draw_line((1100, 0), (1100, 999), &RED.to_rgba())
+ .unwrap();
+ back.draw_line((0, 1100), (999, 1100), &RED.to_rgba())
+ .unwrap();
+ back.draw_rect((1100, 0), (1100, 999), &RED.to_rgba(), true)
+ .unwrap();
+ }
+
+ for x in 0..1000 {
+ for y in 0..1000 {
+ assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 0], 0);
+ assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 1], 0);
+ assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 2], 0);
+ }
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_draw_line_out_of_range() {
+ use crate::prelude::*;
+ let mut buffer = vec![0; 1000 * 1000 * 3];
+
+ {
+ let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
+
+ back.draw_line((-1000, -1000), (2000, 2000), &WHITE.to_rgba())
+ .unwrap();
+
+ back.draw_line((999, -1000), (999, 2000), &WHITE.to_rgba())
+ .unwrap();
+ }
+
+ for x in 0..1000 {
+ for y in 0..1000 {
+ let expected_value = if x == y || x == 999 { 255 } else { 0 };
+ assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 0], expected_value);
+ assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 1], expected_value);
+ assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 2], expected_value);
+ }
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_bitmap_blend_large() {
+ use crate::prelude::*;
+ let mut buffer = vec![0; 1000 * 1000 * 3];
+
+ for fill_color in [RED, GREEN, BLUE].iter() {
+ buffer.iter_mut().for_each(|x| *x = 0);
+
+ {
+ let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
+
+ back.draw_rect((0, 0), (1000, 1000), &WHITE.mix(0.1), true)
+ .unwrap(); // should be (24, 24, 24)
+ back.draw_rect((0, 0), (100, 100), &fill_color.mix(0.5), true)
+ .unwrap(); // should be (139, 24, 24)
+ }
+
+ for x in 0..1000 {
+ for y in 0..1000 {
+ let expected_value = if x <= 100 && y <= 100 {
+ let (r, g, b) = fill_color.to_rgba().rgb();
+ (
+ if r > 0 { 139 } else { 12 },
+ if g > 0 { 139 } else { 12 },
+ if b > 0 { 139 } else { 12 },
+ )
+ } else {
+ (24, 24, 24)
+ };
+ assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 0], expected_value.0);
+ assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 1], expected_value.1);
+ assert_eq!(buffer[(y * 1000 + x) as usize * 3 + 2], expected_value.2);
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_bitmap_bgrx_pixel_format() {
+ use crate::drawing::bitmap_pixel::BGRXPixel;
+ use crate::prelude::*;
+ let mut rgb_buffer = vec![0; 1000 * 1000 * 3];
+ let mut bgrx_buffer = vec![0; 1000 * 1000 * 4];
+
+ {
+ let mut rgb_back = BitMapBackend::with_buffer(&mut rgb_buffer, (1000, 1000));
+ let mut bgrx_back =
+ BitMapBackend::<BGRXPixel>::with_buffer_and_format(&mut bgrx_buffer, (1000, 1000))
+ .unwrap();
+
+ rgb_back
+ .draw_rect((0, 0), (1000, 1000), &BLACK, true)
+ .unwrap();
+ bgrx_back
+ .draw_rect((0, 0), (1000, 1000), &BLACK, true)
+ .unwrap();
+
+ rgb_back
+ .draw_rect(
+ (0, 0),
+ (1000, 1000),
+ &RGBColor(0xaa, 0xbb, 0xcc).mix(0.85),
+ true,
+ )
+ .unwrap();
+ bgrx_back
+ .draw_rect(
+ (0, 0),
+ (1000, 1000),
+ &RGBColor(0xaa, 0xbb, 0xcc).mix(0.85),
+ true,
+ )
+ .unwrap();
+
+ rgb_back
+ .draw_rect((0, 0), (1000, 1000), &RED.mix(0.85), true)
+ .unwrap();
+ bgrx_back
+ .draw_rect((0, 0), (1000, 1000), &RED.mix(0.85), true)
+ .unwrap();
+
+ rgb_back.draw_circle((300, 300), 100, &GREEN, true).unwrap();
+ bgrx_back
+ .draw_circle((300, 300), 100, &GREEN, true)
+ .unwrap();
+
+ rgb_back.draw_rect((10, 10), (50, 50), &BLUE, true).unwrap();
+ bgrx_back
+ .draw_rect((10, 10), (50, 50), &BLUE, true)
+ .unwrap();
+
+ rgb_back
+ .draw_rect((10, 10), (50, 50), &WHITE, true)
+ .unwrap();
+ bgrx_back
+ .draw_rect((10, 10), (50, 50), &WHITE, true)
+ .unwrap();
+
+ rgb_back
+ .draw_rect((10, 10), (15, 50), &YELLOW, true)
+ .unwrap();
+ bgrx_back
+ .draw_rect((10, 10), (15, 50), &YELLOW, true)
+ .unwrap();
+ }
+
+ for x in 0..1000 {
+ for y in 0..1000 {
+ assert!(
+ (rgb_buffer[y * 3000 + x * 3 + 0] as i32
+ - bgrx_buffer[y * 4000 + x * 4 + 2] as i32)
+ .abs()
+ <= 1
+ );
+ assert!(
+ (rgb_buffer[y * 3000 + x * 3 + 1] as i32
+ - bgrx_buffer[y * 4000 + x * 4 + 1] as i32)
+ .abs()
+ <= 1
+ );
+ assert!(
+ (rgb_buffer[y * 3000 + x * 3 + 2] as i32
+ - bgrx_buffer[y * 4000 + x * 4 + 0] as i32)
+ .abs()
+ <= 1
+ );
+ }
+ }
+}
+#[cfg(test)]
+#[test]
+fn test_draw_simple_lines() {
+ use crate::prelude::*;
+ let mut buffer = vec![0; 1000 * 1000 * 3];
+
+ {
+ let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
+ back.draw_line((500, 0), (500, 1000), &WHITE.filled().stroke_width(5))
+ .unwrap();
+ }
+
+ let nz_count = buffer.into_iter().filter(|x| *x != 0).count();
+
+ assert_eq!(nz_count, 6 * 1000 * 3);
+}
+
+#[cfg(test)]
+#[test]
+fn test_bitmap_blit() {
+ let src_bitmap: Vec<u8> = (0..100)
+ .map(|y| (0..300).map(move |x| ((x * y) % 253) as u8))
+ .flatten()
+ .collect();
+
+ use crate::prelude::*;
+ let mut buffer = vec![0; 1000 * 1000 * 3];
+
+ {
+ let mut back = BitMapBackend::with_buffer(&mut buffer, (1000, 1000));
+ back.blit_bitmap((500, 500), (100, 100), &src_bitmap[..])
+ .unwrap();
+ }
+
+ for y in 0..1000 {
+ for x in 0..1000 {
+ if x >= 500 && x < 600 && y >= 500 && y < 600 {
+ let lx = x - 500;
+ let ly = y - 500;
+ assert_eq!(buffer[y * 3000 + x * 3 + 0] as usize, (ly * lx * 3) % 253);
+ assert_eq!(
+ buffer[y * 3000 + x * 3 + 1] as usize,
+ (ly * (lx * 3 + 1)) % 253
+ );
+ assert_eq!(
+ buffer[y * 3000 + x * 3 + 2] as usize,
+ (ly * (lx * 3 + 2)) % 253
+ );
+ } else {
+ assert_eq!(buffer[y * 3000 + x * 3 + 0], 0);
+ assert_eq!(buffer[y * 3000 + x * 3 + 1], 0);
+ assert_eq!(buffer[y * 3000 + x * 3 + 2], 0);
+ }
+ }
+ }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+#[cfg(test)]
+mod test {
+ use crate::prelude::*;
+ use crate::style::text_anchor::{HPos, Pos, VPos};
+ use image::{ImageBuffer, Rgb};
+ use std::fs;
+ use std::path::Path;
+
+ static DST_DIR: &str = "target/test/bitmap";
+
+ fn checked_save_file(name: &str, content: &[u8], w: u32, h: u32) {
+ /*
+ Please use the PNG file to manually verify the results.
+ */
+ assert!(content.iter().any(|x| *x != 0));
+ fs::create_dir_all(DST_DIR).unwrap();
+ let file_name = format!("{}.png", name);
+ let file_path = Path::new(DST_DIR).join(file_name);
+ println!("{:?} created", file_path);
+ let img = ImageBuffer::<Rgb<u8>, &[u8]>::from_raw(w, h, content).unwrap();
+ img.save(&file_path).unwrap();
+ }
+
+ fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
+ let (width, height) = (500, 500);
+ let mut buffer = vec![0; (width * height * 3) as usize];
+ {
+ let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
+ root.fill(&WHITE).unwrap();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("This is a test", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..10, 0..10)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .set_all_tick_mark_size(tick_size)
+ .draw()
+ .unwrap();
+ }
+ checked_save_file(test_name, &buffer, width, height);
+ }
+
+ #[test]
+ fn test_draw_mesh_no_ticks() {
+ draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
+ }
+
+ #[test]
+ fn test_draw_mesh_negative_ticks() {
+ draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
+ }
+
+ #[test]
+ fn test_text_draw() {
+ let (width, height) = (1500, 800);
+ let mut buffer = vec![0; (width * height * 3) as usize];
+ {
+ let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
+ root.fill(&WHITE).unwrap();
+ let root = root
+ .titled("Image Title", ("sans-serif", 60).into_font())
+ .unwrap();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("All anchor point positions", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..100, 0..50)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .x_desc("X Axis")
+ .y_desc("Y Axis")
+ .draw()
+ .unwrap();
+
+ let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30));
+
+ for (dy, trans) in [
+ FontTransform::None,
+ FontTransform::Rotate90,
+ FontTransform::Rotate180,
+ FontTransform::Rotate270,
+ ]
+ .iter()
+ .enumerate()
+ {
+ for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() {
+ for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() {
+ let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150;
+ let y = 120 + dy as i32 * 150;
+ let draw = |x, y, text| {
+ root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap();
+ let style = TextStyle::from(("sans-serif", 20).into_font())
+ .pos(Pos::new(*h_pos, *v_pos))
+ .transform(trans.clone());
+ root.draw_text(text, &style, (x, y)).unwrap();
+ };
+ draw(x + x1, y + y1, "dood");
+ draw(x + x2, y + y2, "dog");
+ draw(x + x3, y + y3, "goog");
+ }
+ }
+ }
+ }
+ checked_save_file("test_text_draw", &buffer, width, height);
+ }
+
+ #[test]
+ fn test_text_clipping() {
+ let (width, height) = (500_i32, 500_i32);
+ let mut buffer = vec![0; (width * height * 3) as usize];
+ {
+ let root = BitMapBackend::with_buffer(&mut buffer, (width as u32, height as u32))
+ .into_drawing_area();
+ root.fill(&WHITE).unwrap();
+
+ let style = TextStyle::from(("sans-serif", 20).into_font())
+ .pos(Pos::new(HPos::Center, VPos::Center));
+ root.draw_text("TOP LEFT", &style, (0, 0)).unwrap();
+ root.draw_text("TOP CENTER", &style, (width / 2, 0))
+ .unwrap();
+ root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap();
+
+ root.draw_text("MIDDLE LEFT", &style, (0, height / 2))
+ .unwrap();
+ root.draw_text("MIDDLE RIGHT", &style, (width, height / 2))
+ .unwrap();
+
+ root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap();
+ root.draw_text("BOTTOM CENTER", &style, (width / 2, height))
+ .unwrap();
+ root.draw_text("BOTTOM RIGHT", &style, (width, height))
+ .unwrap();
+ }
+ checked_save_file("test_text_clipping", &buffer, width as u32, height as u32);
+ }
+
+ #[test]
+ fn test_series_labels() {
+ let (width, height) = (500, 500);
+ let mut buffer = vec![0; (width * height * 3) as usize];
+ {
+ let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
+ root.fill(&WHITE).unwrap();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("All series label positions", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..50, 0..50)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .draw()
+ .unwrap();
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
+ .expect("Drawing error")
+ .label("Series 1")
+ .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
+ .expect("Drawing error")
+ .label("Series 2")
+ .legend(|(x, y)| Circle::new((x, y), 3, BLUE.filled()));
+
+ for pos in vec![
+ SeriesLabelPosition::UpperLeft,
+ SeriesLabelPosition::MiddleLeft,
+ SeriesLabelPosition::LowerLeft,
+ SeriesLabelPosition::UpperMiddle,
+ SeriesLabelPosition::MiddleMiddle,
+ SeriesLabelPosition::LowerMiddle,
+ SeriesLabelPosition::UpperRight,
+ SeriesLabelPosition::MiddleRight,
+ SeriesLabelPosition::LowerRight,
+ SeriesLabelPosition::Coordinate(70, 70),
+ ]
+ .into_iter()
+ {
+ chart
+ .configure_series_labels()
+ .border_style(&BLACK.mix(0.5))
+ .position(pos)
+ .draw()
+ .expect("Drawing error");
+ }
+ }
+ checked_save_file("test_series_labels", &buffer, width, height);
+ }
+
+ #[test]
+ fn test_draw_pixel_alphas() {
+ let (width, height) = (100_i32, 100_i32);
+ let mut buffer = vec![0; (width * height * 3) as usize];
+ {
+ let root = BitMapBackend::with_buffer(&mut buffer, (width as u32, height as u32))
+ .into_drawing_area();
+ root.fill(&WHITE).unwrap();
+ for i in -20..20 {
+ let alpha = i as f64 * 0.1;
+ root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
+ .unwrap();
+ }
+ }
+ checked_save_file(
+ "test_draw_pixel_alphas",
+ &buffer,
+ width as u32,
+ height as u32,
+ );
+ }
+}
diff --git a/src/drawing/backend_impl/cairo.rs b/src/drawing/backend_impl/cairo.rs
new file mode 100644
index 0000000..a65bb7b
--- /dev/null
+++ b/src/drawing/backend_impl/cairo.rs
@@ -0,0 +1,567 @@
+use cairo::{Context as CairoContext, FontSlant, FontWeight, Status as CairoStatus};
+
+#[allow(unused_imports)]
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
+use crate::style::text_anchor::{HPos, VPos};
+#[allow(unused_imports)]
+use crate::style::{Color, FontDesc, FontStyle, FontTransform, RGBAColor, TextStyle};
+
+/// The drawing backend that is backed with a Cairo context
+pub struct CairoBackend<'a> {
+ context: &'a CairoContext,
+ width: u32,
+ height: u32,
+ init_flag: bool,
+}
+
+#[derive(Debug)]
+pub struct CairoError(CairoStatus);
+
+impl std::fmt::Display for CairoError {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(fmt, "{:?}", self)
+ }
+}
+
+impl std::error::Error for CairoError {}
+
+impl<'a> CairoBackend<'a> {
+ /// Call cairo functions and verify the cairo status afterward.
+ ///
+ /// All major cairo objects retain an error status internally
+ /// which can be queried anytime by the users using status() method.
+ /// In the mean time, it is safe to call all cairo functions normally even
+ /// if the underlying object is in an error status.
+ /// This means that no error handling code is required before or after
+ /// each individual cairo function call.
+ ///
+ /// - `f`: The function to call
+ /// - *Returns* The wrapped result of the function
+ fn call_cairo<T, F: Fn(&CairoContext) -> T>(
+ &self,
+ f: F,
+ ) -> Result<T, DrawingErrorKind<CairoError>> {
+ let result = f(self.context);
+ let status = self.context.status();
+ if status == CairoStatus::Success {
+ Ok(result)
+ } else {
+ Err(DrawingErrorKind::DrawingError(CairoError(status)))
+ }
+ }
+
+ fn set_color(&self, color: &RGBAColor) -> Result<(), DrawingErrorKind<CairoError>> {
+ self.call_cairo(|c| {
+ c.set_source_rgba(
+ f64::from(color.rgb().0) / 255.0,
+ f64::from(color.rgb().1) / 255.0,
+ f64::from(color.rgb().2) / 255.0,
+ color.alpha(),
+ )
+ })
+ }
+
+ fn set_stroke_width(&self, width: u32) -> Result<(), DrawingErrorKind<CairoError>> {
+ self.call_cairo(|c| c.set_line_width(f64::from(width)))
+ }
+
+ fn set_font<'b>(&self, font: &FontDesc<'b>) -> Result<(), DrawingErrorKind<CairoError>> {
+ let actual_size = font.get_size();
+ self.call_cairo(|c| {
+ match font.get_style() {
+ FontStyle::Normal => {
+ c.select_font_face(font.get_name(), FontSlant::Normal, FontWeight::Normal)
+ }
+ FontStyle::Bold => {
+ c.select_font_face(font.get_name(), FontSlant::Normal, FontWeight::Bold)
+ }
+ FontStyle::Oblique => {
+ c.select_font_face(font.get_name(), FontSlant::Oblique, FontWeight::Normal)
+ }
+ FontStyle::Italic => {
+ c.select_font_face(font.get_name(), FontSlant::Italic, FontWeight::Normal)
+ }
+ };
+ c.set_font_size(actual_size);
+ })
+ }
+
+ pub fn new(context: &'a CairoContext, (w, h): (u32, u32)) -> Result<Self, CairoError> {
+ let ret = Self {
+ context,
+ width: w,
+ height: h,
+ init_flag: false,
+ };
+ Ok(ret)
+ }
+}
+
+impl<'a> DrawingBackend for CairoBackend<'a> {
+ type ErrorType = CairoError;
+
+ fn get_size(&self) -> (u32, u32) {
+ (self.width, self.height)
+ }
+
+ fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if !self.init_flag {
+ self.call_cairo(|c| {
+ let (x0, y0, x1, y1) = c.clip_extents();
+ c.scale(
+ (x1 - x0) / f64::from(self.width),
+ (y1 - y0) / f64::from(self.height),
+ )
+ })?;
+ self.init_flag = true;
+ }
+ Ok(())
+ }
+
+ fn present(&mut self) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ Ok(())
+ }
+
+ fn draw_pixel(
+ &mut self,
+ point: BackendCoord,
+ color: &RGBAColor,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.call_cairo(|c| {
+ c.rectangle(f64::from(point.0), f64::from(point.1), 1.0, 1.0);
+ c.set_source_rgba(
+ f64::from(color.rgb().0) / 255.0,
+ f64::from(color.rgb().1) / 255.0,
+ f64::from(color.rgb().2) / 255.0,
+ color.alpha(),
+ );
+ c.fill();
+ })
+ }
+
+ fn draw_line<S: BackendStyle>(
+ &mut self,
+ from: BackendCoord,
+ to: BackendCoord,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.set_color(&style.as_color())?;
+ self.set_stroke_width(style.stroke_width())?;
+
+ self.call_cairo(|c| {
+ c.move_to(f64::from(from.0), f64::from(from.1));
+ c.line_to(f64::from(to.0), f64::from(to.1));
+ c.stroke();
+ })
+ }
+
+ fn draw_rect<S: BackendStyle>(
+ &mut self,
+ upper_left: BackendCoord,
+ bottom_right: BackendCoord,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.set_color(&style.as_color())?;
+ self.set_stroke_width(style.stroke_width())?;
+
+ self.call_cairo(|c| {
+ c.rectangle(
+ f64::from(upper_left.0),
+ f64::from(upper_left.1),
+ f64::from(bottom_right.0 - upper_left.0),
+ f64::from(bottom_right.1 - upper_left.1),
+ );
+ if fill {
+ c.fill();
+ } else {
+ c.stroke();
+ }
+ })
+ }
+
+ fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ path: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.set_color(&style.as_color())?;
+ self.set_stroke_width(style.stroke_width())?;
+
+ let mut path = path.into_iter();
+ if let Some((x, y)) = path.next() {
+ self.call_cairo(|c| c.move_to(f64::from(x), f64::from(y)))?;
+ }
+
+ for (x, y) in path {
+ self.call_cairo(|c| c.line_to(f64::from(x), f64::from(y)))?;
+ }
+
+ self.call_cairo(|c| c.stroke())
+ }
+
+ fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ path: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.set_color(&style.as_color())?;
+ self.set_stroke_width(style.stroke_width())?;
+
+ let mut path = path.into_iter();
+
+ if let Some((x, y)) = path.next() {
+ self.call_cairo(|c| c.move_to(f64::from(x), f64::from(y)))?;
+
+ for (x, y) in path {
+ self.call_cairo(|c| c.line_to(f64::from(x), f64::from(y)))?;
+ }
+
+ self.call_cairo(|c| {
+ c.close_path();
+ c.fill();
+ })
+ } else {
+ Ok(())
+ }
+ }
+
+ fn draw_circle<S: BackendStyle>(
+ &mut self,
+ center: BackendCoord,
+ radius: u32,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.set_color(&style.as_color())?;
+ self.set_stroke_width(style.stroke_width())?;
+
+ self.call_cairo(|c| {
+ c.new_sub_path();
+ c.arc(
+ f64::from(center.0),
+ f64::from(center.1),
+ f64::from(radius),
+ 0.0,
+ std::f64::consts::PI * 2.0,
+ );
+
+ if fill {
+ c.fill();
+ } else {
+ c.stroke();
+ }
+ })
+ }
+
+ fn estimate_text_size<'b>(
+ &self,
+ text: &str,
+ font: &FontDesc<'b>,
+ ) -> Result<(u32, u32), DrawingErrorKind<Self::ErrorType>> {
+ self.set_font(&font)?;
+ self.call_cairo(|c| {
+ let extents = c.text_extents(text);
+ (extents.width as u32, extents.height as u32)
+ })
+ }
+
+ fn draw_text(
+ &mut self,
+ text: &str,
+ style: &TextStyle,
+ pos: BackendCoord,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let font = &style.font;
+ let color = &style.color;
+ let (mut x, mut y) = (pos.0, pos.1);
+
+ let degree = match font.get_transform() {
+ FontTransform::None => 0.0,
+ FontTransform::Rotate90 => 90.0,
+ FontTransform::Rotate180 => 180.0,
+ FontTransform::Rotate270 => 270.0,
+ } / 180.0
+ * std::f64::consts::PI;
+
+ if degree != 0.0 {
+ self.call_cairo(|c| {
+ c.save();
+ c.translate(f64::from(x), f64::from(y));
+ c.rotate(degree);
+ })?;
+ x = 0;
+ y = 0;
+ }
+
+ self.set_font(&font)?;
+ self.set_color(&color)?;
+
+ self.call_cairo(|c| {
+ let extents = c.text_extents(text);
+ let dx = match style.pos.h_pos {
+ HPos::Left => 0.0,
+ HPos::Right => -extents.width,
+ HPos::Center => -extents.width / 2.0,
+ };
+ let dy = match style.pos.v_pos {
+ VPos::Top => extents.height,
+ VPos::Center => extents.height / 2.0,
+ VPos::Bottom => 0.0,
+ };
+ c.move_to(
+ f64::from(x) + dx - extents.x_bearing,
+ f64::from(y) + dy - extents.y_bearing - extents.height,
+ );
+ c.show_text(text);
+ if degree != 0.0 {
+ c.restore();
+ }
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::prelude::*;
+ use crate::style::text_anchor::{HPos, Pos, VPos};
+ use std::fs;
+ use std::path::Path;
+
+ static DST_DIR: &str = "target/test/cairo";
+
+ fn checked_save_file(name: &str, content: &str) {
+ /*
+ Please use the PS file to manually verify the results.
+
+ You may want to use Ghostscript to view the file.
+ */
+ assert!(!content.is_empty());
+ fs::create_dir_all(DST_DIR).unwrap();
+ let file_name = format!("{}.ps", name);
+ let file_path = Path::new(DST_DIR).join(file_name);
+ println!("{:?} created", file_path);
+ fs::write(file_path, &content).unwrap();
+ }
+
+ fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
+ let buffer: Vec<u8> = vec![];
+ let surface = cairo::PsSurface::for_stream(500.0, 500.0, buffer).unwrap();
+ let cr = CairoContext::new(&surface);
+ let root = CairoBackend::new(&cr, (500, 500))
+ .unwrap()
+ .into_drawing_area();
+
+ // Text could be rendered to different elements if has whitespaces
+ let mut chart = ChartBuilder::on(&root)
+ .caption("this-is-a-test", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..10, 0..10)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .set_all_tick_mark_size(tick_size)
+ .draw()
+ .unwrap();
+
+ let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
+ let content = String::from_utf8(buffer).unwrap();
+ checked_save_file(test_name, &content);
+
+ assert!(content.contains("this-is-a-test"));
+ }
+
+ #[test]
+ fn test_draw_mesh_no_ticks() {
+ draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
+ }
+
+ #[test]
+ fn test_draw_mesh_negative_ticks() {
+ draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
+ }
+
+ #[test]
+ fn test_text_draw() {
+ let buffer: Vec<u8> = vec![];
+ let (width, height) = (1500, 800);
+ let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
+ let cr = CairoContext::new(&surface);
+ let root = CairoBackend::new(&cr, (width, height))
+ .unwrap()
+ .into_drawing_area();
+ let root = root
+ .titled("Image Title", ("sans-serif", 60).into_font())
+ .unwrap();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("All anchor point positions", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..100, 0..50)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .x_desc("X Axis")
+ .y_desc("Y Axis")
+ .draw()
+ .unwrap();
+
+ let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30));
+
+ for (dy, trans) in [
+ FontTransform::None,
+ FontTransform::Rotate90,
+ FontTransform::Rotate180,
+ FontTransform::Rotate270,
+ ]
+ .iter()
+ .enumerate()
+ {
+ for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() {
+ for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() {
+ let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150;
+ let y = 120 + dy as i32 * 150;
+ let draw = |x, y, text| {
+ root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap();
+ let style = TextStyle::from(("sans-serif", 20).into_font())
+ .pos(Pos::new(*h_pos, *v_pos))
+ .transform(trans.clone());
+ root.draw_text(text, &style, (x, y)).unwrap();
+ };
+ draw(x + x1, y + y1, "dood");
+ draw(x + x2, y + y2, "dog");
+ draw(x + x3, y + y3, "goog");
+ }
+ }
+ }
+
+ let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
+ let content = String::from_utf8(buffer).unwrap();
+ checked_save_file("test_text_draw", &content);
+
+ assert_eq!(content.matches("dog").count(), 36);
+ assert_eq!(content.matches("dood").count(), 36);
+ assert_eq!(content.matches("goog").count(), 36);
+ }
+
+ #[test]
+ fn test_text_clipping() {
+ let buffer: Vec<u8> = vec![];
+ let (width, height) = (500_i32, 500_i32);
+ let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
+ let cr = CairoContext::new(&surface);
+ let root = CairoBackend::new(&cr, (width as u32, height as u32))
+ .unwrap()
+ .into_drawing_area();
+
+ let style = TextStyle::from(("sans-serif", 20).into_font())
+ .pos(Pos::new(HPos::Center, VPos::Center));
+ root.draw_text("TOP LEFT", &style, (0, 0)).unwrap();
+ root.draw_text("TOP CENTER", &style, (width / 2, 0))
+ .unwrap();
+ root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap();
+
+ root.draw_text("MIDDLE LEFT", &style, (0, height / 2))
+ .unwrap();
+ root.draw_text("MIDDLE RIGHT", &style, (width, height / 2))
+ .unwrap();
+
+ root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap();
+ root.draw_text("BOTTOM CENTER", &style, (width / 2, height))
+ .unwrap();
+ root.draw_text("BOTTOM RIGHT", &style, (width, height))
+ .unwrap();
+
+ let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
+ let content = String::from_utf8(buffer).unwrap();
+ checked_save_file("test_text_clipping", &content);
+ }
+
+ #[test]
+ fn test_series_labels() {
+ let buffer: Vec<u8> = vec![];
+ let (width, height) = (500, 500);
+ let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
+ let cr = CairoContext::new(&surface);
+ let root = CairoBackend::new(&cr, (width, height))
+ .unwrap()
+ .into_drawing_area();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("All series label positions", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..50, 0..50)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .draw()
+ .unwrap();
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
+ .expect("Drawing error")
+ .label("Series 1")
+ .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
+ .expect("Drawing error")
+ .label("Series 2")
+ .legend(|(x, y)| Circle::new((x, y), 3, BLUE.filled()));
+
+ for pos in vec![
+ SeriesLabelPosition::UpperLeft,
+ SeriesLabelPosition::MiddleLeft,
+ SeriesLabelPosition::LowerLeft,
+ SeriesLabelPosition::UpperMiddle,
+ SeriesLabelPosition::MiddleMiddle,
+ SeriesLabelPosition::LowerMiddle,
+ SeriesLabelPosition::UpperRight,
+ SeriesLabelPosition::MiddleRight,
+ SeriesLabelPosition::LowerRight,
+ SeriesLabelPosition::Coordinate(70, 70),
+ ]
+ .into_iter()
+ {
+ chart
+ .configure_series_labels()
+ .border_style(&BLACK.mix(0.5))
+ .position(pos)
+ .draw()
+ .expect("Drawing error");
+ }
+
+ let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
+ let content = String::from_utf8(buffer).unwrap();
+ checked_save_file("test_series_labels", &content);
+ }
+
+ #[test]
+ fn test_draw_pixel_alphas() {
+ let buffer: Vec<u8> = vec![];
+ let (width, height) = (100_i32, 100_i32);
+ let surface = cairo::PsSurface::for_stream(width.into(), height.into(), buffer).unwrap();
+ let cr = CairoContext::new(&surface);
+ let root = CairoBackend::new(&cr, (width as u32, height as u32))
+ .unwrap()
+ .into_drawing_area();
+
+ for i in -20..20 {
+ let alpha = i as f64 * 0.1;
+ root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
+ .unwrap();
+ }
+
+ let buffer = *surface.finish_output_stream().unwrap().downcast().unwrap();
+ let content = String::from_utf8(buffer).unwrap();
+ checked_save_file("test_draw_pixel_alphas", &content);
+ }
+}
diff --git a/src/drawing/backend_impl/canvas.rs b/src/drawing/backend_impl/canvas.rs
new file mode 100644
index 0000000..f57639f
--- /dev/null
+++ b/src/drawing/backend_impl/canvas.rs
@@ -0,0 +1,530 @@
+use js_sys::JSON;
+use wasm_bindgen::{JsCast, JsValue};
+use web_sys::{window, CanvasRenderingContext2d, HtmlCanvasElement};
+
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
+use crate::style::text_anchor::{HPos, VPos};
+use crate::style::{Color, FontTransform, RGBAColor, TextStyle};
+
+/// The backend that is drawing on the HTML canvas
+/// TODO: Support double buffering
+pub struct CanvasBackend {
+ canvas: HtmlCanvasElement,
+ context: CanvasRenderingContext2d,
+}
+
+pub struct CanvasError(String);
+
+impl std::fmt::Display for CanvasError {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ return write!(fmt, "Canvas Error: {}", self.0);
+ }
+}
+
+impl std::fmt::Debug for CanvasError {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ return write!(fmt, "CanvasError({})", self.0);
+ }
+}
+
+impl From<JsValue> for DrawingErrorKind<CanvasError> {
+ fn from(e: JsValue) -> DrawingErrorKind<CanvasError> {
+ DrawingErrorKind::DrawingError(CanvasError(
+ JSON::stringify(&e)
+ .map(|s| Into::<String>::into(&s))
+ .unwrap_or_else(|_| "Unknown".to_string()),
+ ))
+ }
+}
+
+impl std::error::Error for CanvasError {}
+
+impl CanvasBackend {
+ fn init_backend(canvas: HtmlCanvasElement) -> Option<Self> {
+ let context: CanvasRenderingContext2d = canvas.get_context("2d").ok()??.dyn_into().ok()?;
+ Some(CanvasBackend { canvas, context })
+ }
+
+ /// Create a new drawing backend backed with an HTML5 canvas object with given Id
+ /// - `elem_id` The element id for the canvas
+ /// - Return either some drawing backend has been created, or none in error case
+ pub fn new(elem_id: &str) -> Option<Self> {
+ let document = window()?.document()?;
+ let canvas = document.get_element_by_id(elem_id)?;
+ let canvas: HtmlCanvasElement = canvas.dyn_into().ok()?;
+ Self::init_backend(canvas)
+ }
+
+ /// Create a new drawing backend backend with a HTML5 canvas object passed in
+ /// - `canvas` The object we want to use as backend
+ /// - Return either the drawing backend or None for error
+ pub fn with_canvas_object(canvas: HtmlCanvasElement) -> Option<Self> {
+ Self::init_backend(canvas)
+ }
+}
+
+fn make_canvas_color(color: RGBAColor) -> JsValue {
+ let (r, g, b) = color.rgb();
+ let a = color.alpha();
+ format!("rgba({},{},{},{})", r, g, b, a).into()
+}
+
+impl DrawingBackend for CanvasBackend {
+ type ErrorType = CanvasError;
+
+ fn get_size(&self) -> (u32, u32) {
+ // Getting just canvas.width gives poor results on HighDPI screens.
+ let rect = self.canvas.get_bounding_client_rect();
+ (rect.width() as u32, rect.height() as u32)
+ }
+
+ fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
+ Ok(())
+ }
+
+ fn present(&mut self) -> Result<(), DrawingErrorKind<CanvasError>> {
+ Ok(())
+ }
+
+ fn draw_pixel(
+ &mut self,
+ point: BackendCoord,
+ style: &RGBAColor,
+ ) -> Result<(), DrawingErrorKind<CanvasError>> {
+ if style.alpha() == 0.0 {
+ return Ok(());
+ }
+
+ self.context
+ .set_fill_style(&make_canvas_color(style.as_color()));
+ self.context
+ .fill_rect(f64::from(point.0), f64::from(point.1), 1.0, 1.0);
+ Ok(())
+ }
+
+ fn draw_line<S: BackendStyle>(
+ &mut self,
+ from: BackendCoord,
+ to: BackendCoord,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+
+ self.context
+ .set_stroke_style(&make_canvas_color(style.as_color()));
+ self.context.set_line_width(style.stroke_width() as f64);
+ self.context.begin_path();
+ self.context.move_to(f64::from(from.0), f64::from(from.1));
+ self.context.line_to(f64::from(to.0), f64::from(to.1));
+ self.context.stroke();
+ Ok(())
+ }
+
+ fn draw_rect<S: BackendStyle>(
+ &mut self,
+ upper_left: BackendCoord,
+ bottom_right: BackendCoord,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+ if fill {
+ self.context
+ .set_fill_style(&make_canvas_color(style.as_color()));
+ self.context.fill_rect(
+ f64::from(upper_left.0),
+ f64::from(upper_left.1),
+ f64::from(bottom_right.0 - upper_left.0),
+ f64::from(bottom_right.1 - upper_left.1),
+ );
+ } else {
+ self.context
+ .set_stroke_style(&make_canvas_color(style.as_color()));
+ self.context.stroke_rect(
+ f64::from(upper_left.0),
+ f64::from(upper_left.1),
+ f64::from(bottom_right.0 - upper_left.0),
+ f64::from(bottom_right.1 - upper_left.1),
+ );
+ }
+ Ok(())
+ }
+
+ fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ path: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+ let mut path = path.into_iter();
+ self.context.begin_path();
+ if let Some(start) = path.next() {
+ self.context
+ .set_stroke_style(&make_canvas_color(style.as_color()));
+ self.context.move_to(f64::from(start.0), f64::from(start.1));
+ for next in path {
+ self.context.line_to(f64::from(next.0), f64::from(next.1));
+ }
+ }
+ self.context.stroke();
+ Ok(())
+ }
+
+ fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ path: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+ let mut path = path.into_iter();
+ self.context.begin_path();
+ if let Some(start) = path.next() {
+ self.context
+ .set_fill_style(&make_canvas_color(style.as_color()));
+ self.context.move_to(f64::from(start.0), f64::from(start.1));
+ for next in path {
+ self.context.line_to(f64::from(next.0), f64::from(next.1));
+ }
+ self.context.close_path();
+ }
+ self.context.fill();
+ Ok(())
+ }
+
+ fn draw_circle<S: BackendStyle>(
+ &mut self,
+ center: BackendCoord,
+ radius: u32,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+ if fill {
+ self.context
+ .set_fill_style(&make_canvas_color(style.as_color()));
+ } else {
+ self.context
+ .set_stroke_style(&make_canvas_color(style.as_color()));
+ }
+ self.context.begin_path();
+ self.context.arc(
+ f64::from(center.0),
+ f64::from(center.1),
+ f64::from(radius),
+ 0.0,
+ std::f64::consts::PI * 2.0,
+ )?;
+ if fill {
+ self.context.fill();
+ } else {
+ self.context.stroke();
+ }
+ Ok(())
+ }
+
+ fn draw_text(
+ &mut self,
+ text: &str,
+ style: &TextStyle,
+ pos: BackendCoord,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let font = &style.font;
+ let color = &style.color;
+ if color.alpha() == 0.0 {
+ return Ok(());
+ }
+
+ let (mut x, mut y) = (pos.0, pos.1);
+
+ let degree = match font.get_transform() {
+ FontTransform::None => 0.0,
+ FontTransform::Rotate90 => 90.0,
+ FontTransform::Rotate180 => 180.0,
+ FontTransform::Rotate270 => 270.0,
+ } / 180.0
+ * std::f64::consts::PI;
+
+ if degree != 0.0 {
+ self.context.save();
+ self.context.translate(f64::from(x), f64::from(y))?;
+ self.context.rotate(degree)?;
+ x = 0;
+ y = 0;
+ }
+
+ let text_baseline = match style.pos.v_pos {
+ VPos::Top => "top",
+ VPos::Center => "middle",
+ VPos::Bottom => "bottom",
+ };
+ self.context.set_text_baseline(text_baseline);
+
+ let text_align = match style.pos.h_pos {
+ HPos::Left => "start",
+ HPos::Right => "end",
+ HPos::Center => "center",
+ };
+ self.context.set_text_align(text_align);
+
+ self.context
+ .set_fill_style(&make_canvas_color(color.clone()));
+ self.context.set_font(&format!(
+ "{} {}px {}",
+ font.get_style().as_str(),
+ font.get_size(),
+ font.get_name()
+ ));
+ self.context.fill_text(text, f64::from(x), f64::from(y))?;
+
+ if degree != 0.0 {
+ self.context.restore();
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::element::Circle;
+ use crate::prelude::*;
+ use crate::style::text_anchor::Pos;
+ use wasm_bindgen_test::wasm_bindgen_test_configure;
+ use wasm_bindgen_test::*;
+ use web_sys::Document;
+
+ wasm_bindgen_test_configure!(run_in_browser);
+
+ fn create_canvas(document: &Document, id: &str, width: u32, height: u32) -> HtmlCanvasElement {
+ let canvas = document
+ .create_element("canvas")
+ .unwrap()
+ .dyn_into::<HtmlCanvasElement>()
+ .unwrap();
+ let div = document.create_element("div").unwrap();
+ div.append_child(&canvas).unwrap();
+ document.body().unwrap().append_child(&div).unwrap();
+ canvas.set_attribute("id", id).unwrap();
+ canvas.set_width(width);
+ canvas.set_height(height);
+ canvas
+ }
+
+ fn check_content(document: &Document, id: &str) {
+ let canvas = document
+ .get_element_by_id(id)
+ .unwrap()
+ .dyn_into::<HtmlCanvasElement>()
+ .unwrap();
+ let data_uri = canvas.to_data_url().unwrap();
+ let prefix = "data:image/png;base64,";
+ assert!(&data_uri.starts_with(prefix));
+ }
+
+ fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
+ let document = window().unwrap().document().unwrap();
+ let canvas = create_canvas(&document, test_name, 500, 500);
+ let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
+ let root = backend.into_drawing_area();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("This is a test", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..10, 0..10)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .set_all_tick_mark_size(tick_size)
+ .draw()
+ .unwrap();
+
+ check_content(&document, test_name);
+ }
+
+ #[wasm_bindgen_test]
+ fn test_draw_mesh_no_ticks() {
+ draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
+ }
+
+ #[wasm_bindgen_test]
+ fn test_draw_mesh_negative_ticks() {
+ draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
+ }
+
+ #[wasm_bindgen_test]
+ fn test_text_draw() {
+ let document = window().unwrap().document().unwrap();
+ let canvas = create_canvas(&document, "test_text_draw", 1500, 800);
+ let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
+ let root = backend.into_drawing_area();
+ let root = root
+ .titled("Image Title", ("sans-serif", 60).into_font())
+ .unwrap();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("All anchor point positions", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..100, 0..50)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .x_desc("X Axis")
+ .y_desc("Y Axis")
+ .draw()
+ .unwrap();
+
+ let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30));
+
+ for (dy, trans) in [
+ FontTransform::None,
+ FontTransform::Rotate90,
+ FontTransform::Rotate180,
+ FontTransform::Rotate270,
+ ]
+ .iter()
+ .enumerate()
+ {
+ for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() {
+ for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() {
+ let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150;
+ let y = 120 + dy as i32 * 150;
+ let draw = |x, y, text| {
+ root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap();
+ let style = TextStyle::from(("sans-serif", 20).into_font())
+ .pos(Pos::new(*h_pos, *v_pos))
+ .transform(trans.clone());
+ root.draw_text(text, &style, (x, y)).unwrap();
+ };
+ draw(x + x1, y + y1, "dood");
+ draw(x + x2, y + y2, "dog");
+ draw(x + x3, y + y3, "goog");
+ }
+ }
+ }
+ check_content(&document, "test_text_draw");
+ }
+
+ #[wasm_bindgen_test]
+ fn test_text_clipping() {
+ let (width, height) = (500_i32, 500_i32);
+ let document = window().unwrap().document().unwrap();
+ let canvas = create_canvas(&document, "test_text_clipping", width as u32, height as u32);
+ let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
+ let root = backend.into_drawing_area();
+
+ let style = TextStyle::from(("sans-serif", 20).into_font())
+ .pos(Pos::new(HPos::Center, VPos::Center));
+ root.draw_text("TOP LEFT", &style, (0, 0)).unwrap();
+ root.draw_text("TOP CENTER", &style, (width / 2, 0))
+ .unwrap();
+ root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap();
+
+ root.draw_text("MIDDLE LEFT", &style, (0, height / 2))
+ .unwrap();
+ root.draw_text("MIDDLE RIGHT", &style, (width, height / 2))
+ .unwrap();
+
+ root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap();
+ root.draw_text("BOTTOM CENTER", &style, (width / 2, height))
+ .unwrap();
+ root.draw_text("BOTTOM RIGHT", &style, (width, height))
+ .unwrap();
+
+ check_content(&document, "test_text_clipping");
+ }
+
+ #[wasm_bindgen_test]
+ fn test_series_labels() {
+ let (width, height) = (500, 500);
+ let document = window().unwrap().document().unwrap();
+ let canvas = create_canvas(&document, "test_series_labels", width, height);
+ let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
+ let root = backend.into_drawing_area();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("All series label positions", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..50, 0..50)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .draw()
+ .unwrap();
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
+ .expect("Drawing error")
+ .label("Series 1")
+ .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
+ .expect("Drawing error")
+ .label("Series 2")
+ .legend(|(x, y)| Circle::new((x, y), 3, BLUE.filled()));
+
+ for pos in vec![
+ SeriesLabelPosition::UpperLeft,
+ SeriesLabelPosition::MiddleLeft,
+ SeriesLabelPosition::LowerLeft,
+ SeriesLabelPosition::UpperMiddle,
+ SeriesLabelPosition::MiddleMiddle,
+ SeriesLabelPosition::LowerMiddle,
+ SeriesLabelPosition::UpperRight,
+ SeriesLabelPosition::MiddleRight,
+ SeriesLabelPosition::LowerRight,
+ SeriesLabelPosition::Coordinate(70, 70),
+ ]
+ .into_iter()
+ {
+ chart
+ .configure_series_labels()
+ .border_style(&BLACK.mix(0.5))
+ .position(pos)
+ .draw()
+ .expect("Drawing error");
+ }
+
+ check_content(&document, "test_series_labels");
+ }
+
+ #[wasm_bindgen_test]
+ fn test_draw_pixel_alphas() {
+ let (width, height) = (100_i32, 100_i32);
+ let document = window().unwrap().document().unwrap();
+ let canvas = create_canvas(
+ &document,
+ "test_draw_pixel_alphas",
+ width as u32,
+ height as u32,
+ );
+ let backend = CanvasBackend::with_canvas_object(canvas).expect("cannot find canvas");
+ let root = backend.into_drawing_area();
+
+ for i in -20..20 {
+ let alpha = i as f64 * 0.1;
+ root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
+ .unwrap();
+ }
+
+ check_content(&document, "test_draw_pixel_alphas");
+ }
+}
diff --git a/src/drawing/backend_impl/mocked.rs b/src/drawing/backend_impl/mocked.rs
new file mode 100644
index 0000000..b416ba9
--- /dev/null
+++ b/src/drawing/backend_impl/mocked.rs
@@ -0,0 +1,276 @@
+use crate::coord::Shift;
+use crate::drawing::area::IntoDrawingArea;
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
+use crate::drawing::DrawingArea;
+use crate::style::{Color, RGBAColor, TextStyle};
+
+use std::collections::VecDeque;
+
+pub struct MockedBackend {
+ height: u32,
+ width: u32,
+ init_count: u32,
+ pub draw_count: u32,
+ pub num_draw_pixel_call: u32,
+ pub num_draw_line_call: u32,
+ pub num_draw_rect_call: u32,
+ pub num_draw_circle_call: u32,
+ pub num_draw_text_call: u32,
+ pub num_draw_path_call: u32,
+ pub num_fill_polygon_call: u32,
+ check_draw_pixel: VecDeque<Box<dyn FnMut(RGBAColor, BackendCoord)>>,
+ check_draw_line: VecDeque<Box<dyn FnMut(RGBAColor, u32, BackendCoord, BackendCoord)>>,
+ check_draw_rect: VecDeque<Box<dyn FnMut(RGBAColor, u32, bool, BackendCoord, BackendCoord)>>,
+ check_draw_path: VecDeque<Box<dyn FnMut(RGBAColor, u32, Vec<BackendCoord>)>>,
+ check_draw_circle: VecDeque<Box<dyn FnMut(RGBAColor, u32, bool, BackendCoord, u32)>>,
+ check_draw_text: VecDeque<Box<dyn FnMut(RGBAColor, &str, f64, BackendCoord, &str)>>,
+ check_fill_polygon: VecDeque<Box<dyn FnMut(RGBAColor, Vec<BackendCoord>)>>,
+ drop_check: Option<Box<dyn FnMut(&Self)>>,
+}
+
+macro_rules! def_set_checker_func {
+ (drop_check, $($param:ty),*) => {
+ pub fn drop_check<T: FnMut($($param,)*) + 'static>(&mut self, check:T) -> &mut Self {
+ self.drop_check = Some(Box::new(check));
+ self
+ }
+ };
+ ($name:ident, $($param:ty),*) => {
+ pub fn $name<T: FnMut($($param,)*) + 'static>(&mut self, check:T) -> &mut Self {
+ self.$name.push_back(Box::new(check));
+ self
+ }
+ }
+}
+
+impl MockedBackend {
+ pub fn new(width: u32, height: u32) -> Self {
+ MockedBackend {
+ height,
+ width,
+ init_count: 0,
+ draw_count: 0,
+ num_draw_pixel_call: 0,
+ num_draw_line_call: 0,
+ num_draw_rect_call: 0,
+ num_draw_circle_call: 0,
+ num_draw_text_call: 0,
+ num_draw_path_call: 0,
+ num_fill_polygon_call: 0,
+ check_draw_pixel: vec![].into(),
+ check_draw_line: vec![].into(),
+ check_draw_rect: vec![].into(),
+ check_draw_path: vec![].into(),
+ check_draw_circle: vec![].into(),
+ check_draw_text: vec![].into(),
+ check_fill_polygon: vec![].into(),
+ drop_check: None,
+ }
+ }
+
+ def_set_checker_func!(check_draw_pixel, RGBAColor, BackendCoord);
+ def_set_checker_func!(check_draw_line, RGBAColor, u32, BackendCoord, BackendCoord);
+ def_set_checker_func!(
+ check_draw_rect,
+ RGBAColor,
+ u32,
+ bool,
+ BackendCoord,
+ BackendCoord
+ );
+ def_set_checker_func!(check_draw_path, RGBAColor, u32, Vec<BackendCoord>);
+ def_set_checker_func!(check_draw_circle, RGBAColor, u32, bool, BackendCoord, u32);
+ def_set_checker_func!(check_draw_text, RGBAColor, &str, f64, BackendCoord, &str);
+ def_set_checker_func!(drop_check, &Self);
+ def_set_checker_func!(check_fill_polygon, RGBAColor, Vec<BackendCoord>);
+
+ fn check_before_draw(&mut self) {
+ self.draw_count += 1;
+ //assert_eq!(self.init_count, self.draw_count);
+ }
+}
+
+#[derive(Debug)]
+pub struct MockedError;
+
+impl std::fmt::Display for MockedError {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(fmt, "MockedError")
+ }
+}
+
+impl std::error::Error for MockedError {}
+
+impl DrawingBackend for MockedBackend {
+ type ErrorType = MockedError;
+
+ fn get_size(&self) -> (u32, u32) {
+ (self.width, self.height)
+ }
+
+ fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<MockedError>> {
+ self.init_count += 1;
+ Ok(())
+ }
+
+ fn present(&mut self) -> Result<(), DrawingErrorKind<MockedError>> {
+ self.init_count = 0;
+ self.draw_count = 0;
+ Ok(())
+ }
+
+ fn draw_pixel(
+ &mut self,
+ point: BackendCoord,
+ color: &RGBAColor,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.check_before_draw();
+ self.num_draw_pixel_call += 1;
+ let color = color.to_rgba();
+ if let Some(mut checker) = self.check_draw_pixel.pop_front() {
+ checker(color, point);
+
+ if self.check_draw_pixel.is_empty() {
+ self.check_draw_pixel.push_back(checker);
+ }
+ }
+ Ok(())
+ }
+
+ fn draw_line<S: BackendStyle>(
+ &mut self,
+ from: BackendCoord,
+ to: BackendCoord,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.check_before_draw();
+ self.num_draw_line_call += 1;
+ let color = style.as_color().to_rgba();
+ if let Some(mut checker) = self.check_draw_line.pop_front() {
+ checker(color, style.stroke_width(), from, to);
+
+ if self.check_draw_line.is_empty() {
+ self.check_draw_line.push_back(checker);
+ }
+ }
+ Ok(())
+ }
+
+ fn draw_rect<S: BackendStyle>(
+ &mut self,
+ upper_left: BackendCoord,
+ bottom_right: BackendCoord,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.check_before_draw();
+ self.num_draw_rect_call += 1;
+ let color = style.as_color().to_rgba();
+ if let Some(mut checker) = self.check_draw_rect.pop_front() {
+ checker(color, style.stroke_width(), fill, upper_left, bottom_right);
+
+ if self.check_draw_rect.is_empty() {
+ self.check_draw_rect.push_back(checker);
+ }
+ }
+ Ok(())
+ }
+
+ fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ path: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.check_before_draw();
+ self.num_draw_path_call += 1;
+ let color = style.as_color().to_rgba();
+ if let Some(mut checker) = self.check_draw_path.pop_front() {
+ checker(color, style.stroke_width(), path.into_iter().collect());
+
+ if self.check_draw_path.is_empty() {
+ self.check_draw_path.push_back(checker);
+ }
+ }
+ Ok(())
+ }
+
+ fn draw_circle<S: BackendStyle>(
+ &mut self,
+ center: BackendCoord,
+ radius: u32,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.check_before_draw();
+ self.num_draw_circle_call += 1;
+ let color = style.as_color().to_rgba();
+ if let Some(mut checker) = self.check_draw_circle.pop_front() {
+ checker(color, style.stroke_width(), fill, center, radius);
+
+ if self.check_draw_circle.is_empty() {
+ self.check_draw_circle.push_back(checker);
+ }
+ }
+ Ok(())
+ }
+
+ fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ path: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ self.check_before_draw();
+ self.num_fill_polygon_call += 1;
+ let color = style.as_color().to_rgba();
+ if let Some(mut checker) = self.check_fill_polygon.pop_front() {
+ checker(color, path.into_iter().collect());
+
+ if self.check_fill_polygon.is_empty() {
+ self.check_fill_polygon.push_back(checker);
+ }
+ }
+ Ok(())
+ }
+
+ fn draw_text(
+ &mut self,
+ text: &str,
+ style: &TextStyle,
+ pos: BackendCoord,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let font = &style.font;
+ let color = &style.color;
+ self.check_before_draw();
+ self.num_draw_text_call += 1;
+ let color = color.to_rgba();
+ if let Some(mut checker) = self.check_draw_text.pop_front() {
+ checker(color, font.get_name(), font.get_size(), pos, text);
+
+ if self.check_draw_text.is_empty() {
+ self.check_draw_text.push_back(checker);
+ }
+ }
+ Ok(())
+ }
+}
+
+impl Drop for MockedBackend {
+ fn drop(&mut self) {
+ let mut temp = None;
+ std::mem::swap(&mut temp, &mut self.drop_check);
+
+ if let Some(mut checker) = temp {
+ checker(self);
+ }
+ }
+}
+
+pub fn create_mocked_drawing_area<F: FnOnce(&mut MockedBackend)>(
+ width: u32,
+ height: u32,
+ setup: F,
+) -> DrawingArea<MockedBackend, Shift> {
+ let mut backend = MockedBackend::new(width, height);
+ setup(&mut backend);
+ backend.into_drawing_area()
+}
diff --git a/src/drawing/backend_impl/mod.rs b/src/drawing/backend_impl/mod.rs
new file mode 100644
index 0000000..719f375
--- /dev/null
+++ b/src/drawing/backend_impl/mod.rs
@@ -0,0 +1,46 @@
+#[cfg(feature = "svg")]
+mod svg;
+#[cfg(feature = "svg")]
+pub use self::svg::SVGBackend;
+
+#[cfg(feature = "bitmap")]
+mod bitmap;
+#[cfg(feature = "bitmap")]
+pub use bitmap::BitMapBackend;
+
+#[cfg(feature = "bitmap")]
+pub mod bitmap_pixel {
+ pub use super::bitmap::{BGRXPixel, PixelFormat, RGBPixel};
+}
+
+#[cfg(target_arch = "wasm32")]
+mod canvas;
+#[cfg(target_arch = "wasm32")]
+pub use canvas::CanvasBackend;
+
+#[cfg(test)]
+mod mocked;
+#[cfg(test)]
+pub use mocked::{create_mocked_drawing_area, MockedBackend};
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "piston"))]
+mod piston;
+#[cfg(all(not(target_arch = "wasm32"), feature = "piston"))]
+pub use piston::{draw_piston_window, PistonBackend};
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "cairo-rs"))]
+mod cairo;
+#[cfg(all(not(target_arch = "wasm32"), feature = "cairo-rs"))]
+pub use self::cairo::CairoBackend;
+
+/// This is the dummy backend placeholder for the backend that never fails
+#[derive(Debug)]
+pub struct DummyBackendError;
+
+impl std::fmt::Display for DummyBackendError {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(fmt, "{:?}", self)
+ }
+}
+
+impl std::error::Error for DummyBackendError {}
diff --git a/src/drawing/backend_impl/piston.rs b/src/drawing/backend_impl/piston.rs
new file mode 100644
index 0000000..4a57905
--- /dev/null
+++ b/src/drawing/backend_impl/piston.rs
@@ -0,0 +1,206 @@
+use piston_window::context::Context;
+use piston_window::ellipse::circle;
+use piston_window::{circle_arc, ellipse, line, rectangle, Event, Loop};
+use piston_window::{G2d, PistonWindow};
+
+use super::DummyBackendError;
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
+use crate::style::{Color, RGBAColor};
+
+pub struct PistonBackend<'a, 'b> {
+ size: (u32, u32),
+ scale: f64,
+ context: Context,
+ graphics: &'b mut G2d<'a>,
+}
+
+fn make_piston_rgba(color: &RGBAColor) -> [f32; 4] {
+ let (r, g, b) = color.rgb();
+ let a = color.alpha();
+
+ [
+ r as f32 / 255.0,
+ g as f32 / 255.0,
+ b as f32 / 255.0,
+ a as f32,
+ ]
+}
+fn make_point_pair(a: BackendCoord, b: BackendCoord, scale: f64) -> [f64; 4] {
+ [
+ a.0 as f64 * scale,
+ a.1 as f64 * scale,
+ b.0 as f64 * scale,
+ b.1 as f64 * scale,
+ ]
+}
+
+impl<'a, 'b> PistonBackend<'a, 'b> {
+ pub fn new(size: (u32, u32), scale: f64, context: Context, graphics: &'b mut G2d<'a>) -> Self {
+ Self {
+ size,
+ context,
+ graphics,
+ scale,
+ }
+ }
+}
+
+impl<'a, 'b> DrawingBackend for PistonBackend<'a, 'b> {
+ type ErrorType = DummyBackendError;
+
+ fn get_size(&self) -> (u32, u32) {
+ self.size
+ }
+
+ fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<DummyBackendError>> {
+ Ok(())
+ }
+
+ fn present(&mut self) -> Result<(), DrawingErrorKind<DummyBackendError>> {
+ Ok(())
+ }
+
+ fn draw_pixel(
+ &mut self,
+ point: BackendCoord,
+ color: &RGBAColor,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ piston_window::rectangle(
+ make_piston_rgba(color),
+ make_point_pair(point, (1, 1), self.scale),
+ self.context.transform,
+ self.graphics,
+ );
+ Ok(())
+ }
+
+ fn draw_line<S: BackendStyle>(
+ &mut self,
+ from: BackendCoord,
+ to: BackendCoord,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ line(
+ make_piston_rgba(&style.as_color()),
+ self.scale,
+ make_point_pair(from, to, self.scale),
+ self.context.transform,
+ self.graphics,
+ );
+ Ok(())
+ }
+
+ fn draw_rect<S: BackendStyle>(
+ &mut self,
+ upper_left: BackendCoord,
+ bottom_right: BackendCoord,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if fill {
+ rectangle(
+ make_piston_rgba(&style.as_color()),
+ make_point_pair(
+ upper_left,
+ (bottom_right.0 - upper_left.0, bottom_right.1 - upper_left.1),
+ self.scale,
+ ),
+ self.context.transform,
+ self.graphics,
+ );
+ } else {
+ let color = make_piston_rgba(&style.as_color());
+ let [x0, y0, x1, y1] = make_point_pair(upper_left, bottom_right, self.scale);
+ line(
+ color,
+ self.scale,
+ [x0, y0, x0, y1],
+ self.context.transform,
+ self.graphics,
+ );
+ line(
+ color,
+ self.scale,
+ [x0, y1, x1, y1],
+ self.context.transform,
+ self.graphics,
+ );
+ line(
+ color,
+ self.scale,
+ [x1, y1, x1, y0],
+ self.context.transform,
+ self.graphics,
+ );
+ line(
+ color,
+ self.scale,
+ [x1, y0, x0, y0],
+ self.context.transform,
+ self.graphics,
+ );
+ }
+ Ok(())
+ }
+
+ fn draw_circle<S: BackendStyle>(
+ &mut self,
+ center: BackendCoord,
+ radius: u32,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let rect = circle(center.0 as f64, center.1 as f64, radius as f64);
+ if fill {
+ ellipse(
+ make_piston_rgba(&style.as_color()),
+ rect,
+ self.context.transform,
+ self.graphics,
+ );
+ } else {
+ circle_arc(
+ make_piston_rgba(&style.as_color()),
+ self.scale,
+ std::f64::consts::PI,
+ 0.0,
+ rect,
+ self.context.transform,
+ self.graphics,
+ );
+ circle_arc(
+ make_piston_rgba(&style.as_color()),
+ self.scale,
+ 0.0,
+ std::f64::consts::PI,
+ rect,
+ self.context.transform,
+ self.graphics,
+ );
+ }
+ Ok(())
+ }
+}
+
+#[allow(clippy::single_match)]
+pub fn draw_piston_window<F: FnOnce(PistonBackend) -> Result<(), Box<dyn std::error::Error>>>(
+ window: &mut PistonWindow,
+ draw: F,
+) -> Option<Event> {
+ if let Some(event) = window.next() {
+ window.draw_2d(&event, |c, g, _| match event {
+ Event::Loop(Loop::Render(arg)) => {
+ draw(PistonBackend::new(
+ (arg.draw_size[0], arg.draw_size[1]),
+ arg.window_size[0] / arg.draw_size[0] as f64,
+ c,
+ g,
+ ))
+ .ok();
+ }
+ _ => {}
+ });
+ return Some(event);
+ }
+ None
+}
diff --git a/src/drawing/backend_impl/svg.rs b/src/drawing/backend_impl/svg.rs
new file mode 100644
index 0000000..53e49bc
--- /dev/null
+++ b/src/drawing/backend_impl/svg.rs
@@ -0,0 +1,832 @@
+/*!
+The SVG image drawing backend
+*/
+
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingBackend, DrawingErrorKind};
+use crate::style::text_anchor::{HPos, VPos};
+use crate::style::{Color, FontStyle, FontTransform, RGBAColor, TextStyle};
+
+use std::fs::File;
+#[allow(unused_imports)]
+use std::io::Cursor;
+use std::io::{BufWriter, Error, Write};
+use std::path::Path;
+
+fn make_svg_color<C: Color>(color: &C) -> String {
+ let (r, g, b) = color.rgb();
+ return format!("#{:02X}{:02X}{:02X}", r, g, b);
+}
+
+fn make_svg_opacity<C: Color>(color: &C) -> String {
+ return format!("{}", color.alpha());
+}
+
+enum Target<'a> {
+ File(String, &'a Path),
+ Buffer(&'a mut String),
+ // TODO: At this point we won't make the breaking change
+ // so the u8 buffer is still supported. But in 0.3, we definitely
+ // should get rid of this.
+ #[cfg(feature = "deprecated_items")]
+ U8Buffer(String, &'a mut Vec<u8>),
+}
+
+impl Target<'_> {
+ fn get_mut(&mut self) -> &mut String {
+ match self {
+ Target::File(ref mut buf, _) => buf,
+ Target::Buffer(buf) => buf,
+ #[cfg(feature = "deprecated_items")]
+ Target::U8Buffer(ref mut buf, _) => buf,
+ }
+ }
+}
+
+enum SVGTag {
+ SVG,
+ Circle,
+ Line,
+ Polygon,
+ Polyline,
+ Rectangle,
+ Text,
+ #[allow(dead_code)]
+ Image,
+}
+
+impl SVGTag {
+ fn to_tag_name(&self) -> &'static str {
+ match self {
+ SVGTag::SVG => "svg",
+ SVGTag::Circle => "circle",
+ SVGTag::Line => "line",
+ SVGTag::Polyline => "polyline",
+ SVGTag::Rectangle => "rect",
+ SVGTag::Text => "text",
+ SVGTag::Image => "image",
+ SVGTag::Polygon => "polygon",
+ }
+ }
+}
+
+/// The SVG image drawing backend
+pub struct SVGBackend<'a> {
+ target: Target<'a>,
+ size: (u32, u32),
+ tag_stack: Vec<SVGTag>,
+ saved: bool,
+}
+
+impl<'a> SVGBackend<'a> {
+ fn escape_and_push(buf: &mut String, value: &str) {
+ value.chars().for_each(|c| match c {
+ '<' => buf.push_str("&lt;"),
+ '>' => buf.push_str("&gt;"),
+ '&' => buf.push_str("&amp;"),
+ '"' => buf.push_str("&quot;"),
+ '\'' => buf.push_str("&apos;"),
+ other => buf.push(other),
+ });
+ }
+ fn open_tag(&mut self, tag: SVGTag, attr: &[(&str, &str)], close: bool) {
+ let buf = self.target.get_mut();
+ buf.push_str("<");
+ buf.push_str(tag.to_tag_name());
+ for (key, value) in attr {
+ buf.push_str(" ");
+ buf.push_str(key);
+ buf.push_str("=\"");
+ Self::escape_and_push(buf, value);
+ buf.push_str("\"");
+ }
+ if close {
+ buf.push_str("/>\n");
+ } else {
+ self.tag_stack.push(tag);
+ buf.push_str(">\n");
+ }
+ }
+
+ fn close_tag(&mut self) -> bool {
+ if let Some(tag) = self.tag_stack.pop() {
+ let buf = self.target.get_mut();
+ buf.push_str("</");
+ buf.push_str(tag.to_tag_name());
+ buf.push_str(">\n");
+ return true;
+ }
+ false
+ }
+
+ fn init_svg_file(&mut self, size: (u32, u32)) {
+ self.open_tag(
+ SVGTag::SVG,
+ &[
+ ("width", &format!("{}", size.0)),
+ ("height", &format!("{}", size.1)),
+ ("viewBox", &format!("0 0 {} {}", size.0, size.1)),
+ ("xmlns", "http://www.w3.org/2000/svg"),
+ ],
+ false,
+ );
+ }
+
+ /// Create a new SVG drawing backend
+ pub fn new<T: AsRef<Path> + ?Sized>(path: &'a T, size: (u32, u32)) -> Self {
+ let mut ret = Self {
+ target: Target::File(String::default(), path.as_ref()),
+ size,
+ tag_stack: vec![],
+ saved: false,
+ };
+
+ ret.init_svg_file(size);
+ ret
+ }
+
+ /// Create a new SVG drawing backend and store the document into a u8 vector
+ #[cfg(feature = "deprecated_items")]
+ #[deprecated(
+ note = "This will be replaced by `with_string`, consider use `with_string` to avoid breaking change in the future"
+ )]
+ pub fn with_buffer(buf: &'a mut Vec<u8>, size: (u32, u32)) -> Self {
+ let mut ret = Self {
+ target: Target::U8Buffer(String::default(), buf),
+ size,
+ tag_stack: vec![],
+ saved: false,
+ };
+
+ ret.init_svg_file(size);
+
+ ret
+ }
+
+ /// Create a new SVG drawing backend and store the document into a String buffer
+ pub fn with_string(buf: &'a mut String, size: (u32, u32)) -> Self {
+ let mut ret = Self {
+ target: Target::Buffer(buf),
+ size,
+ tag_stack: vec![],
+ saved: false,
+ };
+
+ ret.init_svg_file(size);
+
+ ret
+ }
+}
+
+impl<'a> DrawingBackend for SVGBackend<'a> {
+ type ErrorType = Error;
+
+ fn get_size(&self) -> (u32, u32) {
+ self.size
+ }
+
+ fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind<Error>> {
+ Ok(())
+ }
+
+ fn present(&mut self) -> Result<(), DrawingErrorKind<Error>> {
+ if !self.saved {
+ while self.close_tag() {}
+ match self.target {
+ Target::File(ref buf, path) => {
+ let outfile = File::create(path).map_err(DrawingErrorKind::DrawingError)?;
+ let mut outfile = BufWriter::new(outfile);
+ outfile
+ .write_all(buf.as_ref())
+ .map_err(DrawingErrorKind::DrawingError)?;
+ }
+ Target::Buffer(_) => {}
+ #[cfg(feature = "deprecated_items")]
+ Target::U8Buffer(ref actual, ref mut target) => {
+ target.clear();
+ target.extend_from_slice(actual.as_bytes());
+ }
+ }
+ self.saved = true;
+ }
+ Ok(())
+ }
+
+ fn draw_pixel(
+ &mut self,
+ point: BackendCoord,
+ color: &RGBAColor,
+ ) -> Result<(), DrawingErrorKind<Error>> {
+ if color.alpha() == 0.0 {
+ return Ok(());
+ }
+ self.open_tag(
+ SVGTag::Rectangle,
+ &[
+ ("x", &format!("{}", point.0)),
+ ("y", &format!("{}", point.1)),
+ ("width", "1"),
+ ("height", "1"),
+ ("stroke", "none"),
+ ("opacity", &make_svg_opacity(color)),
+ ("fill", &make_svg_color(color)),
+ ],
+ true,
+ );
+ Ok(())
+ }
+
+ fn draw_line<S: BackendStyle>(
+ &mut self,
+ from: BackendCoord,
+ to: BackendCoord,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+ self.open_tag(
+ SVGTag::Line,
+ &[
+ ("opacity", &make_svg_opacity(&style.as_color())),
+ ("stroke", &make_svg_color(&style.as_color())),
+ ("stroke-width", &format!("{}", style.stroke_width())),
+ ("x1", &format!("{}", from.0)),
+ ("y1", &format!("{}", from.1)),
+ ("x2", &format!("{}", to.0)),
+ ("y2", &format!("{}", to.1)),
+ ],
+ true,
+ );
+ Ok(())
+ }
+
+ fn draw_rect<S: BackendStyle>(
+ &mut self,
+ upper_left: BackendCoord,
+ bottom_right: BackendCoord,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+
+ let (fill, stroke) = if !fill {
+ ("none".to_string(), make_svg_color(&style.as_color()))
+ } else {
+ (make_svg_color(&style.as_color()), "none".to_string())
+ };
+
+ self.open_tag(
+ SVGTag::Rectangle,
+ &[
+ ("x", &format!("{}", upper_left.0)),
+ ("y", &format!("{}", upper_left.1)),
+ ("width", &format!("{}", bottom_right.0 - upper_left.0)),
+ ("height", &format!("{}", bottom_right.1 - upper_left.1)),
+ ("opacity", &make_svg_opacity(&style.as_color())),
+ ("fill", &fill),
+ ("stroke", &stroke),
+ ],
+ true,
+ );
+
+ Ok(())
+ }
+
+ fn draw_path<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ path: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+ self.open_tag(
+ SVGTag::Polyline,
+ &[
+ ("fill", "none"),
+ ("opacity", &make_svg_opacity(&style.as_color())),
+ ("stroke", &make_svg_color(&style.as_color())),
+ ("stroke-width", &format!("{}", style.stroke_width())),
+ (
+ "points",
+ &path.into_iter().fold(String::new(), |mut s, (x, y)| {
+ s.push_str(&format!("{},{} ", x, y));
+ s
+ }),
+ ),
+ ],
+ true,
+ );
+ Ok(())
+ }
+
+ fn fill_polygon<S: BackendStyle, I: IntoIterator<Item = BackendCoord>>(
+ &mut self,
+ path: I,
+ style: &S,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+ self.open_tag(
+ SVGTag::Polygon,
+ &[
+ ("opacity", &make_svg_opacity(&style.as_color())),
+ ("fill", &make_svg_color(&style.as_color())),
+ (
+ "points",
+ &path.into_iter().fold(String::new(), |mut s, (x, y)| {
+ s.push_str(&format!("{},{} ", x, y));
+ s
+ }),
+ ),
+ ],
+ true,
+ );
+ Ok(())
+ }
+
+ fn draw_circle<S: BackendStyle>(
+ &mut self,
+ center: BackendCoord,
+ radius: u32,
+ style: &S,
+ fill: bool,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+ let (stroke, fill) = if !fill {
+ (make_svg_color(&style.as_color()), "none".to_string())
+ } else {
+ ("none".to_string(), make_svg_color(&style.as_color()))
+ };
+ self.open_tag(
+ SVGTag::Circle,
+ &[
+ ("cx", &format!("{}", center.0)),
+ ("cy", &format!("{}", center.1)),
+ ("r", &format!("{}", radius)),
+ ("opacity", &make_svg_opacity(&style.as_color())),
+ ("fill", &fill),
+ ("stroke", &stroke),
+ ],
+ true,
+ );
+ Ok(())
+ }
+
+ fn draw_text(
+ &mut self,
+ text: &str,
+ style: &TextStyle,
+ pos: BackendCoord,
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ let font = &style.font;
+ let color = &style.color;
+ if color.alpha() == 0.0 {
+ return Ok(());
+ }
+
+ let (x0, y0) = pos;
+ let text_anchor = match style.pos.h_pos {
+ HPos::Left => "start",
+ HPos::Right => "end",
+ HPos::Center => "middle",
+ };
+
+ let dy = match style.pos.v_pos {
+ VPos::Top => "0.76em",
+ VPos::Center => "0.5ex",
+ VPos::Bottom => "-0.5ex",
+ };
+
+ #[cfg(feature = "debug")]
+ {
+ let ((fx0, fy0), (fx1, fy1)) =
+ font.layout_box(text).map_err(DrawingErrorKind::FontError)?;
+ let x0 = match style.pos.h_pos {
+ HPos::Left => x0,
+ HPos::Center => x0 - fx1 / 2 + fx0 / 2,
+ HPos::Right => x0 - fx1 + fx0,
+ };
+ let y0 = match style.pos.v_pos {
+ VPos::Top => y0,
+ VPos::Center => y0 - fy1 / 2 + fy0 / 2,
+ VPos::Bottom => y0 - fy1 + fy0,
+ };
+ self.draw_rect(
+ (x0, y0),
+ (x0 + fx1 - fx0, y0 + fy1 - fy0),
+ &crate::prelude::RED,
+ false,
+ )
+ .unwrap();
+ self.draw_circle((x0, y0), 2, &crate::prelude::RED, false)
+ .unwrap();
+ }
+
+ let mut attrs = vec![
+ ("x", format!("{}", x0)),
+ ("y", format!("{}", y0)),
+ ("dy", dy.to_owned()),
+ ("text-anchor", text_anchor.to_string()),
+ ("font-family", font.get_name().to_string()),
+ ("font-size", format!("{}", font.get_size() / 1.24)),
+ ("opacity", make_svg_opacity(color)),
+ ("fill", make_svg_color(color)),
+ ];
+
+ match font.get_style() {
+ FontStyle::Normal => {}
+ FontStyle::Bold => attrs.push(("font-weight", "bold".to_string())),
+ other_style => attrs.push(("font-style", other_style.as_str().to_string())),
+ };
+
+ let trans = font.get_transform();
+ match trans {
+ FontTransform::Rotate90 => {
+ attrs.push(("transform", format!("rotate(90, {}, {})", x0, y0)))
+ }
+ FontTransform::Rotate180 => {
+ attrs.push(("transform", format!("rotate(180, {}, {})", x0, y0)));
+ }
+ FontTransform::Rotate270 => {
+ attrs.push(("transform", format!("rotate(270, {}, {})", x0, y0)));
+ }
+ _ => {}
+ }
+
+ self.open_tag(
+ SVGTag::Text,
+ attrs
+ .iter()
+ .map(|(a, b)| (*a, b.as_ref()))
+ .collect::<Vec<_>>()
+ .as_ref(),
+ false,
+ );
+
+ Self::escape_and_push(self.target.get_mut(), text);
+ self.target.get_mut().push_str("\n");
+
+ self.close_tag();
+
+ Ok(())
+ }
+
+ #[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+ fn blit_bitmap<'b>(
+ &mut self,
+ pos: BackendCoord,
+ (w, h): (u32, u32),
+ src: &'b [u8],
+ ) -> Result<(), DrawingErrorKind<Self::ErrorType>> {
+ use image::png::PNGEncoder;
+
+ let mut data = vec![0; 0];
+
+ {
+ let cursor = Cursor::new(&mut data);
+
+ let encoder = PNGEncoder::new(cursor);
+
+ let color = image::ColorType::Rgb8;
+
+ encoder.encode(src, w, h, color).map_err(|e| {
+ DrawingErrorKind::DrawingError(Error::new(
+ std::io::ErrorKind::Other,
+ format!("Image error: {}", e),
+ ))
+ })?;
+ }
+
+ let padding = (3 - data.len() % 3) % 3;
+ for _ in 0..padding {
+ data.push(0);
+ }
+
+ let mut rem_bits = 0;
+ let mut rem_num = 0;
+
+ fn cvt_base64(from: u8) -> char {
+ (if from < 26 {
+ b'A' + from
+ } else if from < 52 {
+ b'a' + from - 26
+ } else if from < 62 {
+ b'0' + from - 52
+ } else if from == 62 {
+ b'+'
+ } else {
+ b'/'
+ })
+ .into()
+ }
+
+ let mut buf = String::new();
+ buf.push_str("data:png;base64,");
+
+ for byte in data {
+ let value = (rem_bits << (6 - rem_num)) | (byte >> (rem_num + 2));
+ rem_bits = byte & ((1 << (2 + rem_num)) - 1);
+ rem_num += 2;
+
+ buf.push(cvt_base64(value));
+ if rem_num == 6 {
+ buf.push(cvt_base64(rem_bits));
+ rem_bits = 0;
+ rem_num = 0;
+ }
+ }
+
+ for _ in 0..padding {
+ buf.pop();
+ buf.push('=');
+ }
+
+ self.open_tag(
+ SVGTag::Image,
+ &[
+ ("x", &format!("{}", pos.0)),
+ ("y", &format!("{}", pos.1)),
+ ("width", &format!("{}", w)),
+ ("height", &format!("{}", h)),
+ ("href", buf.as_str()),
+ ],
+ true,
+ );
+
+ Ok(())
+ }
+}
+
+impl Drop for SVGBackend<'_> {
+ fn drop(&mut self) {
+ if !self.saved {
+ // drop should not panic, so we ignore a failed present
+ let _ = self.present();
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::element::Circle;
+ use crate::prelude::*;
+ use crate::style::text_anchor::{HPos, Pos, VPos};
+ use std::fs;
+ use std::path::Path;
+
+ static DST_DIR: &str = "target/test/svg";
+
+ fn checked_save_file(name: &str, content: &str) {
+ /*
+ Please use the SVG file to manually verify the results.
+ */
+ assert!(!content.is_empty());
+ fs::create_dir_all(DST_DIR).unwrap();
+ let file_name = format!("{}.svg", name);
+ let file_path = Path::new(DST_DIR).join(file_name);
+ println!("{:?} created", file_path);
+ fs::write(file_path, &content).unwrap();
+ }
+
+ fn draw_mesh_with_custom_ticks(tick_size: i32, test_name: &str) {
+ let mut content: String = Default::default();
+ {
+ let root = SVGBackend::with_string(&mut content, (500, 500)).into_drawing_area();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("This is a test", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..10, 0..10)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .set_all_tick_mark_size(tick_size)
+ .draw()
+ .unwrap();
+ }
+
+ checked_save_file(test_name, &content);
+
+ assert!(content.contains("This is a test"));
+ }
+
+ #[test]
+ fn test_draw_mesh_no_ticks() {
+ draw_mesh_with_custom_ticks(0, "test_draw_mesh_no_ticks");
+ }
+
+ #[test]
+ fn test_draw_mesh_negative_ticks() {
+ draw_mesh_with_custom_ticks(-10, "test_draw_mesh_negative_ticks");
+ }
+
+ #[test]
+ fn test_text_alignments() {
+ let mut content: String = Default::default();
+ {
+ let mut root = SVGBackend::with_string(&mut content, (500, 500));
+
+ let style = TextStyle::from(("sans-serif", 20).into_font())
+ .pos(Pos::new(HPos::Right, VPos::Top));
+ root.draw_text("right-align", &style, (150, 50)).unwrap();
+
+ let style = style.pos(Pos::new(HPos::Center, VPos::Top));
+ root.draw_text("center-align", &style, (150, 150)).unwrap();
+
+ let style = style.pos(Pos::new(HPos::Left, VPos::Top));
+ root.draw_text("left-align", &style, (150, 200)).unwrap();
+ }
+
+ checked_save_file("test_text_alignments", &content);
+
+ for svg_line in content.split("</text>") {
+ if let Some(anchor_and_rest) = svg_line.split("text-anchor=\"").nth(1) {
+ if anchor_and_rest.starts_with("end") {
+ assert!(anchor_and_rest.contains("right-align"))
+ }
+ if anchor_and_rest.starts_with("middle") {
+ assert!(anchor_and_rest.contains("center-align"))
+ }
+ if anchor_and_rest.starts_with("start") {
+ assert!(anchor_and_rest.contains("left-align"))
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn test_text_draw() {
+ let mut content: String = Default::default();
+ {
+ let root = SVGBackend::with_string(&mut content, (1500, 800)).into_drawing_area();
+ let root = root
+ .titled("Image Title", ("sans-serif", 60).into_font())
+ .unwrap();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("All anchor point positions", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..100, 0..50)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .x_desc("X Axis")
+ .y_desc("Y Axis")
+ .draw()
+ .unwrap();
+
+ let ((x1, y1), (x2, y2), (x3, y3)) = ((-30, 30), (0, -30), (30, 30));
+
+ for (dy, trans) in [
+ FontTransform::None,
+ FontTransform::Rotate90,
+ FontTransform::Rotate180,
+ FontTransform::Rotate270,
+ ]
+ .iter()
+ .enumerate()
+ {
+ for (dx1, h_pos) in [HPos::Left, HPos::Right, HPos::Center].iter().enumerate() {
+ for (dx2, v_pos) in [VPos::Top, VPos::Center, VPos::Bottom].iter().enumerate() {
+ let x = 150_i32 + (dx1 as i32 * 3 + dx2 as i32) * 150;
+ let y = 120 + dy as i32 * 150;
+ let draw = |x, y, text| {
+ root.draw(&Circle::new((x, y), 3, &BLACK.mix(0.5))).unwrap();
+ let style = TextStyle::from(("sans-serif", 20).into_font())
+ .pos(Pos::new(*h_pos, *v_pos))
+ .transform(trans.clone());
+ root.draw_text(text, &style, (x, y)).unwrap();
+ };
+ draw(x + x1, y + y1, "dood");
+ draw(x + x2, y + y2, "dog");
+ draw(x + x3, y + y3, "goog");
+ }
+ }
+ }
+ }
+
+ checked_save_file("test_text_draw", &content);
+
+ assert_eq!(content.matches("dog").count(), 36);
+ assert_eq!(content.matches("dood").count(), 36);
+ assert_eq!(content.matches("goog").count(), 36);
+ }
+
+ #[test]
+ fn test_text_clipping() {
+ let mut content: String = Default::default();
+ {
+ let (width, height) = (500_i32, 500_i32);
+ let root = SVGBackend::with_string(&mut content, (width as u32, height as u32))
+ .into_drawing_area();
+
+ let style = TextStyle::from(("sans-serif", 20).into_font())
+ .pos(Pos::new(HPos::Center, VPos::Center));
+ root.draw_text("TOP LEFT", &style, (0, 0)).unwrap();
+ root.draw_text("TOP CENTER", &style, (width / 2, 0))
+ .unwrap();
+ root.draw_text("TOP RIGHT", &style, (width, 0)).unwrap();
+
+ root.draw_text("MIDDLE LEFT", &style, (0, height / 2))
+ .unwrap();
+ root.draw_text("MIDDLE RIGHT", &style, (width, height / 2))
+ .unwrap();
+
+ root.draw_text("BOTTOM LEFT", &style, (0, height)).unwrap();
+ root.draw_text("BOTTOM CENTER", &style, (width / 2, height))
+ .unwrap();
+ root.draw_text("BOTTOM RIGHT", &style, (width, height))
+ .unwrap();
+ }
+
+ checked_save_file("test_text_clipping", &content);
+ }
+
+ #[test]
+ fn test_series_labels() {
+ let mut content = String::default();
+ {
+ let (width, height) = (500, 500);
+ let root = SVGBackend::with_string(&mut content, (width, height)).into_drawing_area();
+
+ let mut chart = ChartBuilder::on(&root)
+ .caption("All series label positions", ("sans-serif", 20))
+ .set_all_label_area_size(40)
+ .build_ranged(0..50, 0..50)
+ .unwrap();
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .draw()
+ .unwrap();
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 15), 5, &RED)))
+ .expect("Drawing error")
+ .label("Series 1")
+ .legend(|(x, y)| Circle::new((x, y), 3, RED.filled()));
+
+ chart
+ .draw_series(std::iter::once(Circle::new((5, 15), 10, &BLUE)))
+ .expect("Drawing error")
+ .label("Series 2")
+ .legend(|(x, y)| Circle::new((x, y), 3, BLUE.filled()));
+
+ for pos in vec![
+ SeriesLabelPosition::UpperLeft,
+ SeriesLabelPosition::MiddleLeft,
+ SeriesLabelPosition::LowerLeft,
+ SeriesLabelPosition::UpperMiddle,
+ SeriesLabelPosition::MiddleMiddle,
+ SeriesLabelPosition::LowerMiddle,
+ SeriesLabelPosition::UpperRight,
+ SeriesLabelPosition::MiddleRight,
+ SeriesLabelPosition::LowerRight,
+ SeriesLabelPosition::Coordinate(70, 70),
+ ]
+ .into_iter()
+ {
+ chart
+ .configure_series_labels()
+ .border_style(&BLACK.mix(0.5))
+ .position(pos)
+ .draw()
+ .expect("Drawing error");
+ }
+ }
+
+ checked_save_file("test_series_labels", &content);
+ }
+
+ #[test]
+ fn test_draw_pixel_alphas() {
+ let mut content = String::default();
+ {
+ let (width, height) = (100_i32, 100_i32);
+ let root = SVGBackend::with_string(&mut content, (width as u32, height as u32))
+ .into_drawing_area();
+ root.fill(&WHITE).unwrap();
+
+ for i in -20..20 {
+ let alpha = i as f64 * 0.1;
+ root.draw_pixel((50 + i, 50 + i), &BLACK.mix(alpha))
+ .unwrap();
+ }
+ }
+
+ checked_save_file("test_draw_pixel_alphas", &content);
+ }
+}
diff --git a/src/drawing/mod.rs b/src/drawing/mod.rs
new file mode 100644
index 0000000..e2c59bd
--- /dev/null
+++ b/src/drawing/mod.rs
@@ -0,0 +1,31 @@
+/*!
+The drawing utils for Plotter. Which handles the both low-level and high-level
+drawing.
+
+For the low-level drawing abstraction, the module defines the `DrawingBackend` trait,
+which handles low-level drawing of different shapes, such as, pixels, lines, rectangles, etc.
+
+On the top of drawing backend, one or more drawing area can be defined and different coordinate
+system can be applied to the drawing areas. And the drawing area implement the high-level drawing
+interface, which draws an element.
+
+Currently we have following backend implemented:
+
+- `BitMapBackend`: The backend that creates bitmap, this is based on `image` crate
+- `SVGBackend`: The backend that creates SVG image, based on `svg` crate.
+- `PistonBackend`: The backend that uses Piston Window for real time rendering. Disabled by default, use feature `piston` to turn on.
+- `CanvasBackend`: The backend that operates HTML5 Canvas, this is available when `Plotters` is targeting WASM.
+
+*/
+mod area;
+mod backend_impl;
+
+pub mod rasterizer;
+
+pub mod backend;
+
+pub use area::{DrawingArea, DrawingAreaErrorKind, IntoDrawingArea};
+
+pub use backend_impl::*;
+
+pub use backend::DrawingBackend;
diff --git a/src/drawing/rasterizer/circle.rs b/src/drawing/rasterizer/circle.rs
new file mode 100644
index 0000000..d38e00a
--- /dev/null
+++ b/src/drawing/rasterizer/circle.rs
@@ -0,0 +1,67 @@
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind};
+use crate::drawing::DrawingBackend;
+
+use crate::style::Color;
+
+pub fn draw_circle<B: DrawingBackend, S: BackendStyle>(
+ b: &mut B,
+ center: BackendCoord,
+ radius: u32,
+ style: &S,
+ fill: bool,
+) -> Result<(), DrawingErrorKind<B::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+
+ if !fill && style.stroke_width() != 1 {
+ // FIXME: We are currently ignore the stroke width for circles
+ }
+
+ let min = (f64::from(radius) * (1.0 - (2f64).sqrt() / 2.0)).ceil() as i32;
+ let max = (f64::from(radius) * (1.0 + (2f64).sqrt() / 2.0)).floor() as i32;
+
+ let range = min..=max;
+
+ let (up, down) = (
+ range.start() + center.1 - radius as i32,
+ range.end() + center.1 - radius as i32,
+ );
+
+ for dy in range {
+ let dy = dy - radius as i32;
+ let y = center.1 + dy;
+
+ let lx = (f64::from(radius) * f64::from(radius)
+ - (f64::from(dy) * f64::from(dy)).max(1e-5))
+ .sqrt();
+
+ let left = center.0 - lx.floor() as i32;
+ let right = center.0 + lx.floor() as i32;
+
+ let v = lx - lx.floor();
+
+ let x = center.0 + dy;
+ let top = center.1 - lx.floor() as i32;
+ let bottom = center.1 + lx.floor() as i32;
+
+ if fill {
+ check_result!(b.draw_line((left, y), (right, y), &style.as_color()));
+ check_result!(b.draw_line((x, top), (x, up), &style.as_color()));
+ check_result!(b.draw_line((x, down), (x, bottom), &style.as_color()));
+ } else {
+ check_result!(b.draw_pixel((left, y), &style.as_color().mix(1.0 - v)));
+ check_result!(b.draw_pixel((right, y), &style.as_color().mix(1.0 - v)));
+
+ check_result!(b.draw_pixel((x, top), &style.as_color().mix(1.0 - v)));
+ check_result!(b.draw_pixel((x, bottom), &style.as_color().mix(1.0 - v)));
+ }
+
+ check_result!(b.draw_pixel((left - 1, y), &style.as_color().mix(v)));
+ check_result!(b.draw_pixel((right + 1, y), &style.as_color().mix(v)));
+ check_result!(b.draw_pixel((x, top - 1), &style.as_color().mix(v)));
+ check_result!(b.draw_pixel((x, bottom + 1), &style.as_color().mix(v)));
+ }
+
+ Ok(())
+}
diff --git a/src/drawing/rasterizer/line.rs b/src/drawing/rasterizer/line.rs
new file mode 100644
index 0000000..e1f9e5f
--- /dev/null
+++ b/src/drawing/rasterizer/line.rs
@@ -0,0 +1,126 @@
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind};
+use crate::drawing::DrawingBackend;
+
+use crate::style::Color;
+
+pub fn draw_line<DB: DrawingBackend, S: BackendStyle>(
+ back: &mut DB,
+ mut from: BackendCoord,
+ mut to: BackendCoord,
+ style: &S,
+) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+
+ if style.stroke_width() != 1 {
+ // If the line is wider than 1px, then we need to make it a polygon
+ let v = (i64::from(to.0 - from.0), i64::from(to.1 - from.1));
+ let l = ((v.0 * v.0 + v.1 * v.1) as f64).sqrt();
+
+ if l < 1e-5 {
+ return Ok(());
+ }
+
+ let v = (v.0 as f64 / l, v.1 as f64 / l);
+
+ let r = f64::from(style.stroke_width()) / 2.0;
+ let mut trans = [(v.1 * r, -v.0 * r), (-v.1 * r, v.0 * r)];
+ let mut vertices = vec![];
+
+ for point in [from, to].iter() {
+ for t in trans.iter() {
+ vertices.push((
+ (f64::from(point.0) + t.0) as i32,
+ (f64::from(point.1) + t.1) as i32,
+ ))
+ }
+
+ trans.swap(0, 1);
+ }
+
+ return back.fill_polygon(vertices, &style.as_color());
+ }
+
+ if from.0 == to.0 {
+ if from.1 > to.1 {
+ std::mem::swap(&mut from, &mut to);
+ }
+ for y in from.1..=to.1 {
+ check_result!(back.draw_pixel((from.0, y), &style.as_color()));
+ }
+ return Ok(());
+ }
+
+ if from.1 == to.1 {
+ if from.0 > to.0 {
+ std::mem::swap(&mut from, &mut to);
+ }
+ for x in from.0..=to.0 {
+ check_result!(back.draw_pixel((x, from.1), &style.as_color()));
+ }
+ return Ok(());
+ }
+
+ let steep = (from.0 - to.0).abs() < (from.1 - to.1).abs();
+
+ if steep {
+ from = (from.1, from.0);
+ to = (to.1, to.0);
+ }
+
+ let (from, to) = if from.0 > to.0 {
+ (to, from)
+ } else {
+ (from, to)
+ };
+
+ let mut size_limit = back.get_size();
+
+ if steep {
+ size_limit = (size_limit.1, size_limit.0);
+ }
+
+ let grad = f64::from(to.1 - from.1) / f64::from(to.0 - from.0);
+
+ let mut put_pixel = |(x, y): BackendCoord, b: f64| {
+ if steep {
+ back.draw_pixel((y, x), &style.as_color().mix(b))
+ } else {
+ back.draw_pixel((x, y), &style.as_color().mix(b))
+ }
+ };
+
+ let y_step_limit =
+ (f64::from(to.1.min(size_limit.1 as i32 - 1).max(0) - from.1) / grad).floor() as i32;
+
+ let batch_start = (f64::from(from.1.min(size_limit.1 as i32 - 2).max(0) - from.1) / grad)
+ .abs()
+ .ceil() as i32
+ + from.0;
+
+ let batch_limit =
+ to.0.min(size_limit.0 as i32 - 2)
+ .min(from.0 + y_step_limit - 1);
+
+ let mut y = f64::from(from.1) + f64::from(batch_start - from.0) * grad;
+
+ for x in batch_start..=batch_limit {
+ check_result!(put_pixel((x, y as i32), 1.0 + y.floor() - y));
+ check_result!(put_pixel((x, y as i32 + 1), y - y.floor()));
+
+ y += grad;
+ }
+
+ if to.0 > batch_limit && y < f64::from(to.1) {
+ let x = batch_limit as i32 + 1;
+ if 1.0 + y.floor() - y > 1e-5 {
+ check_result!(put_pixel((x, y as i32), 1.0 + y.floor() - y));
+ }
+ if y - y.floor() > 1e-5 && y + 1.0 < f64::from(to.1) {
+ check_result!(put_pixel((x, y as i32 + 1), y - y.floor()));
+ }
+ }
+
+ Ok(())
+}
diff --git a/src/drawing/rasterizer/mod.rs b/src/drawing/rasterizer/mod.rs
new file mode 100644
index 0000000..1fba804
--- /dev/null
+++ b/src/drawing/rasterizer/mod.rs
@@ -0,0 +1,24 @@
+// TODO: ? operator is very slow. See issue #58 for details
+macro_rules! check_result {
+ ($e:expr) => {
+ let result = $e;
+ if result.is_err() {
+ return result;
+ }
+ };
+}
+
+mod line;
+pub use line::draw_line;
+
+mod rect;
+pub use rect::draw_rect;
+
+mod circle;
+pub use circle::draw_circle;
+
+mod polygon;
+pub use polygon::fill_polygon;
+
+mod path;
+pub use path::polygonize;
diff --git a/src/drawing/rasterizer/path.rs b/src/drawing/rasterizer/path.rs
new file mode 100644
index 0000000..ff0be10
--- /dev/null
+++ b/src/drawing/rasterizer/path.rs
@@ -0,0 +1,115 @@
+use crate::drawing::backend::BackendCoord;
+
+fn get_dir_vector(from: BackendCoord, to: BackendCoord, flag: bool) -> ((f64, f64), (f64, f64)) {
+ let v = (i64::from(to.0 - from.0), i64::from(to.1 - from.1));
+ let l = ((v.0 * v.0 + v.1 * v.1) as f64).sqrt();
+
+ let v = (v.0 as f64 / l, v.1 as f64 / l);
+
+ if flag {
+ (v, (v.1, -v.0))
+ } else {
+ (v, (-v.1, v.0))
+ }
+}
+
+fn compute_polygon_vertex(triple: &[BackendCoord; 3], d: f64) -> BackendCoord {
+ let (a_t, a_n) = get_dir_vector(triple[0], triple[1], false);
+ let (b_t, b_n) = get_dir_vector(triple[2], triple[1], true);
+
+ let a_p = (
+ f64::from(triple[1].0) + d * a_n.0,
+ f64::from(triple[1].1) + d * a_n.1,
+ );
+ let b_p = (
+ f64::from(triple[1].0) + d * b_n.0,
+ f64::from(triple[1].1) + d * b_n.1,
+ );
+
+ // u * a_t + a_p = v * b_t + b_p
+ // u * a_t.0 - v * b_t.0 = b_p.0 - a_p.0
+ // u * a_t.1 - v * b_t.1 = b_p.1 - a_p.1
+ if a_p.0 as i32 == b_p.0 as i32 && a_p.1 as i32 == b_p.1 as i32 {
+ return (a_p.0 as i32, a_p.1 as i32);
+ }
+
+ let a0 = a_t.0;
+ let b0 = -b_t.0;
+ let c0 = b_p.0 - a_p.0;
+ let a1 = a_t.1;
+ let b1 = -b_t.1;
+ let c1 = b_p.1 - a_p.1;
+
+ // This is the coner case that
+ if (a0 * b1 - a1 * b0).abs() < 1e-10 {
+ return (a_p.0 as i32, a_p.1 as i32);
+ }
+
+ let u = (c0 * b1 - c1 * b0) / (a0 * b1 - a1 * b0);
+
+ let x = a_p.0 + u * a_t.0;
+ let y = a_p.1 + u * a_t.1;
+
+ (x.round() as i32, y.round() as i32)
+}
+
+fn traverse_vertices<'a>(
+ mut vertices: impl Iterator<Item = &'a BackendCoord>,
+ width: u32,
+ mut op: impl FnMut(BackendCoord),
+) {
+ let mut a = vertices.next().unwrap();
+ let mut b = vertices.next().unwrap();
+
+ while a == b {
+ a = b;
+ if let Some(new_b) = vertices.next() {
+ b = new_b;
+ } else {
+ return;
+ }
+ }
+
+ let (_, n) = get_dir_vector(*a, *b, false);
+
+ op((
+ (f64::from(a.0) + n.0 * f64::from(width) / 2.0).round() as i32,
+ (f64::from(a.1) + n.1 * f64::from(width) / 2.0).round() as i32,
+ ));
+
+ let mut recent = [(0, 0), *a, *b];
+
+ for p in vertices {
+ if *p == recent[2] {
+ continue;
+ }
+ recent.swap(0, 1);
+ recent.swap(1, 2);
+ recent[2] = *p;
+
+ op(compute_polygon_vertex(&recent, f64::from(width) / 2.0));
+ }
+
+ let b = recent[1];
+ let a = recent[2];
+
+ let (_, n) = get_dir_vector(a, b, true);
+
+ op((
+ (f64::from(a.0) + n.0 * f64::from(width) / 2.0).round() as i32,
+ (f64::from(a.1) + n.1 * f64::from(width) / 2.0).round() as i32,
+ ));
+}
+
+pub fn polygonize(vertices: &[BackendCoord], stroke_width: u32) -> Vec<BackendCoord> {
+ if vertices.len() < 2 {
+ return vec![];
+ }
+
+ let mut ret = vec![];
+
+ traverse_vertices(vertices.iter(), stroke_width, |v| ret.push(v));
+ traverse_vertices(vertices.iter().rev(), stroke_width, |v| ret.push(v));
+
+ ret
+}
diff --git a/src/drawing/rasterizer/polygon.rs b/src/drawing/rasterizer/polygon.rs
new file mode 100644
index 0000000..169b83a
--- /dev/null
+++ b/src/drawing/rasterizer/polygon.rs
@@ -0,0 +1,245 @@
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind};
+use crate::drawing::DrawingBackend;
+
+use crate::style::Color;
+
+use std::cmp::{Ord, Ordering, PartialOrd};
+
+#[derive(Clone, Debug)]
+struct Edge {
+ epoch: u32,
+ total_epoch: u32,
+ slave_begin: i32,
+ slave_end: i32,
+}
+
+impl Edge {
+ fn horizontal_sweep(mut from: BackendCoord, mut to: BackendCoord) -> Option<Edge> {
+ if from.0 == to.0 {
+ return None;
+ }
+
+ if from.0 > to.0 {
+ std::mem::swap(&mut from, &mut to);
+ }
+
+ Some(Edge {
+ epoch: 0,
+ total_epoch: (to.0 - from.0) as u32,
+ slave_begin: from.1,
+ slave_end: to.1,
+ })
+ }
+
+ fn vertical_sweep(from: BackendCoord, to: BackendCoord) -> Option<Edge> {
+ Edge::horizontal_sweep((from.1, from.0), (to.1, to.0))
+ }
+
+ fn get_master_pos(&self) -> i32 {
+ (self.total_epoch - self.epoch) as i32
+ }
+
+ fn inc_epoch(&mut self) {
+ self.epoch += 1;
+ }
+
+ fn get_slave_pos(&self) -> f64 {
+ f64::from(self.slave_begin)
+ + (i64::from(self.slave_end - self.slave_begin) * i64::from(self.epoch)) as f64
+ / f64::from(self.total_epoch)
+ }
+}
+
+impl PartialOrd for Edge {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ self.get_slave_pos().partial_cmp(&other.get_slave_pos())
+ }
+}
+
+impl PartialEq for Edge {
+ fn eq(&self, other: &Self) -> bool {
+ self.get_slave_pos() == other.get_slave_pos()
+ }
+}
+
+impl Eq for Edge {}
+
+impl Ord for Edge {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.get_slave_pos()
+ .partial_cmp(&other.get_slave_pos())
+ .unwrap()
+ }
+}
+
+pub fn fill_polygon<DB: DrawingBackend, S: BackendStyle>(
+ back: &mut DB,
+ vertices: &[BackendCoord],
+ style: &S,
+) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some((x_span, y_span)) =
+ vertices
+ .iter()
+ .fold(None, |res: Option<((i32, i32), (i32, i32))>, (x, y)| {
+ Some(
+ res.map(|((min_x, max_x), (min_y, max_y))| {
+ (
+ (min_x.min(*x), max_x.max(*x)),
+ (min_y.min(*y), max_y.max(*y)),
+ )
+ })
+ .unwrap_or(((*x, *x), (*y, *y))),
+ )
+ })
+ {
+ // First of all, let's handle the case that all the points is in a same vertical or
+ // horizontal line
+ if x_span.0 == x_span.1 || y_span.0 == y_span.1 {
+ return back.draw_line((x_span.0, y_span.0), (x_span.1, y_span.1), style);
+ }
+
+ let horizontal_sweep = x_span.1 - x_span.0 > y_span.1 - y_span.0;
+
+ let mut edges: Vec<_> = vertices
+ .iter()
+ .zip(vertices.iter().skip(1))
+ .map(|(a, b)| (*a, *b))
+ .collect();
+ edges.push((vertices[vertices.len() - 1], vertices[0]));
+ edges.sort_by_key(|((x1, y1), (x2, y2))| {
+ if horizontal_sweep {
+ *x1.min(x2)
+ } else {
+ *y1.min(y2)
+ }
+ });
+
+ for edge in &mut edges.iter_mut() {
+ if horizontal_sweep {
+ if (edge.0).0 > (edge.1).0 {
+ std::mem::swap(&mut edge.0, &mut edge.1);
+ }
+ } else if (edge.0).1 > (edge.1).1 {
+ std::mem::swap(&mut edge.0, &mut edge.1);
+ }
+ }
+
+ let (low, high) = if horizontal_sweep { x_span } else { y_span };
+
+ let mut idx = 0;
+
+ let mut active_edge: Vec<Edge> = vec![];
+
+ for sweep_line in low..=high {
+ let mut new_vec = vec![];
+
+ for mut e in active_edge {
+ if e.get_master_pos() > 0 {
+ e.inc_epoch();
+ new_vec.push(e);
+ }
+ }
+
+ active_edge = new_vec;
+
+ loop {
+ if idx >= edges.len() {
+ break;
+ }
+ let line = if horizontal_sweep {
+ (edges[idx].0).0
+ } else {
+ (edges[idx].0).1
+ };
+ if line > sweep_line {
+ break;
+ }
+
+ let edge_obj = if horizontal_sweep {
+ Edge::horizontal_sweep(edges[idx].0, edges[idx].1)
+ } else {
+ Edge::vertical_sweep(edges[idx].0, edges[idx].1)
+ };
+
+ if let Some(edge_obj) = edge_obj {
+ active_edge.push(edge_obj);
+ }
+
+ idx += 1;
+ }
+
+ active_edge.sort();
+
+ let mut first = None;
+ let mut second = None;
+
+ for edge in active_edge.iter() {
+ if first.is_none() {
+ first = Some(edge.clone())
+ } else if second.is_none() {
+ second = Some(edge.clone())
+ }
+
+ if let Some(a) = first.clone() {
+ if let Some(b) = second.clone() {
+ if a.get_master_pos() == 0 && b.get_master_pos() != 0 {
+ first = Some(b);
+ second = None;
+ continue;
+ }
+
+ if a.get_master_pos() != 0 && b.get_master_pos() == 0 {
+ first = Some(a);
+ second = None;
+ continue;
+ }
+
+ let from = a.get_slave_pos();
+ let to = b.get_slave_pos();
+
+ if a.get_master_pos() == 0 && b.get_master_pos() == 0 && to - from > 1.0 {
+ first = None;
+ second = None;
+ continue;
+ }
+
+ if horizontal_sweep {
+ check_result!(back.draw_line(
+ (sweep_line, from.ceil() as i32),
+ (sweep_line, to.floor() as i32),
+ &style.as_color(),
+ ));
+ check_result!(back.draw_pixel(
+ (sweep_line, from.floor() as i32),
+ &style.as_color().mix(from.ceil() - from),
+ ));
+ check_result!(back.draw_pixel(
+ (sweep_line, to.ceil() as i32),
+ &style.as_color().mix(to - to.floor()),
+ ));
+ } else {
+ check_result!(back.draw_line(
+ (from.ceil() as i32, sweep_line),
+ (to.floor() as i32, sweep_line),
+ &style.as_color(),
+ ));
+ check_result!(back.draw_pixel(
+ (from.floor() as i32, sweep_line),
+ &style.as_color().mix(from.ceil() - from),
+ ));
+ check_result!(back.draw_pixel(
+ (to.ceil() as i32, sweep_line),
+ &style.as_color().mix(to.floor() - to),
+ ));
+ }
+
+ first = None;
+ second = None;
+ }
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
diff --git a/src/drawing/rasterizer/rect.rs b/src/drawing/rasterizer/rect.rs
new file mode 100644
index 0000000..659fbba
--- /dev/null
+++ b/src/drawing/rasterizer/rect.rs
@@ -0,0 +1,60 @@
+use crate::drawing::backend::{BackendCoord, BackendStyle, DrawingErrorKind};
+use crate::drawing::DrawingBackend;
+
+use crate::style::Color;
+
+pub fn draw_rect<B: DrawingBackend, S: BackendStyle>(
+ b: &mut B,
+ upper_left: BackendCoord,
+ bottom_right: BackendCoord,
+ style: &S,
+ fill: bool,
+) -> Result<(), DrawingErrorKind<B::ErrorType>> {
+ if style.as_color().alpha() == 0.0 {
+ return Ok(());
+ }
+ let (upper_left, bottom_right) = (
+ (
+ upper_left.0.min(bottom_right.0),
+ upper_left.1.min(bottom_right.1),
+ ),
+ (
+ upper_left.0.max(bottom_right.0),
+ upper_left.1.max(bottom_right.1),
+ ),
+ );
+
+ if fill {
+ if bottom_right.0 - upper_left.0 < bottom_right.1 - upper_left.1 {
+ for x in upper_left.0..=bottom_right.0 {
+ check_result!(b.draw_line((x, upper_left.1), (x, bottom_right.1), style));
+ }
+ } else {
+ for y in upper_left.1..=bottom_right.1 {
+ check_result!(b.draw_line((upper_left.0, y), (bottom_right.0, y), style));
+ }
+ }
+ } else {
+ b.draw_line(
+ (upper_left.0, upper_left.1),
+ (upper_left.0, bottom_right.1),
+ style,
+ )?;
+ b.draw_line(
+ (upper_left.0, upper_left.1),
+ (bottom_right.0, upper_left.1),
+ style,
+ )?;
+ b.draw_line(
+ (bottom_right.0, bottom_right.1),
+ (upper_left.0, bottom_right.1),
+ style,
+ )?;
+ b.draw_line(
+ (bottom_right.0, bottom_right.1),
+ (bottom_right.0, upper_left.1),
+ style,
+ )?;
+ }
+ Ok(())
+}
diff --git a/src/element/basic_shapes.rs b/src/element/basic_shapes.rs
new file mode 100644
index 0000000..c0a453f
--- /dev/null
+++ b/src/element/basic_shapes.rs
@@ -0,0 +1,348 @@
+use super::{Drawable, PointCollection};
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use crate::style::{ShapeStyle, SizeDesc};
+
+/// An element of a single pixel
+pub struct Pixel<Coord> {
+ pos: Coord,
+ style: ShapeStyle,
+}
+
+impl<Coord> Pixel<Coord> {
+ pub fn new<P: Into<Coord>, S: Into<ShapeStyle>>(pos: P, style: S) -> Self {
+ Self {
+ pos: pos.into(),
+ style: style.into(),
+ }
+ }
+}
+
+impl<'a, Coord> PointCollection<'a, Coord> for &'a Pixel<Coord> {
+ type Borrow = &'a Coord;
+ type IntoIter = std::iter::Once<&'a Coord>;
+ fn point_iter(self) -> Self::IntoIter {
+ std::iter::once(&self.pos)
+ }
+}
+
+impl<Coord, DB: DrawingBackend> Drawable<DB> for Pixel<Coord> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some((x, y)) = points.next() {
+ return backend.draw_pixel((x, y), &self.style.color);
+ }
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_pixel_element() {
+ use crate::prelude::*;
+ let da = crate::create_mocked_drawing_area(300, 300, |m| {
+ m.check_draw_pixel(|c, (x, y)| {
+ assert_eq!(x, 150);
+ assert_eq!(y, 152);
+ assert_eq!(c, RED.to_rgba());
+ });
+
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_pixel_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ });
+ da.draw(&Pixel::new((150, 152), &RED))
+ .expect("Drawing Failure");
+}
+
+#[deprecated(note = "Use new name PathElement instead")]
+pub type Path<Coord> = PathElement<Coord>;
+
+/// An element of a series of connected lines
+pub struct PathElement<Coord> {
+ points: Vec<Coord>,
+ style: ShapeStyle,
+}
+impl<Coord> PathElement<Coord> {
+ /// Create a new path
+ /// - `points`: The iterator of the points
+ /// - `style`: The shape style
+ /// - returns the created element
+ pub fn new<P: Into<Vec<Coord>>, S: Into<ShapeStyle>>(points: P, style: S) -> Self {
+ Self {
+ points: points.into(),
+ style: style.into(),
+ }
+ }
+}
+
+impl<'a, Coord> PointCollection<'a, Coord> for &'a PathElement<Coord> {
+ type Borrow = &'a Coord;
+ type IntoIter = &'a [Coord];
+ fn point_iter(self) -> &'a [Coord] {
+ &self.points
+ }
+}
+
+impl<Coord, DB: DrawingBackend> Drawable<DB> for PathElement<Coord> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ backend.draw_path(points, &self.style)
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_path_element() {
+ use crate::prelude::*;
+ let da = crate::create_mocked_drawing_area(300, 300, |m| {
+ m.check_draw_path(|c, s, path| {
+ assert_eq!(c, BLUE.to_rgba());
+ assert_eq!(s, 5);
+ assert_eq!(path, vec![(100, 101), (105, 107), (150, 157)]);
+ });
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_path_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ });
+ da.draw(&PathElement::new(
+ vec![(100, 101), (105, 107), (150, 157)],
+ Into::<ShapeStyle>::into(&BLUE).stroke_width(5),
+ ))
+ .expect("Drawing Failure");
+}
+
+/// A rectangle element
+pub struct Rectangle<Coord> {
+ points: [Coord; 2],
+ style: ShapeStyle,
+ margin: (u32, u32, u32, u32),
+}
+
+impl<Coord> Rectangle<Coord> {
+ /// Create a new path
+ /// - `points`: The left upper and right lower corner of the rectangle
+ /// - `style`: The shape style
+ /// - returns the created element
+ pub fn new<S: Into<ShapeStyle>>(points: [Coord; 2], style: S) -> Self {
+ Self {
+ points,
+ style: style.into(),
+ margin: (0, 0, 0, 0),
+ }
+ }
+
+ /// Set the margin of the rectangle
+ /// - `t`: The top margin
+ /// - `b`: The bottom margin
+ /// - `l`: The left margin
+ /// - `r`: The right margin
+ pub fn set_margin(&mut self, t: u32, b: u32, l: u32, r: u32) -> &mut Self {
+ self.margin = (t, b, l, r);
+ self
+ }
+}
+
+impl<'a, Coord> PointCollection<'a, Coord> for &'a Rectangle<Coord> {
+ type Borrow = &'a Coord;
+ type IntoIter = &'a [Coord];
+ fn point_iter(self) -> &'a [Coord] {
+ &self.points
+ }
+}
+
+impl<Coord, DB: DrawingBackend> Drawable<DB> for Rectangle<Coord> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ match (points.next(), points.next()) {
+ (Some(a), Some(b)) => {
+ let (mut a, mut b) = ((a.0.min(b.0), a.1.min(b.1)), (a.0.max(b.0), a.1.max(b.1)));
+ a.1 += self.margin.0 as i32;
+ b.1 -= self.margin.1 as i32;
+ a.0 += self.margin.2 as i32;
+ b.0 -= self.margin.3 as i32;
+ backend.draw_rect(a, b, &self.style, self.style.filled)
+ }
+ _ => Ok(()),
+ }
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_rect_element() {
+ use crate::prelude::*;
+ {
+ let da = crate::create_mocked_drawing_area(300, 300, |m| {
+ m.check_draw_rect(|c, s, f, u, d| {
+ assert_eq!(c, BLUE.to_rgba());
+ assert_eq!(f, false);
+ assert_eq!(s, 5);
+ assert_eq!([u, d], [(100, 101), (105, 107)]);
+ });
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_rect_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ });
+ da.draw(&Rectangle::new(
+ [(100, 101), (105, 107)],
+ BLUE.stroke_width(5),
+ ))
+ .expect("Drawing Failure");
+ }
+
+ {
+ let da = crate::create_mocked_drawing_area(300, 300, |m| {
+ m.check_draw_rect(|c, _, f, u, d| {
+ assert_eq!(c, BLUE.to_rgba());
+ assert_eq!(f, true);
+ assert_eq!([u, d], [(100, 101), (105, 107)]);
+ });
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_rect_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ });
+ da.draw(&Rectangle::new([(100, 101), (105, 107)], BLUE.filled()))
+ .expect("Drawing Failure");
+ }
+}
+
+/// A circle element
+pub struct Circle<Coord, Size: SizeDesc> {
+ center: Coord,
+ size: Size,
+ style: ShapeStyle,
+}
+
+impl<Coord, Size: SizeDesc> Circle<Coord, Size> {
+ /// Create a new circle element
+ /// - `coord` The center of the circle
+ /// - `size` The radius of the circle
+ /// - `style` The style of the circle
+ /// - Return: The newly created circle element
+ pub fn new<S: Into<ShapeStyle>>(coord: Coord, size: Size, style: S) -> Self {
+ Self {
+ center: coord,
+ size,
+ style: style.into(),
+ }
+ }
+}
+
+impl<'a, Coord, Size: SizeDesc> PointCollection<'a, Coord> for &'a Circle<Coord, Size> {
+ type Borrow = &'a Coord;
+ type IntoIter = std::iter::Once<&'a Coord>;
+ fn point_iter(self) -> std::iter::Once<&'a Coord> {
+ std::iter::once(&self.center)
+ }
+}
+
+impl<Coord, DB: DrawingBackend, Size: SizeDesc> Drawable<DB> for Circle<Coord, Size> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut points: I,
+ backend: &mut DB,
+ ps: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some((x, y)) = points.next() {
+ let size = self.size.in_pixels(&ps).max(0) as u32;
+ return backend.draw_circle((x, y), size, &self.style, self.style.filled);
+ }
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_circle_element() {
+ use crate::prelude::*;
+ let da = crate::create_mocked_drawing_area(300, 300, |m| {
+ m.check_draw_circle(|c, _, f, s, r| {
+ assert_eq!(c, BLUE.to_rgba());
+ assert_eq!(f, false);
+ assert_eq!(s, (150, 151));
+ assert_eq!(r, 20);
+ });
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_circle_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ });
+ da.draw(&Circle::new((150, 151), 20, &BLUE))
+ .expect("Drawing Failure");
+}
+
+/// An element of a filled polygon
+pub struct Polygon<Coord> {
+ points: Vec<Coord>,
+ style: ShapeStyle,
+}
+impl<Coord> Polygon<Coord> {
+ /// Create a new polygon
+ /// - `points`: The iterator of the points
+ /// - `style`: The shape style
+ /// - returns the created element
+ pub fn new<P: Into<Vec<Coord>>, S: Into<ShapeStyle>>(points: P, style: S) -> Self {
+ Self {
+ points: points.into(),
+ style: style.into(),
+ }
+ }
+}
+
+impl<'a, Coord> PointCollection<'a, Coord> for &'a Polygon<Coord> {
+ type Borrow = &'a Coord;
+ type IntoIter = &'a [Coord];
+ fn point_iter(self) -> &'a [Coord] {
+ &self.points
+ }
+}
+
+impl<Coord, DB: DrawingBackend> Drawable<DB> for Polygon<Coord> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ backend.fill_polygon(points, &self.style.color)
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_polygon_element() {
+ use crate::prelude::*;
+ let points = vec![(100, 100), (50, 500), (300, 400), (200, 300), (550, 200)];
+ let expected_points = points.clone();
+
+ let da = crate::create_mocked_drawing_area(800, 800, |m| {
+ m.check_fill_polygon(move |c, p| {
+ assert_eq!(c, BLUE.to_rgba());
+ assert_eq!(expected_points.len(), p.len());
+ assert_eq!(expected_points, p);
+ });
+ m.drop_check(|b| {
+ assert_eq!(b.num_fill_polygon_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ });
+
+ da.draw(&Polygon::new(points.clone(), &BLUE))
+ .expect("Drawing Failure");
+}
diff --git a/src/element/boxplot.rs b/src/element/boxplot.rs
new file mode 100644
index 0000000..c8040bc
--- /dev/null
+++ b/src/element/boxplot.rs
@@ -0,0 +1,283 @@
+use std::marker::PhantomData;
+
+use crate::data::Quartiles;
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use crate::element::{Drawable, PointCollection};
+use crate::style::{ShapeStyle, BLACK};
+
+/// The boxplot orientation trait
+pub trait BoxplotOrient<K, V> {
+ type XType;
+ type YType;
+
+ fn make_coord(key: K, val: V) -> (Self::XType, Self::YType);
+ fn with_offset(coord: BackendCoord, offset: f64) -> BackendCoord;
+}
+
+/// The vertical boxplot phantom
+pub struct BoxplotOrientV<K, V>(PhantomData<(K, V)>);
+
+/// The horizontal boxplot phantom
+pub struct BoxplotOrientH<K, V>(PhantomData<(K, V)>);
+
+impl<K, V> BoxplotOrient<K, V> for BoxplotOrientV<K, V> {
+ type XType = K;
+ type YType = V;
+
+ fn make_coord(key: K, val: V) -> (K, V) {
+ (key, val)
+ }
+
+ fn with_offset(coord: BackendCoord, offset: f64) -> BackendCoord {
+ (coord.0 + offset as i32, coord.1)
+ }
+}
+
+impl<K, V> BoxplotOrient<K, V> for BoxplotOrientH<K, V> {
+ type XType = V;
+ type YType = K;
+
+ fn make_coord(key: K, val: V) -> (V, K) {
+ (val, key)
+ }
+
+ fn with_offset(coord: BackendCoord, offset: f64) -> BackendCoord {
+ (coord.0, coord.1 + offset as i32)
+ }
+}
+
+const DEFAULT_WIDTH: u32 = 10;
+
+/// The boxplot element
+pub struct Boxplot<K, O: BoxplotOrient<K, f32>> {
+ style: ShapeStyle,
+ width: u32,
+ whisker_width: f64,
+ offset: f64,
+ key: K,
+ values: [f32; 5],
+ _p: PhantomData<O>,
+}
+
+impl<K: Clone> Boxplot<K, BoxplotOrientV<K, f32>> {
+ /// Create a new vertical boxplot element.
+ ///
+ /// - `key`: The key (the X axis value)
+ /// - `quartiles`: The quartiles values for the Y axis
+ /// - **returns** The newly created boxplot element
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]);
+ /// let plot = Boxplot::new_vertical("group", &quartiles);
+ /// ```
+ pub fn new_vertical(key: K, quartiles: &Quartiles) -> Self {
+ Self {
+ style: Into::<ShapeStyle>::into(&BLACK),
+ width: DEFAULT_WIDTH,
+ whisker_width: 1.0,
+ offset: 0.0,
+ key,
+ values: quartiles.values(),
+ _p: PhantomData,
+ }
+ }
+}
+
+impl<K: Clone> Boxplot<K, BoxplotOrientH<K, f32>> {
+ /// Create a new horizontal boxplot element.
+ ///
+ /// - `key`: The key (the Y axis value)
+ /// - `quartiles`: The quartiles values for the X axis
+ /// - **returns** The newly created boxplot element
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]);
+ /// let plot = Boxplot::new_horizontal("group", &quartiles);
+ /// ```
+ pub fn new_horizontal(key: K, quartiles: &Quartiles) -> Self {
+ Self {
+ style: Into::<ShapeStyle>::into(&BLACK),
+ width: DEFAULT_WIDTH,
+ whisker_width: 1.0,
+ offset: 0.0,
+ key,
+ values: quartiles.values(),
+ _p: PhantomData,
+ }
+ }
+}
+
+impl<K, O: BoxplotOrient<K, f32>> Boxplot<K, O> {
+ /// Set the style of the boxplot.
+ ///
+ /// - `S`: The required style
+ /// - **returns** The up-to-dated boxplot element
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]);
+ /// let plot = Boxplot::new_horizontal("group", &quartiles).style(&BLUE);
+ /// ```
+ pub fn style<S: Into<ShapeStyle>>(mut self, style: S) -> Self {
+ self.style = style.into();
+ self
+ }
+
+ /// Set the bar width.
+ ///
+ /// - `width`: The required width
+ /// - **returns** The up-to-dated boxplot element
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]);
+ /// let plot = Boxplot::new_horizontal("group", &quartiles).width(10);
+ /// ```
+ pub fn width(mut self, width: u32) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Set the width of the whiskers as a fraction of the bar width.
+ ///
+ /// - `whisker_width`: The required fraction
+ /// - **returns** The up-to-dated boxplot element
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]);
+ /// let plot = Boxplot::new_horizontal("group", &quartiles).whisker_width(0.5);
+ /// ```
+ pub fn whisker_width(mut self, whisker_width: f64) -> Self {
+ self.whisker_width = whisker_width;
+ self
+ }
+
+ /// Set the element offset on the key axis.
+ ///
+ /// - `offset`: The required offset (on the X axis for vertical, on the Y axis for horizontal)
+ /// - **returns** The up-to-dated boxplot element
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let quartiles = Quartiles::new(&[7, 15, 36, 39, 40, 41]);
+ /// let plot = Boxplot::new_horizontal("group", &quartiles).offset(-5);
+ /// ```
+ pub fn offset<T: Into<f64> + Copy>(mut self, offset: T) -> Self {
+ self.offset = offset.into();
+ self
+ }
+}
+
+impl<'a, K: 'a + Clone, O: BoxplotOrient<K, f32>> PointCollection<'a, (O::XType, O::YType)>
+ for &'a Boxplot<K, O>
+{
+ type Borrow = (O::XType, O::YType);
+ type IntoIter = Vec<Self::Borrow>;
+ fn point_iter(self) -> Self::IntoIter {
+ self.values
+ .iter()
+ .map(|v| O::make_coord(self.key.clone(), *v))
+ .collect()
+ }
+}
+
+impl<K, DB: DrawingBackend, O: BoxplotOrient<K, f32>> Drawable<DB> for Boxplot<K, O> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ let points: Vec<_> = points.take(5).collect();
+ if points.len() == 5 {
+ let width = f64::from(self.width);
+ let moved = |coord| O::with_offset(coord, self.offset);
+ let start_bar = |coord| O::with_offset(moved(coord), -width / 2.0);
+ let end_bar = |coord| O::with_offset(moved(coord), width / 2.0);
+ let start_whisker =
+ |coord| O::with_offset(moved(coord), -width * self.whisker_width / 2.0);
+ let end_whisker =
+ |coord| O::with_offset(moved(coord), width * self.whisker_width / 2.0);
+
+ // |---[ | ]----|
+ // ^________________
+ backend.draw_line(
+ start_whisker(points[0]),
+ end_whisker(points[0]),
+ &self.style,
+ )?;
+
+ // |---[ | ]----|
+ // _^^^_____________
+ backend.draw_line(moved(points[0]), moved(points[1]), &self.style.color)?;
+
+ // |---[ | ]----|
+ // ____^______^_____
+ let corner1 = start_bar(points[3]);
+ let corner2 = end_bar(points[1]);
+ let upper_left = (corner1.0.min(corner2.0), corner1.1.min(corner2.1));
+ let bottom_right = (corner1.0.max(corner2.0), corner1.1.max(corner2.1));
+ backend.draw_rect(upper_left, bottom_right, &self.style, false)?;
+
+ // |---[ | ]----|
+ // ________^________
+ backend.draw_line(start_bar(points[2]), end_bar(points[2]), &self.style)?;
+
+ // |---[ | ]----|
+ // ____________^^^^_
+ backend.draw_line(moved(points[3]), moved(points[4]), &self.style)?;
+
+ // |---[ | ]----|
+ // ________________^
+ backend.draw_line(
+ start_whisker(points[4]),
+ end_whisker(points[4]),
+ &self.style,
+ )?;
+ }
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::prelude::*;
+
+ #[test]
+ fn test_draw_v() {
+ let root = MockedBackend::new(1024, 768).into_drawing_area();
+ let chart = ChartBuilder::on(&root)
+ .build_ranged(0..2, 0f32..100f32)
+ .unwrap();
+
+ let values = Quartiles::new(&[6]);
+ assert!(chart
+ .plotting_area()
+ .draw(&Boxplot::new_vertical(1, &values))
+ .is_ok());
+ }
+
+ #[test]
+ fn test_draw_h() {
+ let root = MockedBackend::new(1024, 768).into_drawing_area();
+ let chart = ChartBuilder::on(&root)
+ .build_ranged(0f32..100f32, 0..2)
+ .unwrap();
+
+ let values = Quartiles::new(&[6]);
+ assert!(chart
+ .plotting_area()
+ .draw(&Boxplot::new_horizontal(1, &values))
+ .is_ok());
+ }
+}
diff --git a/src/element/candlestick.rs b/src/element/candlestick.rs
new file mode 100644
index 0000000..b026425
--- /dev/null
+++ b/src/element/candlestick.rs
@@ -0,0 +1,100 @@
+/*!
+ The candlestick element, which showing the high/low/open/close price
+*/
+
+use std::cmp::Ordering;
+
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use crate::element::{Drawable, PointCollection};
+use crate::style::ShapeStyle;
+
+/// The candlestick data point element
+pub struct CandleStick<X, Y: PartialOrd> {
+ style: ShapeStyle,
+ width: u32,
+ points: [(X, Y); 4],
+}
+
+impl<X: Clone, Y: PartialOrd> CandleStick<X, Y> {
+ /// Create a new candlestick element, which requires the Y coordinate can be compared
+ ///
+ /// - `x`: The x coordinate
+ /// - `open`: The open value
+ /// - `high`: The high value
+ /// - `low`: The low value
+ /// - `close`: The close value
+ /// - `gain_style`: The style for gain
+ /// - `loss_style`: The style for loss
+ /// - `width`: The width
+ /// - **returns** The newly created candlestick element
+ ///
+ /// ```rust
+ /// use chrono::prelude::*;
+ /// use plotters::prelude::*;
+ ///
+ /// let candlestick = CandleStick::new(Local::now(), 130.0600, 131.3700, 128.8300, 129.1500, &GREEN, &RED, 15);
+ /// ```
+ #[allow(clippy::too_many_arguments)]
+ pub fn new<GS: Into<ShapeStyle>, LS: Into<ShapeStyle>>(
+ x: X,
+ open: Y,
+ high: Y,
+ low: Y,
+ close: Y,
+ gain_style: GS,
+ loss_style: LS,
+ width: u32,
+ ) -> Self {
+ Self {
+ style: match open.partial_cmp(&close) {
+ Some(Ordering::Less) => gain_style.into(),
+ _ => loss_style.into(),
+ },
+ width,
+ points: [
+ (x.clone(), open),
+ (x.clone(), high),
+ (x.clone(), low),
+ (x, close),
+ ],
+ }
+ }
+}
+
+impl<'a, X: 'a, Y: PartialOrd + 'a> PointCollection<'a, (X, Y)> for &'a CandleStick<X, Y> {
+ type Borrow = &'a (X, Y);
+ type IntoIter = &'a [(X, Y)];
+ fn point_iter(self) -> &'a [(X, Y)] {
+ &self.points
+ }
+}
+
+impl<X, Y: PartialOrd, DB: DrawingBackend> Drawable<DB> for CandleStick<X, Y> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ let mut points: Vec<_> = points.take(4).collect();
+ if points.len() == 4 {
+ let fill = false;
+ if points[0].1 > points[3].1 {
+ points.swap(0, 3);
+ }
+ let (l, r) = (
+ self.width as i32 / 2,
+ self.width as i32 - self.width as i32 / 2,
+ );
+
+ backend.draw_line(points[0], points[1], &self.style)?;
+ backend.draw_line(points[2], points[3], &self.style)?;
+
+ points[0].0 -= l;
+ points[3].0 += r;
+
+ backend.draw_rect(points[0], points[3], &self.style, fill)?;
+ }
+ Ok(())
+ }
+}
diff --git a/src/element/composable.rs b/src/element/composable.rs
new file mode 100644
index 0000000..95ff380
--- /dev/null
+++ b/src/element/composable.rs
@@ -0,0 +1,200 @@
+use super::*;
+use crate::drawing::backend::DrawingBackend;
+use std::borrow::Borrow;
+use std::iter::{once, Once};
+use std::marker::PhantomData;
+use std::ops::Add;
+
+/// An empty composable element, which is the start point of an ad-hoc composable element
+pub struct EmptyElement<Coord, DB: DrawingBackend> {
+ coord: Coord,
+ phantom: PhantomData<DB>,
+}
+
+impl<Coord, DB: DrawingBackend> EmptyElement<Coord, DB> {
+ pub fn at(coord: Coord) -> Self {
+ Self {
+ coord,
+ phantom: PhantomData,
+ }
+ }
+}
+
+impl<Coord, Other, DB: DrawingBackend> Add<Other> for EmptyElement<Coord, DB>
+where
+ Other: Drawable<DB>,
+ for<'a> &'a Other: PointCollection<'a, BackendCoord>,
+{
+ type Output = BoxedElement<Coord, DB, Other>;
+ fn add(self, other: Other) -> Self::Output {
+ BoxedElement {
+ offset: self.coord,
+ inner: other,
+ phantom: PhantomData,
+ }
+ }
+}
+
+impl<'a, Coord, DB: DrawingBackend> PointCollection<'a, Coord> for &'a EmptyElement<Coord, DB> {
+ type Borrow = &'a Coord;
+ type IntoIter = Once<&'a Coord>;
+ fn point_iter(self) -> Self::IntoIter {
+ once(&self.coord)
+ }
+}
+
+impl<Coord, DB: DrawingBackend> Drawable<DB> for EmptyElement<Coord, DB> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ _pos: I,
+ _backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ Ok(())
+ }
+}
+
+/// An composed element has only one component
+pub struct BoxedElement<Coord, DB: DrawingBackend, A: Drawable<DB>> {
+ inner: A,
+ offset: Coord,
+ phantom: PhantomData<DB>,
+}
+
+impl<'b, Coord, DB: DrawingBackend, A: Drawable<DB>> PointCollection<'b, Coord>
+ for &'b BoxedElement<Coord, DB, A>
+{
+ type Borrow = &'b Coord;
+ type IntoIter = Once<&'b Coord>;
+ fn point_iter(self) -> Self::IntoIter {
+ once(&self.offset)
+ }
+}
+
+impl<Coord, DB: DrawingBackend, A> Drawable<DB> for BoxedElement<Coord, DB, A>
+where
+ for<'a> &'a A: PointCollection<'a, BackendCoord>,
+ A: Drawable<DB>,
+{
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut pos: I,
+ backend: &mut DB,
+ ps: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some((x0, y0)) = pos.next() {
+ self.inner.draw(
+ self.inner.point_iter().into_iter().map(|p| {
+ let p = p.borrow();
+ (p.0 + x0, p.1 + y0)
+ }),
+ backend,
+ ps,
+ )?;
+ }
+ Ok(())
+ }
+}
+
+impl<Coord, DB: DrawingBackend, My, Yours> Add<Yours> for BoxedElement<Coord, DB, My>
+where
+ My: Drawable<DB>,
+ for<'a> &'a My: PointCollection<'a, BackendCoord>,
+ Yours: Drawable<DB>,
+ for<'a> &'a Yours: PointCollection<'a, BackendCoord>,
+{
+ type Output = ComposedElement<Coord, DB, My, Yours>;
+ fn add(self, yours: Yours) -> Self::Output {
+ ComposedElement {
+ offset: self.offset,
+ first: self.inner,
+ second: yours,
+ phantom: PhantomData,
+ }
+ }
+}
+
+/// The composed element which has at least two components
+pub struct ComposedElement<Coord, DB: DrawingBackend, A, B>
+where
+ A: Drawable<DB>,
+ B: Drawable<DB>,
+{
+ first: A,
+ second: B,
+ offset: Coord,
+ phantom: PhantomData<DB>,
+}
+
+impl<'b, Coord, DB: DrawingBackend, A, B> PointCollection<'b, Coord>
+ for &'b ComposedElement<Coord, DB, A, B>
+where
+ A: Drawable<DB>,
+ B: Drawable<DB>,
+{
+ type Borrow = &'b Coord;
+ type IntoIter = Once<&'b Coord>;
+ fn point_iter(self) -> Self::IntoIter {
+ once(&self.offset)
+ }
+}
+
+impl<Coord, DB: DrawingBackend, A, B> Drawable<DB> for ComposedElement<Coord, DB, A, B>
+where
+ for<'a> &'a A: PointCollection<'a, BackendCoord>,
+ for<'b> &'b B: PointCollection<'b, BackendCoord>,
+ A: Drawable<DB>,
+ B: Drawable<DB>,
+{
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut pos: I,
+ backend: &mut DB,
+ ps: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some((x0, y0)) = pos.next() {
+ self.first.draw(
+ self.first.point_iter().into_iter().map(|p| {
+ let p = p.borrow();
+ (p.0 + x0, p.1 + y0)
+ }),
+ backend,
+ ps,
+ )?;
+ self.second.draw(
+ self.second.point_iter().into_iter().map(|p| {
+ let p = p.borrow();
+ (p.0 + x0, p.1 + y0)
+ }),
+ backend,
+ ps,
+ )?;
+ }
+ Ok(())
+ }
+}
+
+impl<Coord, DB: DrawingBackend, A, B, C> Add<C> for ComposedElement<Coord, DB, A, B>
+where
+ A: Drawable<DB>,
+ for<'a> &'a A: PointCollection<'a, BackendCoord>,
+ B: Drawable<DB>,
+ for<'a> &'a B: PointCollection<'a, BackendCoord>,
+ C: Drawable<DB>,
+ for<'a> &'a C: PointCollection<'a, BackendCoord>,
+{
+ type Output = ComposedElement<Coord, DB, A, ComposedElement<BackendCoord, DB, B, C>>;
+ fn add(self, rhs: C) -> Self::Output {
+ ComposedElement {
+ offset: self.offset,
+ first: self.first,
+ second: ComposedElement {
+ offset: (0, 0),
+ first: self.second,
+ second: rhs,
+ phantom: PhantomData,
+ },
+ phantom: PhantomData,
+ }
+ }
+}
diff --git a/src/element/dynelem.rs b/src/element/dynelem.rs
new file mode 100644
index 0000000..d32c06d
--- /dev/null
+++ b/src/element/dynelem.rs
@@ -0,0 +1,84 @@
+use super::{Drawable, PointCollection};
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+
+use std::borrow::Borrow;
+
+trait DynDrawable<DB: DrawingBackend> {
+ fn draw_dyn(
+ &self,
+ points: &mut dyn Iterator<Item = BackendCoord>,
+ backend: &mut DB,
+ parent_dim: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>>;
+}
+
+impl<DB: DrawingBackend, T: Drawable<DB>> DynDrawable<DB> for T {
+ fn draw_dyn(
+ &self,
+ points: &mut dyn Iterator<Item = BackendCoord>,
+ backend: &mut DB,
+ parent_dim: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ T::draw(self, points, backend, parent_dim)
+ }
+}
+
+/// The container for a dynamically dispatched element
+pub struct DynElement<'a, DB, Coord>
+where
+ DB: DrawingBackend,
+ Coord: Clone,
+{
+ points: Vec<Coord>,
+ drawable: Box<dyn DynDrawable<DB> + 'a>,
+}
+
+impl<'a, 'b: 'a, DB: DrawingBackend, Coord: Clone> PointCollection<'a, Coord>
+ for &'a DynElement<'b, DB, Coord>
+{
+ type Borrow = &'a Coord;
+ type IntoIter = &'a Vec<Coord>;
+ fn point_iter(self) -> Self::IntoIter {
+ &self.points
+ }
+}
+
+impl<'a, DB: DrawingBackend, Coord: Clone> Drawable<DB> for DynElement<'a, DB, Coord> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut pos: I,
+ backend: &mut DB,
+ parent_dim: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ self.drawable.draw_dyn(&mut pos, backend, parent_dim)
+ }
+}
+
+/// The trait that makes the conversion from the statically dispatched element
+/// to the dynamically dispatched element
+pub trait IntoDynElement<'a, DB: DrawingBackend, Coord: Clone>
+where
+ Self: 'a,
+{
+ /// Make the conversion
+ fn into_dyn(self) -> DynElement<'a, DB, Coord>;
+}
+
+impl<'b, T, DB, Coord> IntoDynElement<'b, DB, Coord> for T
+where
+ T: Drawable<DB> + 'b,
+ for<'a> &'a T: PointCollection<'a, Coord>,
+ Coord: Clone,
+ DB: DrawingBackend,
+{
+ fn into_dyn(self) -> DynElement<'b, DB, Coord> {
+ DynElement {
+ points: self
+ .point_iter()
+ .into_iter()
+ .map(|x| x.borrow().clone())
+ .collect(),
+ drawable: Box::new(self),
+ }
+ }
+}
diff --git a/src/element/errorbar.rs b/src/element/errorbar.rs
new file mode 100644
index 0000000..855cd72
--- /dev/null
+++ b/src/element/errorbar.rs
@@ -0,0 +1,147 @@
+use std::marker::PhantomData;
+
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use crate::element::{Drawable, PointCollection};
+use crate::style::ShapeStyle;
+
+pub trait ErrorBarOrient<K, V> {
+ type XType;
+ type YType;
+
+ fn make_coord(key: K, val: V) -> (Self::XType, Self::YType);
+ fn ending_coord(coord: BackendCoord, w: u32) -> (BackendCoord, BackendCoord);
+}
+
+pub struct ErrorBarOrientH<K, V>(PhantomData<(K, V)>);
+
+pub struct ErrorBarOrientV<K, V>(PhantomData<(K, V)>);
+
+impl<K, V> ErrorBarOrient<K, V> for ErrorBarOrientH<K, V> {
+ type XType = V;
+ type YType = K;
+
+ fn make_coord(key: K, val: V) -> (V, K) {
+ (val, key)
+ }
+
+ fn ending_coord(coord: BackendCoord, w: u32) -> (BackendCoord, BackendCoord) {
+ (
+ (coord.0, coord.1 - w as i32 / 2),
+ (coord.0, coord.1 + w as i32 / 2),
+ )
+ }
+}
+
+impl<K, V> ErrorBarOrient<K, V> for ErrorBarOrientV<K, V> {
+ type XType = K;
+ type YType = V;
+
+ fn make_coord(key: K, val: V) -> (K, V) {
+ (key, val)
+ }
+
+ fn ending_coord(coord: BackendCoord, w: u32) -> (BackendCoord, BackendCoord) {
+ (
+ (coord.0 - w as i32 / 2, coord.1),
+ (coord.0 + w as i32 / 2, coord.1),
+ )
+ }
+}
+
+pub struct ErrorBar<K, V, O: ErrorBarOrient<K, V>> {
+ style: ShapeStyle,
+ width: u32,
+ key: K,
+ values: [V; 3],
+ _p: PhantomData<O>,
+}
+
+impl<K, V> ErrorBar<K, V, ErrorBarOrientV<K, V>> {
+ pub fn new_vertical<S: Into<ShapeStyle>>(
+ key: K,
+ min: V,
+ avg: V,
+ max: V,
+ style: S,
+ width: u32,
+ ) -> Self {
+ Self {
+ style: style.into(),
+ width,
+ key,
+ values: [min, avg, max],
+ _p: PhantomData,
+ }
+ }
+}
+
+impl<K, V> ErrorBar<K, V, ErrorBarOrientH<K, V>> {
+ pub fn new_horizontal<S: Into<ShapeStyle>>(
+ key: K,
+ min: V,
+ avg: V,
+ max: V,
+ style: S,
+ width: u32,
+ ) -> Self {
+ Self {
+ style: style.into(),
+ width,
+ key,
+ values: [min, avg, max],
+ _p: PhantomData,
+ }
+ }
+}
+
+impl<'a, K: 'a + Clone, V: 'a + Clone, O: ErrorBarOrient<K, V>>
+ PointCollection<'a, (O::XType, O::YType)> for &'a ErrorBar<K, V, O>
+{
+ type Borrow = (O::XType, O::YType);
+ type IntoIter = Vec<Self::Borrow>;
+ fn point_iter(self) -> Self::IntoIter {
+ self.values
+ .iter()
+ .map(|v| O::make_coord(self.key.clone(), v.clone()))
+ .collect()
+ }
+}
+
+impl<K, V, O: ErrorBarOrient<K, V>, DB: DrawingBackend> Drawable<DB> for ErrorBar<K, V, O> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ let points: Vec<_> = points.take(3).collect();
+
+ let (from, to) = O::ending_coord(points[0], self.width);
+ backend.draw_line(from, to, &self.style)?;
+
+ let (from, to) = O::ending_coord(points[2], self.width);
+ backend.draw_line(from, to, &self.style)?;
+
+ backend.draw_line(points[0], points[2], &self.style)?;
+
+ backend.draw_circle(points[1], self.width / 2, &self.style, self.style.filled)?;
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn test_preserve_stroke_width() {
+ let v = ErrorBar::new_vertical(100, 20, 50, 70, WHITE.filled().stroke_width(5), 3);
+ let h = ErrorBar::new_horizontal(100, 20, 50, 70, WHITE.filled().stroke_width(5), 3);
+
+ use crate::prelude::*;
+ let da = crate::create_mocked_drawing_area(300, 300, |m| {
+ m.check_draw_line(|_, w, _, _| {
+ assert_eq!(w, 5);
+ });
+ });
+ da.draw(&h).expect("Drawing Failure");
+ da.draw(&v).expect("Drawing Failure");
+}
diff --git a/src/element/image.rs b/src/element/image.rs
new file mode 100644
index 0000000..12f3f30
--- /dev/null
+++ b/src/element/image.rs
@@ -0,0 +1,213 @@
+#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+use image::{DynamicImage, GenericImageView};
+
+use super::{Drawable, PointCollection};
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use crate::drawing::bitmap_pixel::{PixelFormat, RGBPixel};
+
+use crate::drawing::BitMapBackend;
+use std::borrow::Borrow;
+use std::marker::PhantomData;
+
+enum Buffer<'a> {
+ Owned(Vec<u8>),
+ Borrowed(&'a [u8]),
+ BorrowedMut(&'a mut [u8]),
+}
+
+impl<'a> Borrow<[u8]> for Buffer<'a> {
+ fn borrow(&self) -> &[u8] {
+ self.as_ref()
+ }
+}
+
+impl AsRef<[u8]> for Buffer<'_> {
+ fn as_ref(&self) -> &[u8] {
+ match self {
+ Buffer::Owned(owned) => owned.as_ref(),
+ Buffer::Borrowed(target) => target,
+ Buffer::BorrowedMut(target) => target,
+ }
+ }
+}
+
+impl<'a> Buffer<'a> {
+ fn to_mut(&mut self) -> &mut [u8] {
+ let owned = match self {
+ Buffer::Owned(owned) => return &mut owned[..],
+ Buffer::BorrowedMut(target) => return target,
+ Buffer::Borrowed(target) => {
+ let mut value = vec![];
+ value.extend_from_slice(target);
+ value
+ }
+ };
+
+ *self = Buffer::Owned(owned);
+ self.to_mut()
+ }
+}
+
+/// The element that contains a bitmap on it
+pub struct BitMapElement<'a, Coord, P: PixelFormat = RGBPixel> {
+ image: Buffer<'a>,
+ size: (u32, u32),
+ pos: Coord,
+ phantom: PhantomData<P>,
+}
+
+impl<'a, Coord, P: PixelFormat> BitMapElement<'a, Coord, P> {
+ /// Create a new empty bitmap element. This can be use as
+ /// the draw and blit pattern.
+ ///
+ /// - `pos`: The left upper coordinate for the element
+ /// - `size`: The size of the bitmap
+ pub fn new(pos: Coord, size: (u32, u32)) -> Self {
+ Self {
+ image: Buffer::Owned(vec![0; (size.0 * size.1) as usize * P::PIXEL_SIZE]),
+ size,
+ pos,
+ phantom: PhantomData,
+ }
+ }
+
+ /// Create a new bitmap element with an pre-allocated owned buffer, this function will
+ /// take the ownership of the buffer.
+ ///
+ /// - `pos`: The left upper coordinate of the elelent
+ /// - `size`: The size of the bitmap
+ /// - `buf`: The buffer to use
+ /// - **returns**: The newly created image element, if the buffer isn't fit the image
+ /// dimension, this will returns an `None`.
+ pub fn with_owned_buffer(pos: Coord, size: (u32, u32), buf: Vec<u8>) -> Option<Self> {
+ if buf.len() < (size.0 * size.1) as usize * P::PIXEL_SIZE {
+ return None;
+ }
+
+ Some(Self {
+ image: Buffer::Owned(buf),
+ size,
+ pos,
+ phantom: PhantomData,
+ })
+ }
+
+ /// Create a new bitmap element with a mut borrow to an existing buffer
+ ///
+ /// - `pos`: The left upper coordinate of the elelent
+ /// - `size`: The size of the bitmap
+ /// - `buf`: The buffer to use
+ /// - **returns**: The newly created image element, if the buffer isn't fit the image
+ /// dimension, this will returns an `None`.
+ pub fn with_mut(pos: Coord, size: (u32, u32), buf: &'a mut [u8]) -> Option<Self> {
+ if buf.len() < (size.0 * size.1) as usize * P::PIXEL_SIZE {
+ return None;
+ }
+
+ Some(Self {
+ image: Buffer::BorrowedMut(buf),
+ size,
+ pos,
+ phantom: PhantomData,
+ })
+ }
+
+ /// Create a new bitmap element with a shared borrowed buffer. This means if we want to modifiy
+ /// the content of the image, the buffer is automatically copied
+ ///
+ /// - `pos`: The left upper coordinate of the elelent
+ /// - `size`: The size of the bitmap
+ /// - `buf`: The buffer to use
+ /// - **returns**: The newly created image element, if the buffer isn't fit the image
+ /// dimension, this will returns an `None`.
+ pub fn with_ref(pos: Coord, size: (u32, u32), buf: &'a [u8]) -> Option<Self> {
+ if buf.len() < (size.0 * size.1) as usize * P::PIXEL_SIZE {
+ return None;
+ }
+
+ Some(Self {
+ image: Buffer::Borrowed(buf),
+ size,
+ pos,
+ phantom: PhantomData,
+ })
+ }
+
+ /// Copy the existing bitmap element to another location
+ ///
+ /// - `pos`: The new location to copy
+ pub fn copy_to<Coord2>(&self, pos: Coord2) -> BitMapElement<Coord2, P> {
+ BitMapElement {
+ image: Buffer::Borrowed(self.image.borrow()),
+ size: self.size,
+ pos,
+ phantom: PhantomData,
+ }
+ }
+
+ /// Move the existing bitmap element to a new position
+ ///
+ /// - `pos`: The new position
+ pub fn move_to(&mut self, pos: Coord) {
+ self.pos = pos;
+ }
+
+ /// Make the bitmap element as a bitmap backend, so that we can use
+ /// plotters drawing functionality on the bitmap element
+ pub fn as_bitmap_backend(&mut self) -> BitMapBackend<P> {
+ BitMapBackend::with_buffer_and_format(self.image.to_mut(), self.size).unwrap()
+ }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+impl<'a, Coord> From<(Coord, DynamicImage)> for BitMapElement<'a, Coord, RGBPixel> {
+ fn from((pos, image): (Coord, DynamicImage)) -> Self {
+ let (w, h) = image.dimensions();
+ let rgb_image = image.to_rgb().into_raw();
+ Self {
+ pos,
+ image: Buffer::Owned(rgb_image),
+ size: (w, h),
+ phantom: PhantomData,
+ }
+ }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "image"))]
+impl<'a, Coord> From<(Coord, DynamicImage)>
+ for BitMapElement<'a, Coord, crate::drawing::bitmap_pixel::BGRXPixel>
+{
+ fn from((pos, image): (Coord, DynamicImage)) -> Self {
+ let (w, h) = image.dimensions();
+ let rgb_image = image.to_bgra().into_raw();
+ Self {
+ pos,
+ image: Buffer::Owned(rgb_image),
+ size: (w, h),
+ phantom: PhantomData,
+ }
+ }
+}
+
+impl<'a, 'b, Coord> PointCollection<'a, Coord> for &'a BitMapElement<'b, Coord> {
+ type Borrow = &'a Coord;
+ type IntoIter = std::iter::Once<&'a Coord>;
+ fn point_iter(self) -> Self::IntoIter {
+ std::iter::once(&self.pos)
+ }
+}
+
+impl<'a, Coord, DB: DrawingBackend> Drawable<DB> for BitMapElement<'a, Coord> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some((x, y)) = points.next() {
+ // TODO: convert the pixel format when needed
+ return backend.blit_bitmap((x, y), self.size, self.image.as_ref());
+ }
+ Ok(())
+ }
+}
diff --git a/src/element/mod.rs b/src/element/mod.rs
new file mode 100644
index 0000000..99b7a62
--- /dev/null
+++ b/src/element/mod.rs
@@ -0,0 +1,218 @@
+/*!
+ Defines the drawing elements, the high-level drawing unit in Plotters drawing system
+
+ ## Introduction
+ An element is the drawing unit for Plotter's high-level drawing API.
+ Different from low-level drawing API, an element is a logic unit of component in the image.
+ There are few built-in elements, including `Circle`, `Pixel`, `Rectangle`, `Path`, `Text`, etc.
+
+ All element can be drawn onto the drawing area using API `DrawingArea::draw(...)`.
+ Plotters use "iterator of elements" as the abstraction of any type of plot.
+
+ ## Implementing your own element
+ You can also define your own element, `CandleStick` is a good sample of implementing complex
+ element. There are two trait required for an element:
+
+ - `PointCollection` - the struct should be able to return an iterator of key-points under guest coordinate
+ - `Drawable` - the struct is a pending drawing operation on a drawing backend with pixel-based coordinate
+
+ An example of element that draws a red "X" in a red rectangle onto the backend:
+
+ ```rust
+ use std::iter::{Once, once};
+ use plotters::element::{PointCollection, Drawable};
+ use plotters::drawing::backend::{BackendCoord, DrawingErrorKind};
+ use plotters::style::IntoTextStyle;
+ use plotters::prelude::*;
+
+ // Any example drawing a red X
+ struct RedBoxedX((i32, i32));
+
+ // For any reference to RedX, we can convert it into an iterator of points
+ impl <'a> PointCollection<'a, (i32, i32)> for &'a RedBoxedX {
+ type Borrow = &'a (i32, i32);
+ type IntoIter = Once<&'a (i32, i32)>;
+ fn point_iter(self) -> Self::IntoIter {
+ once(&self.0)
+ }
+ }
+
+ // How to actually draw this element
+ impl <DB:DrawingBackend> Drawable<DB> for RedBoxedX {
+ fn draw<I:Iterator<Item = BackendCoord>>(
+ &self,
+ mut pos: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ let pos = pos.next().unwrap();
+ let color = RED.to_rgba();
+ backend.draw_rect(pos, (pos.0 + 10, pos.1 + 12), &color, false)?;
+ let text_style = &("sans-serif", 20).into_text_style(backend).color(&color);
+ backend.draw_text("X", &text_style, pos)
+ }
+ }
+
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new(
+ "plotters-doc-data/element-0.png",
+ (640, 480)
+ ).into_drawing_area();
+ root.draw(&RedBoxedX((200, 200)))?;
+ Ok(())
+ }
+ ```
+ ![](https://plotters-rs.github.io/plotters-doc-data/element-0.png)
+
+ ## Composable Elements
+ You also have an convenient way to build an element that isn't built into the Plotters library by
+ combining existing elements into a logic group. To build an composable element, you need to use an
+ logic empty element that draws nothing to the backend but denotes the relative zero point of the logical
+ group. Any element defined with pixel based offset coordinate can be added into the group later using
+ the `+` operator.
+
+ For example, the red boxed X element can be implemented with Composable element in the following way:
+ ```rust
+ use plotters::prelude::*;
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new(
+ "plotters-doc-data/element-1.png",
+ (640, 480)
+ ).into_drawing_area();
+ let font:FontDesc = ("sans-serif", 20).into();
+ root.draw(&(EmptyElement::at((200, 200))
+ + Text::new("X", (0, 0), &"sans-serif".into_font().resize(20.0).color(&RED))
+ + Rectangle::new([(0,0), (10, 12)], &RED)
+ ))?;
+ Ok(())
+ }
+ ```
+ ![](https://plotters-rs.github.io/plotters-doc-data/element-1.png)
+
+ ## Dynamic Elements
+ By default, Plotters uses static dispatch for all the elements and series. For example,
+ the `ChartContext::draw_series` method accepts an iterator of `T` where type `T` implements
+ all the traits a element should implement. Although, we can use the series of composable element
+ for complex series drawing. But sometimes, we still want to make the series heterogynous, which means
+ the iterator should be able to holds elements in different type.
+ For example, a point series with cross and circle. This requires the dynamically dispatched elements.
+ In plotters, all the elements can be converted into `DynElement`, the dynamic dispatch container for
+ all elements (include external implemented ones).
+ Plotters automatically implements `IntoDynElement` for all elements, by doing so, any dynamic element should have
+ `into_dyn` function which would wrap the element into a dynamic element wrapper.
+
+ For example, the following code counts the number of factors of integer and mark all prime numbers in cross.
+ ```rust
+ use plotters::prelude::*;
+ fn num_of_factor(n: i32) -> i32 {
+ let mut ret = 2;
+ for i in 2..n {
+ if i * i > n {
+ break;
+ }
+
+ if n % i == 0 {
+ if i * i != n {
+ ret += 2;
+ } else {
+ ret += 1;
+ }
+ }
+ }
+ return ret;
+ }
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root =
+ BitMapBackend::new("plotters-doc-data/element-3.png", (640, 480))
+ .into_drawing_area();
+ root.fill(&WHITE)?;
+ let mut chart = ChartBuilder::on(&root)
+ .x_label_area_size(40)
+ .y_label_area_size(40)
+ .margin(5)
+ .build_ranged(0..50, 0..10)?;
+
+ chart
+ .configure_mesh()
+ .disable_x_mesh()
+ .disable_y_mesh()
+ .draw()?;
+
+ chart.draw_series((0..50).map(|x| {
+ let center = (x, num_of_factor(x));
+ // Although the arms of if statement has different types,
+ // but they can be placed into a dynamic element wrapper,
+ // by doing so, the type is unified.
+ if center.1 == 2 {
+ Cross::new(center, 4, Into::<ShapeStyle>::into(&RED).filled()).into_dyn()
+ } else {
+ Circle::new(center, 4, Into::<ShapeStyle>::into(&GREEN).filled()).into_dyn()
+ }
+ }))?;
+
+ Ok(())
+ }
+ ```
+ ![](https://plotters-rs.github.io/plotters-doc-data/element-3.png)
+*/
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use std::borrow::Borrow;
+
+mod basic_shapes;
+pub use basic_shapes::*;
+
+mod text;
+pub use text::*;
+
+mod points;
+pub use points::*;
+
+mod composable;
+pub use composable::{ComposedElement, EmptyElement};
+
+#[cfg(feature = "candlestick")]
+mod candlestick;
+#[cfg(feature = "candlestick")]
+pub use candlestick::CandleStick;
+
+#[cfg(feature = "errorbar")]
+mod errorbar;
+#[cfg(feature = "errorbar")]
+pub use errorbar::{ErrorBar, ErrorBarOrientH, ErrorBarOrientV};
+
+#[cfg(feature = "boxplot")]
+mod boxplot;
+#[cfg(feature = "boxplot")]
+pub use boxplot::Boxplot;
+
+#[cfg(feature = "bitmap")]
+mod image;
+#[cfg(feature = "bitmap")]
+pub use self::image::BitMapElement;
+
+mod dynelem;
+pub use dynelem::{DynElement, IntoDynElement};
+
+/// A type which is logically a collection of points, under any given coordinate system
+pub trait PointCollection<'a, Coord> {
+ /// The item in point iterator
+ type Borrow: Borrow<Coord>;
+
+ /// The point iterator
+ type IntoIter: IntoIterator<Item = Self::Borrow>;
+
+ /// framework to do the coordinate mapping
+ fn point_iter(self) -> Self::IntoIter;
+}
+
+/// The trait indicates we are able to draw it on a drawing area
+pub trait Drawable<DB: DrawingBackend> {
+ /// Actually draws the element. The key points is already translated into the
+ /// image coordinate and can be used by DC directly
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ pos: I,
+ backend: &mut DB,
+ parent_dim: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>>;
+}
diff --git a/src/element/points.rs b/src/element/points.rs
new file mode 100644
index 0000000..aa07de3
--- /dev/null
+++ b/src/element/points.rs
@@ -0,0 +1,125 @@
+use super::*;
+use super::{Drawable, PointCollection};
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use crate::style::{ShapeStyle, SizeDesc};
+
+/// The element that used to describe a point
+pub trait PointElement<Coord, Size: SizeDesc> {
+ fn make_point(pos: Coord, size: Size, style: ShapeStyle) -> Self;
+}
+
+/// Describe a cross
+pub struct Cross<Coord, Size: SizeDesc> {
+ center: Coord,
+ size: Size,
+ style: ShapeStyle,
+}
+
+impl<Coord, Size: SizeDesc> Cross<Coord, Size> {
+ pub fn new<T: Into<ShapeStyle>>(coord: Coord, size: Size, style: T) -> Self {
+ Self {
+ center: coord,
+ size,
+ style: style.into(),
+ }
+ }
+}
+
+impl<'a, Coord: 'a, Size: SizeDesc> PointCollection<'a, Coord> for &'a Cross<Coord, Size> {
+ type Borrow = &'a Coord;
+ type IntoIter = std::iter::Once<&'a Coord>;
+ fn point_iter(self) -> std::iter::Once<&'a Coord> {
+ std::iter::once(&self.center)
+ }
+}
+
+impl<Coord, DB: DrawingBackend, Size: SizeDesc> Drawable<DB> for Cross<Coord, Size> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut points: I,
+ backend: &mut DB,
+ ps: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some((x, y)) = points.next() {
+ let size = self.size.in_pixels(&ps);
+ let (x0, y0) = (x - size, y - size);
+ let (x1, y1) = (x + size, y + size);
+ backend.draw_line((x0, y0), (x1, y1), &self.style)?;
+ backend.draw_line((x0, y1), (x1, y0), &self.style)?;
+ }
+ Ok(())
+ }
+}
+
+/// Describe a triangle marker
+pub struct TriangleMarker<Coord, Size: SizeDesc> {
+ center: Coord,
+ size: Size,
+ style: ShapeStyle,
+}
+
+impl<Coord, Size: SizeDesc> TriangleMarker<Coord, Size> {
+ pub fn new<T: Into<ShapeStyle>>(coord: Coord, size: Size, style: T) -> Self {
+ Self {
+ center: coord,
+ size,
+ style: style.into(),
+ }
+ }
+}
+
+impl<'a, Coord: 'a, Size: SizeDesc> PointCollection<'a, Coord> for &'a TriangleMarker<Coord, Size> {
+ type Borrow = &'a Coord;
+ type IntoIter = std::iter::Once<&'a Coord>;
+ fn point_iter(self) -> std::iter::Once<&'a Coord> {
+ std::iter::once(&self.center)
+ }
+}
+
+impl<Coord, DB: DrawingBackend, Size: SizeDesc> Drawable<DB> for TriangleMarker<Coord, Size> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut points: I,
+ backend: &mut DB,
+ ps: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some((x, y)) = points.next() {
+ let size = self.size.in_pixels(&ps);
+ let points = [-90, -210, -330]
+ .iter()
+ .map(|deg| f64::from(*deg) * std::f64::consts::PI / 180.0)
+ .map(|rad| {
+ (
+ (rad.cos() * f64::from(size) + f64::from(x)).ceil() as i32,
+ (rad.sin() * f64::from(size) + f64::from(y)).ceil() as i32,
+ )
+ });
+ backend.fill_polygon(points, &self.style.color)?;
+ }
+ Ok(())
+ }
+}
+
+impl<Coord, Size: SizeDesc> PointElement<Coord, Size> for Cross<Coord, Size> {
+ fn make_point(pos: Coord, size: Size, style: ShapeStyle) -> Self {
+ Self::new(pos, size, style)
+ }
+}
+
+impl<Coord, Size: SizeDesc> PointElement<Coord, Size> for TriangleMarker<Coord, Size> {
+ fn make_point(pos: Coord, size: Size, style: ShapeStyle) -> Self {
+ Self::new(pos, size, style)
+ }
+}
+
+impl<Coord, Size: SizeDesc> PointElement<Coord, Size> for Circle<Coord, Size> {
+ fn make_point(pos: Coord, size: Size, style: ShapeStyle) -> Self {
+ Self::new(pos, size, style)
+ }
+}
+
+impl<Coord, Size: SizeDesc> PointElement<Coord, Size> for Pixel<Coord> {
+ fn make_point(pos: Coord, _: Size, style: ShapeStyle) -> Self {
+ Self::new(pos, style)
+ }
+}
diff --git a/src/element/text.rs b/src/element/text.rs
new file mode 100644
index 0000000..3acaac3
--- /dev/null
+++ b/src/element/text.rs
@@ -0,0 +1,242 @@
+use std::borrow::Borrow;
+use std::i32;
+
+use super::{Drawable, PointCollection};
+use crate::drawing::backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
+use crate::style::{FontDesc, FontResult, LayoutBox, TextStyle};
+
+/// A single line text element. This can be owned or borrowed string, dependents on
+/// `String` or `str` moved into.
+pub struct Text<'a, Coord, T: Borrow<str>> {
+ text: T,
+ coord: Coord,
+ style: TextStyle<'a>,
+}
+
+impl<'a, Coord, T: Borrow<str>> Text<'a, Coord, T> {
+ /// Create a new text element
+ /// - `text`: The text for the element
+ /// - `points`: The upper left conner for the text element
+ /// - `style`: The text style
+ /// - Return the newly created text element
+ pub fn new<S: Into<TextStyle<'a>>>(text: T, points: Coord, style: S) -> Self {
+ Self {
+ text,
+ coord: points,
+ style: style.into(),
+ }
+ }
+}
+
+impl<'b, 'a, Coord: 'a, T: Borrow<str> + 'a> PointCollection<'a, Coord> for &'a Text<'b, Coord, T> {
+ type Borrow = &'a Coord;
+ type IntoIter = std::iter::Once<&'a Coord>;
+ fn point_iter(self) -> Self::IntoIter {
+ std::iter::once(&self.coord)
+ }
+}
+
+impl<'a, Coord: 'a, DB: DrawingBackend, T: Borrow<str>> Drawable<DB> for Text<'a, Coord, T> {
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some(a) = points.next() {
+ return backend.draw_text(self.text.borrow(), &self.style, a);
+ }
+ Ok(())
+ }
+}
+
+/// An multi-line text element. The `Text` element allows only single line text
+/// and the `MultiLineText` supports drawing multiple lines
+pub struct MultiLineText<'a, Coord, T: Borrow<str>> {
+ lines: Vec<T>,
+ coord: Coord,
+ style: TextStyle<'a>,
+ line_height: f64,
+}
+
+impl<'a, Coord, T: Borrow<str>> MultiLineText<'a, Coord, T> {
+ /// Create an empty multi-line text element.
+ /// Lines can be append to the empty multi-line by calling `push_line` method
+ ///
+ /// `pos`: The upper left corner
+ /// `style`: The style of the text
+ pub fn new<S: Into<TextStyle<'a>>>(pos: Coord, style: S) -> Self {
+ MultiLineText {
+ lines: vec![],
+ coord: pos,
+ style: style.into(),
+ line_height: 1.25,
+ }
+ }
+
+ /// Set the line height of the multi-line text element
+ pub fn set_line_height(&mut self, value: f64) -> &mut Self {
+ self.line_height = value;
+ self
+ }
+
+ /// Push a new line into the given multi-line text
+ /// `line`: The line to be pushed
+ pub fn push_line<L: Into<T>>(&mut self, line: L) {
+ self.lines.push(line.into());
+ }
+
+ /// Estimate the multi-line text element's dimension
+ pub fn estimate_dimension(&self) -> FontResult<(i32, i32)> {
+ let (mut mx, mut my) = (0, 0);
+
+ for ((x, y), t) in self.layout_lines((0, 0)).zip(self.lines.iter()) {
+ let (dx, dy) = self.style.font.box_size(t.borrow())?;
+ mx = mx.max(x + dx as i32);
+ my = my.max(y + dy as i32);
+ }
+
+ Ok((mx, my))
+ }
+
+ /// Move the location to the specified location
+ pub fn relocate(&mut self, coord: Coord) {
+ self.coord = coord
+ }
+
+ fn layout_lines(&self, (x0, y0): BackendCoord) -> impl Iterator<Item = BackendCoord> {
+ let font_height = self.style.font.get_size();
+ let actual_line_height = font_height * self.line_height;
+ (0..self.lines.len() as u32).map(move |idx| {
+ let y = f64::from(y0) + f64::from(idx) * actual_line_height;
+ // TODO: Support text alignment as well, currently everything is left aligned
+ let x = f64::from(x0);
+ (x.round() as i32, y.round() as i32)
+ })
+ }
+}
+
+fn layout_multiline_text<'a, F: FnMut(&'a str)>(
+ text: &'a str,
+ max_width: u32,
+ font: FontDesc<'a>,
+ mut func: F,
+) {
+ for line in text.lines() {
+ if max_width == 0 || line.is_empty() {
+ func(line);
+ } else {
+ let mut remaining = &line[0..];
+
+ while !remaining.is_empty() {
+ let mut left = 0;
+ while left < remaining.len() {
+ let width = font.box_size(&remaining[0..=left]).unwrap_or((0, 0)).0 as i32;
+
+ if width > max_width as i32 {
+ break;
+ }
+ left += 1;
+ }
+
+ if left == 0 {
+ left += 1;
+ }
+
+ let cur_line = &remaining[..left];
+ remaining = &remaining[left..];
+
+ func(cur_line);
+ }
+ }
+ }
+}
+
+impl<'a, T: Borrow<str>> MultiLineText<'a, BackendCoord, T> {
+ /// Compute the line layout
+ pub fn compute_line_layout(&self) -> FontResult<Vec<LayoutBox>> {
+ let mut ret = vec![];
+ for ((x, y), t) in self.layout_lines(self.coord).zip(self.lines.iter()) {
+ let (dx, dy) = self.style.font.box_size(t.borrow())?;
+ ret.push(((x, y), (x + dx as i32, y + dy as i32)));
+ }
+ Ok(ret)
+ }
+}
+
+impl<'a, Coord> MultiLineText<'a, Coord, &'a str> {
+ /// Parse a multi-line text into an multi-line element.
+ ///
+ /// `text`: The text that is parsed
+ /// `pos`: The position of the text
+ /// `style`: The style for this text
+ /// `max_width`: The width of the multi-line text element, the line will break
+ /// into two lines if the line is wider than the max_width. If 0 is given, do not
+ /// do any line wrapping
+ pub fn from_str<ST: Into<&'a str>, S: Into<TextStyle<'a>>>(
+ text: ST,
+ pos: Coord,
+ style: S,
+ max_width: u32,
+ ) -> Self {
+ let text = text.into();
+ let mut ret = MultiLineText::new(pos, style);
+
+ layout_multiline_text(text, max_width, ret.style.font.clone(), |l| {
+ ret.push_line(l)
+ });
+ ret
+ }
+}
+
+impl<'a, Coord> MultiLineText<'a, Coord, String> {
+ /// Parse a multi-line text into an multi-line element.
+ ///
+ /// `text`: The text that is parsed
+ /// `pos`: The position of the text
+ /// `style`: The style for this text
+ /// `max_width`: The width of the multi-line text element, the line will break
+ /// into two lines if the line is wider than the max_width. If 0 is given, do not
+ /// do any line wrapping
+ pub fn from_string<S: Into<TextStyle<'a>>>(
+ text: String,
+ pos: Coord,
+ style: S,
+ max_width: u32,
+ ) -> Self {
+ let mut ret = MultiLineText::new(pos, style);
+
+ layout_multiline_text(text.as_str(), max_width, ret.style.font.clone(), |l| {
+ ret.push_line(l.to_string())
+ });
+ ret
+ }
+}
+
+impl<'b, 'a, Coord: 'a, T: Borrow<str> + 'a> PointCollection<'a, Coord>
+ for &'a MultiLineText<'b, Coord, T>
+{
+ type Borrow = &'a Coord;
+ type IntoIter = std::iter::Once<&'a Coord>;
+ fn point_iter(self) -> Self::IntoIter {
+ std::iter::once(&self.coord)
+ }
+}
+
+impl<'a, Coord: 'a, DB: DrawingBackend, T: Borrow<str>> Drawable<DB>
+ for MultiLineText<'a, Coord, T>
+{
+ fn draw<I: Iterator<Item = BackendCoord>>(
+ &self,
+ mut points: I,
+ backend: &mut DB,
+ _: (u32, u32),
+ ) -> Result<(), DrawingErrorKind<DB::ErrorType>> {
+ if let Some(a) = points.next() {
+ for (point, text) in self.layout_lines(a).zip(self.lines.iter()) {
+ backend.draw_text(text.borrow(), &self.style, point)?;
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/src/evcxr.rs b/src/evcxr.rs
new file mode 100644
index 0000000..1a17077
--- /dev/null
+++ b/src/evcxr.rs
@@ -0,0 +1,40 @@
+use crate::coord::Shift;
+use crate::drawing::{DrawingArea, IntoDrawingArea, SVGBackend};
+
+/// The wrapper for the generated SVG
+pub struct SVGWrapper(String, String);
+
+impl SVGWrapper {
+ pub fn evcxr_display(&self) {
+ println!("{:?}", self);
+ }
+
+ pub fn style<S: Into<String>>(mut self, style: S) -> Self {
+ self.1 = style.into();
+ self
+ }
+}
+
+impl std::fmt::Debug for SVGWrapper {
+ fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ let svg = self.0.as_str();
+ write!(
+ formatter,
+ "EVCXR_BEGIN_CONTENT text/html\n<div style=\"{}\">{}</div>\nEVCXR_END_CONTENT",
+ self.1, svg
+ )
+ }
+}
+
+/// Start drawing an evcxr figure
+pub fn evcxr_figure<
+ Draw: FnOnce(DrawingArea<SVGBackend, Shift>) -> Result<(), Box<dyn std::error::Error>>,
+>(
+ size: (u32, u32),
+ draw: Draw,
+) -> SVGWrapper {
+ let mut buffer = "".to_string();
+ let root = SVGBackend::with_string(&mut buffer, size).into_drawing_area();
+ draw(root).expect("Drawing failure");
+ SVGWrapper(buffer, "".to_string())
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..06c6d6c
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,764 @@
+/*!
+
+# Plotters - A Rust drawing library focus on data plotting for both WASM and native applications 🦀📈🚀
+
+<a href="https://crates.io/crates/plotters">
+ <img style="display: inline!important" src="https://img.shields.io/crates/v/plotters.svg"></img>
+</a>
+<a href="https://docs.rs/plotters">
+ <img style="display: inline!important" src="https://docs.rs/plotters/badge.svg"></img>
+</a>
+<a href="https://plotters-rs.github.io/rustdoc/plotters/">
+ <img style="display: inline! important" src="https://img.shields.io/badge/docs-development-lightgrey.svg"></img>
+</a>
+<a href="https://travis-ci.org/38/plotters">
+ <img style="display: inline! important" src="https://travis-ci.org/38/plotters.svg?branch=master"></img>
+</a>
+<a href="https://codecov.io/gh/38/plotters">
+ <img style="display: inline! important" src="https://codecov.io/gh/38/plotters/branch/master/graph/badge.svg" />
+</a>
+
+Plotters is drawing library designed for rendering figures, plots, and charts, in pure rust. Plotters supports various types of back-ends,
+including bitmap, vector graph, piston window, GTK/Cairo and WebAssembly.
+
+- A new Plotters Developer's Guide is working in progress. The preview version is available at [here](https://plotters-rs.github.io/book).
+- To try Plotters with interactive Jupyter notebook, or view [here](https://plotters-rs.github.io/plotters-doc-data/evcxr-jupyter-integration.html) for the static HTML version.
+- To view the WASM example, go to this [link](https://plumberserver.com/plotters-wasm-demo/index.html)
+- Currently we have all the internal code ready for console plotting, but a console based backend is still not ready. See [this example](https://github.com/38/plotters/blob/master/examples/console.rs) for how to plotting on Console with a customized backend.
+
+## Gallery
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/sample.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/sample.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Multiple Plot
+ <a href="https://github.com/38/plotters/blob/master/examples/chart.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/stock.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/stock.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Candlestick Plot
+ <a href="https://github.com/38/plotters/blob/master/examples/stock.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/histogram.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/histogram.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Histogram
+ <a href="https://github.com/38/plotters/blob/master/examples/histogram.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/0.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/0.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Simple Chart
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/console-2.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/console-2.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Plotting the Console
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/mandelbrot.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/mandelbrot.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Mandelbrot set
+ <a href="https://github.com/38/plotters/blob/master/examples/mandelbrot.rs">[code]</a>
+ </div>
+</div>
+
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/evcxr_animation.gif">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/evcxr_animation.gif" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Jupyter Support
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/plotters-piston.gif">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/plotters-piston.gif" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Real-time Rendering
+ <a href="https://github.com/38/plotters/tree/master/examples/piston-demo">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/normal-dist.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/normal-dist.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Histogram with Scatter
+ <a href="https://github.com/38/plotters/blob/master/examples/normal-dist.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/twoscale.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/twoscale.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Dual Y-Axis Example
+ <a href="https://github.com/38/plotters/blob/master/examples/two-scales.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/matshow.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/matshow.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ The Matplotlib Matshow Example
+ <a href="https://github.com/38/plotters/blob/master/examples/matshow.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/sierpinski.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/sierpinski.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ The Sierpinski Carpet
+ <a href="https://github.com/38/plotters/blob/master/examples/sierpinski.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/normal-dist2.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/normal-dist2.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ The 1D Gaussian Distribution
+ <a href="https://github.com/38/plotters/blob/master/examples/nomal-dist2.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/errorbar.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/errorbar.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ The 1D Gaussian Distribution
+ <a href="https://github.com/38/plotters/blob/master/examples/errorbar.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/slc-temp.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/slc-temp.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Monthly Time Coordinate
+ <a href="https://github.com/38/plotters/blob/master/examples/slc-temp.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/area-chart.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/area-chart.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Monthly Time Coordinate
+ <a href="https://github.com/38/plotters/blob/master/examples/area-chart.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/snowflake.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/snowflake.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Koch Snowflake
+ <a href="https://github.com/38/plotters/blob/master/examples/snowflake.rs">[code]</a>
+ </div>
+</div>
+
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/animation.gif">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/animation.gif" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Koch Snowflake Animation
+ <a href="https://github.com/38/plotters/blob/master/examples/animation.rs">[code]</a>
+ </div>
+</div>
+
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/console.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/console.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Drawing on a Console
+ <a href="https://github.com/38/plotters/blob/master/examples/console.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/blit-bitmap.png">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/blit-bitmap.png" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Drawing bitmap on chart
+ <a href="https://github.com/38/plotters/blob/master/examples/blit-bitmap.rs">[code]</a>
+ </div>
+</div>
+
+<div class="galleryItem">
+ <a href="https://plotters-rs.github.io/plotters-doc-data/boxplot.svg">
+ <img src="https://plotters-rs.github.io/plotters-doc-data/boxplot.svg" class="galleryItem"></img>
+ </a>
+ <div class="galleryText">
+ Drawing bitmap on chart
+ <a href="https://github.com/38/plotters/blob/master/examples/boxplot.rs">[code]</a>
+ </div>
+</div>
+
+
+## Table of Contents
+ * [Gallery](#gallery)
+ * [Quick Start](#quick-start)
+ * [Trying with Jupyter evcxr Kernel Interactively](#trying-with-jupyter-evcxr-kernel-interactively)
+ * [Interactive Tutorial with Jupyter Notebook](#interactive-tutorial-with-jupyter-notebook)
+ * [Plotting in Rust](#plotting-in-rust)
+ * [Plotting on HTML5 canvas with WASM Backend](#plotting-on-html5-canvas-with-wasm-backend)
+ * [What types of figure are supported?](#what-types-of-figure-are-supported)
+ * [Concepts by examples](#concepts-by-examples)
+ + [Drawing Back-ends](#drawing-backends)
+ + [Drawing Area](#drawing-area)
+ + [Elements](#elements)
+ + [Composable Elements](#composable-elements)
+ + [Chart Context](#chart-context)
+ * [Misc](#misc)
+ + [Development Version](#development-version)
+ + [Reducing Depending Libraries && Turning Off Backends](#reducing-depending-libraries--turning-off-backends)
+ + [List of Features](#list-of-features)
+ * [FAQ List](#faq-list)
+
+## Quick Start
+
+To use Plotters, you can simply add Plotters into your `Cargo.toml`
+```toml
+[dependencies]
+plotters = "^0.2.15"
+```
+
+And the following code draws a quadratic function. `src/main.rs`,
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new("plotters-doc-data/0.png", (640, 480)).into_drawing_area();
+ root.fill(&WHITE)?;
+ let mut chart = ChartBuilder::on(&root)
+ .caption("y=x^2", ("sans-serif", 50).into_font())
+ .margin(5)
+ .x_label_area_size(30)
+ .y_label_area_size(30)
+ .build_ranged(-1f32..1f32, -0.1f32..1f32)?;
+
+ chart.configure_mesh().draw()?;
+
+ chart
+ .draw_series(LineSeries::new(
+ (-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
+ &RED,
+ ))?
+ .label("y = x^2")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));
+
+ chart
+ .configure_series_labels()
+ .background_style(&WHITE.mix(0.8))
+ .border_style(&BLACK)
+ .draw()?;
+
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/0.png)
+
+
+## Trying with Jupyter evcxr Kernel Interactively
+
+Plotters now supports integrate with `evcxr` and is able to interactively drawing plots in Jupyter Notebook.
+The feature `evcxr` should be enabled when including Plotters to Jupyter Notebook.
+
+The following code shows a minimal example of this.
+
+```text
+:dep plotters = { git = "https://github.com/38/plotters", default_features = false, features = ["evcxr"] }
+extern crate plotters;
+use plotters::prelude::*;
+
+let figure = evcxr_figure((640, 480), |root| {
+ root.fill(&WHITE);
+ let mut chart = ChartBuilder::on(&root)
+ .caption("y=x^2", ("Arial", 50).into_font())
+ .margin(5)
+ .x_label_area_size(30)
+ .y_label_area_size(30)
+ .build_ranged(-1f32..1f32, -0.1f32..1f32)?;
+
+ chart.configure_mesh().draw()?;
+
+ chart.draw_series(LineSeries::new(
+ (-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
+ &RED,
+ )).unwrap()
+ .label("y = x^2")
+ .legend(|(x,y)| PathElement::new(vec![(x,y), (x + 20,y)], &RED));
+
+ chart.configure_series_labels()
+ .background_style(&WHITE.mix(0.8))
+ .border_style(&BLACK)
+ .draw()?;
+ Ok(())
+});
+figure
+```
+
+<img src="https://plotters-rs.github.io/plotters-doc-data/evcxr_animation.gif" width="450px"></img>
+
+## Interactive Tutorial with Jupyter Notebook
+
+*This tutorial is now working in progress and isn't complete*
+
+Thanks to the evcxr, now we have an interactive tutorial for Plotters!
+To use the interactive notebook, you must have Jupyter and evcxr installed on your computer.
+Follow the instruction on [this page](https://github.com/google/evcxr/tree/master/evcxr_jupyter) below to install it.
+
+After that, you should be able to start your Jupyter server locally and load the tutorial!
+
+```bash
+git clone https://github.com/38/plotters-doc-data
+cd plotteres-doc-data
+jupyter notebook
+```
+
+And select the notebook called `evcxr-jupyter-integration.ipynb`.
+
+Also, there's a static HTML version of this notebook available at the [this location](https://plumberserver.com/plotters-docs/evcxr-jupyter-integration.html)
+
+## Plotting in Rust
+
+Rust is a perfect language for data visualization. Although there are many mature visualization libraries in many different languages.
+But Rust is one of the best languages fits the need.
+
+* **Easy to use** Rust has a very good iterator system built into the standard library. With the help of iterators,
+Plotting in Rust can be as easy as most of the high-level programming languages. The Rust based plotting library
+can be very easy to use.
+
+* **Fast** If you need rendering a figure with trillions of data points,
+Rust is a good choice. Rust's performance allows you to combine data processing step
+and rendering step into a single application. When plotting in high-level programming languages,
+e.g. Javascript or Python, data points must be down-sampled before feeding into the plotting
+program because of the performance considerations. Rust is fast enough to do the data processing and visualization
+within a single program. You can also integrate the
+figure rendering code into your application handling a huge amount of data and visualize it in real-time.
+
+* **WebAssembly Support** Rust is one of few the language with the best WASM support. Plotting in Rust could be
+very useful for visualization on a web page and would have a huge performance improvement comparing to Javascript.
+
+## Plotting on HTML5 canvas with WASM Backend
+
+Plotters currently supports backend that uses the HTML5 canvas. To use the WASM support, you can simply use
+`CanvasBackend` instead of other backend and all other API remains the same!
+
+There's a small demo for Plotters + WASM under `examples/wasm-demo` directory of this repo.
+To play with the deployed version, follow this [link](https://plumberserver.com/plotters-wasm-demo/index.html).
+
+
+## What types of figure are supported?
+
+Plotters is not limited to any specific type of figure.
+You can create your own types of figures easily with the Plotters API.
+
+But Plotters provides some builtin figure types for convenience.
+Currently, we support line series, point series, candlestick series, and histogram.
+And the library is designed to be able to render multiple figure into a single image.
+But Plotter is aimed to be a platform that is fully extendable to support any other types of figure.
+
+## Concepts by examples
+
+### Drawing Back-ends
+Plotters can use different drawing back-ends, including SVG, BitMap, and even real-time rendering. For example, a bitmap drawing backend.
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ // Create a 800*600 bitmap and start drawing
+ let mut backend = BitMapBackend::new("plotters-doc-data/1.png", (300, 200));
+ // And if we want SVG backend
+ // let backend = SVGBackend::new("output.svg", (800, 600));
+ backend.draw_rect((50, 50), (200, 150), &RED, true)?;
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/1.png)
+
+### Drawing Area
+Plotters uses a concept called drawing area for layout purpose.
+Plotters support multiple integrating into a single image.
+This is done by creating sub-drawing-areas.
+
+Besides that, the drawing area also allows the customized coordinate system, by doing so, the coordinate mapping is done by the drawing area automatically.
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root_drawing_area =
+ BitMapBackend::new("plotters-doc-data/2.png", (300, 200)).into_drawing_area();
+ // And we can split the drawing area into 3x3 grid
+ let child_drawing_areas = root_drawing_area.split_evenly((3, 3));
+ // Then we fill the drawing area with different color
+ for (area, color) in child_drawing_areas.into_iter().zip(0..) {
+ area.fill(&Palette99::pick(color))?;
+ }
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/2.png)
+
+### Elements
+
+In Plotters, elements are build blocks of figures. All elements are able to draw on a drawing area.
+There are different types of built-in elements, like lines, texts, circles, etc.
+You can also define your own element in the application code.
+
+You may also combine existing elements to build a complex element.
+
+To learn more about the element system, please read the [element module documentation](./element/index.html).
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new("plotters-doc-data/3.png", (300, 200)).into_drawing_area();
+ root.fill(&WHITE)?;
+ // Draw an circle on the drawing area
+ root.draw(&Circle::new(
+ (100, 100),
+ 50,
+ Into::<ShapeStyle>::into(&GREEN).filled(),
+ ))?;
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/3.png)
+
+### Composable Elements
+
+Besides the built-in elements, elements can be composed into a logic group we called composed elements.
+When composing new elements, the upper-left corner is given in the target coordinate, and a new pixel-based
+coordinate which has the upper-left corner defined as `(0,0)` is used for further element composition purpose.
+
+For example, we can have an element which includes a dot and its coordinate.
+
+```rust
+use plotters::prelude::*;
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new("plotters-doc-data/4.png", (640, 480)).into_drawing_area();
+
+ root.fill(&RGBColor(240, 200, 200))?;
+
+ let root = root.apply_coord_spec(RangedCoord::<RangedCoordf32, RangedCoordf32>::new(
+ 0f32..1f32,
+ 0f32..1f32,
+ (0..640, 0..480),
+ ));
+
+ let dot_and_label = |x: f32, y: f32| {
+ return EmptyElement::at((x, y))
+ + Circle::new((0, 0), 3, ShapeStyle::from(&BLACK).filled())
+ + Text::new(
+ format!("({:.2},{:.2})", x, y),
+ (10, 0),
+ ("sans-serif", 15.0).into_font(),
+ );
+ };
+
+ root.draw(&dot_and_label(0.5, 0.6))?;
+ root.draw(&dot_and_label(0.25, 0.33))?;
+ root.draw(&dot_and_label(0.8, 0.8))?;
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/4.png)
+
+### Chart Context
+
+In order to draw a chart, Plotters need a data object built on top of the drawing area called `ChartContext`.
+The chart context defines even higher level constructs compare to the drawing area.
+For example, you can define the label areas, meshes, and put a data series onto the drawing area with the help
+of the chart context object.
+
+```rust
+use plotters::prelude::*;
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let root = BitMapBackend::new("plotters-doc-data/5.png", (640, 480)).into_drawing_area();
+ root.fill(&WHITE);
+ let root = root.margin(10, 10, 10, 10);
+ // After this point, we should be able to draw construct a chart context
+ let mut chart = ChartBuilder::on(&root)
+ // Set the caption of the chart
+ .caption("This is our first plot", ("sans-serif", 40).into_font())
+ // Set the size of the label region
+ .x_label_area_size(20)
+ .y_label_area_size(40)
+ // Finally attach a coordinate on the drawing area and make a chart context
+ .build_ranged(0f32..10f32, 0f32..10f32)?;
+
+ // Then we can draw a mesh
+ chart
+ .configure_mesh()
+ // We can customize the maximum number of labels allowed for each axis
+ .x_labels(5)
+ .y_labels(5)
+ // We can also change the format of the label text
+ .y_label_formatter(&|x| format!("{:.3}", x))
+ .draw()?;
+
+ // And we can draw something in the drawing area
+ chart.draw_series(LineSeries::new(
+ vec![(0.0, 0.0), (5.0, 5.0), (8.0, 7.0)],
+ &RED,
+ ))?;
+ // Similarly, we can draw point series
+ chart.draw_series(PointSeries::of_element(
+ vec![(0.0, 0.0), (5.0, 5.0), (8.0, 7.0)],
+ 5,
+ &RED,
+ &|c, s, st| {
+ return EmptyElement::at(c) // We want to construct a composed element on-the-fly
+ + Circle::new((0,0),s,st.filled()) // At this point, the new pixel coordinate is established
+ + Text::new(format!("{:?}", c), (10, 0), ("sans-serif", 10).into_font());
+ },
+ ))?;
+ Ok(())
+}
+```
+
+![](https://plotters-rs.github.io/plotters-doc-data/5.png)
+
+## Misc
+
+### Development Version
+
+To use the latest development version, pull https://github.com/38/plotters.git. In `Cargo.toml`
+
+```toml
+[dependencies]
+plotters = { git = "https://github.com/38/plotters.git" }
+```
+
+### Reducing Depending Libraries && Turning Off Backends
+Plotters now supports use features to control the backend dependencies. By default, `BitMapBackend` and `SVGBackend` are supported,
+use `default_features = false` in the dependency description in `Cargo.toml` and you can cherry-pick the backend implementations.
+
+- `svg` Enable the `SVGBackend`
+- `bitmap` Enable the `BitMapBackend`
+
+For example, the following dependency description would avoid compiling with bitmap support:
+
+```toml
+[dependencies]
+plotters = { git = "https://github.com/38/plotters.git", default_features = false, features = ["svg"] }
+```
+
+The library also allows consumers to make use of the [`Palette`](https://crates.io/crates/palette/) crate's color types by default.
+This behavior can also be turned off by setting `default_features = false`.
+
+### List of Features
+
+This is the full list of features that is defined by `Plotters` crate.
+Use `default_features = false` to disable those default enabled features,
+and then you should be able to cherry-pick what features you want to include into `Plotters` crate.
+By doing so, you can minimize the number of dependencies down to only `itertools` and compile time is less than 6s.
+
+The following list is a complete list of features that can be opt in and out.
+
+- Drawing backends related features
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| image\_encoder | Allow `BitMapBackend` save the result to bitmap files | image, rusttype, font-kit | Yes |
+| svg | Enable `SVGBackend` Support | None | Yes |
+| gif\_backend| Opt-in GIF animation Rendering support for `BitMapBackend`, implies `bitmap` enabled | gif | Yes |
+| piston | Enable `PistonWindowBackend` | piston\_window, rusttype, font-kit | No |
+| cairo | Enable `CairoBackend` | cairo-rs, rusttype, font-kit | No |
+
+- Font manipulation features
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| ttf | Allows TrueType font support | rusttype, font-kit | Yes |
+
+- Coordinate features
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| datetime | Eanble the date and time coordinate support | chrono | Yes |
+
+- Element, series and util functions
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| errorbar | The errorbar element support | None | Yes |
+| candlestick | The candlestick element support | None | Yes |
+| boxplot | The boxplot element support | None | Yes |
+| area\_series | The area series support | None | Yes |
+| line\_series | The line series support | None | Yes |
+| histogram | The histogram series support | None | Yes |
+| point\_series| The point series support | None | Yes |
+
+- Misc
+
+| Name | Description | Additional Dependency |Default?|
+|---------|--------------|--------|------------|
+| deprecated\_items | This feature allows use of deprecated items which is going to be removed in the future | None | Yes |
+| debug | Enable the code used for debugging | None | No |
+
+
+## FAQ List
+
+* Why does the WASM example break on my machine ?
+
+ The WASM example requires using `wasm32` target to build. Using `cargo build` is likely to use the default target
+ which in most of the case is any of the x86 target. Thus you need add `--target=wasm32-unknown-unknown` in the cargo
+ parameter list to build it.
+
+* How to draw text/circle/point/rectangle/... on the top of chart ?
+
+ As you may realized, Plotters is a drawing library rather than a traditional data plotting library,
+ you have the freedom to draw anything you want on the drawing area.
+ Use `DrawingArea::draw` to draw any element on the drawing area.
+
+
+<style>
+ img {
+ display: block;
+ margin: 0 auto;
+ max-width: 500px;
+ }
+ .galleryItem {
+ width: 250px;
+ display: inline-block;
+ }
+ .galleryImg {
+ max-width: 100%;
+ }
+ .galleryText {
+ text-align: center;
+ }
+</style>
+
+
+*/
+pub mod chart;
+pub mod coord;
+pub mod data;
+pub mod drawing;
+pub mod element;
+pub mod series;
+pub mod style;
+
+#[cfg(feature = "evcxr")]
+pub mod evcxr;
+
+#[cfg(test)]
+pub use crate::drawing::create_mocked_drawing_area;
+
+#[cfg(feature = "palette_ext")]
+pub use palette;
+
+/// The module imports the most commonly used types and modules in Plotters
+pub mod prelude {
+ pub use crate::chart::{ChartBuilder, ChartContext, LabelAreaPosition, SeriesLabelPosition};
+ pub use crate::coord::{
+ Category, CoordTranslate, GroupBy, IntoCentric, IntoPartialAxis, LogCoord, LogRange,
+ LogScalable, Ranged, RangedCoord, RangedCoordf32, RangedCoordf64, RangedCoordi32,
+ RangedCoordi64, RangedCoordu32, RangedCoordu64, ToGroupByRange,
+ };
+
+ #[cfg(feature = "chrono")]
+ pub use crate::coord::{make_partial_axis, RangedDate, RangedDateTime, RangedDuration};
+
+ pub use crate::drawing::*;
+ #[cfg(feature = "area_series")]
+ pub use crate::series::AreaSeries;
+ #[cfg(feature = "histogram")]
+ pub use crate::series::Histogram;
+ #[cfg(feature = "line_series")]
+ pub use crate::series::LineSeries;
+ #[cfg(feature = "point_series")]
+ pub use crate::series::PointSeries;
+
+ pub use crate::style::{
+ AsRelative, Color, FontDesc, FontFamily, FontStyle, FontTransform, HSLColor, IntoFont,
+ Palette, Palette100, Palette99, Palette9999, PaletteColor, RGBColor, ShapeStyle,
+ SimpleColor, TextStyle,
+ };
+ pub use crate::style::{BLACK, BLUE, CYAN, GREEN, MAGENTA, RED, TRANSPARENT, WHITE, YELLOW};
+
+ pub use crate::element::{
+ Circle, Cross, DynElement, EmptyElement, IntoDynElement, MultiLineText, PathElement, Pixel,
+ Polygon, Rectangle, Text, TriangleMarker,
+ };
+
+ #[cfg(feature = "boxplot")]
+ pub use crate::element::Boxplot;
+ #[cfg(feature = "candlestick")]
+ pub use crate::element::CandleStick;
+ #[cfg(feature = "errorbar")]
+ pub use crate::element::ErrorBar;
+
+ #[cfg(feature = "bitmap")]
+ pub use crate::element::BitMapElement;
+
+ pub use crate::data::Quartiles;
+
+ // TODO: This should be deprecated and completely removed
+ #[cfg(feature = "deprecated_items")]
+ #[allow(deprecated)]
+ pub use crate::element::Path;
+
+ #[allow(type_alias_bounds)]
+ /// The type used to returns a drawing operation that can be failed
+ /// - `T`: The return type
+ /// - `D`: The drawing backend type
+ pub type DrawResult<T, D: DrawingBackend> =
+ Result<T, crate::drawing::DrawingAreaErrorKind<D::ErrorType>>;
+
+ #[cfg(feature = "evcxr")]
+ pub use crate::evcxr::evcxr_figure;
+}
diff --git a/src/series/area_series.rs b/src/series/area_series.rs
new file mode 100644
index 0000000..f6dce0c
--- /dev/null
+++ b/src/series/area_series.rs
@@ -0,0 +1,62 @@
+use crate::drawing::DrawingBackend;
+use crate::element::{DynElement, IntoDynElement, PathElement, Polygon};
+use crate::style::colors::TRANSPARENT;
+use crate::style::ShapeStyle;
+
+/// An area series is similar to a line series but use a filled polygon
+pub struct AreaSeries<DB: DrawingBackend, X: Clone, Y: Clone> {
+ area_style: ShapeStyle,
+ border_style: ShapeStyle,
+ baseline: Y,
+ data: Vec<(X, Y)>,
+ state: u32,
+ _p: std::marker::PhantomData<DB>,
+}
+
+impl<DB: DrawingBackend, X: Clone, Y: Clone> AreaSeries<DB, X, Y> {
+ pub fn new<S: Into<ShapeStyle>, I: IntoIterator<Item = (X, Y)>>(
+ iter: I,
+ baseline: Y,
+ area_style: S,
+ ) -> Self {
+ Self {
+ area_style: area_style.into(),
+ baseline,
+ data: iter.into_iter().collect(),
+ state: 0,
+ border_style: (&TRANSPARENT).into(),
+ _p: std::marker::PhantomData,
+ }
+ }
+
+ pub fn border_style<S: Into<ShapeStyle>>(mut self, style: S) -> Self {
+ self.border_style = style.into();
+ self
+ }
+}
+
+impl<DB: DrawingBackend, X: Clone + 'static, Y: Clone + 'static> Iterator for AreaSeries<DB, X, Y> {
+ type Item = DynElement<'static, DB, (X, Y)>;
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.state == 0 {
+ let mut data: Vec<_> = self.data.clone();
+
+ if !data.is_empty() {
+ data.push((data[data.len() - 1].0.clone(), self.baseline.clone()));
+ data.push((data[0].0.clone(), self.baseline.clone()));
+ }
+
+ self.state = 1;
+
+ Some(Polygon::new(data, self.area_style.clone()).into_dyn())
+ } else if self.state == 1 {
+ let data: Vec<_> = self.data.clone();
+
+ self.state = 2;
+
+ Some(PathElement::new(data, self.border_style.clone()).into_dyn())
+ } else {
+ None
+ }
+ }
+}
diff --git a/src/series/histogram.rs b/src/series/histogram.rs
new file mode 100644
index 0000000..75c2fb2
--- /dev/null
+++ b/src/series/histogram.rs
@@ -0,0 +1,213 @@
+use std::collections::{hash_map::IntoIter as HashMapIter, HashMap};
+use std::hash::Hash;
+use std::marker::PhantomData;
+use std::ops::AddAssign;
+
+use crate::chart::ChartContext;
+use crate::coord::{DiscreteRanged, Ranged, RangedCoord};
+use crate::drawing::DrawingBackend;
+use crate::element::Rectangle;
+use crate::style::{Color, ShapeStyle, GREEN};
+
+pub trait HistogramType {}
+pub struct Vertical;
+pub struct Horizontal;
+
+impl HistogramType for Vertical {}
+impl HistogramType for Horizontal {}
+
+/// The series that aggregate data into a histogram
+pub struct Histogram<'a, BR, A, Tag = Vertical>
+where
+ BR: DiscreteRanged,
+ BR::ValueType: Eq + Hash,
+ A: AddAssign<A> + Default,
+ Tag: HistogramType,
+{
+ style: Box<dyn Fn(&BR::ValueType, &A) -> ShapeStyle + 'a>,
+ margin: u32,
+ iter: HashMapIter<BR::ValueType, A>,
+ baseline: Box<dyn Fn(BR::ValueType) -> A + 'a>,
+ br_param: BR::RangeParameter,
+ _p: PhantomData<(BR, Tag)>,
+}
+
+impl<'a, BR, A, Tag> Histogram<'a, BR, A, Tag>
+where
+ BR: DiscreteRanged,
+ BR::ValueType: Eq + Hash,
+ A: AddAssign<A> + Default + 'a,
+ Tag: HistogramType,
+{
+ fn empty(br_param: BR::RangeParameter) -> Self {
+ Self {
+ style: Box::new(|_, _| GREEN.filled()),
+ margin: 5,
+ iter: HashMap::new().into_iter(),
+ baseline: Box::new(|_| A::default()),
+ br_param,
+ _p: PhantomData,
+ }
+ }
+ /// Set the style of the histogram
+ pub fn style<S: Into<ShapeStyle>>(mut self, style: S) -> Self {
+ let style = style.into();
+ self.style = Box::new(move |_, _| style.clone());
+ self
+ }
+
+ /// Set the style of histogram using a lambda function
+ pub fn style_func(
+ mut self,
+ style_func: impl Fn(&BR::ValueType, &A) -> ShapeStyle + 'a,
+ ) -> Self {
+ self.style = Box::new(style_func);
+ self
+ }
+
+ /// Set the baseline of the histogram
+ pub fn baseline(mut self, baseline: A) -> Self
+ where
+ A: Clone,
+ {
+ self.baseline = Box::new(move |_| baseline.clone());
+ self
+ }
+
+ /// Set a function that defines variant baseline
+ pub fn baseline_func(mut self, func: impl Fn(BR::ValueType) -> A + 'a) -> Self {
+ self.baseline = Box::new(func);
+ self
+ }
+
+ /// Set the margin for each bar
+ pub fn margin(mut self, value: u32) -> Self {
+ self.margin = value;
+ self
+ }
+
+ /// Set the data iterator
+ pub fn data<I: IntoIterator<Item = (BR::ValueType, A)>>(mut self, iter: I) -> Self {
+ let mut buffer = HashMap::<BR::ValueType, A>::new();
+ for (x, y) in iter.into_iter() {
+ *buffer.entry(x).or_insert_with(Default::default) += y;
+ }
+ self.iter = buffer.into_iter();
+ self
+ }
+}
+
+pub trait UseDefaultParameter: Default {
+ fn new() -> Self {
+ Default::default()
+ }
+}
+
+impl UseDefaultParameter for () {}
+
+impl<'a, BR, A> Histogram<'a, BR, A, Vertical>
+where
+ BR: DiscreteRanged,
+ BR::ValueType: Eq + Hash,
+ A: AddAssign<A> + Default + 'a,
+{
+ /// Create a new histogram series.
+ ///
+ /// - `iter`: The data iterator
+ /// - `margin`: The margin between bars
+ /// - `style`: The style of bars
+ ///
+ /// Returns the newly created histogram series
+ #[allow(clippy::redundant_closure)]
+ pub fn new<S: Into<ShapeStyle>, I: IntoIterator<Item = (BR::ValueType, A)>>(
+ iter: I,
+ margin: u32,
+ style: S,
+ ) -> Self
+ where
+ BR::RangeParameter: UseDefaultParameter,
+ {
+ let mut buffer = HashMap::<BR::ValueType, A>::new();
+ for (x, y) in iter.into_iter() {
+ *buffer.entry(x).or_insert_with(Default::default) += y;
+ }
+ let style = style.into();
+ Self {
+ style: Box::new(move |_, _| style.clone()),
+ margin,
+ iter: buffer.into_iter(),
+ baseline: Box::new(|_| A::default()),
+ br_param: BR::RangeParameter::new(),
+ _p: PhantomData,
+ }
+ }
+
+ pub fn vertical<ACoord, DB: DrawingBackend + 'a>(
+ parent: &ChartContext<DB, RangedCoord<BR, ACoord>>,
+ ) -> Self
+ where
+ ACoord: Ranged<ValueType = A>,
+ {
+ let dp = parent.as_coord_spec().x_spec().get_range_parameter();
+
+ Self::empty(dp)
+ }
+}
+
+impl<'a, BR, A> Histogram<'a, BR, A, Horizontal>
+where
+ BR: DiscreteRanged,
+ BR::ValueType: Eq + Hash,
+ A: AddAssign<A> + Default + 'a,
+{
+ pub fn horizontal<ACoord, DB: DrawingBackend>(
+ parent: &ChartContext<DB, RangedCoord<ACoord, BR>>,
+ ) -> Self
+ where
+ ACoord: Ranged<ValueType = A>,
+ {
+ let dp = parent.as_coord_spec().y_spec().get_range_parameter();
+ Self::empty(dp)
+ }
+}
+
+impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Vertical>
+where
+ BR: DiscreteRanged,
+ BR::ValueType: Eq + Hash,
+ A: AddAssign<A> + Default,
+{
+ type Item = Rectangle<(BR::ValueType, A)>;
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some((x, y)) = self.iter.next() {
+ let nx = BR::next_value(&x, &self.br_param);
+ let base = (self.baseline)(BR::previous_value(&nx, &self.br_param));
+ let style = (self.style)(&x, &y);
+ let mut rect = Rectangle::new([(x, y), (nx, base)], style);
+ rect.set_margin(0, 0, self.margin, self.margin);
+ return Some(rect);
+ }
+ None
+ }
+}
+
+impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Horizontal>
+where
+ BR: DiscreteRanged,
+ BR::ValueType: Eq + Hash,
+ A: AddAssign<A> + Default,
+{
+ type Item = Rectangle<(A, BR::ValueType)>;
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some((y, x)) = self.iter.next() {
+ let ny = BR::next_value(&y, &self.br_param);
+ // With this trick we can avoid the clone trait bound
+ let base = (self.baseline)(BR::previous_value(&ny, &self.br_param));
+ let style = (self.style)(&y, &x);
+ let mut rect = Rectangle::new([(x, y), (base, ny)], style);
+ rect.set_margin(self.margin, self.margin, 0, 0);
+ return Some(rect);
+ }
+ None
+ }
+}
diff --git a/src/series/line_series.rs b/src/series/line_series.rs
new file mode 100644
index 0000000..d3a5971
--- /dev/null
+++ b/src/series/line_series.rs
@@ -0,0 +1,86 @@
+use crate::drawing::DrawingBackend;
+use crate::element::{Circle, DynElement, IntoDynElement, PathElement};
+use crate::style::ShapeStyle;
+use std::marker::PhantomData;
+
+/// The line series object, which takes an iterator of points in guest coordinate system
+/// and creates the element rendering the line plot
+pub struct LineSeries<DB: DrawingBackend, Coord> {
+ style: ShapeStyle,
+ data: Vec<Coord>,
+ point_idx: usize,
+ point_size: u32,
+ phantom: PhantomData<DB>,
+}
+
+impl<DB: DrawingBackend, Coord: Clone + 'static> Iterator for LineSeries<DB, Coord> {
+ type Item = DynElement<'static, DB, Coord>;
+ fn next(&mut self) -> Option<Self::Item> {
+ if !self.data.is_empty() {
+ if self.point_size > 0 && self.point_idx < self.data.len() {
+ let idx = self.point_idx;
+ self.point_idx += 1;
+ return Some(
+ Circle::new(self.data[idx].clone(), self.point_size, self.style.clone())
+ .into_dyn(),
+ );
+ }
+ let mut data = vec![];
+ std::mem::swap(&mut self.data, &mut data);
+ Some(PathElement::new(data, self.style.clone()).into_dyn())
+ } else {
+ None
+ }
+ }
+}
+
+impl<DB: DrawingBackend, Coord> LineSeries<DB, Coord> {
+ pub fn new<I: IntoIterator<Item = Coord>, S: Into<ShapeStyle>>(iter: I, style: S) -> Self {
+ Self {
+ style: style.into(),
+ data: iter.into_iter().collect(),
+ point_size: 0,
+ point_idx: 0,
+ phantom: PhantomData,
+ }
+ }
+
+ pub fn point_size(mut self, size: u32) -> Self {
+ self.point_size = size;
+ self
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::prelude::*;
+
+ #[test]
+ fn test_line_series() {
+ let drawing_area = create_mocked_drawing_area(200, 200, |m| {
+ m.check_draw_path(|c, s, path| {
+ assert_eq!(c, RED.to_rgba());
+ assert_eq!(s, 3);
+ for i in 0..100 {
+ assert_eq!(path[i], (i as i32 * 2, 200 - i as i32 * 2 - 1));
+ }
+ });
+
+ m.drop_check(|b| {
+ assert_eq!(b.num_draw_path_call, 1);
+ assert_eq!(b.draw_count, 1);
+ });
+ });
+
+ let mut chart = ChartBuilder::on(&drawing_area)
+ .build_ranged(0..100, 0..100)
+ .expect("Build chart error");
+
+ chart
+ .draw_series(LineSeries::new(
+ (0..100).map(|x| (x, x)),
+ Into::<ShapeStyle>::into(&RED).stroke_width(3),
+ ))
+ .expect("Drawing Error");
+ }
+}
diff --git a/src/series/mod.rs b/src/series/mod.rs
new file mode 100644
index 0000000..103135e
--- /dev/null
+++ b/src/series/mod.rs
@@ -0,0 +1,29 @@
+/*!
+ This module contains predefined types of series.
+ The series in Plotters is actually an iterator of elements, which
+ can be taken by `ChartContext::draw_series` function.
+
+ This module defines some "iterator transformer", which transform the data
+ iterator to the element iterator.
+
+ Any type that implements iterator emitting drawable elements are acceptable series.
+ So iterator combinator such as `map`, `zip`, etc can also be used.
+*/
+
+#[cfg(feature = "area_series")]
+mod area_series;
+#[cfg(feature = "histogram")]
+mod histogram;
+#[cfg(feature = "line_series")]
+mod line_series;
+#[cfg(feature = "point_series")]
+mod point_series;
+
+#[cfg(feature = "area_series")]
+pub use area_series::AreaSeries;
+#[cfg(feature = "histogram")]
+pub use histogram::Histogram;
+#[cfg(feature = "line_series")]
+pub use line_series::LineSeries;
+#[cfg(feature = "point_series")]
+pub use point_series::PointSeries;
diff --git a/src/series/point_series.rs b/src/series/point_series.rs
new file mode 100644
index 0000000..c304ddf
--- /dev/null
+++ b/src/series/point_series.rs
@@ -0,0 +1,61 @@
+use crate::element::PointElement;
+use crate::style::{ShapeStyle, SizeDesc};
+
+/// The point plot object, which takes an iterator of points in guest coordinate system
+/// and create an element for each point
+pub struct PointSeries<'a, Coord, I: IntoIterator<Item = Coord>, E, Size: SizeDesc + Clone> {
+ style: ShapeStyle,
+ size: Size,
+ data_iter: I::IntoIter,
+ make_point: &'a dyn Fn(Coord, Size, ShapeStyle) -> E,
+}
+
+impl<'a, Coord, I: IntoIterator<Item = Coord>, E, Size: SizeDesc + Clone> Iterator
+ for PointSeries<'a, Coord, I, E, Size>
+{
+ type Item = E;
+ fn next(&mut self) -> Option<Self::Item> {
+ self.data_iter
+ .next()
+ .map(|x| (self.make_point)(x, self.size.clone(), self.style.clone()))
+ }
+}
+
+impl<'a, Coord, I: IntoIterator<Item = Coord>, E, Size: SizeDesc + Clone>
+ PointSeries<'a, Coord, I, E, Size>
+where
+ E: PointElement<Coord, Size>,
+{
+ /// Create a new point series with the element that implements point trait.
+ /// You may also use a more general way to create a point series with `of_element`
+ /// function which allows a customized element construction function
+ pub fn new<S: Into<ShapeStyle>>(iter: I, size: Size, style: S) -> Self {
+ Self {
+ data_iter: iter.into_iter(),
+ size,
+ style: style.into(),
+ make_point: &|a, b, c| E::make_point(a, b, c),
+ }
+ }
+}
+
+impl<'a, Coord, I: IntoIterator<Item = Coord>, E, Size: SizeDesc + Clone>
+ PointSeries<'a, Coord, I, E, Size>
+{
+ /// Create a new point series. Similar to `PointSeries::new` but it doesn't
+ /// requires the element implements point trait. So instead of using the point
+ /// constructor, it uses the customized function for element creation
+ pub fn of_element<S: Into<ShapeStyle>, F: Fn(Coord, Size, ShapeStyle) -> E>(
+ iter: I,
+ size: Size,
+ style: S,
+ cons: &'a F,
+ ) -> Self {
+ Self {
+ data_iter: iter.into_iter(),
+ size,
+ style: style.into(),
+ make_point: cons,
+ }
+ }
+}
diff --git a/src/style/color.rs b/src/style/color.rs
new file mode 100644
index 0000000..f43f863
--- /dev/null
+++ b/src/style/color.rs
@@ -0,0 +1,152 @@
+use super::palette::Palette;
+use super::ShapeStyle;
+
+use std::marker::PhantomData;
+
+/// Any color representation
+pub trait Color {
+ /// Convert the RGB representation to the standard RGB tuple
+ fn rgb(&self) -> (u8, u8, u8);
+
+ /// Get the alpha channel of the color
+ fn alpha(&self) -> f64;
+
+ /// Mix the color with given opacity
+ fn mix(&self, value: f64) -> RGBAColor {
+ let (r, g, b) = self.rgb();
+ let a = self.alpha() * value;
+ RGBAColor(r, g, b, a)
+ }
+
+ /// Convert the color into the RGBA color which is internally used by Plotters
+ fn to_rgba(&self) -> RGBAColor {
+ let (r, g, b) = self.rgb();
+ let a = self.alpha();
+ RGBAColor(r, g, b, a)
+ }
+
+ /// Make a filled style form the color
+ fn filled(&self) -> ShapeStyle
+ where
+ Self: Sized,
+ {
+ Into::<ShapeStyle>::into(self).filled()
+ }
+
+ /// Make a shape style with stroke width from a color
+ fn stroke_width(&self, width: u32) -> ShapeStyle
+ where
+ Self: Sized,
+ {
+ Into::<ShapeStyle>::into(self).stroke_width(width)
+ }
+}
+
+/// The RGBA representation of the color, Plotters use RGBA as the internal representation
+/// of color
+#[derive(Clone, PartialEq, Debug)]
+pub struct RGBAColor(pub(super) u8, pub(super) u8, pub(super) u8, pub(super) f64);
+
+impl Color for RGBAColor {
+ #[inline(always)]
+ fn rgb(&self) -> (u8, u8, u8) {
+ (self.0, self.1, self.2)
+ }
+
+ #[inline(always)]
+ fn alpha(&self) -> f64 {
+ self.3
+ }
+
+ fn to_rgba(&self) -> RGBAColor {
+ self.clone()
+ }
+}
+
+/// Color without alpha channel
+pub trait SimpleColor {
+ fn rgb(&self) -> (u8, u8, u8);
+}
+
+impl<T: SimpleColor> Color for T {
+ fn rgb(&self) -> (u8, u8, u8) {
+ SimpleColor::rgb(self)
+ }
+
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+/// A color in the given palette
+pub struct PaletteColor<P: Palette>(usize, PhantomData<P>);
+
+impl<P: Palette> PaletteColor<P> {
+ /// Pick a color from the palette
+ pub fn pick(idx: usize) -> PaletteColor<P> {
+ PaletteColor(idx % P::COLORS.len(), PhantomData)
+ }
+}
+
+impl<P: Palette> SimpleColor for PaletteColor<P> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ P::COLORS[self.0]
+ }
+}
+
+/// The color described by its RGB value
+#[derive(Debug)]
+pub struct RGBColor(pub u8, pub u8, pub u8);
+
+impl SimpleColor for RGBColor {
+ fn rgb(&self) -> (u8, u8, u8) {
+ (self.0, self.1, self.2)
+ }
+}
+
+/// The color described by HSL color space
+pub struct HSLColor(pub f64, pub f64, pub f64);
+
+impl SimpleColor for HSLColor {
+ #[allow(clippy::many_single_char_names)]
+ fn rgb(&self) -> (u8, u8, u8) {
+ let (h, s, l) = (
+ self.0.min(1.0).max(0.0),
+ self.1.min(1.0).max(0.0),
+ self.2.min(1.0).max(0.0),
+ );
+
+ if s == 0.0 {
+ let value = (l * 255.0).round() as u8;
+ return (value, value, value);
+ }
+
+ let q = if l < 0.5 {
+ l * (1.0 + s)
+ } else {
+ l + s - l * s
+ };
+ let p = 2.0 * l - q;
+
+ let cvt = |mut t| {
+ if t < 0.0 {
+ t += 1.0;
+ }
+ if t > 1.0 {
+ t -= 1.0;
+ }
+ let value = if t < 1.0 / 6.0 {
+ p + (q - p) * 6.0 * t
+ } else if t < 1.0 / 2.0 {
+ q
+ } else if t < 2.0 / 3.0 {
+ p + (q - p) * (2.0 / 3.0 - t) * 6.0
+ } else {
+ p
+ };
+ (value * 255.0).round() as u8
+ };
+
+ (cvt(h + 1.0 / 3.0), cvt(h), cvt(h - 1.0 / 3.0))
+ }
+}
diff --git a/src/style/colors.rs b/src/style/colors.rs
new file mode 100644
index 0000000..4854da8
--- /dev/null
+++ b/src/style/colors.rs
@@ -0,0 +1,57 @@
+//! Basic predefined colors.
+use super::{RGBAColor, RGBColor};
+
+macro_rules! predefined_color {
+ ($name:ident, $r:expr, $g:expr, $b:expr, $doc:expr) => {
+ #[doc = $doc]
+ pub const $name: RGBColor = RGBColor($r, $g, $b);
+ };
+
+ ($name:ident, $r:expr, $g:expr, $b:expr, $a: expr, $doc:expr) => {
+ #[doc = $doc]
+ pub const $name: RGBAColor = RGBAColor($r, $g, $b, $a);
+ }
+}
+
+predefined_color!(WHITE, 255, 255, 255, "The predefined white color");
+predefined_color!(BLACK, 0, 0, 0, "The predefined black color");
+predefined_color!(RED, 255, 0, 0, "The predefined red color");
+predefined_color!(GREEN, 0, 255, 0, "The predefined green color");
+predefined_color!(BLUE, 0, 0, 255, "The predefined blue color");
+predefined_color!(YELLOW, 255, 255, 0, "The predefined yellow color");
+predefined_color!(CYAN, 0, 255, 255, "The predefined cyan color");
+predefined_color!(MAGENTA, 255, 0, 255, "The predefined magenta color");
+predefined_color!(TRANSPARENT, 0, 0, 0, 0.0, "The predefined transparent");
+
+/// Predefined Color definitions using the [palette](https://docs.rs/palette/) color types
+#[cfg(feature = "palette_ext")]
+pub mod palette_ext {
+ use palette::rgb::Srgb;
+ use palette::Alpha;
+
+ use std::marker::PhantomData;
+
+ macro_rules! predefined_color_pal {
+ ($name:ident, $r:expr, $g:expr, $b:expr, $doc:expr) => {
+ #[doc = $doc]
+ pub const $name: Srgb<u8> = predefined_color_pal!(@gen_c $r, $g, $b);
+ };
+ ($name:ident, $r:expr, $g:expr, $b:expr, $a:expr, $doc:expr) => {
+ #[doc = $doc]
+ pub const $name: Alpha<Srgb<u8>, f64> = Alpha{ alpha: $a, color: predefined_color_pal!(@gen_c $r, $g, $b) };
+ };
+ (@gen_c $r:expr, $g:expr, $b:expr) => {
+ Srgb { red: $r, green: $g, blue: $b, standard: PhantomData }
+ };
+ }
+
+ predefined_color_pal!(WHITE, 255, 255, 255, "The predefined white color");
+ predefined_color_pal!(BLACK, 0, 0, 0, "The predefined black color");
+ predefined_color_pal!(RED, 255, 0, 0, "The predefined red color");
+ predefined_color_pal!(GREEN, 0, 255, 0, "The predefined green color");
+ predefined_color_pal!(BLUE, 0, 0, 255, "The predefined blue color");
+ predefined_color_pal!(YELLOW, 255, 255, 0, "The predefined yellow color");
+ predefined_color_pal!(CYAN, 0, 255, 255, "The predefined cyan color");
+ predefined_color_pal!(MAGENTA, 255, 0, 255, "The predefined magenta color");
+ predefined_color_pal!(TRANSPARENT, 0, 0, 0, 0.0, "The predefined transparent");
+}
diff --git a/src/style/font/font_desc.rs b/src/style/font/font_desc.rs
new file mode 100644
index 0000000..7caa4e4
--- /dev/null
+++ b/src/style/font/font_desc.rs
@@ -0,0 +1,296 @@
+use super::{FontData, FontDataInternal};
+use crate::style::text_anchor::Pos;
+use crate::style::{Color, TextStyle};
+
+use std::convert::From;
+
+/// The error type for the font implementation
+pub type FontError = <FontDataInternal as FontData>::ErrorType;
+
+/// The type we used to represent a result of any font operations
+pub type FontResult<T> = Result<T, FontError>;
+
+/// Specifying text transformations
+#[derive(Clone)]
+pub enum FontTransform {
+ /// Nothing to transform
+ None,
+ /// Rotating the text 90 degree clockwise
+ Rotate90,
+ /// Rotating the text 180 degree clockwise
+ Rotate180,
+ /// Rotating the text 270 degree clockwise
+ Rotate270,
+}
+
+impl FontTransform {
+ /// Transform the coordinate to perform the rotation
+ ///
+ /// - `x`: The x coordinate in pixels before transform
+ /// - `y`: The y coordinate in pixels before transform
+ /// - **returns**: The coordinate after transform
+ pub fn transform(&self, x: i32, y: i32) -> (i32, i32) {
+ match self {
+ FontTransform::None => (x, y),
+ FontTransform::Rotate90 => (-y, x),
+ FontTransform::Rotate180 => (-x, -y),
+ FontTransform::Rotate270 => (y, -x),
+ }
+ }
+}
+
+/// Describes a font
+#[derive(Clone)]
+pub struct FontDesc<'a> {
+ size: f64,
+ family: FontFamily<'a>,
+ data: FontResult<FontDataInternal>,
+ transform: FontTransform,
+ style: FontStyle,
+}
+
+/// Describes font family.
+/// This can be either a specific font family name, such as "arial",
+/// or a general font family class, such as "serif" and "sans-serif"
+#[derive(Clone, Copy)]
+pub enum FontFamily<'a> {
+ /// The system default serif font family
+ Serif,
+ /// The system default sans-serif font family
+ SansSerif,
+ /// The system default monospace font
+ Monospace,
+ /// A specific font family name
+ Name(&'a str),
+}
+
+impl<'a> FontFamily<'a> {
+ /// Make a CSS compatible string for the font family name.
+ /// This can be used as the value of `font-family` attribute in SVG.
+ pub fn as_str(&self) -> &str {
+ match self {
+ FontFamily::Serif => "serif",
+ FontFamily::SansSerif => "sans-serif",
+ FontFamily::Monospace => "monospace",
+ FontFamily::Name(face) => face,
+ }
+ }
+}
+
+impl<'a> From<&'a str> for FontFamily<'a> {
+ fn from(from: &'a str) -> FontFamily<'a> {
+ match from.to_lowercase().as_str() {
+ "serif" => FontFamily::Serif,
+ "sans-serif" => FontFamily::SansSerif,
+ "monospace" => FontFamily::Monospace,
+ _ => FontFamily::Name(from),
+ }
+ }
+}
+
+/// Describes the font style. Such as Italic, Oblique, etc.
+#[derive(Clone, Copy)]
+pub enum FontStyle {
+ /// The normal style
+ Normal,
+ /// The oblique style
+ Oblique,
+ /// The italic style
+ Italic,
+ /// The bold style
+ Bold,
+}
+
+impl FontStyle {
+ /// Convert the font style into a CSS compatible string which can be used in `font-style` attribute.
+ pub fn as_str(&self) -> &str {
+ match self {
+ FontStyle::Normal => "normal",
+ FontStyle::Italic => "italic",
+ FontStyle::Oblique => "oblique",
+ FontStyle::Bold => "bold",
+ }
+ }
+}
+
+impl<'a> From<&'a str> for FontStyle {
+ fn from(from: &'a str) -> FontStyle {
+ match from.to_lowercase().as_str() {
+ "normal" => FontStyle::Normal,
+ "italic" => FontStyle::Italic,
+ "oblique" => FontStyle::Oblique,
+ "bold" => FontStyle::Bold,
+ _ => FontStyle::Normal,
+ }
+ }
+}
+
+impl<'a> From<&'a str> for FontDesc<'a> {
+ fn from(from: &'a str) -> FontDesc<'a> {
+ FontDesc::new(from.into(), 1.0, FontStyle::Normal)
+ }
+}
+
+impl<'a> From<FontFamily<'a>> for FontDesc<'a> {
+ fn from(family: FontFamily<'a>) -> FontDesc<'a> {
+ FontDesc::new(family, 1.0, FontStyle::Normal)
+ }
+}
+
+impl<'a, T: Into<f64>> From<(FontFamily<'a>, T)> for FontDesc<'a> {
+ fn from((family, size): (FontFamily<'a>, T)) -> FontDesc<'a> {
+ FontDesc::new(family, size.into(), FontStyle::Normal)
+ }
+}
+
+impl<'a, T: Into<f64>> From<(&'a str, T)> for FontDesc<'a> {
+ fn from((typeface, size): (&'a str, T)) -> FontDesc<'a> {
+ FontDesc::new(typeface.into(), size.into(), FontStyle::Normal)
+ }
+}
+
+impl<'a, T: Into<f64>, S: Into<FontStyle>> From<(FontFamily<'a>, T, S)> for FontDesc<'a> {
+ fn from((family, size, style): (FontFamily<'a>, T, S)) -> FontDesc<'a> {
+ FontDesc::new(family, size.into(), style.into())
+ }
+}
+
+impl<'a, T: Into<f64>, S: Into<FontStyle>> From<(&'a str, T, S)> for FontDesc<'a> {
+ fn from((typeface, size, style): (&'a str, T, S)) -> FontDesc<'a> {
+ FontDesc::new(typeface.into(), size.into(), style.into())
+ }
+}
+
+/// The trait that allows some type turns into a font description
+pub trait IntoFont<'a> {
+ /// Make the font description from the source type
+ fn into_font(self) -> FontDesc<'a>;
+}
+
+impl<'a, T: Into<FontDesc<'a>>> IntoFont<'a> for T {
+ fn into_font(self) -> FontDesc<'a> {
+ self.into()
+ }
+}
+
+impl<'a> FontDesc<'a> {
+ /// Create a new font
+ ///
+ /// - `family`: The font family name
+ /// - `size`: The size of the font
+ /// - `style`: The font variations
+ /// - **returns** The newly created font description
+ pub fn new(family: FontFamily<'a>, size: f64, style: FontStyle) -> Self {
+ Self {
+ size,
+ family,
+ data: FontDataInternal::new(family, style),
+ transform: FontTransform::None,
+ style,
+ }
+ }
+
+ /// Create a new font desc with the same font but different size
+ ///
+ /// - `size`: The new size to set
+ /// - **returns** The newly created font descriptor with a new size
+ pub fn resize(&self, size: f64) -> FontDesc<'a> {
+ Self {
+ size,
+ family: self.family,
+ data: self.data.clone(),
+ transform: self.transform.clone(),
+ style: self.style,
+ }
+ }
+
+ /// Set the style of the font
+ ///
+ /// - `style`: The new style
+ /// - **returns** The new font description with this style applied
+ pub fn style(&self, style: FontStyle) -> Self {
+ Self {
+ size: self.size,
+ family: self.family,
+ data: self.data.clone(),
+ transform: self.transform.clone(),
+ style,
+ }
+ }
+
+ /// Set the font transformation
+ ///
+ /// - `trans`: The new transformation
+ /// - **returns** The new font description with this font transformation applied
+ pub fn transform(&self, trans: FontTransform) -> Self {
+ Self {
+ size: self.size,
+ family: self.family,
+ data: self.data.clone(),
+ transform: trans,
+ style: self.style,
+ }
+ }
+
+ /// Get the font transformation description
+ pub fn get_transform(&self) -> FontTransform {
+ self.transform.clone()
+ }
+
+ /// Set the color of the font and return the result text style object
+ pub fn color<C: Color>(&self, color: &C) -> TextStyle<'a> {
+ TextStyle {
+ font: self.clone(),
+ color: color.to_rgba(),
+ pos: Pos::default(),
+ }
+ }
+
+ /// Get the name of the font
+ pub fn get_name(&self) -> &str {
+ self.family.as_str()
+ }
+
+ /// Get the name of the style
+ pub fn get_style(&self) -> FontStyle {
+ self.style
+ }
+
+ /// Get the size of font
+ pub fn get_size(&self) -> f64 {
+ self.size
+ }
+
+ /// Get the size of the text if rendered in this font
+ ///
+ /// For a TTF type, zero point of the layout box is the left most baseline char of the string
+ /// Thus the upper bound of the box is most likely be negative
+ pub fn layout_box(&self, text: &str) -> FontResult<((i32, i32), (i32, i32))> {
+ match &self.data {
+ Ok(ref font) => font.estimate_layout(self.size, text),
+ Err(e) => Err(e.clone()),
+ }
+ }
+
+ /// Get the size of the text if rendered in this font.
+ /// This is similar to `layout_box` function, but it apply the font transformation
+ /// and estimate the overall size of the font
+ pub fn box_size(&self, text: &str) -> FontResult<(u32, u32)> {
+ let ((min_x, min_y), (max_x, max_y)) = self.layout_box(text)?;
+ let (w, h) = self.get_transform().transform(max_x - min_x, max_y - min_y);
+ Ok((w.abs() as u32, h.abs() as u32))
+ }
+
+ /// Actually draws a font with a drawing function
+ pub fn draw<E, DrawFunc: FnMut(i32, i32, f32) -> Result<(), E>>(
+ &self,
+ text: &str,
+ (x, y): (i32, i32),
+ draw: DrawFunc,
+ ) -> FontResult<Result<(), E>> {
+ match &self.data {
+ Ok(ref font) => font.draw((x, y), self.size, text, draw),
+ Err(e) => Err(e.clone()),
+ }
+ }
+}
diff --git a/src/style/font/mod.rs b/src/style/font/mod.rs
new file mode 100644
index 0000000..99c3ca6
--- /dev/null
+++ b/src/style/font/mod.rs
@@ -0,0 +1,42 @@
+/// The implementation of an actual font implementation
+///
+/// This exists since for the image rendering task, we want to use
+/// the system font. But in wasm application, we want the browser
+/// to handle all the font issue.
+///
+/// Thus we need different mechanism for the font implementation
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "ttf"))]
+mod ttf;
+#[cfg(all(not(target_arch = "wasm32"), feature = "ttf"))]
+use ttf::FontDataInternal;
+
+#[cfg(all(not(target_arch = "wasm32"), not(feature = "ttf")))]
+mod naive;
+#[cfg(all(not(target_arch = "wasm32"), not(feature = "ttf")))]
+use naive::FontDataInternal;
+
+#[cfg(target_arch = "wasm32")]
+mod web;
+#[cfg(target_arch = "wasm32")]
+use web::FontDataInternal;
+
+mod font_desc;
+pub use font_desc::*;
+
+pub type LayoutBox = ((i32, i32), (i32, i32));
+
+pub trait FontData: Clone {
+ type ErrorType: Sized + std::error::Error + Clone;
+ fn new(family: FontFamily, style: FontStyle) -> Result<Self, Self::ErrorType>;
+ fn estimate_layout(&self, size: f64, text: &str) -> Result<LayoutBox, Self::ErrorType>;
+ fn draw<E, DrawFunc: FnMut(i32, i32, f32) -> Result<(), E>>(
+ &self,
+ _pos: (i32, i32),
+ _size: f64,
+ _text: &str,
+ _draw: DrawFunc,
+ ) -> Result<Result<(), E>, Self::ErrorType> {
+ panic!("The font implementation is unable to draw text");
+ }
+}
diff --git a/src/style/font/naive.rs b/src/style/font/naive.rs
new file mode 100644
index 0000000..9953040
--- /dev/null
+++ b/src/style/font/naive.rs
@@ -0,0 +1,40 @@
+use super::{FontData, FontFamily, FontStyle, LayoutBox};
+
+#[derive(Debug, Clone)]
+pub struct FontError;
+
+impl std::fmt::Display for FontError {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ write!(fmt, "General Error")?;
+ Ok(())
+ }
+}
+
+impl std::error::Error for FontError {}
+
+#[derive(Clone)]
+pub struct FontDataInternal(String, String);
+
+impl FontData for FontDataInternal {
+ type ErrorType = FontError;
+ fn new(family: FontFamily, style: FontStyle) -> Result<Self, FontError> {
+ Ok(FontDataInternal(
+ family.as_str().into(),
+ style.as_str().into(),
+ ))
+ }
+
+ /// Note: This is only a crude estimatation, since for some backend such as SVG, we have no way to
+ /// know the real size of the text anyway. Thus using font-kit is an overkill and doesn't helps
+ /// the layout.
+ fn estimate_layout(&self, size: f64, text: &str) -> Result<LayoutBox, Self::ErrorType> {
+ let em = size / 1.24 / 1.24;
+ Ok((
+ (0, -em.round() as i32),
+ (
+ (em * 0.7 * text.len() as f64).round() as i32,
+ (em * 0.24).round() as i32,
+ ),
+ ))
+ }
+}
diff --git a/src/style/font/ttf.rs b/src/style/font/ttf.rs
new file mode 100644
index 0000000..c76a2df
--- /dev/null
+++ b/src/style/font/ttf.rs
@@ -0,0 +1,209 @@
+use std::borrow::{Borrow, Cow};
+use std::collections::HashMap;
+use std::i32;
+use std::io::Read;
+use std::sync::{Arc, RwLock};
+
+use lazy_static::lazy_static;
+use rusttype::{point, Error, Font, FontCollection, Scale, SharedBytes};
+
+use font_kit::family_name::FamilyName;
+use font_kit::handle::Handle;
+use font_kit::properties::{Properties, Style, Weight};
+use font_kit::source::SystemSource;
+
+use super::{FontData, FontFamily, FontStyle, LayoutBox};
+
+type FontResult<T> = Result<T, FontError>;
+
+#[derive(Debug, Clone)]
+pub enum FontError {
+ LockError,
+ NoSuchFont(String, String),
+ FontLoadError(Arc<Error>),
+}
+
+impl std::fmt::Display for FontError {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ match self {
+ FontError::LockError => write!(fmt, "Could not lock mutex"),
+ FontError::NoSuchFont(family, style) => {
+ write!(fmt, "No such font: {} {}", family, style)
+ }
+ FontError::FontLoadError(e) => write!(fmt, "Font loading error: {}", e),
+ }
+ }
+}
+
+impl std::error::Error for FontError {}
+
+lazy_static! {
+ static ref CACHE: RwLock<HashMap<String, FontResult<Font<'static>>>> =
+ RwLock::new(HashMap::new());
+}
+
+thread_local! {
+ static FONT_SOURCE: SystemSource = SystemSource::new();
+}
+
+#[allow(dead_code)]
+/// Lazily load font data. Font type doesn't own actual data, which
+/// lives in the cache.
+fn load_font_data(face: FontFamily, style: FontStyle) -> FontResult<Font<'static>> {
+ let key = match style {
+ FontStyle::Normal => Cow::Borrowed(face.as_str()),
+ _ => Cow::Owned(format!("{}, {}", face.as_str(), style.as_str())),
+ };
+ let cache = CACHE.read().unwrap();
+ if let Some(cached) = cache.get(Borrow::<str>::borrow(&key)) {
+ return cached.clone();
+ }
+ drop(cache);
+
+ let mut properties = Properties::new();
+ match style {
+ FontStyle::Normal => properties.style(Style::Normal),
+ FontStyle::Italic => properties.style(Style::Italic),
+ FontStyle::Oblique => properties.style(Style::Oblique),
+ FontStyle::Bold => properties.weight(Weight::BOLD),
+ };
+
+ let family = match face {
+ FontFamily::Serif => FamilyName::Serif,
+ FontFamily::SansSerif => FamilyName::SansSerif,
+ FontFamily::Monospace => FamilyName::Monospace,
+ FontFamily::Name(name) => FamilyName::Title(name.to_owned()),
+ };
+
+ let make_not_found_error =
+ || FontError::NoSuchFont(face.as_str().to_owned(), style.as_str().to_owned());
+
+ if let Ok(handle) = FONT_SOURCE
+ .with(|source| source.select_best_match(&[family, FamilyName::SansSerif], &properties))
+ {
+ let (data, id) = match handle {
+ Handle::Path {
+ path,
+ font_index: idx,
+ } => {
+ let mut buf = vec![];
+ std::fs::File::open(path)
+ .map_err(|_| make_not_found_error())?
+ .read_to_end(&mut buf)
+ .map_err(|_| make_not_found_error())?;
+ (buf, idx)
+ }
+ Handle::Memory {
+ bytes,
+ font_index: idx,
+ } => (bytes[..].to_owned(), idx),
+ };
+ // TODO: font-kit actually have rasterizer, so consider remove dependency for rusttype as
+ // well
+ let result = FontCollection::from_bytes(Into::<SharedBytes>::into(data))
+ .map_err(|err| FontError::FontLoadError(Arc::new(err)))?
+ .font_at(id.max(0) as usize)
+ .map_err(|err| FontError::FontLoadError(Arc::new(err)));
+
+ CACHE
+ .write()
+ .map_err(|_| FontError::LockError)?
+ .insert(key.into_owned(), result.clone());
+
+ return result;
+ }
+ Err(make_not_found_error())
+}
+
+/// Remove all cached fonts data.
+#[allow(dead_code)]
+pub fn clear_font_cache() -> FontResult<()> {
+ let mut cache = CACHE.write().map_err(|_| FontError::LockError)?;
+ cache.clear();
+ Ok(())
+}
+
+#[derive(Clone)]
+pub struct FontDataInternal(Font<'static>);
+
+impl FontData for FontDataInternal {
+ type ErrorType = FontError;
+
+ fn new(family: FontFamily, style: FontStyle) -> Result<Self, FontError> {
+ Ok(FontDataInternal(load_font_data(family, style)?))
+ }
+
+ fn estimate_layout(&self, size: f64, text: &str) -> Result<LayoutBox, Self::ErrorType> {
+ let scale = Scale::uniform(size as f32);
+
+ let (mut min_x, mut min_y) = (i32::MAX, i32::MAX);
+ let (mut max_x, mut max_y) = (0, 0);
+
+ let font = &self.0;
+
+ for g in font.layout(text, scale, point(0.0, 0.0)) {
+ if let Some(rect) = g.pixel_bounding_box() {
+ min_x = min_x.min(rect.min.x);
+ min_y = min_y.min(rect.min.y);
+ max_x = max_x.max(rect.max.x);
+ max_y = max_y.max(rect.max.y);
+ }
+ }
+
+ if min_x == i32::MAX || min_y == i32::MAX {
+ return Ok(((0, 0), (0, 0)));
+ }
+
+ Ok(((min_x, min_y), (max_x, max_y)))
+ }
+
+ fn draw<E, DrawFunc: FnMut(i32, i32, f32) -> Result<(), E>>(
+ &self,
+ (base_x, base_y): (i32, i32),
+ size: f64,
+ text: &str,
+ mut draw: DrawFunc,
+ ) -> Result<Result<(), E>, Self::ErrorType> {
+ let scale = Scale::uniform(size as f32);
+ let mut result = Ok(());
+ let font = &self.0;
+
+ for g in font.layout(text, scale, point(0.0, 0.0)) {
+ if let Some(rect) = g.pixel_bounding_box() {
+ let (x0, y0) = (rect.min.x, rect.min.y);
+ g.draw(|x, y, v| {
+ let (x, y) = (x as i32 + x0, y as i32 + y0);
+ result = draw(x + base_x, y + base_y, v);
+ });
+ if result.is_err() {
+ break;
+ }
+ }
+ }
+ Ok(result)
+ }
+}
+
+#[cfg(test)]
+mod test {
+
+ use super::*;
+
+ #[test]
+ fn test_font_cache() -> FontResult<()> {
+ clear_font_cache()?;
+
+ // We cannot only check the size of font cache, because
+ // the test case may be run in parallel. Thus the font cache
+ // may contains other fonts.
+ let _a = load_font_data(FontFamily::Serif, FontStyle::Normal)?;
+ assert!(CACHE.read().unwrap().contains_key("serif"));
+
+ let _b = load_font_data(FontFamily::Serif, FontStyle::Normal)?;
+ assert!(CACHE.read().unwrap().contains_key("serif"));
+
+ // TODO: Check they are the same
+
+ return Ok(());
+ }
+}
diff --git a/src/style/font/web.rs b/src/style/font/web.rs
new file mode 100644
index 0000000..e70e7b1
--- /dev/null
+++ b/src/style/font/web.rs
@@ -0,0 +1,46 @@
+use super::{FontData, FontFamily, FontStyle, LayoutBox};
+use wasm_bindgen::JsCast;
+use web_sys::{window, HtmlElement};
+
+#[derive(Debug, Clone)]
+pub enum FontError {
+ UnknownError,
+}
+
+impl std::fmt::Display for FontError {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+ match self {
+ _ => write!(fmt, "Unknown error"),
+ }
+ }
+}
+
+impl std::error::Error for FontError {}
+
+#[derive(Clone)]
+pub struct FontDataInternal(String, String);
+
+impl FontData for FontDataInternal {
+ type ErrorType = FontError;
+ fn new(family: FontFamily, style: FontStyle) -> Result<Self, FontError> {
+ Ok(FontDataInternal(
+ family.as_str().into(),
+ style.as_str().into(),
+ ))
+ }
+ fn estimate_layout(&self, size: f64, text: &str) -> Result<LayoutBox, Self::ErrorType> {
+ let window = window().unwrap();
+ let document = window.document().unwrap();
+ let body = document.body().unwrap();
+ let span = document.create_element("span").unwrap();
+ span.set_text_content(Some(text));
+ span.set_attribute("style", &format!("display: inline-block; font-family:{}; font-size: {}px; position: fixed; top: 100%", self.0, size)).unwrap();
+ let span = span.into();
+ body.append_with_node_1(&span).unwrap();
+ let elem = JsCast::dyn_into::<HtmlElement>(span).unwrap();
+ let height = elem.offset_height() as i32;
+ let width = elem.offset_width() as i32;
+ elem.remove();
+ Ok(((0, 0), (width, height)))
+ }
+}
diff --git a/src/style/mod.rs b/src/style/mod.rs
new file mode 100644
index 0000000..39c171d
--- /dev/null
+++ b/src/style/mod.rs
@@ -0,0 +1,25 @@
+/*!
+ The style for shapes and text, font, color, etc.
+*/
+mod color;
+pub mod colors;
+mod font;
+mod palette;
+mod shape;
+mod size;
+mod text;
+
+#[cfg(feature = "palette_ext")]
+mod palette_ext;
+
+/// Definitions of palettes of accessibility
+pub use self::palette::*;
+pub use color::{Color, HSLColor, PaletteColor, RGBAColor, RGBColor, SimpleColor};
+pub use colors::{BLACK, BLUE, CYAN, GREEN, MAGENTA, RED, TRANSPARENT, WHITE, YELLOW};
+pub use font::{
+ FontDesc, FontError, FontFamily, FontResult, FontStyle, FontTransform, IntoFont, LayoutBox,
+};
+pub use shape::ShapeStyle;
+pub use size::{AsRelative, RelativeSize, SizeDesc};
+pub use text::text_anchor;
+pub use text::{IntoTextStyle, TextStyle};
diff --git a/src/style/palette.rs b/src/style/palette.rs
new file mode 100644
index 0000000..3b37b43
--- /dev/null
+++ b/src/style/palette.rs
@@ -0,0 +1,63 @@
+use super::color::PaletteColor;
+
+pub trait Palette {
+ const COLORS: &'static [(u8, u8, u8)];
+ fn pick(idx: usize) -> PaletteColor<Self>
+ where
+ Self: Sized,
+ {
+ PaletteColor::<Self>::pick(idx)
+ }
+}
+
+/// The palette of 99% accessibility
+pub struct Palette99;
+/// The palette of 99.99% accessibility
+pub struct Palette9999;
+/// The palette of 100% accessibility
+pub struct Palette100;
+
+impl Palette for Palette99 {
+ const COLORS: &'static [(u8, u8, u8)] = &[
+ (230, 25, 75),
+ (60, 180, 75),
+ (255, 225, 25),
+ (0, 130, 200),
+ (245, 130, 48),
+ (145, 30, 180),
+ (70, 240, 240),
+ (240, 50, 230),
+ (210, 245, 60),
+ (250, 190, 190),
+ (0, 128, 128),
+ (230, 190, 255),
+ (170, 110, 40),
+ (255, 250, 200),
+ (128, 0, 0),
+ (170, 255, 195),
+ (128, 128, 0),
+ (255, 215, 180),
+ (0, 0, 128),
+ (128, 128, 128),
+ (0, 0, 0),
+ ];
+}
+
+impl Palette for Palette9999 {
+ const COLORS: &'static [(u8, u8, u8)] = &[
+ (255, 225, 25),
+ (0, 130, 200),
+ (245, 130, 48),
+ (250, 190, 190),
+ (230, 190, 255),
+ (128, 0, 0),
+ (0, 0, 128),
+ (128, 128, 128),
+ (0, 0, 0),
+ ];
+}
+
+impl Palette for Palette100 {
+ const COLORS: &'static [(u8, u8, u8)] =
+ &[(255, 225, 25), (0, 130, 200), (128, 128, 128), (0, 0, 0)];
+}
diff --git a/src/style/palette_ext.rs b/src/style/palette_ext.rs
new file mode 100644
index 0000000..35e15ff
--- /dev/null
+++ b/src/style/palette_ext.rs
@@ -0,0 +1,136 @@
+use num_traits::Float;
+
+use palette::encoding::Linear;
+use palette::luma::{Luma, LumaStandard};
+use palette::rgb::RgbStandard;
+use palette::rgb::{Rgb, RgbSpace};
+use palette::white_point::D65;
+use palette::{Alpha, Component, Hsl, Hsv, Hwb, Lab, Lch, LinSrgb, Xyz, Yxy};
+
+use super::color::Color;
+
+impl<S: RgbStandard, T: Component> Color for Rgb<S, T> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ self.into_format::<u8>().into_components()
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+impl<S: LumaStandard, T: Component> Color for Luma<S, T> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ let (luma,) = self.into_format::<u8>().into_components();
+ (luma, luma, luma)
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+impl<S: RgbSpace, T: Component + Float> Color for Hsl<S, T> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ Rgb::<Linear<S>, T>::from(*self)
+ .into_format::<u8>()
+ .into_components()
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+impl<S: RgbSpace, T: Component + Float> Color for Hsv<S, T> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ Rgb::<Linear<S>, T>::from(*self)
+ .into_format::<u8>()
+ .into_components()
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+impl<S: RgbSpace, T: Component + Float> Color for Hwb<S, T> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ Rgb::<Linear<S>, T>::from(*self)
+ .into_format::<u8>()
+ .into_components()
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+impl<T: Component + Float> Color for Lab<D65, T> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ LinSrgb::<T>::from(*self)
+ .into_format::<u8>()
+ .into_components()
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+impl<T: Component + Float> Color for Lch<D65, T> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ LinSrgb::<T>::from(*self)
+ .into_format::<u8>()
+ .into_components()
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+impl<T: Component + Float> Color for Xyz<D65, T> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ LinSrgb::<T>::from(*self)
+ .into_format::<u8>()
+ .into_components()
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+impl<T: Component + Float> Color for Yxy<D65, T> {
+ fn rgb(&self) -> (u8, u8, u8) {
+ LinSrgb::<T>::from(*self)
+ .into_format::<u8>()
+ .into_components()
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ 1.0
+ }
+}
+
+impl<C: Color, T: Component> Color for Alpha<C, T> {
+ #[inline]
+ fn rgb(&self) -> (u8, u8, u8) {
+ self.color.rgb()
+ }
+
+ #[inline]
+ fn alpha(&self) -> f64 {
+ self.alpha.convert()
+ }
+}
diff --git a/src/style/shape.rs b/src/style/shape.rs
new file mode 100644
index 0000000..4a56a1d
--- /dev/null
+++ b/src/style/shape.rs
@@ -0,0 +1,38 @@
+use super::color::{Color, RGBAColor};
+
+/// Style for any of shape
+#[derive(Clone)]
+pub struct ShapeStyle {
+ pub color: RGBAColor,
+ pub filled: bool,
+ pub stroke_width: u32,
+}
+
+impl ShapeStyle {
+ /// Make a filled shape style
+ pub fn filled(&self) -> Self {
+ Self {
+ color: self.color.to_rgba(),
+ filled: true,
+ stroke_width: self.stroke_width,
+ }
+ }
+
+ pub fn stroke_width(&self, width: u32) -> Self {
+ Self {
+ color: self.color.to_rgba(),
+ filled: self.filled,
+ stroke_width: width,
+ }
+ }
+}
+
+impl<'a, T: Color> From<&'a T> for ShapeStyle {
+ fn from(f: &'a T) -> Self {
+ ShapeStyle {
+ color: f.to_rgba(),
+ filled: false,
+ stroke_width: 1,
+ }
+ }
+}
diff --git a/src/style/size.rs b/src/style/size.rs
new file mode 100644
index 0000000..22e436b
--- /dev/null
+++ b/src/style/size.rs
@@ -0,0 +1,180 @@
+use crate::coord::CoordTranslate;
+use crate::drawing::DrawingArea;
+use crate::drawing::DrawingBackend;
+
+/// The trait indicates that the type has a dimensional data.
+/// This is the abstraction for the relative sizing model.
+/// A relative sizing value is able to be converted into a concrete size
+/// when coupling with a type with `HasDimension` type.
+pub trait HasDimension {
+ /// Get the dimensional data for this object
+ fn dim(&self) -> (u32, u32);
+}
+
+impl<T: DrawingBackend> HasDimension for T {
+ fn dim(&self) -> (u32, u32) {
+ self.get_size()
+ }
+}
+
+impl<D: DrawingBackend, C: CoordTranslate> HasDimension for DrawingArea<D, C> {
+ fn dim(&self) -> (u32, u32) {
+ self.dim_in_pixel()
+ }
+}
+
+impl HasDimension for (u32, u32) {
+ fn dim(&self) -> (u32, u32) {
+ *self
+ }
+}
+
+/// The trait that describes a size, it may be a relative size which the
+/// size is determined by the parent size, e.g., 10% of the parent width
+pub trait SizeDesc {
+ /// Convert the size into the number of pixels
+ ///
+ /// - `parent`: The reference to the parent container of this size
+ /// - **returns**: The number of pixels
+ fn in_pixels<T: HasDimension>(&self, parent: &T) -> i32;
+}
+
+impl SizeDesc for i32 {
+ fn in_pixels<D: HasDimension>(&self, _parent: &D) -> i32 {
+ *self
+ }
+}
+
+impl SizeDesc for u32 {
+ fn in_pixels<D: HasDimension>(&self, _parent: &D) -> i32 {
+ *self as i32
+ }
+}
+
+/// Describes a relative size, might be
+/// 1. portion of height
+/// 2. portion of width
+/// 3. portion of the minimal of height and weight
+pub enum RelativeSize {
+ /// Percentage height
+ Height(f64),
+ /// Percentage width
+ Width(f64),
+ /// Percentage of either height or width, which is smaller
+ Smaller(f64),
+}
+
+impl RelativeSize {
+ /// Set the lower bound of the relative size.
+ ///
+ /// - `min_sz`: The minimal size the relative size can be in pixels
+ /// - **returns**: The relative size with the bound
+ pub fn min(self, min_sz: i32) -> RelativeSizeWithBound {
+ RelativeSizeWithBound {
+ size: self,
+ min: Some(min_sz),
+ max: None,
+ }
+ }
+
+ /// Set the upper bound of the relative size
+ ///
+ /// - `max_size`: The maximum size in pixels for this relative size
+ /// - **returns** The relative size with the upper bound
+ pub fn max(self, max_sz: i32) -> RelativeSizeWithBound {
+ RelativeSizeWithBound {
+ size: self,
+ max: Some(max_sz),
+ min: None,
+ }
+ }
+}
+
+impl SizeDesc for RelativeSize {
+ fn in_pixels<D: HasDimension>(&self, parent: &D) -> i32 {
+ let (w, h) = parent.dim();
+ match self {
+ RelativeSize::Width(p) => *p * f64::from(w),
+ RelativeSize::Height(p) => *p * f64::from(h),
+ RelativeSize::Smaller(p) => *p * f64::from(w.min(h)),
+ }
+ .round() as i32
+ }
+}
+
+/// Allows a value turns into a relative size
+pub trait AsRelative: Into<f64> {
+ /// Make the value a relative size of percentage of width
+ fn percent_width(self) -> RelativeSize {
+ RelativeSize::Width(self.into() / 100.0)
+ }
+ /// Make the value a relative size of percentage of height
+ fn percent_height(self) -> RelativeSize {
+ RelativeSize::Height(self.into() / 100.0)
+ }
+ /// Make the value a relative size of percentage of minimal of height and width
+ fn percent(self) -> RelativeSize {
+ RelativeSize::Smaller(self.into() / 100.0)
+ }
+}
+
+impl<T: Into<f64>> AsRelative for T {}
+
+/// The struct describes a relative size with upper bound and lower bound
+pub struct RelativeSizeWithBound {
+ size: RelativeSize,
+ min: Option<i32>,
+ max: Option<i32>,
+}
+
+impl RelativeSizeWithBound {
+ /// Set the lower bound of the bounded relative size
+ ///
+ /// - `min_sz`: The lower bound of this size description
+ /// - **returns**: The newly created size description with the bound
+ pub fn min(mut self, min_sz: i32) -> RelativeSizeWithBound {
+ self.min = Some(min_sz);
+ self
+ }
+
+ /// Set the upper bound of the bounded relative size
+ ///
+ /// - `min_sz`: The upper bound of this size description
+ /// - **returns**: The newly created size description with the bound
+ pub fn max(mut self, max_sz: i32) -> RelativeSizeWithBound {
+ self.max = Some(max_sz);
+ self
+ }
+}
+
+impl SizeDesc for RelativeSizeWithBound {
+ fn in_pixels<D: HasDimension>(&self, parent: &D) -> i32 {
+ let size = self.size.in_pixels(parent);
+ let size_lower_capped = self.min.map_or(size, |x| x.max(size));
+ self.max.map_or(size_lower_capped, |x| x.min(size))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[test]
+ fn test_relative_size() {
+ let size = (10).percent_height();
+ assert_eq!(size.in_pixels(&(100, 200)), 20);
+
+ let size = (10).percent_width();
+ assert_eq!(size.in_pixels(&(100, 200)), 10);
+
+ let size = (-10).percent_width();
+ assert_eq!(size.in_pixels(&(100, 200)), -10);
+
+ let size = (10).percent_width().min(30);
+ assert_eq!(size.in_pixels(&(100, 200)), 30);
+ assert_eq!(size.in_pixels(&(400, 200)), 40);
+
+ let size = (10).percent();
+ assert_eq!(size.in_pixels(&(100, 200)), 10);
+ assert_eq!(size.in_pixels(&(400, 200)), 20);
+ }
+}
diff --git a/src/style/text.rs b/src/style/text.rs
new file mode 100644
index 0000000..a609767
--- /dev/null
+++ b/src/style/text.rs
@@ -0,0 +1,220 @@
+use super::color::{Color, RGBAColor};
+use super::font::{FontDesc, FontFamily, FontStyle, FontTransform};
+use super::size::{HasDimension, SizeDesc};
+use super::BLACK;
+
+/// Text anchor attributes are used to properly position the text.
+///
+/// # Examples
+///
+/// In the example below, the text anchor (X) position is `Pos::new(HPos::Right, VPos::Center)`.
+/// ```text
+/// ***** X
+/// ```
+/// The position is always relative to the text regardless of its rotation.
+/// In the example below, the text has style
+/// `style.transform(FontTransform::Rotate90).pos(Pos::new(HPos::Center, VPos::Top))`.
+/// ```text
+/// *
+/// *
+/// * X
+/// *
+/// *
+/// ```
+pub mod text_anchor {
+ /// The horizontal position of the anchor point relative to the text.
+ #[derive(Clone, Copy)]
+ pub enum HPos {
+ /// Anchor point is on the left side of the text
+ Left,
+ /// Anchor point is on the right side of the text
+ Right,
+ /// Anchor point is in the horizontal center of the text
+ Center,
+ }
+
+ /// The vertical position of the anchor point relative to the text.
+ #[derive(Clone, Copy)]
+ pub enum VPos {
+ /// Anchor point is on the top of the text
+ Top,
+ /// Anchor point is in the vertical center of the text
+ Center,
+ /// Anchor point is on the bottom of the text
+ Bottom,
+ }
+
+ /// The text anchor position.
+ #[derive(Clone, Copy)]
+ pub struct Pos {
+ /// The horizontal position of the anchor point
+ pub h_pos: HPos,
+ /// The vertical position of the anchor point
+ pub v_pos: VPos,
+ }
+
+ impl Pos {
+ /// Create a new text anchor position.
+ ///
+ /// - `h_pos`: The horizontal position of the anchor point
+ /// - `v_pos`: The vertical position of the anchor point
+ /// - **returns** The newly created text anchor position
+ ///
+ /// ```rust
+ /// use plotters::style::text_anchor::{Pos, HPos, VPos};
+ ///
+ /// let pos = Pos::new(HPos::Left, VPos::Top);
+ /// ```
+ pub fn new(h_pos: HPos, v_pos: VPos) -> Self {
+ Pos { h_pos, v_pos }
+ }
+
+ /// Create a default text anchor position (top left).
+ ///
+ /// - **returns** The default text anchor position
+ ///
+ /// ```rust
+ /// use plotters::style::text_anchor::{Pos, HPos, VPos};
+ ///
+ /// let pos = Pos::default();
+ /// ```
+ pub fn default() -> Self {
+ Pos {
+ h_pos: HPos::Left,
+ v_pos: VPos::Top,
+ }
+ }
+ }
+}
+
+/// Style of a text
+#[derive(Clone)]
+pub struct TextStyle<'a> {
+ /// The font description
+ pub font: FontDesc<'a>,
+ /// The text color
+ pub color: RGBAColor,
+ /// The anchor point position
+ pub pos: text_anchor::Pos,
+}
+
+pub trait IntoTextStyle<'a> {
+ fn into_text_style<P: HasDimension>(self, parent: &P) -> TextStyle<'a>;
+}
+
+impl<'a> IntoTextStyle<'a> for FontDesc<'a> {
+ fn into_text_style<P: HasDimension>(self, _: &P) -> TextStyle<'a> {
+ self.into()
+ }
+}
+
+impl<'a> IntoTextStyle<'a> for TextStyle<'a> {
+ fn into_text_style<P: HasDimension>(self, _: &P) -> TextStyle<'a> {
+ self
+ }
+}
+
+impl<'a> IntoTextStyle<'a> for FontFamily<'a> {
+ fn into_text_style<P: HasDimension>(self, _: &P) -> TextStyle<'a> {
+ self.into()
+ }
+}
+
+impl<'a, T: SizeDesc> IntoTextStyle<'a> for (&'a str, T) {
+ fn into_text_style<P: HasDimension>(self, parent: &P) -> TextStyle<'a> {
+ (self.0, self.1.in_pixels(parent)).into()
+ }
+}
+
+impl<'a, T: SizeDesc> IntoTextStyle<'a> for (FontFamily<'a>, T) {
+ fn into_text_style<P: HasDimension>(self, parent: &P) -> TextStyle<'a> {
+ (self.0, self.1.in_pixels(parent)).into()
+ }
+}
+
+impl<'a, T: SizeDesc> IntoTextStyle<'a> for (&'a str, T, FontStyle) {
+ fn into_text_style<P: HasDimension>(self, parent: &P) -> TextStyle<'a> {
+ Into::<FontDesc>::into((self.0, self.1.in_pixels(parent), self.2)).into()
+ }
+}
+
+impl<'a, T: SizeDesc> IntoTextStyle<'a> for (FontFamily<'a>, T, FontStyle) {
+ fn into_text_style<P: HasDimension>(self, parent: &P) -> TextStyle<'a> {
+ Into::<FontDesc>::into((self.0, self.1.in_pixels(parent), self.2)).into()
+ }
+}
+
+impl<'a> TextStyle<'a> {
+ /// Sets the color of the style.
+ ///
+ /// - `color`: The required color
+ /// - **returns** The up-to-dated text style
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let style = TextStyle::from(("sans-serif", 20).into_font()).color(&RED);
+ /// ```
+ pub fn color<C: Color>(&self, color: &'a C) -> Self {
+ Self {
+ font: self.font.clone(),
+ color: color.to_rgba(),
+ pos: self.pos,
+ }
+ }
+
+ /// Sets the font transformation of the style.
+ ///
+ /// - `trans`: The required transformation
+ /// - **returns** The up-to-dated text style
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ ///
+ /// let style = TextStyle::from(("sans-serif", 20).into_font()).transform(FontTransform::Rotate90);
+ /// ```
+ pub fn transform(&self, trans: FontTransform) -> Self {
+ Self {
+ font: self.font.clone().transform(trans),
+ color: self.color.clone(),
+ pos: self.pos,
+ }
+ }
+
+ /// Sets the anchor position.
+ ///
+ /// - `pos`: The required anchor position
+ /// - **returns** The up-to-dated text style
+ ///
+ /// ```rust
+ /// use plotters::prelude::*;
+ /// use plotters::style::text_anchor::{Pos, HPos, VPos};
+ ///
+ /// let pos = Pos::new(HPos::Left, VPos::Top);
+ /// let style = TextStyle::from(("sans-serif", 20).into_font()).pos(pos);
+ /// ```
+ pub fn pos(&self, pos: text_anchor::Pos) -> Self {
+ Self {
+ font: self.font.clone(),
+ color: self.color.clone(),
+ pos,
+ }
+ }
+}
+
+/// Make sure that we are able to automatically copy the `TextStyle`
+impl<'a, 'b: 'a> Into<TextStyle<'a>> for &'b TextStyle<'a> {
+ fn into(self) -> TextStyle<'a> {
+ self.clone()
+ }
+}
+
+impl<'a, T: Into<FontDesc<'a>>> From<T> for TextStyle<'a> {
+ fn from(font: T) -> Self {
+ Self {
+ font: font.into(),
+ color: BLACK.to_rgba(),
+ pos: text_anchor::Pos::default(),
+ }
+ }
+}