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