plotters_statistical/figures/
correlation_heatmap.rs1use plotters::coord::Shift;
5use plotters::prelude::*;
6use plotters::style::text_anchor::{HPos, VPos};
7
8use super::colorbar::draw_colorbar;
9use super::{contrast_text, text_style};
10use crate::colormap::{GradientColorMap, Normalization};
11use crate::stats::{correlation_matrix, CorrelationMethod, StatsError};
12
13const MISSING_COLOR: RGBColor = RGBColor(235, 235, 235);
14
15#[derive(Debug, Clone)]
21pub struct CorrelationHeatmap {
22 matrix: Vec<Vec<f64>>,
23 labels: Vec<String>,
24 colormap: GradientColorMap,
25 norm: Normalization,
26 annotate: bool,
27 precision: usize,
28 show_colorbar: bool,
29 gridlines: bool,
30 title: Option<String>,
31}
32
33impl CorrelationHeatmap {
34 pub fn from_columns(
41 columns: &[Vec<f64>],
42 labels: Vec<String>,
43 method: CorrelationMethod,
44 ) -> Result<Self, StatsError> {
45 if labels.len() != columns.len() {
46 return Err(StatsError::LengthMismatch {
47 scores: columns.len(),
48 labels: labels.len(),
49 });
50 }
51 let matrix = correlation_matrix(columns, method)?;
52 Ok(Self::from_matrix(matrix, labels))
53 }
54
55 pub fn from_matrix(matrix: Vec<Vec<f64>>, labels: Vec<String>) -> Self {
57 Self {
58 matrix,
59 labels,
60 colormap: GradientColorMap::rd_bu(),
61 norm: Normalization::Symmetric {
62 center: 0.0,
63 half: 1.0,
64 },
65 annotate: true,
66 precision: 2,
67 show_colorbar: true,
68 gridlines: true,
69 title: None,
70 }
71 }
72
73 pub fn colormap(mut self, colormap: GradientColorMap) -> Self {
75 self.colormap = colormap;
76 self
77 }
78
79 pub fn normalization(mut self, norm: Normalization) -> Self {
81 self.norm = norm;
82 self
83 }
84
85 pub fn annotate(mut self, on: bool) -> Self {
87 self.annotate = on;
88 self
89 }
90
91 pub fn precision(mut self, precision: usize) -> Self {
93 self.precision = precision;
94 self
95 }
96
97 pub fn colorbar(mut self, on: bool) -> Self {
99 self.show_colorbar = on;
100 self
101 }
102
103 pub fn gridlines(mut self, on: bool) -> Self {
105 self.gridlines = on;
106 self
107 }
108
109 pub fn title(mut self, title: impl Into<String>) -> Self {
111 self.title = Some(title.into());
112 self
113 }
114
115 pub fn draw<DB: DrawingBackend>(
117 &self,
118 area: &DrawingArea<DB, Shift>,
119 ) -> Result<(), Box<dyn std::error::Error>>
120 where
121 DB::ErrorType: 'static,
122 {
123 let n = self.labels.len();
124 if n == 0 || self.matrix.is_empty() {
125 return Ok(());
126 }
127 let (full_w, _) = area.dim_in_pixel();
128 let (main, cbar) = if self.show_colorbar {
129 let (m, c) = area.split_horizontally(full_w as i32 - 84);
130 (m, Some(c))
131 } else {
132 (area.clone(), None)
133 };
134
135 let (w, h) = main.dim_in_pixel();
136 let (w, h) = (w as i32, h as i32);
137 let top = if self.title.is_some() { 34 } else { 12 };
138 let left = 92;
139 let bottom = 66;
140 let gx0 = left;
141 let gy0 = top;
142 let gx1 = (w - 12).max(gx0 + 1);
143 let gy1 = (h - bottom).max(gy0 + 1);
144 let gw = gx1 - gx0;
145 let gh = gy1 - gy0;
146 let ni = n as i32;
147
148 if let Some(t) = &self.title {
149 area.draw(&Text::new(
150 t.clone(),
151 ((gx0 + gx1) / 2, 8),
152 text_style(HPos::Center, VPos::Top, 18, &BLACK),
153 ))?;
154 }
155
156 for i in 0..n {
157 for j in 0..n {
158 let x0 = gx0 + gw * j as i32 / ni;
159 let x1 = gx0 + gw * (j as i32 + 1) / ni;
160 let y0 = gy0 + gh * i as i32 / ni;
161 let y1 = gy0 + gh * (i as i32 + 1) / ni;
162 let v = self.matrix[i][j];
163 let color = if v.is_finite() {
164 self.colormap.color(self.norm.t(v))
165 } else {
166 MISSING_COLOR
167 };
168 main.draw(&Rectangle::new([(x0, y0), (x1, y1)], color.filled()))?;
169 if self.gridlines {
170 main.draw(&Rectangle::new(
171 [(x0, y0), (x1, y1)],
172 RGBColor(255, 255, 255).stroke_width(1),
173 ))?;
174 }
175 if self.annotate && v.is_finite() {
176 main.draw(&Text::new(
177 format!("{:.*}", self.precision, v),
178 ((x0 + x1) / 2, (y0 + y1) / 2),
179 text_style(HPos::Center, VPos::Center, 12, &contrast_text(color)),
180 ))?;
181 }
182 }
183 }
184
185 for (i, label) in self.labels.iter().enumerate() {
187 let yc = gy0 + gh * (2 * i as i32 + 1) / (2 * ni);
188 main.draw(&Text::new(
189 label.clone(),
190 (gx0 - 6, yc),
191 text_style(HPos::Right, VPos::Center, 13, &BLACK),
192 ))?;
193 }
194 for (j, label) in self.labels.iter().enumerate() {
196 let xc = gx0 + gw * (2 * j as i32 + 1) / (2 * ni);
197 main.draw(&Text::new(
198 label.clone(),
199 (xc, gy1 + 8),
200 text_style(HPos::Center, VPos::Top, 13, &BLACK),
201 ))?;
202 }
203
204 if let Some(c) = cbar {
205 draw_colorbar(&c, &self.colormap, &self.norm, 5)?;
206 }
207 Ok(())
208 }
209}