Skip to main content

celestial_pointing/plot/
svg.rs

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}