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, 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 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: Plotly,
77 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 pub fn add_trace(&mut self, t: Box<dyn Trace>) {
94 self.plotly.add_trace(t);
95 }
96 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 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), );
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 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 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 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 }
220 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 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 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 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 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 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}