1use std::ffi::OsString;
2
3use plotters::{coord::Shift, prelude::*};
4use scarlet::colormap::{ColorMap, ListedColorMap};
5
6use crate::{colormap, error::VisualizerError, Backend};
7
8use autd3_driver::{autd3_device::AUTD3, geometry::Geometry};
9
10#[derive(Clone, Debug)]
11pub struct PlotConfig {
12 pub figsize: (u32, u32),
13 pub cbar_size: f32,
14 pub font_size: u32,
15 pub label_area_size: u32,
16 pub margin: u32,
17 pub ticks_step: f32,
18 pub cmap: ListedColorMap,
19 pub fname: OsString,
20}
21
22impl Default for PlotConfig {
23 fn default() -> Self {
24 Self {
25 figsize: (960, 640),
26 cbar_size: 0.15,
27 ticks_step: 10.,
28 label_area_size: 80,
29 margin: 10,
30 font_size: 24,
31 cmap: colormap::jet(),
32 fname: OsString::new(),
33 }
34 }
35}
36
37impl PartialEq for PlotConfig {
38 fn eq(&self, other: &Self) -> bool {
39 self.figsize == other.figsize
40 && self.cbar_size == other.cbar_size
41 && self.font_size == other.font_size
42 && self.label_area_size == other.label_area_size
43 && self.margin == other.margin
44 && self.ticks_step == other.ticks_step
45 && self.cmap.vals == other.cmap.vals
46 && self.fname == other.fname
47 }
48}
49
50pub struct PlottersBackend {}
51
52impl PlottersBackend {
53 fn plot_modulation_impl<B: plotters::backend::DrawingBackend>(
54 root: DrawingArea<B, Shift>,
55 modulation: Vec<f32>,
56 config: &PlotConfig,
57 ) -> Result<(), crate::error::VisualizerError>
58 where
59 VisualizerError:
60 From<DrawingAreaErrorKind<<B as plotters::backend::DrawingBackend>::ErrorType>>,
61 {
62 root.fill(&WHITE)?;
63
64 let mut chart = ChartBuilder::on(&root)
65 .margin(config.margin)
66 .x_label_area_size(config.label_area_size)
67 .y_label_area_size(config.label_area_size)
68 .build_cartesian_2d::<_, std::ops::Range<f32>>(0..modulation.len(), 0.0..1.0)?;
69
70 chart
71 .configure_mesh()
72 .disable_x_mesh()
73 .disable_y_mesh()
74 .x_label_style(("sans-serif", config.font_size).into_text_style(&root))
75 .y_label_style(("sans-serif", config.font_size).into_text_style(&root))
76 .x_desc("Index")
77 .y_desc("Modulation")
78 .draw()?;
79
80 chart.draw_series(LineSeries::new(
81 modulation.iter().enumerate().map(|(i, &v)| (i, v)),
82 BLUE.stroke_width(2),
83 ))?;
84
85 root.present()?;
86
87 Ok(())
88 }
89
90 fn plot_1d_impl<B: plotters::backend::DrawingBackend>(
91 root: &DrawingArea<B, Shift>,
92 observe_points: &[f32],
93 acoustic_pressures: &[autd3_driver::defined::Complex],
94 x_label: &str,
95 yrange: (f32, f32),
96 config: &PlotConfig,
97 ) -> Result<(), crate::error::VisualizerError>
98 where
99 VisualizerError:
100 From<DrawingAreaErrorKind<<B as plotters::backend::DrawingBackend>::ErrorType>>,
101 {
102 root.fill(&WHITE)?;
103
104 let xrange = observe_points
105 .iter()
106 .fold((f32::MAX, f32::MIN), |acc, &x| (acc.0.min(x), acc.1.max(x)));
107
108 let x_labels = ((xrange.1 - xrange.0).floor() / config.ticks_step) as usize + 1;
109
110 let mut chart = ChartBuilder::on(root)
111 .margin(config.margin)
112 .x_label_area_size(config.label_area_size)
113 .y_label_area_size(config.label_area_size)
114 .build_cartesian_2d(xrange.0..xrange.1, yrange.0..yrange.1)?;
115
116 chart
117 .configure_mesh()
118 .disable_x_mesh()
119 .disable_y_mesh()
120 .x_labels(x_labels)
121 .x_label_style(("sans-serif", config.font_size).into_text_style(root))
122 .y_label_style(("sans-serif", config.font_size).into_text_style(root))
123 .x_desc(x_label)
124 .y_desc("Amplitude [-]")
125 .draw()?;
126
127 chart.draw_series(LineSeries::new(
128 observe_points
129 .iter()
130 .zip(acoustic_pressures.iter())
131 .map(|(&x, v)| (x, v.norm())),
132 BLUE.stroke_width(2),
133 ))?;
134
135 root.present()?;
136
137 Ok(())
138 }
139
140 #[allow(clippy::too_many_arguments)]
141 fn plot_2d_impl<B: plotters::backend::DrawingBackend>(
142 root: &DrawingArea<B, Shift>,
143 observe_points_x: &[f32],
144 observe_points_y: &[f32],
145 acoustic_pressures: &[autd3_driver::defined::Complex],
146 x_label: &str,
147 y_label: &str,
148 zrange: (f32, f32),
149 resolution: f32,
150 config: &PlotConfig,
151 ) -> Result<(), crate::error::VisualizerError>
152 where
153 VisualizerError:
154 From<DrawingAreaErrorKind<<B as plotters::backend::DrawingBackend>::ErrorType>>,
155 {
156 root.fill(&WHITE)?;
157
158 let main_area_size_x = (config.figsize.0 as f32 * (1.0 - config.cbar_size)) as u32;
159
160 let (main_area, cbar_area) = root.split_horizontally(main_area_size_x);
161
162 let color_map_size = 1000;
163 let cmap: Vec<scarlet::color::RGBColor> = config
164 .cmap
165 .transform((0..=color_map_size).map(|x| x as f64 / color_map_size as f64));
166
167 {
168 let xrange = observe_points_x
169 .iter()
170 .fold((f32::MAX, f32::MIN), |acc, &x| (acc.0.min(x), acc.1.max(x)));
171 let yrange = observe_points_y
172 .iter()
173 .fold((f32::MAX, f32::MIN), |acc, &x| (acc.0.min(x), acc.1.max(x)));
174
175 let plot_range_x = xrange.1 - xrange.0;
176 let plot_range_y = yrange.1 - yrange.0;
177
178 let x_labels = (plot_range_x.floor() / config.ticks_step) as usize + 1;
179 let y_labels = (plot_range_y.floor() / config.ticks_step) as usize + 1;
180
181 let available_size_x = main_area_size_x - config.label_area_size - config.margin;
182 let available_size_y = config.figsize.1 - config.label_area_size - config.margin * 2;
183
184 let px_per_ps = (available_size_x as f32 / plot_range_x)
185 .min(available_size_y as f32 / plot_range_y);
186
187 let plot_size_x = (plot_range_x * px_per_ps) as u32;
188 let plot_size_y = (plot_range_y * px_per_ps) as u32;
189
190 let left_margin = config.margin
191 + (main_area_size_x - plot_size_x - config.label_area_size - config.margin).max(0)
192 / 2;
193 let right_margin = config.margin
194 + (main_area_size_x - plot_size_x - config.label_area_size - config.margin).max(0)
195 / 2;
196 let top_margin = config.margin
197 + (config.figsize.1 - plot_size_y - config.label_area_size - config.margin).max(0)
198 / 2;
199 let bottom_margin = config.margin
200 + (config.figsize.1 - plot_size_y - config.label_area_size - config.margin).max(0)
201 / 2;
202
203 let mut chart = ChartBuilder::on(&main_area)
204 .margin_left(left_margin)
205 .margin_top(top_margin)
206 .margin_bottom(bottom_margin)
207 .margin_right(right_margin)
208 .x_label_area_size(config.label_area_size)
209 .y_label_area_size(config.label_area_size)
210 .build_cartesian_2d(xrange.0..xrange.1, yrange.0..yrange.1)?;
211
212 chart
213 .configure_mesh()
214 .x_labels(x_labels)
215 .y_labels(y_labels)
216 .disable_x_mesh()
217 .disable_y_mesh()
218 .label_style(("sans-serif", config.font_size))
219 .x_desc(x_label)
220 .y_desc(y_label)
221 .draw()?;
222
223 chart.draw_series(
224 itertools::iproduct!(observe_points_y, observe_points_x)
225 .zip(acoustic_pressures.iter())
226 .map(|((&y, &x), c)| {
227 #[allow(clippy::unnecessary_cast)]
228 let c: scarlet::color::RGBColor = config.cmap.transform_single(
229 ((c.norm() - zrange.0) / (zrange.1 - zrange.0)) as f64,
230 );
231 Rectangle::new(
232 [(x, y), (x + resolution, y + resolution)],
233 RGBAColor(c.int_r(), c.int_g(), c.int_b(), 1.0).filled(),
234 )
235 }),
236 )?;
237 }
238
239 {
240 let mut chart = ChartBuilder::on(&cbar_area)
241 .margin_left(config.margin)
242 .margin_top(config.margin)
243 .margin_bottom(config.margin + config.label_area_size)
244 .margin_right(config.margin)
245 .y_label_area_size(config.label_area_size)
246 .set_label_area_size(LabelAreaPosition::Left, 0)
247 .set_label_area_size(LabelAreaPosition::Right, 80)
248 .build_cartesian_2d(0i32..1i32, 0i32..color_map_size)?;
249
250 chart
251 .configure_mesh()
252 .disable_x_axis()
253 .disable_x_mesh()
254 .disable_y_mesh()
255 .axis_style(BLACK.stroke_width(1))
256 .label_style(("sans-serif", config.font_size))
257 .y_label_formatter(&|&v| {
258 format!(
259 "{:.2}",
260 zrange.0 + (zrange.1 - zrange.0) * v as f32 / color_map_size as f32
261 )
262 })
263 .y_desc("Amplitude [-]")
264 .draw()?;
265
266 chart.draw_series(cmap.iter().enumerate().map(|(i, c)| {
267 Rectangle::new(
268 [(0, i as i32), (1, i as i32 + 1)],
269 RGBAColor(c.int_r(), c.int_g(), c.int_b(), 1.0).filled(),
270 )
271 }))?;
272
273 chart.draw_series([Rectangle::new(
274 [(0, 0), (1, color_map_size + 1)],
275 BLACK.stroke_width(1),
276 )])?;
277 }
278
279 root.present()?;
280
281 Ok(())
282 }
283
284 fn plot_phase_impl<B: plotters::backend::DrawingBackend>(
285 root: DrawingArea<B, Shift>,
286 config: &PlotConfig,
287 geometry: &Geometry,
288 phases: Vec<f32>,
289 ) -> Result<(), crate::error::VisualizerError>
290 where
291 VisualizerError:
292 From<DrawingAreaErrorKind<<B as plotters::backend::DrawingBackend>::ErrorType>>,
293 {
294 root.fill(&WHITE)?;
295
296 let main_area_size_x = (config.figsize.0 as f32 * (1.0 - config.cbar_size)) as u32;
297
298 let (main_area, cbar_area) = root.split_horizontally(main_area_size_x);
299
300 let color_map_size = 1000;
301 let cmap: Vec<scarlet::color::RGBColor> = config
302 .cmap
303 .transform((0..=color_map_size).map(|x| x as f64 / color_map_size as f64));
304
305 {
306 let p = geometry
307 .iter()
308 .flat_map(|dev| dev.iter().map(|t| (t.position().x, t.position().y)))
309 .collect::<Vec<_>>();
310
311 let min_x =
312 p.iter().fold(f32::MAX, |acc, &(x, _)| acc.min(x)) - AUTD3::TRANS_SPACING / 2.0;
313 let min_y =
314 p.iter().fold(f32::MAX, |acc, &(_, y)| acc.min(y)) - AUTD3::TRANS_SPACING / 2.0;
315 let max_x =
316 p.iter().fold(f32::MIN, |acc, &(x, _)| acc.max(x)) + AUTD3::TRANS_SPACING / 2.0;
317 let max_y =
318 p.iter().fold(f32::MIN, |acc, &(_, y)| acc.max(y)) + AUTD3::TRANS_SPACING / 2.0;
319
320 let plot_range_x = max_x - min_x;
321 let plot_range_y = max_y - min_y;
322
323 let available_size_x = main_area_size_x - config.label_area_size - config.margin;
324 let available_size_y = config.figsize.1 - config.label_area_size - config.margin * 2;
325
326 let px_per_ps = (available_size_x as f32 / plot_range_x)
327 .min(available_size_y as f32 / plot_range_y);
328
329 let plot_size_x = (plot_range_x * px_per_ps) as u32;
330 let plot_size_y = (plot_range_y * px_per_ps) as u32;
331
332 let left_margin = config.margin
333 + (main_area_size_x - plot_size_x - config.label_area_size - config.margin).max(0)
334 / 2;
335 let right_margin = config.margin
336 + (main_area_size_x - plot_size_x - config.label_area_size - config.margin).max(0)
337 / 2;
338 let top_margin = config.margin
339 + (config.figsize.1 - plot_size_y - config.label_area_size - config.margin).max(0)
340 / 2;
341 let bottom_margin = config.margin
342 + (config.figsize.1 - plot_size_y - config.label_area_size - config.margin).max(0)
343 / 2;
344
345 let mut scatter_ctx = ChartBuilder::on(&main_area)
346 .margin_left(left_margin)
347 .margin_right(right_margin)
348 .margin_top(top_margin)
349 .margin_bottom(bottom_margin)
350 .x_label_area_size(config.label_area_size)
351 .y_label_area_size(config.label_area_size)
352 .build_cartesian_2d(min_x..max_x, min_y..max_y)?;
353 scatter_ctx
354 .configure_mesh()
355 .disable_x_mesh()
356 .disable_y_mesh()
357 .x_label_formatter(&|v| format!("{:.1}", v))
358 .y_label_formatter(&|v| format!("{:.1}", v))
359 .x_label_style(("sans-serif", config.font_size).into_text_style(&main_area))
360 .y_label_style(("sans-serif", config.font_size).into_text_style(&main_area))
361 .x_desc("x [mm]")
362 .y_desc("y [mm]")
363 .draw()?;
364
365 scatter_ctx.draw_series(p.iter().zip(phases.iter()).map(|(&(x, y), &p)| {
366 let v = (p / (2.0 * autd3_driver::defined::PI)) % 1.;
367 let c = cmap[((v * color_map_size as f32) as usize).clamp(0, cmap.len() - 1)];
368 Circle::new(
369 (x, y),
370 AUTD3::TRANS_SPACING * px_per_ps / 2.0,
371 RGBColor(c.int_r(), c.int_g(), c.int_b())
372 .filled()
373 .stroke_width(0),
374 )
375 }))?;
376 }
377
378 {
379 let mut chart = ChartBuilder::on(&cbar_area)
380 .margin_left(config.margin)
381 .margin_top(config.margin)
382 .margin_bottom(config.margin + config.label_area_size)
383 .margin_right(config.margin)
384 .y_label_area_size(config.label_area_size)
385 .set_label_area_size(LabelAreaPosition::Left, 0)
386 .set_label_area_size(LabelAreaPosition::Right, 80)
387 .build_cartesian_2d(0i32..1i32, 0i32..color_map_size)?;
388
389 chart
390 .configure_mesh()
391 .disable_x_axis()
392 .y_labels(3)
393 .disable_x_mesh()
394 .disable_y_mesh()
395 .axis_style(BLACK.stroke_width(1))
396 .label_style(("sans-serif", config.font_size))
397 .y_label_formatter(&|&v| {
398 if v == 0 {
399 "0".to_owned()
400 } else if v == color_map_size / 2 {
401 "π".to_owned()
402 } else {
403 "2π".to_owned()
404 }
405 })
406 .draw()?;
407
408 chart.draw_series(cmap.iter().enumerate().map(|(i, c)| {
409 Rectangle::new(
410 [(0, i as i32), (1, i as i32 + 1)],
411 RGBAColor(c.int_r(), c.int_g(), c.int_b(), 1.0).filled(),
412 )
413 }))?;
414
415 chart.draw_series([Rectangle::new(
416 [(0, 0), (1, color_map_size + 1)],
417 BLACK.stroke_width(1),
418 )])?;
419 }
420
421 root.present()?;
422
423 Ok(())
424 }
425}
426
427impl Backend for PlottersBackend {
428 type PlotConfig = PlotConfig;
429
430 fn new() -> Self {
431 Self {}
432 }
433
434 fn initialize(&mut self) -> Result<(), crate::error::VisualizerError> {
435 Ok(())
436 }
437
438 fn plot_1d(
439 observe_points: Vec<f32>,
440 acoustic_pressures: Vec<autd3_driver::defined::Complex>,
441 _resolution: f32,
442 x_label: &str,
443 config: Self::PlotConfig,
444 ) -> Result<(), crate::error::VisualizerError> {
445 let path = std::path::Path::new(&config.fname);
446 if !path.parent().map_or(true, |p| p.exists()) {
447 std::fs::create_dir_all(path.parent().unwrap())?;
448 }
449
450 let yrange = acoustic_pressures
451 .iter()
452 .fold((f32::MAX, f32::MIN), |acc, &x| {
453 (acc.0.min(x.norm()), acc.1.max(x.norm()))
454 });
455
456 if path.extension().map_or(false, |e| e == "svg") {
457 Self::plot_1d_impl(
458 &SVGBackend::new(&config.fname, config.figsize).into_drawing_area(),
459 &observe_points,
460 &acoustic_pressures,
461 x_label,
462 yrange,
463 &config,
464 )
465 } else {
466 Self::plot_1d_impl(
467 &BitMapBackend::new(&config.fname, config.figsize).into_drawing_area(),
468 &observe_points,
469 &acoustic_pressures,
470 x_label,
471 yrange,
472 &config,
473 )
474 }
475 }
476
477 fn plot_2d(
478 observe_x: Vec<f32>,
479 observe_y: Vec<f32>,
480 acoustic_pressures: Vec<autd3_driver::defined::Complex>,
481 resolution: f32,
482 x_label: &str,
483 y_label: &str,
484 config: Self::PlotConfig,
485 ) -> Result<(), crate::error::VisualizerError> {
486 let path = std::path::Path::new(&config.fname);
487 if !path.parent().map_or(true, |p| p.exists()) {
488 std::fs::create_dir_all(path.parent().unwrap())?;
489 }
490
491 let zrange = acoustic_pressures
492 .iter()
493 .fold((f32::MAX, f32::MIN), |acc, &x| {
494 (acc.0.min(x.norm()), acc.1.max(x.norm()))
495 });
496
497 if path.extension().map_or(false, |e| e == "svg") {
498 Self::plot_2d_impl(
499 &SVGBackend::new(&config.fname, config.figsize).into_drawing_area(),
500 &observe_x,
501 &observe_y,
502 &acoustic_pressures,
503 x_label,
504 y_label,
505 zrange,
506 resolution,
507 &config,
508 )
509 } else {
510 Self::plot_2d_impl(
511 &BitMapBackend::new(&config.fname, config.figsize).into_drawing_area(),
512 &observe_x,
513 &observe_y,
514 &acoustic_pressures,
515 x_label,
516 y_label,
517 zrange,
518 resolution,
519 &config,
520 )
521 }
522 }
523
524 fn plot_modulation(
525 modulation: Vec<f32>,
526 config: Self::PlotConfig,
527 ) -> Result<(), crate::error::VisualizerError> {
528 let path = std::path::Path::new(&config.fname);
529 if !path.parent().map_or(true, |p| p.exists()) {
530 std::fs::create_dir_all(path.parent().unwrap())?;
531 }
532
533 if path.extension().map_or(false, |e| e == "svg") {
534 Self::plot_modulation_impl(
535 SVGBackend::new(&config.fname, config.figsize).into_drawing_area(),
536 modulation,
537 &config,
538 )
539 } else {
540 Self::plot_modulation_impl(
541 BitMapBackend::new(&config.fname, config.figsize).into_drawing_area(),
542 modulation,
543 &config,
544 )
545 }
546 }
547
548 fn plot_phase(
549 config: Self::PlotConfig,
550 geometry: &autd3_driver::geometry::Geometry,
551 phases: Vec<f32>,
552 ) -> Result<(), crate::error::VisualizerError> {
553 let path = std::path::Path::new(&config.fname);
554 if !path.parent().map_or(true, |p| p.exists()) {
555 std::fs::create_dir_all(path.parent().unwrap())?;
556 }
557
558 if path.extension().map_or(false, |e| e == "svg") {
559 Self::plot_phase_impl(
560 SVGBackend::new(&config.fname, config.figsize).into_drawing_area(),
561 &config,
562 geometry,
563 phases,
564 )
565 } else {
566 Self::plot_phase_impl(
567 BitMapBackend::new(&config.fname, config.figsize).into_drawing_area(),
568 &config,
569 geometry,
570 phases,
571 )
572 }
573 }
574}