├── examples ├── index.css ├── favicon.png ├── chart_js_rs_example_bg.wasm ├── Cargo.toml ├── snippets │ ├── chart-js-rs-339a98cce73d8073 │ │ └── js │ │ │ └── exports.js │ ├── chart-js-rs-71705c35f71c3454 │ │ └── js │ │ │ └── exports.js │ ├── chart-js-rs-78d36b04da5ebd5d │ │ └── js │ │ │ └── exports.js │ ├── chart-js-rs-f75c9d6e04df1ec1 │ │ └── js │ │ │ └── exports.js │ └── chart-js-rs-65afd13e28fe916c │ │ └── js │ │ └── exports.js ├── worker_imports.js ├── index.html └── src │ ├── utils.rs │ └── lib.rs ├── .gitignore ├── .cargo └── config.toml ├── index.html ├── src ├── objects │ ├── methods.rs │ ├── mod.rs │ └── helper_objects.rs ├── exports.rs ├── functions.rs ├── pie.rs ├── scatter.rs ├── doughnut.rs ├── bar.rs ├── traits.rs ├── lib.rs ├── utils.rs ├── coordinate.rs └── worker.rs ├── .github └── workflows │ └── fmt.yml ├── Cargo.toml ├── js ├── exports.js └── worker_shim.js ├── README.md ├── LICENSE └── Cargo.lock /examples/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Chart.js/ 2 | node_modules/ 3 | 4 | # Added by cargo 5 | 6 | target 7 | build_logs.txt 8 | -------------------------------------------------------------------------------- /examples/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billy-Sheppard/chart-js-rs/HEAD/examples/favicon.png -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/chart_js_rs_example_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billy-Sheppard/chart-js-rs/HEAD/examples/chart_js_rs_example_bg.wasm -------------------------------------------------------------------------------- /src/objects/methods.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::default_constructed_unit_structs)] 2 | 3 | include!(concat!(env!("OUT_DIR"), "/methods.rs")); 4 | -------------------------------------------------------------------------------- /src/exports.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; 2 | 3 | #[wasm_bindgen(module = "/js/exports.js")] 4 | extern "C" { 5 | pub fn get_chart(id: &str) -> JsValue; 6 | 7 | pub fn render_chart(v: JsValue, id: &str, mutate: bool, plugins: String, defaults: String); 8 | 9 | pub fn update_chart(updated: JsValue, id: &str, animate: bool) -> bool; 10 | } 11 | -------------------------------------------------------------------------------- /src/functions.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::JsValue; 2 | 3 | pub fn tick_callback(js: &str, arg1: &str, arg2: &str, arg3: &str) -> JsValue { 4 | JsValue::from_str(&format!( 5 | r#"function ({arg1}, {arg2}, {arg3}) {{ 6 | {js} 7 | }} 8 | "#, 9 | )) 10 | } 11 | pub fn single_arg_callback(js: &str, arg: &str) -> JsValue { 12 | JsValue::from_str(&format!( 13 | r#"function ({arg}) {{ 14 | {js} 15 | }} 16 | "#, 17 | )) 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/fmt.yml: -------------------------------------------------------------------------------- 1 | name: "PR Checks" 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | formatting: 7 | name: Check Code Formatting 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | # Ensure rustfmt is installed and setup problem matcher 12 | - uses: actions-rust-lang/setup-rust-toolchain@v1 13 | with: 14 | components: rustfmt 15 | - name: Rustfmt Check 16 | uses: actions-rust-lang/rustfmt@v1 17 | security_audit: 18 | name: Rust Audit Check 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v1 22 | - uses: actions-rs/audit-check@v1 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Billy Sheppard "] 3 | name = "chart-js-rs-example" 4 | version = "0.1.2" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | chart-js-rs = { path = "../", features = ["workers"]} 12 | console_error_panic_hook = { version = "*", default-features = false } 13 | dominator = { version = "*", default-features = false } 14 | futures-signals = { version = "*", default-features = false } 15 | getrandom = { version = "*", features = ["js"] } 16 | gloo = { version = "*" } 17 | itertools = "*" 18 | js-sys = { version = "*", default-features = false } 19 | rand = { version = "*", default-features = false, features = ["std_rng"] } 20 | serde_json = "*" 21 | wasm-bindgen = { version = "*" } 22 | wasm-bindgen-futures = { version = "*", default-features = false } 23 | web-sys = { version = "*", default-features = false } 24 | -------------------------------------------------------------------------------- /examples/snippets/chart-js-rs-339a98cce73d8073/js/exports.js: -------------------------------------------------------------------------------- 1 | export function get_chart(id) { 2 | return Chart.getChart(document.getElementById(id)).config._config 3 | } 4 | 5 | export function render_chart(v, id, mutate, plugins, defaults) { 6 | console.debug('Before mutate:', v); 7 | 8 | let obj; 9 | 10 | if (defaults != null || defaults != undefined) { 11 | defaults = eval(defaults); 12 | } 13 | 14 | if (plugins != null || plugins != undefined) { 15 | v.plugins = eval(plugins); 16 | } 17 | 18 | if (mutate) { 19 | obj = window.mutate_chart_object(v) 20 | } 21 | else { 22 | obj = v 23 | }; 24 | 25 | console.debug('After mutate:', obj); 26 | 27 | const ctx = document.getElementById(id); 28 | let chart = new Chart(ctx, obj); 29 | } 30 | 31 | export function update_chart(updated, id, animate) { 32 | try { 33 | let chart = Chart.getChart(document.getElementById(id)); 34 | chart.config._config.type = updated.type; 35 | chart.config._config.data = updated.data; 36 | chart.config._config.options = updated.options; 37 | 38 | console.debug('Updated chart:', chart); 39 | 40 | if (animate) { 41 | chart.update(); 42 | } else { 43 | chart.update('none'); 44 | } 45 | 46 | true 47 | } 48 | catch { 49 | false 50 | } 51 | } -------------------------------------------------------------------------------- /examples/snippets/chart-js-rs-71705c35f71c3454/js/exports.js: -------------------------------------------------------------------------------- 1 | export function get_chart(id) { 2 | return Chart.getChart(document.getElementById(id)).config._config 3 | } 4 | 5 | export function render_chart(v, id, mutate, plugins, defaults) { 6 | console.debug('Before mutate:', v); 7 | 8 | let obj; 9 | 10 | if (defaults != null || defaults != undefined) { 11 | defaults = eval(defaults); 12 | } 13 | 14 | if (plugins != null || plugins != undefined) { 15 | v.plugins = eval(plugins); 16 | } 17 | 18 | if (mutate) { 19 | obj = window.mutate_chart_object(v) 20 | } 21 | else { 22 | obj = v 23 | }; 24 | 25 | console.debug('After mutate:', obj); 26 | 27 | const ctx = document.getElementById(id); 28 | let chart = new Chart(ctx, obj); 29 | } 30 | 31 | export function update_chart(updated, id, animate) { 32 | try { 33 | let chart = Chart.getChart(document.getElementById(id)); 34 | chart.config._config.type = updated.type; 35 | chart.config._config.data = updated.data; 36 | chart.config._config.options = updated.options; 37 | 38 | console.debug('Updated chart:', chart); 39 | 40 | if (animate) { 41 | chart.update(); 42 | } else { 43 | chart.update('none'); 44 | } 45 | 46 | true 47 | } 48 | catch { 49 | false 50 | } 51 | } -------------------------------------------------------------------------------- /examples/snippets/chart-js-rs-78d36b04da5ebd5d/js/exports.js: -------------------------------------------------------------------------------- 1 | export function get_chart(id) { 2 | return Chart.getChart(document.getElementById(id)).config._config 3 | } 4 | 5 | export function render_chart(v, id, mutate, plugins, defaults) { 6 | console.debug('Before mutate:', v); 7 | 8 | let obj; 9 | 10 | if (defaults != null || defaults != undefined) { 11 | defaults = eval(defaults); 12 | } 13 | 14 | if (plugins != null || plugins != undefined) { 15 | v.plugins = eval(plugins); 16 | } 17 | 18 | if (mutate) { 19 | obj = window.mutate_chart_object(v) 20 | } 21 | else { 22 | obj = v 23 | }; 24 | 25 | console.debug('After mutate:', obj); 26 | 27 | const ctx = document.getElementById(id); 28 | let chart = new Chart(ctx, obj); 29 | } 30 | 31 | export function update_chart(updated, id, animate) { 32 | try { 33 | let chart = Chart.getChart(document.getElementById(id)); 34 | chart.config._config.type = updated.type; 35 | chart.config._config.data = updated.data; 36 | chart.config._config.options = updated.options; 37 | 38 | console.debug('Updated chart:', chart); 39 | 40 | if (animate) { 41 | chart.update(); 42 | } else { 43 | chart.update('none'); 44 | } 45 | 46 | true 47 | } 48 | catch { 49 | false 50 | } 51 | } -------------------------------------------------------------------------------- /examples/snippets/chart-js-rs-f75c9d6e04df1ec1/js/exports.js: -------------------------------------------------------------------------------- 1 | export function get_chart(id) { 2 | return Chart.getChart(document.getElementById(id)).config._config 3 | } 4 | 5 | export function render_chart(v, id, mutate, plugins, defaults) { 6 | console.debug('Before mutate:', v); 7 | 8 | let obj; 9 | 10 | if (defaults != null || defaults != undefined) { 11 | defaults = eval(defaults); 12 | } 13 | 14 | if (plugins != null || plugins != undefined) { 15 | v.plugins = eval(plugins); 16 | } 17 | 18 | if (mutate) { 19 | obj = window.mutate_chart_object(v) 20 | } 21 | else { 22 | obj = v 23 | }; 24 | 25 | console.debug('After mutate:', obj); 26 | 27 | const ctx = document.getElementById(id); 28 | let chart = new Chart(ctx, obj); 29 | } 30 | 31 | export function update_chart(updated, id, animate) { 32 | try { 33 | let chart = Chart.getChart(document.getElementById(id)); 34 | chart.config._config.type = updated.type; 35 | chart.config._config.data = updated.data; 36 | chart.config._config.options = updated.options; 37 | 38 | console.debug('Updated chart:', chart); 39 | 40 | if (animate) { 41 | chart.update(); 42 | } else { 43 | chart.update('none'); 44 | } 45 | 46 | true 47 | } 48 | catch { 49 | false 50 | } 51 | } -------------------------------------------------------------------------------- /src/objects/mod.rs: -------------------------------------------------------------------------------- 1 | mod chart_objects; 2 | mod helper_objects; 3 | mod methods; 4 | 5 | pub use chart_objects::*; 6 | pub use helper_objects::*; 7 | 8 | use js_sys::Reflect; 9 | use serde::Deserialize; 10 | use wasm_bindgen::JsValue; 11 | 12 | fn rationalise_1_level Deserialize<'a>>( 13 | obj: &JsValue, 14 | name: &'static str, 15 | f: impl Fn(T), 16 | ) { 17 | if let Ok(a) = Reflect::get(obj, &name.into()) { 18 | // If the property is undefined, dont try serialize it 19 | if a == JsValue::UNDEFINED { 20 | return; 21 | } 22 | 23 | if let Ok(o) = serde_wasm_bindgen::from_value::(a) { 24 | f(o) 25 | } 26 | } 27 | } 28 | fn rationalise_2_levels Deserialize<'a>>( 29 | obj: &JsValue, 30 | name: (&'static str, &'static str), 31 | f: impl Fn(JsValue, T), 32 | ) { 33 | if let Ok(a) = Reflect::get(obj, &name.0.into()) { 34 | // If the property is undefined, dont try serialize it 35 | if a == JsValue::UNDEFINED { 36 | return; 37 | } 38 | 39 | if let Ok(b) = Reflect::get(&a, &name.1.into()) { 40 | // If the property is undefined, dont try serialize it 41 | if b == JsValue::UNDEFINED { 42 | return; 43 | } 44 | 45 | if let Ok(o) = serde_wasm_bindgen::from_value::(b) { 46 | f(a, o) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/worker_imports.js: -------------------------------------------------------------------------------- 1 | const origin = self.location.origin; 2 | const wasm = await import(`${origin}/chart_js_rs_example.js`); 3 | 4 | self.window.callbacks = wasm; 5 | await wasm.default(); 6 | 7 | self.window.mutate_chart_object = function (v) { 8 | if (v.id == "bar") { 9 | v.options.scales.y1.ticks = { 10 | callback: 11 | function (value, _index, _values) { 12 | return '$' + value.toFixed(2); 13 | } 14 | }; 15 | } 16 | return v 17 | }; 18 | 19 | // Import Chart.js first 20 | await import("https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"); 21 | 22 | // Import Luxon with explicit global setup 23 | await import("https://cdn.jsdelivr.net/npm/luxon@^2/build/global/luxon.min.js"); 24 | 25 | // Ensure luxon is properly set up globally 26 | if (typeof self.luxon === 'undefined') { 27 | // If the global version didn't work, try importing the ESM version 28 | const luxonESM = await import("https://cdn.jsdelivr.net/npm/luxon@^2/+esm"); 29 | self.luxon = luxonESM; 30 | } 31 | 32 | // Verify DateTime and its constants are available 33 | if (self.luxon && self.luxon.DateTime && self.luxon.DateTime.DATETIME_MED_WITH_SECONDS) { 34 | // console.log('DateTime constants available'); 35 | } else { 36 | console.error('DateTime constants not available'); 37 | } 38 | 39 | // Now import the adapter 40 | await import("https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@^1/dist/chartjs-adapter-luxon.umd.min.js"); -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | chart-js-rs Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 30 | 31 | 32 | 49 | 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chart-js-rs" 3 | version = "0.1.5" 4 | edition = "2021" 5 | authors = ["Billy Sheppard", "Luis Moreno"] 6 | license = "Apache-2.0" 7 | keywords = ["javascript", "dom", "wasm", "charts"] 8 | categories = ["gui", "web-programming", "wasm"] 9 | readme = "README.md" 10 | description = "Chart JS API for Rust WebAssembly" 11 | repository = "https://github.com/Billy-Sheppard/chart-js-rs" 12 | homepage = "https://github.com/Billy-Sheppard/chart-js-rs" 13 | 14 | [dependencies] 15 | gloo-console = { version = "0.3" } 16 | gloo-utils = { version = "0.2", features = ["serde"] } 17 | js-sys = "0.3.77" 18 | rust_decimal = "1.37.2" 19 | serde = { version = "1.0.219", features = ["derive"] } 20 | serde_json = "1.0.141" 21 | serde-wasm-bindgen = "0.6" 22 | wasm-bindgen = { version = "0.2.104", features = ["serde-serialize"] } 23 | uuid = { version = "1.17.0", features = ["v4", "js"] } 24 | web-sys = { version = "0.3.77", optional = true, features = [ 25 | "DomRect", 26 | "HtmlCanvasElement", 27 | "OffscreenCanvas", 28 | "WorkerOptions", 29 | "WorkerType", 30 | ] } 31 | tokio = { version = "1.47.1", features = ["sync"], optional = true } 32 | itertools = "0.14.0" 33 | 34 | [workspace] 35 | members = ["examples"] 36 | 37 | [build-dependencies] 38 | heck = "0.5.0" 39 | itertools = "0.14.0" 40 | quote = "1.0.40" 41 | proc-macro2 = "1.0.95" 42 | syn = { features = ["parsing", "full"], version = "2.0.111" } 43 | 44 | [features] 45 | default = [] 46 | workers = [ 47 | "dep:web-sys", 48 | "dep:tokio", 49 | "web-sys/CssStyleDeclaration", 50 | "web-sys/Element", 51 | "web-sys/MouseEvent", 52 | "web-sys/Window", 53 | ] 54 | -------------------------------------------------------------------------------- /src/pie.rs: -------------------------------------------------------------------------------- 1 | use serde::{ 2 | de::{self}, 3 | Deserialize, Serialize, 4 | }; 5 | 6 | use crate::{objects::*, ChartExt}; 7 | 8 | #[derive(Debug, Clone, Deserialize, Serialize, Default)] 9 | pub struct Pie { 10 | #[serde(rename = "type")] 11 | r#type: PieString, 12 | data: Dataset>, 13 | options: ChartOptions, 14 | id: String, 15 | } 16 | 17 | #[cfg(feature = "workers")] 18 | impl crate::WorkerChartExt for Pie {} 19 | impl ChartExt for Pie { 20 | type DS = Dataset>; 21 | 22 | fn get_id(&self) -> &str { 23 | &self.id 24 | } 25 | fn id(mut self, id: String) -> Self { 26 | self.id = id; 27 | self 28 | } 29 | 30 | fn get_data(&mut self) -> &mut Self::DS { 31 | &mut self.data 32 | } 33 | 34 | fn get_options(&mut self) -> &mut ChartOptions { 35 | &mut self.options 36 | } 37 | } 38 | 39 | #[derive(Debug, Default, Clone)] 40 | pub struct PieString; 41 | impl<'de> Deserialize<'de> for PieString { 42 | fn deserialize(deserializer: D) -> Result 43 | where 44 | D: serde::Deserializer<'de>, 45 | { 46 | match String::deserialize(deserializer)?.to_lowercase().as_str() { 47 | "pie" => Ok(PieString), 48 | other => Err(de::Error::custom(format!( 49 | "`{other}` is not a valid PieString." 50 | ))), 51 | } 52 | } 53 | } 54 | impl Serialize for PieString { 55 | fn serialize(&self, serializer: S) -> Result 56 | where 57 | S: serde::Serializer, 58 | { 59 | serializer.serialize_str("pie") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/scatter.rs: -------------------------------------------------------------------------------- 1 | use serde::{ 2 | de::{self}, 3 | Deserialize, Serialize, 4 | }; 5 | 6 | use crate::{objects::*, ChartExt}; 7 | 8 | #[derive(Debug, Clone, Deserialize, Serialize, Default)] 9 | pub struct Scatter { 10 | #[serde(rename = "type")] 11 | r#type: ScatterString, 12 | data: Dataset>, 13 | options: ChartOptions, 14 | id: String, 15 | } 16 | 17 | #[cfg(feature = "workers")] 18 | impl crate::WorkerChartExt for Scatter {} 19 | impl ChartExt for Scatter { 20 | type DS = Dataset>; 21 | 22 | fn get_id(&self) -> &str { 23 | &self.id 24 | } 25 | fn id(mut self, id: String) -> Self { 26 | self.id = id; 27 | self 28 | } 29 | 30 | fn get_data(&mut self) -> &mut Self::DS { 31 | &mut self.data 32 | } 33 | 34 | fn get_options(&mut self) -> &mut ChartOptions { 35 | &mut self.options 36 | } 37 | } 38 | 39 | #[derive(Debug, Default, Clone)] 40 | 41 | pub struct ScatterString; 42 | impl<'de> Deserialize<'de> for ScatterString { 43 | fn deserialize(deserializer: D) -> Result 44 | where 45 | D: serde::Deserializer<'de>, 46 | { 47 | match String::deserialize(deserializer)?.to_lowercase().as_str() { 48 | "scatter" => Ok(ScatterString), 49 | other => Err(de::Error::custom(format!( 50 | "`{other}` is not a valid ScatterString." 51 | ))), 52 | } 53 | } 54 | } 55 | impl Serialize for ScatterString { 56 | fn serialize(&self, serializer: S) -> Result 57 | where 58 | S: serde::Serializer, 59 | { 60 | serializer.serialize_str("scatter") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/doughnut.rs: -------------------------------------------------------------------------------- 1 | use serde::{ 2 | de::{self}, 3 | Deserialize, Serialize, 4 | }; 5 | 6 | use crate::{objects::*, ChartExt}; 7 | 8 | #[derive(Debug, Clone, Deserialize, Serialize, Default)] 9 | pub struct Doughnut { 10 | #[serde(rename = "type")] 11 | r#type: DoughnutString, 12 | data: Dataset>, 13 | options: ChartOptions, 14 | id: String, 15 | } 16 | #[cfg(feature = "workers")] 17 | impl crate::WorkerChartExt for Doughnut {} 18 | impl ChartExt for Doughnut { 19 | type DS = Dataset>; 20 | 21 | fn get_id(&self) -> &str { 22 | &self.id 23 | } 24 | fn id(mut self, id: String) -> Self { 25 | self.id = id; 26 | self 27 | } 28 | 29 | fn get_data(&mut self) -> &mut Self::DS { 30 | &mut self.data 31 | } 32 | 33 | fn get_options(&mut self) -> &mut ChartOptions { 34 | &mut self.options 35 | } 36 | } 37 | 38 | #[derive(Debug, Default, Clone)] 39 | pub struct DoughnutString; 40 | impl<'de> Deserialize<'de> for DoughnutString { 41 | fn deserialize(deserializer: D) -> Result 42 | where 43 | D: serde::Deserializer<'de>, 44 | { 45 | match String::deserialize(deserializer)?.to_lowercase().as_str() { 46 | "doughnut" => Ok(DoughnutString), 47 | other => Err(de::Error::custom(format!( 48 | "`{other}` is not a valid DoughnutString." 49 | ))), 50 | } 51 | } 52 | } 53 | impl Serialize for DoughnutString { 54 | fn serialize(&self, serializer: S) -> Result 55 | where 56 | S: serde::Serializer, 57 | { 58 | serializer.serialize_str("doughnut") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /js/exports.js: -------------------------------------------------------------------------------- 1 | const chartAreaBackground = { 2 | id: 'canvas_background_color', 3 | beforeDraw: (chart, args, options) => { 4 | const { ctx, width, height } = chart; 5 | ctx.save(); 6 | ctx.fillStyle = options.color || 'white'; // default to white 7 | ctx.fillRect(0, 0, width, height); // fill entire canvas 8 | ctx.restore(); 9 | } 10 | }; 11 | export function get_chart(id) { 12 | return Chart.getChart(document.getElementById(id)).config._config 13 | } 14 | 15 | export function render_chart(v, id, mutate, plugins, defaults) { 16 | console.debug('Before mutate:', v); 17 | 18 | let obj; 19 | 20 | if (defaults != null || defaults != undefined) { 21 | defaults = eval(defaults); 22 | } 23 | 24 | if (plugins != null || plugins != undefined) { 25 | v.plugins = eval(plugins); 26 | } 27 | 28 | if (mutate) { 29 | obj = window.mutate_chart_object(v) 30 | } 31 | else { 32 | obj = v 33 | }; 34 | 35 | console.debug('After mutate:', obj); 36 | 37 | const ctx = document.getElementById(id); 38 | let chart = new Chart(ctx, obj); 39 | } 40 | 41 | export function update_chart(updated, id, animate) { 42 | try { 43 | let chart = Chart.getChart(document.getElementById(id)); 44 | chart.config._config.type = updated.type; 45 | chart.config._config.data = updated.data; 46 | chart.config._config.options = updated.options; 47 | 48 | console.debug('Updated chart:', chart); 49 | 50 | if (animate) { 51 | chart.update(); 52 | } else { 53 | chart.update('none'); 54 | } 55 | 56 | true 57 | } 58 | catch { 59 | false 60 | } 61 | } -------------------------------------------------------------------------------- /src/bar.rs: -------------------------------------------------------------------------------- 1 | use serde::{ 2 | de::{self}, 3 | Deserialize, Serialize, 4 | }; 5 | 6 | use crate::{objects::*, ChartExt, DatasetTrait}; 7 | 8 | #[derive(Debug, Clone, Deserialize, Serialize, Default)] 9 | #[serde(bound = "D: DatasetTrait")] 10 | pub struct Bar { 11 | #[serde(rename = "type")] 12 | r#type: BarString, 13 | data: Dataset, 14 | options: ChartOptions, 15 | id: String, 16 | } 17 | 18 | #[cfg(feature = "workers")] 19 | impl crate::WorkerChartExt for Bar {} 20 | impl ChartExt for Bar { 21 | type DS = Dataset; 22 | 23 | fn get_id(&self) -> &str { 24 | &self.id 25 | } 26 | fn id(mut self, id: String) -> Self { 27 | self.id = id; 28 | self 29 | } 30 | 31 | fn get_data(&mut self) -> &mut Self::DS { 32 | &mut self.data 33 | } 34 | 35 | fn get_options(&mut self) -> &mut ChartOptions { 36 | &mut self.options 37 | } 38 | } 39 | 40 | #[derive(Debug, Default, Clone)] 41 | pub struct BarString; 42 | impl<'de> Deserialize<'de> for BarString { 43 | fn deserialize(deserializer: D) -> Result 44 | where 45 | D: serde::Deserializer<'de>, 46 | { 47 | match String::deserialize(deserializer)?.to_lowercase().as_str() { 48 | "bar" => Ok(BarString), 49 | other => Err(de::Error::custom(format!( 50 | "`{other}` is not a valid BarString." 51 | ))), 52 | } 53 | } 54 | } 55 | impl Serialize for BarString { 56 | fn serialize(&self, serializer: S) -> Result 57 | where 58 | S: serde::Serializer, 59 | { 60 | serializer.serialize_str("bar") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/snippets/chart-js-rs-65afd13e28fe916c/js/exports.js: -------------------------------------------------------------------------------- 1 | const chartAreaBackground = { 2 | id: 'canvas_background_color', 3 | beforeDraw: (chart, args, options) => { 4 | const { ctx, width, height } = chart; 5 | ctx.save(); 6 | ctx.fillStyle = options.color || 'white'; // default to white 7 | ctx.fillRect(0, 0, width, height); // fill entire canvas 8 | ctx.restore(); 9 | } 10 | }; 11 | export function get_chart(id) { 12 | return Chart.getChart(document.getElementById(id)).config._config 13 | } 14 | 15 | export function render_chart(v, id, mutate, plugins, defaults) { 16 | console.debug('Before mutate:', v); 17 | 18 | let obj; 19 | 20 | if (defaults != null || defaults != undefined) { 21 | defaults = eval(defaults); 22 | } 23 | 24 | if (plugins != null || plugins != undefined) { 25 | v.plugins = eval(plugins); 26 | } 27 | 28 | if (mutate) { 29 | obj = window.mutate_chart_object(v) 30 | } 31 | else { 32 | obj = v 33 | }; 34 | 35 | console.debug('After mutate:', obj); 36 | 37 | const ctx = document.getElementById(id); 38 | let chart = new Chart(ctx, obj); 39 | } 40 | 41 | export function update_chart(updated, id, animate) { 42 | try { 43 | let chart = Chart.getChart(document.getElementById(id)); 44 | chart.config._config.type = updated.type; 45 | chart.config._config.data = updated.data; 46 | chart.config._config.options = updated.options; 47 | 48 | console.debug('Updated chart:', chart); 49 | 50 | if (animate) { 51 | chart.update(); 52 | } else { 53 | chart.update('none'); 54 | } 55 | 56 | true 57 | } 58 | catch { 59 | false 60 | } 61 | } -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::objects::*; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::Value; 4 | use std::{collections::*, fmt::Display}; 5 | 6 | pub(crate) trait ChartJsRsObject { 7 | fn is_empty(&self) -> bool; 8 | } 9 | impl ChartJsRsObject for T { 10 | fn is_empty(&self) -> bool { 11 | self.to_string().is_empty() 12 | } 13 | } 14 | 15 | pub trait DatasetTrait: for<'a> Deserialize<'a> + Serialize + Default + Clone { 16 | fn labels(self) -> Vec; 17 | } 18 | pub trait DatasetDataExt { 19 | fn presorted_to_dataset_data(self) -> DatasetData; 20 | fn unsorted_to_dataset_data(self) -> DatasetData; 21 | } 22 | 23 | impl DatasetDataExt for I 24 | where 25 | I: Iterator)>, 26 | { 27 | fn presorted_to_dataset_data(self) -> DatasetData { 28 | DatasetData(serde_json::to_value(self.map(XYPoint::from).collect::>()).unwrap()) 29 | } 30 | fn unsorted_to_dataset_data(self) -> DatasetData { 31 | DatasetData(serde_json::to_value(self.map(XYPoint::from).collect::>()).unwrap()) 32 | } 33 | } 34 | 35 | pub trait DatasetIterExt: Iterator { 36 | fn into_data_iter( 37 | self, 38 | ) -> impl Iterator)> 39 | where 40 | Self: Iterator + Sized, 41 | X: Into, 42 | Y: Into, 43 | { 44 | self.map(|(x, y)| (x.into(), y.into(), None)) 45 | } 46 | fn into_data_iter_with_description( 47 | self, 48 | ) -> impl Iterator)> 49 | where 50 | Self: Iterator + Sized, 51 | X: Into, 52 | Y: Into, 53 | D: Serialize, 54 | { 55 | self.map(|(x, y, d)| (x.into(), y.into(), Some(serde_json::to_value(d).unwrap()))) 56 | } 57 | } 58 | impl DatasetIterExt for T where T: Iterator + ?Sized {} 59 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | #![doc = include_str!("../README.md")] 3 | 4 | pub mod bar; 5 | pub mod coordinate; 6 | pub mod doughnut; 7 | pub mod exports; 8 | pub mod functions; 9 | pub mod objects; 10 | pub mod pie; 11 | pub mod scatter; 12 | pub mod traits; 13 | 14 | #[cfg(feature = "workers")] 15 | pub mod worker; 16 | 17 | pub use objects::*; 18 | pub use traits::*; 19 | pub use utils::*; 20 | 21 | #[cfg(feature = "workers")] 22 | pub use worker_chart::*; 23 | 24 | #[doc(hidden)] 25 | mod utils; 26 | 27 | use exports::get_chart; 28 | use gloo_utils::format::JsValueSerdeExt; 29 | use serde::{de::DeserializeOwned, Serialize}; 30 | 31 | #[cfg(feature = "workers")] 32 | use wasm_bindgen::{self, prelude::*}; 33 | 34 | #[cfg(feature = "workers")] 35 | use web_sys::WorkerGlobalScope; 36 | 37 | pub trait ChartExt: DeserializeOwned + Serialize + Default { 38 | type DS; 39 | 40 | fn new(id: impl AsRef) -> Self { 41 | Self::default().id(id.as_ref().into()) 42 | } 43 | 44 | fn get_id(&self) -> &str; 45 | fn id(self, id: String) -> Self; 46 | 47 | fn get_data(&mut self) -> &mut Self::DS; 48 | fn data(mut self, data: impl Into) -> Self { 49 | *self.get_data() = data.into(); 50 | self 51 | } 52 | 53 | fn get_options(&mut self) -> &mut ChartOptions; 54 | fn options(mut self, options: impl Into) -> Self { 55 | *self.get_options() = options.into(); 56 | self 57 | } 58 | 59 | fn into_chart(self) -> Chart { 60 | Chart { 61 | obj: <::wasm_bindgen::JsValue as JsValueSerdeExt>::from_serde(&self) 62 | .expect("Unable to serialize chart."), 63 | id: self.get_id().into(), 64 | mutate: false, 65 | plugins: String::new(), 66 | defaults: String::new(), 67 | } 68 | } 69 | 70 | fn get_chart_from_id(id: &str) -> Option { 71 | let chart = get_chart(id); 72 | 73 | serde_wasm_bindgen::from_value(chart) 74 | .inspect_err(|e| { 75 | gloo_console::error!(e.to_string()); 76 | }) 77 | .ok() 78 | } 79 | } 80 | 81 | #[cfg(feature = "workers")] 82 | pub fn is_worker() -> bool { 83 | js_sys::global().dyn_into::().is_ok() 84 | } 85 | 86 | #[cfg(feature = "workers")] 87 | mod worker_chart { 88 | use crate::*; 89 | 90 | pub trait WorkerChartExt: ChartExt { 91 | #[allow(async_fn_in_trait)] 92 | async fn into_worker_chart( 93 | self, 94 | imports_block: &str, 95 | ) -> Result> { 96 | Ok(WorkerChart { 97 | obj: <::wasm_bindgen::JsValue as JsValueSerdeExt>::from_serde(&self) 98 | .expect("Unable to serialize chart."), 99 | id: self.get_id().into(), 100 | mutate: false, 101 | plugins: String::new(), 102 | defaults: String::new(), 103 | worker: crate::worker::ChartWorker::new(imports_block).await?, 104 | }) 105 | } 106 | } 107 | #[wasm_bindgen] 108 | #[derive(Clone)] 109 | #[must_use = "\nAppend .render_async()\n"] 110 | pub struct WorkerChart { 111 | pub(crate) obj: JsValue, 112 | pub(crate) id: String, 113 | pub(crate) mutate: bool, 114 | pub(crate) plugins: String, 115 | pub(crate) defaults: String, 116 | pub(crate) worker: crate::worker::ChartWorker, 117 | } 118 | impl WorkerChart { 119 | pub async fn render_async(self) -> Result<(), Box> { 120 | self.worker 121 | .render(self.obj, &self.id, self.mutate, self.plugins, self.defaults) 122 | .await 123 | } 124 | 125 | pub async fn update_async(self, animate: bool) -> Result> { 126 | self.worker.update(self.obj, &self.id, animate).await 127 | } 128 | 129 | #[must_use = "\nAppend .render_async()\n"] 130 | pub fn mutate(&mut self) -> Self { 131 | self.mutate = true; 132 | self.clone() 133 | } 134 | 135 | #[must_use = "\nAppend .render_async()\n"] 136 | pub fn plugins(&mut self, plugins: impl Into) -> Self { 137 | self.plugins = plugins.into(); 138 | self.clone() 139 | } 140 | 141 | #[must_use = "\nAppend .render_async()\n"] 142 | pub fn defaults(&mut self, defaults: impl Into) -> Self { 143 | self.defaults = format!("{}\n{}", self.defaults, defaults.into()); 144 | self.to_owned() 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Material Bread logo 3 |

4 | 5 | # Chart.js types API in Rust 6 | [![crates.io](https://img.shields.io/crates/v/chart-js-rs.svg)](https://crates.io/crates/chart-js-rs) 7 | [![docs.rs](https://docs.rs/chart-js-rs/badge.svg)](https://docs.rs/chart-js-rs) 8 | 9 | ***In Alpha, types added as needed, feel free to PR.*** 10 | 11 | ## How to use 12 | 13 | Check out the example folder for some code examples. The example uses WebAssembly and the [dominator](https://github.com/Pauan/rust-dominator) crate to produce charts. This library should be compatible with any WASM/HTML library. 14 | 15 | The compiled webpage can be found here: https://billy-sheppard.github.io/chart-js-rs/examples/index.html 16 | 17 | ### Cargo.toml: 18 | ```toml 19 | [dependencies.chart-js-rs] 20 | git = "https://github.com/Billy-Sheppard/chart-js-rs" 21 | ``` 22 | 23 | ### Rust: 24 | ```rust 25 | use chart_js_rs::ChartExt; 26 | 27 | let id = "[YOUR CHART ID HERE]"; 28 | let chart = chart_js_rs::scatter::Scatter { 29 | id: id.to_string(), 30 | options: ChartOptions { .. }, 31 | data: Dataset { .. }, 32 | ..Default::default() 33 | }; 34 | // to use any JS callbacks or functions you use render_mutate and refer to the JS below 35 | chart.into_chart().mutate().render(); 36 | 37 | // to use any chart-js plugins, a few examples 38 | chart.into_chart().plugins("[window['chartjs-plugin-autocolors']]").render(); // for autocolors and no mutating 39 | chart.into_chart().mutate().plugins("[window['chartjs-plugin-autocolors']]").render(); // for autocolors and mutating 40 | chart.into_chart().mutate().plugins("[ChartDataLabels, window['chartjs-plugin-autocolors']]").render(); // for datalabels, autocolors, and mutating 41 | 42 | // else use render 43 | chart.into_chart().render(); 44 | ``` 45 | 46 | ### Your html file: 47 | ```html 48 | 49 | 50 | ... 51 | 52 | 61 | 62 | ... 63 | 64 | 81 | ``` 82 | 83 |
84 | 85 | # Explainers 86 | 87 | ## Whats the difference between `my_chart.render()` and `mychart.mutate().render()`? 88 | `.render()` is for simple charts, that don't require any further changes done using javascript code. 89 | 90 | `.mutate().render()` allows for chart objects to be accessible in your javascript file, so you can mutate the object however required, especially useful for ChartJS functions not yet available in this library. 91 | 92 | `.plugins("[MyPlugin]").mutate().render()` allows for ChartJS plugins to be used with your charts, the parameter is the direct string representation of the Javascript array containing your plugins. [Docs](https://www.chartjs.org/docs/latest/developers/plugins.html) 93 | 94 | `.plugins("[MyPlugin]").defaults("Chart.defaults.font.size = 20;").mutate().render()` allows for ChartJS defaults to be set. 95 | 96 | ## How to use `struct FnWithArgs`? 97 | `FnWithArgs` is a helper struct to allow serialization of javascript functions by encoding their body and arguments as a string. Then, as needed, the function can be rebuilt in JavaScipt, and called. 98 | 99 | It is important then, that you know which variables are being parsed to the function. For this information, you can refer to the [Chart.js documentation](https://www.chartjs.org/docs/latest/). 100 | 101 | `FnWithArgs` is used, for example, in implementing conditional line segment colouring, according to the [docs](https://www.chartjs.org/docs/latest/samples/line/segments.html). 102 | It can also be used to leaverage logic written in Rust, to calculate outputs for ChartJS. 103 | ```rust 104 | #[wasm_bindgen] // your function must be a wasm_bindgen export 105 | pub fn add(a: u32, b: u32) -> u32 { 106 | a + b 107 | } 108 | 109 | // ... 110 | 111 | Scatter:: { 112 | data: { 113 | datasets: vec![ 114 | Dataset { 115 | // ... 116 | segment: Segment { 117 | borderColor: 118 | // todo!() 119 | // write some examples here! 120 | } 121 | } 122 | ] 123 | } 124 | } 125 | ``` 126 | -------------------------------------------------------------------------------- /examples/src/utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use futures_signals::signal::{Mutable, MutableSignalCloned, Signal}; 4 | use std::{ 5 | pin::Pin, 6 | task::{Context, Poll}, 7 | }; 8 | 9 | pub struct Mutable3( 10 | (MutableSignalCloned, Mutable), 11 | (MutableSignalCloned, Mutable), 12 | (MutableSignalCloned, Mutable), 13 | ) 14 | where 15 | A: Clone, 16 | B: Clone, 17 | C: Clone; 18 | impl Mutable3 19 | where 20 | A: Clone, 21 | B: Clone, 22 | C: Clone, 23 | { 24 | pub fn new(a: Mutable, b: Mutable, c: Mutable) -> Self { 25 | Mutable3( 26 | (a.signal_cloned(), a), 27 | (b.signal_cloned(), b), 28 | (c.signal_cloned(), c), 29 | ) 30 | } 31 | } 32 | impl Signal for Mutable3 33 | where 34 | A: Clone, 35 | B: Clone, 36 | C: Clone, 37 | { 38 | type Item = (A, B, C); 39 | fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 40 | let a = Pin::new(&mut self.0 .0).poll_change(cx); 41 | let b = Pin::new(&mut self.1 .0).poll_change(cx); 42 | let c = Pin::new(&mut self.2 .0).poll_change(cx); 43 | let mut changed = false; 44 | let left_done = match a { 45 | Poll::Ready(None) => true, 46 | Poll::Ready(_) => { 47 | changed = true; 48 | false 49 | } 50 | Poll::Pending => false, 51 | }; 52 | let middle_done = match b { 53 | Poll::Ready(None) => true, 54 | Poll::Ready(_) => { 55 | changed = true; 56 | false 57 | } 58 | Poll::Pending => false, 59 | }; 60 | let right_done = match c { 61 | Poll::Ready(None) => true, 62 | Poll::Ready(_) => { 63 | changed = true; 64 | false 65 | } 66 | Poll::Pending => false, 67 | }; 68 | if changed { 69 | Poll::Ready(Some(( 70 | self.0 .1.get_cloned(), 71 | self.1 .1.get_cloned(), 72 | self.2 .1.get_cloned(), 73 | ))) 74 | } else if left_done && middle_done && right_done { 75 | Poll::Ready(None) 76 | } else { 77 | Poll::Pending 78 | } 79 | } 80 | } 81 | pub struct Mutable4( 82 | (MutableSignalCloned, Mutable), 83 | (MutableSignalCloned, Mutable), 84 | (MutableSignalCloned, Mutable), 85 | (MutableSignalCloned, Mutable), 86 | ) 87 | where 88 | A: Clone, 89 | B: Clone, 90 | C: Clone, 91 | D: Clone; 92 | impl Mutable4 93 | where 94 | A: Clone, 95 | B: Clone, 96 | C: Clone, 97 | D: Clone, 98 | { 99 | pub fn new(a: Mutable, b: Mutable, c: Mutable, d: Mutable) -> Self { 100 | Mutable4( 101 | (a.signal_cloned(), a), 102 | (b.signal_cloned(), b), 103 | (c.signal_cloned(), c), 104 | (d.signal_cloned(), d), 105 | ) 106 | } 107 | } 108 | impl Signal for Mutable4 109 | where 110 | A: Clone, 111 | B: Clone, 112 | C: Clone, 113 | D: Clone, 114 | { 115 | type Item = (A, B, C, D); 116 | fn poll_change(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 117 | let a = Pin::new(&mut self.0 .0).poll_change(cx); 118 | let b = Pin::new(&mut self.1 .0).poll_change(cx); 119 | let c = Pin::new(&mut self.2 .0).poll_change(cx); 120 | let d = Pin::new(&mut self.3 .0).poll_change(cx); 121 | let mut changed = false; 122 | let left_done = match a { 123 | Poll::Ready(None) => true, 124 | Poll::Ready(_) => { 125 | changed = true; 126 | false 127 | } 128 | Poll::Pending => false, 129 | }; 130 | let left_middle_done = match b { 131 | Poll::Ready(None) => true, 132 | Poll::Ready(_) => { 133 | changed = true; 134 | false 135 | } 136 | Poll::Pending => false, 137 | }; 138 | let right_middle_done = match c { 139 | Poll::Ready(None) => true, 140 | Poll::Ready(_) => { 141 | changed = true; 142 | false 143 | } 144 | Poll::Pending => false, 145 | }; 146 | let right_done = match d { 147 | Poll::Ready(None) => true, 148 | Poll::Ready(_) => { 149 | changed = true; 150 | false 151 | } 152 | Poll::Pending => false, 153 | }; 154 | if changed { 155 | Poll::Ready(Some(( 156 | self.0 .1.get_cloned(), 157 | self.1 .1.get_cloned(), 158 | self.2 .1.get_cloned(), 159 | self.3 .1.get_cloned(), 160 | ))) 161 | } else if left_done && left_middle_done && right_middle_done && right_done { 162 | Poll::Ready(None) 163 | } else { 164 | Poll::Pending 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use js_sys::{Array, Object, Reflect}; 2 | use std::cell::RefCell; 3 | use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; 4 | 5 | use crate::{exports::*, BoolString, FnWithArgs, FnWithArgsOrT, NumberString}; 6 | 7 | pub fn get_order_fn( 8 | lhs: &crate::NumberOrDateString, 9 | rhs: &crate::NumberOrDateString, 10 | ) -> std::cmp::Ordering { 11 | crate::utils::ORDER_FN.with_borrow(|f| f(lhs, rhs)) 12 | } 13 | pub fn set_order_fn< 14 | F: Fn(&crate::NumberOrDateString, &crate::NumberOrDateString) -> std::cmp::Ordering + 'static, 15 | >( 16 | f: F, 17 | ) { 18 | let _ = ORDER_FN.replace(Box::new(f)); 19 | } 20 | 21 | thread_local! { 22 | #[allow(clippy::type_complexity)] 23 | pub static ORDER_FN: RefCell< 24 | Box std::cmp::Ordering>, 25 | > = RefCell::new({ 26 | Box::new( 27 | |lhs: &crate::NumberOrDateString, rhs: &crate::NumberOrDateString| -> std::cmp::Ordering { 28 | lhs.cmp(rhs) 29 | }, 30 | )as Box<_> 31 | }); 32 | } 33 | 34 | pub fn uncircle_chartjs_value_to_serde_json_value( 35 | js: impl AsRef, 36 | ) -> Result { 37 | // this makes sure we don't get any circular objects, `JsValue` allows this, `serde_json::Value` does not! 38 | let blacklist_function = 39 | js_sys::Function::new_with_args("key, val", "if (!key.startsWith('$')) { return val; }"); 40 | let js_string = 41 | js_sys::JSON::stringify_with_replacer(js.as_ref(), &JsValue::from(blacklist_function)) 42 | .map_err(|e| e.as_string().unwrap_or_default())? 43 | .as_string() 44 | .unwrap(); 45 | 46 | serde_json::from_str(&js_string).map_err(|e| e.to_string()) 47 | } 48 | 49 | #[wasm_bindgen] 50 | #[derive(Clone)] 51 | #[must_use = "\nAppend .render()\n"] 52 | pub struct Chart { 53 | pub(crate) obj: JsValue, 54 | pub(crate) id: String, 55 | pub(crate) mutate: bool, 56 | pub(crate) plugins: String, 57 | pub(crate) defaults: String, 58 | } 59 | 60 | /// Walks the JsValue object to get the value of a nested property 61 | /// using the JS dot notation 62 | fn get_path(j: &JsValue, item: &str) -> Option { 63 | let mut path = item.split('.'); 64 | let item = &path.next().unwrap().to_string().into(); 65 | let k = Reflect::get(j, item); 66 | 67 | if k.is_err() { 68 | return None; 69 | } 70 | 71 | let k = k.unwrap(); 72 | if path.clone().count() > 0 { 73 | return get_path(&k, path.collect::>().join(".").as_str()); 74 | } 75 | 76 | Some(k) 77 | } 78 | 79 | /// Get values of an object as an array at the given path. 80 | /// See get_path() 81 | fn object_values_at(j: &JsValue, item: &str) -> Option { 82 | let o = get_path(j, item); 83 | o.and_then(|o| { 84 | if o == JsValue::UNDEFINED { 85 | None 86 | } else { 87 | Some(o) 88 | } 89 | }) 90 | } 91 | 92 | impl Chart { 93 | // pub fn new(chart: JsValue, id: String) -> Option { 94 | // chart.is_object().then_some(Self{ 95 | // obj: chart, 96 | // id, 97 | // mutate: false, 98 | // plugins: String::new(), 99 | // }) 100 | // } 101 | 102 | #[must_use = "\nAppend .render()\n"] 103 | pub fn mutate(&mut self) -> Self { 104 | self.mutate = true; 105 | self.clone() 106 | } 107 | 108 | #[must_use = "\nAppend .render()\n"] 109 | pub fn plugins(&mut self, plugins: impl Into) -> Self { 110 | self.plugins = plugins.into(); 111 | self.clone() 112 | } 113 | 114 | #[must_use = "\nAppend .render()\n"] 115 | pub fn defaults(&mut self, defaults: impl Into) -> Self { 116 | self.defaults = format!("{}\n{}", self.defaults, defaults.into()); 117 | self.to_owned() 118 | } 119 | 120 | /// This should not be used on a chart with a worker attached. 121 | /// If it is, it will do nothing. 122 | pub fn render(self) { 123 | self.rationalise_js(); 124 | 125 | render_chart(self.obj, &self.id, self.mutate, self.plugins, self.defaults); 126 | } 127 | 128 | /// This should not be used on a chart with a worker attached. 129 | /// If it is, it will always return `false` 130 | pub fn update(self, animate: bool) -> bool { 131 | update_chart(self.obj, &self.id, animate) 132 | } 133 | 134 | /// Converts serialized FnWithArgs to JS Function's 135 | /// For new chart options, this will need to be updated 136 | pub fn rationalise_js(&self) { 137 | // Handle data.datasets 138 | Array::from(&get_path(&self.obj, "data.datasets").unwrap()) 139 | .iter() 140 | .for_each(|dataset| { 141 | FnWithArgsOrT::<2, String>::rationalise_1_level(&dataset, "backgroundColor"); 142 | FnWithArgs::<1>::rationalise_2_levels(&dataset, ("segment", "borderDash")); 143 | FnWithArgs::<1>::rationalise_2_levels(&dataset, ("segment", "borderColor")); 144 | FnWithArgsOrT::<1, String>::rationalise_2_levels(&dataset, ("datalabels", "align")); 145 | FnWithArgsOrT::<1, String>::rationalise_2_levels( 146 | &dataset, 147 | ("datalabels", "anchor"), 148 | ); 149 | FnWithArgsOrT::<1, String>::rationalise_2_levels( 150 | &dataset, 151 | ("datalabels", "backgroundColor"), 152 | ); 153 | FnWithArgs::<2>::rationalise_2_levels(&dataset, ("datalabels", "formatter")); 154 | FnWithArgsOrT::<1, NumberString>::rationalise_2_levels( 155 | &dataset, 156 | ("datalabels", "offset"), 157 | ); 158 | FnWithArgsOrT::<1, BoolString>::rationalise_2_levels( 159 | &dataset, 160 | ("datalabels", "display"), 161 | ); 162 | }); 163 | 164 | // Handle options.scales 165 | if let Some(scales) = object_values_at(&self.obj, "options.scales") { 166 | Object::values(&scales.dyn_into().unwrap()) 167 | .iter() 168 | .for_each(|scale| { 169 | FnWithArgs::<3>::rationalise_2_levels(&scale, ("ticks", "callback")); 170 | }); 171 | } 172 | 173 | // Handle options.plugins.legend 174 | if let Some(legend) = object_values_at(&self.obj, "options.plugins.legend") { 175 | FnWithArgs::<2>::rationalise_2_levels(&legend, ("labels", "filter")); 176 | FnWithArgs::<3>::rationalise_2_levels(&legend, ("labels", "sort")); 177 | } 178 | // Handle options.plugins.tooltip 179 | if let Some(legend) = object_values_at(&self.obj, "options.plugins.tooltip") { 180 | FnWithArgs::<1>::rationalise_1_level(&legend, "filter"); 181 | FnWithArgs::<1>::rationalise_2_levels(&legend, ("callbacks", "label")); 182 | FnWithArgs::<1>::rationalise_2_levels(&legend, ("callbacks", "title")); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /js/worker_shim.js: -------------------------------------------------------------------------------- 1 | const chartAreaBackground = { 2 | id: 'canvas_background_color', 3 | beforeDraw: (chart, args, options) => { 4 | const { ctx, width, height } = chart; 5 | ctx.save(); 6 | ctx.fillStyle = options.color || 'white'; // default to white 7 | ctx.fillRect(0, 0, width, height); // fill entire canvas 8 | ctx.restore(); 9 | } 10 | }; 11 | 12 | console.log('Chart worker ready'); 13 | 14 | self.window = { 15 | callbacks: {} //callbacks 16 | }; 17 | 18 | (async () => { 19 | /// IMPORTS 20 | })().then(() => { 21 | const is_obj = x => typeof x === 'object' && !Array.isArray(x) && x !== null; 22 | const derationalize = (o) => { 23 | 24 | // Handle arrays separately 25 | if (Array.isArray(o)) { 26 | return o.map((item, index) => { 27 | return derationalize(item); 28 | }); 29 | } 30 | 31 | if (!is_obj(o)) { 32 | return o; 33 | } else if ('args' in o && 'body' in o && 'closure_id' in o && 'return_value' in o) { 34 | let { args, body, closure_id, return_value } = o; 35 | 36 | if (closure_id) { 37 | return Function(...args, `{ 38 | return 'orange'; 39 | }`); 40 | } else { 41 | return Function(...args, `{ ${body} \n return ${return_value} }`); 42 | } 43 | } else { 44 | return Object.fromEntries(Object.entries(o).map(v => { 45 | return [v[0], derationalize(v[1])] 46 | })) 47 | } 48 | } 49 | 50 | // Store chart instances for mouse event handling 51 | let chartInstances = new Map(); 52 | 53 | // Store computed styles for each chart 54 | let chartComputedStyles = new Map(); 55 | 56 | // Handle mouse events from main thread 57 | function handleChartMouseEvent(eventType, x, y, chartId, computedStyles) { 58 | const chart = chartInstances.get(chartId); 59 | if (!chart) { 60 | console.warn(`Chart with id ${chartId} not found for mouse event`); 61 | return; 62 | } 63 | 64 | // Store the real computed styles from main thread 65 | if (computedStyles) { 66 | chartComputedStyles.set(chartId, computedStyles); 67 | } 68 | 69 | // Mock DOM properties that Chart.js expects in worker context 70 | if (!chart.canvas.ownerDocument) { 71 | chart.canvas.ownerDocument = { 72 | defaultView: { 73 | getComputedStyle: function (element, pseudoElement) { 74 | const styles = chartComputedStyles.get(chartId); 75 | 76 | if (styles) { 77 | styles.getPropertyValue = function (prop) { 78 | // Convert kebab-case to camelCase 79 | const camelProp = prop.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase()); 80 | return styles[camelProp] || styles[prop]; 81 | }; 82 | } 83 | 84 | return styles; 85 | } 86 | } 87 | }; 88 | } 89 | 90 | // Create synthetic event that Chart.js expects 91 | const syntheticEvent = { 92 | type: eventType, 93 | x: x, 94 | y: y, 95 | offsetX: x, 96 | offsetY: y, 97 | target: chart.canvas, 98 | currentTarget: chart.canvas, 99 | preventDefault: function () { }, 100 | stopPropagation: function () { } 101 | }; 102 | 103 | if (eventType === 'mousemove') { 104 | // Use Chart.js's internal event handling to match main thread behavior 105 | const elements = chart.getElementsAtEventForMode( 106 | syntheticEvent, 107 | chart.options.interaction?.mode || 'nearest', 108 | chart.options.interaction || { intersect: false }, 109 | false 110 | ); 111 | 112 | // Let Chart.js handle the tooltip logic exactly as it would normally 113 | chart.tooltip.setActiveElements(elements, syntheticEvent); 114 | chart.draw(); 115 | 116 | } else if (eventType === 'mouseleave') { 117 | // Clear tooltip when mouse leaves - use same position format as mousemove 118 | chart.tooltip.setActiveElements([], syntheticEvent); 119 | chart.draw(); 120 | 121 | } else if (eventType === 'click') { 122 | // Handle click events - this includes legend clicks and data point clicks 123 | // console.log('Click event received in worker:', x, y, chartId); 124 | // console.log('Chart instance found:', !!chart); 125 | 126 | // For legend clicks, we need to check if the click is within the legend area 127 | const legend = chart.legend; 128 | if (legend && legend.legendHitBoxes) { 129 | // Check if click is within any legend item 130 | for (let i = 0; i < legend.legendHitBoxes.length; i++) { 131 | const hitBox = legend.legendHitBoxes[i]; 132 | if (x >= hitBox.left && x <= hitBox.left + hitBox.width && 133 | y >= hitBox.top && y <= hitBox.top + hitBox.height) { 134 | // console.log('Legend item clicked:', i); 135 | // Manually toggle the dataset 136 | const meta = chart.getDatasetMeta(hitBox.datasetIndex !== undefined ? hitBox.datasetIndex : i); 137 | if (meta) { 138 | meta.hidden = !meta.hidden; 139 | chart.update(); 140 | return; 141 | } 142 | } 143 | } 144 | } 145 | 146 | // For data point clicks 147 | const elements = chart.getElementsAtEventForMode( 148 | syntheticEvent, 149 | chart.options.interaction?.mode || 'nearest', 150 | chart.options.interaction || { intersect: false }, 151 | false 152 | ); 153 | // console.log('Elements found at click:', elements); 154 | 155 | // Try the internal Chart.js event handler 156 | chart._handleEvent(syntheticEvent); 157 | 158 | // Try triggering click handlers manually 159 | if (chart.options.onClick) { 160 | chart.options.onClick(syntheticEvent, elements, chart); 161 | } 162 | } 163 | } 164 | 165 | // console.log("sending ready"); 166 | self.postMessage(""); 167 | 168 | self.onmessage = (async event => { 169 | try { 170 | // Handle mouse events from main thread 171 | if (event.data && event.data.type === 'mouse-event') { 172 | const { eventType, x, y, chartId, computedStyles } = event.data; 173 | handleChartMouseEvent(eventType, x, y, chartId, computedStyles); 174 | return; 175 | } 176 | 177 | let [transaction, data] = event.data ?? []; 178 | // console.log(transaction, data); 179 | 180 | let { 181 | canvas, width, height, 182 | obj, mutate, plugins, defaults, id, // render() 183 | updated, animate // update() 184 | } = (data ?? {}); 185 | 186 | if (obj) { 187 | if (defaults != null || defaults != undefined) { 188 | defaults = eval(defaults); 189 | } 190 | 191 | if (plugins != null || plugins != undefined) { 192 | obj.plugins = eval(plugins); 193 | } 194 | 195 | console.debug('Before mutate:', obj); 196 | if (mutate) { 197 | obj = window.mutate_chart_object(obj) 198 | } 199 | console.debug('After mutate:', obj); 200 | } 201 | 202 | if (obj) obj = derationalize(obj); 203 | if (updated) updated = derationalize(updated); 204 | 205 | if (!updated) { 206 | console.log('New Chart'); 207 | 208 | canvas.width = width; 209 | canvas.height = height; 210 | 211 | const chart = new Chart(canvas, obj); 212 | 213 | // Store chart instance for mouse event handling 214 | if (id) { 215 | chartInstances.set(id, chart); 216 | } 217 | 218 | // Ensure animations work properly 219 | chart.resize(); 220 | 221 | // Force an initial animation if configured 222 | if (obj.options?.animation !== false) { 223 | chart.update('active'); 224 | } 225 | } else { 226 | try { 227 | let chart = Chart.getChart(canvas); 228 | chart.config._config.type = updated.type; 229 | chart.config._config.data = updated.data; 230 | chart.config._config.options = updated.options; 231 | 232 | console.debug('Updated chart:', chart); 233 | 234 | if (animate) { 235 | chart.update(); 236 | chart.resize(); 237 | } else { 238 | chart.update('none'); 239 | } 240 | } catch { 241 | // console.log("sending update failure"); 242 | postMessage([transaction, false]) 243 | } 244 | } 245 | 246 | // console.log("sending success"); 247 | postMessage([transaction, true]) 248 | } catch (e) { 249 | console.error(e); 250 | } 251 | }); 252 | }) -------------------------------------------------------------------------------- /src/coordinate.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use { 4 | gloo_utils::format::JsValueSerdeExt, 5 | js_sys::{Array, Reflect}, 6 | std::{ 7 | any::type_name, 8 | error::Error, 9 | fmt::{self, Display}, 10 | str::FromStr, 11 | }, 12 | wasm_bindgen::{JsCast, JsValue}, 13 | }; 14 | 15 | /// All the possible error states that result from asserting a given ChartJS coordinate is valid 16 | #[derive(Debug)] 17 | pub enum CoordinateError { 18 | InvalidX { 19 | ty: &'static str, 20 | error: Box, 21 | input: String, 22 | }, 23 | InvalidY { 24 | ty: &'static str, 25 | error: Box, 26 | input: serde_json::Number, 27 | }, 28 | 29 | Deserialize { 30 | value: JsValue, 31 | error: serde_json::Error, 32 | }, 33 | MissingKey { 34 | value: JsValue, 35 | key: String, 36 | }, 37 | GetTypedKey { 38 | value: JsValue, 39 | key: String, 40 | ty: &'static str, 41 | }, 42 | } 43 | 44 | impl Display for CoordinateError { 45 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | match self { 47 | CoordinateError::InvalidX { ty, error, input } => write!( 48 | f, 49 | "Invalid X coordinate of type `{ty}`: `{input}`, parsing failed due to: {error:?}" 50 | ), 51 | CoordinateError::InvalidY { ty, error, input } => write!( 52 | f, 53 | "Invalid Y coordinate of type `{ty}`: `{input}`, parsing failed due to: {error:?}" 54 | ), 55 | CoordinateError::Deserialize { value, error } => { 56 | write!(f, "Error deserializing value `{value:?}`: {error:?}") 57 | } 58 | CoordinateError::MissingKey { value, key } => { 59 | write!(f, "Key `{key}` is missing from object `{value:?}`") 60 | } 61 | CoordinateError::GetTypedKey { value, key, ty } => write!( 62 | f, 63 | "Key `{key}` (of type {ty}) is missing form object `{value:?}`" 64 | ), 65 | } 66 | } 67 | } 68 | 69 | impl Error for CoordinateError {} 70 | 71 | /// A representation in rust of a ChartJS coordinate, allowing convenient formatting when constructing tooltips 72 | #[derive(Debug)] 73 | #[non_exhaustive] 74 | pub struct Coordinate { 75 | pub x: T, 76 | pub y: U, 77 | } 78 | 79 | impl Coordinate 80 | where 81 | T: FromStr, 82 | U: FromStr, 83 | TE: std::error::Error + Sync + Send + 'static, 84 | UE: std::error::Error + Sync + Send + 'static, 85 | { 86 | fn from_raw(coord: Coordinate_) -> Result, CoordinateError> { 87 | Ok(Coordinate { 88 | x: coord.x.parse().map_err(|e| CoordinateError::InvalidX { 89 | ty: type_name::(), 90 | error: Box::new(e), 91 | input: coord.x.to_string(), 92 | })?, 93 | y: coord 94 | .y 95 | .to_string() 96 | .parse() 97 | .map_err(|e| CoordinateError::InvalidY { 98 | ty: type_name::(), 99 | error: Box::new(e), 100 | input: coord.y.clone(), 101 | })?, 102 | }) 103 | } 104 | 105 | /// This should be used inside a `#[wasm_bindgen]` function 106 | /// where it is known that the parameter passed to the function 107 | /// will have the shape of an individual coordinate 108 | /// 109 | /// 1. first create a function 110 | /// 111 | /// ```rust no_run 112 | /// #[wasm_bindgen] 113 | /// pub fn chart_data_label_formatter(a: JsValue, _: JsValue) -> JsValue { 114 | /// match Coordinate::::from_js_value(a) { 115 | /// Ok(val) => JsValue::from_str(&val.y.format().to_string()), 116 | /// Err(e) => { 117 | /// console_dbg!("Error converting to serde", e); 118 | /// JsValue::from_str("") 119 | /// } 120 | /// } 121 | /// } 122 | /// ``` 123 | /// 124 | /// 2. then use it with the [`crate::objects::DataLabels`] builder: 125 | /// 126 | /// ```rust no_run 127 | /// DataLabels::new() 128 | /// .formatter(FnWithArgs::<2>::new().run_rust_fn(chart_data_label_formatter)), 129 | /// ``` 130 | pub fn from_js_value(val: JsValue) -> Result { 131 | JsValueSerdeExt::into_serde::(&val) 132 | .map_err(|error| CoordinateError::Deserialize { 133 | value: val.clone(), 134 | error, 135 | }) 136 | .and_then(Coordinate::::from_raw) 137 | } 138 | } 139 | 140 | #[derive(serde::Deserialize)] 141 | struct Coordinate_ { 142 | x: String, 143 | y: serde_json::Number, 144 | } 145 | 146 | /// A representation in rust of a ChartJS poinrt, generally exposed via the tooltips plugin 147 | #[derive(Debug)] 148 | #[non_exhaustive] 149 | pub struct ChartJsPoint { 150 | /// Probably don't use this 151 | pub formatted_value: String, 152 | /// Probably don't use this 153 | pub label: String, 154 | /// Details about the dataset that are seen by the viewer of the chart 155 | pub dataset: ChartJsPointDataset, 156 | /// The raw coordinate value for the point 157 | pub raw: Coordinate, 158 | } 159 | 160 | fn get_field(val: &JsValue, key: &str) -> Result { 161 | match Reflect::get(val, &JsValue::from_str(key)) { 162 | Ok(v) => Ok(v), 163 | Err(_) => Err(CoordinateError::MissingKey { 164 | value: val.clone(), 165 | key: key.to_string(), 166 | }), 167 | } 168 | } 169 | 170 | fn get_string(val: &JsValue, key: &str) -> Result { 171 | get_field(val, key)? 172 | .as_string() 173 | .ok_or_else(|| CoordinateError::GetTypedKey { 174 | value: val.clone(), 175 | key: key.to_string(), 176 | ty: "String", 177 | }) 178 | } 179 | 180 | fn get_f64(val: &JsValue, key: &str) -> Result { 181 | get_field(val, key)? 182 | .as_f64() 183 | .ok_or_else(|| CoordinateError::GetTypedKey { 184 | value: val.clone(), 185 | key: key.to_string(), 186 | ty: "f64", 187 | }) 188 | } 189 | 190 | impl ChartJsPoint 191 | where 192 | T: FromStr, 193 | U: FromStr, 194 | TE: std::error::Error + Sync + Send + 'static, 195 | UE: std::error::Error + Sync + Send + 'static, 196 | T: fmt::Debug, 197 | U: fmt::Debug, 198 | { 199 | /// This should be used for parameter of [`crate::objects::TooltipCallbacks::label`] 200 | /// 201 | /// 1. first create a function 202 | /// 203 | /// ```rust no_run 204 | /// #[wasm_bindgen] 205 | /// pub fn tooltip_value_callback(context: JsValue) -> JsValue { 206 | /// match ChartJsPoint::::parse(context) { 207 | /// Ok(val) => { 208 | /// let label = val.dataset.label; 209 | /// let y = val.raw.y.format(); 210 | /// JsValue::from_str(&format!("{label}: {y}")) 211 | /// } 212 | /// Err(e) => { 213 | /// console_dbg!("Error parsing", e); 214 | /// JsValue::from_str("") 215 | /// } 216 | /// } 217 | /// } 218 | /// ``` 219 | /// 220 | /// 2. then use with the builder: 221 | /// 222 | /// ```rust no_run 223 | /// ChartOptions::new() 224 | /// .plugins( 225 | /// ChartPlugins::new() 226 | /// .tooltip( 227 | /// TooltipPlugin::new().callbacks( 228 | /// TooltipCallbacks::new() 229 | /// .label( 230 | /// FnWithArgs::<1>::new() 231 | /// .run_rust_fn(tooltip_value_callback), 232 | /// ) 233 | /// ), 234 | /// ), 235 | /// ) 236 | /// ``` 237 | pub fn parse(val: JsValue) -> Result { 238 | Ok(ChartJsPoint { 239 | formatted_value: get_string(&val, "formattedValue")?, 240 | label: get_string(&val, "label")?, 241 | dataset: JsValueSerdeExt::into_serde::(&get_field( 242 | &val, "dataset", 243 | )?) 244 | .map_err(|error| CoordinateError::Deserialize { 245 | value: val.clone(), 246 | error, 247 | })?, 248 | raw: Coordinate::from_js_value(get_field(&val, "raw")?)?, 249 | }) 250 | } 251 | 252 | /// This should be used for the parameter of [`crate::objects::TooltipCallbacks::title`] 253 | /// 254 | /// 1. first create a function 255 | /// 256 | /// ```rust no_run 257 | /// #[wasm_bindgen] 258 | /// pub fn tooltip_date_title_callback(context: JsValue) -> JsValue { 259 | /// match ChartJsPoint::::parse_array(context) { 260 | /// Ok(vals) if !vals.is_empty() => JsValue::from_str(vals[0].label.split(",").next().unwrap_or_default()), 261 | /// Ok(_) => { 262 | /// console_dbg!("Empty array"); 263 | /// JsValue::from_str("") 264 | /// } 265 | /// Err(e) => { 266 | /// console_dbg!("Error parsing", e); 267 | /// JsValue::from_str("") 268 | /// } 269 | /// } 270 | /// } 271 | /// ``` 272 | /// 273 | /// 2. then use with the builder: 274 | /// 275 | /// ```rust no_run 276 | /// ChartOptions::new() 277 | /// .plugins( 278 | /// ChartPlugins::new() 279 | /// .tooltip( 280 | /// TooltipPlugin::new().callbacks( 281 | /// TooltipCallbacks::new() 282 | /// .title( 283 | /// FnWithArgs::<1>::new() 284 | /// .run_rust_fn(tooltip_date_title_callback), 285 | /// ) 286 | /// ), 287 | /// ), 288 | /// ) 289 | /// ``` 290 | pub fn parse_array(val: JsValue) -> Result, CoordinateError> { 291 | let vec = if val.is_array() { 292 | let array = val.dyn_into::().unwrap_or_default().to_vec(); 293 | let mut parsed = Vec::new(); 294 | for item in array { 295 | parsed.push(Self::parse(item)?); 296 | } 297 | parsed 298 | } else { 299 | Vec::from([Self::parse(val)?]) 300 | }; 301 | Ok(vec) 302 | } 303 | } 304 | 305 | #[derive(serde::Deserialize)] 306 | struct ChartJsPoint_ { 307 | formatted_value: String, 308 | label: String, 309 | dataset: ChartJsPointDataset, 310 | raw: Coordinate_, 311 | } 312 | 313 | #[derive(serde::Deserialize, Debug)] 314 | pub struct ChartJsPointDataset { 315 | /// The label for the dataset that is seen by the viewer of the chart 316 | pub label: String, 317 | pub r#type: String, 318 | } 319 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/worker.rs: -------------------------------------------------------------------------------- 1 | use gloo_console::console_dbg; 2 | use gloo_utils::{document, window}; 3 | use itertools::Itertools; 4 | use js_sys::{Array, Function, Object, Reflect}; 5 | use tokio::sync::broadcast::{self}; 6 | use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; 7 | use web_sys::{MessageEvent, Worker, WorkerOptions, WorkerType}; 8 | 9 | type Callback = dyn FnMut(MessageEvent); 10 | 11 | const WORKER_SHIM: &str = include_str!("../js/worker_shim.js"); 12 | 13 | macro_rules! obj { 14 | ($($key:literal => $val:expr),+ $(,)?) => { 15 | { 16 | let object = Object::new(); 17 | $( 18 | let _ = Reflect::set(&object, &$key.into(), &{$val}.into()); 19 | )* 20 | object 21 | } 22 | }; 23 | } 24 | 25 | macro_rules! destructure { 26 | ($obj:expr => $($key:ident),+ $(,)?) => { 27 | $( 28 | let $key = Reflect::get(&$obj, &stringify!($key).into()); 29 | )* 30 | }; 31 | } 32 | 33 | macro_rules! if_let_all { 34 | ($($var:ident),+ => $body:block) => { 35 | if let Some(($($var,)+)) = [$($var),+] 36 | .into_iter() 37 | .collect::, _>>() 38 | .ok() 39 | .and_then(|v| v.into_iter().collect_tuple()) { 40 | $body 41 | } 42 | }; 43 | } 44 | 45 | #[derive(Clone, Debug)] 46 | pub(crate) enum MessageContent { 47 | CallbackRequest { 48 | function: String, 49 | args: Array, 50 | }, 51 | 52 | CallbackResponse { 53 | data: JsValue, 54 | }, 55 | 56 | Render { 57 | canvas: JsValue, 58 | width: JsValue, 59 | height: JsValue, 60 | obj: JsValue, 61 | id: String, 62 | mutate: bool, 63 | plugins: String, 64 | defaults: String, 65 | }, 66 | 67 | Update { 68 | updated: JsValue, 69 | id: String, 70 | animate: bool, 71 | }, 72 | 73 | Other, 74 | } 75 | 76 | impl From for JsValue { 77 | fn from(val: MessageContent) -> Self { 78 | match val { 79 | MessageContent::CallbackRequest { function, args } => obj! { 80 | "function" => function, 81 | "args" => args 82 | }, 83 | MessageContent::CallbackResponse { data } => obj! { 84 | "data" => data 85 | }, 86 | MessageContent::Render { 87 | canvas, 88 | width, 89 | height, 90 | obj, 91 | id, 92 | mutate, 93 | plugins, 94 | defaults, 95 | } => obj! { 96 | "canvas" => canvas, 97 | "width" => width, 98 | "height" => height, 99 | "obj" => obj, 100 | "id" => id, 101 | "mutate" => mutate, 102 | "plugins" => plugins, 103 | "defaults" => defaults, 104 | }, 105 | MessageContent::Update { 106 | updated, 107 | id, 108 | animate, 109 | } => obj! { 110 | "updated" => updated, 111 | "id" => id, 112 | "animate" => animate, 113 | }, 114 | 115 | _ => return Array::new().into(), 116 | } 117 | .into() 118 | } 119 | } 120 | 121 | impl From for MessageContent { 122 | fn from(value: JsValue) -> Self { 123 | // console_dbg!(value); 124 | 125 | if !value.is_object() { 126 | return Self::Other; 127 | } 128 | 129 | destructure!( 130 | value => function, args, data, canvas, width, height, obj, id, mutate, plugins, defaults, updated, animate 131 | ); 132 | 133 | if_let_all!(function, args => { 134 | return MessageContent::CallbackRequest { 135 | function: function.as_string().unwrap_or_default(), 136 | args: Array::from(&args), 137 | }; 138 | }); 139 | 140 | if let Ok(data) = data { 141 | return MessageContent::CallbackResponse { data }; 142 | } 143 | 144 | { 145 | let id = id.clone(); 146 | if_let_all!(canvas, width, height, obj, id, mutate, plugins, defaults => { 147 | return MessageContent::Render { 148 | canvas, 149 | width, 150 | height, 151 | obj, 152 | id: id.as_string().unwrap_or_default(), 153 | mutate: mutate.as_bool().unwrap_or_default(), 154 | plugins: plugins.as_string().unwrap_or_default(), 155 | defaults: defaults.as_string().unwrap_or_default(), 156 | }; 157 | }); 158 | } 159 | 160 | if_let_all!(updated, id, animate => { 161 | return MessageContent::Update { 162 | updated, 163 | id: id.as_string().unwrap_or_default(), 164 | animate: animate.as_bool().unwrap_or_default(), 165 | }; 166 | }); 167 | 168 | MessageContent::Other 169 | } 170 | } 171 | 172 | #[derive(Clone)] 173 | pub struct ChartWorker { 174 | pub(crate) worker: Worker, 175 | pub(crate) from_worker: broadcast::Sender<(String, MessageContent)>, 176 | } 177 | 178 | impl ChartWorker { 179 | pub(crate) async fn render( 180 | &self, 181 | obj: JsValue, 182 | chart_id: &String, 183 | mutate: bool, 184 | plugins: String, 185 | defaults: String, 186 | ) -> Result<(), Box> { 187 | let canvas_element = document().get_element_by_id(chart_id).unwrap(); 188 | 189 | let width = canvas_element.client_width(); 190 | let height = canvas_element.client_height(); 191 | let rect = canvas_element.get_bounding_client_rect(); 192 | let computed_styles = web_sys::window() 193 | .unwrap() 194 | .get_computed_style(&canvas_element) 195 | .unwrap() 196 | .unwrap(); 197 | 198 | let mousemove_handler = { 199 | let worker = self.clone(); 200 | let chart_id = chart_id.clone(); 201 | Closure::wrap(Box::new({ 202 | let computed_styles = computed_styles.clone(); 203 | let rect = rect.clone(); 204 | move |event: web_sys::MouseEvent| { 205 | let x = (event.client_x() as f64 - rect.left()) * (width as f64 / rect.width()); 206 | let y = 207 | (event.client_y() as f64 - rect.top()) * (height as f64 / rect.height()); 208 | 209 | let message = obj! { 210 | "type" => "mouse-event", 211 | "eventType" => "mousemove", 212 | "x" => x, 213 | "y" => y, 214 | "chartId" => chart_id.clone(), 215 | "computedStyles" => obj! { 216 | "fontFamily" => computed_styles 217 | .get_property_value("font-family") 218 | .unwrap(), 219 | 220 | "fontSize" => computed_styles 221 | .get_property_value("font-size") 222 | .unwrap(), 223 | 224 | "fontWeight" => computed_styles 225 | .get_property_value("font-weight") 226 | .unwrap(), 227 | 228 | "fontStyle" => computed_styles 229 | .get_property_value("font-style") 230 | .unwrap(), 231 | 232 | "lineHeight" => computed_styles 233 | .get_property_value("line-height") 234 | .unwrap(), 235 | 236 | "color" => computed_styles.get_property_value("color").unwrap(), 237 | } 238 | }; 239 | 240 | worker.worker.post_message(&message).unwrap(); 241 | } 242 | }) as Box) 243 | }; 244 | 245 | let mouseleave_handler = { 246 | let worker = self.clone(); 247 | let chart_id = chart_id.clone(); 248 | Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| { 249 | let message = obj! { 250 | "type" => "mouse-event", 251 | "eventType" => "mouseleave", 252 | "chartId" => chart_id.clone(), 253 | }; 254 | 255 | worker.worker.post_message(&message).unwrap(); 256 | }) as Box) 257 | }; 258 | let click_handler = { 259 | let worker = self.clone(); 260 | let chart_id = chart_id.clone(); 261 | let computed_styles = computed_styles.clone(); 262 | let rect = rect.clone(); 263 | Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { 264 | let x = (event.client_x() as f64 - rect.left()) * (width as f64 / rect.width()); 265 | let y = (event.client_y() as f64 - rect.top()) * (height as f64 / rect.height()); 266 | 267 | let message = obj! { 268 | "type" => "mouse-event", 269 | "eventType" => "click", 270 | "x" => x, 271 | "y" => y, 272 | "chartId" => chart_id.clone(), 273 | "computedStyles" => obj! { 274 | "fontFamily" => computed_styles 275 | .get_property_value("font-family") 276 | .unwrap(), 277 | 278 | "fontSize" => computed_styles 279 | .get_property_value("font-size") 280 | .unwrap(), 281 | 282 | "fontWeight" => computed_styles 283 | .get_property_value("font-weight") 284 | .unwrap(), 285 | 286 | "fontStyle" => computed_styles 287 | .get_property_value("font-style") 288 | .unwrap(), 289 | 290 | "lineHeight" => computed_styles 291 | .get_property_value("line-height") 292 | .unwrap(), 293 | 294 | }, 295 | }; 296 | 297 | worker.worker.post_message(&message).unwrap(); 298 | }) as Box) 299 | }; 300 | 301 | canvas_element 302 | .add_event_listener_with_callback( 303 | "mousemove", 304 | mousemove_handler.as_ref().unchecked_ref(), 305 | ) 306 | .unwrap(); 307 | canvas_element 308 | .add_event_listener_with_callback( 309 | "mouseleave", 310 | mouseleave_handler.as_ref().unchecked_ref(), 311 | ) 312 | .unwrap(); 313 | canvas_element 314 | .add_event_listener_with_callback("click", click_handler.as_ref().unchecked_ref()) 315 | .unwrap(); 316 | 317 | mousemove_handler.forget(); 318 | mouseleave_handler.forget(); 319 | click_handler.forget(); 320 | 321 | let offscreen_canvas = canvas_element 322 | .dyn_into::() 323 | .unwrap() 324 | .transfer_control_to_offscreen() 325 | .unwrap(); 326 | 327 | self.send( 328 | MessageContent::Render { 329 | canvas: offscreen_canvas.clone().into(), 330 | width: width.into(), 331 | height: height.into(), 332 | obj, 333 | id: chart_id.to_string(), 334 | mutate, 335 | plugins, 336 | defaults, 337 | }, 338 | &[offscreen_canvas.into()], 339 | ) 340 | .await 341 | .map(|_| ()) 342 | } 343 | 344 | pub(crate) async fn update( 345 | &self, 346 | updated: JsValue, 347 | id: &String, 348 | animate: bool, 349 | ) -> Result> { 350 | self.send( 351 | MessageContent::Update { 352 | updated, 353 | id: id.to_string(), 354 | animate, 355 | }, 356 | &[], 357 | ) 358 | .await 359 | .map(|v| v.as_bool().unwrap_or_default()) 360 | } 361 | 362 | pub(crate) async fn new(imports_block: &str) -> Result> { 363 | // Spawn Worker 364 | let worker_options = WorkerOptions::new(); 365 | worker_options.set_type(WorkerType::Module); 366 | 367 | let from_worker = broadcast::channel::<(String, MessageContent)>(1).0; 368 | let worker = Worker::new_with_options(&shim_blob(imports_block), &worker_options) 369 | .map_err(|e| format!("{e:?}"))?; 370 | 371 | let handler = { 372 | let from_worker = from_worker.clone(); 373 | let worker = worker.clone(); 374 | Closure::::new(move |ev: MessageEvent| { 375 | let data = Array::from(&ev.data()); 376 | let id = data.get(0).as_string().unwrap_or_default(); 377 | let message: MessageContent = data.get(1).into(); 378 | 379 | match message.clone() { 380 | MessageContent::CallbackRequest { function, args } => { 381 | console_dbg!(message); 382 | 383 | let callbacks = 384 | Reflect::get(&window().into(), &"callbacks".into()).unwrap(); 385 | let function = Reflect::get(&callbacks, &function.into()).unwrap(); 386 | let function = Function::from(function); 387 | 388 | let data = function.apply(&JsValue::null(), &args).unwrap(); 389 | let _ = worker.post_message(&{ 390 | let arr = Array::new(); 391 | arr.push(&id.into()); 392 | arr.push(&MessageContent::CallbackResponse { data }.into()); 393 | arr.into() 394 | }); 395 | } 396 | _ => { 397 | let _ = from_worker.send((id, message)); 398 | } 399 | } 400 | }) 401 | }; 402 | 403 | worker.set_onmessage(Some(handler.as_ref().unchecked_ref())); 404 | handler.forget(); 405 | from_worker.subscribe().recv().await?; 406 | 407 | // Return tx 408 | Ok(Self { 409 | worker, 410 | from_worker, 411 | }) 412 | } 413 | 414 | async fn send( 415 | &self, 416 | v: MessageContent, 417 | t: &[JsValue], 418 | ) -> Result> { 419 | let ts = uuid::Uuid::new_v4().to_string(); 420 | let arr = Array::new(); 421 | arr.push(&ts.clone().into()); 422 | arr.push(&v.into()); 423 | 424 | self.worker 425 | .post_message_with_transfer(&arr.into(), &Array::from_iter(t.iter())) 426 | .map_err(|e| format!("{e:?}"))?; 427 | 428 | loop { 429 | if let Ok((id, data)) = self.from_worker.subscribe().recv().await { 430 | let arr = Array::from(&data.into()); 431 | let data = arr.get(1); 432 | 433 | if id == ts { 434 | return Ok(data); 435 | } 436 | } 437 | } 438 | } 439 | } 440 | 441 | fn shim_blob(imports_block: &str) -> String { 442 | let _imports = js_sys::eval( 443 | "[...document.head.querySelectorAll('script')].map(s => s.src).filter(Boolean)", 444 | ) 445 | .map(|v| Array::from(&v)) 446 | .unwrap_or_default(); 447 | 448 | let shim = WORKER_SHIM.replace("/// IMPORTS", imports_block); 449 | 450 | web_sys::Url::create_object_url_with_blob( 451 | &web_sys::Blob::new_with_blob_sequence_and_options( 452 | &{ 453 | let a = Array::new(); 454 | a.push(&shim.into()); 455 | a.into() 456 | }, 457 | &{ 458 | let bag = web_sys::BlobPropertyBag::new(); 459 | bag.set_type("application/javascript"); 460 | bag 461 | }, 462 | ) 463 | .unwrap(), 464 | ) 465 | .unwrap() 466 | } 467 | -------------------------------------------------------------------------------- /src/objects/helper_objects.rs: -------------------------------------------------------------------------------- 1 | #![allow(unreachable_patterns)] 2 | 3 | use { 4 | crate::traits::*, 5 | js_sys::{Function, Reflect}, 6 | serde::{ 7 | de::{self, DeserializeOwned}, 8 | Deserialize, Serialize, 9 | }, 10 | std::fmt::{Debug, Display}, 11 | wasm_bindgen::{JsCast, JsValue}, 12 | }; 13 | 14 | #[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)] 15 | #[serde(transparent)] 16 | pub struct DatasetData(pub serde_json::Value); 17 | impl DatasetData { 18 | pub fn is_empty(&self) -> bool { 19 | serde_json::to_value(self) 20 | .unwrap() 21 | .as_array() 22 | .unwrap() 23 | .is_empty() 24 | } 25 | 26 | pub fn from_single_point_array(iter: impl Iterator) -> Self { 27 | DatasetData(serde_json::to_value(iter.collect::>()).unwrap()) 28 | } 29 | 30 | pub fn from_minmax_array(iter: impl Iterator) -> Self { 31 | DatasetData(serde_json::to_value(iter.collect::>()).unwrap()) 32 | } 33 | } 34 | impl PartialOrd for DatasetData { 35 | fn partial_cmp(&self, other: &Self) -> Option { 36 | Some(self.cmp(other)) 37 | } 38 | } 39 | impl Ord for DatasetData { 40 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 41 | self.0.to_string().cmp(&other.0.to_string()) 42 | } 43 | } 44 | 45 | #[derive(Debug, Clone, Deserialize, Serialize, Default)] 46 | pub struct NoDatasets {} 47 | impl DatasetTrait for NoDatasets { 48 | fn labels(self) -> Vec { 49 | Vec::new() 50 | } 51 | } 52 | 53 | #[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, PartialOrd, Ord)] 54 | #[serde(bound = "D: DatasetTrait")] 55 | #[allow(unreachable_patterns)] 56 | pub struct Dataset { 57 | datasets: D, 58 | #[serde(skip_serializing_if = "Option::is_none", rename(serialize = "labels"))] 59 | forced_labels: Option>, 60 | #[serde(skip_serializing_if = "option_vec_is_none")] 61 | labels: Option>, 62 | } 63 | impl Dataset { 64 | pub fn new() -> Self { 65 | Self { 66 | datasets: D::default(), 67 | labels: None, 68 | forced_labels: None, 69 | } 70 | } 71 | 72 | pub fn get_datasets(&mut self) -> &mut D { 73 | &mut self.datasets 74 | } 75 | 76 | pub fn datasets(mut self, datasets: impl Into) -> Self { 77 | self.datasets = datasets.into(); 78 | 79 | if self.forced_labels.is_none() { 80 | let labels = self.datasets.clone(); 81 | self._labels(labels.labels()) 82 | } else { 83 | self 84 | } 85 | } 86 | 87 | pub fn get_labels(&mut self) -> &mut Option> { 88 | match (&self.labels, &self.forced_labels) { 89 | (Some(_), None) => &mut self.labels, 90 | _ => &mut self.forced_labels, 91 | } 92 | } 93 | 94 | fn _labels>(mut self, labels: impl IntoIterator) -> Self { 95 | self.labels = Some(labels.into_iter().map(Into::into).collect()); 96 | 97 | self 98 | } 99 | 100 | pub fn labels>( 101 | mut self, 102 | labels: impl IntoIterator, 103 | ) -> Self { 104 | self.forced_labels = Some(labels.into_iter().map(Into::into).collect()); 105 | self.labels = None; 106 | 107 | self 108 | } 109 | } 110 | fn option_vec_is_none(opt: &Option>) -> bool { 111 | match opt { 112 | Some(vec) => vec.is_empty() || vec.clone().try_into() == Ok([T::default()]), 113 | None => true, 114 | } 115 | } 116 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] 117 | #[serde(untagged)] 118 | pub enum Any { 119 | String(String), 120 | Int(isize), 121 | Bool(bool), 122 | Vec(Vec<()>), 123 | } 124 | impl From for Any { 125 | fn from(value: bool) -> Self { 126 | Self::Bool(value) 127 | } 128 | } 129 | impl From for Any { 130 | fn from(value: String) -> Self { 131 | Self::String(value) 132 | } 133 | } 134 | impl Any { 135 | pub fn is_empty(&self) -> bool { 136 | match self { 137 | Any::String(s) => s.is_empty(), 138 | Any::Int(_i) => false, 139 | Any::Bool(_b) => false, 140 | Any::Vec(v) => v.is_empty(), 141 | } 142 | } 143 | } 144 | impl Display for Any { 145 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 146 | match self { 147 | Any::String(s) => write!(f, "{s}"), 148 | Any::Bool(b) => write!(f, "{b}"), 149 | Any::Int(i) => write!(f, "{i}"), 150 | Any::Vec(_) => write!(f, ""), 151 | } 152 | } 153 | } 154 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 155 | pub struct NumberOrDateString(String); 156 | impl From for NumberOrDateString { 157 | fn from(value: NumberString) -> Self { 158 | value.0.into() 159 | } 160 | } 161 | impl NumberOrDateString { 162 | pub fn is_empty(&self) -> bool { 163 | self.0.is_empty() 164 | } 165 | } 166 | impl PartialOrd for NumberOrDateString { 167 | fn partial_cmp(&self, other: &Self) -> Option { 168 | Some(self.cmp(other)) 169 | } 170 | } 171 | impl Ord for NumberOrDateString { 172 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 173 | if let Some((s, o)) = self 174 | .0 175 | .parse::() 176 | .ok() 177 | .zip(other.0.parse::().ok()) 178 | { 179 | s.cmp(&o) 180 | } else { 181 | self.0.cmp(&other.0) 182 | } 183 | } 184 | } 185 | impl From for NumberOrDateString { 186 | fn from(s: T) -> Self { 187 | Self(s.to_string()) 188 | } 189 | } 190 | #[allow(unknown_lints, clippy::to_string_trait_impl)] 191 | impl ToString for NumberOrDateString { 192 | fn to_string(&self) -> String { 193 | self.0.to_string() 194 | } 195 | } 196 | impl Serialize for NumberOrDateString { 197 | fn serialize(&self, serializer: S) -> Result 198 | where 199 | S: serde::Serializer, 200 | { 201 | let fnum: Result = self.0.parse(); 202 | let inum: Result = self.0.parse(); 203 | match (fnum, inum) { 204 | (Ok(_), Ok(inum)) => serializer.serialize_i64(inum), 205 | (Ok(fnum), _) => serializer.serialize_f64(fnum), 206 | _ => serializer.serialize_str(&self.0), 207 | } 208 | } 209 | } 210 | impl<'de> Deserialize<'de> for NumberOrDateString { 211 | fn deserialize(deserializer: D) -> Result 212 | where 213 | D: serde::Deserializer<'de>, 214 | { 215 | Any::deserialize(deserializer).map(|soi| Self(soi.to_string())) 216 | } 217 | } 218 | 219 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 220 | pub struct BoolString(String); 221 | impl BoolString { 222 | pub fn opt_true() -> Option { 223 | BoolString("true".into()).into() 224 | } 225 | pub fn opt_false() -> Option { 226 | BoolString("false".into()).into() 227 | } 228 | pub fn _true() -> BoolString { 229 | BoolString("true".into()) 230 | } 231 | pub fn _false() -> BoolString { 232 | BoolString("false".into()) 233 | } 234 | pub fn is_empty(&self) -> bool { 235 | self.0.is_empty() 236 | } 237 | } 238 | impl Default for BoolString { 239 | fn default() -> Self { 240 | Self::_false() 241 | } 242 | } 243 | impl ChartJsRsObject for BoolString { 244 | fn is_empty(&self) -> bool { 245 | self.is_empty() 246 | } 247 | } 248 | impl From for BoolString { 249 | fn from(s: T) -> Self { 250 | Self(s.to_string()) 251 | } 252 | } 253 | impl Serialize for BoolString { 254 | fn serialize(&self, serializer: S) -> Result 255 | where 256 | S: serde::Serializer, 257 | { 258 | let bool_: Result = self.0.parse(); 259 | let any: Result = self.0.parse(); 260 | match (bool_, any) { 261 | (Ok(bool_), _) => serializer.serialize_bool(bool_), 262 | (_, Ok(any)) => serializer.serialize_str(&any), 263 | _ => unreachable!(), 264 | } 265 | } 266 | } 267 | impl<'de> Deserialize<'de> for BoolString { 268 | fn deserialize(deserializer: D) -> Result 269 | where 270 | D: serde::Deserializer<'de>, 271 | { 272 | Any::deserialize(deserializer).map(|soi| Self(soi.to_string())) 273 | } 274 | } 275 | 276 | #[derive(Debug, Deserialize, Serialize)] 277 | struct JavascriptFunction { 278 | args: Vec, 279 | body: String, 280 | return_value: String, 281 | closure_id: Option, 282 | } 283 | 284 | const ALPHABET: [&str; 32] = [ 285 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", 286 | "t", "u", "v", "w", "x", "y", "z", "aa", "bb", "cc", "dd", "ee", "ff", 287 | ]; 288 | 289 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 290 | pub struct FnWithArgs { 291 | pub(crate) args: [String; N], 292 | pub(crate) body: String, 293 | pub(crate) return_value: String, 294 | pub(crate) closure_id: Option, 295 | } 296 | impl FnWithArgs { 297 | pub fn rationalise_1_level(obj: &JsValue, name: &'static str) { 298 | super::rationalise_1_level::(obj, name, |o| { 299 | let _ = Reflect::set(obj, &name.into(), &o.build()); 300 | }) 301 | } 302 | pub fn rationalise_2_levels(obj: &JsValue, name: (&'static str, &'static str)) { 303 | super::rationalise_2_levels::(obj, name, |a, o| { 304 | let _ = Reflect::set(&a, &name.1.into(), &o.build()); 305 | }) 306 | } 307 | } 308 | 309 | impl Default for FnWithArgs { 310 | fn default() -> Self { 311 | Self { 312 | args: (0..N) 313 | .map(|idx| ALPHABET[idx].to_string()) 314 | .collect::>() 315 | .try_into() 316 | .unwrap(), 317 | body: Default::default(), 318 | return_value: Default::default(), 319 | closure_id: None, 320 | } 321 | } 322 | } 323 | impl<'de, const N: usize> Deserialize<'de> for FnWithArgs { 324 | fn deserialize(deserializer: D) -> Result 325 | where 326 | D: serde::Deserializer<'de>, 327 | { 328 | let js = JavascriptFunction::deserialize(deserializer)?; 329 | Ok(FnWithArgs:: { 330 | args: js.args.clone().try_into().map_err(|_| { 331 | de::Error::custom(format!("Array had length {}, needed {}.", js.args.len(), N)) 332 | })?, 333 | body: js.body, 334 | return_value: js.return_value, 335 | closure_id: js.closure_id, 336 | }) 337 | } 338 | } 339 | impl Serialize for FnWithArgs { 340 | fn serialize(&self, serializer: S) -> Result 341 | where 342 | S: serde::Serializer, 343 | { 344 | JavascriptFunction::serialize( 345 | &JavascriptFunction { 346 | args: self.args.to_vec(), 347 | body: self.body.clone(), 348 | return_value: self.return_value.clone(), 349 | closure_id: self.closure_id.clone(), 350 | }, 351 | serializer, 352 | ) 353 | } 354 | } 355 | 356 | impl FnWithArgs { 357 | pub fn is_empty(&self) -> bool { 358 | match self.closure_id { 359 | Some(_) => false, 360 | None => self.body.is_empty(), 361 | } 362 | } 363 | 364 | pub fn new() -> Self { 365 | Self::default() 366 | } 367 | 368 | pub fn args>(mut self, args: [S; N]) -> Self { 369 | self.args = args 370 | .into_iter() 371 | .enumerate() 372 | .map(|(idx, s)| { 373 | let arg = s.as_ref(); 374 | if arg.is_empty() { ALPHABET[idx] } else { arg }.to_string() 375 | }) 376 | .collect::>() 377 | .try_into() 378 | .unwrap(); 379 | self 380 | } 381 | 382 | pub fn js_body(mut self, body: &str) -> Self { 383 | self.body = format!("{}\n{body}", self.body); 384 | self.to_owned() 385 | } 386 | 387 | pub fn js_return_value(self, return_value: &str) -> Self { 388 | let mut s = if self.body.is_empty() { 389 | self.js_body("") 390 | } else { 391 | self 392 | }; 393 | s.return_value = return_value.to_string(); 394 | s.to_owned() 395 | } 396 | 397 | pub fn build(self) -> Function { 398 | if let Some(id) = self.closure_id { 399 | let args = self.args.join(", "); 400 | Function::new_with_args(&args, &format!("{{ return window['{id}']({args}) }}")) 401 | } else { 402 | Function::new_with_args( 403 | &self.args.join(", "), 404 | &format!("{{ {}\nreturn {} }}", self.body, self.return_value), 405 | ) 406 | } 407 | } 408 | } 409 | 410 | impl FnWithArgs<1> { 411 | pub fn run_rust_fn B>(mut self, _func: FN) -> Self { 412 | let fn_name = std::any::type_name::() 413 | .split("::") 414 | .collect::>() 415 | .into_iter() 416 | .next_back() 417 | .unwrap(); 418 | 419 | self.body = format!( 420 | "{}\nconst _out_ = window.callbacks.{}({});", 421 | self.body, 422 | fn_name, 423 | self.args.join(", ") 424 | ); 425 | self.js_return_value("_out_") 426 | } 427 | 428 | #[track_caller] 429 | pub fn rust_closure JsValue + 'static>(mut self, closure: F) -> Self { 430 | let js_closure = wasm_bindgen::closure::Closure::wrap( 431 | Box::new(closure) as Box JsValue> 432 | ); 433 | let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref(); 434 | 435 | let js_window = gloo_utils::window(); 436 | let id = uuid::Uuid::new_v4().to_string(); 437 | Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap(); 438 | js_closure.forget(); 439 | 440 | gloo_console::debug!(format!( 441 | "Closure at {}:{}:{} set at window.['{id}'].", 442 | file!(), 443 | line!(), 444 | column!() 445 | )); 446 | self.closure_id = Some(id); 447 | self 448 | } 449 | } 450 | 451 | impl FnWithArgs<2> { 452 | pub fn run_rust_fn C>(mut self, _func: FN) -> Self { 453 | let fn_name = std::any::type_name::() 454 | .split("::") 455 | .collect::>() 456 | .into_iter() 457 | .next_back() 458 | .unwrap(); 459 | 460 | self.body = format!( 461 | "{}\nconst _out_ = window.callbacks.{}({});", 462 | self.body, 463 | fn_name, 464 | self.args.join(", ") 465 | ); 466 | self.js_return_value("_out_") 467 | } 468 | 469 | #[track_caller] 470 | pub fn rust_closure JsValue + 'static>( 471 | mut self, 472 | closure: F, 473 | ) -> Self { 474 | let js_closure = wasm_bindgen::closure::Closure::wrap( 475 | Box::new(closure) as Box JsValue> 476 | ); 477 | let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref(); 478 | 479 | let js_window = gloo_utils::window(); 480 | let id = uuid::Uuid::new_v4().to_string(); 481 | Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap(); 482 | js_closure.forget(); 483 | 484 | gloo_console::debug!(format!( 485 | "Closure at {}:{}:{} set at window.['{id}'].", 486 | file!(), 487 | line!(), 488 | column!() 489 | )); 490 | self.closure_id = Some(id); 491 | self 492 | } 493 | } 494 | 495 | impl FnWithArgs<3> { 496 | pub fn run_rust_fn D>(mut self, _func: FN) -> Self { 497 | let fn_name = std::any::type_name::() 498 | .split("::") 499 | .collect::>() 500 | .into_iter() 501 | .next_back() 502 | .unwrap(); 503 | 504 | self.body = format!( 505 | "{}\nconst _out_ = window.callbacks.{}({});", 506 | self.body, 507 | fn_name, 508 | self.args.join(", ") 509 | ); 510 | self.js_return_value("_out_") 511 | } 512 | 513 | #[track_caller] 514 | pub fn rust_closure JsValue + 'static>( 515 | mut self, 516 | closure: F, 517 | ) -> Self { 518 | let js_closure = wasm_bindgen::closure::Closure::wrap( 519 | Box::new(closure) as Box JsValue> 520 | ); 521 | let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref(); 522 | 523 | let js_window = gloo_utils::window(); 524 | let id = uuid::Uuid::new_v4().to_string(); 525 | Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap(); 526 | js_closure.forget(); 527 | 528 | gloo_console::debug!(format!( 529 | "Closure at {}:{}:{} set at window.['{id}'].", 530 | file!(), 531 | line!(), 532 | column!() 533 | )); 534 | self.closure_id = Some(id); 535 | self 536 | } 537 | } 538 | 539 | impl FnWithArgs<4> { 540 | pub fn run_rust_fn E>(mut self, _func: FN) -> Self { 541 | let fn_name = std::any::type_name::() 542 | .split("::") 543 | .collect::>() 544 | .into_iter() 545 | .next_back() 546 | .unwrap(); 547 | 548 | self.body = format!( 549 | "{}\nconst _out_ = window.callbacks.{}({});", 550 | self.body, 551 | fn_name, 552 | self.args.join(", ") 553 | ); 554 | self.js_return_value("_out_") 555 | } 556 | 557 | #[track_caller] 558 | pub fn rust_closure JsValue + 'static>( 559 | mut self, 560 | closure: F, 561 | ) -> Self { 562 | let js_closure = wasm_bindgen::closure::Closure::wrap( 563 | Box::new(closure) as Box JsValue> 564 | ); 565 | let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref(); 566 | 567 | let js_window = gloo_utils::window(); 568 | let id = uuid::Uuid::new_v4().to_string(); 569 | Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap(); 570 | js_closure.forget(); 571 | 572 | gloo_console::debug!(format!( 573 | "Closure at {}:{}:{} set at window.['{id}'].", 574 | file!(), 575 | line!(), 576 | column!() 577 | )); 578 | self.closure_id = Some(id); 579 | self 580 | } 581 | } 582 | 583 | impl FnWithArgs<5> { 584 | pub fn run_rust_fn F>(mut self, _func: FN) -> Self { 585 | let fn_name = std::any::type_name::() 586 | .split("::") 587 | .collect::>() 588 | .into_iter() 589 | .next_back() 590 | .unwrap(); 591 | 592 | self.body = format!( 593 | "{}\nconst _out_ = window.callbacks.{}({});", 594 | self.body, 595 | fn_name, 596 | self.args.join(", ") 597 | ); 598 | self.js_return_value("_out_") 599 | } 600 | 601 | #[track_caller] 602 | pub fn rust_closure JsValue + 'static>( 603 | mut self, 604 | closure: F, 605 | ) -> Self { 606 | let js_closure = wasm_bindgen::closure::Closure::wrap(Box::new(closure) 607 | as Box JsValue>); 608 | let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref(); 609 | 610 | let js_window = gloo_utils::window(); 611 | let id = uuid::Uuid::new_v4().to_string(); 612 | Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap(); 613 | js_closure.forget(); 614 | 615 | gloo_console::debug!(format!( 616 | "Closure at {}:{}:{} set at window.['{id}'].", 617 | file!(), 618 | line!(), 619 | column!() 620 | )); 621 | self.closure_id = Some(id); 622 | self 623 | } 624 | } 625 | 626 | impl FnWithArgs<6> { 627 | pub fn run_rust_fn G>( 628 | mut self, 629 | _func: FN, 630 | ) -> Self { 631 | let fn_name = std::any::type_name::() 632 | .split("::") 633 | .collect::>() 634 | .into_iter() 635 | .next_back() 636 | .unwrap(); 637 | 638 | self.body = format!( 639 | "{}\nconst _out_ = window.callbacks.{}({});", 640 | self.body, 641 | fn_name, 642 | self.args.join(", ") 643 | ); 644 | self.js_return_value("_out_") 645 | } 646 | 647 | #[track_caller] 648 | pub fn rust_closure< 649 | F: Fn(JsValue, JsValue, JsValue, JsValue, JsValue, JsValue) -> JsValue + 'static, 650 | >( 651 | mut self, 652 | closure: F, 653 | ) -> Self { 654 | let js_closure = wasm_bindgen::closure::Closure::wrap(Box::new(closure) 655 | as Box JsValue>); 656 | let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref(); 657 | 658 | let js_window = gloo_utils::window(); 659 | let id = uuid::Uuid::new_v4().to_string(); 660 | Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap(); 661 | js_closure.forget(); 662 | 663 | gloo_console::debug!(format!( 664 | "Closure at {}:{}:{} set at window.['{id}'].", 665 | file!(), 666 | line!(), 667 | column!() 668 | )); 669 | self.closure_id = Some(id); 670 | self 671 | } 672 | } 673 | 674 | // 7 is the maximum wasm_bindgen can handle rn AFAIK 675 | impl FnWithArgs<7> { 676 | pub fn run_rust_fn H>( 677 | mut self, 678 | _func: FN, 679 | ) -> Self { 680 | let fn_name = std::any::type_name::() 681 | .split("::") 682 | .collect::>() 683 | .into_iter() 684 | .next_back() 685 | .unwrap(); 686 | 687 | self.body = format!( 688 | "{}\nconst _out_ = window.callbacks.{}({});", 689 | self.body, 690 | fn_name, 691 | self.args.join(", ") 692 | ); 693 | self.js_return_value("_out_") 694 | } 695 | 696 | #[track_caller] 697 | pub fn rust_closure< 698 | F: Fn(JsValue, JsValue, JsValue, JsValue, JsValue, JsValue, JsValue) -> JsValue + 'static, 699 | >( 700 | mut self, 701 | closure: F, 702 | ) -> Self { 703 | let js_closure = wasm_bindgen::closure::Closure::wrap(Box::new(closure) 704 | as Box< 705 | dyn Fn(JsValue, JsValue, JsValue, JsValue, JsValue, JsValue, JsValue) -> JsValue, 706 | >); 707 | let js_sys_fn: &js_sys::Function = js_closure.as_ref().unchecked_ref(); 708 | 709 | let js_window = gloo_utils::window(); 710 | let id = uuid::Uuid::new_v4().to_string(); 711 | Reflect::set(&js_window, &JsValue::from_str(&id), js_sys_fn).unwrap(); 712 | js_closure.forget(); 713 | 714 | gloo_console::debug!(format!( 715 | "Closure at {}:{}:{} set at window.['{id}'].", 716 | file!(), 717 | line!(), 718 | column!() 719 | )); 720 | self.closure_id = Some(id); 721 | self 722 | } 723 | } 724 | 725 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] 726 | #[serde(untagged)] 727 | pub enum FnWithArgsOrT { 728 | T(T), 729 | FnWithArgs(FnWithArgs), 730 | } 731 | 732 | impl Deserialize<'a>> FnWithArgsOrT { 733 | pub fn rationalise_1_level(obj: &JsValue, name: &'static str) { 734 | super::rationalise_1_level::(obj, name, |o| match o { 735 | FnWithArgsOrT::T(_) => (), 736 | FnWithArgsOrT::FnWithArgs(fnwa) => { 737 | let _ = Reflect::set(obj, &name.into(), &fnwa.build()); 738 | } 739 | }) 740 | } 741 | pub fn rationalise_2_levels(obj: &JsValue, name: (&'static str, &'static str)) { 742 | super::rationalise_2_levels::(obj, name, |a, o| match o { 743 | FnWithArgsOrT::T(_) => (), 744 | FnWithArgsOrT::FnWithArgs(fnwa) => { 745 | let _ = Reflect::set(&a, &name.1.into(), &fnwa.build()); 746 | } 747 | }) 748 | } 749 | } 750 | #[allow(private_bounds)] 751 | impl FnWithArgsOrT { 752 | pub fn is_empty(&self) -> bool { 753 | match self { 754 | FnWithArgsOrT::T(a) => a.is_empty(), 755 | FnWithArgsOrT::FnWithArgs(fnwa) => fnwa.is_empty(), 756 | } 757 | } 758 | } 759 | impl Default for FnWithArgsOrT { 760 | fn default() -> Self { 761 | FnWithArgsOrT::T(T::default()) 762 | } 763 | } 764 | impl> From for FnWithArgsOrT { 765 | fn from(s: T) -> Self { 766 | Self::T(s.into()) 767 | } 768 | } 769 | impl> From for FnWithArgsOrT { 770 | fn from(ns: T) -> Self { 771 | Self::T(ns.into()) 772 | } 773 | } 774 | impl> From for FnWithArgsOrT { 775 | fn from(bs: T) -> Self { 776 | Self::T(bs.into()) 777 | } 778 | } 779 | impl From> for FnWithArgsOrT { 780 | fn from(value: FnWithArgs) -> Self { 781 | Self::FnWithArgs(value) 782 | } 783 | } 784 | 785 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 786 | pub struct NumberString(String); 787 | impl From for NumberString { 788 | fn from(value: NumberOrDateString) -> Self { 789 | value.0.into() 790 | } 791 | } 792 | impl NumberString { 793 | pub fn is_empty(&self) -> bool { 794 | self.0.is_empty() 795 | } 796 | } 797 | impl ChartJsRsObject for NumberString { 798 | fn is_empty(&self) -> bool { 799 | self.is_empty() 800 | } 801 | } 802 | impl PartialOrd for NumberString { 803 | fn partial_cmp(&self, other: &Self) -> Option { 804 | Some(self.cmp(other)) 805 | } 806 | } 807 | impl Ord for NumberString { 808 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 809 | if let Some((s, o)) = self 810 | .0 811 | .parse::() 812 | .ok() 813 | .zip(other.0.parse::().ok()) 814 | { 815 | s.cmp(&o) 816 | } else { 817 | self.0.cmp(&other.0) 818 | } 819 | } 820 | } 821 | impl From for NumberString { 822 | fn from(s: T) -> Self { 823 | Self(s.to_string()) 824 | } 825 | } 826 | #[allow(clippy::to_string_trait_impl)] 827 | impl ToString for NumberString { 828 | fn to_string(&self) -> String { 829 | self.0.to_string() 830 | } 831 | } 832 | impl Serialize for NumberString { 833 | fn serialize(&self, serializer: S) -> Result 834 | where 835 | S: serde::Serializer, 836 | { 837 | let fnum: Result = self.0.parse(); 838 | let inum: Result = self.0.parse(); 839 | match (fnum, inum) { 840 | (Ok(_), Ok(inum)) => serializer.serialize_i64(inum), 841 | (Ok(fnum), _) => serializer.serialize_f64(fnum), 842 | _ => serializer.serialize_str(&self.0), 843 | } 844 | } 845 | } 846 | impl<'de> Deserialize<'de> for NumberString { 847 | fn deserialize(deserializer: D) -> Result 848 | where 849 | D: serde::Deserializer<'de>, 850 | { 851 | Any::deserialize(deserializer).map(|soi| Self(soi.to_string())) 852 | } 853 | } 854 | 855 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] 856 | #[serde(untagged)] 857 | pub enum NumberStringOrT { 858 | T(T), 859 | NumberString(NumberString), 860 | } 861 | impl<'de, T: Serialize + DeserializeOwned> Deserialize<'de> for NumberStringOrT { 862 | fn deserialize(deserializer: D) -> Result 863 | where 864 | D: de::Deserializer<'de>, 865 | { 866 | let value = serde_json::Value::deserialize(deserializer)?; 867 | 868 | match serde_json::from_value::(value.clone()) { 869 | Ok(ns) => Ok(Self::NumberString(ns)), 870 | Err(_) => serde_json::from_value::(value) 871 | .map(Self::T) 872 | .map_err(de::Error::custom), 873 | } 874 | } 875 | } 876 | impl NumberStringOrT { 877 | pub fn is_empty(&self) -> bool { 878 | match self { 879 | NumberStringOrT::T(_t) => false, 880 | NumberStringOrT::NumberString(ns) => ns.is_empty(), 881 | } 882 | } 883 | } 884 | 885 | impl From 886 | for NumberStringOrT 887 | { 888 | fn from(value: T) -> Self { 889 | serde_json::from_value(serde_json::to_value(value).unwrap()).unwrap() 890 | } 891 | } 892 | -------------------------------------------------------------------------------- /examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | use chart_js_rs::{bar::Bar, doughnut::Doughnut, pie::Pie, scatter::Scatter, *}; 2 | use dominator::{events, html, Dom}; 3 | use futures_signals::signal::{Mutable, Signal, SignalExt}; 4 | use itertools::Itertools; 5 | use rand::{Rng, SeedableRng}; 6 | use std::{collections::BTreeMap, sync::Arc}; 7 | use utils::*; 8 | use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; 9 | use wasm_bindgen_futures::spawn_local; 10 | 11 | mod utils; 12 | 13 | const WORKER_IMPORTS: &str = include_str!("../worker_imports.js"); 14 | 15 | fn limit() -> usize { 16 | if gloo::utils::window() 17 | .location() 18 | .search() 19 | .unwrap_or_default() 20 | .contains("async") 21 | { 22 | 100 23 | } else { 24 | 20 25 | } 26 | } 27 | 28 | fn random() -> Vec { 29 | let rnd = (0..=limit()).map(|_| { 30 | let mut buf: [u8; 32] = Default::default(); 31 | getrandom::getrandom(&mut buf).unwrap(); 32 | let mut rng = rand::prelude::StdRng::from_seed(buf); 33 | 34 | rng.random_range(1..50) 35 | }); 36 | 37 | rnd.collect() 38 | } 39 | 40 | #[derive(Debug, Clone)] 41 | pub struct Model { 42 | tick: Mutable, 43 | chart: Mutable>, 44 | x: Mutable>>, 45 | y1: Mutable>>, 46 | y2: Mutable>>, 47 | } 48 | impl Model { 49 | fn init() -> Arc { 50 | let query_string = gloo::utils::window() 51 | .location() 52 | .search() 53 | .unwrap_or_default() 54 | .replace('?', ""); 55 | let query = query_string 56 | .split('=') 57 | .tuples::<(&str, &str)>() 58 | .collect::>(); 59 | 60 | let chart = query.get("chart").cloned().unwrap_or("scatter"); 61 | Arc::new(Model { 62 | tick: Mutable::default(), 63 | x: Mutable::new(Arc::new((0..=limit()).collect())), 64 | chart: Mutable::new(Arc::from(chart)), 65 | y1: Mutable::new(Arc::new(random())), 66 | y2: Mutable::new(Arc::new(random())), 67 | }) 68 | } 69 | 70 | fn set_query(self: Arc) { 71 | gloo::utils::window() 72 | .location() 73 | .set_search(&format!("chart={}", self.chart.get_cloned())) 74 | .unwrap(); 75 | } 76 | 77 | fn chart_selected(self: Arc, chart: &'static str) -> impl Signal { 78 | self.chart.signal_cloned().map(move |c| c.as_ref() == chart) 79 | } 80 | fn chart_not_selected(self: Arc, chart: &'static str) -> impl Signal { 81 | self.chart.signal_cloned().map(move |c| c.as_ref() != chart) 82 | } 83 | 84 | fn show_charts(self: Arc) -> impl Signal> { 85 | Mutable4::new( 86 | self.chart.clone(), 87 | self.x.clone(), 88 | self.y1.clone(), 89 | self.y2.clone(), 90 | ) 91 | .map(move |(c, x, y1, y2)| match c.to_string().as_str() { 92 | "scatter" => Some(self.clone().show_scatter( 93 | x.as_slice(), 94 | y1.as_slice(), 95 | y2.as_slice(), 96 | )), 97 | "bar" => Some(self.clone().show_bar(y1.as_slice())), 98 | "donut" => Some(self.clone().show_donut()), 99 | "line" => Some( 100 | self.clone() 101 | .show_line(x.as_slice(), y1.as_slice(), y2.as_slice()), 102 | ), 103 | "line-async" => Some(self.clone().show_line_async( 104 | x.iter().as_slice(), 105 | y1.as_slice(), 106 | y2.as_slice(), 107 | )), 108 | _ => None, 109 | }) 110 | } 111 | 112 | fn show_scatter(self: Arc, x: &[usize], y1: &[usize], y2: &[usize]) -> Dom { 113 | // construct and render chart here 114 | let id = "scatter"; 115 | 116 | let chart = Scatter::new(id) 117 | // we use here to type hint for the compiler 118 | .data( 119 | Dataset::new().datasets([ 120 | XYDataset::new() 121 | .data(x.iter().zip(y1).into_data_iter().unsorted_to_dataset_data()) // collect into dataset 122 | .border_color("red") 123 | .background_color("lightcoral") 124 | .point_radius(4) 125 | .label("Dataset 1"), 126 | XYDataset::new() 127 | .data(x.iter().zip(y2).into_data_iter().unsorted_to_dataset_data()) // collect into dataset 128 | .border_color("blue") 129 | .background_color("lightskyblue") 130 | .point_radius(4) 131 | .label("Dataset 2"), 132 | ]), 133 | ) 134 | .options(ChartOptions::new().maintain_aspect_ratio(false)); 135 | html!("canvas", { // construct a html canvas element, and after its rendered into the DOM we can insert our chart 136 | .prop("id", id) 137 | .style("height", "calc(100vh - 270px)") 138 | .style("width", "100%") 139 | .after_inserted(move |_| { 140 | chart.into_chart().mutate().render(); 141 | }) 142 | }) 143 | } 144 | 145 | fn show_line(self: Arc, x: &[usize], y1: &[usize], y2: &[usize]) -> Dom { 146 | // construct and render chart here 147 | let id = "line"; 148 | 149 | let chart = Scatter::new(id) 150 | // we use here to type hint for the compiler 151 | .data( 152 | Dataset::new().datasets([ 153 | XYDataset::new() 154 | .data( 155 | x.iter() 156 | .zip(y1) 157 | .enumerate() 158 | .map(|(x, d)| { 159 | if x % 5 == 0 { 160 | ("NaN".to_string(), "NaN".to_string()) 161 | } else { 162 | (d.0.to_string(), d.1.to_string()) 163 | } 164 | }) 165 | .into_data_iter() 166 | .unsorted_to_dataset_data(), // collect into dataset 167 | ) 168 | .span_gaps(true) 169 | .point_radius(4) 170 | .point_border_color("darkgreen") 171 | .point_background_color("palegreen") 172 | .label("Dataset 1") 173 | .dataset_type("line") 174 | .segment( 175 | Segment::new() 176 | .border_dash( 177 | // one way is to write your logic in Javascript 178 | FnWithArgs::new() 179 | .args(["ctx"]) 180 | .js_body( 181 | "if (ctx.p0.skip || ctx.p1.skip) { 182 | var out = [2, 2] 183 | } else { 184 | var out = undefined 185 | };", 186 | ) 187 | .js_return_value("out"), 188 | ) 189 | .border_color( 190 | // alternatively you can pass a closure with the same amount of arguments as the FnWithArgs 191 | FnWithArgs::new().args(["ctx"]).rust_closure(|ctx| { 192 | let ctx = uncircle_chartjs_value_to_serde_json_value(ctx) 193 | .unwrap(); 194 | 195 | if ctx["p0"]["skip"].as_bool().unwrap() 196 | || ctx["p1"]["skip"].as_bool().unwrap() 197 | { 198 | "lightgrey" 199 | } else if ctx["p0"]["parsed"]["y"].as_i64() 200 | > ctx["p1"]["parsed"]["y"].as_i64() 201 | { 202 | "firebrick" 203 | } else { 204 | "green" 205 | } 206 | .into() 207 | }), 208 | ), 209 | ), 210 | XYDataset::new() 211 | .data(x.iter().zip(y2).into_data_iter().unsorted_to_dataset_data()) // collect into dataset 212 | .border_color("blue") 213 | .background_color("lightskyblue") 214 | .point_border_color("blue") 215 | .point_background_color("lightskyblue") 216 | .point_radius(4) 217 | .label("Dataset 2") 218 | .dataset_type("line"), 219 | ]), 220 | ) 221 | .options( 222 | ChartOptions::new() 223 | .scales([( 224 | "x", 225 | ChartScale::new().scale_type("linear").ticks( 226 | ScaleTicks::new().callback( 227 | // we can call rust functions in callbacks 228 | FnWithArgs::<3>::new() 229 | // we can override any arguments going in, in this case we must as rust cannot handle `this`. 230 | // Note: if you don't define your variables with ``.args([..])`, they get the default label of the letter of the alphabet they're the index of 231 | // 1st arg: `a` 232 | // 2nd arg: `b` 233 | // ... 234 | .js_body("var a = this.getLabelForValue(a);") 235 | // function pointer goes here - note that the count of arguments must equal the const param (3 in this case) 236 | .run_rust_fn(show_line_ticks), 237 | ), 238 | ), 239 | )]) 240 | .maintain_aspect_ratio(false), 241 | ); 242 | html!("canvas", { // construct a html canvas element, and after its rendered into the DOM we can insert our chart 243 | .prop("id", id) 244 | .style("height", "calc(100vh - 270px)") 245 | .style("width", "100%") 246 | .after_inserted(move |_| { 247 | chart.into_chart().mutate().render(); 248 | }) 249 | }) 250 | } 251 | 252 | fn show_line_async(self: Arc, x: &[usize], y1: &[usize], y2: &[usize]) -> Dom { 253 | // construct and render chart here 254 | let id = "line-async"; 255 | 256 | let chart = Scatter::new(id) 257 | // we use here to type hint for the compiler 258 | .data( 259 | Dataset::new().datasets([ 260 | XYDataset::new() 261 | .data( 262 | x.iter() 263 | .zip(y1) 264 | .enumerate() 265 | .map(|(x, d)| { 266 | if x % 5 == 0 { 267 | ("NaN".to_string(), "NaN".to_string()) 268 | } else { 269 | (d.0.to_string(), d.1.to_string()) 270 | } 271 | }) 272 | .into_data_iter() 273 | .unsorted_to_dataset_data(), // collect into dataset 274 | ) 275 | .span_gaps(true) 276 | .point_radius(4) 277 | .point_border_color("darkgreen") 278 | .point_background_color("palegreen") 279 | .label("Dataset 1") 280 | .dataset_type("line") 281 | .segment( 282 | Segment::new() 283 | .border_dash( 284 | // one way is to write your logic in Javascript 285 | FnWithArgs::new() 286 | .args(["ctx"]) 287 | .js_body( 288 | "if (ctx.p0.skip || ctx.p1.skip) { 289 | var out = [2, 2] 290 | } else { 291 | var out = undefined 292 | };", 293 | ) 294 | .js_return_value("out"), 295 | ) 296 | .border_color( 297 | // alternatively you can pass a closure with the same amount of arguments as the FnWithArgs 298 | FnWithArgs::new().args(["ctx"]).rust_closure(|ctx| { 299 | let ctx = uncircle_chartjs_value_to_serde_json_value(ctx) 300 | .unwrap(); 301 | 302 | if ctx["p0"]["skip"].as_bool().unwrap() 303 | || ctx["p1"]["skip"].as_bool().unwrap() 304 | { 305 | "lightgrey" 306 | } else if ctx["p0"]["parsed"]["y"].as_i64() 307 | > ctx["p1"]["parsed"]["y"].as_i64() 308 | { 309 | "firebrick" 310 | } else { 311 | "green" 312 | } 313 | .into() 314 | }), 315 | ), 316 | ), 317 | XYDataset::new() 318 | .data(x.iter().zip(y2).into_data_iter().unsorted_to_dataset_data()) // collect into dataset 319 | .border_color("blue") 320 | .background_color("lightskyblue") 321 | .point_border_color("blue") 322 | .point_background_color("lightskyblue") 323 | .point_radius(4) 324 | .label("Dataset 2") 325 | .dataset_type("line"), 326 | ]), 327 | ) 328 | .options( 329 | ChartOptions::new() 330 | .scales([( 331 | "x", 332 | ChartScale::new().scale_type("linear").ticks( 333 | ScaleTicks::new().callback( 334 | // we can call rust functions in callbacks 335 | FnWithArgs::<3>::new() 336 | // we can override any arguments going in, in this case we must as rust cannot handle `this`. 337 | // Note: if you don't define your variables with ``.args([..])`, they get the default label of the letter of the alphabet they're the index of 338 | // 1st arg: `a` 339 | // 2nd arg: `b` 340 | // ... 341 | .js_body("var a = this.getLabelForValue(a);") 342 | // function pointer goes here - note that the count of arguments must equal the const param (3 in this case) 343 | .run_rust_fn(show_line_ticks), 344 | ), 345 | ), 346 | )]) 347 | .maintain_aspect_ratio(false), 348 | ); 349 | html!("canvas", { // construct a html canvas element, and after its rendered into the DOM we can insert our chart 350 | .prop("id", id) 351 | .style("height", "calc(100vh - 270px)") 352 | .style("width", "100%") 353 | .after_inserted(move |_| { 354 | spawn_local(async { 355 | gloo::console::log!("Starting render..."); 356 | chart.into_worker_chart(WORKER_IMPORTS).await.unwrap().mutate().render_async().await.unwrap(); 357 | gloo::console::log!("Completed render!"); 358 | }); 359 | }) 360 | }) 361 | } 362 | 363 | fn show_bar(self: Arc, data: &[usize]) -> Dom { 364 | // construct and render chart here 365 | let id = "bar"; 366 | 367 | let chart = Bar::>::new(id) 368 | // we use here to type hint for the compiler 369 | .data( 370 | Dataset::new() 371 | .labels( 372 | // use a range to give us our X axis labels 373 | (0..data.len()).map(|d| d + 1), 374 | ) 375 | .datasets([XYDataset::new() 376 | .data( 377 | data.iter() 378 | .enumerate() 379 | .map(|(x, y)| ((x + 1), y)) 380 | .into_data_iter() 381 | .unsorted_to_dataset_data(), // collect into dataset 382 | ) 383 | .background_color("palegreen") 384 | .border_color("green") 385 | .border_width(2) 386 | .label("Dataset 1") 387 | .y_axis_id("y")]), 388 | ) 389 | .options(ChartOptions::new().maintain_aspect_ratio(false)); 390 | html!("canvas", { // construct a html canvas element, and after its rendered into the DOM we can insert our chart 391 | .prop("id", id) 392 | .style("height", "calc(100vh - 270px)") 393 | .style("width", "100%") 394 | .after_inserted(move |_| { 395 | chart.into_chart().render() // use.to_chart().render_mutate(id) if you wish to run some javascript on this chart, for more detail see bar and index.html 396 | }) 397 | }) 398 | } 399 | 400 | fn show_donut(self: Arc) -> Dom { 401 | // construct and render chart here 402 | let three_a_id = "donut_a"; 403 | let three_b_id = "donut_b"; 404 | 405 | let three_a_chart = Doughnut::new(three_a_id) 406 | .data( 407 | Dataset::new() 408 | .datasets({ 409 | [SinglePointDataset::new() 410 | .data([300, 40, 56, 22]) 411 | .background_color([ 412 | "dodgerblue", 413 | "limegreen", 414 | "firebrick", 415 | "goldenrod", 416 | ])] 417 | }) 418 | .labels(["Blueberries", "Limes", "Apples", "Lemons"]), 419 | ) 420 | .options(ChartOptions::new().maintain_aspect_ratio(false)); 421 | let three_b_chart = Pie::new(three_b_id) 422 | .data( 423 | Dataset::new() 424 | .datasets({ 425 | [SinglePointDataset::new() 426 | .data([300, 40, 56, 22]) 427 | .background_color([ 428 | "dodgerblue", 429 | "limegreen", 430 | "firebrick", 431 | "goldenrod", 432 | ])] 433 | }) 434 | .labels(["Blueberries", "Limes", "Apples", "Lemons"]), 435 | ) 436 | .options(ChartOptions::new().maintain_aspect_ratio(false)); 437 | html!("div", { 438 | .class("columns") 439 | .children([ 440 | html!("div", { 441 | .class(["column", "is-half"]) 442 | .child( 443 | html!("canvas", { 444 | .prop("id", three_a_id) 445 | .style("height", "calc(100vh - 270px)") 446 | .style("width", "100%") 447 | .after_inserted(move |_| { 448 | three_a_chart.into_chart().render() 449 | }) 450 | })) 451 | }), 452 | html!("div", { 453 | .class(["column", "is-half"]) 454 | .child( 455 | html!("canvas", { 456 | .prop("id", three_b_id) 457 | .style("height", "calc(100vh - 270px)") 458 | .style("width", "100%") 459 | .after_inserted(move |_| { 460 | three_b_chart.into_chart().render() 461 | }) 462 | })) 463 | }) 464 | ]) 465 | }) 466 | } 467 | 468 | fn render(self: Arc) -> Dom { 469 | html!("div", { 470 | .class("section") 471 | .child( 472 | html!("div", { 473 | .class(["buttons", "has-addons"]) 474 | .child( 475 | html!("button", { 476 | .class(["button", "is-info"]) 477 | .prop_signal("disabled", self.clone().chart_selected("donut")) 478 | .text("Randomise") 479 | .event({ 480 | let model = self.clone(); 481 | move |_: events::Click| { 482 | // randomise the data on button click 483 | model.clone().y1.set(Arc::new(random())); 484 | model.clone().y2.set(Arc::new(random())); 485 | } 486 | }) 487 | }) 488 | ) 489 | .child( 490 | html!("button", { 491 | .class(["button", "is-primary"]) 492 | .class_signal("is-light", self.clone().chart_not_selected("scatter")) 493 | .text("Scatter") 494 | .event({ 495 | let model = self.clone(); 496 | move |_: events::Click| { 497 | model.clone().chart.set("scatter".into()); // change which chart is in view 498 | model.clone().set_query(); 499 | } 500 | }) 501 | }) 502 | ) 503 | .child( 504 | html!("button", { 505 | .class(["button", "is-success"]) 506 | .class_signal("is-light", self.clone().chart_not_selected("line")) 507 | .text("Line") 508 | .event({ 509 | let model = self.clone(); 510 | move |_: events::Click| { 511 | model.clone().chart.set("line".into()); // change which chart is in view 512 | model.clone().set_query(); 513 | } 514 | }) 515 | }) 516 | ) 517 | .child( 518 | html!("button", { 519 | .class(["button", "is-success"]) 520 | .class_signal("is-light", self.clone().chart_not_selected("line-async")) 521 | .text("Line (Async)") 522 | .event({ 523 | let model = self.clone(); 524 | move |_: events::Click| { 525 | model.clone().chart.set("line-async".into()); // change which chart is in view 526 | model.clone().set_query(); 527 | } 528 | }) 529 | }) 530 | ) 531 | .child( 532 | html!("button", { 533 | .class(["button", "is-primary"]) 534 | .class_signal("is-light", self.clone().chart_not_selected("bar")) 535 | .text("Bar") 536 | .event({ 537 | let model = self.clone(); 538 | move |_: events::Click| { 539 | model.clone().chart.set("bar".into()); // change which chart is in view 540 | model.clone().set_query(); 541 | } 542 | }) 543 | }) 544 | ) 545 | .child( 546 | html!("button", { 547 | .class(["button", "is-success"]) 548 | .class_signal("is-light", self.clone().chart_not_selected("donut")) 549 | .text("Donut") 550 | .event({ 551 | let model = self.clone(); 552 | move |_: events::Click| { 553 | model.clone().chart.set("donut".into()); // change which chart is in view 554 | model.clone().set_query(); 555 | } 556 | }) 557 | }) 558 | ) 559 | .child_signal(self.chart.signal_cloned().map(|c| 560 | if c.as_ref() == "scatter" { 561 | Some(html!("button", { 562 | .class("button") 563 | .prop("disabled", true) 564 | })) 565 | } 566 | else { 567 | None 568 | }) 569 | ) 570 | .child_signal(self.chart.signal_cloned().map({ 571 | let _self = self.clone(); 572 | move |c| 573 | if c.as_ref() == "scatter" { 574 | Some( 575 | html!("button", { 576 | .class(["button", "is-info"]) 577 | .text("Update Chart") 578 | .event({ 579 | let _self = _self.clone(); 580 | move |_: events::Click| { 581 | // update scatter chart colour 582 | let mut chart: Scatter = ChartExt::get_chart_from_id("scatter").expect("Unable to retrieve chart from JS."); 583 | chart.get_data().get_datasets().get_mut(0).map(|d| { 584 | if _self.tick.get() { 585 | *d.get_background_color() = "lightcoral".into(); 586 | *d.get_border_color() = "red".into(); 587 | } else { 588 | *d.get_background_color() = "palegreen".into(); 589 | *d.get_border_color() = "green".into(); 590 | } 591 | }).unwrap(); 592 | chart.into_chart().update(true); 593 | _self.tick.set(!_self.tick.get()); 594 | } 595 | }) 596 | }) 597 | ) 598 | } 599 | else { 600 | None 601 | } 602 | }) 603 | ) 604 | .child_signal(self.chart.signal_cloned().map({ 605 | let _self = self.clone(); 606 | move |c| 607 | if c.as_ref() == "scatter" { 608 | Some( 609 | html!("button", { 610 | .class(["button", "is-info"]) 611 | .text("Update Chart without animation") 612 | .event({ 613 | let _self = _self.clone(); 614 | move |_: events::Click| { 615 | // update scatter chart colour 616 | let mut chart: Scatter = ChartExt::get_chart_from_id("scatter").expect("Unable to retrieve chart from JS."); 617 | chart.get_data().get_datasets().get_mut(0).map(|d| { 618 | if _self.tick.get() { 619 | *d.get_background_color() = "lightcoral".into(); 620 | *d.get_border_color() = "red".into(); 621 | } else { 622 | *d.get_background_color() = "palegreen".into(); 623 | *d.get_border_color() = "green".into(); 624 | } 625 | }).unwrap(); 626 | chart.into_chart().update(false); 627 | _self.tick.set(!_self.tick.get()); 628 | } 629 | }) 630 | }) 631 | ) 632 | } 633 | else { 634 | None 635 | } 636 | }) 637 | ) 638 | }) 639 | ) 640 | .child( 641 | html!("div", { 642 | .class("section") 643 | .child_signal(self.show_charts()) // render charts here, signal allows us to change the chart, see the `Dominator` crate for more 644 | }) 645 | ) 646 | }) 647 | } 648 | } 649 | 650 | #[wasm_bindgen] 651 | pub fn show_line_ticks(this: String, index: u32, _ticks: JsValue) -> String { 652 | if index.is_multiple_of(2) { 653 | this 654 | } else { 655 | String::new() 656 | } 657 | } 658 | 659 | #[wasm_bindgen(start)] 660 | pub fn main_js() -> Result<(), JsValue> { 661 | // this allows the wasm_bindgen to export the functions in the worker but not run any code 662 | if is_worker() { 663 | return Ok(()); 664 | } 665 | 666 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 667 | 668 | let app = Model::init(); 669 | dominator::append_dom(&dominator::body(), Model::render(app)); 670 | Ok(()) 671 | } 672 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.7.8" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" 10 | dependencies = [ 11 | "getrandom 0.2.16", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "arrayvec" 18 | version = "0.7.6" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 21 | 22 | [[package]] 23 | name = "autocfg" 24 | version = "1.5.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 27 | 28 | [[package]] 29 | name = "bincode" 30 | version = "1.3.3" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 33 | dependencies = [ 34 | "serde", 35 | ] 36 | 37 | [[package]] 38 | name = "bitvec" 39 | version = "1.0.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 42 | dependencies = [ 43 | "funty", 44 | "radium", 45 | "tap", 46 | "wyz", 47 | ] 48 | 49 | [[package]] 50 | name = "borsh" 51 | version = "1.6.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" 54 | dependencies = [ 55 | "borsh-derive", 56 | "cfg_aliases", 57 | ] 58 | 59 | [[package]] 60 | name = "borsh-derive" 61 | version = "1.6.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" 64 | dependencies = [ 65 | "once_cell", 66 | "proc-macro-crate 3.4.0", 67 | "proc-macro2", 68 | "quote", 69 | "syn 2.0.111", 70 | ] 71 | 72 | [[package]] 73 | name = "bumpalo" 74 | version = "3.19.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 77 | 78 | [[package]] 79 | name = "bytecheck" 80 | version = "0.6.12" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" 83 | dependencies = [ 84 | "bytecheck_derive", 85 | "ptr_meta", 86 | "simdutf8", 87 | ] 88 | 89 | [[package]] 90 | name = "bytecheck_derive" 91 | version = "0.6.12" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" 94 | dependencies = [ 95 | "proc-macro2", 96 | "quote", 97 | "syn 1.0.109", 98 | ] 99 | 100 | [[package]] 101 | name = "bytes" 102 | version = "1.11.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 105 | 106 | [[package]] 107 | name = "cfg-if" 108 | version = "1.0.4" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 111 | 112 | [[package]] 113 | name = "cfg_aliases" 114 | version = "0.2.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 117 | 118 | [[package]] 119 | name = "chart-js-rs" 120 | version = "0.1.5" 121 | dependencies = [ 122 | "gloo-console", 123 | "gloo-utils", 124 | "heck", 125 | "itertools", 126 | "js-sys", 127 | "proc-macro2", 128 | "quote", 129 | "rust_decimal", 130 | "serde", 131 | "serde-wasm-bindgen", 132 | "serde_json", 133 | "syn 2.0.111", 134 | "tokio", 135 | "uuid", 136 | "wasm-bindgen", 137 | "web-sys", 138 | ] 139 | 140 | [[package]] 141 | name = "chart-js-rs-example" 142 | version = "0.1.2" 143 | dependencies = [ 144 | "chart-js-rs", 145 | "console_error_panic_hook", 146 | "dominator", 147 | "futures-signals", 148 | "getrandom 0.2.16", 149 | "gloo", 150 | "itertools", 151 | "js-sys", 152 | "rand 0.9.2", 153 | "serde_json", 154 | "wasm-bindgen", 155 | "wasm-bindgen-futures", 156 | "web-sys", 157 | ] 158 | 159 | [[package]] 160 | name = "console_error_panic_hook" 161 | version = "0.1.7" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 164 | dependencies = [ 165 | "cfg-if", 166 | "wasm-bindgen", 167 | ] 168 | 169 | [[package]] 170 | name = "discard" 171 | version = "1.0.4" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" 174 | 175 | [[package]] 176 | name = "dominator" 177 | version = "0.5.38" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "40348e50c3b38c0d76bc116e7e02eb98ef5dd3536d79d9411acb07663f10ec9d" 180 | dependencies = [ 181 | "discard", 182 | "futures-channel", 183 | "futures-signals", 184 | "futures-util", 185 | "gloo-events 0.1.2", 186 | "js-sys", 187 | "once_cell", 188 | "pin-project", 189 | "wasm-bindgen", 190 | "wasm-bindgen-futures", 191 | "web-sys", 192 | ] 193 | 194 | [[package]] 195 | name = "either" 196 | version = "1.15.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 199 | 200 | [[package]] 201 | name = "equivalent" 202 | version = "1.0.2" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 205 | 206 | [[package]] 207 | name = "fnv" 208 | version = "1.0.7" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 211 | 212 | [[package]] 213 | name = "form_urlencoded" 214 | version = "1.2.2" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 217 | dependencies = [ 218 | "percent-encoding", 219 | ] 220 | 221 | [[package]] 222 | name = "funty" 223 | version = "2.0.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 226 | 227 | [[package]] 228 | name = "futures" 229 | version = "0.3.31" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 232 | dependencies = [ 233 | "futures-channel", 234 | "futures-core", 235 | "futures-io", 236 | "futures-sink", 237 | "futures-task", 238 | "futures-util", 239 | ] 240 | 241 | [[package]] 242 | name = "futures-channel" 243 | version = "0.3.31" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 246 | dependencies = [ 247 | "futures-core", 248 | "futures-sink", 249 | ] 250 | 251 | [[package]] 252 | name = "futures-core" 253 | version = "0.3.31" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 256 | 257 | [[package]] 258 | name = "futures-io" 259 | version = "0.3.31" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 262 | 263 | [[package]] 264 | name = "futures-macro" 265 | version = "0.3.31" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 268 | dependencies = [ 269 | "proc-macro2", 270 | "quote", 271 | "syn 2.0.111", 272 | ] 273 | 274 | [[package]] 275 | name = "futures-signals" 276 | version = "0.3.34" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "70abe9c40a0dccd69bf7c59ba58714ebeb6c15a88143a10c6be7130e895f1696" 279 | dependencies = [ 280 | "discard", 281 | "futures-channel", 282 | "futures-core", 283 | "futures-util", 284 | "gensym", 285 | "log", 286 | "pin-project", 287 | "serde", 288 | ] 289 | 290 | [[package]] 291 | name = "futures-sink" 292 | version = "0.3.31" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 295 | 296 | [[package]] 297 | name = "futures-task" 298 | version = "0.3.31" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 301 | 302 | [[package]] 303 | name = "futures-util" 304 | version = "0.3.31" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 307 | dependencies = [ 308 | "futures-channel", 309 | "futures-core", 310 | "futures-io", 311 | "futures-macro", 312 | "futures-sink", 313 | "futures-task", 314 | "memchr", 315 | "pin-project-lite", 316 | "pin-utils", 317 | "slab", 318 | ] 319 | 320 | [[package]] 321 | name = "gensym" 322 | version = "0.1.1" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "913dce4c5f06c2ea40fc178c06f777ac89fc6b1383e90c254fafb1abe4ba3c82" 325 | dependencies = [ 326 | "proc-macro2", 327 | "quote", 328 | "syn 2.0.111", 329 | "uuid", 330 | ] 331 | 332 | [[package]] 333 | name = "getrandom" 334 | version = "0.2.16" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 337 | dependencies = [ 338 | "cfg-if", 339 | "js-sys", 340 | "libc", 341 | "wasi", 342 | "wasm-bindgen", 343 | ] 344 | 345 | [[package]] 346 | name = "getrandom" 347 | version = "0.3.4" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 350 | dependencies = [ 351 | "cfg-if", 352 | "libc", 353 | "r-efi", 354 | "wasip2", 355 | ] 356 | 357 | [[package]] 358 | name = "gloo" 359 | version = "0.11.0" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" 362 | dependencies = [ 363 | "gloo-console", 364 | "gloo-dialogs", 365 | "gloo-events 0.2.0", 366 | "gloo-file", 367 | "gloo-history", 368 | "gloo-net", 369 | "gloo-render", 370 | "gloo-storage", 371 | "gloo-timers", 372 | "gloo-utils", 373 | "gloo-worker", 374 | ] 375 | 376 | [[package]] 377 | name = "gloo-console" 378 | version = "0.3.0" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" 381 | dependencies = [ 382 | "gloo-utils", 383 | "js-sys", 384 | "serde", 385 | "wasm-bindgen", 386 | "web-sys", 387 | ] 388 | 389 | [[package]] 390 | name = "gloo-dialogs" 391 | version = "0.2.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" 394 | dependencies = [ 395 | "wasm-bindgen", 396 | "web-sys", 397 | ] 398 | 399 | [[package]] 400 | name = "gloo-events" 401 | version = "0.1.2" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" 404 | dependencies = [ 405 | "wasm-bindgen", 406 | "web-sys", 407 | ] 408 | 409 | [[package]] 410 | name = "gloo-events" 411 | version = "0.2.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" 414 | dependencies = [ 415 | "wasm-bindgen", 416 | "web-sys", 417 | ] 418 | 419 | [[package]] 420 | name = "gloo-file" 421 | version = "0.3.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" 424 | dependencies = [ 425 | "gloo-events 0.2.0", 426 | "js-sys", 427 | "wasm-bindgen", 428 | "web-sys", 429 | ] 430 | 431 | [[package]] 432 | name = "gloo-history" 433 | version = "0.2.2" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" 436 | dependencies = [ 437 | "getrandom 0.2.16", 438 | "gloo-events 0.2.0", 439 | "gloo-utils", 440 | "serde", 441 | "serde-wasm-bindgen", 442 | "serde_urlencoded", 443 | "thiserror", 444 | "wasm-bindgen", 445 | "web-sys", 446 | ] 447 | 448 | [[package]] 449 | name = "gloo-net" 450 | version = "0.5.0" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" 453 | dependencies = [ 454 | "futures-channel", 455 | "futures-core", 456 | "futures-sink", 457 | "gloo-utils", 458 | "http", 459 | "js-sys", 460 | "pin-project", 461 | "serde", 462 | "serde_json", 463 | "thiserror", 464 | "wasm-bindgen", 465 | "wasm-bindgen-futures", 466 | "web-sys", 467 | ] 468 | 469 | [[package]] 470 | name = "gloo-render" 471 | version = "0.2.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" 474 | dependencies = [ 475 | "wasm-bindgen", 476 | "web-sys", 477 | ] 478 | 479 | [[package]] 480 | name = "gloo-storage" 481 | version = "0.3.0" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" 484 | dependencies = [ 485 | "gloo-utils", 486 | "js-sys", 487 | "serde", 488 | "serde_json", 489 | "thiserror", 490 | "wasm-bindgen", 491 | "web-sys", 492 | ] 493 | 494 | [[package]] 495 | name = "gloo-timers" 496 | version = "0.3.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" 499 | dependencies = [ 500 | "js-sys", 501 | "wasm-bindgen", 502 | ] 503 | 504 | [[package]] 505 | name = "gloo-utils" 506 | version = "0.2.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" 509 | dependencies = [ 510 | "js-sys", 511 | "serde", 512 | "serde_json", 513 | "wasm-bindgen", 514 | "web-sys", 515 | ] 516 | 517 | [[package]] 518 | name = "gloo-worker" 519 | version = "0.5.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" 522 | dependencies = [ 523 | "bincode", 524 | "futures", 525 | "gloo-utils", 526 | "gloo-worker-macros", 527 | "js-sys", 528 | "pinned", 529 | "serde", 530 | "thiserror", 531 | "wasm-bindgen", 532 | "wasm-bindgen-futures", 533 | "web-sys", 534 | ] 535 | 536 | [[package]] 537 | name = "gloo-worker-macros" 538 | version = "0.1.0" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" 541 | dependencies = [ 542 | "proc-macro-crate 1.3.1", 543 | "proc-macro2", 544 | "quote", 545 | "syn 2.0.111", 546 | ] 547 | 548 | [[package]] 549 | name = "hashbrown" 550 | version = "0.12.3" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 553 | dependencies = [ 554 | "ahash", 555 | ] 556 | 557 | [[package]] 558 | name = "hashbrown" 559 | version = "0.16.1" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 562 | 563 | [[package]] 564 | name = "heck" 565 | version = "0.5.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 568 | 569 | [[package]] 570 | name = "http" 571 | version = "0.2.12" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 574 | dependencies = [ 575 | "bytes", 576 | "fnv", 577 | "itoa", 578 | ] 579 | 580 | [[package]] 581 | name = "indexmap" 582 | version = "2.12.1" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 585 | dependencies = [ 586 | "equivalent", 587 | "hashbrown 0.16.1", 588 | ] 589 | 590 | [[package]] 591 | name = "itertools" 592 | version = "0.14.0" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 595 | dependencies = [ 596 | "either", 597 | ] 598 | 599 | [[package]] 600 | name = "itoa" 601 | version = "1.0.15" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 604 | 605 | [[package]] 606 | name = "js-sys" 607 | version = "0.3.83" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" 610 | dependencies = [ 611 | "once_cell", 612 | "wasm-bindgen", 613 | ] 614 | 615 | [[package]] 616 | name = "libc" 617 | version = "0.2.177" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 620 | 621 | [[package]] 622 | name = "log" 623 | version = "0.4.28" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 626 | 627 | [[package]] 628 | name = "memchr" 629 | version = "2.7.6" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 632 | 633 | [[package]] 634 | name = "num-traits" 635 | version = "0.2.19" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 638 | dependencies = [ 639 | "autocfg", 640 | ] 641 | 642 | [[package]] 643 | name = "once_cell" 644 | version = "1.21.3" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 647 | 648 | [[package]] 649 | name = "percent-encoding" 650 | version = "2.3.2" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 653 | 654 | [[package]] 655 | name = "pin-project" 656 | version = "1.1.10" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 659 | dependencies = [ 660 | "pin-project-internal", 661 | ] 662 | 663 | [[package]] 664 | name = "pin-project-internal" 665 | version = "1.1.10" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 668 | dependencies = [ 669 | "proc-macro2", 670 | "quote", 671 | "syn 2.0.111", 672 | ] 673 | 674 | [[package]] 675 | name = "pin-project-lite" 676 | version = "0.2.16" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 679 | 680 | [[package]] 681 | name = "pin-utils" 682 | version = "0.1.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 685 | 686 | [[package]] 687 | name = "pinned" 688 | version = "0.1.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" 691 | dependencies = [ 692 | "futures", 693 | "rustversion", 694 | "thiserror", 695 | ] 696 | 697 | [[package]] 698 | name = "ppv-lite86" 699 | version = "0.2.21" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 702 | dependencies = [ 703 | "zerocopy", 704 | ] 705 | 706 | [[package]] 707 | name = "proc-macro-crate" 708 | version = "1.3.1" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" 711 | dependencies = [ 712 | "once_cell", 713 | "toml_edit 0.19.15", 714 | ] 715 | 716 | [[package]] 717 | name = "proc-macro-crate" 718 | version = "3.4.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" 721 | dependencies = [ 722 | "toml_edit 0.23.7", 723 | ] 724 | 725 | [[package]] 726 | name = "proc-macro2" 727 | version = "1.0.103" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 730 | dependencies = [ 731 | "unicode-ident", 732 | ] 733 | 734 | [[package]] 735 | name = "ptr_meta" 736 | version = "0.1.4" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" 739 | dependencies = [ 740 | "ptr_meta_derive", 741 | ] 742 | 743 | [[package]] 744 | name = "ptr_meta_derive" 745 | version = "0.1.4" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" 748 | dependencies = [ 749 | "proc-macro2", 750 | "quote", 751 | "syn 1.0.109", 752 | ] 753 | 754 | [[package]] 755 | name = "quote" 756 | version = "1.0.42" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 759 | dependencies = [ 760 | "proc-macro2", 761 | ] 762 | 763 | [[package]] 764 | name = "r-efi" 765 | version = "5.3.0" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 768 | 769 | [[package]] 770 | name = "radium" 771 | version = "0.7.0" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 774 | 775 | [[package]] 776 | name = "rand" 777 | version = "0.8.5" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 780 | dependencies = [ 781 | "libc", 782 | "rand_chacha 0.3.1", 783 | "rand_core 0.6.4", 784 | ] 785 | 786 | [[package]] 787 | name = "rand" 788 | version = "0.9.2" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 791 | dependencies = [ 792 | "rand_chacha 0.9.0", 793 | "rand_core 0.9.3", 794 | ] 795 | 796 | [[package]] 797 | name = "rand_chacha" 798 | version = "0.3.1" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 801 | dependencies = [ 802 | "ppv-lite86", 803 | "rand_core 0.6.4", 804 | ] 805 | 806 | [[package]] 807 | name = "rand_chacha" 808 | version = "0.9.0" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 811 | dependencies = [ 812 | "ppv-lite86", 813 | "rand_core 0.9.3", 814 | ] 815 | 816 | [[package]] 817 | name = "rand_core" 818 | version = "0.6.4" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 821 | dependencies = [ 822 | "getrandom 0.2.16", 823 | ] 824 | 825 | [[package]] 826 | name = "rand_core" 827 | version = "0.9.3" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 830 | 831 | [[package]] 832 | name = "rend" 833 | version = "0.4.2" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" 836 | dependencies = [ 837 | "bytecheck", 838 | ] 839 | 840 | [[package]] 841 | name = "rkyv" 842 | version = "0.7.45" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" 845 | dependencies = [ 846 | "bitvec", 847 | "bytecheck", 848 | "bytes", 849 | "hashbrown 0.12.3", 850 | "ptr_meta", 851 | "rend", 852 | "rkyv_derive", 853 | "seahash", 854 | "tinyvec", 855 | "uuid", 856 | ] 857 | 858 | [[package]] 859 | name = "rkyv_derive" 860 | version = "0.7.45" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" 863 | dependencies = [ 864 | "proc-macro2", 865 | "quote", 866 | "syn 1.0.109", 867 | ] 868 | 869 | [[package]] 870 | name = "rust_decimal" 871 | version = "1.39.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" 874 | dependencies = [ 875 | "arrayvec", 876 | "borsh", 877 | "bytes", 878 | "num-traits", 879 | "rand 0.8.5", 880 | "rkyv", 881 | "serde", 882 | "serde_json", 883 | ] 884 | 885 | [[package]] 886 | name = "rustversion" 887 | version = "1.0.22" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 890 | 891 | [[package]] 892 | name = "ryu" 893 | version = "1.0.20" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 896 | 897 | [[package]] 898 | name = "seahash" 899 | version = "4.1.0" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" 902 | 903 | [[package]] 904 | name = "serde" 905 | version = "1.0.228" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 908 | dependencies = [ 909 | "serde_core", 910 | "serde_derive", 911 | ] 912 | 913 | [[package]] 914 | name = "serde-wasm-bindgen" 915 | version = "0.6.5" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" 918 | dependencies = [ 919 | "js-sys", 920 | "serde", 921 | "wasm-bindgen", 922 | ] 923 | 924 | [[package]] 925 | name = "serde_core" 926 | version = "1.0.228" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 929 | dependencies = [ 930 | "serde_derive", 931 | ] 932 | 933 | [[package]] 934 | name = "serde_derive" 935 | version = "1.0.228" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 938 | dependencies = [ 939 | "proc-macro2", 940 | "quote", 941 | "syn 2.0.111", 942 | ] 943 | 944 | [[package]] 945 | name = "serde_json" 946 | version = "1.0.145" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 949 | dependencies = [ 950 | "itoa", 951 | "memchr", 952 | "ryu", 953 | "serde", 954 | "serde_core", 955 | ] 956 | 957 | [[package]] 958 | name = "serde_urlencoded" 959 | version = "0.7.1" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 962 | dependencies = [ 963 | "form_urlencoded", 964 | "itoa", 965 | "ryu", 966 | "serde", 967 | ] 968 | 969 | [[package]] 970 | name = "simdutf8" 971 | version = "0.1.5" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" 974 | 975 | [[package]] 976 | name = "slab" 977 | version = "0.4.11" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 980 | 981 | [[package]] 982 | name = "syn" 983 | version = "1.0.109" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 986 | dependencies = [ 987 | "proc-macro2", 988 | "quote", 989 | "unicode-ident", 990 | ] 991 | 992 | [[package]] 993 | name = "syn" 994 | version = "2.0.111" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 997 | dependencies = [ 998 | "proc-macro2", 999 | "quote", 1000 | "unicode-ident", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "tap" 1005 | version = "1.0.1" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 1008 | 1009 | [[package]] 1010 | name = "thiserror" 1011 | version = "1.0.69" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1014 | dependencies = [ 1015 | "thiserror-impl", 1016 | ] 1017 | 1018 | [[package]] 1019 | name = "thiserror-impl" 1020 | version = "1.0.69" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1023 | dependencies = [ 1024 | "proc-macro2", 1025 | "quote", 1026 | "syn 2.0.111", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "tinyvec" 1031 | version = "1.10.0" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 1034 | dependencies = [ 1035 | "tinyvec_macros", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "tinyvec_macros" 1040 | version = "0.1.1" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1043 | 1044 | [[package]] 1045 | name = "tokio" 1046 | version = "1.48.0" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 1049 | dependencies = [ 1050 | "pin-project-lite", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "toml_datetime" 1055 | version = "0.6.11" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 1058 | 1059 | [[package]] 1060 | name = "toml_datetime" 1061 | version = "0.7.3" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" 1064 | dependencies = [ 1065 | "serde_core", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "toml_edit" 1070 | version = "0.19.15" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 1073 | dependencies = [ 1074 | "indexmap", 1075 | "toml_datetime 0.6.11", 1076 | "winnow 0.5.40", 1077 | ] 1078 | 1079 | [[package]] 1080 | name = "toml_edit" 1081 | version = "0.23.7" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" 1084 | dependencies = [ 1085 | "indexmap", 1086 | "toml_datetime 0.7.3", 1087 | "toml_parser", 1088 | "winnow 0.7.14", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "toml_parser" 1093 | version = "1.0.4" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" 1096 | dependencies = [ 1097 | "winnow 0.7.14", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "unicode-ident" 1102 | version = "1.0.22" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 1105 | 1106 | [[package]] 1107 | name = "uuid" 1108 | version = "1.19.0" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" 1111 | dependencies = [ 1112 | "getrandom 0.3.4", 1113 | "js-sys", 1114 | "wasm-bindgen", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "version_check" 1119 | version = "0.9.5" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1122 | 1123 | [[package]] 1124 | name = "wasi" 1125 | version = "0.11.1+wasi-snapshot-preview1" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1128 | 1129 | [[package]] 1130 | name = "wasip2" 1131 | version = "1.0.1+wasi-0.2.4" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 1134 | dependencies = [ 1135 | "wit-bindgen", 1136 | ] 1137 | 1138 | [[package]] 1139 | name = "wasm-bindgen" 1140 | version = "0.2.106" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" 1143 | dependencies = [ 1144 | "cfg-if", 1145 | "once_cell", 1146 | "rustversion", 1147 | "serde", 1148 | "serde_json", 1149 | "wasm-bindgen-macro", 1150 | "wasm-bindgen-shared", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "wasm-bindgen-futures" 1155 | version = "0.4.56" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" 1158 | dependencies = [ 1159 | "cfg-if", 1160 | "js-sys", 1161 | "once_cell", 1162 | "wasm-bindgen", 1163 | "web-sys", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "wasm-bindgen-macro" 1168 | version = "0.2.106" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" 1171 | dependencies = [ 1172 | "quote", 1173 | "wasm-bindgen-macro-support", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "wasm-bindgen-macro-support" 1178 | version = "0.2.106" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" 1181 | dependencies = [ 1182 | "bumpalo", 1183 | "proc-macro2", 1184 | "quote", 1185 | "syn 2.0.111", 1186 | "wasm-bindgen-shared", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "wasm-bindgen-shared" 1191 | version = "0.2.106" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" 1194 | dependencies = [ 1195 | "unicode-ident", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "web-sys" 1200 | version = "0.3.83" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" 1203 | dependencies = [ 1204 | "js-sys", 1205 | "wasm-bindgen", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "winnow" 1210 | version = "0.5.40" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 1213 | dependencies = [ 1214 | "memchr", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "winnow" 1219 | version = "0.7.14" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" 1222 | dependencies = [ 1223 | "memchr", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "wit-bindgen" 1228 | version = "0.46.0" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 1231 | 1232 | [[package]] 1233 | name = "wyz" 1234 | version = "0.5.1" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 1237 | dependencies = [ 1238 | "tap", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "zerocopy" 1243 | version = "0.8.31" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" 1246 | dependencies = [ 1247 | "zerocopy-derive", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "zerocopy-derive" 1252 | version = "0.8.31" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" 1255 | dependencies = [ 1256 | "proc-macro2", 1257 | "quote", 1258 | "syn 2.0.111", 1259 | ] 1260 | --------------------------------------------------------------------------------