plotters_statistical/figures/
pair_plot.rs1use plotters::coord::Shift;
7use plotters::prelude::*;
8
9use crate::series::Ecdf;
10use crate::stats::{histogram, BinRule};
11use crate::style::{palette_color, translucent_fill};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Diagonal {
16 Histogram,
18 Ecdf,
20}
21
22#[derive(Debug, Clone)]
24pub struct PairPlot {
25 columns: Vec<Vec<f64>>,
26 labels: Vec<String>,
27 diagonal: Diagonal,
28 marker_radius: u32,
29 hue: Option<Vec<usize>>,
30 bins: BinRule,
31}
32
33impl PairPlot {
34 pub fn new(columns: Vec<Vec<f64>>, labels: Vec<String>) -> Self {
39 Self {
40 columns,
41 labels,
42 diagonal: Diagonal::Histogram,
43 marker_radius: 2,
44 hue: None,
45 bins: BinRule::Sturges,
46 }
47 }
48
49 pub fn diagonal(mut self, diagonal: Diagonal) -> Self {
51 self.diagonal = diagonal;
52 self
53 }
54
55 pub fn marker_radius(mut self, radius: u32) -> Self {
57 self.marker_radius = radius;
58 self
59 }
60
61 pub fn hue(mut self, groups: Vec<usize>) -> Self {
64 self.hue = Some(groups);
65 self
66 }
67
68 pub fn bins(mut self, bins: BinRule) -> Self {
70 self.bins = bins;
71 self
72 }
73
74 pub fn draw<DB: DrawingBackend>(
76 &self,
77 area: &DrawingArea<DB, Shift>,
78 ) -> Result<(), Box<dyn std::error::Error>>
79 where
80 DB::ErrorType: 'static,
81 {
82 let n = self.columns.len();
83 if n == 0 || self.labels.len() != n {
84 return Ok(());
85 }
86 let len = self.columns[0].len();
87 if self.columns.iter().any(|c| c.len() != len) || len == 0 {
88 return Ok(());
89 }
90
91 let ranges: Vec<(f64, f64)> = self.columns.iter().map(|c| padded_range(c)).collect();
93
94 let panels = area.split_evenly((n, n));
95 for i in 0..n {
96 for j in 0..n {
97 let panel = &panels[i * n + j];
98 if i == j {
99 self.draw_diagonal(panel, i, ranges[i], i == n - 1, j == 0)?;
100 } else {
101 self.draw_scatter(panel, j, i, ranges[j], ranges[i], i == n - 1, j == 0)?;
102 }
103 }
104 }
105 Ok(())
106 }
107
108 #[allow(clippy::too_many_arguments)]
109 fn draw_scatter<DB: DrawingBackend>(
110 &self,
111 panel: &DrawingArea<DB, Shift>,
112 xcol: usize,
113 ycol: usize,
114 xr: (f64, f64),
115 yr: (f64, f64),
116 bottom_row: bool,
117 left_col: bool,
118 ) -> Result<(), Box<dyn std::error::Error>>
119 where
120 DB::ErrorType: 'static,
121 {
122 let mut chart = self.panel_chart(panel, xr, yr, xcol, ycol, bottom_row, left_col)?;
123 let r = self.marker_radius;
124 let default_fill = translucent_fill(&palette_color(0), 0.55);
125 chart.draw_series(
126 self.columns[xcol]
127 .iter()
128 .zip(&self.columns[ycol])
129 .enumerate()
130 .map(|(k, (&x, &y))| {
131 let style = match &self.hue {
132 Some(g) if k < g.len() => translucent_fill(&palette_color(g[k]), 0.6),
133 _ => default_fill,
134 };
135 Circle::new((x, y), r, style)
136 }),
137 )?;
138 Ok(())
139 }
140
141 fn draw_diagonal<DB: DrawingBackend>(
142 &self,
143 panel: &DrawingArea<DB, Shift>,
144 col: usize,
145 xr: (f64, f64),
146 bottom_row: bool,
147 left_col: bool,
148 ) -> Result<(), Box<dyn std::error::Error>>
149 where
150 DB::ErrorType: 'static,
151 {
152 let data = &self.columns[col];
153 match self.diagonal {
154 Diagonal::Ecdf => {
155 let mut chart =
156 self.panel_chart(panel, xr, (0.0, 1.0), col, col, bottom_row, left_col)?;
157 if let Ok(e) = Ecdf::from_data(data) {
158 chart.draw_series(std::iter::once(e))?;
159 }
160 }
161 Diagonal::Histogram => {
162 if let Ok(h) = histogram(data, self.bins) {
163 let ymax = h.counts.iter().copied().max().unwrap_or(1).max(1) as f64;
164 let mut chart = self.panel_chart(
165 panel,
166 xr,
167 (0.0, ymax * 1.05),
168 col,
169 col,
170 bottom_row,
171 left_col,
172 )?;
173 let fill = translucent_fill(&palette_color(0), 0.6);
174 chart.draw_series(
175 h.edges
176 .windows(2)
177 .zip(&h.counts)
178 .map(|(e, &c)| Rectangle::new([(e[0], 0.0), (e[1], c as f64)], fill)),
179 )?;
180 }
181 }
182 }
183 Ok(())
184 }
185
186 #[allow(clippy::too_many_arguments)]
187 fn panel_chart<'b, DB: DrawingBackend>(
188 &self,
189 panel: &'b DrawingArea<DB, Shift>,
190 xr: (f64, f64),
191 yr: (f64, f64),
192 xcol: usize,
193 ycol: usize,
194 bottom_row: bool,
195 left_col: bool,
196 ) -> Result<
197 ChartContext<
198 'b,
199 DB,
200 Cartesian2d<
201 plotters::coord::types::RangedCoordf64,
202 plotters::coord::types::RangedCoordf64,
203 >,
204 >,
205 Box<dyn std::error::Error>,
206 >
207 where
208 DB::ErrorType: 'static,
209 {
210 let mut builder = ChartBuilder::on(panel);
211 builder.margin(3);
212 if left_col {
213 builder.set_label_area_size(LabelAreaPosition::Left, 38);
214 }
215 if bottom_row {
216 builder.set_label_area_size(LabelAreaPosition::Bottom, 26);
217 }
218 let mut chart = builder.build_cartesian_2d(xr.0..xr.1, yr.0..yr.1)?;
219 let x_desc = if bottom_row {
220 self.labels[xcol].clone()
221 } else {
222 String::new()
223 };
224 let y_desc = if left_col {
225 self.labels[ycol].clone()
226 } else {
227 String::new()
228 };
229 chart
230 .configure_mesh()
231 .disable_mesh()
232 .x_labels(if bottom_row { 4 } else { 0 })
233 .y_labels(if left_col { 4 } else { 0 })
234 .x_desc(x_desc)
235 .y_desc(y_desc)
236 .label_style(("sans-serif", 11))
237 .draw()?;
238 Ok(chart)
239 }
240}
241
242fn padded_range(data: &[f64]) -> (f64, f64) {
243 let mut lo = f64::INFINITY;
244 let mut hi = f64::NEG_INFINITY;
245 for &v in data {
246 if v.is_finite() {
247 lo = lo.min(v);
248 hi = hi.max(v);
249 }
250 }
251 if !lo.is_finite() {
252 return (-1.0, 1.0);
253 }
254 if hi <= lo {
255 return (lo - 0.5, lo + 0.5);
256 }
257 let pad = (hi - lo) * 0.05;
258 (lo - pad, hi + pad)
259}