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    r#type: Option<ScaleType>,
525
526    #[serde(skip_serializing_if = "Option::is_none")]
527    labels: Option<Vec<T>>,
528
529    #[serde(skip_serializing_if = "Option::is_none")]
530    align_to_pixels: Option<bool>,
531
532    reverse: bool,
533
534    #[serde(skip_serializing_if = "Option::is_none")]
535    max: Option<T>,
536
537    #[serde(skip_serializing_if = "Option::is_none")]
538    min: Option<T>,
539
540    #[serde(skip_serializing_if = "Option::is_none")]
541    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    pub fn with_type(mut self, r#type: ScaleType) -> Self {
571        self.r#type = Some(r#type);
572        self
573    }
574
575    pub fn with_align_to_pixels(mut self, align_to_pixels: bool) -> Self {
576        self.align_to_pixels = Some(align_to_pixels);
577        self
578    }
579
580    pub fn with_max(mut self, max: T) -> Self {
581        self.max = Some(max);
582        self
583    }
584
585    pub fn with_min(mut self, min: T) -> Self {
586        self.min = Some(min);
587        self
588    }
589
590    pub fn with_str_title(mut self, title: &str) -> Self {
591        self.title = Some(title.into());
592        self
593    }
594
595    pub fn with_title(mut self, title: AxisTitle) -> Self {
596        self.title = Some(title);
597        self
598    }
599
600    pub fn with_labels(mut self, labels: Vec<T>) -> Self {
601        self.labels = Some(labels);
602        self
603    }
604
605    pub fn with_reverse(mut self, reverse: bool) -> Self {
606        self.reverse = reverse;
607        self
608    }
609
610}
611
612#[derive(Serialize, Deserialize, Debug, Clone,PartialEq)]
613#[serde(rename_all = "lowercase")]
614pub enum ScaleType{
615    Linear,
616    Logarithmic,
617    Category,
618    Time,
619    TimeSeries,
620    RadialLinear
621}
622
623#[derive(Serialize, Deserialize, Debug, Clone)]
624#[serde(rename_all = "camelCase")]
625pub struct AxisTitle{
626    display: bool,
627    text: String,
628    align: Alignment,
629}
630
631
632impl From<&str> for AxisTitle {
633    fn from(value: &str) -> Self {
634        Self{
635            display: true,
636            text: value.to_string(),
637            align: Alignment::Center
638        }
639    }
640}
641
642impl AxisTitle{
643    pub fn new(text: String) -> Self {
644        Self{
645            display: true,
646            text,
647            align: Alignment::Center
648        }
649    }
650
651    pub fn with_display(mut self, display: bool) -> Self {
652        self.display = display;
653        self
654    }
655
656    pub fn with_text(mut self, text: String) -> Self {
657        self.text = text;
658        self
659    }
660
661    pub fn with_align(mut self, align: Alignment) -> Self {
662        self.align = align;
663        self
664    }
665
666}
667
668#[derive(Serialize, Deserialize, Debug, Clone)]
669#[serde(rename_all = "lowercase")]
670pub enum Alignment{
671    Start,
672    Center,
673    End
674}
675
676#[derive(Debug, Clone)]
677#[derive(Default)]
678pub struct Plugins{
679    pub title: Option<Title>,
680    pub subtitle: Option<Title>,
681    pub legend: Option<Legend>,
682    pub tooltip: Option<Tooltip>
683}
684
685#[derive(Serialize, Deserialize, Debug, Clone)]
686#[serde(rename_all = "camelCase")]
687pub struct Legend{
688    display: bool,
689
690    #[serde(skip_serializing_if = "Option::is_none")]
691    position: Option<Position>,
692
693    #[serde(skip_serializing_if = "Option::is_none")]
694    align: Option<Alignment>,
695
696    pub full_size: bool,
697}
698
699
700impl Default for Legend{
701    fn default() -> Self {
702        Legend{
703            display: true,
704            position: None,
705            align: None,
706            full_size: false
707        }
708    }
709}
710
711impl Legend {
712    pub fn new_position(position: Position) -> Self {
713        Self {
714            display: true,
715            position: Some(position),
716            align: None,
717            full_size: false
718        }
719    }
720
721    pub fn  with_align(mut self, align: Alignment) -> Self {
722        self.align = Some(align);
723        self
724    }
725
726    pub fn  with_full_size(mut self, full_size: bool) -> Self {
727        self.full_size = full_size;
728        self
729    }
730
731    pub fn  with_display(mut self, display: bool) -> Self {
732        self.display = display;
733        self
734    }
735
736    pub fn  with_position(mut self, position: Position) -> Self {
737        self.position = Some(position);
738        self
739    }
740
741}
742
743#[derive(Serialize, Deserialize, Debug, Clone)]
744#[serde(rename_all = "lowercase")]
745pub enum Position{
746    Top,
747    Left,
748    Bottom,
749    Right
750}
751
752#[derive(Serialize, Deserialize, Debug, Clone)]
753#[serde(rename_all = "lowercase")]
754pub enum Align{
755    Start,
756    Center,
757    End
758}
759
760#[derive(Serialize, Deserialize, Debug, Clone)]
761#[serde(rename_all = "camelCase")]
762pub struct Title{
763    ///Is the title shown?
764    display: bool,
765
766    ///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.
767    full_size: bool,
768
769    ///Title text to display. If specified as an array, text is rendered on multiple lines.
770    text: Vec<String>,
771
772    ///Padding to apply around the title. Only top and bottom are implemented.
773    #[serde(skip_serializing_if = "Option::is_none")]
774    padding: Option<Padding>,
775
776    ///position of the title
777    #[serde(skip_serializing_if = "Option::is_none")]
778    position: Option<Position>
779
780}
781
782
783#[derive(Serialize, Deserialize, Debug, Clone)]
784#[serde(rename_all = "lowercase")]
785pub enum TooltipMode{
786    Average,
787    Nearest
788}
789
790
791impl Default for Tooltip {
792    fn default() -> Self {
793        Tooltip{
794            enabled: true,
795            mode: None,
796            background_color: None,
797            title_color: None,
798            callbacks: None
799        }
800    }
801}
802
803#[derive(Debug, Clone)]
804pub struct Tooltip{
805    pub enabled: bool,
806    pub mode: Option<TooltipMode>,
807    pub background_color: Option<Rgb>,
808    pub title_color: Option<Rgb>,
809    pub callbacks: Option<TooltipCallbacks>
810}
811
812#[derive(Debug, Clone,Default)]
813pub struct TooltipCallbacks{
814    pub before_title: Option<JsExpr>,
815    pub title: Option<JsExpr>,
816    pub after_title: Option<JsExpr>,
817    pub before_body: Option<JsExpr>,
818    pub before_label: Option<JsExpr>,
819    pub label: Option<JsExpr>,
820    pub label_color: Option<JsExpr>,
821    pub label_text_color: Option<JsExpr>,
822    pub after_label: Option<JsExpr>,
823    pub after_body: Option<JsExpr>,
824    pub before_footer: Option<JsExpr>,
825    pub footer: Option<JsExpr>,
826    pub after_footer: Option<JsExpr>,
827}
828
829
830#[derive(Debug, Clone)]
831pub struct JsExpr(pub &'static str);
832
833impl std::fmt::Display for JsExpr {
834    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
835        // Print raw JS, no quotes
836        f.write_str(&self.0)
837    }
838}
839