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 }
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!("30px {font}"),
106 x_axis_font: format!("20px {font}"),
107 y_axis_font: format!("20px {font}"),
108 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 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!("30px {font}"),
124 x_axis_font: format!("20px {font}"),
125 y_axis_font: format!("20px {font}"),
126 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 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 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 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 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 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 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 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 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 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 ticks = self.x.ticks(tick_count);
581 let tick_format = self.x.tick_format();
583 let context = &self.context;
584 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 let x = self
598 .x
599 .call1(&JsValue::NULL, &tick)
600 .unwrap()
601 .as_f64()
602 .unwrap();
603 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 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 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 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(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 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 self.set_value(text);
898
899 self.store(time, value_f64)?;
900
901 self.handle_retention().unwrap_or_else(|err| {
903 log_error!("Error handling retention: {err:?}");
904 });
905
906 let time_u64 = time as u64;
908 self.time.store(time_u64, Ordering::Relaxed);
909
910 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}