1use crate::plot::colormap::ColorMap;
2use std::collections::BTreeMap;
3
4const DICE_POSITIONS: [&[usize]; 7] = [
14 &[], &[5], &[1, 9], &[1, 5, 9], &[1, 7, 3, 9], &[1, 7, 5, 3, 9], &[1, 4, 7, 3, 6, 9], ];
22
23pub struct DicePoint {
25 pub x_cat: String,
27 pub y_cat: String,
29 pub present: Vec<usize>,
31 pub fill: Option<f64>,
33 pub size: Option<f64>,
35 pub dot_colors: Vec<Option<String>>,
37 pub dot_fills: Vec<Option<f64>>,
39 pub dot_sizes: Vec<Option<f64>>,
41}
42
43pub struct DicePlot {
45 pub points: Vec<DicePoint>,
46 pub x_categories: Vec<String>,
47 pub y_categories: Vec<String>,
48 pub category_labels: Vec<String>,
49 pub ndots: usize,
50 pub cell_width: f64,
51 pub cell_height: f64,
52 pub pad: f64,
53 pub dot_radius: f64,
54 pub color_map: ColorMap,
55 pub fill_range: Option<(f64, f64)>,
56 pub size_range: Option<(f64, f64)>,
57 pub fill_legend_label: Option<String>,
58 pub size_legend_label: Option<String>,
59 pub dot_legend: Vec<(String, String)>,
60 pub position_legend_label: Option<String>,
61 pub grid_lines: bool,
63}
64
65impl Default for DicePlot {
66 fn default() -> Self {
67 Self::new(4)
68 }
69}
70
71impl DicePlot {
72 pub fn new(ndots: usize) -> Self {
73 let ndots = ndots.clamp(1, 6);
74 Self {
75 points: Vec::new(),
76 x_categories: Vec::new(),
77 y_categories: Vec::new(),
78 category_labels: (0..ndots).map(|i| format!("Cat {}", i + 1)).collect(),
79 ndots,
80 cell_width: 0.8,
81 cell_height: 0.8,
82 pad: 0.1,
83 dot_radius: 0.0,
84 color_map: ColorMap::Viridis,
85 fill_range: None,
86 size_range: None,
87 fill_legend_label: None,
88 size_legend_label: None,
89 dot_legend: Vec::new(),
90 position_legend_label: None,
91 grid_lines: false,
92 }
93 }
94
95 pub fn with_points<I, Sx, Sy>(mut self, iter: I) -> Self
96 where
97 I: IntoIterator<Item = (Sx, Sy, Vec<usize>, Option<f64>, Option<f64>)>,
98 Sx: Into<String>,
99 Sy: Into<String>,
100 {
101 for (x_cat, y_cat, present, fill, size) in iter {
102 let x_cat: String = x_cat.into();
103 let y_cat: String = y_cat.into();
104 if !self.x_categories.contains(&x_cat) {
105 self.x_categories.push(x_cat.clone());
106 }
107 if !self.y_categories.contains(&y_cat) {
108 self.y_categories.push(y_cat.clone());
109 }
110 self.points.push(DicePoint {
111 x_cat,
112 y_cat,
113 present,
114 fill,
115 size,
116 dot_colors: Vec::new(),
117 dot_fills: Vec::new(),
118 dot_sizes: Vec::new(),
119 });
120 }
121 self
122 }
123
124 pub fn with_records<I, Sx, Sy, Sd, Sc>(mut self, iter: I) -> Self
125 where
126 I: IntoIterator<Item = (Sx, Sy, Sd, Sc)>,
127 Sx: Into<String>,
128 Sy: Into<String>,
129 Sd: Into<String>,
130 Sc: Into<String>,
131 {
132 let mut cell_map: BTreeMap<(String, String), Vec<(usize, String)>> = BTreeMap::new();
133 for (x_cat, y_cat, dot_cat, color) in iter {
134 let x_cat: String = x_cat.into();
135 let y_cat: String = y_cat.into();
136 let dot_cat: String = dot_cat.into();
137 let color: String = color.into();
138 let dot_idx = self.category_labels.iter().position(|l| l == &dot_cat);
139 if let Some(dot_idx) = dot_idx {
140 if !self.x_categories.contains(&x_cat) {
141 self.x_categories.push(x_cat.clone());
142 }
143 if !self.y_categories.contains(&y_cat) {
144 self.y_categories.push(y_cat.clone());
145 }
146 cell_map
147 .entry((x_cat, y_cat))
148 .or_default()
149 .push((dot_idx, color));
150 }
151 }
152 for ((x_cat, y_cat), dot_entries) in cell_map {
153 let mut dot_colors: Vec<Option<String>> = vec![None; self.ndots];
154 for (idx, color) in dot_entries {
155 if idx < self.ndots {
156 dot_colors[idx] = Some(color);
157 }
158 }
159 self.points.push(DicePoint {
160 x_cat,
161 y_cat,
162 present: Vec::new(),
163 fill: None,
164 size: None,
165 dot_colors,
166 dot_fills: Vec::new(),
167 dot_sizes: Vec::new(),
168 });
169 }
170 self
171 }
172
173 pub fn with_dot_data<I, Sx, Sy>(mut self, iter: I) -> Self
174 where
175 I: IntoIterator<Item = (Sx, Sy, usize, Option<f64>, Option<f64>)>,
176 Sx: Into<String>,
177 Sy: Into<String>,
178 {
179 type DotEntry = (usize, Option<f64>, Option<f64>);
180 let mut cell_map: BTreeMap<(String, String), Vec<DotEntry>> = BTreeMap::new();
181 for (x_cat, y_cat, dot_idx, fill, size) in iter {
182 let x_cat: String = x_cat.into();
183 let y_cat: String = y_cat.into();
184 if !self.x_categories.contains(&x_cat) {
185 self.x_categories.push(x_cat.clone());
186 }
187 if !self.y_categories.contains(&y_cat) {
188 self.y_categories.push(y_cat.clone());
189 }
190 if dot_idx < self.ndots {
191 cell_map
192 .entry((x_cat, y_cat))
193 .or_default()
194 .push((dot_idx, fill, size));
195 }
196 }
197 for ((x_cat, y_cat), dot_entries) in cell_map {
198 let mut dot_fills: Vec<Option<f64>> = vec![None; self.ndots];
199 let mut dot_sizes: Vec<Option<f64>> = vec![None; self.ndots];
200 for (idx, fill, size) in dot_entries {
201 dot_fills[idx] = fill;
202 dot_sizes[idx] = size;
203 }
204 if dot_fills.iter().all(|v| v.is_none()) && dot_sizes.iter().all(|v| v.is_none()) {
205 continue;
206 }
207 self.points.push(DicePoint {
208 x_cat,
209 y_cat,
210 present: Vec::new(),
211 fill: None,
212 size: None,
213 dot_colors: Vec::new(),
214 dot_fills,
215 dot_sizes,
216 });
217 }
218 self
219 }
220
221 pub fn with_x_categories(mut self, cats: Vec<String>) -> Self {
222 self.x_categories = cats;
223 self
224 }
225 pub fn with_y_categories(mut self, cats: Vec<String>) -> Self {
226 self.y_categories = cats;
227 self
228 }
229 pub fn with_category_labels(mut self, labels: Vec<String>) -> Self {
230 self.category_labels = labels;
231 self
232 }
233 pub fn with_color_map(mut self, map: ColorMap) -> Self {
234 self.color_map = map;
235 self
236 }
237 pub fn with_fill_range(mut self, min: f64, max: f64) -> Self {
238 self.fill_range = Some((min, max));
239 self
240 }
241 pub fn with_size_range(mut self, min: f64, max: f64) -> Self {
242 self.size_range = Some((min, max));
243 self
244 }
245 pub fn with_fill_legend<S: Into<String>>(mut self, label: S) -> Self {
246 self.fill_legend_label = Some(label.into());
247 self
248 }
249 pub fn with_size_legend<S: Into<String>>(mut self, label: S) -> Self {
250 self.size_legend_label = Some(label.into());
251 self
252 }
253 pub fn with_dot_legend<I, S1, S2>(mut self, entries: I) -> Self
254 where
255 I: IntoIterator<Item = (S1, S2)>,
256 S1: Into<String>,
257 S2: Into<String>,
258 {
259 self.dot_legend = entries
260 .into_iter()
261 .map(|(l, c)| (l.into(), c.into()))
262 .collect();
263 self
264 }
265 pub fn with_position_legend<S: Into<String>>(mut self, label: S) -> Self {
266 self.position_legend_label = Some(label.into());
267 self
268 }
269 pub fn with_grid_lines(mut self, v: bool) -> Self {
270 self.grid_lines = v;
271 self
272 }
273 pub fn with_dot_radius(mut self, r: f64) -> Self {
274 self.dot_radius = r;
275 self
276 }
277 pub fn with_cell_size(mut self, width: f64, height: f64) -> Self {
278 self.cell_width = width;
279 self.cell_height = height;
280 self
281 }
282 pub fn with_pad(mut self, pad: f64) -> Self {
283 self.pad = pad;
284 self
285 }
286
287 pub fn dot_grid_positions(&self) -> Vec<(usize, usize)> {
290 let positions = DICE_POSITIONS.get(self.ndots).copied().unwrap_or(&[]);
291 positions
292 .iter()
293 .map(|&p| ((p - 1) / 3, (p - 1) % 3))
294 .collect()
295 }
296
297 pub fn dot_offsets(&self) -> Vec<(f64, f64)> {
298 let positions = DICE_POSITIONS.get(self.ndots).copied().unwrap_or(&[]);
299 let w = self.cell_width;
300 let h = self.cell_height;
301 let pad = self.pad;
302 let avail_w = w - 2.0 * pad;
303 let avail_h = h - 2.0 * pad;
304 positions
305 .iter()
306 .map(|&p| {
307 let col = ((p - 1) / 3) as f64; let row = ((p - 1) % 3) as f64; let dx = col / 2.0 * avail_w + pad - w / 2.0;
310 let dy = row / 2.0 * avail_h + pad - h / 2.0;
311 (dx, dy)
312 })
313 .collect()
314 }
315
316 pub fn fill_extent(&self) -> (f64, f64) {
317 let mut min = f64::INFINITY;
318 let mut max = f64::NEG_INFINITY;
319 for p in &self.points {
320 if let Some(v) = p.fill {
321 min = min.min(v);
322 max = max.max(v);
323 }
324 for v in p.dot_fills.iter().flatten() {
325 min = min.min(*v);
326 max = max.max(*v);
327 }
328 }
329 if min.is_infinite() {
330 (0.0, 1.0)
331 } else {
332 (min, max)
333 }
334 }
335
336 pub fn size_extent(&self) -> (f64, f64) {
337 let mut min = f64::INFINITY;
338 let mut max = f64::NEG_INFINITY;
339 for p in &self.points {
340 if let Some(v) = p.size {
341 min = min.min(v);
342 max = max.max(v);
343 }
344 for v in p.dot_sizes.iter().flatten() {
345 min = min.min(*v);
346 max = max.max(*v);
347 }
348 }
349 if min.is_infinite() {
350 (0.0, 1.0)
351 } else {
352 (min, max)
353 }
354 }
355}