gnss_qc/
plot.rs

1use hifitime::Epoch;
2use maud::{html, Markup, PreEscaped, Render};
3use plotly::{
4    common::HoverInfo,
5    layout::{
6        update_menu::UpdateMenu, Axis, Center, DragMode, Mapbox, Margin, RangeSelector,
7        RangeSlider, SelectorButton, SelectorStep,
8    },
9    DensityMapbox,
10    Layout,
11    Plot as Plotly,
12    Scatter,
13    Scatter3D, //ScatterGeo,
14    ScatterMapbox,
15    ScatterPolar,
16    Trace,
17};
18
19use serde::Serialize;
20
21pub use plotly::{
22    color::NamedColor,
23    common::{Marker, MarkerSymbol, Mode, Visible},
24    layout::{
25        update_menu::{Button, ButtonBuilder},
26        MapboxStyle,
27    },
28};
29
30pub struct CompassArrow {
31    pub scatter: Box<ScatterPolar<f64, f64>>,
32}
33
34impl CompassArrow {
35    /// Creates new [CompassArrow] to be projected in Polar.
36    /// tip_base_fraction: fraction of r base as unitary fraction.
37    /// tip_angle_deg: angle with base in degrees
38    pub fn new(
39        mode: Mode,
40        rho: f64,
41        theta: f64,
42        hover_text: String,
43        visible: bool,
44        tip_base_fraction: f64,
45        tip_angle_deg: f64,
46    ) -> Self {
47        let (tip_left_rho, tip_left_theta) =
48            (rho * (1.0 - tip_base_fraction), theta + tip_angle_deg);
49        let (tip_right_rho, tip_right_theta) =
50            (rho * (1.0 - tip_base_fraction), theta - tip_angle_deg);
51        Self {
52            scatter: {
53                ScatterPolar::new(
54                    vec![0.0, theta, tip_left_theta, theta, tip_right_theta],
55                    vec![0.0, rho, tip_left_rho, rho, tip_right_rho],
56                )
57                .mode(mode)
58                .web_gl_mode(true)
59                .hover_text_array(vec![hover_text])
60                .hover_info(HoverInfo::All)
61                .visible({
62                    if visible {
63                        Visible::True
64                    } else {
65                        Visible::LegendOnly
66                    }
67                })
68                .connect_gaps(false)
69            },
70        }
71    }
72}
73
74pub struct Plot {
75    /// [Plotly]
76    plotly: Plotly,
77    /// html (div) id
78    plot_id: String,
79}
80
81impl Render for Plot {
82    fn render(&self) -> Markup {
83        html! {
84            div id=(&self.plot_id) {
85                (PreEscaped (self.plotly.to_inline_html(None)))
86            }
87        }
88    }
89}
90
91impl Plot {
92    /// Adds one [Trace] to self
93    pub fn add_trace(&mut self, t: Box<dyn Trace>) {
94        self.plotly.add_trace(t);
95    }
96    /// Define custom controls for [Self]
97    pub fn add_custom_controls(&mut self, buttons: Vec<Button>) {
98        let layout = self.plotly.layout();
99        let layout = layout
100            .clone()
101            .update_menus(vec![UpdateMenu::new().y(0.8).buttons(buttons)]);
102        self.plotly.set_layout(layout);
103    }
104    /// Builds new standardized 1D Time domain plot
105    pub fn timedomain_plot(
106        plot_id: &str,
107        title: &str,
108        y_axis_label: &str,
109        show_legend: bool,
110    ) -> Self {
111        let mut buttons = Vec::<SelectorButton>::new();
112        for (step, count, label) in [
113            (SelectorStep::All, 1, "all"),
114            (SelectorStep::Second, 10, "10s"),
115            (SelectorStep::Second, 30, "30s"),
116            (SelectorStep::Minute, 1, "1min"),
117            (SelectorStep::Hour, 1, "1hr"),
118            (SelectorStep::Hour, 4, "4hr"),
119            (SelectorStep::Day, 1, "1day"),
120            (SelectorStep::Month, 1, "1mon"),
121        ] {
122            buttons.push(
123                SelectorButton::new().count(count).label(label).step(step), //.step_mode(StepMode::ToDate/Backward)
124            );
125        }
126        let layout = Layout::new()
127            .title(title)
128            .x_axis(
129                Axis::new()
130                    .title("MJD (UTC)")
131                    .zero_line(true)
132                    .show_tick_labels(true)
133                    .dtick(0.25)
134                    .range_slider(RangeSlider::new().visible(true))
135                    .range_selector(RangeSelector::new().buttons(buttons))
136                    .tick_format("{:05}"),
137            )
138            .y_axis(Axis::new().title(y_axis_label).zero_line(true))
139            .show_legend(show_legend)
140            .auto_size(true);
141        let mut plotly = Plotly::new();
142        plotly.set_layout(layout);
143        Self {
144            plotly,
145            plot_id: plot_id.to_string(),
146        }
147    }
148    /// Builds new 3D plot
149    pub fn plot_3d(
150        plot_id: &str,
151        title: &str,
152        x_label: &str,
153        y_label: &str,
154        z_label: &str,
155        show_legend: bool,
156    ) -> Self {
157        let layout = Layout::new()
158            .title(title)
159            .x_axis(
160                Axis::new()
161                    .title(x_label)
162                    .zero_line(true)
163                    .show_tick_labels(false),
164            )
165            .y_axis(
166                Axis::new()
167                    .title(y_label)
168                    .zero_line(true)
169                    .show_tick_labels(false),
170            )
171            .z_axis(
172                Axis::new()
173                    .title(z_label)
174                    .zero_line(true)
175                    .show_tick_labels(false),
176            )
177            .auto_size(true)
178            .show_legend(show_legend);
179        let mut plotly = Plotly::new();
180        plotly.set_layout(layout);
181        Self {
182            plotly,
183            plot_id: plot_id.to_string(),
184        }
185    }
186    /// Builds new Skyplot
187    pub fn sky_plot(plot_id: &str, title: &str, show_legend: bool) -> Self {
188        Self::polar_plot(
189            plot_id,
190            title,
191            "Elevation (Deg°)",
192            "Azimuth (Deg°)",
193            show_legend,
194        )
195    }
196    /// Trace for a skyplot
197    pub fn sky_trace<T: Default + Clone + Serialize>(
198        name: &str,
199        t: &Vec<Epoch>,
200        elev: Vec<T>,
201        azim: Vec<T>,
202        visible: bool,
203    ) -> Box<ScatterPolar<T, T>> {
204        let txt = t.iter().map(|t| t.to_string()).collect::<Vec<_>>();
205        ScatterPolar::new(azim, elev)
206            .web_gl_mode(true)
207            .hover_text_array(txt)
208            .hover_info(HoverInfo::All)
209            .visible({
210                if visible {
211                    Visible::True
212                } else {
213                    Visible::LegendOnly
214                }
215            })
216            .connect_gaps(false)
217            .name(name)
218        //TODO alpha gradient per time
219    }
220    /// Builds new Polar plot
221    pub fn polar_plot(
222        plot_id: &str,
223        title: &str,
224        x_label: &str,
225        y_label: &str,
226        show_legend: bool,
227    ) -> Self {
228        let mut plotly = Plotly::new();
229
230        let layout = Layout::new()
231            .title(title)
232            .x_axis(Axis::new().title(x_label).zero_line(true))
233            .y_axis(Axis::new().title(y_label).zero_line(true))
234            .show_legend(show_legend)
235            .auto_size(true);
236
237        plotly.set_layout(layout);
238
239        Self {
240            plotly,
241            plot_id: plot_id.to_string(),
242        }
243    }
244    /// Builds new World Map
245    pub fn world_map(
246        plot_id: &str,
247        title: &str,
248        map_style: MapboxStyle,
249        center_ddeg: (f64, f64),
250        zoom: u8,
251        show_legend: bool,
252    ) -> Self {
253        let layout = Layout::new()
254            .title(title)
255            .drag_mode(DragMode::Zoom)
256            .margin(Margin::new().top(0).left(0).bottom(0).right(0))
257            .show_legend(show_legend)
258            .mapbox(
259                Mapbox::new()
260                    .style(map_style)
261                    .center(Center::new(center_ddeg.0, center_ddeg.1))
262                    .zoom(zoom),
263            );
264        let mut plotly = Plotly::new();
265        plotly.set_layout(layout);
266        Self {
267            plotly,
268            plot_id: plot_id.to_string(),
269        }
270    }
271    /// Builds new Mapbox trace
272    pub fn mapbox<T: Clone + Default + Serialize>(
273        lat: Vec<T>,
274        lon: Vec<T>,
275        legend: &str,
276        size: usize,
277        symbol: MarkerSymbol,
278        color: Option<NamedColor>,
279        opacity: f64,
280        visible: bool,
281    ) -> Box<ScatterMapbox<T, T>> {
282        let mut marker = Marker::new().size(size).symbol(symbol).opacity(opacity);
283        if let Some(color) = color {
284            marker = marker.color(color);
285        }
286        ScatterMapbox::new(lat, lon)
287            .marker(marker)
288            .name(legend)
289            .visible({
290                if visible {
291                    Visible::True
292                } else {
293                    Visible::LegendOnly
294                }
295            })
296    }
297    // /// Builds ScatterGeo
298    // pub fn scattergeo<T: Clone + Default + Serialize>(
299    //     lat: Vec<T>,
300    //     lon: Vec<T>,
301    //     legend: &str,
302    // ) -> Box<ScatterGeo<T, T>> {
303    //     ScatterGeo::new(lat, lon).name(legend)
304    // }
305    /// Builds new Density Mapbox trace
306    pub fn density_mapbox<T: Clone + Default + Serialize>(
307        lat: Vec<T>,
308        lon: Vec<T>,
309        z: Vec<T>,
310        legend: &str,
311        opacity: f64,
312        zoom: u8,
313        visible: bool,
314    ) -> Box<DensityMapbox<T, T, T>> {
315        DensityMapbox::new(lat, lon, z)
316            .name(legend)
317            .opacity(opacity)
318            .zauto(true)
319            .zoom(zoom)
320            .visible({
321                if visible {
322                    Visible::True
323                } else {
324                    Visible::LegendOnly
325                }
326            })
327    }
328    /// Builds new 3D chart
329    pub fn chart_3d<T: Clone + Default + Serialize>(
330        name: &str,
331        mode: Mode,
332        symbol: MarkerSymbol,
333        t: &Vec<Epoch>,
334        x: Vec<T>,
335        y: Vec<T>,
336        z: Vec<T>,
337    ) -> Box<Scatter3D<T, T, T>> {
338        let txt = t.iter().map(|t| t.to_string()).collect::<Vec<_>>();
339        Scatter3D::new(x, y, z)
340            .mode(mode)
341            .name(name)
342            .hover_text_array(txt)
343            .hover_info(HoverInfo::All)
344            .marker(Marker::new().symbol(symbol))
345    }
346    /// Builds new Time domain chart
347    pub fn timedomain_chart<Y: Clone + Default + Serialize>(
348        name: &str,
349        mode: Mode,
350        symbol: MarkerSymbol,
351        t: &Vec<Epoch>,
352        y: Vec<Y>,
353        visible: bool,
354    ) -> Box<Scatter<f64, Y>> {
355        let txt = t.iter().map(|t| t.to_string()).collect::<Vec<_>>();
356        Scatter::new(t.iter().map(|t| t.to_mjd_utc_days()).collect(), y)
357            .name(name)
358            .mode(mode)
359            .web_gl_mode(true)
360            .hover_text_array(txt)
361            .hover_info(HoverInfo::All)
362            .marker(Marker::new().symbol(symbol))
363            .visible({
364                if visible {
365                    Visible::True
366                } else {
367                    Visible::LegendOnly
368                }
369            })
370    }
371}