1use plotters::prelude::*;
2use std::path::Path;
3
4type PlotResult = std::result::Result<(), Box<dyn std::error::Error>>;
5
6pub fn scatter_svg(
7 points: &[(f64, f64)],
8 path: &Path,
9 title: &str,
10 x_label: &str,
11 y_label: &str,
12) -> PlotResult {
13 let (x_range, y_range) = padded_ranges(points);
14 let root = SVGBackend::new(path, (800, 600)).into_drawing_area();
15 root.fill(&WHITE)?;
16 let mut chart = build_chart(&root, title, x_label, y_label, &x_range, &y_range)?;
17 draw_crosshairs(&mut chart, &x_range, &y_range)?;
18 draw_points(&mut chart, points)?;
19 root.present()?;
20 Ok(())
21}
22
23pub fn histogram_svg(values: &[f64], path: &Path, title: &str, x_label: &str) -> PlotResult {
24 if values.is_empty() {
25 return Ok(());
26 }
27 let (bins, bin_width, min_val) = bin_values(values, 20);
28 let max_count = bins.iter().copied().max().unwrap_or(1);
29 let root = SVGBackend::new(path, (800, 600)).into_drawing_area();
30 root.fill(&WHITE)?;
31 let x_max = min_val + 20.0 * bin_width;
32 let mut chart = ChartBuilder::on(&root)
33 .caption(title, ("sans-serif", 24))
34 .margin(20)
35 .x_label_area_size(40)
36 .y_label_area_size(60)
37 .build_cartesian_2d(min_val..x_max, 0u32..(max_count + 1))?;
38 chart
39 .configure_mesh()
40 .x_desc(x_label)
41 .y_desc("Count")
42 .draw()?;
43 draw_histogram_bars(&mut chart, &bins, min_val, bin_width)?;
44 root.present()?;
45 Ok(())
46}
47
48pub fn vector_map_svg(
49 positions: &[(f64, f64)],
50 vectors: &[(f64, f64)],
51 path: &Path,
52 title: &str,
53 x_label: &str,
54 y_label: &str,
55 scale: f64,
56) -> PlotResult {
57 let (x_range, y_range) = padded_ranges(positions);
58 let root = SVGBackend::new(path, (800, 600)).into_drawing_area();
59 root.fill(&WHITE)?;
60 let mut chart = build_chart(&root, title, x_label, y_label, &x_range, &y_range)?;
61 draw_vectors(&mut chart, positions, vectors, scale)?;
62 root.present()?;
63 Ok(())
64}
65
66fn padded_ranges(points: &[(f64, f64)]) -> ((f64, f64), (f64, f64)) {
67 if points.is_empty() {
68 return ((-1.0, 1.0), (-1.0, 1.0));
69 }
70 let (mut x_min, mut x_max) = extent(points.iter().map(|p| p.0));
71 let (mut y_min, mut y_max) = extent(points.iter().map(|p| p.1));
72 let x_pad = (x_max - x_min).abs() * 0.1 + 1e-6;
73 let y_pad = (y_max - y_min).abs() * 0.1 + 1e-6;
74 x_min -= x_pad;
75 x_max += x_pad;
76 y_min -= y_pad;
77 y_max += y_pad;
78 ((x_min, x_max), (y_min, y_max))
79}
80
81fn extent(iter: impl Iterator<Item = f64>) -> (f64, f64) {
82 iter.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| {
83 (lo.min(v), hi.max(v))
84 })
85}
86
87fn build_chart<'a, DB: DrawingBackend + 'a>(
88 area: &'a DrawingArea<DB, plotters::coord::Shift>,
89 title: &str,
90 x_label: &str,
91 y_label: &str,
92 x_range: &(f64, f64),
93 y_range: &(f64, f64),
94) -> std::result::Result<
95 ChartContext<
96 'a,
97 DB,
98 Cartesian2d<plotters::coord::types::RangedCoordf64, plotters::coord::types::RangedCoordf64>,
99 >,
100 Box<dyn std::error::Error>,
101>
102where
103 DB::ErrorType: 'static,
104{
105 let mut chart = ChartBuilder::on(area)
106 .caption(title, ("sans-serif", 24))
107 .margin(20)
108 .x_label_area_size(40)
109 .y_label_area_size(60)
110 .build_cartesian_2d(x_range.0..x_range.1, y_range.0..y_range.1)?;
111 chart
112 .configure_mesh()
113 .x_desc(x_label)
114 .y_desc(y_label)
115 .draw()?;
116 Ok(chart)
117}
118
119fn draw_crosshairs<DB: DrawingBackend>(
120 chart: &mut ChartContext<
121 DB,
122 Cartesian2d<plotters::coord::types::RangedCoordf64, plotters::coord::types::RangedCoordf64>,
123 >,
124 x_range: &(f64, f64),
125 y_range: &(f64, f64),
126) -> PlotResult
127where
128 DB::ErrorType: 'static,
129{
130 let style = BLACK.mix(0.3).stroke_width(1);
131 chart.draw_series(std::iter::once(PathElement::new(
132 vec![(x_range.0, 0.0), (x_range.1, 0.0)],
133 style,
134 )))?;
135 chart.draw_series(std::iter::once(PathElement::new(
136 vec![(0.0, y_range.0), (0.0, y_range.1)],
137 style,
138 )))?;
139 Ok(())
140}
141
142fn draw_points<DB: DrawingBackend>(
143 chart: &mut ChartContext<
144 DB,
145 Cartesian2d<plotters::coord::types::RangedCoordf64, plotters::coord::types::RangedCoordf64>,
146 >,
147 points: &[(f64, f64)],
148) -> PlotResult
149where
150 DB::ErrorType: 'static,
151{
152 chart.draw_series(
153 points
154 .iter()
155 .map(|&(x, y)| Circle::new((x, y), 3, BLUE.filled())),
156 )?;
157 Ok(())
158}
159
160fn draw_histogram_bars<DB: DrawingBackend>(
161 chart: &mut ChartContext<
162 DB,
163 Cartesian2d<plotters::coord::types::RangedCoordf64, plotters::coord::types::RangedCoordu32>,
164 >,
165 bins: &[u32],
166 min_val: f64,
167 bin_width: f64,
168) -> PlotResult
169where
170 DB::ErrorType: 'static,
171{
172 chart.draw_series(bins.iter().enumerate().map(|(i, &count)| {
173 let x0 = min_val + i as f64 * bin_width;
174 let x1 = x0 + bin_width;
175 Rectangle::new([(x0, 0), (x1, count)], BLUE.filled())
176 }))?;
177 Ok(())
178}
179
180fn draw_vectors<DB: DrawingBackend>(
181 chart: &mut ChartContext<
182 DB,
183 Cartesian2d<plotters::coord::types::RangedCoordf64, plotters::coord::types::RangedCoordf64>,
184 >,
185 positions: &[(f64, f64)],
186 vectors: &[(f64, f64)],
187 scale: f64,
188) -> PlotResult
189where
190 DB::ErrorType: 'static,
191{
192 for (&(px, py), &(vx, vy)) in positions.iter().zip(vectors.iter()) {
193 chart.draw_series(std::iter::once(Circle::new((px, py), 3, BLUE.filled())))?;
194 let ex = px + vx * scale;
195 let ey = py + vy * scale;
196 chart.draw_series(std::iter::once(PathElement::new(
197 vec![(px, py), (ex, ey)],
198 RED.stroke_width(1),
199 )))?;
200 }
201 Ok(())
202}
203
204fn bin_values(values: &[f64], n_bins: usize) -> (Vec<u32>, f64, f64) {
205 let (min_val, max_val) = extent(values.iter().copied());
206 let range = (max_val - min_val).max(1e-10);
207 let bin_width = range / n_bins as f64;
208 let mut bins = vec![0u32; n_bins];
209 for &v in values {
210 let idx = libm::floor((v - min_val) / bin_width) as usize;
211 bins[idx.min(n_bins - 1)] += 1;
212 }
213 (bins, bin_width, min_val)
214}