plotters_statistical/figures/
missingness_heatmap.rs1use plotters::coord::Shift;
6use plotters::prelude::*;
7use plotters::style::text_anchor::{HPos, VPos};
8
9use super::text_style;
10use crate::stats::StatsError;
11
12const PRESENT_COLOR: RGBColor = RGBColor(38, 70, 108); const MISSING_COLOR: RGBColor = RGBColor(232, 232, 232); #[derive(Debug, Clone)]
17pub struct MissingnessHeatmap {
18 present: Vec<Vec<bool>>,
20 labels: Vec<String>,
21 nrows: usize,
22 present_color: RGBColor,
23 missing_color: RGBColor,
24 show_percent: bool,
25 title: Option<String>,
26}
27
28impl MissingnessHeatmap {
29 pub fn from_columns(
37 columns: &[Vec<Option<f64>>],
38 labels: Vec<String>,
39 ) -> Result<Self, StatsError> {
40 if columns.is_empty() {
41 return Err(StatsError::EmptyInput);
42 }
43 if labels.len() != columns.len() {
44 return Err(StatsError::LengthMismatch {
45 scores: columns.len(),
46 labels: labels.len(),
47 });
48 }
49 let nrows = columns.iter().map(|c| c.len()).max().unwrap_or(0);
50 let ncols = columns.len();
51 let mut present = vec![vec![false; ncols]; nrows];
52 for (j, col) in columns.iter().enumerate() {
53 for (i, v) in col.iter().enumerate() {
54 present[i][j] = v.is_some();
55 }
56 }
57 Ok(Self {
58 present,
59 labels,
60 nrows,
61 present_color: PRESENT_COLOR,
62 missing_color: MISSING_COLOR,
63 show_percent: true,
64 title: None,
65 })
66 }
67
68 pub fn present_color(mut self, color: RGBColor) -> Self {
70 self.present_color = color;
71 self
72 }
73
74 pub fn missing_color(mut self, color: RGBColor) -> Self {
76 self.missing_color = color;
77 self
78 }
79
80 pub fn show_percent(mut self, on: bool) -> Self {
82 self.show_percent = on;
83 self
84 }
85
86 pub fn title(mut self, title: impl Into<String>) -> Self {
88 self.title = Some(title.into());
89 self
90 }
91
92 fn missing_fraction(&self, col: usize) -> f64 {
93 if self.nrows == 0 {
94 return 0.0;
95 }
96 let missing = self.present.iter().filter(|row| !row[col]).count();
97 missing as f64 / self.nrows as f64
98 }
99
100 pub fn draw<DB: DrawingBackend>(
102 &self,
103 area: &DrawingArea<DB, Shift>,
104 ) -> Result<(), Box<dyn std::error::Error>>
105 where
106 DB::ErrorType: 'static,
107 {
108 let ncols = self.labels.len();
109 if ncols == 0 || self.nrows == 0 {
110 return Ok(());
111 }
112 let (w, h) = area.dim_in_pixel();
113 let (w, h) = (w as i32, h as i32);
114 let top = if self.title.is_some() { 34 } else { 12 };
115 let left = 12;
116 let bottom = 70;
117 let gx0 = left;
118 let gy0 = top;
119 let gx1 = (w - 12).max(gx0 + 1);
120 let gy1 = (h - bottom).max(gy0 + 1);
121 let gw = gx1 - gx0;
122 let gh = gy1 - gy0;
123 let nc = ncols as i32;
124
125 if let Some(t) = &self.title {
126 area.draw(&Text::new(
127 t.clone(),
128 ((gx0 + gx1) / 2, 8),
129 text_style(HPos::Center, VPos::Top, 18, &RGBColor(0, 0, 0)),
130 ))?;
131 }
132
133 for py in 0..gh {
136 let row = (py as usize * self.nrows) / gh as usize;
137 let y0 = gy0 + py;
138 for j in 0..ncols {
139 let x0 = gx0 + gw * j as i32 / nc;
140 let x1 = gx0 + gw * (j as i32 + 1) / nc;
141 let color = if self.present[row][j] {
142 self.present_color
143 } else {
144 self.missing_color
145 };
146 area.draw(&Rectangle::new([(x0, y0), (x1, y0 + 1)], color.filled()))?;
147 }
148 }
149
150 for (j, label) in self.labels.iter().enumerate() {
152 let xc = gx0 + gw * (2 * j as i32 + 1) / (2 * nc);
153 area.draw(&Text::new(
154 label.clone(),
155 (xc, gy1 + 8),
156 text_style(HPos::Center, VPos::Top, 12, &RGBColor(0, 0, 0)),
157 ))?;
158 if self.show_percent {
159 area.draw(&Text::new(
160 format!("{:.0}%", 100.0 * self.missing_fraction(j)),
161 (xc, gy1 + 26),
162 text_style(HPos::Center, VPos::Top, 11, &RGBColor(120, 120, 120)),
163 ))?;
164 }
165 }
166 Ok(())
167 }
168}