use crate::plot::colormap::ColorMap;
use std::collections::BTreeMap;
const DICE_POSITIONS: [&[usize]; 7] = [
&[], &[5], &[1, 9], &[1, 5, 9], &[1, 7, 3, 9], &[1, 7, 5, 3, 9], &[1, 4, 7, 3, 6, 9], ];
pub struct DicePoint {
pub x_cat: String,
pub y_cat: String,
pub present: Vec<usize>,
pub fill: Option<f64>,
pub size: Option<f64>,
pub dot_colors: Vec<Option<String>>,
pub dot_fills: Vec<Option<f64>>,
pub dot_sizes: Vec<Option<f64>>,
}
pub struct DicePlot {
pub points: Vec<DicePoint>,
pub x_categories: Vec<String>,
pub y_categories: Vec<String>,
pub category_labels: Vec<String>,
pub ndots: usize,
pub cell_width: f64,
pub cell_height: f64,
pub pad: f64,
pub dot_radius: f64,
pub color_map: ColorMap,
pub fill_range: Option<(f64, f64)>,
pub size_range: Option<(f64, f64)>,
pub fill_legend_label: Option<String>,
pub size_legend_label: Option<String>,
pub dot_legend: Vec<(String, String)>,
pub position_legend_label: Option<String>,
pub grid_lines: bool,
}
impl Default for DicePlot {
fn default() -> Self {
Self::new(4)
}
}
impl DicePlot {
pub fn new(ndots: usize) -> Self {
let ndots = ndots.clamp(1, 6);
Self {
points: Vec::new(),
x_categories: Vec::new(),
y_categories: Vec::new(),
category_labels: (0..ndots).map(|i| format!("Cat {}", i + 1)).collect(),
ndots,
cell_width: 0.8,
cell_height: 0.8,
pad: 0.1,
dot_radius: 0.0,
color_map: ColorMap::Viridis,
fill_range: None,
size_range: None,
fill_legend_label: None,
size_legend_label: None,
dot_legend: Vec::new(),
position_legend_label: None,
grid_lines: false,
}
}
pub fn with_points<I, Sx, Sy>(mut self, iter: I) -> Self
where
I: IntoIterator<Item = (Sx, Sy, Vec<usize>, Option<f64>, Option<f64>)>,
Sx: Into<String>,
Sy: Into<String>,
{
for (x_cat, y_cat, present, fill, size) in iter {
let x_cat: String = x_cat.into();
let y_cat: String = y_cat.into();
if !self.x_categories.contains(&x_cat) {
self.x_categories.push(x_cat.clone());
}
if !self.y_categories.contains(&y_cat) {
self.y_categories.push(y_cat.clone());
}
self.points.push(DicePoint {
x_cat,
y_cat,
present,
fill,
size,
dot_colors: Vec::new(),
dot_fills: Vec::new(),
dot_sizes: Vec::new(),
});
}
self
}
pub fn with_records<I, Sx, Sy, Sd, Sc>(mut self, iter: I) -> Self
where
I: IntoIterator<Item = (Sx, Sy, Sd, Sc)>,
Sx: Into<String>,
Sy: Into<String>,
Sd: Into<String>,
Sc: Into<String>,
{
let mut cell_map: BTreeMap<(String, String), Vec<(usize, String)>> = BTreeMap::new();
for (x_cat, y_cat, dot_cat, color) in iter {
let x_cat: String = x_cat.into();
let y_cat: String = y_cat.into();
let dot_cat: String = dot_cat.into();
let color: String = color.into();
let dot_idx = self.category_labels.iter().position(|l| l == &dot_cat);
if let Some(dot_idx) = dot_idx {
if !self.x_categories.contains(&x_cat) {
self.x_categories.push(x_cat.clone());
}
if !self.y_categories.contains(&y_cat) {
self.y_categories.push(y_cat.clone());
}
cell_map
.entry((x_cat, y_cat))
.or_default()
.push((dot_idx, color));
}
}
for ((x_cat, y_cat), dot_entries) in cell_map {
let mut dot_colors: Vec<Option<String>> = vec![None; self.ndots];
for (idx, color) in dot_entries {
if idx < self.ndots {
dot_colors[idx] = Some(color);
}
}
self.points.push(DicePoint {
x_cat,
y_cat,
present: Vec::new(),
fill: None,
size: None,
dot_colors,
dot_fills: Vec::new(),
dot_sizes: Vec::new(),
});
}
self
}
pub fn with_dot_data<I, Sx, Sy>(mut self, iter: I) -> Self
where
I: IntoIterator<Item = (Sx, Sy, usize, Option<f64>, Option<f64>)>,
Sx: Into<String>,
Sy: Into<String>,
{
type DotEntry = (usize, Option<f64>, Option<f64>);
let mut cell_map: BTreeMap<(String, String), Vec<DotEntry>> = BTreeMap::new();
for (x_cat, y_cat, dot_idx, fill, size) in iter {
let x_cat: String = x_cat.into();
let y_cat: String = y_cat.into();
if !self.x_categories.contains(&x_cat) {
self.x_categories.push(x_cat.clone());
}
if !self.y_categories.contains(&y_cat) {
self.y_categories.push(y_cat.clone());
}
if dot_idx < self.ndots {
cell_map
.entry((x_cat, y_cat))
.or_default()
.push((dot_idx, fill, size));
}
}
for ((x_cat, y_cat), dot_entries) in cell_map {
let mut dot_fills: Vec<Option<f64>> = vec![None; self.ndots];
let mut dot_sizes: Vec<Option<f64>> = vec![None; self.ndots];
for (idx, fill, size) in dot_entries {
dot_fills[idx] = fill;
dot_sizes[idx] = size;
}
if dot_fills.iter().all(|v| v.is_none()) && dot_sizes.iter().all(|v| v.is_none()) {
continue;
}
self.points.push(DicePoint {
x_cat,
y_cat,
present: Vec::new(),
fill: None,
size: None,
dot_colors: Vec::new(),
dot_fills,
dot_sizes,
});
}
self
}
pub fn with_x_categories(mut self, cats: Vec<String>) -> Self {
self.x_categories = cats;
self
}
pub fn with_y_categories(mut self, cats: Vec<String>) -> Self {
self.y_categories = cats;
self
}
pub fn with_category_labels(mut self, labels: Vec<String>) -> Self {
self.category_labels = labels;
self
}
pub fn with_color_map(mut self, map: ColorMap) -> Self {
self.color_map = map;
self
}
pub fn with_fill_range(mut self, min: f64, max: f64) -> Self {
self.fill_range = Some((min, max));
self
}
pub fn with_size_range(mut self, min: f64, max: f64) -> Self {
self.size_range = Some((min, max));
self
}
pub fn with_fill_legend<S: Into<String>>(mut self, label: S) -> Self {
self.fill_legend_label = Some(label.into());
self
}
pub fn with_size_legend<S: Into<String>>(mut self, label: S) -> Self {
self.size_legend_label = Some(label.into());
self
}
pub fn with_dot_legend<I, S1, S2>(mut self, entries: I) -> Self
where
I: IntoIterator<Item = (S1, S2)>,
S1: Into<String>,
S2: Into<String>,
{
self.dot_legend = entries
.into_iter()
.map(|(l, c)| (l.into(), c.into()))
.collect();
self
}
pub fn with_position_legend<S: Into<String>>(mut self, label: S) -> Self {
self.position_legend_label = Some(label.into());
self
}
pub fn with_grid_lines(mut self, v: bool) -> Self {
self.grid_lines = v;
self
}
pub fn with_dot_radius(mut self, r: f64) -> Self {
self.dot_radius = r;
self
}
pub fn with_cell_size(mut self, width: f64, height: f64) -> Self {
self.cell_width = width;
self.cell_height = height;
self
}
pub fn with_pad(mut self, pad: f64) -> Self {
self.pad = pad;
self
}
pub fn dot_grid_positions(&self) -> Vec<(usize, usize)> {
let positions = DICE_POSITIONS.get(self.ndots).copied().unwrap_or(&[]);
positions
.iter()
.map(|&p| ((p - 1) / 3, (p - 1) % 3))
.collect()
}
pub fn dot_offsets(&self) -> Vec<(f64, f64)> {
let positions = DICE_POSITIONS.get(self.ndots).copied().unwrap_or(&[]);
let w = self.cell_width;
let h = self.cell_height;
let pad = self.pad;
let avail_w = w - 2.0 * pad;
let avail_h = h - 2.0 * pad;
positions
.iter()
.map(|&p| {
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;
let dy = row / 2.0 * avail_h + pad - h / 2.0;
(dx, dy)
})
.collect()
}
pub fn fill_extent(&self) -> (f64, f64) {
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for p in &self.points {
if let Some(v) = p.fill {
min = min.min(v);
max = max.max(v);
}
for v in p.dot_fills.iter().flatten() {
min = min.min(*v);
max = max.max(*v);
}
}
if min.is_infinite() {
(0.0, 1.0)
} else {
(min, max)
}
}
pub fn size_extent(&self) -> (f64, f64) {
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for p in &self.points {
if let Some(v) = p.size {
min = min.min(v);
max = max.max(v);
}
for v in p.dot_sizes.iter().flatten() {
min = min.min(*v);
max = max.max(*v);
}
}
if min.is_infinite() {
(0.0, 1.0)
} else {
(min, max)
}
}
}