├── RELEASE-NOTES.md ├── .gitignore ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── general-questions.md │ ├── feature_request.md │ └── bug_report.md ├── codecov.yml └── workflows │ ├── audit.yaml │ ├── tests.yml │ └── coverage.yaml ├── CONTRIBUTORS.md ├── CHANGELOG.md ├── TODO.md ├── SECURITY.md ├── Cargo.toml ├── tests └── datatable.rs ├── CODE_OF_CONDUCT.md ├── src ├── lib.rs ├── utils.rs ├── gauge.rs ├── pie.rs ├── radar.rs ├── basechart.rs └── options.rs ├── README.md ├── CONTRIBUTING.md └── LICENSE /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | /target/ 4 | Cargo.lock 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.toml] 2 | indent_size = 2 3 | indent_style = space 4 | [*.{rs,md}] 5 | indent_style = space 6 | indent_size = 4 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: dudochkin 4 | liberapay: dudochkin 5 | open_collective: dudochkin -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general-questions.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General Questions 3 | about: Any other issues 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "diff, flags, files" 3 | require_changes: true 4 | 5 | coverage: 6 | status: 7 | project: 8 | default: 9 | informational: true 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | UX Charts contributors (sorted alphabetically) 2 | ============================================ 3 | 4 | * **[Victor Dudochkin](https://github.com/dudochkin.victor)** 5 | 6 | * Core development 7 | 8 | 9 | 10 | **[Full contributors list](https://github.com/angular-rust/ux-charts/contributors).** -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Package vX.X.X (YYYY-MM-DD) 4 | 5 | ### Improved 6 | 7 | - A here your changes 8 | 9 | ### Added 10 | 11 | - A here your changes 12 | 13 | ### Fixed 14 | 15 | - A here your changes 16 | 17 | ### Improvement 18 | 19 | - A here your changes 20 | 21 | ### Removed 22 | 23 | - A here your changes -------------------------------------------------------------------------------- /.github/workflows/audit.yaml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | schedule: 8 | - cron: '0 0 * * 0' 9 | 10 | jobs: 11 | security_audit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions-rs/audit-check@v1 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea to UX Charts maintainers 4 | title: "[Feature Request]" 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### What is the feature ? 11 | *Detailed feature descrption* 12 | 13 | ### (Optional) Why this feature is useful and how people would use the feature ? 14 | *Explain why this feature is important* 15 | 16 | ### (Optional) Additional Information 17 | *More details are appreciated:)* 18 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | `set_canvas_size` should be implemented at client level 2 | 3 | ```rust 4 | fn set_canvas_size(&self, ctx: &Option) { 5 | // Scale the drawing canvas by [dpr] to ensure sharp rendering on 6 | // high pixel density displays. 7 | if let Some(ctx) = ctx { 8 | // ctx.canvas 9 | // ..style.width = "${w}px" 10 | // ..style.height = "${h}px" 11 | // ..width = scaledW 12 | // ..height = scaledH; 13 | // ctx.set_transform(dpr, 0, 0, dpr, 0, 0); 14 | } 15 | } 16 | ``` 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: About unexpected behaviors 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | Describe what is expected, what you actually get. 12 | It would be nice to have screenshot or result image uploaded 13 | 14 | **To Reproduce** 15 | Some minimal reproduce code is highly recommended 16 | 17 | **Version Information** 18 | Please give us what version you are using. If you are pulling `UX Charts` directly from git repo, please mention this as well 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you believe you have found a security vulnerability in Angular Rust, we encourage you to let us know right away. We will investigate all legitimate reports and do our best to quickly fix the problem. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | To report a security vulnerability, please create a Github issue [here](https://github.com/angular-rust/ux-charts/issues/new). 8 | 9 | If you can, please include the following details: 10 | * An MCVE (minimum complete verifiable example) – this is a short code snippet which demonstrates the error in the 11 | the simplest possible (or just a simple) way. 12 | * Which versions of Angular Rust the vulnerability is present in 13 | * What effects the vulnerability has and how serious the vulnerability is -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Install Clutter 13 | run: sudo apt-get update && sudo apt-get install libclutter-1.0-dev libpango1.0-dev 14 | - name: Build 15 | run: cargo build --verbose 16 | - name: Run tests 17 | run: cargo test --verbose 18 | 19 | clippy_check: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v1 25 | - run: rustup component add clippy 26 | - name: Install Clutter 27 | run: sudo apt-get update && sudo apt-get install libclutter-1.0-dev libpango1.0-dev 28 | - uses: actions-rs/clippy-check@v1 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | args: --all-features 32 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | #on: 4 | # push: 5 | # branches: 6 | # - main 7 | # pull_request: 8 | 9 | on: [push, pull_request] 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | RUST_BACKTRACE: full 14 | 15 | jobs: 16 | coverage: 17 | name: Coverage 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v2 22 | 23 | - name: Install Rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: nightly 27 | profile: minimal 28 | default: true 29 | 30 | - name: Install Clutter 31 | run: sudo apt-get update && sudo apt-get install libclutter-1.0-dev libpango1.0-dev 32 | 33 | - name: Restore cache 34 | uses: Swatinem/rust-cache@v1 35 | 36 | - name: Run cargo-tarpaulin 37 | uses: actions-rs/tarpaulin@v0.1 38 | with: 39 | args: '--run-types Doctests,Tests' 40 | timeout: 120 41 | 42 | - name: Upload to codecov.io 43 | uses: codecov/codecov-action@239febf655bba88b16ff5dea1d3135ea8663a1f9 44 | with: 45 | token: ${{ secrets.CODECOV_TOKEN }} 46 | 47 | - name: Archive code coverage results 48 | uses: actions/upload-artifact@v2 49 | with: 50 | name: code-coverage-report 51 | path: cobertura.xml 52 | retention-days: 30 53 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ux-charts" 3 | version = "0.1.2" 4 | authors = ["Victor Dudochkin "] 5 | readme = "README.md" 6 | homepage = "https://angular-rust.github.io/ux-charts" 7 | repository = "https://github.com/angular-rust/ux-charts" 8 | documentation = "https://docs.rs/ux-charts" 9 | description = "Backend and runtime agnostic chart library" 10 | keywords = ["webassembly", "gtk", "cairo", "canvas", "charts"] 11 | categories = ["multimedia", "wasm", "web-programming", "gui", "visualization"] 12 | edition = "2018" 13 | license = "MPL-2.0" 14 | 15 | [badges] 16 | maintenance = { status = "actively-developed" } 17 | 18 | [lib] 19 | name = "charts" 20 | 21 | [package.metadata.docs.rs] 22 | features = ["dox"] 23 | 24 | [features] 25 | dox = ["ux-animate/dox", "cairo-rs/dox"] 26 | 27 | [dependencies] 28 | log = "0.4" 29 | lazy_static = "1.4" 30 | ux-dataflow = "0.1.1" 31 | ux-animate = "0.1.2" 32 | 33 | [dependencies.ux-primitives] 34 | version = "0.1.3" 35 | features = [ "canvas" ] 36 | 37 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 38 | cairo-rs = "0.9" 39 | 40 | [target.'cfg(target_arch = "wasm32")'.dependencies] 41 | wasm-bindgen = "0.2" 42 | js-sys = "0.3" 43 | wasm-bindgen-futures = "0.4" 44 | wasm-logger = "0.2" 45 | gloo = "0.2" 46 | wasm-bindgen-test = "0.3" 47 | 48 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] 49 | version = "0.3" 50 | features = [ 51 | "KeyboardEvent", 52 | "ValidityState", 53 | "CustomEvent", 54 | "Node", 55 | "Element", 56 | "HtmlElement", 57 | "Window", 58 | "Document", 59 | "CanvasRenderingContext2d", 60 | "TextMetrics", 61 | "HtmlCanvasElement", 62 | "HtmlImageElement", 63 | "SvgImageElement", 64 | "HtmlVideoElement", 65 | "ImageBitmap", 66 | "CanvasWindingRule", 67 | "Path2d", 68 | "CanvasPattern", 69 | "CanvasGradient", 70 | "HitRegionOptions", 71 | "ImageData", 72 | "DomMatrix" 73 | ] 74 | 75 | [dev-dependencies] 76 | 77 | [build-dependencies] 78 | -------------------------------------------------------------------------------- /tests/datatable.rs: -------------------------------------------------------------------------------- 1 | // DataTable createDataTable() => DataTable([ 2 | // ["Browser", "Share"], 3 | // ["Chrome", 35], 4 | // ["IE", 30], 5 | // ["Firefox", 20] 6 | // ]); 7 | 8 | // test("columns", || { 9 | // let table = createDataTable(); 10 | // expect(table.columns.len(), equals(2)); 11 | // expect(table.columns[0].name, equals("Browser")); 12 | // }); 13 | 14 | // test("getColumnIndexByName", || { 15 | // let table = createDataTable(); 16 | // expect(table.getColumnIndexByName("Share"), equals(1)); 17 | // expect(table.getColumnIndexByName("X"), equals(-1)); 18 | // }); 19 | 20 | // test("getColumnValues", || { 21 | // let table = createDataTable(); 22 | // expect(table.getColumnValues(1), orderedEquals([35, 30, 20])); 23 | // }); 24 | 25 | // test("rows", || { 26 | // let table = createDataTable(); 27 | // expect(table.rows.len(), equals(3)); 28 | // expect(table.rows[1].toList(), orderedEquals(["IE", 30])); 29 | // }); 30 | 31 | // test("columns.insert", || { 32 | // let table = createDataTable(); 33 | // table.columns.insert(1, DataColumn("Latest Version", num)); 34 | // expect(table.columns.len(), equals(3)); 35 | // expect(table.columns[1].name, equals("Latest Version")); 36 | // }); 37 | 38 | // test("rows.add", || { 39 | // let table = createDataTable(); 40 | // table.rows.add(["Opera", 10, "discarded"]); 41 | // expect(table.rows.len(), equals(4)); 42 | // expect(table.rows.last.toList(), orderedEquals(["Opera", 10])); 43 | // }); 44 | 45 | // test("rows.remove_range", || { 46 | // let table = createDataTable(); 47 | // table.rows.remove_range(0, 3); 48 | // expect(table.rows, isEmpty); 49 | // }); 50 | 51 | // test("cells", || { 52 | // let table = createDataTable(); 53 | // expect(table.rows[0][0], equals("Chrome")); 54 | // table.rows[0][0] = "Unknown"; 55 | // expect(table.rows[0][0], equals("Unknown")); 56 | // }); 57 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | * Accepting and using the preferred gender pronouns of all people who have specified them involved in the project. 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement by emailing 64 | `dudochkin.victor@gmail.com`. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 121 | 122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // #![allow(unused_imports)] 2 | #![allow(clippy::needless_return)] 3 | 4 | use dataflow::*; 5 | use primitives::{CanvasContext, Color, Point}; 6 | use animate::Pattern; 7 | use std::fmt; 8 | 9 | #[macro_use] 10 | extern crate log; 11 | 12 | mod basechart; 13 | pub use basechart::*; 14 | 15 | mod bar; 16 | pub use bar::*; 17 | 18 | mod gauge; 19 | pub use gauge::*; 20 | 21 | mod line; 22 | pub use line::*; 23 | 24 | mod options; 25 | pub use options::*; 26 | 27 | mod pie; 28 | pub use pie::*; 29 | 30 | mod radar; 31 | pub use radar::*; 32 | 33 | pub mod utils; 34 | 35 | pub(crate) const CLOCKWISE: i64 = 1; 36 | pub(crate) const COUNTERCLOCKWISE: i64 = -1; 37 | pub(crate) const HIGHLIGHT_OUTER_RADIUS_FACTOR: f64 = 1.05; 38 | 39 | pub const PI: f64 = std::f64::consts::PI; 40 | /// The 2*pi constant - TAU 41 | pub const TAU: f64 = std::f64::consts::TAU; 42 | 43 | /// The pi/2 constant. 44 | pub const PI_2: f64 = std::f64::consts::FRAC_PI_2; 45 | 46 | pub const DEFAULT_FONT_FAMILY: &str = "monospace"; 47 | 48 | /// The padding of the chart itself. 49 | pub const CHART_PADDING: f64 = 12.0; 50 | 51 | /// The margin between the legend and the chart-axes box in pixels. 52 | pub const LEGEND_MARGIN: f64 = 12.0; 53 | 54 | pub const CHART_TITLE_MARGIN: f64 = 12.0; 55 | 56 | /// The padding around the chart title and axis titles. 57 | pub const TITLE_PADDING: f64 = 6.0; 58 | 59 | /// The top-and/or-bottom margin of x-axis labels and the right-and/or-left 60 | /// margin of y-axis labels. 61 | /// 62 | /// x-axis labels always have top margin. If the x-axis title is N/A, x-axis 63 | /// labels also have bottom margin. 64 | /// 65 | /// y-axis labels always have right margin. If the y-axis title is N/A, y-axis 66 | /// labels also have left margin. 67 | pub const AXIS_LABEL_MARGIN: usize = 12; 68 | 69 | pub type LabelFormatter = fn(label: String) -> String; 70 | 71 | pub type ValueFormatter = fn(value: f64) -> String; 72 | 73 | pub fn default_label_formatter(label: String) -> String { 74 | label 75 | } 76 | 77 | pub fn default_value_formatter(value: f64) -> String { 78 | format!("{}", value) 79 | } 80 | 81 | #[derive(Debug, Clone, Copy, PartialEq)] 82 | pub enum Visibility { 83 | Hidden, 84 | Hiding, 85 | Showing, 86 | Shown, 87 | } 88 | 89 | impl Default for Visibility { 90 | fn default() -> Self { 91 | Visibility::Hidden 92 | } 93 | } 94 | 95 | pub struct MouseEvent; 96 | 97 | /// A chart entity such as a point, a bar, a pie... 98 | pub trait Entity { 99 | fn free(&mut self); 100 | fn save(&self); 101 | } 102 | 103 | pub trait Drawable 104 | where 105 | C: CanvasContext, 106 | { 107 | fn draw(&self, ctx: &C, percent: f64, highlight: bool); 108 | } 109 | 110 | #[derive(Default, Debug, Clone)] 111 | pub struct ChartChannel 112 | where 113 | E: Entity, 114 | { 115 | name: String, 116 | color: Color, 117 | highlight: Color, 118 | state: Visibility, 119 | entities: Vec, 120 | } 121 | 122 | impl ChartChannel 123 | where 124 | E: Entity, 125 | { 126 | pub fn new(name: &str, color: Color, highlight: Color, entities: Vec) -> Self { 127 | Self { 128 | name: name.into(), 129 | color, 130 | highlight, 131 | state: Visibility::Shown, 132 | entities, 133 | } 134 | } 135 | 136 | pub fn free_entities(&self, start: usize, end: Option) { 137 | let end = match end { 138 | Some(end) => end, 139 | None => self.entities.len(), 140 | }; 141 | 142 | let mut start = start; 143 | while start < end { 144 | // self.entities[start].free(); 145 | start += 1; 146 | } 147 | unimplemented!() 148 | } 149 | } 150 | 151 | pub trait Chart<'a, C, M, D, E> 152 | where 153 | E: Entity, 154 | C: CanvasContext, 155 | M: fmt::Display, 156 | D: fmt::Display + Copy, 157 | { 158 | /// Calculates various drawing sizes. 159 | /// 160 | /// Overriding methods must call this method first to have [area] 161 | /// calculated. 162 | /// 163 | fn calculate_drawing_sizes(&self, ctx: &C); 164 | 165 | /// Updates the channel at index [index]. If [index] is `null`, updates all 166 | /// channel. 167 | /// 168 | fn update_channel(&self, index: usize); 169 | 170 | /// Draws the axes and the grid. 171 | /// 172 | fn draw_axes_and_grid(&self, ctx: &C); 173 | 174 | /// Draws the channel given the current animation percent [percent]. 175 | /// 176 | /// If this method returns `false`, the animation is continued until [percent] 177 | /// reaches 1.0. 178 | /// 179 | /// If this method returns `true`, the animation is stopped immediately. 180 | /// This is useful as there are cases where no animation is expected. 181 | /// In those cases, the overriding method will return `true` to stop the 182 | /// animation. 183 | /// 184 | fn draw_channels(&self, ctx: &C, percent: f64) -> bool; 185 | 186 | /// Draws the current animation frame. 187 | /// 188 | /// If [time] is `null`, draws the last frame (i.e. no animation). 189 | fn draw_frame(&self, ctx: &C, time: Option); 190 | 191 | // when we impl for concrete chart implementation then it call concrete 192 | fn create_entities( 193 | &self, 194 | channel_index: usize, 195 | start: usize, 196 | end: usize, 197 | color: Color, 198 | highlight_color: Color, 199 | ) -> Vec; 200 | 201 | fn create_entity( 202 | &self, 203 | channel_index: usize, 204 | entity_index: usize, 205 | value: Option, 206 | color: Color, 207 | highlight_color: Color, 208 | ) -> E; 209 | 210 | fn create_channels(&self, start: usize, end: usize); 211 | 212 | /// Returns the position of the tooltip based on 213 | /// [focused_entity_index]. 214 | // tooltip_width - oltip.offset_width 215 | // tooltip_height - tooltip.offset_height 216 | fn get_tooltip_position(&self, tooltip_width: f64, tooltip_height: f64) -> Point; 217 | 218 | fn set_stream(&mut self, stream: DataStream<'a, M, D>); 219 | 220 | /// called to redraw using non_eq pattern 221 | fn draw(&self, ctx: &C); 222 | 223 | /// Resizes the chart to fit the new size of the container. 224 | /// w = container.clientWidth; 225 | /// h = container.clientHeight; 226 | fn resize(&self, w: f64, h: f64); 227 | } 228 | 229 | #[cfg(test)] 230 | mod tests { 231 | #[test] 232 | fn it_works() { 233 | assert_eq!(2 + 2, 4); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![](https://dudochkin-victor.github.io/assets/ux-charts/logo-wide.svg)](#top) 4 | 5 | # UX Charts 6 | 7 | [![API Docs][docrs-badge]][docrs-url] 8 | [![Crates.io][crates-badge]][crates-url] 9 | [![Code coverage][codecov-badge]][codecov-url] 10 | [![Tests][tests-badge]][tests-url] 11 | [![MPL-2.0 licensed][license-badge]][license-url] 12 | [![Gitter chat][gitter-badge]][gitter-url] 13 | [![loc][loc-badge]][loc-url] 14 |
15 | 16 | [docrs-badge]: https://img.shields.io/docsrs/ux-charts?style=flat-square 17 | [docrs-url]: https://docs.rs/ux-charts/ 18 | [crates-badge]: https://img.shields.io/crates/v/ux-charts.svg?style=flat-square 19 | [crates-url]: https://crates.io/crates/ux-charts 20 | [license-badge]: https://img.shields.io/badge/license-MPL--2.0-blue.svg?style=flat-square 21 | [license-url]: https://github.com/angular-rust/ux-charts/blob/master/LICENSE 22 | [gitter-badge]: https://img.shields.io/gitter/room/angular_rust/community.svg?style=flat-square 23 | [gitter-url]: https://gitter.im/angular_rust/community 24 | [tests-badge]: https://img.shields.io/github/workflow/status/angular-rust/ux-charts/Tests?label=tests&logo=github&style=flat-square 25 | [tests-url]: https://github.com/angular-rust/ux-charts/actions/workflows/tests.yml 26 | [codecov-badge]: https://img.shields.io/codecov/c/github/angular-rust/ux-charts?logo=codecov&style=flat-square&token=B8OVKMLHDS 27 | [codecov-url]: https://codecov.io/gh/angular-rust/ux-charts 28 | [loc-badge]: https://img.shields.io/tokei/lines/github/angular-rust/ux-charts?style=flat-square 29 | [loc-url]: https://github.com/angular-rust/ux-charts 30 | 31 | **UX Charts** is a drawing library designed for clean charts. UX Charts supports various types of backend including GTK/Cairo and HTML5 Canvas. UX Charts are designed with the concept - `one code for all`. UX Charts uses the [UX Dataflow](https://github.com/angular-rust/ux-dataflow) library as the data source and the [UX Animate](https://github.com/angular-rust/ux-animate) library as the canvas implementation. 32 | 33 | **UX Charts** is part of the Angular Rust framework. 34 | 35 | **Angular Rust** is a high productivity, `platform-agnostic` frontend framework for the [Rust language](https://www.rust-lang.org/). It now supports desktop and web development. Angular Rust currently uses GTK for desktop development and WebAssembly for web development. We are planning to add support for mobile development. 36 | 37 | ![Angular Rust structure](https://dudochkin-victor.github.io/assets/angular-rust/structure.svg) 38 | 39 | ## Gallery 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ## Features 49 | 50 | - [x] Various charts: barchart, linechart, piechart, gaugechart, and radarchart. 51 | - [ ] Animation support 52 | - [ ] Gradient fills 53 | - [ ] User interaction 54 | 55 | > The unimplemented features depend on `User-Experience` during the development of the [UX Animate](https://github.com/angular-rust/ux-animate) crate. So far, we have implemented basic features to visualize your data streams. 56 | 57 | ## Quick Start 58 | 59 | Install UX Charts: 60 | 61 | cargo add ux-charts 62 | 63 | ## Learn More 64 | 65 | * [Manual, Docs, etc](https://angular-rust.github.io/) 66 | * [Samples](https://github.com/angular-rust/ux-samples) 67 | * [Apps using Angular Rust](https://github.com/angular-rust/ux-charts/wiki/Apps-in-the-Wild) 68 | * [Articles Featuring Angular Rust](https://github.com/angular-rust/ux-charts/wiki/Articles) 69 | 70 | ## Community 71 | 72 | [![](https://img.shields.io/badge/Facebook-1877F2?style=for-the-badge&logo=facebook&logoColor=white)](https://www.facebook.com/groups/angular.rust) 73 | [![](https://img.shields.io/badge/Stack_Overflow-FE7A16?style=for-the-badge&logo=stack-overflow&logoColor=white)](https://stackoverflow.com/questions/tagged/angular-rust) 74 | [![](https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/channel/UCBJTkSl_JWShuolUy4JksTQ) 75 | [![](https://img.shields.io/badge/Medium-12100E?style=for-the-badge&logo=medium&logoColor=white)](https://medium.com/@angular.rust) 76 | [![](https://img.shields.io/gitter/room/angular_rust/angular_rust?style=for-the-badge)](https://gitter.im/angular_rust/community) 77 | 78 | 79 | ## Contributing 80 | 81 | We believe the wider community can create better code. The first tool for improving the community is to tell the developers about the project by giving it a star. More stars - more members. 82 | 83 | [![](https://dudochkin-victor.github.io/assets/star-me-wide.svg)](https://github.com/angular-rust/ux-charts#top) 84 | 85 | Angular Rust is a community effort and we welcome all kinds of contributions, big or small, from developers of all backgrounds. We want the Angular Rust community to be a fun and friendly place, so please review our [Code of Conduct](CODE_OF_CONDUCT.md) to learn what behavior will not be tolerated. 86 | 87 | ### New to Angular Rust? 88 | 89 | Start learning about the framework by helping us improve our [documentation](https://angular-rust.github.io/). Pull requests which improve test coverage are also very welcome. 90 | 91 | ### Looking for inspiration? 92 | 93 | Check out the community curated list of awesome things related to Angular Rust / WebAssembly at [awesome-angular-rust](https://github.com/angular-rust/awesome-angular-rust). 94 | 95 | ### Confused about something? 96 | 97 | Feel free to drop into our [Gitter chatroom](https://gitter.im/angular_rust/community) or open a [new "Question" issue](https://github.com/angular-rust/ux-charts/issues/new/choose) to get help from contributors. Often questions lead to improvements to the ergonomics of the framework, better documentation, and even new features! 98 | 99 | ### Ready to dive into the code? 100 | 101 | After reviewing the [Contributing Code Guidelines](CONTRIBUTING.md), check out the ["Good First Issues"](https://github.com/angular-rust/ux-charts/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) (they are eager for attention!). Once you find one that interests you, feel free to assign yourself to an issue and don't hesitate to reach out for guidance, the issues vary in complexity. 102 | 103 | ### Let's help each other! 104 | 105 | Come help us on the [issues that matter that the most](https://github.com/angular-rust/ux-charts/labels/%3Adollar%3A%20Funded%20on%20Issuehunt) and receive a small cash reward for your troubles. We use [Issuehunt](https://issuehunt.io/r/angular-rust/ux-charts/) to fund issues from our Open Collective funds. If you really care about an issue, you can choose to add funds yourself! 106 | 107 | ### Found a bug? 108 | 109 | Please [report all bugs!](https://github.com/angular-rust/ux-charts/issues/new/choose) We are happy to help support developers fix the bugs they find if they are interested and have the time. 110 | 111 | ## Todo 112 | - [ ] Documentation -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | #![allow(unused_imports)] 3 | #![allow(clippy::needless_lifetimes)] 4 | 5 | use std::{ 6 | f64::consts::PI, 7 | fmt, 8 | ops::{Add, Mul, Sub}, 9 | }; 10 | 11 | use crate::DEFAULT_FONT_FAMILY; 12 | 13 | use super::StyleOption; 14 | use dataflow::*; 15 | use animate::Pattern; 16 | use primitives::{CanvasContext, Point, TextStyle, TextWeight}; 17 | 18 | /// Converts [angle] in radians to degrees. 19 | pub fn rad2deg(angle: f64) -> f64 { 20 | angle * 180.0 / PI 21 | } 22 | 23 | /// Converts [angle] in degrees to radians. 24 | pub fn deg2rad(angle: f64) -> f64 { 25 | angle * PI / 180.0 26 | } 27 | 28 | /// Tests if [value] is in range [min]..[max], inclusively. 29 | pub fn is_in_range(value: f64, min: f64, max: f64) -> bool { 30 | value >= min && value <= max 31 | } 32 | 33 | pub fn polar2cartesian(center: &Point, radius: f64, angle: f64) -> Point { 34 | let x = center.x + radius * (angle).cos(); 35 | let y = center.y + radius * (angle).sin(); 36 | Point::new(x, y) 37 | } 38 | 39 | /// Rounds [value] to [places] decimal places. 40 | pub fn round2places(value: f64, places: usize) -> f64 { 41 | let p = f64::powf(10.0, places as f64); 42 | let value = value * p; 43 | value.round() / p 44 | } 45 | 46 | /// Converts [hexColor] and [alpha] to an RGBA color string. 47 | pub fn hex2rgba(hex_color: &str, alpha: f64) -> String { 48 | // let componentLength = (hexColor.len() / 3).trunc(); 49 | // let i = 1 + componentLength; 50 | // let j = i + componentLength; 51 | // let r = int.parse(hexColor.substring(1, i), radix: 16); 52 | // let g = int.parse(hexColor.substring(i, j), radix: 16); 53 | // let b = int.parse(hexColor.substring(j), radix: 16); 54 | // if (componentLength == 1) { 55 | // r += r << 4; 56 | // g += g << 4; 57 | // b += b << 4; 58 | // } 59 | // return "rgba($r, $g, $b, $alpha)"; 60 | unimplemented!(); 61 | } 62 | 63 | /// Returns the hyphenated version of [s]. 64 | pub fn hyphenate(s: &str) -> String { 65 | // return s.replaceAllMapped(RegExp("[A-Z]"), (Match m) { 66 | // return "-" + m[0].toLowerCase(); 67 | // }); 68 | unimplemented!(); 69 | } 70 | 71 | /// Returns the maximum value in a [DataTable]. 72 | pub fn find_max_value<'a, M, D>(stream: &DataStream<'a, M, D>) -> D 73 | where 74 | M: fmt::Display, 75 | D: fmt::Display + Copy + Into + Ord + Default, 76 | { 77 | let mut result: Option = None; 78 | for channel in stream.meta.iter() { 79 | let channel_index = channel.tag as u64; 80 | for frame in stream.frames.iter() { 81 | if let Some(value) = frame.data.get(channel_index) { 82 | match result { 83 | Some(max_value) => { 84 | if *value > max_value { 85 | result = Some(*value); 86 | } 87 | } 88 | None => result = Some(*value), 89 | } 90 | } 91 | } 92 | } 93 | 94 | result.unwrap_or_default() 95 | } 96 | 97 | /// Returns the minimum value in a [DataTable]. 98 | pub fn find_min_value<'a, M, D>(stream: &DataStream<'a, M, D>) -> D 99 | where 100 | M: fmt::Display, 101 | D: fmt::Display + Copy + Into + Ord + Default, 102 | { 103 | let mut result: Option = None; 104 | for channel in stream.meta.iter() { 105 | let channel_index = channel.tag as u64; 106 | for frame in stream.frames.iter() { 107 | if let Some(value) = frame.data.get(channel_index) { 108 | match result { 109 | Some(min_value) => { 110 | if *value < min_value { 111 | error!("ASSIGN MIN VALUE {}", value); 112 | result = Some(*value); 113 | } 114 | } 115 | None => result = Some(*value), 116 | } 117 | } 118 | } 119 | } 120 | 121 | result.unwrap_or_default() 122 | } 123 | 124 | /// Calculates a nice axis interval given 125 | /// - the axis range [range] 126 | /// - the desired number of steps [targetSteps] 127 | /// - and the minimum interval [min_interval] 128 | pub fn calculate_interval(range: f64, target_steps: usize, min_interval: Option) -> f64 { 129 | let interval = range / target_steps as f64; 130 | let mag = interval.log10().floor(); 131 | let mut mag_pow = f64::powf(10.0, mag); 132 | if let Some(min_interval) = min_interval { 133 | mag_pow = mag_pow.max(min_interval); 134 | } 135 | let mut msd = (interval / mag_pow).round(); 136 | if msd > 5. { 137 | msd = 10.; 138 | } else if msd > 2. { 139 | msd = 5.; 140 | } else if msd == 0. { 141 | msd = 1.; 142 | } 143 | msd * mag_pow 144 | } 145 | 146 | pub fn calculate_max_text_width( 147 | ctx: &C, 148 | style: &StyleOption, 149 | texts: &[String], 150 | ) -> f64 151 | where 152 | C: CanvasContext 153 | { 154 | let mut result = 0.0; 155 | ctx.set_font( 156 | style.fontfamily.unwrap_or(DEFAULT_FONT_FAMILY), 157 | style.fontstyle.unwrap_or(TextStyle::Normal), 158 | TextWeight::Normal, 159 | style.fontsize.unwrap_or(12.), 160 | ); 161 | 162 | for text in texts.iter() { 163 | let width = ctx.measure_text(text).width; 164 | if result < width { 165 | result = width 166 | } 167 | } 168 | result 169 | } 170 | 171 | /// Calculates the controls for [p2] given the previous point [p1], the next 172 | /// point [p3], and the curve tension [t]; 173 | /// 174 | /// Returns a list that contains two control points for [p2]. 175 | /// 176 | /// Credit: Rob Spencer (http://scaledinnovation.com/analytics/splines/aboutSplines.html) 177 | pub fn calculate_control_points( 178 | p1: Point, 179 | p2: Point, 180 | p3: Point, 181 | t: f64, 182 | ) -> (Point, Point) { 183 | let d21 = p2.distance_to(p1); 184 | let d23 = p2.distance_to(p3); 185 | let fa = t * d21 / (d21 + d23); 186 | let fb = t * d23 / (d21 + d23); 187 | let v13 = p3 - p1; 188 | let cp1 = p2 - v13 * fa; 189 | let cp2 = p2 + v13 * fb; 190 | (cp1, cp2) 191 | } 192 | 193 | /// Returns the number of decimal digits of [value]. 194 | pub fn get_decimal_places(value: f64) -> usize { 195 | if value.fract() == 0. { 196 | return 0; 197 | } 198 | 199 | // See https://code.google.com/p/dart/issues/detail?id=1533 200 | let tmp = format!("{}", value); 201 | let split: Vec<&str> = tmp.split('.').collect(); 202 | split.get(1).unwrap().len() 203 | } 204 | 205 | // /// Returns a CSS font string given a map that contains at least three keys: 206 | // /// `fontStyle`, `font_size`, and `fontFamily`. 207 | // pub fn get_font(opt: &StyleOption) -> String { 208 | // if let Some(style) = opt.font_style { 209 | // if let Some(size) = opt.font_size { 210 | // if let Some(family) = opt.font_family { 211 | // return format!("{} {}px {}", style, size, family); 212 | // } 213 | // return format!("{} {}px", style, size); 214 | // } 215 | // return format!("{}", style); 216 | // } 217 | 218 | // if let Some(size) = opt.font_size { 219 | // if let Some(family) = opt.font_family { 220 | // return format!("{}px {}", size, family); 221 | // } 222 | // return format!("{}px", size); 223 | // } 224 | 225 | // if let Some(family) = opt.font_family { 226 | // return format!("{}", family); 227 | // } 228 | 229 | // "".into() 230 | // } 231 | 232 | // pub struct StreamSubscriptionTracker { 233 | // subs: Vec, 234 | // } 235 | 236 | // impl StreamSubscriptionTracker { 237 | // pub fn add(sub: StreamSubscription) { 238 | // // subs.add(sub); 239 | // } 240 | 241 | // pub fn clear() { 242 | // // for (let sub in subs) { 243 | // // sub.cancel(); 244 | // // } 245 | // // subs.clear(); 246 | // } 247 | // } 248 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to Angular Rust 2 | 3 | This describes how developers may contribute to Angular Rust. 4 | 5 | ## Mission 6 | 7 | Angular Rust's mission is to provide a batteries-included framework for making large scale web and desktop application development as efficient and maintainable as possible. 8 | 9 | The design should be configurable and modular so that it can grow with the developer. However, it should provide a wonderful un-boxing experience and default configuration that can woo new developers and make simple web and desktop apps straight forward. The framework should have an opinion about how to do all of the common tasks in web and desktop development to reduce unnecessary cognitive load. 10 | 11 | Perhaps most important of all, Angular Rust should be a joy to use. We want to reduce the time spent on tedious boilerplate functionality and increase the time 12 | available for creating polished solutions for your application's target users. 13 | 14 | ## How to Contribute 15 | 16 | We believe the wider community can create better code. The first tool for improving the community is to tell the developers about the project by giving it a star. More stars - more members. 17 | 18 | [![](https://dudochkin-victor.github.io/assets/star-me-wide.svg)](https://github.com/angular-rust/ux-charts) 19 | 20 | ### Join the Community 21 | 22 | The first step to improving Angular Rust is to join the community and help grow it! You can find the community on: 23 | 24 | [![](https://img.shields.io/badge/Facebook-1877F2?style=for-the-badge&logo=facebook&logoColor=white)](https://www.facebook.com/groups/angular.rust) 25 | [![](https://img.shields.io/badge/Stack_Overflow-FE7A16?style=for-the-badge&logo=stack-overflow&logoColor=white)](https://stackoverflow.com/questions/tagged/angular-rust) 26 | [![](https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/channel/UCBJTkSl_JWShuolUy4JksTQ) 27 | [![](https://img.shields.io/badge/Medium-12100E?style=for-the-badge&logo=medium&logoColor=white)](https://medium.com/@angular.rust) 28 | [![](https://img.shields.io/gitter/room/angular_rust/angular_rust?style=for-the-badge)](https://gitter.im/angular_rust/community) 29 | 30 | Once you've joined, there are many ways to contribute to Angular Rust: 31 | 32 | * Report bugs (via GitHub) 33 | * Answer questions of other community members (via Gitter or GitHub Discussions) 34 | * Give feedback on new feature discussions (via GitHub and Gitter) 35 | * Propose your own ideas (via Gitter or GitHub) 36 | 37 | ### How Angular Rust is Developed 38 | 39 | We have begun to formalize the development process by adopting pragmatic practices such as: 40 | 41 | * Developing on the `develop` branch 42 | * Merging `develop` branch to `main` branch in 6 week iterations 43 | * Tagging releases with MAJOR.MINOR syntax (e.g. v0.8) 44 | ** We may also tag MAJOR.MINOR.HOTFIX releases as needed (e.g. v0.8.1) to address urgent bugs. Such releases will not introduce or change functionality 45 | * Managing bugs, enhancements, features and release milestones via GitHub's Issue Tracker 46 | * Using feature branches to create pull requests 47 | * Discussing new features **before** hacking away at it 48 | 49 | 50 | ## Dive into code 51 | 52 | ### Fork this repository 53 | 54 | fork this repository 55 | 56 | Fork this repository by clicking on the fork button on the top of this page. 57 | This will create a copy of this repository in your account. 58 | 59 |
60 | 61 | ### Clone the repository 62 | 63 | clone this repository 64 | 65 | Now clone the forked repository to your machine. Go to your GitHub account, open the forked repository, click on the code button and then click the _copy to clipboard_ icon. 66 | 67 | Open a terminal and run the following git command: 68 | 69 | ``` 70 | git clone "url you just copied" 71 | ``` 72 | 73 | where "url you just copied" (without the quotation marks) is the url to this repository (your fork of this project). See the previous steps to obtain the url. 74 | 75 | > use SSH tab to copy proper URL 76 | 77 | copy URL to clipboard 78 | 79 | For example: 80 | 81 | ``` 82 | git clone git@github.com:$USER/ux-charts.git 83 | ``` 84 | 85 | where `$USER` is your GitHub username. Here you're copying the contents of the `ux-charts` repository on GitHub to your computer. 86 | 87 | 88 |
89 | 90 | ### Create a branch 91 | 92 | Change to the repository directory on your computer (if you are not already there): 93 | 94 | ``` 95 | cd ux-charts 96 | ``` 97 | 98 | Now create a branch using the `git checkout` command: 99 | 100 | ``` 101 | git checkout -b origin/develop 102 | ``` 103 | replacing `` with the adequate name of the feature you will develop. 104 | 105 | ### Make necessary changes and commit those changes 106 | 107 | Now that you've properly installed and forked Angular Rust, you are ready to start coding (assuming you have a validated your ideas with other community members)! 108 | 109 | ### Format Your Code 110 | 111 | Remember to run `cargo fmt` before committing your changes. 112 | Many Go developers opt to have their editor run `cargo fmt` automatically when saving Go files. 113 | 114 | Additionally, follow the [core Rust style conventions](https://rustc-dev-guide.rust-lang.org/conventions.html) to have your pull requests accepted. 115 | 116 | ### Write Tests (and Benchmarks for Bonus Points) 117 | 118 | Significant new features require tests. Besides unit tests, it is also possible to test a feature by exercising it in one of the sample apps and verifying its 119 | operation using that app's test suite. This has the added benefit of providing example code for developers to refer to. 120 | 121 | Benchmarks are helpful but not required. 122 | 123 | ### Run the Tests 124 | 125 | Typically running the main set of unit tests will be sufficient: 126 | 127 | ``` 128 | $ cargo test 129 | ``` 130 | ### Document Your Feature 131 | 132 | Due to the wide audience and shared nature of Angular Rust, documentation is an essential addition to your new code. **Pull requests risk not being accepted** until proper documentation is created to detail how to make use of new functionality. 133 | 134 | ### Add yourself to the list of contributors 135 | 136 | Open `CONTRIBUTORS.md` file in a text editor, add your name to it. Don't add it at the beginning or end of the file. Put it anywhere in between. Now, save the file. 137 | 138 | git status 139 | 140 | If you go to the project directory and execute the command `git status`, you'll see there are changes. 141 | 142 | Add those changes to the branch you just created using the `git add` command: 143 | 144 | ``` 145 | git add . 146 | ``` 147 | 148 | Now commit those changes using the `git commit` command: 149 | 150 | ``` 151 | git commit -m "$COMMENT" 152 | ``` 153 | 154 | replacing `$COMMENT` with appropriate description of your changes. 155 | 156 | ### Push changes to GitHub 157 | 158 | Push your changes using the command `git push`: 159 | 160 | ``` 161 | git push origin 162 | ``` 163 | 164 | replacing `` with the name of the branch you created earlier. 165 | 166 | ### Submit your changes for review 167 | 168 | Once you've done all of the above & pushed your changes to your fork, you can create a pull request for review and acceptance. 169 | 170 | If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button. 171 | 172 | create a pull request 173 | 174 | Now submit the pull request. 175 | Do not forget to set develop branch for your pull request. 176 | 177 | submit pull request 178 | 179 | 180 | ## Where to go from here? 181 | 182 | Congrats! You just completed the standard _fork -> clone -> edit -> pull request_ workflow that you'll encounter often as a contributor! 183 | 184 | You can check more details in our **[detailed contribution guide](https://angular-rust.github.io/contributing/)**. 185 | 186 | You could join our gitter team in case you need any help or have any questions. [Join gitter team](https://gitter.im/angular_rust/community). 187 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/gauge.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | #![allow(clippy::explicit_counter_loop, clippy::float_cmp)] 3 | 4 | use animate::{ 5 | easing::{get_easing, Easing}, 6 | interpolate::lerp, 7 | }; 8 | use dataflow::*; 9 | use primitives::{color, CanvasContext, Color, Point, TextStyle, TextWeight}; 10 | use std::cell::RefCell; 11 | 12 | use crate::*; 13 | 14 | const START_ANGLE: f64 = -std::f64::consts::FRAC_PI_2; 15 | 16 | #[derive(Default, Clone)] 17 | pub struct GaugeEntity { 18 | // Chart chart, 19 | color: Color, 20 | highlight_color: Color, 21 | // formatted_value: String, 22 | index: usize, 23 | old_value: Option, 24 | value: Option, 25 | 26 | old_start_angle: f64, 27 | old_end_angle: f64, 28 | start_angle: f64, 29 | end_angle: f64, 30 | 31 | center: Point, 32 | inner_radius: f64, 33 | outer_radius: f64, 34 | 35 | // [Series] field. 36 | name: String, 37 | 38 | background_color: Color, 39 | } 40 | 41 | impl GaugeEntity { 42 | pub fn is_empty(&self) -> bool { 43 | self.start_angle == self.end_angle 44 | } 45 | 46 | pub fn contains_point(&self, p: Point) -> bool { 47 | // let p = p - center; 48 | let mag = p.distance_to(Point::default()); //p.magnitude() 49 | if mag > self.outer_radius || mag < self.inner_radius { 50 | return false; 51 | } 52 | 53 | // let angle = f64::atan2(p.y, p.x); 54 | // let chartStartAngle = (chart as dynamic).start_angle; 55 | 56 | // Make sure [angle] is in range [chartStartAngle]..[chartStartAngle] + TAU. 57 | // angle = (angle - chartStartAngle) % TAU + chartStartAngle; 58 | 59 | // If counterclockwise, make sure [angle] is in range 60 | // [start] - 2*pi..[start]. 61 | // if start_angle > end_angle { 62 | // angle -= angle - TAU; 63 | // } 64 | 65 | // if (start_angle <= end_angle) { 66 | // // Clockwise. 67 | // return is_in_range(angle, start_angle, end_angle); 68 | // } else { 69 | // // Counterclockwise. 70 | // return is_in_range(angle, end_angle, start_angle); 71 | // } 72 | unimplemented!() 73 | } 74 | 75 | fn draw_entity>(&self, ctx: &C, percent: f64, highlight: bool) { 76 | // Draw the background. 77 | { 78 | let mut a1 = lerp(self.old_start_angle, self.start_angle, percent); 79 | let mut a2 = lerp(self.old_end_angle, self.start_angle + TAU, percent); 80 | if a1 > a2 { 81 | std::mem::swap(&mut a1, &mut a2); 82 | } 83 | let center = &self.center; 84 | ctx.set_fill_color(self.background_color); 85 | ctx.begin_path(); 86 | ctx.arc(center.x, center.y, self.outer_radius, a1, a2, false); 87 | ctx.arc(center.x, center.y, self.inner_radius, a2, a1, true); 88 | ctx.fill(); 89 | } 90 | 91 | let mut a1 = lerp(self.old_start_angle, self.start_angle, percent); 92 | let mut a2 = lerp(self.old_end_angle, self.end_angle, percent); 93 | if a1 > a2 { 94 | std::mem::swap(&mut a1, &mut a2); 95 | } 96 | let center = &self.center; 97 | 98 | if highlight { 99 | let highlight_outer_radius = HIGHLIGHT_OUTER_RADIUS_FACTOR * self.outer_radius; 100 | ctx.set_fill_color(self.highlight_color); 101 | ctx.begin_path(); 102 | ctx.arc(center.x, center.y, highlight_outer_radius, a1, a2, false); 103 | ctx.arc(center.x, center.y, self.inner_radius, a2, a1, true); 104 | ctx.fill(); 105 | } 106 | 107 | ctx.set_fill_color(self.color); 108 | ctx.begin_path(); 109 | ctx.arc(center.x, center.y, self.outer_radius, a1, a2, false); 110 | ctx.arc(center.x, center.y, self.inner_radius, a2, a1, true); 111 | ctx.fill(); 112 | ctx.stroke(); 113 | } 114 | } 115 | 116 | impl Entity for GaugeEntity { 117 | fn free(&mut self) { 118 | // chart = null; 119 | } 120 | 121 | fn save(&self) { 122 | // self.old_start_angle = self.start_angle; 123 | // self.old_end_angle = self.end_angle; 124 | // self.old_value = self.value; 125 | } 126 | } 127 | 128 | impl Drawable for GaugeEntity 129 | where 130 | C: CanvasContext, 131 | D: fmt::Display + Copy + Into + Ord + Default, 132 | { 133 | fn draw(&self, ctx: &C, percent: f64, highlight: bool) { 134 | // let tmp_color = &self.color; 135 | // let tmp_end_angle = self.end_angle; 136 | 137 | self.draw_entity(ctx, percent, highlight); 138 | 139 | // Draw the percent. 140 | let fs1 = 0.75 * self.inner_radius; 141 | let family = DEFAULT_FONT_FAMILY; 142 | 143 | let old_value = match self.old_value { 144 | Some(value) => value.into(), 145 | None => 0.0, 146 | }; 147 | 148 | let value = match self.value { 149 | Some(value) => value.into(), 150 | None => 0.0, 151 | }; 152 | 153 | let fs2 = 0.6 * fs1; 154 | let y = self.center.y + 0.3 * fs1; 155 | 156 | let text1 = lerp(old_value, value, percent).round().to_string(); 157 | ctx.set_font(family, TextStyle::Normal, TextWeight::Normal, fs1); 158 | let w1 = ctx.measure_text(text1.as_str()).width; 159 | ctx.fill_text(text1.as_str(), self.center.x - 0.5 * w1, y); 160 | 161 | let text2 = "%"; 162 | ctx.set_font(family, TextStyle::Normal, TextWeight::Normal, fs2); 163 | // let w2 = ctx.measure_text(text2).width; 164 | ctx.fill_text(text2, self.center.x + 0.5 * w1 + 5., y); 165 | } 166 | } 167 | 168 | #[derive(Default, Clone)] 169 | struct GaugeChartProperties { 170 | gauge_hop: f64, 171 | gauge_inner_radius: f64, 172 | gauge_outer_radius: f64, 173 | gauge_center_y: f64, 174 | // start_angle: f64, 175 | } 176 | 177 | pub struct GaugeChart<'a, C, M, D> 178 | where 179 | C: CanvasContext, 180 | M: fmt::Display, 181 | D: fmt::Display + Copy, 182 | { 183 | props: RefCell, 184 | base: BaseChart<'a, C, GaugeEntity, M, D, GaugeChartOptions<'a>>, 185 | } 186 | 187 | impl<'a, C, M, D> GaugeChart<'a, C, M, D> 188 | where 189 | C: CanvasContext, 190 | M: fmt::Display, 191 | D: fmt::Display + Copy + Into + Ord + Default, 192 | { 193 | pub fn new(options: GaugeChartOptions<'a>) -> Self { 194 | Self { 195 | props: Default::default(), 196 | base: BaseChart::new(options), 197 | } 198 | } 199 | 200 | fn get_gauge_center(&self, index: usize) -> Point { 201 | let props = self.props.borrow(); 202 | Point::new((index as f64 + 0.5) * props.gauge_hop, props.gauge_center_y) 203 | } 204 | 205 | fn value_to_angle(&self, value: Option) -> f64 { 206 | match value { 207 | Some(value) => value.into() * TAU / 100., 208 | None => 0.0, 209 | } 210 | } 211 | 212 | // fn update_tooltip_content(&self) { 213 | // // let gauge = channels[0].entities[focused_entity_index] as Gauge; 214 | // // tooltip.style 215 | // // ..borderColor = gauge.color 216 | // // ..padding = "4px 12px"; 217 | // // let label = tooltip_label_formatter(gauge.name); 218 | // // let value = tooltip_value_formatter(gauge.value); 219 | // // tooltip.innerHtml = "$label: $value%"; 220 | // } 221 | 222 | // fn get_entity_group_index(&self, x: f64, y: f64) -> i64 { 223 | // // let p = Point(x, y); 224 | // // for (Gauge g in channels[0].entities) { 225 | // // if (g.containsPoint(p)) return g.index; 226 | // // } 227 | // // return -1; 228 | // unimplemented!() 229 | // } 230 | 231 | // /// Called when [data_table] has been changed. 232 | // fn data_changed(&self) { 233 | // info!("data_changed"); 234 | // // self.calculate_drawing_sizes(ctx); 235 | // self.create_channels(0, self.base.data.meta.len()); 236 | // } 237 | } 238 | 239 | impl<'a, C, M, D> Chart<'a, C, M, D, GaugeEntity> for GaugeChart<'a, C, M, D> 240 | where 241 | C: CanvasContext, 242 | M: fmt::Display, 243 | D: fmt::Display + Copy + Into + Ord + Default, 244 | { 245 | fn calculate_drawing_sizes(&self, ctx: &C) { 246 | self.base.calculate_drawing_sizes(ctx); 247 | 248 | let mut props = self.props.borrow_mut(); 249 | 250 | let gauge_count = self.base.data.frames.len(); 251 | let mut label_total_height = 0.; 252 | 253 | if let Some(style) = &self.base.options.labels { 254 | label_total_height = AXIS_LABEL_MARGIN as f64 + style.fontsize.unwrap_or(12.); 255 | } 256 | 257 | let area = &self.base.props.borrow().area; 258 | props.gauge_center_y = area.origin.y + 0.5 * area.size.height; 259 | props.gauge_hop = area.size.width / gauge_count as f64; 260 | 261 | let avail_w = 0.618 * props.gauge_hop; // Golden ratio. 262 | let avail_h = area.size.height - 2. * label_total_height as f64; 263 | props.gauge_outer_radius = 0.5 * avail_w.min(avail_h) / HIGHLIGHT_OUTER_RADIUS_FACTOR; 264 | props.gauge_inner_radius = 0.5 * props.gauge_outer_radius; 265 | } 266 | 267 | fn set_stream(&mut self, stream: DataStream<'a, M, D>) { 268 | self.base.data = stream; 269 | self.create_channels(0, self.base.data.meta.len()); 270 | } 271 | 272 | fn draw(&self, ctx: &C) { 273 | self.base.dispose(); 274 | // data_tableSubscriptionTracker 275 | // ..add(dataTable.onCellChange.listen(data_cell_changed)) 276 | // ..add(dataTable.onColumnsChange.listen(dataColumnsChanged)) 277 | // ..add(dataTable.onRowsChange.listen(data_rows_changed)); 278 | // self.easing = get_easing(self.options.animation().easing); 279 | // self.base.initialize_legend(); 280 | // self.base.initialize_tooltip(); 281 | // self.base.position_legend(); 282 | 283 | // This call is redundant for row and column changes but necessary for 284 | // cell changes. 285 | self.calculate_drawing_sizes(ctx); 286 | self.update_channel(0); 287 | 288 | self.draw_frame(ctx, None); 289 | } 290 | 291 | fn resize(&self, w: f64, h: f64) { 292 | self.base.resize(w, h); 293 | } 294 | 295 | /// Draws the axes and the grid. 296 | /// 297 | fn draw_axes_and_grid(&self, ctx: &C) { 298 | self.base.draw_axes_and_grid(ctx); 299 | } 300 | 301 | /// Draws the current animation frame. 302 | /// 303 | /// If [time] is `null`, draws the last frame (i.e. no animation). 304 | fn draw_frame(&self, ctx: &C, time: Option) { 305 | // clear surface 306 | self.base.draw_frame(ctx, time); 307 | 308 | self.draw_axes_and_grid(ctx); 309 | 310 | let mut percent = self.base.calculate_percent(time); 311 | 312 | if percent >= 1.0 { 313 | percent = 1.0; 314 | 315 | // Update the visibility states of all channel before the last frame. 316 | let mut channels = self.base.channels.borrow_mut(); 317 | 318 | for channel in channels.iter_mut() { 319 | if channel.state == Visibility::Showing { 320 | channel.state = Visibility::Shown; 321 | } else if channel.state == Visibility::Hiding { 322 | channel.state = Visibility::Hidden; 323 | } 324 | } 325 | } 326 | 327 | let props = self.base.props.borrow(); 328 | 329 | let ease = match props.easing { 330 | Some(val) => val, 331 | None => get_easing(Easing::Linear), 332 | }; 333 | self.draw_channels(ctx, ease(percent)); 334 | // ctx.drawImageScaled(ctx.canvas, 0, 0, width, height); 335 | // ctx.drawImageScaled(ctx.canvas, 0, 0, width, height); 336 | self.base.draw_title(ctx); 337 | 338 | if percent < 1.0 { 339 | // animation_frame_id = window.requestAnimationFrame(draw_frame); 340 | } else if time.is_some() { 341 | self.base.animation_end(); 342 | } 343 | } 344 | 345 | fn draw_channels(&self, ctx: &C, percent: f64) -> bool { 346 | ctx.set_stroke_color(color::WHITE); 347 | // ctx.set_text_align(TextAlign::Center); 348 | 349 | let channels = self.base.channels.borrow(); 350 | let labels = &self.base.options.labels; 351 | // let mut focused_entity_index = self.base.props.borrow().focused_entity_index; 352 | let focused_entity_index = -1; 353 | if let Some(channel) = channels.first() { 354 | if channel.state == Visibility::Showing || channel.state == Visibility::Shown { 355 | for entity in channel.entities.iter() { 356 | let highlight = entity.index as i64 == focused_entity_index; 357 | entity.draw(ctx, percent, highlight); 358 | 359 | if let Some(style) = labels { 360 | let x = entity.center.x; 361 | let y = entity.center.y 362 | + entity.outer_radius 363 | + style.fontsize.unwrap_or(12.) 364 | + AXIS_LABEL_MARGIN as f64; 365 | ctx.set_fill_color(style.color); 366 | 367 | ctx.set_font( 368 | &style.fontfamily.unwrap_or(DEFAULT_FONT_FAMILY), 369 | style.fontstyle.unwrap_or(TextStyle::Normal), 370 | TextWeight::Normal, 371 | style.fontsize.unwrap_or(12.), 372 | ); 373 | // ctx.set_text_align(TextAlign::Center); 374 | let w = ctx.measure_text(&entity.name).width; 375 | ctx.fill_text(&entity.name, x - 0.5 * w, y); 376 | } 377 | } 378 | } 379 | } 380 | 381 | false 382 | } 383 | 384 | fn update_channel(&self, _: usize) { 385 | // let len = self.base.data.frames.len(); 386 | let props = self.props.borrow(); 387 | let mut channels = self.base.channels.borrow_mut(); 388 | let mut idx = 0; 389 | 390 | if let Some(channel) = channels.first_mut() { 391 | for entity in channel.entities.iter_mut() { 392 | if entity.value.is_some() { 393 | let color = self.base.get_color(idx); 394 | let highlight_color = self.base.change_color_alpha(color, 0.5); 395 | 396 | entity.index = idx; 397 | entity.color = color; 398 | entity.highlight_color = highlight_color; 399 | 400 | // here focus 401 | entity.center = self.get_gauge_center(idx); 402 | entity.inner_radius = props.gauge_inner_radius; 403 | entity.outer_radius = props.gauge_outer_radius; 404 | entity.end_angle = START_ANGLE + self.value_to_angle(entity.value); 405 | } 406 | idx += 1; 407 | } 408 | } 409 | } 410 | 411 | fn create_entity( 412 | &self, 413 | channel_index: usize, 414 | entity_index: usize, 415 | value: Option, 416 | color: Color, 417 | highlight_color: Color, 418 | ) -> GaugeEntity { 419 | // Override the colors. 420 | let color = self.base.get_color(entity_index); 421 | let highlight_color = self.base.change_color_alpha(color, 0.5); 422 | 423 | let stream = &self.base.data; 424 | let frame = stream.frames.get(entity_index).unwrap(); 425 | let name = format!("{}", frame.metric); 426 | 427 | let props = self.props.borrow(); 428 | let options = &self.base.options; 429 | let center = self.get_gauge_center(entity_index); 430 | 431 | GaugeEntity { 432 | index: entity_index, 433 | value, 434 | name, 435 | color, 436 | background_color: options.gauge_background, 437 | highlight_color, 438 | old_value: None, 439 | old_start_angle: START_ANGLE, 440 | old_end_angle: START_ANGLE, 441 | center, 442 | inner_radius: props.gauge_inner_radius, 443 | outer_radius: props.gauge_outer_radius, 444 | start_angle: START_ANGLE, 445 | end_angle: START_ANGLE + self.value_to_angle(value), 446 | } 447 | } 448 | 449 | fn create_channels(&self, start: usize, end: usize) { 450 | let mut start = start; 451 | let mut result = Vec::new(); 452 | let count = self.base.data.frames.len(); 453 | let meta = &self.base.data.meta; 454 | while start < end { 455 | let channel = meta.get(start).unwrap(); 456 | let name = channel.name; 457 | let color = self.base.get_color(start); 458 | let highlight = self.base.get_highlight_color(color); 459 | 460 | let entities = self.create_entities(start, 0, count, color, highlight); 461 | result.push(ChartChannel::new(name, color, highlight, entities)); 462 | start += 1; 463 | } 464 | 465 | let mut channels = self.base.channels.borrow_mut(); 466 | *channels = result; 467 | } 468 | 469 | fn create_entities( 470 | &self, 471 | channel_index: usize, 472 | start: usize, 473 | end: usize, 474 | color: Color, 475 | highlight: Color, 476 | ) -> Vec> { 477 | let mut start = start; 478 | let mut result = Vec::new(); 479 | while start < end { 480 | let frame = self.base.data.frames.get(start).unwrap(); 481 | // let value = frame.data.get(channel_index as u64); 482 | let entity = match frame.data.get(channel_index as u64) { 483 | Some(value) => { 484 | let value = *value; 485 | self.create_entity(channel_index, start, Some(value), color, highlight) 486 | } 487 | None => self.create_entity(channel_index, start, None, color, highlight), 488 | }; 489 | 490 | result.push(entity); 491 | start += 1; 492 | } 493 | result 494 | } 495 | 496 | fn get_tooltip_position(&self, tooltip_width: f64, tooltip_height: f64) -> Point { 497 | // let channels = self.base.channels.borrow(); 498 | // let channel = channels.first().unwrap(); 499 | 500 | // let focused_entity_index = self.base.props.borrow().focused_entity_index as usize; 501 | 502 | // let gauge = channel.entities.get(focused_entity_index).unwrap(); 503 | // let x = gauge.center.x - (tooltip_width / 2.).trunc(); 504 | // let y = gauge.center.y 505 | // - HIGHLIGHT_OUTER_RADIUS_FACTOR * gauge.outer_radius 506 | // - tooltip_height 507 | // - 5.; 508 | 509 | // Point::new(x, y) 510 | unimplemented!() 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /src/pie.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_assignments)] 2 | #![allow(unused_variables)] 3 | #![allow(dead_code)] 4 | #![allow(clippy::explicit_counter_loop, clippy::float_cmp)] 5 | 6 | use animate::{ 7 | easing::{get_easing, Easing}, 8 | interpolate::lerp, 9 | }; 10 | use dataflow::*; 11 | use primitives::{color, BaseLine, CanvasContext, Color, Point, TextStyle, TextWeight}; 12 | use std::{cell::RefCell, fmt}; 13 | 14 | use crate::*; 15 | 16 | const START_ANGLE: f64 = -std::f64::consts::FRAC_PI_2; 17 | 18 | /// A pie in a pie chart. 19 | #[derive(Default, Clone)] 20 | pub struct PieEntity { 21 | // Chart chart, 22 | color: Color, 23 | highlight_color: Color, 24 | formatted_value: String, 25 | index: usize, 26 | old_value: Option, 27 | value: Option, 28 | 29 | old_start_angle: f64, 30 | old_end_angle: f64, 31 | start_angle: f64, 32 | end_angle: f64, 33 | 34 | center: Point, 35 | inner_radius: f64, 36 | outer_radius: f64, 37 | 38 | // [Series] field. 39 | name: String, 40 | } 41 | 42 | impl PieEntity { 43 | pub fn is_empty(&self) -> bool { 44 | self.start_angle == self.end_angle 45 | } 46 | 47 | fn contains_point(&self, p: Point) -> bool { 48 | // p -= center; 49 | let mag = p.distance_to(Point::default()); //p.magnitude(); 50 | if mag > self.outer_radius || mag < self.inner_radius { 51 | return false; 52 | } 53 | 54 | let mut angle = f64::atan2(p.y, p.x); 55 | 56 | //TODO: complete it 57 | // let chartStartAngle = (chart as dynamic).start_angle; 58 | 59 | // // Make sure [angle] is in range [chartStartAngle]..[chartStartAngle] + TAU. 60 | // angle = (angle - chartStartAngle) % TAU + chartStartAngle; 61 | 62 | // If counterclockwise, make sure [angle] is in range 63 | // [start] - 2*pi..[start]. 64 | if self.start_angle > self.end_angle { 65 | angle -= TAU; 66 | } 67 | 68 | if self.start_angle <= self.end_angle { 69 | // Clockwise. 70 | utils::is_in_range(angle, self.start_angle, self.end_angle) 71 | } else { 72 | // Counterclockwise. 73 | utils::is_in_range(angle, self.end_angle, self.start_angle) 74 | } 75 | } 76 | } 77 | 78 | impl Entity for PieEntity { 79 | fn free(&mut self) { 80 | // chart = null; 81 | } 82 | 83 | fn save(&self) { 84 | // self.old_start_angle = self.start_angle; 85 | // self.old_end_angle = self.end_angle; 86 | // self.old_value = self.value; 87 | } 88 | } 89 | 90 | impl Drawable for PieEntity 91 | where 92 | C: CanvasContext, 93 | { 94 | fn draw(&self, ctx: &C, percent: f64, highlight: bool) { 95 | let mut a1 = lerp(self.old_start_angle, self.start_angle, percent); 96 | let mut a2 = lerp(self.old_end_angle, self.end_angle, percent); 97 | if a1 > a2 { 98 | std::mem::swap(&mut a1, &mut a2); 99 | } 100 | let center = &self.center; 101 | if highlight { 102 | let highlight_outer_radius = HIGHLIGHT_OUTER_RADIUS_FACTOR * self.outer_radius; 103 | ctx.set_fill_color(self.highlight_color); 104 | ctx.begin_path(); 105 | ctx.arc(center.x, center.y, highlight_outer_radius, a1, a2, false); 106 | ctx.arc(center.x, center.y, self.inner_radius, a2, a1, true); 107 | ctx.fill(); 108 | } 109 | 110 | ctx.set_fill_color(self.color); 111 | ctx.begin_path(); 112 | ctx.arc(center.x, center.y, self.outer_radius, a1, a2, false); 113 | ctx.arc(center.x, center.y, self.inner_radius, a2, a1, true); 114 | ctx.fill(); 115 | ctx.stroke(); 116 | 117 | if !self.formatted_value.is_empty() && a2 - a1 > PI / 36.0 { 118 | // let labels = self.chart.options.channel.labels; 119 | // if labels.enabled { 120 | let r = 0.25 * self.inner_radius + 0.65 * self.outer_radius; 121 | let a = 0.5 * (a1 + a2); 122 | let p = utils::polar2cartesian(center, r, a); 123 | ctx.set_fill_color(color::WHITE); // labels.style.color 124 | let w = ctx.measure_text(self.formatted_value.as_str()).width; 125 | // TODO: should have a global state in ctx i think 126 | ctx.fill_text(self.formatted_value.as_str(), p.x - w / 2., p.y + 4.); 127 | // } 128 | } 129 | } 130 | } 131 | 132 | #[derive(Default, Clone)] 133 | struct PieChartProperties { 134 | center: Point, 135 | outer_radius: f64, 136 | inner_radius: f64, 137 | 138 | /// The start angle in radians. 139 | start_angle: f64, 140 | 141 | /// 1 means clockwise and -1 means counterclockwise. 142 | direction: i64, 143 | } 144 | 145 | pub struct PieChart<'a, C, M, D> 146 | where 147 | C: CanvasContext, 148 | M: fmt::Display, 149 | D: fmt::Display + Copy, 150 | { 151 | props: RefCell, 152 | base: BaseChart<'a, C, PieEntity, M, D, PieChartOptions<'a>>, 153 | } 154 | 155 | impl<'a, C, M, D> PieChart<'a, C, M, D> 156 | where 157 | C: CanvasContext, 158 | M: fmt::Display, 159 | D: fmt::Display + Copy + Into + Ord + Default, 160 | { 161 | pub fn new(options: PieChartOptions<'a>) -> Self { 162 | Self { 163 | props: Default::default(), 164 | base: BaseChart::new(options), 165 | } 166 | } 167 | 168 | fn data_rows_changed(&self, record: DataCollectionChangeRecord) { 169 | self.base 170 | .update_channel_visible(record.index, record.removed_count, record.added_count); 171 | self.base.data_rows_changed(record); 172 | self.base.update_legend_content(); 173 | } 174 | 175 | fn get_entity_group_index(&self, x: f64, y: f64) -> i64 { 176 | let p = Point::new(x, y); 177 | // let entities = channels.first.entities; 178 | // for (let i = entities.len(); i >= 0; i--) { 179 | // let pie = entities[i] as Pie; 180 | // if (pie.containsPoint(p)) return i; 181 | // } 182 | // return -1; 183 | unimplemented!() 184 | } 185 | 186 | pub fn get_legend_labels(&self) -> Vec { 187 | //self.data.getColumnValues(0) 188 | unimplemented!() 189 | } 190 | 191 | fn channel_visibility_changed(&self, index: usize) { 192 | self.update_channel(0); 193 | } 194 | 195 | fn update_tooltip_content(&self) { 196 | // let pie = channels[0].entities[focused_entity_index] as Pie; 197 | // tooltip.style 198 | // ..borderColor = pie.color 199 | // ..padding = "4px 12px"; 200 | // let label = tooltip_label_formatter(pie.name); 201 | // let value = tooltip_value_formatter(pie.value); 202 | // tooltip.innerHtml = "$label: $value"; 203 | unimplemented!() 204 | } 205 | 206 | // Called when [data_table] has been changed. 207 | // fn data_changed(&self) { 208 | // info!("data_changed"); 209 | // // self.calculate_drawing_sizes(ctx); 210 | // self.create_channels(0, self.base.data.meta.len()); 211 | // } 212 | } 213 | 214 | impl<'a, C, M, D> Chart<'a, C, M, D, PieEntity> for PieChart<'a, C, M, D> 215 | where 216 | C: CanvasContext, 217 | M: fmt::Display, 218 | D: fmt::Display + Copy + Into + Ord + Default, 219 | { 220 | fn calculate_drawing_sizes(&self, ctx: &C) { 221 | self.base.calculate_drawing_sizes(ctx); 222 | let mut props = self.props.borrow_mut(); 223 | 224 | props.center = { 225 | let rect = &self.base.props.borrow().area; 226 | 227 | let half_w = rect.size.width as i64 >> 1; 228 | let half_h = rect.size.height as i64 >> 1; 229 | props.outer_radius = (half_w.min(half_h) as f64) / HIGHLIGHT_OUTER_RADIUS_FACTOR; 230 | 231 | let x = rect.origin.x + half_w as f64; 232 | let y = rect.origin.y + half_h as f64; 233 | Point::new(x, y) 234 | }; 235 | 236 | let mut pie_hole = self.base.options.pie_hole; 237 | 238 | if pie_hole > 1.0 { 239 | pie_hole = 0.0; 240 | } 241 | 242 | if pie_hole < 0.0 { 243 | pie_hole = 0.0; 244 | } 245 | 246 | props.inner_radius = pie_hole * props.outer_radius; 247 | 248 | let opt = &self.base.options.channel; 249 | let mut baseprops = self.base.props.borrow_mut(); 250 | baseprops.entity_value_formatter = if let Some(formatter) = opt.labels.formatter { 251 | Some(formatter) 252 | } else { 253 | Some(default_value_formatter) 254 | }; 255 | 256 | props.direction = if opt.counterclockwise { 257 | COUNTERCLOCKWISE 258 | } else { 259 | CLOCKWISE 260 | }; 261 | 262 | props.start_angle = utils::deg2rad(opt.start_angle); 263 | } 264 | 265 | fn set_stream(&mut self, stream: DataStream<'a, M, D>) { 266 | self.base.data = stream; 267 | self.create_channels(0, self.base.data.meta.len()); 268 | } 269 | 270 | fn draw(&self, ctx: &C) { 271 | self.base.dispose(); 272 | // data_tableSubscriptionTracker 273 | // ..add(dataTable.onCellChange.listen(data_cell_changed)) 274 | // ..add(dataTable.onColumnsChange.listen(dataColumnsChanged)) 275 | // ..add(dataTable.onRowsChange.listen(data_rows_changed)); 276 | // self.easing = get_easing(self.options.animation().easing); 277 | // self.base.initialize_legend(); 278 | // self.base.initialize_tooltip(); 279 | // self.base.position_legend(); 280 | 281 | // This call is redundant for row and column changes but necessary for 282 | // cell changes. 283 | self.calculate_drawing_sizes(ctx); 284 | self.update_channel(0); 285 | 286 | self.draw_frame(ctx, None); 287 | } 288 | 289 | fn resize(&self, w: f64, h: f64) { 290 | self.base.resize(w, h); 291 | } 292 | 293 | /// Draws the axes and the grid. 294 | /// 295 | fn draw_axes_and_grid(&self, ctx: &C) { 296 | self.base.draw_axes_and_grid(ctx); 297 | } 298 | 299 | /// Draws the current animation frame. 300 | /// 301 | /// If [time] is `null`, draws the last frame (i.e. no animation). 302 | fn draw_frame(&self, ctx: &C, time: Option) { 303 | // clear surface 304 | self.base.draw_frame(ctx, time); 305 | 306 | self.draw_axes_and_grid(ctx); 307 | 308 | let mut percent = self.base.calculate_percent(time); 309 | 310 | if percent >= 1.0 { 311 | percent = 1.0; 312 | 313 | // Update the visibility states of all channel before the last frame. 314 | let mut channels = self.base.channels.borrow_mut(); 315 | for channel in channels.iter_mut() { 316 | if channel.state == Visibility::Showing { 317 | channel.state = Visibility::Shown; 318 | } else if channel.state == Visibility::Hiding { 319 | channel.state = Visibility::Hidden; 320 | } 321 | } 322 | } 323 | 324 | let props = self.base.props.borrow(); 325 | 326 | let ease = match props.easing { 327 | Some(val) => val, 328 | None => get_easing(Easing::Linear), 329 | }; 330 | self.draw_channels(ctx, ease(percent)); 331 | // ctx.drawImageScaled(ctx.canvas, 0, 0, width, height); 332 | // ctx.drawImageScaled(ctx.canvas, 0, 0, width, height); 333 | self.base.draw_title(ctx); 334 | 335 | if percent < 1.0 { 336 | // animation_frame_id = window.requestAnimationFrame(draw_frame); 337 | } else if time.is_some() { 338 | self.base.animation_end(); 339 | } 340 | } 341 | 342 | fn draw_channels(&self, ctx: &C, percent: f64) -> bool { 343 | ctx.set_line_width(2.); 344 | ctx.set_stroke_color(color::WHITE); 345 | // ctx.set_text_align(TextAlign::Center); 346 | ctx.set_text_baseline(BaseLine::Middle); 347 | 348 | let channels = self.base.channels.borrow(); 349 | let channel = channels.first().unwrap(); 350 | let labels = &self.base.options.channel.labels.style; 351 | 352 | ctx.set_font( 353 | labels.fontfamily.unwrap_or(DEFAULT_FONT_FAMILY), 354 | labels.fontstyle.unwrap_or(TextStyle::Normal), 355 | TextWeight::Normal, 356 | labels.fontsize.unwrap_or(12.), 357 | ); 358 | 359 | let baseprops = self.base.props.borrow(); 360 | let mut focused_channel_index = baseprops.focused_channel_index; 361 | focused_channel_index = -1; //FIXME: 362 | let mut focused_entity_index = baseprops.focused_entity_index; 363 | focused_entity_index = -1; //FIXME: 364 | 365 | for entity in channel.entities.iter() { 366 | if entity.is_empty() && percent == 1.0 { 367 | continue; 368 | } 369 | 370 | let highlight = entity.index as i64 == focused_channel_index 371 | || entity.index as i64 == focused_entity_index; 372 | 373 | entity.draw(ctx, percent, highlight); 374 | } 375 | 376 | false 377 | } 378 | 379 | fn update_channel(&self, _: usize) { 380 | let props = self.props.borrow(); 381 | let mut channels = self.base.channels.borrow_mut(); 382 | 383 | for channel in channels.iter_mut() { 384 | if channel.state == Visibility::Showing || channel.state == Visibility::Shown { 385 | let mut sum: f64 = 0.0; 386 | // Sum the values of all visible pies. 387 | for entity in channel.entities.iter() { 388 | if let Some(value) = entity.value { 389 | sum += value.into(); 390 | } 391 | } 392 | 393 | let mut start_angle = START_ANGLE; //props.start_angle; 394 | let mut idx = 0; 395 | for entity in channel.entities.iter_mut() { 396 | match entity.value { 397 | Some(value) => { 398 | let color = self.base.get_color(idx); 399 | entity.index = idx; 400 | entity.color = color; 401 | entity.highlight_color = self.base.get_highlight_color(color); 402 | entity.center = props.center; 403 | entity.inner_radius = props.inner_radius; 404 | entity.outer_radius = props.outer_radius; 405 | entity.start_angle = start_angle; 406 | entity.end_angle = 407 | start_angle - props.direction as f64 * value.into() * TAU / sum; 408 | start_angle = entity.end_angle; 409 | } 410 | None => { 411 | println!("hole in channel data"); 412 | } 413 | } 414 | idx += 1; 415 | } 416 | } 417 | } 418 | } 419 | 420 | fn create_entity( 421 | &self, 422 | channel_index: usize, 423 | entity_index: usize, 424 | value: Option, 425 | color: Color, 426 | highlight_color: Color, 427 | ) -> PieEntity { 428 | // Override the colors. 429 | let color = self.base.get_color(entity_index); 430 | let highlight_color = self.base.change_color_alpha(color, 0.5); 431 | 432 | let stream = &self.base.data; 433 | let frame = stream.frames.get(entity_index).unwrap(); 434 | let name = format!("{}", frame.metric); 435 | 436 | let props = self.props.borrow(); 437 | let baseprops = self.base.props.borrow(); 438 | 439 | let start_angle = START_ANGLE; // props.start_angle; 440 | 441 | // FIXME: should be handled in update_channel 442 | // if entity_index > 0 { 443 | // let channels = self.base.channels.borrow(); 444 | // let channel = channels.first().unwrap(); 445 | // let prev = channel.entities.get(entity_index - 1).unwrap(); 446 | // start_angle = prev.end_angle; 447 | // } 448 | 449 | let formatted_value = match value { 450 | Some(value) => match baseprops.entity_value_formatter { 451 | Some(formatter) => formatter(value.into()), 452 | None => default_value_formatter(value.into()), 453 | }, 454 | None => "".into(), 455 | }; 456 | 457 | PieEntity { 458 | index: entity_index, 459 | old_value: None, 460 | value, 461 | formatted_value, 462 | name, 463 | color, 464 | highlight_color, 465 | old_start_angle: start_angle, 466 | old_end_angle: start_angle, 467 | center: props.center, 468 | inner_radius: props.inner_radius, 469 | outer_radius: props.outer_radius, 470 | start_angle, 471 | end_angle: start_angle, // To be updated in `update_channel` 472 | } 473 | } 474 | 475 | fn create_channels(&self, start: usize, end: usize) { 476 | let mut start = start; 477 | let mut result = Vec::new(); 478 | let count = self.base.data.frames.len(); 479 | let meta = &self.base.data.meta; 480 | 481 | while start < end { 482 | let channel = meta.get(start).unwrap(); 483 | let name = channel.name; 484 | let color = self.base.get_color(start); 485 | let highlight = self.base.get_highlight_color(color); 486 | 487 | let entities = self.create_entities(start, 0, count, color, highlight); 488 | result.push(ChartChannel::new(name, color, highlight, entities)); 489 | start += 1; 490 | } 491 | 492 | let mut channels = self.base.channels.borrow_mut(); 493 | *channels = result; 494 | } 495 | 496 | fn create_entities( 497 | &self, 498 | channel_index: usize, 499 | start: usize, 500 | end: usize, 501 | color: Color, 502 | highlight: Color, 503 | ) -> Vec> { 504 | let mut start = start; 505 | let mut result = Vec::new(); 506 | while start < end { 507 | let frame = self.base.data.frames.get(start).unwrap(); 508 | let value = frame.data.get(channel_index as u64); 509 | let entity = match frame.data.get(channel_index as u64) { 510 | Some(value) => { 511 | let value = *value; 512 | self.create_entity(channel_index, start, Some(value), color, highlight) 513 | } 514 | None => self.create_entity(channel_index, start, None, color, highlight), 515 | }; 516 | 517 | result.push(entity); 518 | start += 1; 519 | } 520 | result 521 | } 522 | 523 | fn get_tooltip_position(&self, tooltip_width: f64, tooltip_height: f64) -> Point { 524 | let channels = self.base.channels.borrow(); 525 | let channel = channels.first().unwrap(); 526 | 527 | let focused_entity_index = self.base.props.borrow().focused_entity_index as usize; 528 | 529 | let props = self.props.borrow(); 530 | 531 | let pie = channel.entities.get(focused_entity_index).unwrap(); 532 | let angle = 0.5 * (pie.start_angle + pie.end_angle); 533 | let radius = 0.5 * (props.inner_radius + props.outer_radius); 534 | let point = utils::polar2cartesian(&props.center, radius, angle); 535 | let x = point.x - 0.5 * tooltip_width; 536 | let y = point.y - tooltip_height; 537 | Point::new(x, y) 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /src/radar.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_assignments)] 2 | #![allow(unused_variables)] 3 | #![allow(dead_code)] 4 | #![allow(clippy::explicit_counter_loop, clippy::needless_range_loop)] 5 | 6 | use animate::{ 7 | easing::{get_easing, Easing}, 8 | interpolate::lerp, 9 | }; 10 | use dataflow::*; 11 | use primitives::{BaseLine, CanvasContext, Color, Point, Rect, Size, TextStyle, TextWeight}; 12 | use std::{cell::RefCell, fmt}; 13 | 14 | use crate::*; 15 | 16 | #[derive(Default, Clone)] 17 | pub struct PolarPoint { 18 | color: Color, 19 | highlight_color: Color, 20 | // formatted_value: String, 21 | index: usize, 22 | old_value: Option, 23 | value: Option, 24 | 25 | old_radius: f64, 26 | old_angle: f64, 27 | old_point_radius: f64, 28 | 29 | radius: f64, 30 | angle: f64, 31 | point_radius: f64, 32 | 33 | center: Point, 34 | } 35 | 36 | impl Drawable for PolarPoint 37 | where 38 | C: CanvasContext, 39 | { 40 | fn draw(&self, ctx: &C, percent: f64, highlight: bool) { 41 | let r = lerp(self.old_radius, self.radius, percent); 42 | let a = lerp(self.old_angle, self.angle, percent); 43 | let pr = lerp(self.old_point_radius, self.point_radius, percent); 44 | let p = utils::polar2cartesian(&self.center, r, a); 45 | if highlight { 46 | ctx.set_fill_color(self.highlight_color); 47 | ctx.begin_path(); 48 | ctx.arc(p.x, p.y, 2. * pr, 0., TAU, false); 49 | ctx.fill(); 50 | } 51 | ctx.set_fill_color(self.color); 52 | ctx.begin_path(); 53 | ctx.arc(p.x, p.y, pr, 0., TAU, false); 54 | ctx.fill(); 55 | ctx.stroke(); 56 | } 57 | } 58 | 59 | impl Entity for PolarPoint { 60 | fn free(&mut self) {} 61 | 62 | fn save(&self) { 63 | // self.old_radius = self.radius; 64 | // self.old_angle = self.angle; 65 | // self.old_point_radius = self.point_radius; 66 | // self.old_value = self.value; 67 | } 68 | } 69 | 70 | #[derive(Default, Clone)] 71 | struct RadarChartProperties { 72 | center: Point, 73 | radius: f64, 74 | angle_interval: f64, 75 | xlabels: Vec, 76 | ylabels: Vec, 77 | ymax_value: f64, 78 | ylabel_hop: f64, 79 | ylabel_formatter: Option, 80 | /// Each element is the bounding box of each entity group. 81 | /// A `null` element means the group has no visible entities. 82 | bounding_boxes: Vec>, 83 | } 84 | 85 | pub struct RadarChart<'a, C, M, D> 86 | where 87 | C: CanvasContext, 88 | M: fmt::Display, 89 | D: fmt::Display + Copy, 90 | { 91 | props: RefCell, 92 | base: BaseChart<'a, C, PolarPoint, M, D, RadarChartOptions<'a>>, 93 | } 94 | 95 | impl<'a, C, M, D> RadarChart<'a, C, M, D> 96 | where 97 | C: CanvasContext, 98 | M: fmt::Display, 99 | D: fmt::Display + Copy + Into + Ord + Default, 100 | { 101 | pub fn new(options: RadarChartOptions<'a>) -> Self { 102 | Self { 103 | props: Default::default(), 104 | base: BaseChart::new(options), 105 | } 106 | } 107 | 108 | pub fn get_angle(&self, entity_index: usize) -> f64 { 109 | let props = self.props.borrow(); 110 | (entity_index as f64) * props.angle_interval - PI_2 111 | } 112 | 113 | pub fn value2radius(&self, value: Option) -> f64 { 114 | match value { 115 | Some(value) => { 116 | let props = self.props.borrow(); 117 | value.into() * props.radius / props.ymax_value 118 | } 119 | None => 0.0, 120 | } 121 | } 122 | 123 | fn calculate_bounding_boxes(&self) { 124 | if !self.base.options.tooltip.enabled { 125 | return; 126 | } 127 | 128 | let channels = self.base.channels.borrow(); 129 | let channel_count = channels.len(); 130 | 131 | let entity_count = { 132 | let channel = channels.get(0).unwrap(); 133 | channel.entities.len() 134 | }; 135 | 136 | let mut props = self.props.borrow_mut(); 137 | 138 | // OOCH 139 | props 140 | .bounding_boxes 141 | .resize(entity_count, Default::default()); 142 | 143 | for idx in 0..entity_count { 144 | let mut min_x = f64::MAX; 145 | let mut min_y = f64::MAX; 146 | let mut max_x = -f64::MAX; 147 | let mut max_y = -f64::MAX; 148 | let mut count = 0; 149 | for jdx in 0..channel_count { 150 | let channel = channels.get(jdx).unwrap(); 151 | if channel.state == Visibility::Hidden || channel.state == Visibility::Hiding { 152 | continue; 153 | } 154 | 155 | let channel = channels.get(jdx).unwrap(); 156 | let pp = channel.entities.get(idx).unwrap(); 157 | 158 | if pp.value.is_none() { 159 | continue; 160 | } 161 | 162 | let cp = utils::polar2cartesian(&pp.center, pp.radius, pp.angle); 163 | min_x = min_x.min(cp.x); 164 | min_y = min_y.min(cp.y); 165 | max_x = max_x.max(cp.x); 166 | max_y = max_y.max(cp.y); 167 | count += 1; 168 | } 169 | 170 | props.bounding_boxes[idx] = if count > 0 { 171 | Rect::new( 172 | Point::new(min_x, min_y), 173 | Size::new(max_x - min_x, max_y - min_y), 174 | ) 175 | } else { 176 | unimplemented!() 177 | }; 178 | } 179 | } 180 | 181 | fn draw_text(&self, ctx: &C, text: &str, radius: f64, angle: f64, font_size: f64) { 182 | let props = self.props.borrow(); 183 | let w = ctx.measure_text(text).width; 184 | let x = props.center.x + angle.cos() * (props.radius + 0.8 * w) - (0.5 * w); 185 | let y = props.center.y + angle.sin() * (props.radius + 0.8 * font_size) + (0.5 * font_size); 186 | ctx.fill_text(text, x, y); 187 | } 188 | 189 | fn get_entity_group_index(&self, x: f64, y: f64) -> i64 { 190 | let props = self.props.borrow(); 191 | let p = Point::new(x - props.center.x, y - props.center.y); 192 | 193 | if p.distance_to(Point::zero()) >= props.radius { 194 | return -1; 195 | } 196 | 197 | let angle = p.y.atan2(p.x); 198 | let channels = self.base.channels.borrow(); 199 | let channel = channels.first().unwrap(); 200 | let points = &channel.entities; 201 | 202 | for idx in points.len()..0 { 203 | if props.bounding_boxes.get(idx).is_none() { 204 | continue; 205 | } 206 | 207 | let delta = angle - points[idx].angle; 208 | if delta.abs() < 0.5 * props.angle_interval { 209 | return idx as i64; 210 | } 211 | if (delta + TAU).abs() < 0.5 * props.angle_interval { 212 | return idx as i64; 213 | } 214 | } 215 | -1 216 | } 217 | 218 | fn channel_visibility_changed(&self, index: usize) { 219 | let mut channels = self.base.channels.borrow_mut(); 220 | let channel = channels.get_mut(index).unwrap(); 221 | 222 | let visible = channel.state == Visibility::Showing || channel.state == Visibility::Shown; 223 | let marker_size = self.base.options.channel.markers.size; 224 | 225 | for entity in channel.entities.iter_mut() { 226 | if visible { 227 | entity.radius = self.value2radius(entity.value); 228 | entity.point_radius = marker_size; 229 | } else { 230 | entity.radius = 0.0; 231 | entity.point_radius = 0.; 232 | } 233 | } 234 | 235 | self.calculate_bounding_boxes(); 236 | } 237 | 238 | // /// Called when [data_table] has been changed. 239 | // fn data_changed(&self) { 240 | // info!("data_changed"); 241 | // // self.calculate_drawing_sizes(ctx); 242 | // self.create_channels(0, self.base.data.meta.len()); 243 | // } 244 | } 245 | 246 | impl<'a, C, M, D> Chart<'a, C, M, D, PolarPoint> for RadarChart<'a, C, M, D> 247 | where 248 | C: CanvasContext, 249 | M: fmt::Display, 250 | D: fmt::Display + Copy + Into + Ord + Default, 251 | { 252 | fn calculate_drawing_sizes(&self, ctx: &C) { 253 | self.base.calculate_drawing_sizes(ctx); 254 | 255 | let mut props = self.props.borrow_mut(); 256 | 257 | props.xlabels = self 258 | .base 259 | .data 260 | .frames 261 | .iter() 262 | .map(|item| item.metric.to_string()) 263 | .collect(); 264 | 265 | props.angle_interval = TAU / props.xlabels.len() as f64; 266 | 267 | let xlabel_font_size = self.base.options.xaxis.labels.style.fontsize.unwrap(); 268 | 269 | // [_radius]*factor equals the height of the largest polygon. 270 | let factor = 1. + ((props.xlabels.len() >> 1) as f64 * props.angle_interval - PI_2).sin(); 271 | 272 | { 273 | let rect = &self.base.props.borrow().area; 274 | props.radius = rect.size.width.min(rect.size.height) / factor 275 | - factor * (xlabel_font_size + AXIS_LABEL_MARGIN as f64); 276 | props.center = Point::new( 277 | rect.origin.x + rect.size.width / 2., 278 | rect.origin.y + rect.size.height / factor, 279 | ); 280 | } 281 | 282 | // The minimum value on the y-axis is always zero 283 | let yaxis = &self.base.options.yaxis; 284 | let yinterval = match yaxis.interval { 285 | Some(yinterval) => yinterval, 286 | None => { 287 | let ymin_interval = yaxis.min_interval.unwrap_or(0.0); 288 | 289 | props.ymax_value = utils::find_max_value(&self.base.data).into(); 290 | 291 | let yinterval = utils::calculate_interval(props.ymax_value, 3, Some(ymin_interval)); 292 | props.ymax_value = (props.ymax_value / yinterval).ceil() * yinterval; 293 | yinterval 294 | } 295 | }; 296 | 297 | props.ylabel_formatter = yaxis.labels.formatter; 298 | if props.ylabel_formatter.is_none() { 299 | // let max_decimal_places = 300 | // max(utils::get_decimal_places(props.yinterval), utils::get_decimal_places(props.y_min_value)); 301 | // let numberFormat = NumberFormat.decimalPattern() 302 | // ..maximumFractionDigits = max_decimal_places 303 | // ..minimumFractionDigits = max_decimal_places; 304 | // ylabel_formatter = numberFormat.format; 305 | let a = |x: f64| -> String { x.to_string() }; 306 | props.ylabel_formatter = Some(a); 307 | } 308 | 309 | let mut baseprops = self.base.props.borrow_mut(); 310 | baseprops.entity_value_formatter = props.ylabel_formatter; 311 | 312 | props.ylabels.clear(); 313 | let ylabel_formatter = props.ylabel_formatter.unwrap(); 314 | 315 | let mut value = 0.0; 316 | while value <= props.ymax_value { 317 | props.ylabels.push(ylabel_formatter(value)); 318 | value += yinterval; 319 | } 320 | 321 | props.ylabel_hop = props.radius / (props.ylabels.len() as f64 - 1.); 322 | 323 | // Tooltip. 324 | baseprops.tooltip_value_formatter = 325 | if let Some(value_formatter) = self.base.options.tooltip.value_formatter { 326 | Some(value_formatter) 327 | } else { 328 | Some(ylabel_formatter) 329 | } 330 | } 331 | 332 | fn set_stream(&mut self, stream: DataStream<'a, M, D>) { 333 | self.base.data = stream; 334 | self.create_channels(0, self.base.data.meta.len()); 335 | } 336 | 337 | fn draw(&self, ctx: &C) { 338 | self.base.dispose(); 339 | // data_tableSubscriptionTracker 340 | // ..add(dataTable.onCellChange.listen(data_cell_changed)) 341 | // ..add(dataTable.onColumnsChange.listen(dataColumnsChanged)) 342 | // ..add(dataTable.onRowsChange.listen(data_rows_changed)); 343 | // self.easing = get_easing(self.options.animation().easing); 344 | // self.base.initialize_legend(); 345 | // self.base.initialize_tooltip(); 346 | // self.base.position_legend(); 347 | 348 | // This call is redundant for row and column changes but necessary for 349 | // cell changes. 350 | self.calculate_drawing_sizes(ctx); 351 | self.update_channel(0); 352 | 353 | self.calculate_bounding_boxes(); 354 | self.draw_frame(ctx, None); 355 | } 356 | 357 | fn resize(&self, w: f64, h: f64) { 358 | self.base.resize(w, h); 359 | } 360 | 361 | fn draw_axes_and_grid(&self, ctx: &C) { 362 | let props = self.props.borrow(); 363 | let xlabel_count = props.xlabels.len(); 364 | let ylabel_count = props.ylabels.len(); 365 | 366 | // x-axis grid lines (i.e. concentric equilateral polygons). 367 | let mut line_width = self.base.options.xaxis.grid_line_width; 368 | if line_width > 0. { 369 | ctx.set_line_width(line_width); 370 | ctx.set_stroke_color(self.base.options.xaxis.grid_line_color); 371 | ctx.begin_path(); 372 | let mut radius = props.radius; 373 | for idx in 1..ylabel_count { 374 | let mut angle = -PI_2 + props.angle_interval; 375 | ctx.move_to(props.center.x, props.center.y - radius); 376 | for jdx in 0..xlabel_count { 377 | let point = utils::polar2cartesian(&props.center, radius, angle); 378 | ctx.line_to(point.x, point.y); 379 | angle += props.angle_interval; 380 | } 381 | radius -= props.ylabel_hop; 382 | } 383 | ctx.stroke(); 384 | } 385 | 386 | // y-axis grid lines (i.e. radii from the center to the x-axis labels). 387 | line_width = self.base.options.yaxis.grid_line_width; 388 | if line_width > 0. { 389 | ctx.set_line_width(line_width); 390 | ctx.set_stroke_color(self.base.options.yaxis.grid_line_color); 391 | ctx.begin_path(); 392 | let mut angle = -PI_2; 393 | for idx in 0..xlabel_count { 394 | let point = utils::polar2cartesian(&props.center, props.radius, angle); 395 | ctx.move_to(props.center.x, props.center.y); 396 | ctx.line_to(point.x, point.y); 397 | angle += props.angle_interval; 398 | } 399 | ctx.stroke(); 400 | } 401 | 402 | // y-axis labels - don"t draw the first (at center) and the last ones. 403 | let style = &self.base.options.yaxis.labels.style; 404 | let x = props.center.x - AXIS_LABEL_MARGIN as f64; 405 | let mut y = props.center.y - props.ylabel_hop; 406 | ctx.set_fill_color(style.color); 407 | 408 | ctx.set_font( 409 | &style.fontfamily.unwrap_or(DEFAULT_FONT_FAMILY), 410 | style.fontstyle.unwrap_or(TextStyle::Normal), 411 | TextWeight::Normal, 412 | style.fontsize.unwrap_or(12.), 413 | ); 414 | 415 | // ctx.set_text_align(TextAlign::Right); 416 | ctx.set_text_baseline(BaseLine::Middle); 417 | for idx in 1..ylabel_count - 1 { 418 | let text = props.ylabels[idx].as_str(); 419 | let w = ctx.measure_text(text).width; 420 | ctx.fill_text(props.ylabels[idx].as_str(), x - w, y - 4.); 421 | y -= props.ylabel_hop; 422 | } 423 | 424 | // x-axis labels. 425 | let style = &self.base.options.xaxis.labels.style; 426 | ctx.set_fill_color(style.color); 427 | 428 | ctx.set_font( 429 | &style.fontfamily.unwrap_or(DEFAULT_FONT_FAMILY), 430 | style.fontstyle.unwrap_or(TextStyle::Normal), 431 | TextWeight::Normal, 432 | style.fontsize.unwrap_or(12.), 433 | ); 434 | 435 | // ctx.set_text_align(TextAlign::Center); 436 | ctx.set_text_baseline(BaseLine::Middle); 437 | let font_size = style.fontsize.unwrap(); 438 | let mut angle = -PI_2; 439 | let radius = props.radius + AXIS_LABEL_MARGIN as f64; 440 | for idx in 0..xlabel_count { 441 | self.draw_text(ctx, props.xlabels[idx].as_str(), radius, angle, font_size); 442 | angle += props.angle_interval; 443 | } 444 | } 445 | 446 | /// Draws the current animation frame. 447 | /// 448 | /// If [time] is `null`, draws the last frame (i.e. no animation). 449 | fn draw_frame(&self, ctx: &C, time: Option) { 450 | // clear surface 451 | self.base.draw_frame(ctx, time); 452 | 453 | self.draw_axes_and_grid(ctx); 454 | 455 | let mut percent = self.base.calculate_percent(time); 456 | 457 | if percent >= 1.0 { 458 | percent = 1.0; 459 | 460 | // Update the visibility states of all channel before the last frame. 461 | let mut channels = self.base.channels.borrow_mut(); 462 | 463 | for channel in channels.iter_mut() { 464 | if channel.state == Visibility::Showing { 465 | channel.state = Visibility::Shown; 466 | } else if channel.state == Visibility::Hiding { 467 | channel.state = Visibility::Hidden; 468 | } 469 | } 470 | } 471 | 472 | let props = self.base.props.borrow(); 473 | 474 | let ease = match props.easing { 475 | Some(val) => val, 476 | None => get_easing(Easing::Linear), 477 | }; 478 | self.draw_channels(ctx, ease(percent)); 479 | // ctx.drawImageScaled(ctx.canvas, 0, 0, width, height); 480 | // ctx.drawImageScaled(ctx.canvas, 0, 0, width, height); 481 | self.base.draw_title(ctx); 482 | 483 | if percent < 1.0 { 484 | // animation_frame_id = window.requestAnimationFrame(draw_frame); 485 | } else if time.is_some() { 486 | self.base.animation_end(); 487 | } 488 | } 489 | 490 | fn draw_channels(&self, ctx: &C, percent: f64) -> bool { 491 | let props = self.props.borrow(); 492 | 493 | let mut focused_channel_index = self.base.props.borrow().focused_channel_index; 494 | focused_channel_index = -1; //FIXME: 495 | 496 | let fill_opacity = self.base.options.channel.fill_opacity; 497 | let channel_line_width = self.base.options.channel.line_width; 498 | let marker_options = &self.base.options.channel.markers; 499 | let marker_size = marker_options.size; 500 | let point_count = props.xlabels.len(); 501 | 502 | let channels = self.base.channels.borrow(); 503 | let mut focused_entity_index = self.base.props.borrow().focused_entity_index; 504 | focused_entity_index = -1; 505 | 506 | let mut idx = 0; 507 | for channel in channels.iter() { 508 | let scale = if idx as i64 != focused_channel_index { 509 | 1. 510 | } else { 511 | 2. 512 | }; 513 | 514 | idx += 1; 515 | if channel.state == Visibility::Hidden { 516 | continue; 517 | } 518 | 519 | // Optionally fill the polygon. 520 | if fill_opacity > 0. { 521 | ctx.set_fill_color(self.base.change_color_alpha(channel.color, fill_opacity)); 522 | ctx.begin_path(); 523 | for jdx in 0..point_count { 524 | let entity = channel.entities.get(jdx).unwrap(); 525 | // TODO: Optimize. 526 | let radius = lerp(entity.old_radius, entity.radius, percent); 527 | let angle = lerp(entity.old_angle, entity.angle, percent); 528 | let p = utils::polar2cartesian(&props.center, radius, angle); 529 | if jdx > 0 { 530 | ctx.line_to(p.x, p.y); 531 | } else { 532 | ctx.move_to(p.x, p.y); 533 | } 534 | } 535 | ctx.close_path(); 536 | ctx.fill(); 537 | } 538 | 539 | // Draw the polygon. 540 | ctx.set_line_width(scale * channel_line_width); 541 | ctx.set_stroke_color(channel.color); 542 | ctx.begin_path(); 543 | 544 | for jdx in 0..point_count { 545 | let entity = channel.entities.get(jdx).unwrap(); 546 | // TODO: Optimize. 547 | let radius = lerp(entity.old_radius, entity.radius, percent); 548 | let angle = lerp(entity.old_angle, entity.angle, percent); 549 | let p = utils::polar2cartesian(&props.center, radius, angle); 550 | if jdx > 0 { 551 | ctx.line_to(p.x, p.y); 552 | } else { 553 | ctx.move_to(p.x, p.y); 554 | } 555 | } 556 | ctx.close_path(); 557 | ctx.stroke(); 558 | 559 | // Draw the markers. 560 | if marker_size > 0. { 561 | let fill_color = if let Some(color) = marker_options.fill_color { 562 | color 563 | } else { 564 | channel.color 565 | }; 566 | 567 | let stroke_color = if let Some(color) = marker_options.stroke_color { 568 | color 569 | } else { 570 | channel.color 571 | }; 572 | 573 | ctx.set_fill_color(fill_color); 574 | ctx.set_line_width(scale * marker_options.line_width); 575 | ctx.set_stroke_color(stroke_color); 576 | for p in channel.entities.iter() { 577 | if marker_options.enabled { 578 | p.draw(ctx, percent, p.index as i64 == focused_entity_index); 579 | } else if p.index as i64 == focused_entity_index { 580 | // Only draw marker on hover 581 | p.draw(ctx, percent, true); 582 | } 583 | } 584 | } 585 | } 586 | 587 | false 588 | } 589 | 590 | // param should be Option 591 | fn update_channel(&self, _: usize) { 592 | let entity_count = self.base.data.frames.len(); 593 | let mut channels = self.base.channels.borrow_mut(); 594 | let props = self.props.borrow(); 595 | 596 | let mut idx = 0; 597 | for channel in channels.iter_mut() { 598 | let color = self.base.get_color(idx); 599 | let highlight_color = self.base.get_highlight_color(color); 600 | channel.color = color; 601 | channel.highlight = highlight_color; 602 | 603 | let visible = 604 | channel.state == Visibility::Showing || channel.state == Visibility::Shown; 605 | 606 | for jdx in 0..entity_count { 607 | let mut entity = channel.entities.get_mut(jdx).unwrap(); 608 | entity.index = jdx; 609 | entity.center = props.center; 610 | entity.radius = if visible { 611 | self.value2radius(entity.value) 612 | } else { 613 | 0.0 614 | }; 615 | entity.angle = self.get_angle(jdx); 616 | entity.color = color; 617 | entity.highlight_color = highlight_color; 618 | } 619 | idx += 1; 620 | } 621 | } 622 | 623 | fn create_entity( 624 | &self, 625 | channel_index: usize, 626 | entity_index: usize, 627 | value: Option, 628 | color: Color, 629 | highlight_color: Color, 630 | ) -> PolarPoint { 631 | let props = self.props.borrow(); 632 | let angle = self.get_angle(entity_index); 633 | let point_radius = self.base.options.channel.markers.size as f64; 634 | let radius = self.value2radius(value); 635 | 636 | PolarPoint { 637 | index: entity_index, 638 | value, 639 | old_value: None, 640 | color, 641 | highlight_color, 642 | center: props.center, 643 | old_radius: 0., 644 | old_angle: angle, 645 | old_point_radius: 0., 646 | radius, 647 | angle, 648 | point_radius, 649 | } 650 | } 651 | 652 | fn create_channels(&self, start: usize, end: usize) { 653 | let mut start = start; 654 | let mut result = Vec::new(); 655 | let count = self.base.data.frames.len(); 656 | let meta = &self.base.data.meta; 657 | while start < end { 658 | let channel = meta.get(start).unwrap(); 659 | let name = channel.name; 660 | let color = self.base.get_color(start); 661 | let highlight = self.base.get_highlight_color(color); 662 | 663 | let entities = self.create_entities(start, 0, count, color, highlight); 664 | result.push(ChartChannel::new(name, color, highlight, entities)); 665 | start += 1; 666 | } 667 | let mut channels = self.base.channels.borrow_mut(); 668 | *channels = result; 669 | } 670 | 671 | fn create_entities( 672 | &self, 673 | channel_index: usize, 674 | start: usize, 675 | end: usize, 676 | color: Color, 677 | highlight: Color, 678 | ) -> Vec> { 679 | let mut start = start; 680 | let mut result = Vec::new(); 681 | while start < end { 682 | let frame = self.base.data.frames.get(start).unwrap(); 683 | let value = frame.data.get(channel_index as u64); 684 | let entity = match frame.data.get(channel_index as u64) { 685 | Some(value) => { 686 | let value = *value; 687 | self.create_entity(channel_index, start, Some(value), color, highlight) 688 | } 689 | None => self.create_entity(channel_index, start, None, color, highlight), 690 | }; 691 | 692 | result.push(entity); 693 | start += 1; 694 | } 695 | result 696 | } 697 | 698 | fn get_tooltip_position(&self, tooltip_width: f64, tooltip_height: f64) -> Point { 699 | let props = self.props.borrow(); 700 | let focused_entity_index = self.base.props.borrow().focused_entity_index; 701 | 702 | let bounding_box = &props.bounding_boxes[focused_entity_index as usize]; 703 | let offset = self.base.options.channel.markers.size as f64 * 2. + 5.; 704 | let origin = bounding_box.origin; 705 | let mut x = origin.x + bounding_box.width() + offset; 706 | let y = origin.y + ((bounding_box.height() - tooltip_height) / 2.).trunc(); 707 | 708 | let width = self.base.props.borrow().width; 709 | if x + tooltip_width > width { 710 | x = origin.x - tooltip_width - offset; 711 | } 712 | 713 | Point::new(x, y) 714 | } 715 | } 716 | -------------------------------------------------------------------------------- /src/basechart.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | #![allow(clippy::explicit_counter_loop, clippy::float_cmp)] 3 | 4 | use animate::easing::EasingFunction; 5 | use dataflow::*; 6 | use primitives::{CanvasContext, Color, Point, Rect, RgbColor, Size, TextStyle, TextWeight}; 7 | use std::{cell::RefCell, collections::HashMap, fmt}; 8 | 9 | use super::*; 10 | 11 | // channel_states moved to channels 12 | 13 | #[derive(Default, Clone)] 14 | pub struct BaseChartProperties { 15 | /// ID of the current animation frame. 16 | pub animation_frame_id: usize, 17 | 18 | /// The starting time of an animation cycle. 19 | pub animation_start_time: Option, 20 | 21 | // dataTableSubscriptionTracker: StreamSubscriptionTracker, // = StreamSubscriptionTracker(); 22 | pub easing: Option, 23 | 24 | /// The chart"s width. 25 | pub height: f64, 26 | 27 | /// The chart"s height. 28 | pub width: f64, 29 | 30 | /// Index of the highlighted point group/bar group/pie/... 31 | pub focused_entity_index: i64, // = -1; 32 | 33 | pub focused_channel_index: i64, // = -1; 34 | 35 | pub entity_value_formatter: Option, 36 | 37 | /// The legend element. 38 | legend: Option, //Element, 39 | 40 | // /// The subscription tracker for legend items" events. 41 | // legendItemSubscriptionTracker: StreamSubscriptionTracker, // = StreamSubscriptionTracker(); 42 | 43 | // mouseMoveSub: StreamSubscription, 44 | /// The tooltip element. To position the tooltip, change its transform CSS. 45 | tooltip: Option, //Element, 46 | /// The function used to format channel names to display in the tooltip. 47 | pub tooltip_label_formatter: Option, 48 | 49 | /// The function used to format channel data to display in the tooltip. 50 | pub tooltip_value_formatter: Option, 51 | 52 | /// Bounding box of the channel and axes. 53 | pub area: Rect, 54 | 55 | /// Bounding box of the chart title. 56 | pub title_box: Rect, 57 | } 58 | 59 | /// Base class for all charts. 60 | #[derive(Default, Clone)] 61 | pub struct BaseChart<'a, C, E, M, D, O> 62 | where 63 | C: CanvasContext, 64 | E: Entity, 65 | M: fmt::Display, 66 | D: fmt::Display + Copy, 67 | O: BaseOption<'a>, 68 | { 69 | pub props: RefCell, 70 | /// The data table that stores chart data 71 | /// Row 0 contains column names. 72 | /// Column 0 contains x-axis/pie labels. 73 | /// Column 1..n - 1 contain channel data. 74 | pub data: DataStream<'a, M, D>, 75 | 76 | /// The drawing options initialized in the constructor. 77 | pub options: O, 78 | 79 | /// The main rendering context. 80 | pub context: Option, 81 | 82 | /// The rendering context for the axes. 83 | // pub ctx: Option, 84 | 85 | /// The rendering context for the channel. 86 | // pub ctx: Option, 87 | 88 | /// The precalcuated datas 89 | /// A ChartChannel keep track of the visibility of the channel. 90 | pub channels: RefCell>>, 91 | } 92 | 93 | impl<'a, C, E, M, D, O> BaseChart<'a, C, E, M, D, O> 94 | where 95 | C: CanvasContext, 96 | E: Entity, 97 | M: fmt::Display, 98 | D: fmt::Display + Copy, 99 | O: BaseOption<'a>, 100 | { 101 | // /// The element that contains this chart. 102 | // container: Element; 103 | 104 | /// Creates a chart given a container. 105 | /// 106 | /// If the CSS position of [container] is "static", it will be changed to 107 | /// "relative". 108 | pub fn new(options: O) -> Self { 109 | Self { 110 | props: Default::default(), 111 | data: Default::default(), 112 | options, 113 | context: None, 114 | channels: RefCell::new(Vec::new()), 115 | } 116 | } 117 | 118 | /// Creates a new color by combining the R, G, B components of [color] with 119 | /// [alpha] from 0 to 1. 120 | /// TODO: There are question about set the alpha or change from existing alpha 121 | /// 122 | pub fn change_color_alpha(&self, color: Color, alpha: f64) -> Color { 123 | if !(0. ..=1.).contains(&alpha) { 124 | panic!("Wrong alpha value {}", alpha); 125 | } 126 | 127 | let alpha = (alpha * 0xFF as f64).round() as u8; 128 | let color: RgbColor = color.into(); 129 | Color::RGBA(color.red, color.green, color.blue, alpha) 130 | } 131 | 132 | pub fn get_color(&self, index: usize) -> Color { 133 | let colors = self.options.colors(); 134 | let color = colors.get(index % colors.len()).unwrap(); 135 | *color 136 | } 137 | 138 | pub fn get_highlight_color(&self, color: Color) -> Color { 139 | self.change_color_alpha(color, 0.5) 140 | } 141 | 142 | /// Called when the animation ends. 143 | pub fn animation_end(&self) { 144 | let mut props = self.props.borrow_mut(); 145 | props.animation_start_time = None; 146 | 147 | let channels = self.channels.borrow(); 148 | for channel in channels.iter() { 149 | for entity in &channel.entities { 150 | // entity.save(); 151 | } 152 | } 153 | 154 | let animation = self.options.animation(); 155 | 156 | if let Some(callback) = animation.on_end { 157 | callback(); 158 | } 159 | } 160 | 161 | /// Event handler for [DataTable.onCellChanged]. 162 | /// 163 | /// NOTE: This method only handles the case when [record.columnIndex] >= 1; 164 | pub fn data_cell_changed(&self, record: DataCellChangeRecord) { 165 | if record.column_index >= 1 { 166 | // let f = entity_value_formatter != null && record.newValue != null 167 | // ? entity_value_formatter(record.newValue) 168 | // : null; 169 | // channels[record.columnIndex - 1].entities[record.rowIndex] 170 | // ..value = record.newValue 171 | // ..formatted_value = f; 172 | } 173 | } 174 | 175 | /// Event handler for [DataTable.onRowsChanged]. 176 | pub fn data_rows_changed(&self, record: DataCollectionChangeRecord) { 177 | // self.calculate_drawing_sizes(ctx); 178 | let entity_count = self.data.frames.len(); 179 | let removed_end = record.index + record.removed_count; 180 | let added_end = record.index + record.added_count; 181 | let channels = self.channels.borrow(); 182 | for channel in channels.iter() { 183 | // Remove old entities. 184 | if record.removed_count > 0 { 185 | // channel.freeEntities(record.index, removedEnd); 186 | // channel.entities.remove_range(record.index, removedEnd); 187 | } 188 | 189 | // Insert new entities. 190 | if record.added_count > 0 { 191 | // let newEntities = create_entities( 192 | // i, record.index, addedEnd, channel.color, channel.highlight_color); 193 | // channel.entities.insertAll(record.index, newEntities); 194 | 195 | // // Update entity indexes. 196 | // for (let j = addedEnd; j < entity_count; j++) { 197 | // channel.entities[j].index = j; 198 | // } 199 | } 200 | } 201 | } 202 | 203 | /// Event handler for [DataTable.onColumnsChanged]. 204 | pub fn data_columns_changed(&self, record: DataCollectionChangeRecord) { 205 | debug!( 206 | "data_columns_changed remove[{}] add[{}]", 207 | record.removed_count, record.added_count 208 | ); 209 | // self.calculate_drawing_sizes(ctx); 210 | let start = record.index - 1; 211 | self.update_channel_visible(start, record.removed_count, record.added_count); 212 | if record.removed_count > 0 { 213 | let end = start + record.removed_count; 214 | for idx in start..end { 215 | // self.channels[idx].freeEntities(0); 216 | } 217 | // self.channels.remove_range(start, end); 218 | } 219 | 220 | if record.added_count > 0 { 221 | // let list = self.create_channels(start, start + record.added_count); 222 | // self.channels.insertAll(start, list); 223 | } 224 | self.update_legend_content(); 225 | } 226 | 227 | pub fn update_channel_visible(&self, index: usize, removed_count: usize, added_count: usize) { 228 | if removed_count > 0 { 229 | // self.channel_states.remove_range(index, index + removed_count); 230 | } 231 | if added_count > 0 { 232 | // let list = List.filled(added_count, Visibility::showing); 233 | // self.channel_states.insertAll(index, list); 234 | } 235 | unimplemented!() 236 | } 237 | 238 | /// Draws the chart title using the main rendering context. 239 | pub fn draw_title(&self, ctx: &C) { 240 | let title = self.options.title(); 241 | if let Some(text) = title.text { 242 | let props = self.props.borrow(); 243 | // let x = ((props.title_box.origin.x + props.title_box.size.width) / 2.).trunc(); 244 | let x = props.title_box.origin.x; 245 | let y = (props.title_box.origin.y + props.title_box.size.height) - TITLE_PADDING; 246 | let style = &title.style; 247 | ctx.set_font( 248 | style.fontfamily.unwrap_or(DEFAULT_FONT_FAMILY), 249 | style.fontstyle.unwrap_or(TextStyle::Normal), 250 | TextWeight::Normal, 251 | style.fontsize.unwrap_or(12.), 252 | ); 253 | ctx.set_fill_color(title.style.color); 254 | // ctx.set_text_align(TextAlign::Center); 255 | ctx.fill_text(text, x, y); 256 | } 257 | } 258 | 259 | pub fn initialize_legend(&self) { 260 | let new_len = self.get_legend_labels().len(); 261 | let props = self.props.borrow_mut(); 262 | 263 | if let Some(legend) = props.legend { 264 | // self.legend.remove(); 265 | // self.legend = null; 266 | } 267 | 268 | if let Position::None = self.options.legend().position { 269 | return; 270 | } 271 | 272 | // props.legend = self.create_tooltip_or_legend(self.options.legend().style); 273 | // props.legend.style.lineHeight = "180%"; 274 | self.update_legend_content(); 275 | // container.append(props.legend); 276 | } 277 | 278 | /// This must be called after [calculate_drawing_sizes] as we need to know 279 | /// where the title is in order to position the legend correctly. 280 | pub fn position_legend(&self) { 281 | // println!("BaseChart position_legend"); 282 | let props = self.props.borrow(); 283 | if let Some(legend) = props.legend { 284 | // let s = legend.style; 285 | // switch (self.options.legend().position) { 286 | // case "right": 287 | // s.right = "${CHART_PADDING}px"; 288 | // s.top = "50%"; 289 | // s.transform = "translateY(-50%)"; 290 | // break; 291 | // case "bottom": 292 | // let bottom = CHART_PADDING; 293 | // if (self.options.title().position == "below" && title_box.height > 0) { 294 | // bottom += title_box.height; 295 | // } 296 | // s.bottom = "${bottom}px"; 297 | // s.left = "50%"; 298 | // s.transform = "translateX(-50%)"; 299 | // break; 300 | // case "left": 301 | // s.left = "${CHART_PADDING}px"; 302 | // s.top = "50%"; 303 | // s.transform = "translateY(-50%)"; 304 | // break; 305 | // case "top": 306 | // let top = CHART_PADDING; 307 | // if (self.options.title().position == "above" && title_box.height > 0) { 308 | // top += title_box.height; 309 | // } 310 | // s.top = "${top}px"; 311 | // s.left = "50%"; 312 | // s.transform = "translateX(-50%)"; 313 | // break; 314 | // } 315 | } 316 | } 317 | 318 | pub fn update_legend_content(&self) { 319 | let labels = self.get_legend_labels(); 320 | // let formatter = 321 | // self.options.legend().labelFormatter ?? default_label_formatter; 322 | // legend_item_subscription_tracker.clear(); 323 | // legend.innerHtml = ""; 324 | // for (let i = 0; i < labels.len(); i++) { 325 | // let label = labels[i]; 326 | // let formattedLabel = formatter(label); 327 | // let e = create_tooltip_or_legendItem(self.get_color(i), formattedLabel); 328 | // if (label != formattedLabel) { 329 | // e.title = label; 330 | // } 331 | // e.style.cursor = "pointer"; 332 | // e.style.userSelect = "none"; 333 | // legend_item_subscription_tracker 334 | // ..add(e.onClick.listen(legend_item_click)) 335 | // ..add(e.onMouseOver.listen(legend_item_mouse_over)) 336 | // ..add(e.onMouseOut.listen(legend_item_mouse_out)); 337 | 338 | // let state = channel_states[i]; 339 | // if (state == Visibility::hidden || 340 | // state == Visibility::hiding) { 341 | // e.style.opacity = ".4"; 342 | // } 343 | 344 | // // Display the items in one row if the legend"s position is "top" or 345 | // // "bottom". 346 | // let pos = self.options.legend().position; 347 | // if (pos == "top" || pos == "bottom") { 348 | // e.style.display = "inline-block"; 349 | // } 350 | // self.legend.append(e); 351 | // } 352 | } 353 | 354 | pub fn get_legend_labels(&self) -> Vec { 355 | self.data 356 | .meta 357 | .iter() 358 | .map(|channel| channel.name.to_string()) 359 | .collect() 360 | } 361 | 362 | pub fn legend_item_click(&self, e: MouseEvent) { 363 | if !self.is_interactive() { 364 | return; 365 | } 366 | 367 | // let item = e.currentTarget as Element; 368 | // let index = item.parent.children.indexOf(item); 369 | 370 | // if (channel_states[index] == Visibility::shown) { 371 | // channel_states[index] = Visibility::hiding; 372 | // item.style.opacity = ".4"; 373 | // } else { 374 | // channel_states[index] = Visibility::showing; 375 | // item.style.opacity = ""; 376 | // } 377 | 378 | // channel_visibility_changed(index); 379 | self.start_animation(); 380 | } 381 | 382 | pub fn legend_item_mouse_over(&self, e: MouseEvent) { 383 | if !self.is_interactive() { 384 | return; 385 | } 386 | 387 | // let item = e.currentTarget as Element; 388 | // focused_channel_index = item.parent.children.indexOf(item); 389 | // draw_frame(null); 390 | } 391 | 392 | pub fn legend_item_mouse_out(&self, e: MouseEvent) { 393 | if !self.is_interactive() { 394 | return; 395 | } 396 | 397 | // focused_channel_index = -1; 398 | // draw_frame(null); 399 | } 400 | 401 | /// Called when the visibility of a channel is changed. 402 | /// 403 | /// [index] is the index of the affected channel. 404 | /// 405 | pub fn channel_visibility_changed(&self, index: usize) {} 406 | 407 | /// Returns the index of the point group/bar group/pie/... near the position 408 | /// specified by [x] and [y]. 409 | /// 410 | pub fn get_entity_group_index(&self, x: f64, y: f64) -> i64 { 411 | -1 412 | } 413 | 414 | /// Handles `mousemove` or `touchstart` events to highlight appropriate 415 | /// points/bars/pies/... as well as update the tooltip. 416 | pub fn mouse_move(&self, e: MouseEvent) { 417 | // if !self.is_interactive() || e.buttons != 0 { 418 | // return; 419 | // } 420 | 421 | // let rect = ctx.canvas.getBoundingClientRect(); 422 | // let x = e.client.x - rect.left; 423 | // let y = e.client.y - rect.top; 424 | // let index = getEntityGroupIndex(x, y); 425 | 426 | // if index != focused_entity_index { 427 | // focused_entity_index = index; 428 | // draw_frame(null); 429 | // if (index >= 0) { 430 | // update_tooltip_content(); 431 | // tooltip.hidden = false; 432 | // let p = getTooltipPosition(); 433 | // tooltip.style.transform = "translate(${p.x}px, ${p.y}px)"; 434 | // } else { 435 | // tooltip.hidden = true; 436 | // } 437 | // } 438 | } 439 | 440 | pub fn initialize_tooltip(&self) { 441 | // println!("BaseChart initialize_tooltip"); 442 | // if self.tooltip != null { 443 | // tooltip.remove(); 444 | // tooltip = null; 445 | // } 446 | 447 | // let opt = self.options.tooltip; 448 | // if (!opt["enabled"]) return; 449 | 450 | // tooltip_label_formatter = opt["labelFormatter"] ?? default_label_formatter; 451 | // tooltip_value_formatter = opt["value_formatter"] ?? default_value_formatter; 452 | // tooltip = create_tooltip_or_legend(opt.style.) 453 | // ..hidden = true 454 | // ..style.left = "0" 455 | // ..style.top = "0" 456 | // ..style.boxShadow = "4px 4px 4px rgba(0,0,0,.25)" 457 | // ..style.transition = "transform .4s cubic-bezier(.4,1,.4,1)"; 458 | // container.append(tooltip); 459 | 460 | // mouse_move_sub?.cancel(); 461 | // mouse_move_sub = container.onMouseMove.listen(mouseMove); 462 | } 463 | 464 | pub fn update_tooltip_content(&self) { 465 | let props = self.props.borrow(); 466 | let column_count = self.data.meta.len(); 467 | let row = self.data.frames.get(props.focused_entity_index as usize); 468 | // tooltip.innerHtml = ""; 469 | 470 | // // Tooltip title 471 | // tooltip.append(DivElement() 472 | // ..text = row[0] 473 | // ..style.padding = "4px 12px" 474 | // ..style.fontWeight = "bold"); 475 | 476 | // Tooltip items. 477 | for idx in 1..column_count { 478 | // let state = props.channel_states.get(idx - 1); 479 | // if (state == Visibility::hidden) continue; 480 | // if (state == Visibility::hiding) continue; 481 | 482 | // let channel = channels[i - 1]; 483 | // let value = row[i]; 484 | // if (value == null) continue; 485 | 486 | // value = tooltip_value_formatter(value); 487 | // let label = tooltip_label_formatter(channel.name); 488 | 489 | // let e = create_tooltip_or_legendItem( 490 | // channel.color, "$label: $value"); 491 | // tooltip.append(e); 492 | } 493 | } 494 | 495 | /// Creates an absolute positioned div with styles specified by [style]. 496 | // TODO: retusns Element 497 | pub fn create_tooltip_or_legend(&self, style: HashMap) -> Option { 498 | // return DivElement() 499 | // ..style.background_color = style["background_color"] 500 | // ..style.borderColor = style["borderColor"] 501 | // ..style.borderStyle = "solid" 502 | // ..style.borderWidth = "${style["borderWidth"]}px" 503 | // ..style.color = style["color"] 504 | // ..style.fontFamily = style["fontFamily"] 505 | // ..style.font_size = "${style["font_size"]}px" 506 | // ..style.fontStyle = style["fontStyle"] 507 | // ..style.position = "absolute"; 508 | unimplemented!() 509 | } 510 | 511 | // TODO: return Element 512 | pub fn create_tooltip_or_legend_item(&self, color: String, text: String) -> Option { 513 | // let e = DivElement() 514 | // ..innerHtml = " $text" 515 | // ..style.padding = "4px 12px"; 516 | // e.children.first.style 517 | // ..background_color = color 518 | // ..display = "inline-block" 519 | // ..width = "12px" 520 | // ..height = "12px"; 521 | // return e; 522 | unimplemented!() 523 | } 524 | 525 | // real drawing 526 | pub fn start_animation(&self) { 527 | // println!("BaseChart start_animation"); 528 | // animation_frame_id = window.requestAnimationFrame(draw_frame); 529 | } 530 | 531 | pub fn stop_animation(&self) { 532 | // println!("BaseChart stop_animation"); 533 | // animation_start_time = null; 534 | // if self.animation_frame_id != 0 { 535 | // // window.cancelAnimationFrame(animation_frame_id); 536 | // self.animation_frame_id = 0; 537 | // } 538 | } 539 | 540 | // @Deprecated("Use [isAnimating] instead") 541 | pub fn animating(&self) -> bool { 542 | self.is_animating() 543 | } 544 | 545 | /// Whether the chart is animating. 546 | pub fn is_animating(&self) -> bool { 547 | self.props.borrow().animation_start_time.is_some() 548 | } 549 | 550 | /// Whether the chart is interactive. 551 | /// 552 | /// This property returns `false` if the chart is animating or there are no 553 | /// channel to draw. 554 | pub fn is_interactive(&self) -> bool { 555 | !self.is_animating() && self.channels.borrow().len() != 0 556 | } 557 | 558 | /// Disposes of resources used by this chart. The chart will become unusable 559 | /// until [draw] is called again. 560 | /// 561 | /// Be sure to call this method when the chart is no longer used to afn any 562 | /// memory leaks. 563 | /// 564 | /// @mustCallSuper 565 | pub fn dispose(&self) { 566 | // println!("BaseChart dispose"); 567 | // // This causes [canHandleInteraction] to be `false`. 568 | // channels = null; 569 | // mouse_move_sub?.cancel(); 570 | // mouse_move_sub = null; 571 | // data_tableSubscriptionTracker.clear(); 572 | // legend_item_subscription_tracker.clear(); 573 | } 574 | 575 | pub fn calculate_percent(&self, time: Option) -> f64 { 576 | let mut percent = 1.0; 577 | let mut props = self.props.borrow_mut(); 578 | 579 | if props.animation_start_time.is_none() { 580 | props.animation_start_time = time 581 | } 582 | 583 | if let Some(time) = time { 584 | let duration = self.options.animation().duration; 585 | if duration > 0 { 586 | percent = (time - props.animation_start_time.unwrap()) as f64 / duration as f64; 587 | } 588 | } 589 | percent 590 | } 591 | } 592 | 593 | impl<'a, C, E, M, D, O> Chart<'a, C, M, D, E> for BaseChart<'a, C, E, M, D, O> 594 | where 595 | C: CanvasContext, 596 | E: Entity, 597 | M: fmt::Display, 598 | D: fmt::Display + Copy, 599 | O: BaseOption<'a>, 600 | { 601 | /// Calculates various drawing sizes. 602 | /// 603 | /// Overriding methods must call this method first to have [area] 604 | /// calculated. 605 | /// 606 | fn calculate_drawing_sizes(&self, ctx: &C) { 607 | let title = self.options.title(); 608 | 609 | let mut title_x = 0.0; 610 | let mut title_y = 0.0; 611 | let mut title_w = 0.0; 612 | let mut title_h = 0.0; 613 | 614 | let mut props = self.props.borrow_mut(); 615 | 616 | let prepare_title = match title.position { 617 | Position::Above => { 618 | title_h = title.style.fontsize.unwrap_or(12.) + 2.0 * TITLE_PADDING; 619 | title_y = CHART_PADDING; 620 | props.area.origin.y += title_h + CHART_TITLE_MARGIN; 621 | props.area.size.height -= title_h + CHART_TITLE_MARGIN; 622 | true 623 | } 624 | Position::Middle => { 625 | title_h = title.style.fontsize.unwrap_or(12.) + 2.0 * TITLE_PADDING; 626 | title_y = f64::floor((props.height - title_h) / 2.0); 627 | true 628 | } 629 | Position::Below => { 630 | title_h = title.style.fontsize.unwrap_or(12.) + 2.0 * TITLE_PADDING; 631 | title_y = props.height - title_h - CHART_PADDING; 632 | props.area.size.height -= title_h + CHART_TITLE_MARGIN; 633 | true 634 | } 635 | _ => false, 636 | }; 637 | 638 | if prepare_title { 639 | if let Some(text) = title.text { 640 | let style = &title.style; 641 | ctx.set_font( 642 | style.fontfamily.unwrap_or(DEFAULT_FONT_FAMILY), 643 | style.fontstyle.unwrap_or(TextStyle::Normal), 644 | TextWeight::Normal, 645 | style.fontsize.unwrap_or(12.), 646 | ); 647 | title_w = ctx.measure_text(text).width.round() + 2. * TITLE_PADDING; 648 | title_x = ((props.width - title_w - 2. * TITLE_PADDING) / 2.).trunc(); 649 | } 650 | 651 | // Consider the title. 652 | props.title_box = Rect { 653 | origin: Point::new(title_x, title_y), 654 | size: Size::new(title_w, title_h), 655 | }; 656 | } 657 | 658 | // Consider the legend. 659 | if props.legend.is_some() { 660 | // let lwm = self.legend.offset_width + legend_margin; 661 | // let lhm = self.legend.offset_height + legend_margin; 662 | let opt = self.options.legend(); 663 | match opt.position { 664 | Position::Right => { 665 | // props.area.size.width -= lwm; 666 | } 667 | Position::Bottom => { 668 | // props.area.size.height -= lhm; 669 | } 670 | Position::Left => { 671 | // props.area.origin.x += lwm; 672 | // props.area.size.width -= lwm; 673 | } 674 | Position::Top => { 675 | // props.area.origin.y += lhm; 676 | // props.area.size.height -= lhm; 677 | } 678 | _ => {} 679 | } 680 | } 681 | } 682 | 683 | fn set_stream(&mut self, stream: DataStream<'a, M, D>) { 684 | error!("set stream"); 685 | } 686 | 687 | /// Draws the chart given a data table [dataTable] and an optional set of 688 | /// options [options]. 689 | // TODO: handle updates while animation is happening. 690 | fn draw(&self, ctx: &C) { 691 | // TODO: use this not_eq 692 | // let props = self.props.borrow(); 693 | // if props.width == 0_f64 || props.height == 0_f64 { 694 | // return; 695 | // } 696 | 697 | // data_tableSubscriptionTracker 698 | // ..add(dataTable.onCellChange.listen(data_cell_changed)) 699 | // ..add(dataTable.onColumnsChange.listen(dataColumnsChanged)) 700 | // ..add(dataTable.onRowsChange.listen(data_rows_changed)); 701 | 702 | // self.ctx.clearRect(0, 0, self.width, self.height); 703 | } 704 | 705 | /// Resizes just only change size state for chart and do not resize the container/canvas. 706 | fn resize(&self, w: f64, h: f64) { 707 | // println!("BaseChart resize {} {}", w, h); 708 | if w == 0_f64 || h == 0_f64 { 709 | println!("BaseChart resize OOOPS"); 710 | return; 711 | } 712 | 713 | let mut props = self.props.borrow_mut(); 714 | if w != props.width || h != props.height { 715 | props.width = w; 716 | props.height = h; 717 | // force_redraw = true; // now_eq 718 | } 719 | 720 | props.area = Rect { 721 | origin: Point::new(CHART_PADDING, CHART_PADDING), 722 | size: Size::new( 723 | props.width - 2.0 * CHART_PADDING, 724 | props.height - 2.0 * CHART_PADDING, 725 | ), 726 | }; 727 | } 728 | 729 | /// Draws the axes and the grid. 730 | /// 731 | fn draw_axes_and_grid(&self, ctx: &C) {} 732 | 733 | /// Updates the channel at index [index]. If [index] is `null`, updates all 734 | /// channel. 735 | /// 736 | fn update_channel(&self, _: usize) {} 737 | 738 | // println!("SIZE {} {}", width, height); 739 | // println!("BACKGROUND {}", self.options.background()); 740 | 741 | /// Draws the current animation frame. 742 | /// 743 | /// If [time] is `null`, draws the last frame (i.e. no animation). 744 | fn draw_frame(&self, ctx: &C, time: Option) { 745 | let props = self.props.borrow(); 746 | let width = props.width; 747 | let height = props.height; 748 | 749 | ctx.set_fill_color(*self.options.background()); 750 | // just fill instead clear 751 | ctx.fill_rect(0., 0., width, height); 752 | } 753 | 754 | /// Draws the channel given the current animation percent [percent]. 755 | /// 756 | /// If this method returns `false`, the animation is continued until [percent] 757 | /// reaches 1.0. 758 | /// 759 | /// If this method returns `true`, the animation is stopped immediately. 760 | /// This is useful as there are cases where no animation is expected. 761 | /// In those cases, the overriding method will return `true` to stop the 762 | /// animation. 763 | /// 764 | fn draw_channels(&self, ctx: &C, percent: f64) -> bool { 765 | error!("draw_channels"); 766 | false 767 | } 768 | 769 | fn create_entity( 770 | &self, 771 | channel_index: usize, 772 | entity_index: usize, 773 | value: Option, 774 | color: Color, 775 | highlight_color: Color, 776 | ) -> E { 777 | todo!() 778 | } 779 | 780 | fn create_entities( 781 | &self, 782 | channel_index: usize, 783 | start: usize, 784 | end: usize, 785 | color: Color, 786 | highlight: Color, 787 | ) -> Vec { 788 | error!("create_entities"); 789 | Vec::new() 790 | } 791 | 792 | fn create_channels(&self, start: usize, end: usize) { 793 | error!("create_channels"); 794 | } 795 | 796 | fn get_tooltip_position(&self, tooltip_width: f64, tooltip_height: f64) -> Point { 797 | todo!() 798 | } 799 | } 800 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | use super::{LabelFormatter, ValueFormatter}; 2 | use primitives::{color, Color, TextStyle}; 3 | 4 | pub enum Position { 5 | Above, 6 | Middle, 7 | Below, 8 | Left, 9 | Top, 10 | Bottom, 11 | Right, 12 | None, 13 | } 14 | 15 | pub trait BaseOption<'a> { 16 | fn animation(&self) -> &AnimationOptions; 17 | fn colors(&self) -> &Vec; 18 | fn title(&self) -> &TitleOptions<'a>; 19 | fn legend(&self) -> &LegendOptions<'a>; 20 | fn tooltip(&self) -> &TooltipOptions<'a>; 21 | fn background(&self) -> &Color; 22 | } 23 | 24 | pub struct AnimationOptions { 25 | /// The animation duration in ms. 26 | pub duration: usize, 27 | 28 | /// String|EasingFunction - Name of a predefined easing function or an 29 | /// easing function itself. 30 | /// 31 | /// See [animation.dart] for the full list of predefined functions. 32 | pub easing: String, 33 | 34 | /// () -> fn - The function that is called when the animation is complete. 35 | pub on_end: Option, 36 | } 37 | 38 | pub struct LegendOptions<'a> { 39 | /// (String label) -> String - A function that format the labels. 40 | pub label_formatter: Option, 41 | 42 | /// The position of the legend relative to the chart area. 43 | /// Supported values: "left", "top", "bottom", "right", "none". 44 | pub position: Position, 45 | 46 | /// An object that controls the styling of the legend. 47 | pub style: StyleOption<'a>, 48 | } 49 | 50 | pub struct TitleOptions<'a> { 51 | /// The position of the title relative to the chart area. 52 | /// Supported values: "above", "below", "middle", "none"; 53 | pub position: Position, 54 | 55 | /// An object that controls the styling of the chart title. 56 | pub style: StyleOption<'a>, 57 | 58 | /// The title text. A `null` value means the title is hidden. 59 | pub text: Option<&'a str>, 60 | } 61 | 62 | pub struct TooltipOptions<'a> { 63 | /// bool - Whether to show the tooltip. 64 | pub enabled: bool, 65 | 66 | /// (String label) -> String - A function that format the labels. 67 | pub label_formatter: Option, 68 | 69 | /// An object that controls the styling of the tooltip. 70 | pub style: StyleOption<'a>, 71 | 72 | /// (num value) -> String - A function that formats the values. 73 | pub value_formatter: Option, 74 | } 75 | 76 | #[derive(Debug, Clone)] 77 | pub struct BarChartSeriesOptions<'a> { 78 | /// An object that controls the channel labels. 79 | /// bool - Whether to show the labels. 80 | pub labels: Option>, 81 | } 82 | 83 | pub struct BarChartCrosshairOptions { 84 | /// The fill color of the crosshair. 85 | pub color: Color, 86 | } 87 | 88 | pub struct BarChartXAxisLabelsOptions<'a> { 89 | /// The maximum rotation angle in degrees. Must be <= 90. 90 | pub max_rotation: i64, 91 | 92 | /// The minimum rotation angle in degrees. Must be >= -90. 93 | pub min_rotation: i64, 94 | 95 | pub style: StyleOption<'a>, 96 | } 97 | 98 | pub struct BarChartXAxisOptions<'a> { 99 | /// An object that controls the crosshair. 100 | pub crosshair: Option, 101 | 102 | /// The color of the horizontal grid lines. 103 | pub grid_line_color: Color, 104 | 105 | /// The width of the horizontal grid lines. 106 | pub grid_line_width: f64, 107 | 108 | /// The color of the axis itself. 109 | pub line_color: Color, 110 | 111 | /// The width of the axis itself. 112 | pub line_width: f64, 113 | 114 | /// An object that controls the axis labels. 115 | pub labels: BarChartXAxisLabelsOptions<'a>, 116 | 117 | /// The position of the axis relative to the chart area. 118 | /// Supported values: "bottom". 119 | pub position: Position, 120 | 121 | /// An object that controls the axis title. 122 | pub title: TitleOption<'a>, 123 | } 124 | 125 | pub struct BarChartYAxisLabelsOptions<'a> { 126 | /// (num value) -> String - A function that formats the labels. 127 | pub formatter: Option, 128 | 129 | /// An object that controls the styling of the axis labels. 130 | pub style: StyleOption<'a>, 131 | } 132 | 133 | pub struct BarChartYAxisOptions<'a> { 134 | /// The color of the vertical grid lines. 135 | pub grid_line_color: Color, 136 | 137 | /// The width of the vertical grid lines. 138 | pub grid_line_width: f64, 139 | 140 | /// The color of the axis itself. 141 | pub line_color: Color, 142 | 143 | /// The width of the axis itself. 144 | pub line_width: f64, 145 | 146 | /// The interval of the tick marks in axis unit. If `null`, this value 147 | /// is automatically calculated. 148 | pub interval: Option, 149 | 150 | /// An object that controls the axis labels. 151 | pub labels: BarChartYAxisLabelsOptions<'a>, 152 | 153 | /// The desired maximum value on the axis. If set, the calculated value 154 | /// is guaranteed to be >= this value. 155 | pub max_value: Option, 156 | 157 | /// The minimum interval. If `null`, this value is automatically 158 | /// calculated. 159 | pub min_interval: Option, 160 | 161 | /// The desired minimum value on the axis. If set, the calculated value 162 | /// is guaranteed to be <= this value. 163 | pub min_value: Option, 164 | 165 | /// The position of the axis relative to the chart area. 166 | /// Supported values: "left". 167 | pub position: Position, 168 | 169 | /// An object that controls the axis title. 170 | pub title: TitleOption<'a>, 171 | } 172 | 173 | pub struct BarChartOptions<'a> { 174 | /// An object that controls the channel. 175 | pub channel: BarChartSeriesOptions<'a>, 176 | 177 | /// An object that controls the x-axis. 178 | pub xaxis: BarChartXAxisOptions<'a>, 179 | 180 | /// An object that controls the y-axis. 181 | pub yaxis: BarChartYAxisOptions<'a>, 182 | 183 | /// An object that controls the animation. 184 | pub animation: AnimationOptions, 185 | 186 | /// The background color of the chart. 187 | pub background: Color, 188 | 189 | /// The color list used to render the channel. If there are more channel than 190 | /// colors, the colors will be reused. 191 | pub colors: Vec, 192 | 193 | /// An object that controls the legend. 194 | pub legend: LegendOptions<'a>, 195 | 196 | /// An object that controls the chart title. 197 | pub title: TitleOptions<'a>, 198 | 199 | /// An object that controls the tooltip. 200 | pub tooltip: TooltipOptions<'a>, 201 | } 202 | 203 | impl<'a> BaseOption<'a> for BarChartOptions<'a> { 204 | fn animation(&self) -> &AnimationOptions { 205 | &self.animation 206 | } 207 | 208 | fn colors(&self) -> &Vec { 209 | &self.colors 210 | } 211 | 212 | fn title(&self) -> &TitleOptions<'a> { 213 | &self.title 214 | } 215 | 216 | fn legend(&self) -> &LegendOptions<'a> { 217 | &self.legend 218 | } 219 | 220 | fn tooltip(&self) -> &TooltipOptions<'a> { 221 | &self.tooltip 222 | } 223 | 224 | fn background(&self) -> &Color { 225 | &self.background 226 | } 227 | } 228 | 229 | pub struct TitleOption<'a> { 230 | /// An object that controls the styling of the axis title. 231 | pub style: StyleOption<'a>, 232 | 233 | /// The title text. A `null` value means the title is hidden. 234 | pub text: Option<&'a str>, 235 | } 236 | 237 | impl<'a> Default for BarChartOptions<'a> { 238 | fn default() -> Self { 239 | Self { 240 | channel: BarChartSeriesOptions { labels: None }, 241 | xaxis: BarChartXAxisOptions { 242 | crosshair: None, 243 | grid_line_color: color::GRAY_5, 244 | grid_line_width: 1., 245 | line_color: color::GRAY_5, 246 | line_width: 1., 247 | labels: BarChartXAxisLabelsOptions { 248 | max_rotation: 0, 249 | min_rotation: -90, 250 | style: Default::default(), 251 | }, 252 | position: Position::Bottom, 253 | title: TitleOption { 254 | style: Default::default(), 255 | text: None, 256 | }, 257 | }, 258 | yaxis: BarChartYAxisOptions { 259 | grid_line_color: color::GRAY_5, 260 | grid_line_width: 0., 261 | line_color: color::GRAY_5, 262 | line_width: 0., 263 | interval: None, 264 | labels: BarChartYAxisLabelsOptions { 265 | formatter: None, 266 | style: Default::default(), 267 | }, 268 | max_value: None, 269 | min_interval: None, 270 | min_value: None, 271 | position: Position::Left, 272 | title: TitleOption { 273 | style: Default::default(), 274 | text: None, 275 | }, 276 | }, 277 | animation: AnimationOptions { 278 | duration: 800, 279 | easing: "easeOutQuint".into(), 280 | on_end: None, 281 | }, 282 | background: color::WHITE, 283 | colors: vec![ 284 | Color::RGB(0x7c, 0xb5, 0xec), 285 | Color::RGB(0x43, 0x43, 0x48), 286 | Color::RGB(0x90, 0xed, 0x7d), 287 | Color::RGB(0xf7, 0xa3, 0x5c), 288 | Color::RGB(0x80, 0x85, 0xe9), 289 | Color::RGB(0xf1, 0x5c, 0x80), 290 | Color::RGB(0xe4, 0xd3, 0x54), 291 | Color::RGB(0x80, 0x85, 0xe8), 292 | Color::RGB(0x8d, 0x46, 0x53), 293 | Color::RGB(0x91, 0xe8, 0xe1), 294 | ], 295 | legend: LegendOptions { 296 | label_formatter: None, 297 | position: Position::Right, 298 | style: Default::default(), 299 | }, 300 | title: TitleOptions { 301 | position: Position::Above, 302 | style: Default::default(), 303 | text: None, 304 | }, 305 | tooltip: TooltipOptions { 306 | enabled: true, 307 | label_formatter: None, 308 | style: Default::default(), 309 | value_formatter: None, 310 | }, 311 | } 312 | } 313 | } 314 | 315 | #[derive(Debug, Clone)] 316 | pub struct StyleOption<'a> { 317 | pub background: Color, 318 | pub border_color: Color, 319 | pub border_width: f64, // i32? 320 | /// The title"s color 321 | pub color: Color, 322 | /// The title"s font family. 323 | pub fontfamily: Option<&'a str>, 324 | /// The title"s font size. 325 | pub fontsize: Option, 326 | /// The title"s font style. 327 | pub fontstyle: Option, 328 | } 329 | 330 | impl<'a> Default for StyleOption<'a> { 331 | fn default() -> Self { 332 | Self { 333 | background: color::WHITE, 334 | border_color: color::GRAY_4, 335 | border_width: 0_f64, 336 | color: color::GRAY_9, 337 | fontfamily: Some("Roboto"), 338 | fontsize: Some(12_f64), 339 | fontstyle: Some(TextStyle::Normal), 340 | } 341 | } 342 | } 343 | 344 | pub struct GaugeChartOptions<'a> { 345 | /// An object that controls the gauge labels. 346 | /// Whether to show the labels 347 | /// An object that controls the styling of the gauge labels 348 | pub labels: Option>, 349 | 350 | /// An object that controls the animation. 351 | pub animation: AnimationOptions, 352 | 353 | /// The background color of the chart. 354 | pub background: Color, 355 | 356 | /// The background color of the gauge. 357 | pub gauge_background: Color, 358 | 359 | /// The color list used to render the channel. If there are more channel than 360 | /// colors, the colors will be reused. 361 | pub colors: Vec, 362 | 363 | /// An object that controls the legend. 364 | pub legend: LegendOptions<'a>, 365 | 366 | /// An object that controls the chart title. 367 | pub title: TitleOptions<'a>, 368 | 369 | /// An object that controls the tooltip. 370 | pub tooltip: TooltipOptions<'a>, 371 | } 372 | 373 | impl<'a> BaseOption<'a> for GaugeChartOptions<'a> { 374 | fn animation(&self) -> &AnimationOptions { 375 | &self.animation 376 | } 377 | 378 | fn colors(&self) -> &Vec { 379 | &self.colors 380 | } 381 | 382 | fn title(&self) -> &TitleOptions<'a> { 383 | &self.title 384 | } 385 | 386 | fn legend(&self) -> &LegendOptions<'a> { 387 | &self.legend 388 | } 389 | 390 | fn tooltip(&self) -> &TooltipOptions<'a> { 391 | &self.tooltip 392 | } 393 | 394 | fn background(&self) -> &Color { 395 | &self.background 396 | } 397 | } 398 | 399 | impl<'a> Default for GaugeChartOptions<'a> { 400 | fn default() -> Self { 401 | Self { 402 | labels: Default::default(), 403 | animation: AnimationOptions { 404 | duration: 800, 405 | easing: "easeOutQuint".into(), 406 | on_end: None, 407 | }, 408 | background: color::WHITE, 409 | gauge_background: color::GRAY_3, 410 | colors: vec![ 411 | Color::RGB(0x7c, 0xb5, 0xec), 412 | Color::RGB(0x43, 0x43, 0x48), 413 | Color::RGB(0x90, 0xed, 0x7d), 414 | Color::RGB(0xf7, 0xa3, 0x5c), 415 | Color::RGB(0x80, 0x85, 0xe9), 416 | Color::RGB(0xf1, 0x5c, 0x80), 417 | Color::RGB(0xe4, 0xd3, 0x54), 418 | Color::RGB(0x80, 0x85, 0xe8), 419 | Color::RGB(0x8d, 0x46, 0x53), 420 | Color::RGB(0x91, 0xe8, 0xe1), 421 | ], 422 | legend: LegendOptions { 423 | label_formatter: None, 424 | position: Position::Right, 425 | style: Default::default(), 426 | }, 427 | title: TitleOptions { 428 | position: Position::Above, 429 | style: Default::default(), 430 | text: None, 431 | }, 432 | tooltip: TooltipOptions { 433 | enabled: true, 434 | label_formatter: None, 435 | style: Default::default(), 436 | value_formatter: None, 437 | }, 438 | } 439 | } 440 | } 441 | 442 | pub struct LineChartSeriesMarkersOptions { 443 | /// bool - Whether markers are enabled. 444 | pub enabled: bool, 445 | 446 | /// The fill color. If `null`, the stroke color of the channel 447 | /// will be used. 448 | pub fill_color: Option, 449 | 450 | /// The line width of the markers. 451 | pub line_width: usize, 452 | 453 | /// The stroke color. If `null`, the stroke color of the channel 454 | /// will be used. 455 | pub stroke_color: Option, 456 | 457 | /// Size of the markers. 458 | pub size: f64, 459 | } 460 | 461 | pub struct LineChartSeriesOptions<'a> { 462 | /// The curve tension. The typical value is from 0.3 to 0.5. 463 | /// To draw straight lines, set this to zero. 464 | pub curve_tension: f64, 465 | 466 | /// The opacity of the area between a channel and the x-axis. 467 | pub fill_opacity: f64, 468 | 469 | /// The line width of the channel. 470 | pub line_width: f64, 471 | 472 | /// An object that controls the channel labels. 473 | /// Whether to show the labels 474 | pub labels: Option>, 475 | 476 | /// An object that controls the markers. 477 | pub markers: LineChartSeriesMarkersOptions, 478 | } 479 | 480 | pub struct LineChartXAxisLabelsOptions<'a> { 481 | /// The maximum rotation angle in degrees. Must be <= 90. 482 | pub max_rotation: i64, 483 | 484 | /// The minimum rotation angle in degrees. Must be >= -90. 485 | pub min_rotation: i64, 486 | 487 | pub style: StyleOption<'a>, 488 | } 489 | 490 | pub struct LineChartXAxisOptions<'a> { 491 | /// The color of the horizontal grid lines. 492 | pub grid_line_color: Color, 493 | 494 | /// The width of the horizontal grid lines. 495 | pub grid_line_width: f64, 496 | 497 | /// The color of the axis itself. 498 | pub line_color: Color, 499 | 500 | /// The width of the axis itself. 501 | pub line_width: f64, 502 | 503 | /// An object that controls the axis labels. 504 | pub labels: LineChartXAxisLabelsOptions<'a>, 505 | 506 | /// The position of the axis relative to the chart area. 507 | /// Supported values: "bottom". 508 | pub position: Position, 509 | 510 | /// An object that controls the axis title. 511 | pub title: TitleOption<'a>, 512 | } 513 | 514 | pub struct LineChartYAxisLabelsOptions<'a> { 515 | /// (num value) -> String - A function that formats the labels. 516 | pub formatter: Option, 517 | 518 | /// An object that controls the styling of the axis labels. 519 | pub style: StyleOption<'a>, 520 | } 521 | pub struct LineChartYAxisOptions<'a> { 522 | /// The color of the vertical grid lines. 523 | pub grid_line_color: Color, 524 | 525 | /// The width of the vertical grid lines. 526 | pub grid_line_width: f64, 527 | 528 | /// The color of the axis itself. 529 | pub line_color: Color, 530 | 531 | /// The width of the axis itself. 532 | pub line_width: f64, 533 | 534 | /// The interval of the tick marks in axis unit. If `null`, this value 535 | /// is automatically calculated. 536 | pub interval: Option, 537 | 538 | /// An object that controls the axis labels. 539 | pub labels: LineChartYAxisLabelsOptions<'a>, 540 | 541 | /// The desired maximum value on the axis. If set, the calculated value 542 | /// is guaranteed to be >= this value. 543 | pub max_value: Option, 544 | 545 | /// The minimum interval. If `null`, this value is automatically 546 | /// calculated. 547 | pub min_interval: Option, 548 | 549 | /// The desired minimum value on the axis. If set, the calculated value 550 | /// is guaranteed to be <= this value. 551 | pub min_value: Option, 552 | 553 | /// The position of the axis relative to the chart area. 554 | /// Supported values: "left". 555 | pub position: Position, 556 | 557 | /// An object that controls the axis title. 558 | pub title: TitleOption<'a>, 559 | } 560 | 561 | pub struct LineChartOptions<'a> { 562 | /// An object that controls the channel. 563 | pub channel: LineChartSeriesOptions<'a>, 564 | 565 | /// An object that controls the x-axis. 566 | pub xaxis: LineChartXAxisOptions<'a>, 567 | 568 | /// An object that controls the y-axis. 569 | pub yaxis: LineChartYAxisOptions<'a>, 570 | 571 | /// An object that controls the animation. 572 | pub animation: AnimationOptions, 573 | 574 | /// The background color of the chart. 575 | pub background: Color, 576 | 577 | /// The color list used to render the channel. If there are more channel than 578 | /// colors, the colors will be reused. 579 | pub colors: Vec, 580 | 581 | /// An object that controls the legend. 582 | pub legend: LegendOptions<'a>, 583 | 584 | /// An object that controls the chart title. 585 | pub title: TitleOptions<'a>, 586 | 587 | /// An object that controls the tooltip. 588 | pub tooltip: TooltipOptions<'a>, 589 | } 590 | 591 | impl<'a> BaseOption<'a> for LineChartOptions<'a> { 592 | fn animation(&self) -> &AnimationOptions { 593 | &self.animation 594 | } 595 | 596 | fn colors(&self) -> &Vec { 597 | &self.colors 598 | } 599 | 600 | fn title(&self) -> &TitleOptions<'a> { 601 | &self.title 602 | } 603 | 604 | fn legend(&self) -> &LegendOptions<'a> { 605 | &self.legend 606 | } 607 | 608 | fn tooltip(&self) -> &TooltipOptions<'a> { 609 | &self.tooltip 610 | } 611 | 612 | fn background(&self) -> &Color { 613 | &self.background 614 | } 615 | } 616 | 617 | impl<'a> Default for LineChartOptions<'a> { 618 | fn default() -> Self { 619 | Self { 620 | channel: LineChartSeriesOptions { 621 | curve_tension: 0.4, 622 | fill_opacity: 0.25, 623 | line_width: 2_f64, 624 | labels: None, 625 | markers: LineChartSeriesMarkersOptions { 626 | enabled: true, 627 | fill_color: None, 628 | line_width: 1, 629 | stroke_color: Some(color::WHITE), 630 | size: 4., 631 | }, 632 | }, 633 | xaxis: LineChartXAxisOptions { 634 | grid_line_color: color::GRAY_5, 635 | grid_line_width: 1., 636 | line_color: color::GRAY_5, 637 | line_width: 1., 638 | labels: LineChartXAxisLabelsOptions { 639 | max_rotation: 0, 640 | min_rotation: -90, 641 | style: Default::default(), 642 | }, 643 | position: Position::Bottom, 644 | title: TitleOption { 645 | style: Default::default(), 646 | text: None, 647 | }, 648 | }, 649 | yaxis: LineChartYAxisOptions { 650 | grid_line_color: color::GRAY_5, 651 | grid_line_width: 0., 652 | line_color: color::GRAY_5, 653 | line_width: 0., 654 | interval: None, 655 | labels: LineChartYAxisLabelsOptions { 656 | formatter: None, 657 | style: Default::default(), 658 | }, 659 | max_value: None, 660 | min_interval: None, 661 | min_value: None, 662 | position: Position::Left, 663 | title: TitleOption { 664 | style: Default::default(), 665 | text: None, 666 | }, 667 | }, 668 | animation: AnimationOptions { 669 | duration: 800, 670 | easing: "easeOutQuint".into(), 671 | on_end: None, 672 | }, 673 | background: color::WHITE, 674 | colors: vec![ 675 | Color::RGB(0x7c, 0xb5, 0xec), 676 | Color::RGB(0x43, 0x43, 0x48), 677 | Color::RGB(0x90, 0xed, 0x7d), 678 | Color::RGB(0xf7, 0xa3, 0x5c), 679 | Color::RGB(0x80, 0x85, 0xe9), 680 | Color::RGB(0xf1, 0x5c, 0x80), 681 | Color::RGB(0xe4, 0xd3, 0x54), 682 | Color::RGB(0x80, 0x85, 0xe8), 683 | Color::RGB(0x8d, 0x46, 0x53), 684 | Color::RGB(0x91, 0xe8, 0xe1), 685 | ], 686 | legend: LegendOptions { 687 | label_formatter: None, 688 | position: Position::Right, 689 | style: Default::default(), 690 | }, 691 | title: TitleOptions { 692 | position: Position::Above, 693 | style: Default::default(), 694 | text: None, 695 | }, 696 | tooltip: TooltipOptions { 697 | enabled: true, 698 | label_formatter: None, 699 | style: Default::default(), 700 | value_formatter: None, 701 | }, 702 | } 703 | } 704 | } 705 | 706 | pub struct PieChartSeriesLabelsOptions<'a> { 707 | /// bool - Whether to show the labels. 708 | pub enabled: bool, 709 | 710 | /// (num) -> String - A function used to format the labels. 711 | pub formatter: Option, 712 | 713 | pub style: StyleOption<'a>, 714 | } 715 | 716 | pub struct PieChartSeriesOptions<'a> { 717 | /// bool - Whether to draw the slices counterclockwise. 718 | pub counterclockwise: bool, 719 | 720 | /// An object that controls the channel labels. 721 | pub labels: PieChartSeriesLabelsOptions<'a>, 722 | 723 | /// The start angle in degrees. Default is -90, which is 12 o'clock. 724 | pub start_angle: f64, 725 | } 726 | 727 | pub struct PieChartOptions<'a> { 728 | /// If between 0 and 1, displays a donut chart. The hole will have a 729 | /// radius equal to this value times the radius of the chart. 730 | pub pie_hole: f64, 731 | 732 | /// An object that controls the channel. 733 | pub channel: PieChartSeriesOptions<'a>, 734 | 735 | /// An object that controls the animation. 736 | pub animation: AnimationOptions, 737 | 738 | /// The background color of the chart. 739 | pub background: Color, 740 | 741 | /// The color list used to render the channel. If there are more channel than 742 | /// colors, the colors will be reused. 743 | pub colors: Vec, 744 | 745 | /// An object that controls the legend. 746 | pub legend: LegendOptions<'a>, 747 | 748 | /// An object that controls the chart title. 749 | pub title: TitleOptions<'a>, 750 | 751 | /// An object that controls the tooltip. 752 | pub tooltip: TooltipOptions<'a>, 753 | } 754 | 755 | impl<'a> BaseOption<'a> for PieChartOptions<'a> { 756 | fn animation(&self) -> &AnimationOptions { 757 | &self.animation 758 | } 759 | 760 | fn colors(&self) -> &Vec { 761 | &self.colors 762 | } 763 | 764 | fn title(&self) -> &TitleOptions<'a> { 765 | &self.title 766 | } 767 | 768 | fn legend(&self) -> &LegendOptions<'a> { 769 | &self.legend 770 | } 771 | 772 | fn tooltip(&self) -> &TooltipOptions<'a> { 773 | &self.tooltip 774 | } 775 | 776 | fn background(&self) -> &Color { 777 | &self.background 778 | } 779 | } 780 | 781 | impl<'a> Default for PieChartOptions<'a> { 782 | fn default() -> Self { 783 | Self { 784 | pie_hole: 0_f64, 785 | channel: PieChartSeriesOptions { 786 | counterclockwise: false, 787 | labels: PieChartSeriesLabelsOptions { 788 | enabled: false, 789 | formatter: None, 790 | style: Default::default(), 791 | }, 792 | start_angle: -90_f64, 793 | }, 794 | animation: AnimationOptions { 795 | duration: 800, 796 | easing: "easeOutQuint".into(), 797 | on_end: None, 798 | }, 799 | background: color::WHITE, 800 | colors: vec![ 801 | Color::RGB(0x7c, 0xb5, 0xec), 802 | Color::RGB(0x43, 0x43, 0x48), 803 | Color::RGB(0x90, 0xed, 0x7d), 804 | Color::RGB(0xf7, 0xa3, 0x5c), 805 | Color::RGB(0x80, 0x85, 0xe9), 806 | Color::RGB(0xf1, 0x5c, 0x80), 807 | Color::RGB(0xe4, 0xd3, 0x54), 808 | Color::RGB(0x80, 0x85, 0xe8), 809 | Color::RGB(0x8d, 0x46, 0x53), 810 | Color::RGB(0x91, 0xe8, 0xe1), 811 | ], 812 | legend: LegendOptions { 813 | label_formatter: None, 814 | position: Position::Right, 815 | style: Default::default(), 816 | }, 817 | title: TitleOptions { 818 | position: Position::Above, 819 | style: Default::default(), 820 | text: None, 821 | }, 822 | tooltip: TooltipOptions { 823 | enabled: true, 824 | label_formatter: None, 825 | style: Default::default(), 826 | value_formatter: None, 827 | }, 828 | } 829 | } 830 | } 831 | 832 | pub struct RadarChartSeriesMarkersOptions { 833 | /// bool - Whether markers are enabled. 834 | pub enabled: bool, 835 | 836 | /// The fill color. If `null`, the stroke color of the channel 837 | /// will be used. 838 | pub fill_color: Option, 839 | 840 | /// The line width of the markers. 841 | pub line_width: f64, 842 | 843 | /// The stroke color. If `null`, the stroke color of the channel 844 | /// will be used. 845 | pub stroke_color: Option, 846 | 847 | /// Size of the markers. To disable markers, set this to zero. 848 | pub size: f64, 849 | } 850 | 851 | pub struct RadarChartSeriesOptions<'a> { 852 | /// The opacity of the area between a channel and the x-axis. 853 | pub fill_opacity: f64, 854 | 855 | /// The line width of the channel. 856 | pub line_width: f64, 857 | 858 | /// An object that controls the channel labels. 859 | /// Whether to show the labels. 860 | pub labels: Option>, 861 | 862 | /// An object that controls the markers. 863 | pub markers: RadarChartSeriesMarkersOptions, 864 | } 865 | 866 | pub struct RadarChartXAxisLabelsOptions<'a> { 867 | /// (num value) -> String - A function that formats the labels. 868 | pub formatter: Option, 869 | 870 | /// An object that controls the styling of the axis labels. 871 | pub style: StyleOption<'a>, 872 | } 873 | 874 | pub struct RadarChartXAxisOptions<'a> { 875 | /// The color of the horizontal grid lines. 876 | pub grid_line_color: Color, 877 | 878 | /// The width of the horizontal grid lines. 879 | pub grid_line_width: f64, 880 | 881 | /// An object that controls the axis labels. 882 | pub labels: RadarChartXAxisLabelsOptions<'a>, 883 | } 884 | 885 | pub struct RadarChartYAxisLabelsOptions<'a> { 886 | /// (num value) -> String - A function that formats the labels. 887 | pub formatter: Option, 888 | 889 | /// An object that controls the styling of the axis labels. 890 | pub style: StyleOption<'a>, 891 | } 892 | 893 | pub struct RadarChartYAxisOptions<'a> { 894 | /// The color of the vertical grid lines. 895 | pub grid_line_color: Color, 896 | 897 | /// The width of the vertical grid lines. 898 | pub grid_line_width: f64, 899 | 900 | /// The interval of the tick marks in axis unit. If `null`, this value 901 | /// is automatically calculated. 902 | pub interval: Option, 903 | 904 | /// An object that controls the axis labels. 905 | pub labels: RadarChartYAxisLabelsOptions<'a>, 906 | 907 | /// The minimum interval. If `null`, this value is automatically 908 | /// calculated. 909 | pub min_interval: Option, 910 | } 911 | 912 | pub struct RadarChartOptions<'a> { 913 | // An object that controls the channel. 914 | pub channel: RadarChartSeriesOptions<'a>, 915 | 916 | /// An object that controls the x-axis. 917 | pub xaxis: RadarChartXAxisOptions<'a>, 918 | 919 | /// An object that controls the y-axis. 920 | pub yaxis: RadarChartYAxisOptions<'a>, 921 | 922 | /// An object that controls the animation. 923 | pub animation: AnimationOptions, 924 | 925 | /// The background color of the chart. 926 | pub background_color: Color, 927 | 928 | /// The color list used to render the channel. If there are more channel than 929 | /// colors, the colors will be reused. 930 | pub colors: Vec, 931 | 932 | /// An object that controls the legend. 933 | pub legend: LegendOptions<'a>, 934 | 935 | /// An object that controls the chart title. 936 | pub title: TitleOptions<'a>, 937 | 938 | /// An object that controls the tooltip. 939 | pub tooltip: TooltipOptions<'a>, 940 | } 941 | 942 | impl<'a> BaseOption<'a> for RadarChartOptions<'a> { 943 | fn animation(&self) -> &AnimationOptions { 944 | &self.animation 945 | } 946 | 947 | fn colors(&self) -> &Vec { 948 | &self.colors 949 | } 950 | 951 | fn title(&self) -> &TitleOptions<'a> { 952 | &self.title 953 | } 954 | 955 | fn legend(&self) -> &LegendOptions<'a> { 956 | &self.legend 957 | } 958 | 959 | fn tooltip(&self) -> &TooltipOptions<'a> { 960 | &self.tooltip 961 | } 962 | 963 | fn background(&self) -> &Color { 964 | &self.background_color 965 | } 966 | } 967 | 968 | impl<'a> Default for RadarChartOptions<'a> { 969 | fn default() -> Self { 970 | Self { 971 | channel: RadarChartSeriesOptions { 972 | fill_opacity: 0.25, 973 | line_width: 2., 974 | labels: None, 975 | markers: RadarChartSeriesMarkersOptions { 976 | enabled: true, 977 | fill_color: None, 978 | line_width: 1., 979 | stroke_color: Some(color::WHITE), 980 | size: 4., 981 | }, 982 | }, 983 | xaxis: RadarChartXAxisOptions { 984 | grid_line_color: color::GRAY_5, 985 | grid_line_width: 1_f64, 986 | labels: RadarChartXAxisLabelsOptions { 987 | formatter: None, 988 | style: Default::default(), 989 | }, 990 | }, 991 | yaxis: RadarChartYAxisOptions { 992 | grid_line_color: color::GRAY_5, 993 | grid_line_width: 1_f64, 994 | interval: None, 995 | labels: RadarChartYAxisLabelsOptions { 996 | formatter: None, 997 | style: Default::default(), 998 | }, 999 | min_interval: None, 1000 | }, 1001 | animation: AnimationOptions { 1002 | duration: 800, 1003 | easing: "easeOutQuint".into(), 1004 | on_end: None, 1005 | }, 1006 | background_color: color::WHITE, 1007 | colors: vec![ 1008 | Color::RGB(0x7c, 0xb5, 0xec), 1009 | Color::RGB(0x43, 0x43, 0x48), 1010 | Color::RGB(0x90, 0xed, 0x7d), 1011 | Color::RGB(0xf7, 0xa3, 0x5c), 1012 | Color::RGB(0x80, 0x85, 0xe9), 1013 | Color::RGB(0xf1, 0x5c, 0x80), 1014 | Color::RGB(0xe4, 0xd3, 0x54), 1015 | Color::RGB(0x80, 0x85, 0xe8), 1016 | Color::RGB(0x8d, 0x46, 0x53), 1017 | Color::RGB(0x91, 0xe8, 0xe1), 1018 | ], 1019 | legend: LegendOptions { 1020 | label_formatter: None, 1021 | position: Position::Right, 1022 | style: Default::default(), 1023 | }, 1024 | title: TitleOptions { 1025 | position: Position::Above, 1026 | style: Default::default(), 1027 | text: None, 1028 | }, 1029 | tooltip: TooltipOptions { 1030 | enabled: true, 1031 | label_formatter: None, 1032 | style: Default::default(), 1033 | value_formatter: None, 1034 | }, 1035 | } 1036 | } 1037 | } 1038 | --------------------------------------------------------------------------------