├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile.toml ├── README.md ├── examples ├── Cargo.toml ├── Makefile.toml ├── demo │ ├── Cargo.toml │ ├── Makefile.toml │ └── src │ │ ├── input.scss │ │ └── lib.rs ├── desktop │ ├── Cargo.toml │ ├── Makefile.toml │ ├── index.html │ ├── src │ │ ├── input.css │ │ └── main.rs │ └── tailwind.config.js └── web │ ├── Cargo.toml │ ├── Makefile.toml │ ├── index.html │ ├── src │ ├── input.css │ └── main.rs │ └── tailwind.config.js ├── release.toml └── src ├── charts ├── bar.rs ├── line.rs └── pie.rs ├── grid.rs ├── lib.rs ├── types.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | debug/ 2 | target/ 3 | **/dist/ 4 | 5 | **/Cargo.lock 6 | **/*.css 7 | **/*.log 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | 8 | 9 | ## [Unreleased] - ReleaseDate 10 | 11 | ## [0.1.3] - 2023-11-06 12 | 13 | ### Fixed 14 | 15 | - Updated to Dioxus 0.4.0 16 | 17 | ## [0.1.2] - 2023-06-03 18 | 19 | ### Fixed 20 | 21 | - Updated to Dioxus 0.3.2 22 | 23 | ## [0.1.1] - 2023-02-17 24 | 25 | ### Added 26 | 27 | - This changelog 28 | - Cargo release config 29 | - README instructions 30 | 31 | ### Fixed 32 | 33 | - Updated to Dioxus 0.3.1 34 | 35 | ## 0.1.0 - 2022-12-18 36 | 37 | ### Added 38 | 39 | - Initial commit 40 | 41 | 42 | [Unreleased]: https://github.com/hiltonm/dioxus-charts/compare/v0.1.3...HEAD 43 | [0.1.3]: https://github.com/hiltonm/dioxus-charts/compare/v0.1.2...v0.1.3 44 | [0.1.2]: https://github.com/hiltonm/dioxus-charts/compare/v0.1.1...v0.1.2 45 | [0.1.1]: https://github.com/hiltonm/dioxus-charts/compare/v0.1.0...v0.1.1 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dioxus-charts" 3 | version = "0.3.1" 4 | authors = ["Hilton Medeiros"] 5 | edition = "2021" 6 | description = "Chart components library for Dioxus" 7 | repository = "https://github.com/dioxus-community/dioxus-charts" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["ui", "gui", "react", "wasm", "dioxus"] 10 | categories = ["wasm", "gui", "web-programming"] 11 | 12 | [dependencies] 13 | log = "0.4" 14 | dioxus = { version = "0.6", default-features = false, features = ["launch", "macro", "html", "signals"] } 15 | 16 | [profile.release] 17 | lto = true 18 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hilton Medeiros 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without 9 | limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | 2 | [tasks.format] 3 | install_crate = "rustfmt" 4 | command = "cargo" 5 | args = ["fmt", "--", "--emit=files"] 6 | 7 | [tasks.build] 8 | command = "cargo" 9 | args = ["build"] 10 | 11 | [tasks.lint] 12 | command = "cargo" 13 | args = ["clippy"] 14 | 15 | [tasks.dev] 16 | dependencies = [ 17 | "format", 18 | "lint", 19 | "build", 20 | ] 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Discord Server](https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square)](https://discord.gg/sKJSVNSCDJ) 2 | 3 | # dioxus-charts 📊 4 | 5 | A simple chart components library for [Dioxus](https://dioxuslabs.com/). 6 | 7 | This crate provides some basic SVG-based chart components, customizable with 8 | CSS, to be used with the Dioxus GUI library. The 9 | components configuration was designed to be similar to what one would find 10 | in JavaScript chart libraries. 11 | 12 | The components available currently are: 13 | 14 | - `PieChart`: for Pie, Donut and Gauge charts 15 | - `BarChart`: for Bar and Stacked Bar charts, vertical or horizontal 16 | - `LineChart` 17 | 18 | You can check them out at the very simple [demo site](https://hiltonm.github.io/dioxus-charts-demo/) 19 | for now. 20 | 21 | ## Usage 22 | 23 | This crate is [on crates.io](https://crates.io/crates/dioxus-charts) and can be 24 | used by adding `dioxus-charts` to your dependencies in your project's `Cargo.toml`. 25 | 26 | ```toml 27 | [dependencies] 28 | dioxus-charts = "0.3" 29 | ``` 30 | 31 | ## Example 32 | 33 | ```rust 34 | use dioxus::prelude::*; 35 | use dioxus_charts::BarChart; 36 | 37 | fn app() -> Element { 38 | rsx!( 39 | BarChart { 40 | padding_top: 30, 41 | padding_left: 70, 42 | padding_right: 50, 43 | padding_bottom: 30, 44 | bar_width: "10%", 45 | horizontal_bars: true, 46 | label_interpolation: (|v| format!("{v}%")) as fn(f32) -> String, 47 | series: vec![ 48 | vec![63.0, 14.4, 8.0, 5.1, 1.8], 49 | ], 50 | labels: vec!["Chrome".into(), "Safari".into(), "IE/Edge".into(), "Firefox".into(), "Opera".into()] 51 | } 52 | ) 53 | } 54 | ``` 55 | 56 | There is also a couple of examples in the `examples` folder with a `Makefile.toml` that makes it easier 57 | to build them. You need to install cargo-make first to make use of them: 58 | 59 | ```sh 60 | cargo install cargo-make 61 | ``` 62 | 63 | You will also need to have [`sass`](https://sass-lang.com/) and [`tailwindcss`](https://tailwindcss.com/) 64 | installed in your system for the make commands to generate the css files. 65 | 66 | Then for the desktop demo, inside the examples folder: 67 | 68 | ```sh 69 | cd examples 70 | cargo make desktop 71 | ``` 72 | 73 | The web example was used to generate the [demo site](https://hiltonm.github.io/dioxus-charts-demo/). 74 | To test it out yourself you need to have `trunk` for the dev-server and the rust wasm target installed: 75 | 76 | ```sh 77 | cargo install trunk 78 | rustup target add wasm32-unknown-unknown 79 | ``` 80 | 81 | Then build and launch the dev-server inside the examples folder: 82 | 83 | ```sh 84 | cargo make web 85 | ``` 86 | 87 | Note: if you get hit by an error when the web example launches, its possible you were blessed by a version 88 | mismatch issue caused by the rustwasm tooling getting out of sync. The simplest fix for that seems to be 89 | to just remove the Cargo.lock file from the `examples/web` folder. Check 90 | [this issue](https://github.com/rustwasm/wasm-bindgen/issues/2776) for more info if that doesn't do it. 91 | 92 | Please check out the [Dioxus reference guide](https://dioxuslabs.com/learn/0.6/reference) for more 93 | information. 94 | 95 | ## License 96 | 97 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or 98 | [MIT License](LICENSE-MIT) at your option. 99 | 100 | Unless you explicitly state otherwise, any contribution intentionally submitted 101 | for inclusion in Dioxus-Charts by you, as defined in the Apache-2.0 license, shall be 102 | dual licensed as above, without any additional terms or conditions. 103 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "demo", 4 | "web", 5 | "desktop", 6 | ] 7 | -------------------------------------------------------------------------------- /examples/Makefile.toml: -------------------------------------------------------------------------------- 1 | 2 | [tasks.desktop] 3 | workspace = false 4 | env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = ["web"] } 5 | run_task = { name = "dev", fork = true } 6 | 7 | [tasks.web] 8 | workspace = false 9 | env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = ["desktop"] } 10 | run_task = { name = "dev", fork = true } 11 | 12 | -------------------------------------------------------------------------------- /examples/demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "charts-demo" 3 | version = "0.1.0" 4 | authors = ["Hilton Medeiros"] 5 | edition = "2021" 6 | description = "A dioxus-charts element demo" 7 | license = "MIT OR Apache-2.0" 8 | publish = false 9 | 10 | [dependencies] 11 | dioxus = { version = "0.6", default-features = false, features = ["launch", "macro", "html"] } 12 | dioxus-charts = { path = "../.." } 13 | 14 | -------------------------------------------------------------------------------- /examples/demo/Makefile.toml: -------------------------------------------------------------------------------- 1 | 2 | [tasks.format] 3 | install_crate = "rustfmt" 4 | command = "cargo" 5 | args = ["fmt", "--", "--emit=files"] 6 | 7 | [tasks.css] 8 | command = "sass" 9 | args = ["--no-source-map", "src/input.scss", "src/custom.css"] 10 | 11 | [tasks.build] 12 | command = "cargo" 13 | args = ["build"] 14 | dependencies = [ 15 | "format", 16 | "css", 17 | ] 18 | 19 | [tasks.dev] 20 | dependencies = [ 21 | "format", 22 | "css", 23 | ] 24 | 25 | -------------------------------------------------------------------------------- /examples/demo/src/input.scss: -------------------------------------------------------------------------------- 1 | 2 | $col1: #8f327b; 3 | $col2: #b33473; 4 | $col3: #d03e65; 5 | $col4: #e55253; 6 | $col5: #f06d3e; 7 | $col6: #f18c27; 8 | $col7: #e8ab0c; 9 | $col8: #d6ca0b; 10 | 11 | 12 | g.dx-series-0 > path { fill: $col1; } 13 | g.dx-series-1 > path { fill: $col2; } 14 | g.dx-series-2 > path { fill: $col3; } 15 | g.dx-series-3 > path { fill: $col4; } 16 | g.dx-series-4 > path { fill: $col5; } 17 | g.dx-series-5 > path { fill: $col6; } 18 | g.dx-series-6 > path { fill: $col7; } 19 | g.dx-series-7 > path { fill: $col8; } 20 | 21 | g.dx-bar-group-0 > line { stroke: $col1; } 22 | g.dx-bar-group-1 > line { stroke: $col2; } 23 | g.dx-bar-group-2 > line { stroke: $col3; } 24 | g.dx-bar-group-3 > line { stroke: $col4; } 25 | g.dx-bar-group-4 > line { stroke: $col5; } 26 | g.dx-bar-group-5 > line { stroke: $col6; } 27 | g.dx-bar-group-6 > line { stroke: $col7; } 28 | g.dx-bar-group-7 > line { stroke: $col8; } 29 | 30 | g.dx-line-0 > line, g.dx-line-0 > path { stroke: $col1; } 31 | g.dx-line-1 > line, g.dx-line-1 > path { stroke: $col2; } 32 | g.dx-line-2 > line, g.dx-line-2 > path { stroke: $col3; } 33 | g.dx-line-3 > line, g.dx-line-3 > path { stroke: $col4; } 34 | g.dx-line-4 > line, g.dx-line-4 > path { stroke: $col5; } 35 | g.dx-line-5 > line, g.dx-line-5 > path { stroke: $col6; } 36 | g.dx-line-6 > line, g.dx-line-6 > path { stroke: $col7; } 37 | g.dx-line-7 > line, g.dx-line-7 > path { stroke: $col8; } 38 | 39 | -------------------------------------------------------------------------------- /examples/demo/src/lib.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use dioxus_charts::charts::pie::LabelPosition; 4 | use dioxus_charts::{BarChart, LineChart, PieChart}; 5 | 6 | pub fn demo_element() -> Element { 7 | rsx! { 8 | style { 9 | {include_str!("./custom.css")}, 10 | }, 11 | div { 12 | class: "bg-gray-600", 13 | div { 14 | class: "mx-auto max-w-2xl py-8 px-4 sm:py-10 sm:px-6 lg:max-w-7xl lg:px-8 space-y-4", 15 | h2 { 16 | class: "text-2xl font-bold tracking-tight text-gray-200", 17 | "Examples" 18 | } 19 | 20 | div { 21 | class: "grid grid-cols-1 gap-y-12 gap-x-6 sm:grid-cols-2 lg:grid-cols-3 xl:gap-x-8", 22 | 23 | div { 24 | class: "inline-grid grid-cols-1 gap-y-2", 25 | div { 26 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 27 | BarChart { 28 | padding_top: 30, 29 | padding_left: 70, 30 | padding_right: 50, 31 | padding_bottom: 30, 32 | bar_width: "10%", 33 | horizontal_bars: true, 34 | label_interpolation: (|v| format!("{v}%")) as fn(f32) -> String, 35 | series: vec![ 36 | vec![63.0, 14.4, 8.0, 5.1, 1.8], 37 | ], 38 | labels: vec!["Chrome".into(), "Safari".into(), "IE/Edge".into(), "Firefox".into(), "Opera".into()] 39 | } 40 | } 41 | h3 { 42 | class: "truncate tracking-tight text-sm text-gray-200", 43 | "Global desktop browser market share for 2022" 44 | } 45 | } 46 | 47 | div { 48 | class: "inline-grid grid-cols-1 gap-y-2", 49 | div { 50 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 51 | BarChart { 52 | padding_top: 30, 53 | padding_left: 80, 54 | padding_right: 60, 55 | padding_bottom: 30, 56 | bar_distance: 25.0, 57 | horizontal_bars: true, 58 | label_size: 70, 59 | label_interpolation: (|v| format!("${v:.0}B")) as fn(f32) -> String, 60 | series: vec![ 61 | vec![2901.0, 2522.0, 1917.0, 1691.0, 1061.0], 62 | vec![2307.0, 1640.0, 1125.0, 939.8, 668.8], 63 | ], 64 | labels: vec!["Apple".into(), "Microsoft".into(), "Alphabet".into(), "Amazon".into(), "Tesla".into()], 65 | } 66 | } 67 | h3 { 68 | class: "truncate tracking-tight text-sm text-gray-200", 69 | "Market value of top tech companies [Jan-Nov 2022]" 70 | } 71 | } 72 | 73 | div { 74 | class: "inline-grid grid-cols-1 gap-y-2", 75 | div { 76 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 77 | BarChart { 78 | padding_top: 30, 79 | padding_left: 50, 80 | padding_right: 30, 81 | padding_bottom: 40, 82 | bar_width: "3.5%", 83 | bar_distance: 20.0, 84 | show_series_labels: false, 85 | label_interpolation: (|v| format!("{v}%")) as fn(f32) -> String, 86 | label_size: 70, 87 | series: vec![ 88 | vec![11.3, 4.3, 2.6, 1.7, 0.3], 89 | vec![13.7, 6.1, 4.1, 2.3, 0.7], 90 | vec![15.3, 6.9, 4.8, 3.2, 0.9], 91 | ], 92 | labels: vec!["Asia".into(), "Western Europe".into(), "USA".into(), "Eastern Europe".into(), "Latin America".into()], 93 | } 94 | } 95 | h3 { 96 | class: "truncate tracking-tight text-sm text-gray-200", 97 | "Ecommerce share of FMCG value, 2019-2021" 98 | } 99 | } 100 | 101 | div { 102 | class: "inline-grid grid-cols-1 gap-y-2", 103 | div { 104 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 105 | BarChart { 106 | padding_top: 30, 107 | padding_left: 40, 108 | padding_right: 30, 109 | padding_bottom: 30, 110 | label_interpolation: (|v| format!("${v}")) as fn(f32) -> String, 111 | show_series_labels: false, 112 | stacked_bars: true, 113 | series: vec![ 114 | vec![32.0, 24.0, 19.0, 18.5, 11.5], 115 | vec![9.5, 11.0, 17.0, 5.5, 2.5], 116 | vec![4.0, 3.5, 2.5, 5.0, 1.9], 117 | vec![38.5, 24.0, 19.0, 15.0, 2.3], 118 | ], 119 | labels: vec!["Miami".into(), "Barcelona".into(), "Tokyo".into(), "Rio de Janeiro".into(), "Mexico City".into()], 120 | } 121 | } 122 | h3 { 123 | class: "truncate tracking-tight text-sm text-gray-200", 124 | "Date night cost (2 long drinks, taxi, big mac and club entry)" 125 | } 126 | } 127 | 128 | div { 129 | class: "inline-grid grid-cols-1 gap-y-2", 130 | div { 131 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 132 | LineChart { 133 | padding_top: 30, 134 | padding_left: 65, 135 | padding_right: 80, 136 | padding_bottom: 30, 137 | label_interpolation: (|v| format!("${v}B")) as fn(f32) -> String, 138 | series: vec![ 139 | vec![29.0, 30.5, 32.6, 35.0, 37.5], 140 | vec![20.0, 25.1, 26.0, 25.2, 26.6], 141 | vec![18.0, 21.0, 22.5, 24.0, 25.1], 142 | vec![12.5, 17.0, 19.3, 20.1, 21.0], 143 | ], 144 | labels: vec!["2020A".into(), "2021E".into(), "2022E".into(), "2023E".into(), "2024E".into()], 145 | series_labels: vec!["Disney".into(), "Comcast".into(), "Warner".into(), "Netflix".into()], 146 | } 147 | } 148 | h3 { 149 | class: "truncate tracking-tight text-sm text-gray-200", 150 | "Content spending at major media and tech companies" 151 | } 152 | } 153 | 154 | div { 155 | class: "inline-grid grid-cols-1 gap-y-2", 156 | div { 157 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 158 | LineChart { 159 | width: "100%", 160 | height: "100%", 161 | padding_top: 30, 162 | padding_left: 50, 163 | padding_right: 90, 164 | padding_bottom: 30, 165 | show_grid_ticks: true, 166 | show_dotted_grid: false, 167 | label_interpolation: (|v| format!("{v}%")) as fn(f32) -> String, 168 | series: vec![ 169 | vec![75.77, 73.95, 74.56, 78.25, 77.15, 62.64, 67.51], 170 | vec![57.17, 57.78, 54.69, 52.95, 51.78, 41.0, 47.25], 171 | vec![23.12, 26.5, 26.1, 29.84, 25.05, 20.41, 20.1], 172 | vec![26.02, 21.48, 21.05, 22.64, 20.64, 17.19, 16.31], 173 | vec![0.0, 13.65, 12.3, 13.35, 11.17, 9.87, 10.15], 174 | ], 175 | labels: vec!["2016".into(), "2017".into(), "2018".into(), "2019".into(), "2020".into(), "2021".into(), "2022".into()], 176 | series_labels: vec!["Firefox".into(), "Chromium".into(), "Chrome".into(), "Epiphany".into(), "Konqueror".into()], 177 | } 178 | } 179 | h3 { 180 | class: "truncate tracking-tight text-sm text-gray-200", 181 | "Arch Linux browser package relative usage" 182 | } 183 | } 184 | 185 | div { 186 | class: "inline-grid grid-cols-1 gap-y-2", 187 | div { 188 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 189 | PieChart { 190 | width: "100%", 191 | height: "100%", 192 | start_angle: -60.0, 193 | label_position: LabelPosition::Outside, 194 | label_offset: 35.0, 195 | padding: 20.0, 196 | series: vec![59.54, 17.2, 9.59, 7.6, 5.53, 0.55], 197 | labels: vec!["Asia".into(), "Africa".into(), "Europe".into(), "N. America".into(), "S. America".into(), "Oceania".into()], 198 | } 199 | } 200 | h3 { 201 | class: "truncate tracking-tight text-sm text-gray-200", 202 | "World population share by continents 2022" 203 | } 204 | } 205 | 206 | div { 207 | class: "inline-grid grid-cols-1 gap-y-2", 208 | div { 209 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 210 | PieChart { 211 | width: "100%", 212 | height: "100%", 213 | start_angle: 50.0, 214 | label_position: LabelPosition::Outside, 215 | label_offset: 27.0, 216 | donut: true, 217 | padding: 20.0, 218 | series: vec![5.0, 4.0, 4.0, 2.0, 2.0, 2.0, 1.0, 1.0], 219 | labels: vec!["Brazil".into(), "Germany".into(), "Italy".into(), "France".into(), "Uruguay".into(), "Argentina".into(), "England".into(), "Spain".into()], 220 | } 221 | } 222 | h3 { 223 | class: "truncate tracking-tight text-sm text-gray-200", 224 | "World Cup titles at 2022" 225 | } 226 | } 227 | 228 | div { 229 | class: "inline-grid grid-cols-1 gap-y-2", 230 | div { 231 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 232 | PieChart { 233 | width: "100%", 234 | height: "100%", 235 | viewbox_width: 500, 236 | viewbox_height: 300, 237 | start_angle: 270.0, 238 | show_ratio: 0.5, 239 | donut: true, 240 | label_position: LabelPosition::Center, 241 | label_offset: 60.0, 242 | label_interpolation: (|v| format!("{v}%")) as fn(f32) -> String, 243 | series: vec![50.0, 25.0, 25.0], 244 | } 245 | } 246 | h3 { 247 | class: "truncate tracking-tight text-sm text-gray-200", 248 | "Gauge Chart" 249 | } 250 | } 251 | 252 | div { 253 | class: "inline-grid grid-cols-1 gap-y-2", 254 | div { 255 | class: "aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-lg bg-gray-300 xl:aspect-w-7 xl:aspect-h-8", 256 | BarChart { 257 | padding_top: 30, 258 | padding_left: 100, 259 | padding_right: 70, 260 | padding_bottom: 30, 261 | bar_width: "6%", 262 | horizontal_bars: true, 263 | viewbox_width: 600, 264 | viewbox_height: 500, 265 | show_grid_ticks: true, 266 | show_dotted_grid: false, 267 | label_size: 90, 268 | label_interpolation: (|v| format!("{v}km²")) as fn(f32) -> String, 269 | series: vec![ 270 | vec![150.0, 22.3, 13.5, 12.7, 3.7, 2.78, 2.5, 1.47], 271 | ], 272 | labels: vec![ 273 | "Walt Disney Orlando, USA".into(), 274 | "Disneyland Paris, France".into(), 275 | "Beto Carrero, Brazil".into(), 276 | "Disneyland Anaheim, USA".into(), 277 | "Alton Towers, UK".into(), 278 | "Universal Orlando, USA".into(), 279 | "Tokyo Disney, Japan".into(), 280 | "Cedar Point, USA".into(), 281 | ] 282 | } 283 | } 284 | h3 { 285 | class: "truncate tracking-tight text-sm text-gray-200", 286 | "8 biggest amusement parks in the world" 287 | } 288 | } 289 | } 290 | } 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /examples/desktop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "charts-desktop-demo" 3 | version = "0.1.0" 4 | authors = ["Hilton Medeiros"] 5 | edition = "2021" 6 | description = "A dioxus-charts desktop demo" 7 | license = "MIT OR Apache-2.0" 8 | publish = false 9 | 10 | [dependencies] 11 | log = "0.4" 12 | env_logger = "0.10" 13 | charts-demo = { path = "../demo" } 14 | dioxus = { version = "0.6", features = ["desktop"] } 15 | -------------------------------------------------------------------------------- /examples/desktop/Makefile.toml: -------------------------------------------------------------------------------- 1 | 2 | [tasks.format] 3 | install_crate = "rustfmt" 4 | command = "cargo" 5 | args = ["fmt", "--", "--emit=files"] 6 | 7 | [tasks.run] 8 | command = "cargo" 9 | args = ["run"] 10 | 11 | [tasks.css] 12 | command = "npx" 13 | args = ["tailwindcss", "-i", "src/input.css", "-o", "src/tailwind.css"] 14 | 15 | [tasks.dev] 16 | dependencies = [ 17 | "format", 18 | "css", 19 | "run", 20 | ] 21 | -------------------------------------------------------------------------------- /examples/desktop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/desktop/src/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/desktop/src/main.rs: -------------------------------------------------------------------------------- 1 | use dioxus::{ 2 | desktop::{Config, WindowBuilder}, 3 | prelude::*, 4 | }; 5 | 6 | fn main() { 7 | env_logger::init(); 8 | 9 | let config = Config::new() 10 | .with_custom_head(format!("", include_str!("./tailwind.css"))) 11 | .with_window(WindowBuilder::new().with_title("Dioxus Charts Examples")); 12 | 13 | let builder = LaunchBuilder::desktop().with_cfg(config); 14 | builder.launch(charts_demo::demo_element); 15 | } 16 | -------------------------------------------------------------------------------- /examples/desktop/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/*.rs", 4 | "../demo/src/**/*.rs", 5 | "./index.html", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /examples/web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "charts-web-demo" 3 | version = "0.1.0" 4 | authors = ["Hilton Medeiros"] 5 | edition = "2021" 6 | description = "A dioxus-charts web demo" 7 | license = "MIT OR Apache-2.0" 8 | publish = false 9 | 10 | [dependencies] 11 | log = "0.4" 12 | env_logger = "0.10" 13 | dioxus = { version = "0.6", features = ["web"] } 14 | charts-demo = { path = "../demo" } 15 | -------------------------------------------------------------------------------- /examples/web/Makefile.toml: -------------------------------------------------------------------------------- 1 | 2 | [tasks.format] 3 | install_crate = "rustfmt" 4 | command = "cargo" 5 | args = ["fmt", "--", "--emit=files"] 6 | 7 | [tasks.run] 8 | command = "trunk" 9 | args = ["serve"] 10 | 11 | [tasks.css] 12 | command = "npx" 13 | args = ["tailwindcss", "-i", "src/input.css", "-o", "src/tailwind.css"] 14 | 15 | [tasks.dev] 16 | dependencies = [ 17 | "format", 18 | "css", 19 | "run", 20 | ] 21 | -------------------------------------------------------------------------------- /examples/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/web/src/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/web/src/main.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | env_logger::init(); 5 | launch(charts_demo::demo_element); 6 | } 7 | -------------------------------------------------------------------------------- /examples/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/*.rs", 4 | "../demo/src/**/*.rs", 5 | "./index.html", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | allow-branch = ["master"] 2 | pre-release-replacements = [ 3 | {file="README.md", search="dioxus-charts = .*", replace="{{crate_name}} = \"{{version}}\""}, 4 | {file="src/lib.rs", search="dioxus-charts = .*", replace="{{crate_name}} = \"{{version}}\""}, 5 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}"}, 6 | {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, 7 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}"}, 8 | {file="CHANGELOG.md", search="", replace="\n\n## [Unreleased] - ReleaseDate", exactly=1}, 9 | {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/hiltonm/dioxus-charts/compare/{{tag_name}}...HEAD", exactly=1}, 10 | ] 11 | -------------------------------------------------------------------------------- /src/charts/bar.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use crate::grid::{Axis, Grid}; 4 | use crate::types::*; 5 | 6 | /// The `BarChart` properties struct for the configuration of the bar chart. 7 | #[allow(clippy::struct_excessive_bools)] 8 | #[derive(Clone, PartialEq, Props)] 9 | pub struct BarChartProps { 10 | series: Series, 11 | #[props(optional)] 12 | labels: Option, 13 | 14 | #[props(default = "100%".to_string(), into)] 15 | width: String, 16 | #[props(default = "100%".to_string(), into)] 17 | height: String, 18 | #[props(default = 600)] 19 | viewbox_width: i32, 20 | #[props(default = 400)] 21 | viewbox_height: i32, 22 | 23 | #[props(default)] 24 | padding_top: i32, 25 | #[props(default)] 26 | padding_bottom: i32, 27 | #[props(default)] 28 | padding_left: i32, 29 | #[props(default)] 30 | padding_right: i32, 31 | 32 | #[props(optional)] 33 | lowest: Option, 34 | #[props(optional)] 35 | highest: Option, 36 | #[props(default = 8)] 37 | max_ticks: i32, 38 | 39 | #[props(default = true)] 40 | show_grid: bool, 41 | #[props(default = true)] 42 | show_dotted_grid: bool, 43 | #[props(default = false)] 44 | show_grid_ticks: bool, 45 | #[props(default = true)] 46 | show_labels: bool, 47 | #[props(default = true)] 48 | show_series_labels: bool, 49 | 50 | #[props(default = 60)] 51 | label_size: i32, 52 | #[props(optional)] 53 | label_interpolation: Option String>, 54 | 55 | #[props(default = "5%".to_string(), into)] 56 | bar_width: String, 57 | #[props(default = 30.0)] 58 | bar_distance: f32, 59 | #[props(default = false)] 60 | horizontal_bars: bool, 61 | #[props(default = false)] 62 | stacked_bars: bool, 63 | 64 | #[props(default = "dx-chart-bar".to_string(), into)] 65 | class_chart_bar: String, 66 | #[props(default = "dx-bar".to_string(), into)] 67 | class_bar: String, 68 | #[props(default = "dx-bar-group".to_string(), into)] 69 | class_bar_group: String, 70 | #[props(default = "dx-bar-label".to_string(), into)] 71 | class_bar_label: String, 72 | #[props(default = "dx-grid".to_string(), into)] 73 | class_grid: String, 74 | #[props(default = "dx-grid-line".to_string(), into)] 75 | class_grid_line: String, 76 | #[props(default = "dx-grid-label".to_string(), into)] 77 | class_grid_label: String, 78 | #[props(default = "dx-grid-labels".to_string(), into)] 79 | class_grid_labels: String, 80 | } 81 | 82 | /// This is the `BarChart` function used to render the bar chart `Element`. 83 | /// In Dioxus, components are just functions; this is the main `BarChart` 84 | /// component to be used inside `rsx!` macros in your code. 85 | /// 86 | /// # Example 87 | /// 88 | /// ```rust,ignore 89 | /// use dioxus::prelude::*; 90 | /// use dioxus_charts::BarChart; 91 | /// 92 | /// fn app() -> Element { 93 | /// rsx! { 94 | /// BarChart { 95 | /// padding_top: 30, 96 | /// padding_left: 70, 97 | /// padding_right: 50, 98 | /// padding_bottom: 30, 99 | /// bar_width: "10%", 100 | /// horizontal_bars: true, 101 | /// label_interpolation: (|v| format!("{v:.1}%")) as fn(f32) -> String, 102 | /// series: vec![ 103 | /// vec![63.0, 14.4, 8.0, 5.1, 1.8], 104 | /// ], 105 | /// labels: vec!["Chrome".into(), "Safari".into(), "IE/Edge".into(), "Firefox".into(), "Opera".into()] 106 | /// } 107 | /// } 108 | /// } 109 | /// ``` 110 | /// 111 | /// # Props 112 | /// 113 | /// - `series`: [Vec]<[Vec]<[f32]>> (**required**): The series vector of vectors with the all series values. 114 | /// - `labels`: [Vec]<[String]> (optional): Optional labels to show on the labels axis. 115 | /// --- 116 | /// - `width`: &[str] (default: `"100%"`): The SVG element width attribute. It also accepts any 117 | /// other CSS style, i.e., "200px" 118 | /// - `height`: &[str] (default: `"100%"`): The SVG height counter-part of the `width` prop above. 119 | /// - `viewbox_width`: [i32] (default: `600`): The SVG viewbox width. Together with 120 | /// `viewbox_height` it is useful for adjusting the aspect ratio for longer charts. 121 | /// - `viewbox_height`: [i32] (default: `400`): The SVG viewbox height. 122 | /// --- 123 | /// - `padding_top`: [i32] (default: `0`): Padding for the top side of the view box. 124 | /// - `padding_bottom`: [i32] (default: `0`): Padding for the bottom side of the view box. 125 | /// - `padding_left`: [i32] (default: `0`): Padding for the left side of the view box. 126 | /// - `padding_right`: [i32] (default: `0`): Padding for the right side of the view box. 127 | /// --- 128 | /// - `lowest`: [f32] (optional): The lowest number on the chart for the value axis. 129 | /// - `highest`: [f32] (optional): The highest number on the chart for the value axis. 130 | /// - `max_ticks`: [i32] (default: `8`): The maximum number of ticks on the generated value axis. 131 | /// --- 132 | /// - `show_grid`: [bool] (default: `true`): Show/hide the chart grid. 133 | /// - `show_dotted_grid`: [bool] (default: `true`): Show the chart grid with dotted style or not. 134 | /// - `show_grid_ticks`: [bool] (default: `false`): Show the chart grid ticks instead of drawing the 135 | /// whole grid lines for a cleaner look. 136 | /// - `show_labels`: [bool] (default: `true`): Show/hide the labels. 137 | /// - `show_series_labels`: [bool] (default: `true`): Show/hide the values labels at the top of 138 | /// bars. 139 | /// --- 140 | /// - `label_size`: [i32] (default: `60`): The maximum width or height of the label rect depending 141 | /// on whether the chart shows horizontal or vertical bars. 142 | /// - `label_interpolation`: fn([f32]) -> [String] (optional): Function for formatting the 143 | /// generated labels for values. 144 | /// --- 145 | /// - `bar_width`: &[str] (default: `"5%"`): The width of each bar. 146 | /// - `bar_distance`: [f32] (default: `30.0`): The distance between the bars for charts that have 147 | /// multiple ones side by side. 148 | /// - `horizontal_bars`: [bool] (default: `false`): Show horizontal bars. 149 | /// - `stacked_bars`: [bool] (default: `false`): Build a Stacked Bars chart. 150 | /// --- 151 | /// - `class_chart_bar`: &[str] (default: `"dx-chart-line"`): The HTML element `class` of the 152 | /// chart. 153 | /// - `class_bar`: &[str] (default: `"dx-bar"`): The HTML element `class` of the whole line. 154 | /// - `class_bar_group`: &[str] (default: `"dx-bar-group"`): The HTML element `class` of the line path. 155 | /// - `class_bar_label`: &[str] (default: `"dx-bar-label"`): The HTML element `class` of the line 156 | /// labels. 157 | /// - `class_grid`: &[str] (default: `"dx-grid"`): The HTML element `class` of the grid. 158 | /// - `class_grid_line`: &[str] (default: `"dx-grid-line"`): The HTML element `class` of every grid 159 | /// line. 160 | /// - `class_grid_label`: &[str] (default: `"dx-grid-label"`): The HTML element `class` of the grid 161 | /// labels. 162 | /// - `class_grid_labels`: &[str] (default: `"dx-grid-labels"`): The HTML element `class` of the 163 | /// group of grid labels. 164 | #[allow(non_snake_case)] 165 | pub fn BarChart(props: BarChartProps) -> Element { 166 | for series in props.series.iter() { 167 | if series.is_empty() { 168 | return rsx!("Bar chart error: empty series"); 169 | } 170 | } 171 | 172 | let grid = { 173 | let view = Rect::new( 174 | props.padding_left as f32, 175 | props.padding_top as f32, 176 | (props.viewbox_width - props.padding_right) as f32, 177 | (props.viewbox_height - props.padding_bottom) as f32, 178 | ); 179 | 180 | let lowest = if let Some(low) = props.lowest { 181 | low 182 | } else { 183 | 0.0 184 | }; 185 | 186 | let max_ticks = props.max_ticks.max(3); 187 | 188 | let axis_x = Axis::builder() 189 | .with_view(view) 190 | .with_grid_ticks(props.show_grid_ticks) 191 | .with_label_size(props.label_size) 192 | .with_centered_labels(props.labels.as_ref()); 193 | 194 | let axis_y = Axis::builder() 195 | .with_view(view) 196 | .with_max_ticks(max_ticks) 197 | .with_grid_ticks(props.show_grid_ticks) 198 | .with_series(&props.series) 199 | .with_stacked_series(props.stacked_bars) 200 | .with_label_interpolation(props.label_interpolation) 201 | .with_highest(props.highest) 202 | .with_lowest(Some(lowest)); 203 | 204 | if props.horizontal_bars { 205 | Grid::new(axis_y, axis_x) 206 | } else { 207 | Grid::new(axis_x, axis_y) 208 | } 209 | }; 210 | 211 | let (axis_value, axis_label) = if props.horizontal_bars { 212 | (&grid.x, &grid.y) 213 | } else { 214 | (&grid.y, &grid.x) 215 | }; 216 | 217 | let lines = grid.lines(); 218 | 219 | let mut color_var = 255.0; 220 | let dotted_stroke = if props.show_dotted_grid { 221 | &"2px" 222 | } else { 223 | &"0px" 224 | }; 225 | let generated_labels = axis_value.generated_labels(); 226 | 227 | let grid_labels = if props.show_labels { 228 | Some( 229 | axis_value 230 | .text_data(generated_labels.len()) 231 | .into_iter() 232 | .zip(generated_labels.iter()) 233 | .collect::>(), 234 | ) 235 | } else { 236 | None 237 | }; 238 | 239 | let grid_centered_labels = if props.show_labels { 240 | let rects = axis_label 241 | .centered_text_rects(props.labels.as_ref().unwrap().len() as i32) 242 | .into_iter(); 243 | 244 | let labels = if props.horizontal_bars { 245 | rects 246 | .zip(props.labels.as_ref().unwrap().iter().rev()) 247 | .collect::>() 248 | } else { 249 | rects 250 | .zip(props.labels.as_ref().unwrap().iter()) 251 | .collect::>() 252 | }; 253 | 254 | Some(labels) 255 | } else { 256 | None 257 | }; 258 | 259 | let stacked_bars_rects = if props.stacked_bars { 260 | let tick_centers = axis_label.tick_centers(); 261 | let mut all_series_rects = Vec::>::new(); 262 | let mut last_bar_ends: Option> = None; 263 | 264 | for a in props.series.iter() { 265 | let mut rects = Vec::::new(); 266 | let mut view_bar_ends = Vec::::new(); 267 | 268 | for (point, (i, v)) in tick_centers.iter().zip(a.iter().enumerate()) { 269 | let rect = if let Some(bar_ends) = &last_bar_ends { 270 | let last_end = bar_ends[i]; 271 | let end = axis_value.world_to_view(v + last_end, 0.0); 272 | view_bar_ends.push(v + last_end); 273 | 274 | let last_end = axis_value.world_to_view(last_end, 0.0); 275 | 276 | if props.horizontal_bars { 277 | Rect::new(last_end, point.y, end, point.y) 278 | } else { 279 | Rect::new(point.x, last_end, point.x, end) 280 | } 281 | } else { 282 | let end = axis_value.world_to_view(*v, 0.0); 283 | view_bar_ends.push(*v); 284 | 285 | if props.horizontal_bars { 286 | Rect::new(point.x, point.y, end, point.y) 287 | } else { 288 | Rect::new(point.x, point.y, point.x, end) 289 | } 290 | }; 291 | 292 | rects.push(rect); 293 | } 294 | 295 | all_series_rects.push(rects); 296 | last_bar_ends = Some(view_bar_ends); 297 | } 298 | 299 | Some(all_series_rects) 300 | } else { 301 | None 302 | }; 303 | 304 | let stacked_bars_rects_rsx = stacked_bars_rects.map(|all_series_rects| { 305 | rsx! { 306 | //all_series_rects.iter().enumerate().map(|(i, series_rects)| { 307 | for (i, series_rects) in all_series_rects.iter().enumerate() { 308 | {color_var -= 75.0 * (1.0 / (i + 1) as f32);} 309 | 310 | g { 311 | class: "{props.class_bar_group}-{i}", 312 | { 313 | series_rects.iter().map(|rect| { 314 | rsx! { 315 | line { 316 | x1: "{rect.min.x}", 317 | y1: "{rect.min.y}", 318 | x2: "{rect.max.x}", 319 | y2: "{rect.max.y}", 320 | class: "{props.class_bar}", 321 | stroke: "rgb({color_var}, 40, 40)", 322 | stroke_width: "{props.bar_width}", 323 | } 324 | } 325 | }) 326 | } 327 | } 328 | } 329 | } 330 | }); 331 | 332 | let series_rsx = props.series.iter().enumerate().map(|(i, a)| { 333 | color_var -= 75.0 * (1.0 / (i + 1) as f32); 334 | let offset = (i as f32 - (props.series.len() as f32 - 1.0) / 2.0) * props.bar_distance; 335 | let tick_centers = axis_label.tick_centers(); 336 | 337 | let tick_centers_rsx = tick_centers.iter().zip(a.iter()).map(|(point, v)| { 338 | let end = axis_value.world_to_view(*v, 0.0); 339 | let (rect, text) = if props.horizontal_bars { 340 | ( 341 | Rect::new(point.x, point.y + offset, end, point.y + offset), 342 | TextData { 343 | x: end + 5.0, 344 | y: point.y + offset, 345 | anchor: "start", 346 | baseline: "middle", 347 | }, 348 | ) 349 | } else { 350 | ( 351 | Rect::new(point.x + offset, point.y, point.x + offset, end), 352 | TextData { 353 | x: point.x + offset, 354 | y: end - 5.0, 355 | anchor: "middle", 356 | baseline: "text-bottom", 357 | }, 358 | ) 359 | }; 360 | 361 | let bar_label = { 362 | if !props.show_series_labels { 363 | String::new() 364 | } else if let Some(func) = props.label_interpolation { 365 | func(*v) 366 | } else { 367 | format!("{}", *v) 368 | } 369 | }; 370 | 371 | rsx! { 372 | line { 373 | x1: "{rect.min.x}", 374 | y1: "{rect.min.y}", 375 | x2: "{rect.max.x}", 376 | y2: "{rect.max.y}", 377 | class: "{props.class_bar}", 378 | stroke: "rgb({color_var}, 40, 40)", 379 | stroke_width: "{props.bar_width}", 380 | }, 381 | if props.show_series_labels { 382 | text { 383 | dx: "{text.x}", 384 | dy: "{text.y}", 385 | text_anchor: "{text.anchor}", 386 | class: "{props.class_bar_label}", 387 | alignment_baseline: "{text.baseline}", 388 | "{bar_label}" 389 | } 390 | }, 391 | } 392 | }); 393 | 394 | rsx! { 395 | g { 396 | class: "{props.class_bar_group}-{i}", 397 | {tick_centers_rsx} 398 | } 399 | } 400 | }); 401 | 402 | rsx! { 403 | div { 404 | svg { 405 | xmlns: "http://www.w3.org/2000/svg", 406 | width: "{props.width}", 407 | height: "{props.height}", 408 | class: "{props.class_chart_bar}", 409 | preserve_aspect_ratio: "xMidYMid meet", 410 | view_box: "0 0 {props.viewbox_width} {props.viewbox_height}", 411 | 412 | if props.show_grid { 413 | g { 414 | class: "{props.class_grid}", 415 | for line in lines { 416 | line { 417 | x1: "{line.min.x}", 418 | y1: "{line.min.y}", 419 | x2: "{line.max.x}", 420 | y2: "{line.max.y}", 421 | class: "{props.class_grid_line}", 422 | stroke: "rgba(20, 20, 20, 0.8)", 423 | stroke_dasharray: "{dotted_stroke}", 424 | } 425 | } 426 | } 427 | }, 428 | 429 | for labels in grid_labels { 430 | g { 431 | class: "{props.class_grid_labels}", 432 | for (text, label) in labels { 433 | text { 434 | dx: "{text.x}", 435 | dy: "{text.y}", 436 | text_anchor: "{text.anchor}", 437 | class: "{props.class_grid_label}", 438 | alignment_baseline: "{text.baseline}", 439 | "{label}" 440 | } 441 | } 442 | } 443 | }, 444 | 445 | for labels in grid_centered_labels { 446 | g { 447 | class: "{props.class_grid_labels}", 448 | for (rect, label) in labels { 449 | foreignObject { 450 | x: "{rect.min.x}", 451 | y: "{rect.min.y}", 452 | width: "{rect.max.x}", 453 | height: "{rect.max.y}", 454 | if props.horizontal_bars { 455 | span { 456 | class: "{props.class_grid_label}", 457 | //width: "100%", 458 | height: "100%", 459 | display: "inline-flex", 460 | align_items: "center", 461 | line_height: "1", 462 | float: "right", 463 | text_align: "right", 464 | "{label}" 465 | } 466 | } else { 467 | span { 468 | class: "{props.class_grid_label}", 469 | width: "100%", 470 | height: "100%", 471 | display: "inline-block", 472 | line_height: "1", 473 | text_align: "center", 474 | "{label}" 475 | } 476 | } 477 | } 478 | } 479 | } 480 | }, 481 | 482 | {stacked_bars_rects_rsx} 483 | 484 | 485 | if !props.stacked_bars { 486 | {series_rsx} 487 | } 488 | } 489 | } 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/charts/line.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use crate::grid::{Axis, Grid}; 4 | use crate::types::*; 5 | 6 | /// The `LineChart` properties struct for the configuration of the line chart. 7 | #[allow(clippy::struct_excessive_bools)] 8 | #[derive(Clone, PartialEq, Props)] 9 | pub struct LineChartProps { 10 | series: Series, 11 | #[props(optional)] 12 | labels: Option, 13 | #[props(optional)] 14 | series_labels: Option, 15 | 16 | #[props(default = "100%".to_string(), into)] 17 | width: String, 18 | #[props(default = "100%".to_string(), into)] 19 | height: String, 20 | #[props(default = 600)] 21 | viewbox_width: i32, 22 | #[props(default = 400)] 23 | viewbox_height: i32, 24 | 25 | #[props(default)] 26 | padding_top: i32, 27 | #[props(default)] 28 | padding_bottom: i32, 29 | #[props(default)] 30 | padding_left: i32, 31 | #[props(default)] 32 | padding_right: i32, 33 | 34 | #[props(default = true)] 35 | show_grid: bool, 36 | #[props(default = true)] 37 | show_dotted_grid: bool, 38 | #[props(default = false)] 39 | show_grid_ticks: bool, 40 | #[props(default = true)] 41 | show_labels: bool, 42 | #[props(default = true)] 43 | show_dots: bool, 44 | #[props(default = true)] 45 | show_lines: bool, 46 | #[props(default = true)] 47 | show_line_labels: bool, 48 | 49 | #[props(default = "1%".to_string(), into)] 50 | line_width: String, 51 | #[props(default = "3%".to_string(), into)] 52 | dot_size: String, 53 | #[props(optional)] 54 | label_interpolation: Option String>, 55 | 56 | #[props(optional)] 57 | lowest: Option, 58 | #[props(optional)] 59 | highest: Option, 60 | #[props(default = 8)] 61 | max_ticks: i32, 62 | 63 | #[props(default = "dx-chart-line".to_string(), into)] 64 | class_chart_line: String, 65 | #[props(default = "dx-line".to_string(), into)] 66 | class_line: String, 67 | #[props(default = "dx-line-path".to_string(), into)] 68 | class_line_path: String, 69 | #[props(default = "dx-line-dot".to_string(), into)] 70 | class_line_dot: String, 71 | #[props(default = "dx-line-label".to_string(), into)] 72 | class_line_label: String, 73 | #[props(default = "dx-grid".to_string(), into)] 74 | class_grid: String, 75 | #[props(default = "dx-grid-line".to_string(), into)] 76 | class_grid_line: String, 77 | #[props(default = "dx-grid-label".to_string(), into)] 78 | class_grid_label: String, 79 | #[props(default = "dx-grid-labels".to_string(), into)] 80 | class_grid_labels: String, 81 | } 82 | 83 | /// This is the `LineChart` function used to render the line chart `Element`. 84 | /// In Dioxus, components are just functions, so this is the main `PieChart` 85 | /// component to be used inside `rsx!` macros in your code. 86 | /// 87 | /// # Example 88 | /// 89 | /// ```rust,ignore 90 | /// use dioxus::prelude::*; 91 | /// use dioxus_charts::LineChart; 92 | /// 93 | /// fn app() -> Element { 94 | /// rsx! { 95 | /// LineChart { 96 | /// padding_top: 30, 97 | /// padding_left: 65, 98 | /// padding_right: 80, 99 | /// padding_bottom: 30, 100 | /// label_interpolation: (|v| format!("${v:.0}B")) as fn(f32) -> String, 101 | /// series: vec![ 102 | /// vec![29.0, 30.5, 32.6, 35.0, 37.5], 103 | /// vec![20.0, 25.1, 26.0, 25.2, 26.6], 104 | /// vec![18.0, 21.0, 22.5, 24.0, 25.1], 105 | /// vec![12.5, 17.0, 19.3, 20.1, 21.0], 106 | /// ], 107 | /// labels: vec!["2020A".into(), "2021E".into(), "2022E".into(), "2023E".into(), "2024E".into()], 108 | /// series_labels: vec!["Disney".into(), "Comcast".into(), "Warner".into(), "Netflix".into()], 109 | /// } 110 | /// } 111 | /// } 112 | /// ``` 113 | /// 114 | /// # Props 115 | /// 116 | /// - `series`: [Vec]<[Vec]<[f32]>> (**required**): The series vector of vectors with the series values. 117 | /// - `labels`: [Vec]<[String]> (optional): Optional labels to show on the labels axis. 118 | /// - `series_labels`: [Vec]<[String]> (optional): Optional labels to show for each generated line. 119 | /// --- 120 | /// - `width`: &[str] (default: `"100%"`): The SVG element width attribute. It also accepts any 121 | /// other CSS style, i.e., "200px" 122 | /// - `height`: &[str] (default: `"100%"`): The SVG height counter-part of the `width` prop above. 123 | /// - `viewbox_width`: [i32] (default: `600`): The SVG viewbox width. Together with 124 | /// `viewbox_height` it is useful for adjusting the aspect ratio for longer charts. 125 | /// - `viewbox_height`: [i32] (default: `400`): The SVG viewbox height. 126 | /// --- 127 | /// - `padding_top`: [i32] (default: `0`): Padding for the top side of the view box. 128 | /// - `padding_bottom`: [i32] (default: `0`): Padding for the bottom side of the view box. 129 | /// - `padding_left`: [i32] (default: `0`): Padding for the left side of the view box. 130 | /// - `padding_right`: [i32] (default: `0`): Padding for the right side of the view box. 131 | /// --- 132 | /// - `lowest`: [f32] (optional): The lowest number on the chart for the value axis. 133 | /// - `highest`: [f32] (optional): The highest number on the chart for the value axis. 134 | /// - `max_ticks`: [i32] (default: `8`): The maximum number of ticks on the generated value axis. 135 | /// --- 136 | /// - `show_grid`: [bool] (default: `true`): Show/hide the chart grid. 137 | /// - `show_dotted_grid`: [bool] (default: `true`): Show the chart grid with dotted style or not. 138 | /// - `show_grid_ticks`: [bool] (default: `false`): Show the chart grid ticks instead of drawing the 139 | /// whole grid lines for a cleaner look. 140 | /// - `show_labels`: [bool] (default: `true`): Show/hide the labels. 141 | /// - `show_dots`: [bool] (default: `true`): Show/hide the line dots. 142 | /// - `show_lines`: [bool] (default: `true`): Show/hide the series lines. 143 | /// - `show_line_labels`: [bool] (default: `true`): Show/hide the labels for the lines. 144 | /// --- 145 | /// - `line_width`: &[str] (default: `"1%"`): The width of the series lines. 146 | /// - `dot_size`: &[str] (default: `"3%"`): The size of the line dots. 147 | /// - `label_interpolation`: fn([f32]) -> [String] (optional): Function for formatting the 148 | /// generated labels. 149 | /// --- 150 | /// - `class_chart_line`: &[str] (default: `"dx-chart-line"`): The HTML element `class` of the 151 | /// chart. 152 | /// - `class_line`: &[str] (default: `"dx-line"`): The HTML element `class` of the whole line. 153 | /// - `class_line_path`: &[str] (default: `"dx-line"`): The HTML element `class` of the line path. 154 | /// - `class_line_dot`: &[str] (default: `"dx-line-dot"`): The HTML element `class` of the line dot. 155 | /// - `class_line_label`: &[str] (default: `"dx-line-label"`): The HTML element `class` of the line 156 | /// labels. 157 | /// - `class_grid`: &[str] (default: `"dx-grid"`): The HTML element `class` of the grid. 158 | /// - `class_grid_line`: &[str] (default: `"dx-grid-line"`): The HTML element `class` of every grid 159 | /// line. 160 | /// - `class_grid_label`: &[str] (default: `"dx-grid-label"`): The HTML element `class` of the grid 161 | /// labels. 162 | /// - `class_grid_labels`: &[str] (default: `"dx-grid-labels"`): The HTML element `class` of the 163 | /// group of grid labels. 164 | #[allow(non_snake_case)] 165 | pub fn LineChart(props: LineChartProps) -> Element { 166 | for series in props.series.iter() { 167 | if series.is_empty() { 168 | return rsx!("Line chart error: empty series"); 169 | } 170 | } 171 | 172 | let view = Rect::new( 173 | props.padding_left as f32, 174 | props.padding_top as f32, 175 | (props.viewbox_width - props.padding_right) as f32, 176 | (props.viewbox_height - props.padding_bottom) as f32, 177 | ); 178 | 179 | let max_ticks = props.max_ticks.max(3); 180 | 181 | let axis_x = Axis::builder() 182 | .with_view(view) 183 | .with_grid_ticks(props.show_grid_ticks) 184 | .with_labels(props.labels.as_ref()); 185 | 186 | let axis_y = Axis::builder() 187 | .with_view(view) 188 | .with_max_ticks(max_ticks) 189 | .with_grid_ticks(props.show_grid_ticks) 190 | .with_series(&props.series) 191 | .with_label_interpolation(props.label_interpolation) 192 | .with_highest(props.highest) 193 | .with_lowest(props.lowest); 194 | 195 | let grid = Grid::new(axis_x, axis_y); 196 | let lines = grid.lines(); 197 | let generated_labels = grid.y.generated_labels(); 198 | 199 | let grid_labels = if props.show_labels { 200 | if let Some(labels) = props.labels.as_ref() { 201 | Some( 202 | grid.text_data(Some(labels.len()), Some(generated_labels.len())) 203 | .into_iter() 204 | .zip(labels.iter().chain(generated_labels.iter())) 205 | .collect::>(), 206 | ) 207 | } else { 208 | Some( 209 | grid.y 210 | .text_data(generated_labels.len()) 211 | .into_iter() 212 | .zip(generated_labels.iter()) 213 | .collect::>(), 214 | ) 215 | } 216 | } else { 217 | None 218 | }; 219 | 220 | let mut color_var = 255.0; 221 | let dotted_stroke = if props.show_dotted_grid { 222 | &"2px" 223 | } else { 224 | &"0px" 225 | }; 226 | 227 | let string_binding = String::new(); 228 | let vec_binding = vec![]; 229 | 230 | let series_rsx = props 231 | .series 232 | .iter() 233 | .enumerate() 234 | .zip( 235 | props 236 | .series_labels 237 | .as_ref() 238 | .unwrap_or(&vec_binding) 239 | .iter() 240 | .chain(std::iter::repeat(&string_binding)), 241 | ) 242 | .map(|((i, a), label)| { 243 | let mut commands = Vec::::with_capacity(a.len()); 244 | let mut dots = Vec::::with_capacity(a.len()); 245 | let mut text_point: Option = None; 246 | 247 | color_var -= 75.0 * (1.0 / (i + 1) as f32); 248 | 249 | for (index, v) in a.iter().enumerate() { 250 | let point = grid.world_to_view(index as f32, *v, false); 251 | 252 | if index == 0 { 253 | commands.push(format!("M{},{}", point.x, point.y)); 254 | } else { 255 | commands.push(format!("L{},{}", point.x, point.y)); 256 | } 257 | 258 | if props.show_dots { 259 | dots.push(Rect::new(point.x, point.y, point.x + 0.1, point.y)); 260 | } 261 | 262 | if !label.is_empty() && index == (a.len() - 1) { 263 | text_point = Some(point); 264 | } 265 | } 266 | 267 | let commands = commands.join(" "); 268 | 269 | rsx! { 270 | g { 271 | class: "{props.class_line}-{i}", 272 | path { 273 | d: "{commands}", 274 | class: "{props.class_line_path}", 275 | stroke: "rgb({color_var}, 40, 40)", 276 | stroke_width: "{props.line_width}", 277 | stroke_linecap: "round", 278 | fill: "transparent", 279 | }, 280 | for d in dots { 281 | line { 282 | x1: "{d.min.x}", 283 | y1: "{d.min.y}", 284 | x2: "{d.max.x}", 285 | y2: "{d.max.y}", 286 | class: "{props.class_line_dot}", 287 | stroke: "rgb({color_var}, 40, 40)", 288 | stroke_width: "{props.dot_size}", 289 | stroke_linecap: "round", 290 | } 291 | } 292 | for point in text_point { 293 | text { 294 | dx: format_args!("{}", point.x + 10.0), 295 | dy: "{point.y}", 296 | text_anchor: "start", 297 | color: "rgb({color_var}, 40, 40)", 298 | class: "{props.class_line_label}", 299 | "{label}" 300 | } 301 | } 302 | } 303 | } 304 | }); 305 | 306 | rsx! { 307 | div { 308 | svg { 309 | xmlns: "http://www.w3.org/2000/svg", 310 | width: "{props.width}", 311 | height: "{props.height}", 312 | class: "{props.class_chart_line}", 313 | preserve_aspect_ratio: "xMidYMid meet", 314 | view_box: "0 0 {props.viewbox_width} {props.viewbox_height}", 315 | if props.show_grid { 316 | g { 317 | class: "{props.class_grid}", 318 | for line in lines { 319 | line { 320 | x1: "{line.min.x}", 321 | y1: "{line.min.y}", 322 | x2: "{line.max.x}", 323 | y2: "{line.max.y}", 324 | class: "{props.class_grid_line}", 325 | stroke: "rgba(20, 20, 20, 0.8)", 326 | stroke_dasharray: "{dotted_stroke}", 327 | } 328 | } 329 | } 330 | } 331 | 332 | for labels in grid_labels { 333 | g { 334 | class: "{props.class_grid_labels}", 335 | for (text, label) in labels { 336 | text { 337 | dx: "{text.x}", 338 | dy: "{text.y}", 339 | text_anchor: "{text.anchor}", 340 | class: "{props.class_grid_label}", 341 | alignment_baseline: "{text.baseline}", 342 | "{label}" 343 | } 344 | } 345 | } 346 | } 347 | 348 | {series_rsx} 349 | } 350 | } 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/charts/pie.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | use crate::types::{Labels, Point}; 4 | use crate::utils::{normalize_series, polar_to_cartesian}; 5 | 6 | /// A hint for the automatic positioning of labels in the pie chart. 7 | #[derive(Clone, Copy, PartialEq, Eq)] 8 | pub enum LabelPosition { 9 | /// To position the label inside the pie chart. 10 | Inside, 11 | /// To position the label outside close to the border of the pie chart. 12 | Outside, 13 | /// To position the label in the center for manually positioning with the `label_offset` prop. 14 | Center, 15 | } 16 | 17 | /// The `PieChart` properties struct for the configuration of the pie chart. 18 | #[derive(Clone, PartialEq, Props)] 19 | pub struct PieChartProps { 20 | series: Vec, 21 | #[props(optional)] 22 | labels: Option, 23 | 24 | #[props(default = "100%".to_string(), into)] 25 | width: String, 26 | #[props(default = "100%".to_string(), into)] 27 | height: String, 28 | #[props(default = 600)] 29 | viewbox_width: i32, 30 | #[props(default = 400)] 31 | viewbox_height: i32, 32 | 33 | #[props(default = true)] 34 | show_labels: bool, 35 | #[props(default=LabelPosition::Inside)] 36 | label_position: LabelPosition, 37 | #[props(default)] 38 | label_offset: f32, 39 | #[props(optional)] 40 | label_interpolation: Option String>, 41 | 42 | #[props(default)] 43 | start_angle: f32, 44 | #[props(optional)] 45 | total: Option, 46 | #[props(optional)] 47 | show_ratio: Option, 48 | #[props(default)] 49 | padding: f32, 50 | 51 | #[props(default = false)] 52 | donut: bool, 53 | #[props(default = 40.0)] 54 | donut_width: f32, 55 | 56 | #[props(default = "dx-pie-chart".to_string(), into)] 57 | class_chart: String, 58 | #[props(default = "dx-series".to_string(), into)] 59 | class_series: String, 60 | #[props(default = "dx-slice".to_string(), into)] 61 | class_slice: String, 62 | #[props(default = "dx-label".to_string(), into)] 63 | class_label: String, 64 | } 65 | 66 | /// This is the `PieChart` function used to render the pie chart `Element`. 67 | /// In Dioxus, components are just functions, so this is the main `PieChart` 68 | /// component to be used inside `rsx!` macros in your code. 69 | /// 70 | /// # Example 71 | /// 72 | /// ```rust,ignore 73 | /// use dioxus::prelude::*; 74 | /// use dioxus_charts::PieChart; 75 | /// 76 | /// fn app() -> Element { 77 | /// rsx! { 78 | /// PieChart { 79 | /// start_angle: -60.0, 80 | /// label_position: LabelPosition::Outside, 81 | /// label_offset: 35.0, 82 | /// padding: 20.0, 83 | /// series: vec![59.54, 17.2, 9.59, 7.6, 5.53, 0.55] 84 | /// labels: vec!["Asia".into(), "Africa".into(), "Europe".into(), "N. America".into(), "S. America".into(), "Oceania".into()], 85 | /// } 86 | /// } 87 | /// } 88 | /// ``` 89 | /// 90 | /// # Props 91 | /// 92 | /// - `series`: [Vec]<[f32]> (**required**): The series vector with the values. 93 | /// - `labels`: [Vec]<[String]> (optional): Optional labels to show for each value of the 94 | /// series. 95 | /// --- 96 | /// - `width`: &[str] (default: `"100%"`): The SVG element width attribute. It also accepts any 97 | /// other CSS style, i.e., "200px" 98 | /// - `height`: &[str] (default: `"100%"`): The SVG height counter-part of the `width` prop above. 99 | /// - `viewbox_width`: [i32] (default: `600`): The SVG viewbox width. Together with 100 | /// `viewbox_height` it is useful scaling up or down the chart and labels. 101 | /// - `viewbox_height`: [i32] (default: `400`): The SVG viewbox height. 102 | /// --- 103 | /// - `show_labels`: [bool] (default: `true`): Show/hide labels. 104 | /// - `label_position`: [`LabelPosition`] (default: [`LabelPosition::Inside`]): A hint for the 105 | /// automatic positioning of labels on the chart. 106 | /// - `label_offset`: [f32] (default: `0.0`): An extra offset for the labels relative to the center 107 | /// of the pie. 108 | /// - `label_interpolation`: fn([f32]) -> [String] (optional): Function for formatting the 109 | /// generated labels. 110 | /// --- 111 | /// - `start_angle`: [f32] (default: `0.0`): The initial angle used for drawing the pie. 112 | /// - `total`: [f32] (optional): The series total sum. Can be used to make Gauge charts. 113 | /// - `show_ratio`: [f32] (optional): Used for making Gauge charts more easily. `0.0001` to 114 | /// `1.0` is the same as `0%` to `100%`. 115 | /// - `padding`: [f32] (default: `0.0`): Padding for every side of the SVG view box. 116 | /// --- 117 | /// - `donut`: [bool] (default: `false`): Draw the slices differently to make a donut-looking chart 118 | /// instead. 119 | /// - `donut_width`: [f32] (default: `40.0`): The width of each donut slice. 120 | /// --- 121 | /// - `class_chart`: &[str] (default: `"dx-pie-chart"`): The HTML element `class` of the 122 | /// pie chart. 123 | /// - `class_series`: &[str] (default: `"dx-series"`): The HTML element `class` for the group of 124 | /// pie slices. 125 | /// - `class_slice`: &[str] (default: `"dx-slice"`): The HTML element `class` for all pie 126 | /// slices. 127 | /// - `class_label`: &[str] (default: `"dx-label"`): The HTML element `class` for all labels. 128 | #[allow(non_snake_case)] 129 | pub fn PieChart(props: PieChartProps) -> Element { 130 | if props.series.is_empty() { 131 | return rsx!("Pie chart error: empty series"); 132 | } 133 | 134 | let center = Point::new( 135 | props.viewbox_width as f32 / 2.0, 136 | props.viewbox_height as f32 / 2.0, 137 | ); 138 | let center_min = center.x.min(center.y); 139 | let radius = center_min - 30.0 - props.padding; 140 | let label_radius = match props.label_position { 141 | LabelPosition::Inside => radius / 2.0 + props.label_offset, 142 | LabelPosition::Outside => radius + props.label_offset, 143 | LabelPosition::Center => 0.0 + props.label_offset, 144 | }; 145 | 146 | let normalized_series = normalize_series(&props.series); 147 | let normalized_sum: f32 = normalized_series.iter().sum(); 148 | 149 | let values_total: f32 = if let Some(r) = props.show_ratio { 150 | 1.0 / r.clamp(0.0001, 1.0) * normalized_sum 151 | } else if let Some(v) = props.total { 152 | (normalized_sum / props.series.iter().sum::() * v).max(normalized_sum) 153 | } else { 154 | normalized_sum 155 | }; 156 | 157 | let mut m_start_angle = props.start_angle; 158 | let mut color_var = 255.0; 159 | let mut class_index = 0; 160 | let mut label_positions = Vec::::new(); 161 | 162 | let normalized_series_rsx = normalized_series.iter().filter_map(|v| { 163 | if *v != 0.0 { 164 | let mut end_angle = if values_total > 0.0 { 165 | m_start_angle + (v / values_total) * 360.0 166 | } else { 167 | 0.0 168 | }; 169 | let overlap_start_angle = if class_index != 0 { 170 | (m_start_angle - 0.4).max(0.0) 171 | } else { 172 | m_start_angle 173 | }; 174 | if end_angle - overlap_start_angle >= 359.99 { 175 | end_angle = overlap_start_angle + 359.99 176 | } 177 | 178 | let start_position = polar_to_cartesian(center, radius, overlap_start_angle); 179 | let end_position = polar_to_cartesian(center, radius, end_angle); 180 | let large_arc = i32::from(end_angle - m_start_angle > 180.0); 181 | 182 | let dpath = if props.donut { 183 | let donut_radius = radius - props.donut_width; 184 | let start_inside_position = polar_to_cartesian(center, donut_radius, overlap_start_angle); 185 | let end_inside_position = polar_to_cartesian(center, donut_radius, end_angle); 186 | let large_arc_inside = large_arc; 187 | 188 | format!("M{end_position}\ 189 | A{radius},{radius},0,{large_arc},0,{start_position}\ 190 | L{start_inside_position}\ 191 | A{donut_radius},{donut_radius},0,{large_arc_inside},1,{end_inside_position}Z") 192 | } else { 193 | format!("M{end_position}\ 194 | A{radius},{radius},0,{large_arc},0,{start_position}\ 195 | L{center}Z") 196 | }; 197 | 198 | let element = rsx! { 199 | g { 200 | class: "{props.class_series} {props.class_series}-{class_index}", 201 | path { 202 | d: "{dpath}", 203 | class: "{props.class_slice}", 204 | fill: "rgb({color_var}, 40, 40)", 205 | }, 206 | } 207 | }; 208 | 209 | label_positions.push(polar_to_cartesian(center, label_radius, m_start_angle + (end_angle - m_start_angle) / 2.0)); 210 | 211 | color_var -= 75.0 * (1.0 / (class_index + 1) as f32); 212 | class_index += 1; 213 | m_start_angle = end_angle; 214 | Some(element) 215 | } else { 216 | label_positions.push(Point::new(-1.0, -1.0)); 217 | None 218 | } 219 | }); 220 | 221 | rsx! { 222 | div { 223 | svg { 224 | view_box: "0 0 {props.viewbox_width} {props.viewbox_height}", 225 | width: "{props.width}", 226 | height: "{props.height}", 227 | class: "{props.class_chart}", 228 | preserve_aspect_ratio: "xMidYMid meet", 229 | xmlns: "http://www.w3.org/2000/svg", 230 | 231 | {normalized_series_rsx} 232 | 233 | if let Some(ref labels) = props.labels { 234 | g { 235 | { 236 | label_positions.iter().zip(labels.iter()).filter_map(|(position, label)| { 237 | if position.x > 0.0 { 238 | Some(rsx! { 239 | text { 240 | dx: "{position.x}", 241 | dy: "{position.y}", 242 | text_anchor: "middle", 243 | class: "{props.class_label}", 244 | alignment_baseline: "middle", 245 | "{label}" 246 | } 247 | }) 248 | } else { 249 | None 250 | } 251 | }) 252 | } 253 | } 254 | } else if props.show_labels { 255 | g { 256 | { 257 | label_positions.iter().zip(props.series.iter()).filter_map(|(position, value)| { 258 | let label = if let Some(func) = props.label_interpolation { 259 | func(*value) 260 | } else { 261 | value.to_string() 262 | }; 263 | 264 | if position.x > 0.0 { 265 | Some(rsx! { 266 | text { 267 | dx: "{position.x}", 268 | dy: "{position.y}", 269 | text_anchor: "middle", 270 | class: "{props.class_label}", 271 | alignment_baseline: "middle", 272 | "{label}" 273 | } 274 | }) 275 | } else { 276 | None 277 | } 278 | }) 279 | } 280 | } 281 | } 282 | } 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/grid.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | 3 | use crate::types::*; 4 | use crate::utils::magnitude; 5 | 6 | const LABEL_OFFSET: f32 = 6.0; 7 | const TICK_SIZE: f32 = 10.0; 8 | 9 | #[derive(Copy, Clone)] 10 | pub enum Direction { 11 | Horizontal, 12 | Vertical, 13 | } 14 | 15 | #[derive(Copy, Clone)] 16 | pub(crate) struct Axis { 17 | view: Rect, 18 | step_len: f32, 19 | steps: i32, 20 | world_start: f32, 21 | world: f32, 22 | grid_ticks: bool, 23 | label_interpolation: Option String>, 24 | label_size: i32, 25 | direction: Direction, 26 | } 27 | 28 | impl Default for Axis { 29 | fn default() -> Self { 30 | Self { 31 | view: Rect::default(), 32 | step_len: 0.0, 33 | steps: 0, 34 | world_start: 0.0, 35 | world: 0.0, 36 | grid_ticks: false, 37 | label_interpolation: None, 38 | label_size: 60, 39 | direction: Direction::Horizontal, 40 | } 41 | } 42 | } 43 | 44 | impl Axis { 45 | pub fn builder<'a>() -> AxisBuilder<'a> { 46 | AxisBuilder::default() 47 | } 48 | 49 | pub fn world_to_view(&self, v: f32, start_offset: f32) -> f32 { 50 | if self.world > 0.0 { 51 | match self.direction { 52 | Direction::Vertical => v / self.world * self.view.width() + self.view.min.x, 53 | Direction::Horizontal => { 54 | let c = (v - start_offset) / self.world * self.view.height() + self.view.min.y; 55 | self.view.min.y - c + self.view.max.y 56 | } 57 | } 58 | } else { 59 | 0.0 60 | } 61 | } 62 | 63 | pub fn step_to_world(&self, v: f32) -> f32 { 64 | self.world / (self.steps as f32 - 1.0) * v 65 | } 66 | 67 | pub fn lines(&self) -> Vec { 68 | let mut lines = Vec::::new(); 69 | 70 | for i in 0..self.steps { 71 | let w = self.step_to_world(i as f32); 72 | let v = self.world_to_view(w, 0.0); 73 | 74 | match self.direction { 75 | Direction::Vertical => { 76 | let end = if self.grid_ticks && i != 0 { 77 | self.view.max.y - TICK_SIZE 78 | } else { 79 | self.view.min.y 80 | }; 81 | 82 | lines.push(Rect::new(v, self.view.max.y, v, end)); 83 | } 84 | Direction::Horizontal => { 85 | let end = if self.grid_ticks && i != 0 { 86 | self.view.min.x + TICK_SIZE 87 | } else { 88 | self.view.max.x 89 | }; 90 | 91 | lines.push(Rect::new(self.view.min.x, v, end, v)); 92 | } 93 | } 94 | } 95 | 96 | lines 97 | } 98 | 99 | pub fn tick_centers(&self) -> Vec { 100 | let mut points = Vec::::new(); 101 | 102 | for i in 1..self.steps { 103 | let p = i - 1; 104 | let w1 = self.step_to_world(p as f32); 105 | let w2 = self.step_to_world(i as f32); 106 | let v1 = self.world_to_view(w1, 0.0); 107 | let v2 = self.world_to_view(w2, 0.0); 108 | let center = (v1 + v2) / 2.0; 109 | 110 | match self.direction { 111 | Direction::Vertical => { 112 | let center = (v1 + v2) / 2.0; 113 | points.push(Point::new(center, self.view.max.y)); 114 | } 115 | Direction::Horizontal => { 116 | points.push(Point::new(self.view.min.x, center)); 117 | } 118 | } 119 | } 120 | 121 | match self.direction { 122 | Direction::Vertical => points, 123 | Direction::Horizontal => points.into_iter().rev().collect(), 124 | } 125 | } 126 | 127 | pub fn centered_text_rects(&self, n_labels: i32) -> Vec { 128 | let mut texts = Vec::::new(); 129 | let n_labels = self.steps.min(n_labels + 1); 130 | 131 | for i in 1..n_labels { 132 | let p = i - 1; 133 | let w1 = self.step_to_world(p as f32); 134 | let w2 = self.step_to_world(i as f32); 135 | let v1 = self.world_to_view(w1, 0.0); 136 | let v2 = self.world_to_view(w2, 0.0); 137 | 138 | match self.direction { 139 | Direction::Vertical => { 140 | let width = v2 - v1; 141 | let height = self.label_size as f32; 142 | texts.push(Rect::new(v1, self.view.max.y + LABEL_OFFSET, width, height)); 143 | } 144 | Direction::Horizontal => { 145 | let height = v1 - v2; 146 | let width = self.label_size as f32; 147 | texts.push(Rect::new( 148 | self.view.min.x - LABEL_OFFSET - width, 149 | v2, 150 | width, 151 | height, 152 | )); 153 | } 154 | } 155 | } 156 | 157 | texts 158 | } 159 | 160 | pub fn text_data(&self, n_labels: usize) -> Vec { 161 | let mut texts = Vec::::new(); 162 | let n_labels = self.steps.min(n_labels as i32); 163 | 164 | for i in 0..n_labels { 165 | let w = self.step_to_world(i as f32); 166 | let v = self.world_to_view(w, 0.0); 167 | 168 | match self.direction { 169 | Direction::Vertical => { 170 | texts.push(TextData { 171 | x: v, 172 | y: self.view.max.y + LABEL_OFFSET, 173 | anchor: "start", 174 | baseline: "hanging", 175 | }); 176 | } 177 | Direction::Horizontal => { 178 | texts.push(TextData { 179 | x: self.view.min.x - LABEL_OFFSET, 180 | y: v, 181 | anchor: "end", 182 | baseline: "text-bottom", 183 | }); 184 | } 185 | } 186 | } 187 | 188 | texts 189 | } 190 | 191 | pub fn generated_labels(&self) -> Labels { 192 | let mut labels = Labels::new(); 193 | 194 | for i in 0..=self.steps { 195 | if let Some(func) = self.label_interpolation { 196 | labels.push(func(self.world_start + i as f32 * self.step_len)); 197 | } else { 198 | labels.push(format!("{}", self.world_start + i as f32 * self.step_len)); 199 | } 200 | } 201 | 202 | labels 203 | } 204 | } 205 | 206 | pub(crate) struct AxisBuilder<'a> { 207 | view: Rect, 208 | lowest: Option, 209 | highest: Option, 210 | direction: Direction, 211 | label_interpolation: Option String>, 212 | labels_centered: bool, 213 | label_size: i32, 214 | grid_ticks: bool, 215 | max_ticks: i32, 216 | stacked_series: bool, 217 | series: Option<&'a Series>, 218 | labels: Option<&'a Labels>, 219 | } 220 | 221 | impl<'a> Default for AxisBuilder<'a> { 222 | fn default() -> Self { 223 | Self { 224 | view: Rect::default(), 225 | highest: None, 226 | lowest: None, 227 | direction: Direction::Horizontal, 228 | label_interpolation: None, 229 | labels_centered: false, 230 | label_size: 60, 231 | grid_ticks: false, 232 | max_ticks: 8, 233 | stacked_series: false, 234 | series: None, 235 | labels: None, 236 | } 237 | } 238 | } 239 | 240 | impl<'a> AxisBuilder<'a> { 241 | pub fn with_view(mut self, view: Rect) -> Self { 242 | self.view = view; 243 | self 244 | } 245 | 246 | pub fn with_lowest(mut self, lowest: Option) -> Self { 247 | self.lowest = lowest; 248 | self 249 | } 250 | 251 | pub fn with_highest(mut self, highest: Option) -> Self { 252 | self.highest = highest; 253 | self 254 | } 255 | 256 | pub fn with_series(mut self, series: &'a Series) -> Self { 257 | self.series = Some(series); 258 | self 259 | } 260 | 261 | pub fn with_stacked_series(mut self, stacked: bool) -> Self { 262 | self.stacked_series = stacked; 263 | self 264 | } 265 | 266 | pub fn with_labels(mut self, labels: Option<&'a Labels>) -> Self { 267 | self.labels = labels; 268 | self 269 | } 270 | 271 | pub fn with_label_size(mut self, size: i32) -> Self { 272 | self.label_size = size; 273 | self 274 | } 275 | 276 | pub fn with_max_ticks(mut self, n_ticks: i32) -> Self { 277 | self.max_ticks = n_ticks; 278 | self 279 | } 280 | 281 | pub fn with_grid_ticks(mut self, show_ticks: bool) -> Self { 282 | self.grid_ticks = show_ticks; 283 | self 284 | } 285 | 286 | pub fn with_direction(mut self, direction: Direction) -> Self { 287 | self.direction = direction; 288 | self 289 | } 290 | 291 | pub fn with_centered_labels(mut self, labels: Option<&'a Labels>) -> Self { 292 | self.labels = labels; 293 | self.labels_centered = true; 294 | self 295 | } 296 | 297 | pub fn with_label_interpolation(mut self, func: Option String>) -> Self { 298 | self.label_interpolation = func; 299 | self 300 | } 301 | 302 | pub fn build(self) -> Axis { 303 | if let Some(series) = self.series { 304 | let highest = if let Some(high) = self.highest { 305 | high 306 | } else if self.stacked_series { 307 | MultiZip(series.iter().map(|a| a.iter().copied()).collect()) 308 | .map(|t| t.iter().sum()) 309 | .reduce(f32::max) 310 | .unwrap() 311 | } else { 312 | series 313 | .iter() 314 | .map(|a| a.iter().copied().reduce(f32::max).unwrap()) 315 | .reduce(f32::max) 316 | .unwrap() 317 | }; 318 | 319 | //if self.stacked_series { 320 | // let mzip = MultiZip(series.iter().map(|a| a.iter()).collect()); 321 | 322 | // for z in mzip { 323 | // debug!("{z:?}"); 324 | // } 325 | //} 326 | 327 | let lowest = if let Some(low) = self.lowest { 328 | low 329 | } else { 330 | series 331 | .iter() 332 | .map(|a| a.iter().copied().reduce(f32::min).unwrap()) 333 | .reduce(f32::min) 334 | .unwrap() 335 | }; 336 | 337 | debug!("highest: {}", highest); 338 | debug!("lowest: {}", lowest); 339 | let value_range = highest - lowest; 340 | let minimum_tick = value_range / (self.max_ticks as f32 - 2.0); 341 | let magnitude = magnitude(minimum_tick); 342 | let residual = minimum_tick / magnitude; 343 | debug!("magnitude: {}", magnitude); 344 | 345 | let step = match residual { 346 | n if n > 9.0 => 10.0, 347 | n if n > 8.0 => 9.0, 348 | n if n > 7.0 => 8.0, 349 | n if n > 6.0 => 7.0, 350 | n if n > 5.0 => 6.0, 351 | n if n > 4.0 => 5.0, 352 | n if n > 3.0 => 4.0, 353 | n if n > 2.5 => 3.0, 354 | n if n > 2.0 => 2.5, 355 | n if n > 1.5 => 2.0, 356 | n if n > 1.0 => 1.5, 357 | _ => 1.0, 358 | } * magnitude; 359 | 360 | debug!("step_len: {}", step); 361 | 362 | let max = if self.highest.is_some() { 363 | highest 364 | } else { 365 | let max = (highest / step).ceil() * step; 366 | if max < highest { 367 | debug!("step added to max"); 368 | max + step 369 | } else { 370 | max 371 | } 372 | }; 373 | 374 | let min = if self.lowest.is_some() { 375 | lowest 376 | } else { 377 | let min = (lowest / step).floor() * step; 378 | if min > lowest { 379 | debug!("step added to min"); 380 | min - step 381 | } else { 382 | min 383 | } 384 | }; 385 | 386 | let range = max - min; 387 | debug!("range: {} min: {}, max: {}", range, min, max); 388 | let steps = unsafe { (range / step).round().to_int_unchecked::() + 1 }; 389 | debug!("steps: {}", steps); 390 | 391 | Axis { 392 | view: self.view, 393 | step_len: step, 394 | steps, 395 | world_start: min, 396 | world: range, 397 | label_interpolation: self.label_interpolation, 398 | grid_ticks: self.grid_ticks, 399 | label_size: self.label_size, 400 | direction: self.direction, 401 | } 402 | } else if let Some(labels) = self.labels { 403 | let len = labels.len(); 404 | let steps = if self.labels_centered { 405 | len as i32 + 1 406 | } else { 407 | len as i32 408 | }; 409 | 410 | Axis { 411 | view: self.view, 412 | step_len: steps as f32 / (steps as f32 - 1.0), 413 | steps, 414 | world: steps as f32, 415 | grid_ticks: self.grid_ticks, 416 | label_size: self.label_size, 417 | direction: self.direction, 418 | ..Axis::default() 419 | } 420 | } else { 421 | Axis::default() 422 | } 423 | } 424 | } 425 | 426 | pub(crate) struct Grid { 427 | pub x: Axis, 428 | pub y: Axis, 429 | } 430 | 431 | impl Grid { 432 | pub fn new<'a>(x: AxisBuilder<'a>, y: AxisBuilder<'a>) -> Grid { 433 | Grid { 434 | x: x.with_direction(Direction::Vertical).build(), 435 | y: y.with_direction(Direction::Horizontal).build(), 436 | } 437 | } 438 | 439 | pub fn world_to_view(&self, cx: f32, cy: f32, inverted: bool) -> Point { 440 | if inverted { 441 | Point { 442 | x: self.x.world_to_view(cx, self.x.world_start), 443 | y: self.y.world_to_view(self.y.step_to_world(cy), 0.0), 444 | } 445 | } else { 446 | Point { 447 | x: self.x.world_to_view(self.x.step_to_world(cx), 0.0), 448 | y: self.y.world_to_view(cy, self.y.world_start), 449 | } 450 | } 451 | } 452 | 453 | pub fn lines(&self) -> Vec { 454 | [self.x.lines().as_slice(), self.y.lines().as_slice()].concat() 455 | } 456 | 457 | pub fn text_data(&self, x_n_labels: Option, y_n_labels: Option) -> Vec { 458 | [ 459 | self.x.text_data(x_n_labels.unwrap_or(0)).as_slice(), 460 | self.y.text_data(y_n_labels.unwrap_or(0)).as_slice(), 461 | ] 462 | .concat() 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | A simple chart components library for Dioxus 3 | 4 | This crate provides some basic SVG-based chart components, customizable with 5 | CSS, to be used with the [Dioxus] GUI library. The components configuration 6 | was designed to be similar to what one would find in JavaScript chart libraries. 7 | 8 | The components available are: 9 | 10 | - [PieChart](crate::charts::PieChart): for Pie, Donut and Gauge charts 11 | - [BarChart](crate::charts::BarChart): for Bar and Stacked Bar charts, vertical 12 | or horizontal 13 | - [LineChart](crate::charts::LineChart) 14 | 15 | # Usage 16 | This crate is [on crates.io](https://crates.io/crates/dioxus-charts) and can be 17 | used by adding `dioxus-charts` to your dependencies in your project's `Cargo.toml`. 18 | 19 | ```toml 20 | [dependencies] 21 | dioxus-charts = "0.1.3" 22 | ``` 23 | 24 | [Dioxus]: https://dioxuslabs.com/ 25 | */ 26 | 27 | #![deny(missing_docs)] 28 | 29 | mod grid; 30 | mod types; 31 | mod utils; 32 | 33 | pub mod charts { 34 | //! Chart components 35 | //! 36 | //! This module contains all the charts available: 37 | //! - [PieChart](crate::charts::PieChart) 38 | //! - [BarChart](crate::charts::BarChart) 39 | //! - [LineChart](crate::charts::LineChart) 40 | 41 | /// Module for the [BarChart](pie::PieChart) component and its configuration types 42 | pub mod bar; 43 | /// Module for the [LineChart](pie::PieChart) component and its configuration types 44 | pub mod line; 45 | /// Module for the [PieChart](pie::PieChart) component and its configuration types 46 | pub mod pie; 47 | 48 | pub use bar::BarChart; 49 | pub use line::LineChart; 50 | pub use pie::PieChart; 51 | } 52 | 53 | pub use crate::charts::{BarChart, LineChart, PieChart}; 54 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | pub(crate) type Series = Vec>; 4 | pub(crate) type Labels = Vec; 5 | 6 | #[derive(Clone, Copy, Default)] 7 | pub(crate) struct Point { 8 | pub x: f32, 9 | pub y: f32, 10 | } 11 | 12 | impl Point { 13 | pub fn new(x: f32, y: f32) -> Self { 14 | Self { x, y } 15 | } 16 | } 17 | 18 | // Used for cleaner SVG path formatting 19 | impl fmt::Display for Point { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | write!(f, "{},{}", self.x, self.y) 22 | } 23 | } 24 | 25 | #[derive(Clone, Copy, Default)] 26 | pub(crate) struct Rect { 27 | pub min: Point, 28 | pub max: Point, 29 | } 30 | 31 | impl Rect { 32 | pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self { 33 | Self { 34 | min: Point::new(x1, y1), 35 | max: Point::new(x2, y2), 36 | } 37 | } 38 | 39 | pub fn width(&self) -> f32 { 40 | self.max.x - self.min.x 41 | } 42 | 43 | pub fn height(&self) -> f32 { 44 | self.max.y - self.min.y 45 | } 46 | } 47 | 48 | impl fmt::Display for Rect { 49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 50 | write!(f, "({}, {})", self.min, self.max) 51 | } 52 | } 53 | 54 | #[derive(Clone)] 55 | pub(crate) struct TextData { 56 | pub x: f32, 57 | pub y: f32, 58 | pub anchor: &'static str, 59 | pub baseline: &'static str, 60 | } 61 | 62 | impl Default for TextData { 63 | fn default() -> Self { 64 | Self { 65 | x: 0.0, 66 | y: 0.0, 67 | anchor: "start", 68 | baseline: "text-bottom", 69 | } 70 | } 71 | } 72 | 73 | pub(crate) struct MultiZip(pub Vec); 74 | 75 | //pub(crate) struct MultiZip<'a, T> { 76 | // vec: &'a Vec 77 | //} 78 | // 79 | //pub(crate) struct MultiZipIter<'a, T> { 80 | // values: &'a Vec, 81 | // index: usize 82 | //} 83 | // 84 | //impl<'a, T> IntoIterator for MultiZip<'a, T> 85 | //where 86 | // T: Iterator 87 | //{ 88 | // type Item = Vec; 89 | // type IntoIter = MultiZipIter<'a, T>; 90 | // 91 | // fn into_iter(self) -> Self::IntoIter { 92 | // MultiZipIter { 93 | // values: self.vec, 94 | // index: 0 95 | // } 96 | // } 97 | //} 98 | 99 | //impl<'a, T> Iterator for MultiZipIter<'a, T> 100 | impl Iterator for MultiZip 101 | where 102 | T: Iterator, 103 | { 104 | type Item = Vec; 105 | 106 | fn next(&mut self) -> Option { 107 | self.0.iter_mut().map(Iterator::next).collect() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::types::Point; 2 | 3 | pub(crate) fn polar_to_cartesian(c: Point, radius: f32, angle_degrees: f32) -> Point { 4 | let angle_radians = (angle_degrees - 90.0).to_radians(); 5 | Point { 6 | x: c.x + radius * angle_radians.cos(), 7 | y: c.y + radius * angle_radians.sin(), 8 | } 9 | } 10 | 11 | pub(crate) fn normalize_series(series: &[f32]) -> Vec { 12 | let r = series.iter().copied().reduce(f32::max).unwrap() / 100.0; 13 | series.iter().map(|v| v / r).collect() 14 | } 15 | 16 | pub(crate) fn magnitude(value: f32) -> f32 { 17 | 10.0_f32.powf(value.abs().log10().floor()) 18 | } 19 | --------------------------------------------------------------------------------