workflow_d3/
graph.rs

1#![allow(dead_code)]
2
3use crate::container::*;
4use crate::d3::{self, D3};
5use crate::imports::*;
6use atomic_float::AtomicF64;
7use std::rc::Rc;
8use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
9use web_sys::{Element, HtmlCanvasElement};
10use workflow_core::time::*;
11use workflow_dom::inject::*;
12use workflow_log::log_error;
13use workflow_wasm::prelude::*;
14
15static mut DOM_INIT: bool = false;
16
17const ONE_DAY_MSEC: u64 = DAYS;
18const ONE_DAY_SEC: u64 = DAYS / 1000;
19const LOWREW_CELL_SIZE: u64 = ONE_DAY_SEC / 4096;
20
21#[derive(Clone)]
22pub struct GraphDuration;
23
24impl GraphDuration {
25    pub fn parse<T: Into<String>>(value: T) -> std::result::Result<Duration, Error> {
26        let value: String = value.into();
27        let millis = if value.contains('s') {
28            let seconds = value.replace('s', "").parse::<u64>()?;
29            seconds * SECONDS
30        } else if value.contains('m') {
31            let minutes = value.replace('m', "").parse::<u64>()?;
32            minutes * MINUTES
33        } else if value.contains('h') {
34            let hours = value.replace('h', "").parse::<u64>()?;
35            hours * HOURS
36        } else if value.contains('d') {
37            let days = value.replace('d', "").parse::<u64>()?;
38            days * DAYS
39        } else {
40            return Err(Error::Custom(format!("Invalid timeline str: {value:?}")));
41        };
42
43        Ok(Duration::from_millis(millis))
44    }
45}
46
47#[derive(Clone)]
48pub struct GraphThemeOptions {
49    pub area_fill_color: String,
50    pub area_stroke_color: String,
51    pub x_axis_color: String,
52    pub y_axis_color: String,
53    pub title_color: String,
54    pub x_axis_font: String,
55    pub y_axis_font: String,
56    pub title_font: String,
57    pub y_caption_font: String,
58    pub y_caption_color: String,
59    // pub value_color: String,
60    // pub value_font: String,
61}
62
63impl GraphThemeOptions {
64    pub fn new(
65        font_name: &str,
66        title_color: &str,
67        fill_color: &str,
68        stroke_color: &str,
69        axis_color: &str,
70    ) -> GraphThemeOptions {
71        GraphThemeOptions {
72            title_font: format!("30px {font_name}"),
73            x_axis_font: format!("20px {font_name}"),
74            y_axis_font: format!("20px {font_name}"),
75            area_fill_color: fill_color.into(),
76            area_stroke_color: stroke_color.into(),
77            x_axis_color: axis_color.into(),
78            y_axis_color: axis_color.into(),
79            title_color: title_color.into(),
80            y_caption_color: axis_color.into(),
81            y_caption_font: format!("15px {font_name}"),
82        }
83    }
84}
85
86#[derive(Clone)]
87pub enum GraphTheme {
88    Light,
89    Dark,
90    Custom(Box<GraphThemeOptions>),
91}
92
93impl GraphTheme {
94    pub fn get_options(self) -> GraphThemeOptions {
95        match self {
96            Self::Light => Self::light_theme_options(),
97            Self::Dark => Self::dark_theme_options(),
98            Self::Custom(theme) => *theme,
99        }
100    }
101    pub fn light_theme_options() -> GraphThemeOptions {
102        let font = "'Consolas', 'Lucida Grande', 'Roboto Mono', 'Source Code Pro', 'Trebuchet'";
103        GraphThemeOptions {
104            // title_font: format!("bold 30px {font}"),
105            title_font: format!("30px {font}"),
106            x_axis_font: format!("20px {font}"),
107            y_axis_font: format!("20px {font}"),
108            //value_font: String::from("bold 23px sans-serif"),
109            area_fill_color: String::from("rgb(220, 231, 240)"),
110            area_stroke_color: String::from("rgb(17, 125, 187)"),
111            x_axis_color: String::from("black"),
112            y_axis_color: String::from("black"),
113            title_color: String::from("black"),
114            //value_color: String::from("black"),
115            y_caption_color: String::from("#343434"),
116            y_caption_font: String::from("15px {font}"),
117        }
118    }
119    pub fn dark_theme_options() -> GraphThemeOptions {
120        let font = "'Consolas', 'Lucida Grande', 'Roboto Mono', 'Source Code Pro', 'Trebuchet'";
121        GraphThemeOptions {
122            // title_font: format!("bold 30px {font}"),
123            title_font: format!("30px {font}"),
124            x_axis_font: format!("20px {font}"),
125            y_axis_font: format!("20px {font}"),
126            //value_font: String::from("bold 23px sans-serif"),
127            area_fill_color: String::from("grey"),
128            area_stroke_color: String::from("white"),
129            x_axis_color: String::from("white"),
130            y_axis_color: String::from("white"),
131            title_color: String::from("white"),
132            //value_color: String::from("white"),
133            y_caption_color: String::from("white"),
134            y_caption_font: format!("15px {font}"),
135        }
136    }
137}
138
139pub struct Margin {
140    pub left: f32,
141    pub right: f32,
142    pub top: f32,
143    pub bottom: f32,
144}
145
146impl Margin {
147    pub fn new(left: f32, right: f32, top: f32, bottom: f32) -> Self {
148        Self {
149            left,
150            right,
151            top,
152            bottom,
153        }
154    }
155}
156
157struct Inner {
158    width: f32,
159    height: f32,
160    full_width: f32,
161    full_height: f32,
162    margin_left: f32,
163    margin_right: f32,
164    margin_top: f32,
165    margin_bottom: f32,
166    // min_date: js_sys::Date,
167    value: String,
168    title_box_height: f64,
169    x_tick_width: f64,
170    title_padding_y: f64,
171    duration: Duration,
172    retention: Duration,
173}
174
175#[derive(Clone)]
176pub struct Graph {
177    #[allow(dead_code)]
178    element: Element,
179    canvas: HtmlCanvasElement,
180    context: web_sys::CanvasRenderingContext2d,
181
182    inner: Arc<Mutex<Inner>>,
183    x: Rc<d3::ScaleTime>,
184    y: Rc<d3::ScaleLinear>,
185    area: Rc<d3::Area>,
186    data_hirez: Array,
187    data_lowrez: Array,
188    lowrez_cell: Rc<AtomicU64>,
189    lowrez_cell_value: Rc<AtomicF64>,
190    x_tick_size: f64,
191    y_tick_size: f64,
192    x_tick_count: u32,
193    y_tick_count: u32,
194    y_tick_padding: f64,
195    title: Option<String>,
196    y_caption: String,
197    options: Arc<Mutex<GraphThemeOptions>>,
198    time: Arc<AtomicU64>,
199    redraw: Arc<AtomicBool>,
200    last_draw_time: Arc<AtomicU64>,
201
202    /// holds references to [Callback](workflow_wasm::callback::Callback)
203    pub callbacks: CallbackMap,
204}
205
206unsafe impl Sync for Graph {}
207unsafe impl Send for Graph {}
208
209const DEFAULT_STYLE: &str = include_str!("graph.css");
210
211impl Graph {
212    pub async fn try_init(id: Option<&str>) -> Result<()> {
213        if !unsafe { DOM_INIT } {
214            inject_css(id, DEFAULT_STYLE)?;
215            unsafe {
216                DOM_INIT = true;
217            }
218        }
219
220        Ok(())
221    }
222
223    pub async fn default_style() -> Result<String> {
224        Ok(DEFAULT_STYLE.to_string())
225    }
226
227    pub async fn replace_graph_style(id: &str, css: &str) -> Result<()> {
228        inject_css(Some(id), css)?;
229        window().dispatch_event(&web_sys::Event::new("resize")?)?;
230        Ok(())
231    }
232
233    #[allow(clippy::too_many_arguments)]
234    pub async fn try_new<T: Into<String>>(
235        window: &web_sys::Window,
236        container: &Arc<Container>,
237        title: Option<T>,
238        y_caption: T,
239        duration: Duration,
240        retention: Duration,
241        theme: GraphTheme,
242        margin: Margin,
243    ) -> Result<Graph> {
244        let document = window.document().unwrap();
245        let element = document.create_element("div").unwrap();
246        container.element().append_child(&element).unwrap();
247
248        element.set_class_name("graph");
249        let canvas: Element = document.create_element("canvas").unwrap();
250        element.append_child(&canvas).unwrap();
251        let canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().unwrap();
252        let context: web_sys::CanvasRenderingContext2d = canvas
253            .get_context("2d")
254            .unwrap()
255            .unwrap()
256            .dyn_into::<web_sys::CanvasRenderingContext2d>()
257            .unwrap();
258
259        let options = Arc::new(Mutex::new(theme.get_options()));
260
261        let mut graph: Graph = Graph {
262            element,
263            inner: Arc::new(Mutex::new(Inner {
264                width: 0.0,
265                height: 0.0,
266                full_width: 0.0,
267                full_height: 0.0,
268                margin_left: margin.left,
269                margin_right: margin.right,
270                margin_top: margin.top,
271                margin_bottom: margin.bottom,
272                // min_date: js_sys::Date::new_0(),
273                value: "".into(),
274                title_box_height: 20.0,
275                title_padding_y: 20.0,
276                x_tick_width: 20.0,
277                duration,
278                retention,
279            })),
280            x: Rc::new(D3::scale_time()),
281            y: Rc::new(D3::scale_linear()),
282            area: Rc::new(D3::area()),
283            data_hirez: Array::new(),
284            data_lowrez: Array::new(),
285            lowrez_cell: Rc::new(AtomicU64::new(0)),
286            lowrez_cell_value: Rc::new(AtomicF64::new(0.0)),
287            canvas,
288            context,
289            x_tick_size: 6.0,
290            y_tick_size: 6.0,
291            x_tick_count: 10,
292            y_tick_count: 10,
293            y_tick_padding: 3.0,
294            title: title.map(|title| title.into()),
295            y_caption: y_caption.into(),
296            options,
297            callbacks: CallbackMap::new(),
298            time: Arc::new(AtomicU64::new(0)),
299            redraw: Arc::new(AtomicBool::new(true)),
300            last_draw_time: Arc::new(AtomicU64::new(0)),
301        };
302        graph.init().await?;
303        Ok(graph)
304    }
305
306    pub fn set_title<T: Into<String>>(mut self, title: T) -> Self {
307        self.title = Some(title.into());
308        self
309    }
310
311    pub fn set_x_tick_size(mut self, tick_size: f64) -> Self {
312        self.x_tick_size = tick_size;
313        self
314    }
315
316    pub fn set_y_tick_size(mut self, tick_size: f64) -> Self {
317        self.y_tick_size = tick_size;
318        self
319    }
320
321    pub fn set_x_tick_count(mut self, tick_count: u32) -> Self {
322        self.x_tick_count = tick_count;
323        self
324    }
325
326    pub fn set_y_tick_count(mut self, tick_count: u32) -> Self {
327        self.y_tick_count = tick_count;
328        self
329    }
330
331    pub fn set_y_tick_padding(mut self, tick_padding: f64) -> Self {
332        self.y_tick_padding = tick_padding;
333        self
334    }
335
336    pub fn options(&self) -> MutexGuard<GraphThemeOptions> {
337        self.options.lock().unwrap()
338    }
339
340    fn inner(&self) -> MutexGuard<Inner> {
341        self.inner.lock().unwrap()
342    }
343
344    pub fn set_title_font<T: Into<String>>(&self, font: T) -> &Self {
345        self.options().title_font = font.into();
346        self
347    }
348
349    pub fn set_x_axis_font<T: Into<String>>(&self, font: T) -> &Self {
350        self.options().x_axis_font = font.into();
351        self
352    }
353
354    pub fn set_y_axis_font<T: Into<String>>(&self, font: T) -> &Self {
355        self.options().y_axis_font = font.into();
356        self
357    }
358
359    pub fn set_area_fill_color<T: Into<String>>(&self, color: T) -> &Self {
360        self.options().area_fill_color = color.into();
361        self
362    }
363
364    pub fn set_area_stroke_color<T: Into<String>>(&self, color: T) -> &Self {
365        self.options().area_stroke_color = color.into();
366        self
367    }
368
369    pub fn set_x_axis_color<T: Into<String>>(&self, color: T) -> &Self {
370        self.options().x_axis_color = color.into();
371        self
372    }
373
374    pub fn set_y_axis_color<T: Into<String>>(&self, color: T) -> &Self {
375        self.options().y_axis_color = color.into();
376        self
377    }
378
379    pub fn set_title_color<T: Into<String>>(&self, color: T) -> &Self {
380        self.options().title_color = color.into();
381        self
382    }
383
384    pub fn set_y_caption_color<T: Into<String>>(&self, color: T) -> &Self {
385        self.options().y_caption_color = color.into();
386        self
387    }
388
389    pub fn set_y_caption_font<T: Into<String>>(&self, font: T) -> &Self {
390        self.options().y_caption_font = font.into();
391        self
392    }
393
394    pub fn set_theme(&self, theme: GraphTheme) -> Result<()> {
395        {
396            *self.options() = theme.get_options();
397        }
398        self.calculate_title_box()?;
399        self.draw()?;
400        Ok(())
401    }
402
403    pub fn set_duration(&self, duration: Duration) -> Result<()> {
404        self.inner().duration = duration;
405        self.draw()?;
406        Ok(())
407    }
408
409    pub fn duration(&self) -> Duration {
410        self.inner().duration
411    }
412
413    // fn set_cell_value(&self, value: f64) -> Result<()> {
414    //     self.lowrez_cell_value.store(value, Ordering::Relaxed);
415    //     self.draw()?;
416    //     Ok(())
417    // }
418
419    pub fn redraw(&self) {
420        self.redraw.store(true, Ordering::Relaxed);
421    }
422
423    pub fn needs_redraw(&self) -> bool {
424        let flag = self.redraw.load(Ordering::Relaxed);
425        if flag {
426            self.redraw.store(false, Ordering::Relaxed);
427        }
428        flag
429    }
430
431    pub async fn init(&mut self) -> Result<()> {
432        self.calculate_title_box()?;
433        self.update_size()?;
434        self.update_x_domain()?;
435        self.x.set_clamp(true);
436        // line = d3.line()
437        //     .x(function(d) { return x(d.date); })
438        //     .y(function(d) { return y(d.value); })
439        //     .curve(d3.curveStep)
440        //     .context(context);
441
442        let height = self.height();
443        let that = self.clone();
444        let x_cb = callback!(move |d: js_sys::Object| {
445            that.x.call1(&JsValue::NULL, &d.get_value("date").unwrap())
446        });
447        let that = self.clone();
448        let y_cb = callback!(move |d: js_sys::Object| {
449            that.y.call1(&JsValue::NULL, &d.get_value("value").unwrap())
450        });
451        self.area
452            .x(x_cb.get_fn())
453            .y0(height)
454            .y1(y_cb.get_fn())
455            .context(&self.context);
456
457        let that = self.clone();
458        let on_resize = callback!(move || { that.update_size() });
459
460        window().add_event_listener_with_callback("resize", on_resize.get_fn())?;
461
462        self.callbacks.retain(x_cb)?;
463        self.callbacks.retain(y_cb)?;
464        self.callbacks.retain(on_resize)?;
465
466        Ok(())
467    }
468
469    fn update_size(&self) -> Result<()> {
470        let rect = self.canvas.get_bounding_client_rect();
471        let pixel_ratio = workflow_dom::utils::window().device_pixel_ratio() as f32;
472        //workflow_log::log_info!("rectrectrect: {:?}, pixel_ratio:{pixel_ratio}", rect);
473        let width = (pixel_ratio * rect.right() as f32).round()
474            - (pixel_ratio * rect.left() as f32).round();
475        let height = (pixel_ratio * rect.bottom() as f32).round()
476            - (pixel_ratio * rect.top() as f32).round();
477        self.canvas.set_width(width as u32);
478        self.canvas.set_height(height as u32);
479        let (height, margin_left, margin_top) = {
480            let mut inner = self.inner();
481            inner.width = width - inner.margin_left - inner.margin_right;
482            inner.height = height
483                - inner.margin_top
484                - inner.margin_bottom
485                - inner.title_box_height as f32
486                - inner.title_padding_y as f32;
487            inner.full_width = width;
488            inner.full_height = height;
489
490            self.x.range([0.0, inner.width]);
491            self.y.range([inner.height, 0.0]);
492            (
493                inner.height,
494                inner.margin_left,
495                inner.margin_top as f64 + inner.title_box_height + inner.title_padding_y,
496            )
497        };
498        let context = &self.context;
499        context.translate(margin_left as f64, margin_top)?;
500        self.x_axis()?;
501        self.y_axis()?;
502        self.area.y0(height);
503        self.redraw();
504        Ok(())
505    }
506
507    pub fn height(&self) -> f32 {
508        self.inner().height
509    }
510    pub fn width(&self) -> f32 {
511        self.inner().width
512    }
513    // pub fn min_date(&self) -> js_sys::Date {
514    //     self.inner().min_date.clone()
515    // }
516
517    pub fn set_value<T: Into<String>>(&self, value: T) {
518        self.inner().value = value.into();
519    }
520
521    pub fn value(&self) -> String {
522        self.inner().value.clone()
523    }
524
525    pub fn title_box_height(&self) -> f64 {
526        self.inner().title_box_height
527    }
528
529    pub fn x_tick_width(&self) -> f64 {
530        self.inner().x_tick_width
531    }
532
533    // pub fn value_color(&self) -> String {
534    //     self.options().value_color.clone()
535    // }
536
537    // pub fn value_font(&self) -> String {
538    //     self.options().value_font.clone()
539    // }
540
541    pub fn area_fill_color(&self) -> String {
542        self.options().area_fill_color.clone()
543    }
544    pub fn area_stroke_color(&self) -> String {
545        self.options().area_stroke_color.clone()
546    }
547    pub fn area_color(&self) -> (String, String) {
548        let options = self.options();
549        (
550            options.area_fill_color.clone(),
551            options.area_stroke_color.clone(),
552        )
553    }
554    pub fn title_font(&self) -> String {
555        self.options().title_font.clone()
556    }
557    pub fn title_color(&self) -> String {
558        self.options().title_color.clone()
559    }
560    pub fn x_axis_font(&self) -> String {
561        self.options().x_axis_font.clone()
562    }
563    pub fn x_axis_color(&self) -> String {
564        self.options().x_axis_color.clone()
565    }
566    pub fn y_caption_font(&self) -> String {
567        self.options().y_caption_font.clone()
568    }
569    pub fn y_caption_color(&self) -> String {
570        self.options().y_caption_color.clone()
571    }
572
573    fn x_axis(&self) -> Result<()> {
574        let width = self.width();
575        let tick_count = self.x_tick_count;
576        let tick_size = self.x_tick_size;
577        // let tick_width = self.x_tick_width() as f32;
578        // let count = (width / tick_width) as u32;
579        //let ticks = self.x.ticks(count);
580        let ticks = self.x.ticks(tick_count);
581        // let count2 = ticks.length();
582        let tick_format = self.x.tick_format();
583        let context = &self.context;
584        //workflow_log::log_info!("tick_format:::: {:?}", tick_format);
585        let options = self.options();
586        let height = self.height();
587
588        context.begin_path();
589        context.move_to(0.0, height as f64);
590        context.line_to(width as f64, height as f64);
591        context.set_stroke_style(&JsValue::from(&options.x_axis_color));
592        context.stroke();
593
594        context.begin_path();
595        for tick in ticks.clone() {
596            //workflow_log::log_info!("tick:::: {:?}", tick);
597            let x = self
598                .x
599                .call1(&JsValue::NULL, &tick)
600                .unwrap()
601                .as_f64()
602                .unwrap();
603            //workflow_log::log_info!("tick::::x: {:?}", x);
604            context.move_to(x, height as f64);
605            context.line_to(x, height as f64 + tick_size);
606        }
607        context.set_stroke_style(&JsValue::from(&options.x_axis_color));
608        context.stroke();
609
610        // used for debugging
611
612        context.set_text_align("center");
613        context.set_text_baseline("top");
614        context.set_fill_style(&JsValue::from(&options.x_axis_color));
615        context.set_font(&options.x_axis_font);
616        // context.fill_text(
617        //     &format!("{tick_width}/{width}/{count}/{count2}"),
618        //     150.0,
619        //     40.0,
620        // )?;
621
622        let mut last_end = 0.0;
623        for tick in ticks {
624            let x = self
625                .x
626                .call1(&JsValue::NULL, &tick)
627                .unwrap()
628                .as_f64()
629                .unwrap();
630            if x < last_end {
631                continue;
632            }
633
634            let text = tick_format
635                .call1(&JsValue::NULL, &tick)
636                .unwrap()
637                .as_string()
638                .unwrap();
639            context.fill_text(&text, x, height as f64 + tick_size)?;
640            let m = context.measure_text(&text).unwrap();
641            last_end = x + m.width() + 2.0;
642        }
643
644        Ok(())
645    }
646
647    fn y_axis(&self) -> Result<()> {
648        let tick_count = self.y_tick_count;
649        let tick_size = self.y_tick_size;
650        let tick_padding = self.y_tick_padding;
651        let ticks = self.y.ticks(tick_count);
652        let tick_format = self.y.tick_format();
653        let context = &self.context;
654        context.begin_path();
655        let options = self.options();
656        for tick in ticks.clone() {
657            let y = self
658                .y
659                .call1(&JsValue::NULL, &tick)
660                .unwrap()
661                .as_f64()
662                .unwrap();
663            context.move_to(0.0, y);
664            context.line_to(-tick_size, y);
665        }
666        context.set_stroke_style(&JsValue::from(&options.y_axis_color));
667        context.stroke();
668        let height = self.height();
669        context.begin_path();
670        context.move_to(-tick_size, 0.0);
671        context.line_to(0.0, 0.0);
672        context.line_to(0.0, height as f64);
673        context.line_to(-tick_size, height as f64);
674        context.set_stroke_style(&JsValue::from(&options.y_axis_color));
675        context.stroke();
676
677        context.set_text_align("right");
678        context.set_text_baseline("middle");
679        context.set_fill_style(&JsValue::from(&options.y_axis_color));
680        context.set_font(&options.y_axis_font);
681        for tick in ticks {
682            let y = self
683                .y
684                .call1(&JsValue::NULL, &tick)
685                .unwrap()
686                .as_f64()
687                .unwrap();
688            let text = tick_format
689                .call1(&JsValue::NULL, &tick)
690                .unwrap()
691                .as_string()
692                .unwrap();
693            context.fill_text(&text, -tick_size - tick_padding, y)?;
694        }
695        Ok(())
696    }
697
698    fn calculate_title_box(&self) -> Result<()> {
699        let context = &self.context;
700        let title_font = self.title_font();
701        let title_color = self.title_color();
702        let x_axis_font = self.x_axis_font();
703
704        context.save();
705        context.set_text_baseline("top");
706        context.set_font(&title_font);
707        context.set_fill_style(&JsValue::from(&title_color));
708        let metrics = if let Some(title) = self.title.as_ref() {
709            context.measure_text(&format!("{} {}", title, self.value()))?
710        } else {
711            context.measure_text(&self.value())?
712        };
713
714        context.set_font(&x_axis_font);
715        let x_metrics = context.measure_text("_00:00PM_")?;
716
717        {
718            let mut inner = self.inner();
719            inner.title_box_height = metrics.actual_bounding_box_ascent().abs()
720                + metrics.actual_bounding_box_descent().abs();
721            inner.x_tick_width = x_metrics.width();
722        }
723
724        context.restore();
725
726        Ok(())
727    }
728
729    fn draw_all_captions(&self) -> Result<()> {
730        self.draw_axis_captions()?;
731        self.draw_title(false)?;
732        Ok(())
733    }
734
735    fn draw_axis_captions(&self) -> Result<()> {
736        let context = &self.context;
737        let y_caption_color = self.y_caption_color();
738        let y_caption_font = self.y_caption_font();
739        // let value_color = self.value_color();
740        // let value_font = self.value_font();
741        context.save();
742        context.rotate(-std::f64::consts::PI / 2.0)?;
743        context.set_text_align("right");
744        context.set_text_baseline("top");
745        context.set_font(&y_caption_font);
746        context.set_fill_style(&JsValue::from(&y_caption_color));
747        context.fill_text(&self.y_caption, -10.0, 10.0)?;
748        context.restore();
749
750        Ok(())
751    }
752
753    fn draw_title(&self, clear: bool) -> Result<()> {
754        let context = &self.context;
755        let title_font = self.title_font();
756        let title_color = self.title_color();
757
758        context.save();
759
760        context.set_text_align("left");
761        context.set_text_baseline("top");
762        context.set_font(&title_font);
763        context.set_fill_style(&JsValue::from(&title_color));
764
765        {
766            let (y, height, width) = {
767                let inner = self.inner();
768                (
769                    -(inner.margin_top as f64
770                        + inner.title_box_height
771                        + inner.title_padding_y / 2.0),
772                    inner.title_box_height + inner.title_padding_y / 2.0,
773                    inner.width as f64,
774                )
775            };
776
777            if clear {
778                context.clear_rect(0.0, y, width, height);
779            }
780
781            if let Some(title) = self.title.as_ref() {
782                context.fill_text(&format!("{} {}", title, self.value()), 0.0, y)?;
783            } else {
784                context.fill_text(self.value().as_str(), 0.0, y)?;
785            }
786        }
787        context.restore();
788
789        Ok(())
790    }
791
792    pub fn _element(&self) -> &Element {
793        &self.element
794    }
795
796    pub fn clear(&self) -> Result<()> {
797        let inner = self.inner();
798        let context = &self.context;
799        context.clear_rect(
800            -inner.margin_left as f64,
801            -(inner.margin_top as f64 + inner.title_box_height + inner.title_padding_y),
802            inner.full_width as f64,
803            inner.full_height as f64,
804        );
805        Ok(())
806    }
807
808    fn update_x_domain(&self) -> Result<()> {
809        let date1 = js_sys::Date::new_0();
810        let time = date1.get_time();
811        let date2 = js_sys::Date::new(&time.into());
812        let inner = self.inner();
813        date2.set_time(time - inner.duration.as_millis() as f64);
814        let x_domain = js_sys::Array::new();
815        x_domain.push(&date2);
816        x_domain.push(&date1);
817
818        self.x.set_domain_array(x_domain);
819        Ok(())
820    }
821
822    fn update_axis_and_title(&self, data: &Array) -> Result<()> {
823        self.update_x_domain()?;
824        let cb = js_sys::Function::new_with_args("d", "return d.value");
825        // self.y.set_domain_array(D3::extent(&self.data, cb));
826        self.y.set_domain_array(D3::extent(data, cb));
827        self.clear()?;
828        self.x_axis()?;
829        self.y_axis()?;
830        self.draw_all_captions()?;
831
832        Ok(())
833    }
834
835    fn handle_retention(&self) -> Result<()> {
836        let limit = js_sys::Date::new_0();
837        limit.set_time(limit.get_time() - self.inner().retention.as_millis() as f64);
838
839        loop {
840            let first_item_date = self
841                .data_hirez
842                .at(0)
843                .dyn_into::<js_sys::Object>()?
844                .get_value("date")?
845                .dyn_into::<js_sys::Date>()?;
846            if first_item_date.lt(&limit) {
847                self.data_hirez.shift();
848            } else {
849                break;
850            }
851        }
852
853        loop {
854            let first_item_date = self
855                .data_lowrez
856                .at(0)
857                .dyn_into::<js_sys::Object>()?
858                .get_value("date")?
859                .dyn_into::<js_sys::Date>()?;
860            if first_item_date.lt(&limit) {
861                self.data_lowrez.shift();
862            } else {
863                break;
864            }
865        }
866
867        Ok(())
868    }
869
870    fn store(&self, time: f64, value_f64: f64) -> Result<()> {
871        let value = JsValue::from(value_f64);
872        // store ingested data point
873        let item = js_sys::Object::new();
874        let date = js_sys::Date::new(&JsValue::from(time));
875        item.set("date", &date)?;
876        item.set("value", &value)?;
877        self.data_hirez.push(&item.into());
878
879        let lowrez_cell = self.lowrez_cell.fetch_add(1, Ordering::SeqCst);
880        if lowrez_cell % LOWREW_CELL_SIZE == 0 {
881            let lowrez_cell_value = self.lowrez_cell_value.load(Ordering::SeqCst);
882            let lowrez_value = JsValue::from(lowrez_cell_value);
883            let item = js_sys::Object::new();
884            item.set("date", &date)?;
885            item.set("value", &lowrez_value)?;
886            self.data_lowrez.push(&item.into());
887        } else {
888            self.lowrez_cell_value
889                .fetch_max(value_f64, Ordering::SeqCst);
890        }
891
892        Ok(())
893    }
894
895    pub async fn ingest(&self, time: f64, value_f64: f64, text: &str) -> Result<()> {
896        // store text as value
897        self.set_value(text);
898
899        self.store(time, value_f64)?;
900
901        // cleanup data past retention period
902        self.handle_retention().unwrap_or_else(|err| {
903            log_error!("Error handling retention: {err:?}");
904        });
905
906        // store current time for redraw suppression
907        let time_u64 = time as u64;
908        self.time.store(time_u64, Ordering::Relaxed);
909
910        // calculate redraw suppression resolution
911        let msec = self.duration().as_millis() as f32;
912        let width = self.width();
913        let resolution = (msec / width) as u64;
914        let elapsed = time_u64 - self.last_draw_time.load(Ordering::SeqCst);
915        let needs_redraw = elapsed + 1000 > resolution;
916
917        if self.needs_redraw() || needs_redraw {
918            self.draw()?;
919        } else {
920            self.draw_title(true)?;
921        }
922
923        Ok(())
924    }
925
926    fn draw(&self) -> Result<()> {
927        let time_u64 = self.time.load(Ordering::SeqCst);
928        self.last_draw_time.store(time_u64, Ordering::SeqCst);
929
930        let secs = self.duration().as_secs() as u32;
931
932        let data = if secs > ONE_DAY_SEC as u32 {
933            let len = self.data_lowrez.length();
934            let cells = secs / LOWREW_CELL_SIZE as u32;
935            if let Some(start) = len.checked_sub(cells) {
936                self.data_lowrez.slice(start, len)
937            } else {
938                self.data_lowrez.clone()
939            }
940        } else {
941            let len = self.data_hirez.length();
942            if let Some(start) = len.checked_sub(secs) {
943                self.data_hirez.slice(start, len)
944            } else {
945                self.data_hirez.clone()
946            }
947        };
948
949        self.update_axis_and_title(&data)?;
950
951        let (area_fill_color, area_stroke_color) = self.area_color();
952
953        let context = &self.context;
954        context.begin_path();
955        self.area.call1(&JsValue::NULL, &data)?;
956        context.set_fill_style(&JsValue::from(&area_fill_color));
957        context.set_stroke_style(&JsValue::from(&area_stroke_color));
958        context.fill();
959        context.stroke();
960
961        Ok(())
962    }
963}