chart_js_wrapper/
options.rs

1use std::cmp::PartialEq;
2use crate::common::{Padding, Rgb, Size};
3use crate::render::Chart;
4use ndarray::{Array1, Array2};
5use ndarray_linalg::error::LinalgError;
6use ndarray_linalg::LeastSquaresSvd;
7use ndarray_linalg::{Lapack, Scalar};
8use serde::Serialize;
9use serde::Deserialize;
10use uuid::Uuid;
11use crate::data::ChartData;
12use crate::serde::{ValueSerializeWrapper, WithTypeAndSerializer};
13
14const DISPLAY_FN: &'static str = "
15                        function(context){
16                            context = context[0];
17                            let ttp = context.raw.tooltip || '';
18                            if(ttp) return ttp;
19                        }";
20
21#[derive(Debug, Clone)]
22pub struct ChartConfig<X:WithTypeAndSerializer+Serialize,Y:WithTypeAndSerializer+Serialize>
23{
24    pub data: ChartDataSection<X,Y>,
25    pub options: ChartOptions<X,Y>
26}
27
28impl<X, Y> ChartConfig<X, Y>
29where X:WithTypeAndSerializer+Serialize, Y:WithTypeAndSerializer+Serialize{
30
31    pub fn new(options: ChartOptions<X,Y>) -> Self {
32        Self {
33            data: ChartDataSection::default(),
34            options
35        }
36    }
37
38    pub fn set_x_axis(mut self, conf: ScaleConfig<X>) -> Self {
39        let mut scales = self.options.scales;
40        if scales.is_none() {
41            scales = Some(ScalingConfig{
42                x: Some(conf),
43                y: None
44            });
45        }else {
46            scales.as_mut().unwrap().x = Some(conf);
47        }
48        self.options.scales = scales;
49        self
50    }
51
52    pub fn set_y_axis(mut self, conf: ScaleConfig<Y>) -> Self {
53        let mut scales = self.options.scales;
54        if scales.is_none() {
55            scales = Some(ScalingConfig{
56                x: None,
57                y: Some(conf)
58            });
59        }else {
60            scales.as_mut().unwrap().y = Some(conf);
61        }
62        self.options.scales = scales;
63        self
64    }
65
66
67    pub fn with_elements(mut self, elements: ElementsConfig) -> Self {
68        self.options.elements = Some(elements);
69        self
70    }
71
72    pub fn with_aspect_ratio(mut self, ratio: f32) -> Self {
73        self.options.aspect_ratio = Some(ratio);
74        self
75    }
76
77    pub fn add_series_direct(mut self, series:Dataset<X,Y>) -> Self{
78        self.data.datasets.push(series);
79        self
80    }
81
82    pub fn add_series_with_config<T: Into<ChartData<X,Y>>>(mut self, r#type: ChartType, title:String, config:ElementsConfig, data: T)->Self{
83        self.data.datasets.push(Dataset {
84            r#type,
85            label: title,
86            data: data.into(),
87            elements: Some(config)
88        });
89        self
90    }
91
92    pub fn add_series<T: Into<ChartData<X,Y>>>(mut self, r#type: ChartType, title:String, data: T)->Self{
93        self.data.datasets.push(Dataset {
94            r#type,
95            label: title,
96            data: data.into(),
97            elements: None
98        });
99        self
100    }
101
102    pub fn enable_legend(mut self) -> Self{
103        let legend = self.options.plugins.legend.get_or_insert_default();
104        legend.display = true;
105        self
106    }
107
108    pub fn with_title(mut self, title:Title) -> Self{
109        self.options.plugins.title = Some(title);
110        self
111    }
112
113    pub fn with_legend(mut self, legend:Legend) -> Self{
114        self.options.plugins.legend = Some(legend);
115        self
116    }
117
118    pub fn with_tooltip(mut self, tooltip:Tooltip) -> Self{
119        self.options.plugins.tooltip = Some(tooltip);
120        self
121    }
122
123    pub fn with_subtitle(mut self, subtitle:Title) -> Self{
124        self.options.plugins.subtitle = Some(subtitle);
125        self
126    }
127    
128    pub fn build(self, width: Size, height: Size) -> Chart<X,Y>{
129        Chart::new(Uuid::new_v4().to_string(), width, height, self)
130    }
131}
132
133
134impl<X> ChartConfig<X, X> where  X:WithTypeAndSerializer + Scalar + Lapack + Clone + Into<f64> {
135    
136    pub fn add_linear_regression_series<T: Into<ChartData<X,X>>>(self, title: &str, data: T) -> Result<Self, LinalgError> {
137        let data:Vec<(X,X)>  = data.into().into();
138        let n = data.len();
139        let config_with_scatter = self.add_series(ChartType::Scatter, title.to_string(), data.clone());
140        
141        // Allocate design matrix with shape (n, 2)
142        let mut x_matrix = Array2::<X>::zeros((n, 2));
143        let mut y_array = Array1::<X>::zeros(n);
144
145        for (i, (x, y)) in data.into_iter().enumerate() {
146            x_matrix[[i, 0]] = X::one(); // intercept term
147            x_matrix[[i, 1]] = x;
148            y_array[i] = y;
149        }
150        
151        let beta = x_matrix.least_squares(&y_array)?.solution;
152
153        let y_pred = x_matrix.dot(&beta);
154        let r2 = Self::r_squared(&y_array, &y_pred);
155        let mut reg_data = Vec::<(X,X)>::with_capacity(n);
156        
157        y_pred.into_iter().enumerate().for_each(|(i, x)| {
158            reg_data.push((x_matrix[[i,1]],x));
159        });
160        
161        let config_with_both_charts = 
162            config_with_scatter.add_series(ChartType::Line, format!("{} regression(R^2 = {:.4})", title, r2), reg_data);
163        
164        Ok(config_with_both_charts)
165    }
166
167
168    fn r_squared(y_true: &Array1<X>, y_pred: &Array1<X>) -> f64
169    {
170        let n = y_true.len();
171        if n == 0 {
172            return 0.0; // or maybe panic/error, depending on your use case
173        }
174        let n_f = n as f64;
175
176        // Calculate mean once
177        let y_mean = y_true.iter().map(|&v| v.into()).sum::<f64>() / n_f;
178
179        // Sum of squared residuals (errors)
180        let ss_res = y_true
181            .iter()
182            .zip(y_pred.iter())
183            .map(|(&y, &y_hat)| {
184                let y_f = y.into();
185                let y_hat_f = y_hat.into();
186                let diff = y_f - y_hat_f;
187                diff * diff
188            })
189            .sum::<f64>();
190
191        // Total sum of squares
192        let ss_tot = y_true
193            .iter()
194            .map(|&y| {
195                let y_f = y.into();
196                let diff = y_f - y_mean;
197                diff * diff
198            })
199            .sum::<f64>();
200        
201        if ss_tot == 0.0 {
202            // All y_true are constant
203            return if ss_res == 0.0 {
204                1.0  // Perfect fit
205            } else {
206                0.0  // Model fails to match - treat as no explanatory power
207            }
208        }
209        
210        1.0 - ss_res / ss_tot
211    }
212}
213
214
215impl<X,Y> Default  for ChartDataSection<X,Y> where X:WithTypeAndSerializer, Y:WithTypeAndSerializer{
216    fn default() -> Self {
217        ChartDataSection {
218            datasets: vec![],
219        }
220    }
221}
222
223impl<X: WithTypeAndSerializer+Serialize, Y: WithTypeAndSerializer+Serialize> Default for ChartConfig<X,Y> {
224    fn default() -> Self {
225        ChartConfig{
226            data: ChartDataSection::default(),
227            options: ChartOptions{
228                scales: Some(ScalingConfig{
229                    x:Some(ScaleConfig{
230                        r#type: Some(X::scale_type()),
231                        ..ScaleConfig::default()
232                    }),
233                    y:Some(ScaleConfig{
234                        r#type: Some(Y::scale_type()),
235                        reverse: Y::scale_type() == ScaleType::Category,
236                        ..ScaleConfig::default()
237                    }),
238                }),
239                aspect_ratio: None,
240                elements: None,
241                plugins: Plugins::default(),
242            },
243        }
244    }
245}
246
247#[derive(Serialize, Deserialize, Debug, Clone)]
248#[serde(rename_all = "lowercase")]
249pub enum ChartType {
250    Bubble,
251    Bar,
252    Line,
253    Doughnut,
254    Pie,
255    Radar,
256    PolarArea,
257    Scatter
258}
259
260#[derive(Serialize, Debug, Clone)]
261#[serde(rename_all = "camelCase")]
262pub struct ChartDataSection<X:WithTypeAndSerializer,Y:WithTypeAndSerializer>{
263    datasets: Vec<Dataset<X,Y>>
264}
265
266#[derive(Serialize, Debug, Clone)]
267#[serde(rename_all = "camelCase")]
268pub struct Dataset<X:WithTypeAndSerializer,Y:WithTypeAndSerializer>{
269
270    r#type: ChartType,
271
272    label: String,
273
274    data: ChartData<X,Y>,
275
276    #[serde(skip_serializing_if = "Option::is_none")]
277    elements: Option<ElementsConfig>
278}
279
280#[derive(Serialize, Debug, Clone,Default)]
281#[serde(rename_all = "camelCase")]
282pub struct ElementsConfig{
283    #[serde(skip_serializing_if = "Option::is_none")]
284    line:  Option<LineConfig>,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    point: Option<PointConfig>
287
288}
289
290impl ElementsConfig {
291
292    pub fn with_line_config(mut self, conf: LineConfig) -> Self{
293        self.line = Some(conf);
294        self
295    }
296
297    pub fn with_point_config(mut self, conf: PointConfig) -> Self{
298        self.point = Some(conf);
299        self
300    }
301
302}
303
304#[derive(Serialize, Debug, Clone)]
305#[serde(rename_all = "lowercase")]
306pub enum CubicInterpolationMode{
307    Default,
308    Monotone
309}
310
311#[derive(Serialize, Debug, Clone,Default)]
312pub struct LineConfig {
313    ///how much bezier rounding to use, default is 0 - no bezier
314    #[serde(skip_serializing_if = "Option::is_none")]
315    tension: Option<f32>,
316
317    #[serde(skip_serializing_if = "Option::is_none")]
318    cubic_interpolation_mode: Option<CubicInterpolationMode>,
319
320    #[serde(skip_serializing_if = "Option::is_none")]
321    fill: Option<Fill>,
322
323    #[serde(skip_serializing_if = "Option::is_none")]
324    border_color: Option<Rgb>,
325
326    #[serde(skip_serializing_if = "Option::is_none")]
327    background_color: Option<Rgb>,
328
329    stepped: bool
330
331}
332
333impl LineConfig {
334
335    pub fn with_tension(mut self, tension: f32) -> Self{
336        self.tension = Some(tension);
337        self.stepped = false;
338        self
339    }
340
341    pub fn with_stepped(mut self, stepped: bool) -> Self{
342        self.stepped = stepped;
343        self.tension = None;
344        self
345    }
346
347    pub fn with_cubic_interpolation_mode(mut self, mode: CubicInterpolationMode) -> Self{
348        self.cubic_interpolation_mode = Some(mode);
349        self
350    }
351
352    pub fn with_fill(mut self, fill: Fill) -> Self{
353        self.fill = Some(fill);
354        self
355    }
356
357    pub fn with_border_color(mut self, color: Rgb) -> Self{
358        self.border_color = Some(color);
359        self
360    }
361
362    pub fn with_background_color(mut self, color: Rgb) -> Self{
363        self.background_color = Some(color);
364        self
365    }
366}
367
368
369#[derive(Serialize, Debug, Clone)]
370#[serde(rename_all = "camelCase")]
371pub struct PointConfig{
372
373    #[serde(skip_serializing_if = "Option::is_none")]
374    border_color: Option<Rgb>,
375
376    #[serde(skip_serializing_if = "Option::is_none")]
377    background_color: Option<Rgb>,
378
379    point_style: PointStyle,
380
381    ///point rotation in degrees
382    rotation: u16,
383
384    radius: u16,
385
386    border_width: u16,
387
388    hover_radius: u16,
389
390    hit_radius: u16,
391
392    hover_border_width: u16
393
394}
395
396impl Default for PointConfig {
397    fn default() -> Self {
398        PointConfig{
399            radius: 3,
400            point_style: PointStyle::Circle,
401            rotation: 0,
402            border_color: None,
403            background_color: None,
404            border_width: 1,
405            hover_radius: 4,
406            hit_radius: 1,
407            hover_border_width: 1
408        }
409    }
410}
411
412impl PointConfig {
413    pub fn with_radius(mut self, radius: u16) -> Self{
414        self.radius = radius;
415        self
416    }
417
418    pub fn with_point_style(mut self, style: PointStyle) -> Self{
419        self.point_style = style;
420        self
421    }
422    pub fn with_rotation(mut self, rotation: u16) -> Self{
423        self.rotation = rotation;
424        self
425    }
426    pub fn with_border_color(mut self, color: Rgb) -> Self{
427        self.border_color = Some(color);
428        self
429    }
430    pub fn with_background_color(mut self, color: Rgb) -> Self{
431        self.background_color = Some(color);
432        self
433    }
434
435    pub fn with_border_width(mut self, width: u16) -> Self{
436        self.border_width = width;
437        self
438    }
439    pub fn with_hover_radius(mut self, radius: u16) -> Self{
440        self.hover_radius = radius;
441        self
442    }
443    pub fn with_hit_radius(mut self, radius: u16) -> Self{
444        self.hit_radius = radius;
445        self
446    }
447
448    pub fn with_hover_border_width(mut self, width: u16) -> Self{
449        self.hover_border_width = width;
450        self
451    }
452}
453
454
455#[derive(Serialize, Debug, Clone)]
456pub enum PointStyle{
457    Circle,
458    Cross,
459    CrossRot,
460    Dash,
461    Line,
462    Rect,
463    RectRounded,
464    RectRot,
465    Star,
466    Triangle
467}
468
469#[derive(Serialize, Deserialize, Debug, Clone)]
470#[serde(untagged)]
471pub enum FillVariant{
472    AbsIndex(u8),
473    RelativeIndex(String),
474    Boundary(Boundary),
475    AxisValue(AxisValue)
476}
477
478#[derive(Serialize, Deserialize, Debug, Clone)]
479pub struct AxisValue{
480    value: AxisValueVariant
481}
482
483impl AxisValue{
484    pub fn new(value: AxisValueVariant) -> Self{
485        Self{
486            value
487        }
488    }
489}
490
491#[derive(Serialize, Deserialize, Debug, Clone)]
492pub enum AxisValueVariant{
493    Str(String),
494    Num(f64)
495}
496
497#[derive(Serialize, Deserialize, Debug, Clone)]
498pub enum Boundary{
499    Start,
500    End,
501    Origin,
502    Stack,
503    Shape
504}
505
506#[derive(Serialize, Deserialize, Debug, Clone)]
507#[serde(rename_all = "camelCase")]
508struct  Fill {
509    target: FillVariant,
510
511    #[serde(skip_serializing_if = "Option::is_none")]
512    above: Option<Rgb>,
513
514    #[serde(skip_serializing_if = "Option::is_none")]
515    below: Option<Rgb>
516}
517
518
519impl Fill {
520
521    pub fn with_target(mut self, target: FillVariant) -> Self{
522        self.target = target;
523        self
524    }
525    pub fn with_above(mut self, color: Rgb) -> Self{
526        self.above = Some(color);
527        self
528    }
529
530    pub fn with_below(mut self, color: Rgb) -> Self{
531        self.below = Some(color);
532        self
533    }
534
535}
536
537
538#[derive(Serialize, Deserialize, Debug, Clone)]
539#[serde(rename_all = "lowercase")]
540enum AxisName{
541    X,
542    Y
543}
544
545#[derive(Debug, Clone)]
546pub struct ChartOptions<X,Y> where X:WithTypeAndSerializer, Y:WithTypeAndSerializer{
547    pub(crate) scales: Option<ScalingConfig<X,Y>>,
548    pub(crate) aspect_ratio: Option<f32>,
549    pub(crate) elements: Option<ElementsConfig>,
550    pub(crate) plugins: Plugins,
551}
552
553impl<X,Y> Default for ChartOptions<X,Y> where X:WithTypeAndSerializer, Y:WithTypeAndSerializer{
554    fn default() -> Self {
555        ChartOptions{
556            scales: None,
557            aspect_ratio: None,
558            plugins: Plugins::default(),
559            elements: None
560        }
561    }
562}
563
564#[derive(Serialize, Debug, Clone)]
565#[serde(rename_all = "camelCase")]
566pub struct ScalingConfig<X,Y> where X:WithTypeAndSerializer, Y:WithTypeAndSerializer{
567    #[serde(skip_serializing_if = "Option::is_none")]
568    x: Option<ScaleConfig<X>>,
569
570    #[serde(skip_serializing_if = "Option::is_none")]
571    y: Option<ScaleConfig<Y>>
572}
573
574#[derive(Serialize, Debug, Clone)]
575#[serde(rename_all = "camelCase")]
576pub struct ScaleConfig<T> where T:WithTypeAndSerializer{
577
578    #[serde(skip_serializing_if = "Option::is_none")]
579    r#type: Option<ScaleType>,
580
581    #[serde(skip_serializing_if = "Option::is_none")]
582    labels: Option<Vec<ValueSerializeWrapper<T>>>,
583
584    #[serde(skip_serializing_if = "Option::is_none")]
585    align_to_pixels: Option<bool>,
586
587    reverse: bool,
588
589    #[serde(skip_serializing_if = "Option::is_none")]
590    max: Option<ValueSerializeWrapper<T>>,
591
592    #[serde(skip_serializing_if = "Option::is_none")]
593    min: Option<ValueSerializeWrapper<T>>,
594
595    #[serde(skip_serializing_if = "Option::is_none")]
596    title: Option<AxisTitle>
597}
598
599
600impl<T> Default for ScaleConfig<T> where T:WithTypeAndSerializer{
601    fn default() -> Self {
602        ScaleConfig{
603            r#type: None,
604            labels: None,
605            align_to_pixels: None,
606            reverse: false,
607            max: None,
608            min: None,
609            title: None
610        }
611    }
612}
613
614impl<T> ScaleConfig<T> where T:WithTypeAndSerializer{
615
616    pub fn new_category(reverse:bool,values: Vec<T>) -> Self {
617        Self {
618            r#type: Some(ScaleType::Category),
619            labels: Some(values.into_iter().map(|v| v.into()).collect()),
620            reverse,
621            ..ScaleConfig::<T>::default()
622        }
623    }
624
625    pub fn with_type(mut self, r#type: ScaleType) -> Self {
626        self.r#type = Some(r#type);
627        self
628    }
629
630    pub fn with_align_to_pixels(mut self, align_to_pixels: bool) -> Self {
631        self.align_to_pixels = Some(align_to_pixels);
632        self
633    }
634
635    pub fn with_max(mut self, max: T) -> Self {
636        self.max = Some(max.into());
637        self
638    }
639
640    pub fn with_min(mut self, min: T) -> Self {
641        self.min = Some(min.into());
642        self
643    }
644
645    pub fn with_str_title(mut self, title: &str) -> Self {
646        self.title = Some(title.into());
647        self
648    }
649
650    pub fn with_title(mut self, title: AxisTitle) -> Self {
651        self.title = Some(title);
652        self
653    }
654
655    pub fn with_labels(mut self, labels: Vec<T>) -> Self {
656        self.labels = Some(labels.into_iter().map(|v| v.into()).collect());
657        self
658    }
659
660    pub fn with_reverse(mut self, reverse: bool) -> Self {
661        self.reverse = reverse;
662        self
663    }
664
665}
666
667#[derive(Serialize, Deserialize, Debug, Clone,PartialEq)]
668#[serde(rename_all = "lowercase")]
669pub enum ScaleType{
670    Linear,
671    Logarithmic,
672    Category,
673    Time,
674    TimeSeries,
675    RadialLinear
676}
677
678#[derive(Serialize, Deserialize, Debug, Clone)]
679#[serde(rename_all = "camelCase")]
680pub struct AxisTitle{
681    display: bool,
682    text: String,
683    align: Alignment,
684}
685
686
687impl From<&str> for AxisTitle {
688    fn from(value: &str) -> Self {
689        AxisTitle::new(value.to_string())
690    }
691}
692
693impl From<String> for AxisTitle{
694    fn from(value: String) -> Self {
695        AxisTitle::new(value)
696    }
697}
698
699impl AxisTitle{
700    pub fn new(text: String) -> Self {
701        Self{
702            display: true,
703            text,
704            align: Alignment::Center
705        }
706    }
707
708    pub fn with_display(mut self, display: bool) -> Self {
709        self.display = display;
710        self
711    }
712
713    pub fn with_text(mut self, text: String) -> Self {
714        self.text = text;
715        self
716    }
717
718    pub fn with_align(mut self, align: Alignment) -> Self {
719        self.align = align;
720        self
721    }
722
723}
724
725#[derive(Serialize, Deserialize, Debug, Clone)]
726#[serde(rename_all = "lowercase")]
727pub enum Alignment{
728    Start,
729    Center,
730    End
731}
732
733#[derive(Debug, Clone)]
734pub struct Plugins{
735    pub(crate) title: Option<Title>,
736    pub(crate) subtitle: Option<Title>,
737    pub(crate) legend: Option<Legend>,
738    pub(crate) tooltip: Option<Tooltip>
739}
740
741impl Default for Plugins{
742    fn default() -> Self {
743        Plugins{
744            title: None,
745            subtitle: None,
746            legend: None,
747            tooltip: Some(Tooltip::default()),
748        }
749    }
750}
751
752#[derive(Serialize, Deserialize, Debug, Clone)]
753#[serde(rename_all = "camelCase")]
754pub struct Legend{
755    display: bool,
756
757    position: Position,
758
759    align: Alignment,
760
761    full_size: bool,
762}
763
764
765impl Default for Legend{
766    fn default() -> Self {
767        Legend{
768            display: true,
769            position: Position::Bottom,
770            align: Alignment::Center,
771            full_size: false
772        }
773    }
774}
775
776impl Legend {
777    pub fn new_position(position: Position) -> Self {
778        Self {
779            display: true,
780            position,
781            align: Alignment::Center,
782            full_size: false
783        }
784    }
785
786    pub fn  with_align(mut self, align: Alignment) -> Self {
787        self.align = align;
788        self
789    }
790
791    pub fn  with_full_size(mut self, full_size: bool) -> Self {
792        self.full_size = full_size;
793        self
794    }
795
796    pub fn  with_display(mut self, display: bool) -> Self {
797        self.display = display;
798        self
799    }
800
801    pub fn  with_position(mut self, position: Position) -> Self {
802        self.position = position;
803        self
804    }
805
806}
807
808#[derive(Serialize, Deserialize, Debug, Clone)]
809#[serde(rename_all = "lowercase")]
810pub enum Position{
811    Top,
812    Left,
813    Bottom,
814    Right
815}
816
817#[derive(Serialize, Deserialize, Debug, Clone)]
818#[serde(rename_all = "lowercase")]
819pub enum Align{
820    Start,
821    Center,
822    End
823}
824
825#[derive(Serialize, Deserialize, Debug, Clone)]
826#[serde(rename_all = "camelCase")]
827pub struct Title{
828    ///Is the title shown?
829    display: bool,
830
831    ///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.
832    full_size: bool,
833
834    ///Title text to display. If specified as an array, text is rendered on multiple lines.
835    text: Vec<String>,
836
837    ///Padding to apply around the title. Only top and bottom are implemented.
838    #[serde(skip_serializing_if = "Option::is_none")]
839    padding: Option<Padding>,
840
841    ///position of the title
842    #[serde(skip_serializing_if = "Option::is_none")]
843    position: Option<Position>
844
845}
846
847impl From<String> for Title {
848    fn from(value: String) -> Self {
849        Title::new(value)
850    }
851}
852
853impl From<&str> for Title {
854    fn from(value: &str) -> Self {
855        Title::from(value.to_string())
856    }
857}
858
859impl Title{
860    pub fn new(text: String) -> Self {
861        Self{
862            display: true,
863            full_size: false,
864            text: vec![text],
865            padding: None,
866            position: None
867        }
868    }
869
870    pub fn from_str(text: &str) -> Self {
871        Self{
872            display: true,
873            full_size: false,
874            text: vec![text.to_string()],
875            padding: None,
876            position: None
877        }
878    }
879
880    pub fn from_array(text: Vec<String>) -> Self {
881        Self{
882            display: true,
883            full_size: false,
884            text,
885            padding: None,
886            position: None
887        }
888    }
889
890    pub fn with_display(mut self, display: bool) -> Self {
891        self.display = display;
892        self
893    }
894    pub fn with_full_size(mut self, full_size: bool) -> Self {
895        self.full_size = full_size;
896        self
897    }
898    pub fn with_text(mut self, text: Vec<String>) -> Self {
899        self.text = text;
900        self
901    }
902    pub fn with_padding(mut self, padding: Padding) -> Self {
903        self.padding = Some(padding);
904        self
905    }
906    pub fn with_position(mut self, position: Position) -> Self {
907        self.position = Some(position);
908        self
909    }
910}
911
912
913
914#[derive(Serialize, Deserialize, Debug, Clone)]
915#[serde(rename_all = "lowercase")]
916pub enum TooltipMode{
917    Average,
918    Nearest
919}
920
921
922impl Default for Tooltip {
923    fn default() -> Self {
924        Tooltip{
925            enabled: true,
926            mode: None,
927            background_color: None,
928            title_color: None,
929            callbacks: Some(TooltipCallbacks::default())
930        }
931    }
932}
933
934#[derive(Debug, Clone)]
935pub struct Tooltip{
936    pub enabled: bool,
937    pub mode: Option<TooltipMode>,
938    pub background_color: Option<Rgb>,
939    pub title_color: Option<Rgb>,
940    pub callbacks: Option<TooltipCallbacks>
941}
942
943#[derive(Debug, Clone)]
944pub struct TooltipCallbacks{
945    pub before_title: Option<JsExpr>,
946    pub title: Option<JsExpr>,
947    pub after_title: Option<JsExpr>,
948    pub before_body: Option<JsExpr>,
949    pub before_label: Option<JsExpr>,
950    pub label: Option<JsExpr>,
951    pub label_color: Option<JsExpr>,
952    pub label_text_color: Option<JsExpr>,
953    pub after_label: Option<JsExpr>,
954    pub after_body: Option<JsExpr>,
955    pub before_footer: Option<JsExpr>,
956    pub footer: Option<JsExpr>,
957    pub after_footer: Option<JsExpr>,
958}
959
960impl Default for TooltipCallbacks{
961    fn default() -> Self {
962        TooltipCallbacks{
963            before_title: None,
964            title: Some(JsExpr(DISPLAY_FN)),
965            after_title: None,
966            before_body: None,
967            before_label: None,
968            label: None,
969            label_color: None,
970            label_text_color: None,
971            after_label: None,
972            after_body: None,
973            before_footer: None,
974            footer: None,
975            after_footer: None,
976        }
977    }
978}
979
980
981#[derive(Debug, Clone)]
982pub struct JsExpr(pub &'static str);
983
984impl std::fmt::Display for JsExpr {
985    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
986        // Print raw JS, no quotes
987        f.write_str(&self.0)
988    }
989}
990