chart_js_wrapper/
options.rs

1use std::cmp::PartialEq;
2use std::time::{Instant, SystemTime};
3use crate::common::{Padding, Rgb, Size};
4use crate::options::ChartData::Vector2D;
5use crate::render::Chart;
6use crate::ChartData::{VectorWithRadius, VectorWithText};
7use ndarray::{Array1, Array2};
8use ndarray_linalg::error::LinalgError;
9use ndarray_linalg::LeastSquaresSvd;
10use ndarray_linalg::{Lapack, Scalar};
11use serde::{Deserialize, Serialize};
12use serde::ser::SerializeSeq;
13use uuid::Uuid;
14
15const DISPLAY_FN: &'static str = "
16                        function(context){
17                            context = context[0];
18                            let ttp = context.raw.tooltip || '';
19                            if(ttp) return ttp;
20                        }";
21
22#[derive(Debug, Clone)]
23pub struct ChartConfig<X,Y> {
24    pub data: ChartDataSection<X,Y>,
25    pub options: ChartOptions<X,Y>
26}
27
28impl<X, Y> ChartConfig<X, Y> where X: Serialize, Y: Serialize {
29
30    pub fn new(options: ChartOptions<X,Y>) -> Self {
31        Self {
32            data: ChartDataSection::default(),
33            options
34        }
35    }
36
37    pub fn set_x_axis(mut self, conf: ScaleConfig<X>) -> Self {
38        let mut scales = self.options.scales;
39        if scales.is_none() {
40            scales = Some(ScalingConfig{
41                x: Some(conf),
42                y: None
43            });
44        }else {
45            scales.as_mut().unwrap().x = Some(conf);
46        }
47        self.options.scales = scales;
48        self
49    }
50
51    pub fn set_y_axis(mut self, conf: ScaleConfig<Y>) -> Self {
52        let mut scales = self.options.scales;
53        if scales.is_none() {
54            scales = Some(ScalingConfig{
55                x: None,
56                y: Some(conf)
57            });
58        }else {
59            scales.as_mut().unwrap().y = Some(conf);
60        }
61        self.options.scales = scales;
62        self
63    }
64
65
66    pub fn title_str(mut self, text: String) -> Self {
67        self.options.plugins.title = Some(Title{
68                display: true,
69                full_size: false,
70                text: vec![text],
71                padding: None,
72                position: None,
73        });
74        self
75    }
76
77
78
79    pub fn add_series<T: Into<ChartData<X,Y>>>(mut self, r#type: ChartType, title:String, data: T)->Self{
80        let data = data.into();
81        if let VectorWithText(d) = data {
82            let ttp =self.options.plugins.tooltip.get_or_insert_default();
83            let callbacks =ttp.callbacks.get_or_insert_default();
84            callbacks.title = Some(JsExpr(DISPLAY_FN));
85            self.data.datasets.push(Dataset {
86                r#type,
87                label: title,
88                data: VectorWithText(d),
89                fill: None,
90                border_color: None,
91                background_color: None,
92            });
93        }else {
94            self.data.datasets.push(Dataset {
95                r#type,
96                label: title,
97                data,
98                fill: None,
99                border_color: None,
100                background_color: None,
101            });
102        }
103        self
104    }
105
106
107    pub fn enable_legend(mut self) -> Self{
108        if self.options.plugins.legend.is_none() {
109            self.options.plugins.legend = Some(Legend {
110                display: true,
111                full_size: false,
112                position: None,
113                align: None
114            });
115        }else {
116            self.options.plugins.legend.as_mut().unwrap().display = true;
117        }
118        self
119    }
120    
121    pub fn build(self, width: Size, height: Size) -> Chart<X,Y>{
122        Chart::new(Uuid::new_v4().to_string(), width, height, self)
123    }
124    
125}
126
127
128
129impl<X> ChartConfig<X, X> where  X:Serialize + Scalar + Lapack + Clone + Into<f64> {
130    
131    pub fn add_linear_regression_series<T: Into<ChartData<X,X>>>(self, title: &str, data: T) -> Result<Self, LinalgError> {
132        let data:Vec<(X,X)>  = data.into().into();
133        let n = data.len();
134        let config_with_scatter = self.add_series(ChartType::Scatter, title.to_string(), data.clone());
135        
136        // Allocate design matrix with shape (n, 2)
137        let mut x_matrix = Array2::<X>::zeros((n, 2));
138        let mut y_array = Array1::<X>::zeros(n);
139
140        for (i, (x, y)) in data.into_iter().enumerate() {
141            x_matrix[[i, 0]] = X::one(); // intercept term
142            x_matrix[[i, 1]] = x;
143            y_array[i] = y;
144        }
145        
146        let beta = x_matrix.least_squares(&y_array)?.solution;
147
148        let y_pred = x_matrix.dot(&beta);
149        let r2 = Self::r_squared(&y_array, &y_pred);
150        let mut reg_data = Vec::<(X,X)>::with_capacity(n);
151        
152        y_pred.into_iter().enumerate().for_each(|(i, x)| {
153            reg_data.push((x_matrix[[i,1]],x));
154        });
155        
156        let config_with_both_charts = 
157            config_with_scatter.add_series(ChartType::Line, format!("{} regression(R^2 = {:.4})", title, r2), reg_data);
158        
159        Ok(config_with_both_charts)
160    }
161
162
163    fn r_squared(y_true: &Array1<X>, y_pred: &Array1<X>) -> f64
164    {
165        let n = y_true.len();
166        if n == 0 {
167            return 0.0; // or maybe panic/error, depending on your use case
168        }
169        let n_f = n as f64;
170
171        // Calculate mean once
172        let y_mean = y_true.iter().map(|&v| v.into()).sum::<f64>() / n_f;
173
174        // Sum of squared residuals (errors)
175        let ss_res = y_true
176            .iter()
177            .zip(y_pred.iter())
178            .map(|(&y, &y_hat)| {
179                let y_f = y.into();
180                let y_hat_f = y_hat.into();
181                let diff = y_f - y_hat_f;
182                diff * diff
183            })
184            .sum::<f64>();
185
186        // Total sum of squares
187        let ss_tot = y_true
188            .iter()
189            .map(|&y| {
190                let y_f = y.into();
191                let diff = y_f - y_mean;
192                diff * diff
193            })
194            .sum::<f64>();
195        
196        if ss_tot == 0.0 {
197            // All y_true are constant
198            return if ss_res == 0.0 {
199                1.0  // Perfect fit
200            } else {
201                0.0  // Model fails to match - treat as no explanatory power
202            }
203        }
204        
205        1.0 - ss_res / ss_tot
206    }
207}
208
209
210impl<X,Y> Default  for ChartDataSection<X,Y>{
211    fn default() -> Self {
212        ChartDataSection {
213            datasets: vec![],
214        }
215    }
216}
217
218impl<X:WithScaleType, Y:WithScaleType> Default for ChartConfig<X,Y>{
219    fn default() -> Self {
220        ChartConfig{
221            data: ChartDataSection::default(),
222            options: ChartOptions{
223                scales: Some(ScalingConfig{
224                    x:Some(ScaleConfig{
225                        r#type: Some(X::scale_type()),
226                        ..ScaleConfig::default()
227                    }),
228                    y:Some(ScaleConfig{
229                        r#type: Some(Y::scale_type()),
230                        reverse: Y::scale_type() == ScaleType::Category,
231                        ..ScaleConfig::default()
232                    }),
233                }),
234                aspect_ratio: None,
235                plugins: Plugins::default(),
236            },
237        }
238    }
239}
240
241
242pub trait WithScaleType{
243    fn scale_type()->ScaleType;
244}
245
246#[macro_export]
247macro_rules! impl_scale_type {
248    ($type:ident for $($t:ty)*) => ($(
249        impl WithScaleType for $t {
250            fn scale_type() -> ScaleType {
251                ScaleType::$type
252            }
253        }
254    )*)
255}
256
257impl_scale_type!(Linear for u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize f32 f64);
258impl_scale_type!(Category for &str String);
259impl_scale_type!(Time for SystemTime Instant);
260
261#[derive(Serialize, Deserialize, Debug, Clone)]
262#[serde(rename_all = "lowercase")]
263pub enum ChartType {
264    Bubble,
265    Bar,
266    Line,
267    Doughnut,
268    Pie,
269    Radar,
270    PolarArea,
271    Scatter
272}
273
274#[derive(Serialize, Deserialize, Debug, Clone)]
275#[serde(rename_all = "camelCase")]
276pub struct ChartDataSection<X,Y>{
277    datasets: Vec<Dataset<X,Y>>
278}
279
280#[derive(Serialize, Deserialize, Debug, Clone)]
281#[serde(rename_all = "camelCase")]
282pub struct Dataset<X,Y>{
283
284    r#type: ChartType,
285
286    label: String,
287
288    data: ChartData<X,Y>,
289
290    #[serde(skip_serializing_if = "Option::is_none")]
291    fill: Option<Fill>,
292
293    #[serde(skip_serializing_if = "Option::is_none")]
294    border_color: Option<Rgb>,
295
296    #[serde(skip_serializing_if = "Option::is_none")]
297    background_color: Option<Rgb>
298}
299
300
301impl<X,Y> From<DataPointWithRadius<X,Y>> for (X, Y){
302    fn from(value: DataPointWithRadius<X,Y>) -> Self {
303        (value.x, value.y)
304    }
305}
306
307impl<X,Y> From<DataPoint<X,Y>> for (X,Y){
308    fn from(value: DataPoint<X,Y>) -> Self {
309        (value.x, value.y)
310    }
311}
312
313impl<X,Y> From<DataPointWithTooltip<X,Y>> for (X, Y){
314    fn from(value: DataPointWithTooltip<X,Y>) -> Self {
315        (value.x, value.y)
316    }
317}
318
319impl<X,Y> From<DataPointWithTooltip<X,Y>> for (X, Y, String){
320    fn from(value: DataPointWithTooltip<X,Y>) -> Self {
321        (value.x, value.y, value.tooltip)
322    }
323}
324
325
326impl<X,Y> From<(X, Y, String)> for DataPointWithTooltip<X,Y>{
327    fn from(value: (X, Y, String)) -> Self {
328        DataPointWithTooltip{
329            x: value.0,
330            y: value.1,
331            tooltip: value.2
332        }
333    }
334}
335
336impl<X,Y> From<(X, Y, &str)> for DataPointWithTooltip<X,Y>{
337    fn from(value: (X, Y, &str)) -> Self {
338        DataPointWithTooltip{
339            x: value.0,
340            y: value.1,
341            tooltip: value.2.to_string()
342        }
343    }
344}
345
346impl<X,Y> From<ChartData<X,Y>> for Vec<(X,Y)>{
347    fn from(value: ChartData<X, Y>) -> Self {
348        match value {
349            Vector2D(v) => v,
350            VectorWithRadius(val)=> val.into_iter().map(|v| v.into()).collect(),
351            VectorWithText(val)=> val.into_iter().map(|v| v.into()).collect()
352        }
353    }
354}
355
356impl<X,Y> From<Vec<(X,Y,String)>> for ChartData<X,Y>{
357    fn from(value: Vec<(X, Y, String)>) -> Self {
358        VectorWithText(value.into_iter().map(|v| v.into()).collect())
359    }
360}
361
362impl<const N: usize,X,Y> From<[(X,Y,String);N]> for ChartData<X,Y>{
363    fn from(value: [(X,Y,String);N]) -> Self {
364        VectorWithText(value.into_iter().map(|v| v.into()).collect())
365    }
366}
367
368impl<X,Y> From<Vec<(X,Y,&str)>> for ChartData<X,Y>{
369    fn from(value: Vec<(X, Y,&str)>) -> Self {
370        VectorWithText(value.into_iter().map(|v| v.into()).collect())
371    }
372}
373
374impl<const N: usize,X,Y> From<[(X,Y,&str);N]> for ChartData<X,Y>{
375    fn from(value: [(X,Y,&str);N]) -> Self {
376        VectorWithText(value.into_iter().map(|v| v.into()).collect())
377    }
378}
379
380
381
382
383
384impl<X,Y> From<Vec<(X,Y)>> for ChartData<X,Y>{
385    fn from(value: Vec<(X, Y)>) -> Self {
386       Vector2D(value)
387    }
388}
389
390impl<const N: usize,X,Y> From<[(X,Y);N]> for ChartData<X,Y> where X:Clone, Y:Clone {
391    fn from(value: [(X,Y);N]) -> Self {
392        Vector2D(value.to_vec())
393    }
394}
395
396#[derive(Deserialize, Debug, Clone)]
397#[serde(untagged)]
398pub enum ChartData<X,Y>{
399    Vector2D(Vec<(X,Y)>),
400    VectorWithRadius(Vec<DataPointWithRadius<X,Y>>),
401    VectorWithText(Vec<DataPointWithTooltip<X,Y>>)
402}
403
404
405impl<X,Y> Serialize for ChartData<X,Y> where X:Serialize, Y:Serialize{
406    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer {
407        match self {
408            Vector2D(v) => {
409                let mut v_ser = serializer.serialize_seq(Some(v.len()))?;
410                for (x,y) in v {
411                    let  point = DataPoint{x,y};
412                    v_ser.serialize_element(&point)?;
413                }
414                v_ser.end()
415            },
416            VectorWithRadius(v) => v.serialize(serializer),
417            VectorWithText(v) => v.serialize(serializer)
418        }
419    }
420}
421
422
423#[derive(Serialize, Deserialize, Debug, Clone)]
424pub struct DataPoint<X,Y>{
425    x: X,
426    y: Y
427}
428
429#[derive(Serialize, Deserialize, Debug, Clone)]
430pub struct DataPointWithTooltip<X,Y>{
431    x: X,
432    y: Y,
433    tooltip: String
434}
435
436
437#[derive(Serialize, Deserialize, Debug, Clone)]
438pub struct DataPointWithRadius<X,Y>{
439    x: X,
440    y: Y,
441    r: u32
442}
443
444#[derive(Serialize, Deserialize, Debug, Clone)]
445#[serde(untagged)]
446enum FillVariant{
447    AbsIndex(u8),
448    RelativeIndex(String),
449    Boundary(Boundary),
450    AxisValue(AxisValue)
451}
452
453#[derive(Serialize, Deserialize, Debug, Clone)]
454struct AxisValue{
455    value: AxisValueVariant
456}
457
458#[derive(Serialize, Deserialize, Debug, Clone)]
459enum AxisValueVariant{
460    Str(String),
461    Num(f64)
462}
463
464#[derive(Serialize, Deserialize, Debug, Clone)]
465enum Boundary{
466    Start,
467    End,
468    Origin,
469    Stack,
470    Shape
471}
472
473#[derive(Serialize, Deserialize, Debug, Clone)]
474#[serde(rename_all = "camelCase")]
475struct  Fill {
476    target: FillVariant,
477
478    #[serde(skip_serializing_if = "Option::is_none")]
479    above: Option<Rgb>,
480
481    #[serde(skip_serializing_if = "Option::is_none")]
482    below: Option<Rgb>
483}
484
485#[derive(Serialize, Deserialize, Debug, Clone)]
486#[serde(rename_all = "lowercase")]
487enum AxisName{
488    X,
489    Y
490}
491
492#[derive(Debug, Clone)]
493pub struct ChartOptions<X,Y>{
494    pub(crate) scales: Option<ScalingConfig<X,Y>>,
495    pub aspect_ratio: Option<u8>,
496    pub plugins: Plugins,
497}
498
499impl<X,Y> Default for ChartOptions<X,Y>{
500    fn default() -> Self {
501        ChartOptions{
502            scales: None,
503            aspect_ratio: None,
504            plugins: Plugins::default(),
505        }
506    }
507}
508
509#[derive(Serialize, Deserialize, Debug, Clone)]
510#[serde(rename_all = "camelCase")]
511pub struct ScalingConfig<X,Y>{
512    #[serde(skip_serializing_if = "Option::is_none")]
513    x: Option<ScaleConfig<X>>,
514
515    #[serde(skip_serializing_if = "Option::is_none")]
516    y: Option<ScaleConfig<Y>>
517}
518
519#[derive(Serialize, Deserialize, Debug, Clone)]
520#[serde(rename_all = "camelCase")]
521pub struct ScaleConfig<T>{
522
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub r#type: Option<ScaleType>,
525
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub labels: Option<Vec<T>>,
528
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub align_to_pixels: Option<bool>,
531
532    pub reverse: bool,
533
534    #[serde(skip_serializing_if = "Option::is_none")]
535    pub max: Option<T>,
536
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub min: Option<T>,
539
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub title: Option<AxisTitle>
542}
543
544
545impl<T> Default for ScaleConfig<T>{
546    fn default() -> Self {
547        ScaleConfig{
548            r#type: None,
549            labels: None,
550            align_to_pixels: None,
551            reverse: false,
552            max: None,
553            min: None,
554            title: None
555        }
556    }
557}
558
559impl<T> ScaleConfig<T>{
560
561    pub fn new_category(reverse:bool,values: Vec<T>) -> Self {
562        Self {
563            r#type: Some(ScaleType::Category),
564            labels: Some(values),
565            reverse,
566            ..ScaleConfig::<T>::default()
567        }
568    }
569}
570
571
572#[derive(Serialize, Deserialize, Debug, Clone,PartialEq)]
573#[serde(rename_all = "lowercase")]
574pub enum ScaleType{
575    Linear,
576    Logarithmic,
577    Category,
578    Time,
579    TimeSeries,
580    RadialLinear
581}
582
583#[derive(Serialize, Deserialize, Debug, Clone)]
584#[serde(rename_all = "camelCase")]
585pub struct AxisTitle{
586    display: bool,
587    text: String,
588    align: Alignment,
589}
590
591#[derive(Serialize, Deserialize, Debug, Clone)]
592#[serde(rename_all = "lowercase")]
593pub enum Alignment{
594    Start,
595    Center,
596    End
597}
598
599#[derive(Debug, Clone)]
600#[derive(Default)]
601pub struct Plugins{
602    pub title: Option<Title>,
603    pub subtitle: Option<Title>,
604    pub legend: Option<Legend>,
605    pub tooltip: Option<Tooltip>
606}
607
608#[derive(Serialize, Deserialize, Debug, Clone)]
609#[serde(rename_all = "camelCase")]
610pub struct Legend{
611    display: bool,
612
613    #[serde(skip_serializing_if = "Option::is_none")]
614    position: Option<Position>,
615
616    #[serde(skip_serializing_if = "Option::is_none")]
617    align: Option<Alignment>,
618
619    pub full_size: bool,
620}
621
622#[derive(Serialize, Deserialize, Debug, Clone)]
623#[serde(rename_all = "lowercase")]
624enum Position{
625    Top,
626    Left,
627    Bottom,
628    Right
629}
630
631#[derive(Serialize, Deserialize, Debug, Clone)]
632#[serde(rename_all = "lowercase")]
633enum Align{
634    Start,
635    Center,
636    End
637}
638
639#[derive(Serialize, Deserialize, Debug, Clone)]
640#[serde(rename_all = "camelCase")]
641pub struct Title{
642    ///Is the title shown?
643    display: bool,
644
645    ///Marks that this box should take the full width/height of the canvas. If false, the box is sized and placed above/beside the chart area.
646    full_size: bool,
647
648    ///Title text to display. If specified as an array, text is rendered on multiple lines.
649    text: Vec<String>,
650
651    ///Padding to apply around the title. Only top and bottom are implemented.
652    #[serde(skip_serializing_if = "Option::is_none")]
653    padding: Option<Padding>,
654
655    ///position of the title
656    #[serde(skip_serializing_if = "Option::is_none")]
657    position: Option<Position>
658
659}
660
661#[derive(Serialize, Deserialize, Debug, Clone)]
662#[serde(rename_all = "lowercase")]
663pub enum TooltipMode{
664    Average,
665    Nearest
666}
667
668
669impl Default for Tooltip {
670    fn default() -> Self {
671        Tooltip{
672            enabled: true,
673            mode: None,
674            background_color: None,
675            title_color: None,
676            callbacks: None
677        }
678    }
679}
680
681#[derive(Debug, Clone)]
682pub struct Tooltip{
683    pub enabled: bool,
684    pub mode: Option<TooltipMode>,
685    pub background_color: Option<Rgb>,
686    pub title_color: Option<Rgb>,
687    pub callbacks: Option<TooltipCallbacks>
688}
689
690#[derive(Debug, Clone,Default)]
691pub struct TooltipCallbacks{
692    pub before_title: Option<JsExpr>,
693    pub title: Option<JsExpr>,
694    pub after_title: Option<JsExpr>,
695    pub before_body: Option<JsExpr>,
696    pub before_label: Option<JsExpr>,
697    pub label: Option<JsExpr>,
698    pub label_color: Option<JsExpr>,
699    pub label_text_color: Option<JsExpr>,
700    pub after_label: Option<JsExpr>,
701    pub after_body: Option<JsExpr>,
702    pub before_footer: Option<JsExpr>,
703    pub footer: Option<JsExpr>,
704    pub after_footer: Option<JsExpr>,
705}
706
707
708#[derive(Debug, Clone)]
709pub struct JsExpr(pub &'static str);
710
711impl std::fmt::Display for JsExpr {
712    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
713        // Print raw JS, no quotes
714        f.write_str(&self.0)
715    }
716}
717