├── 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 |
3 |
4 |
5 | # Chart.js types API in Rust
6 | [](https://crates.io/crates/chart-js-rs)
7 | [](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