1use crate::aes::Aesthetic;
6use crate::coord::Coord;
7use crate::data::{DataFrame, Value};
8use crate::position::identity::PositionIdentity;
9use crate::position::Position;
10use crate::render::backend::{DrawBackend, FontFace, RectStyle, TextAnchor, TextStyle};
11use crate::render::RenderError;
12use crate::scale::ScaleSet;
13use crate::stat::treemap::StatTreemap;
14use crate::stat::Stat;
15use crate::theme::Theme;
16
17use super::{Geom, GeomParams};
18
19pub struct GeomTreemap {
22 pub aspect: f64,
25 pub group_padding: f64,
28 pub fill: (u8, u8, u8),
30 pub border: (u8, u8, u8),
32 pub border_width: f64,
33 pub group_border_width: f64,
35 pub alpha: f64,
36 pub label_size: f64,
38 pub min_label_size: f64,
39 pub group_label_size: f64,
41}
42
43impl Default for GeomTreemap {
44 fn default() -> Self {
45 GeomTreemap {
46 aspect: 1.6,
47 group_padding: 0.006,
48 fill: (97, 156, 255),
49 border: (255, 255, 255),
50 border_width: 1.0,
51 group_border_width: 3.0,
52 alpha: 1.0,
53 label_size: 11.0,
54 min_label_size: 6.0,
55 group_label_size: 15.0,
56 }
57 }
58}
59
60pub(crate) fn contrast_text(bg: (u8, u8, u8)) -> (u8, u8, u8) {
62 let lin = |c: u8| {
63 let c = c as f64 / 255.0;
64 if c <= 0.03928 {
65 c / 12.92
66 } else {
67 ((c + 0.055) / 1.055).powf(2.4)
68 }
69 };
70 let l = 0.2126 * lin(bg.0) + 0.7152 * lin(bg.1) + 0.0722 * lin(bg.2);
71 if l > 0.4 {
72 (30, 30, 30)
73 } else {
74 (255, 255, 255)
75 }
76}
77
78pub(crate) fn fit_size(text: &str, w: f64, h: f64, max: f64, min: f64) -> Option<f64> {
81 let chars = text.chars().count().max(1) as f64;
82 let size = max.min((w - 6.0) / (chars * 0.68)).min(h - 4.0);
83 (size >= min).then_some(size)
84}
85
86impl Geom for GeomTreemap {
87 fn draw(
88 &self,
89 data: &DataFrame,
90 coord: &dyn Coord,
91 scales: &ScaleSet,
92 theme: &Theme,
93 backend: &mut dyn DrawBackend,
94 ) -> Result<(), RenderError> {
95 let col = |n: &str| {
96 data.column(n)
97 .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
98 };
99 let (part, x0c, x1c, y0c, y1c, area_c) = (
100 col("part")?,
101 col("xmin")?,
102 col("xmax")?,
103 col("ymin")?,
104 col("ymax")?,
105 col("area")?,
106 );
107 let fill_col = data.column("fill");
108 let label_col = data.column("label");
109 let sub_col = data.column("subgroup");
110 let area = backend.plot_area();
111 let px = |i: usize| {
112 let m = |aes: Aesthetic, v: &Value| scales.get(&aes).map(|s| s.map(v)).unwrap_or(0.0);
113 let (a, b) = (
114 coord.transform((m(Aesthetic::X, &x0c[i]), m(Aesthetic::Y, &y1c[i])), &area),
115 coord.transform((m(Aesthetic::X, &x1c[i]), m(Aesthetic::Y, &y0c[i])), &area),
116 );
117 ((a.0.min(b.0), a.1.min(b.1)), (a.0.max(b.0), a.1.max(b.1)))
118 };
119 let family = super::resolve_font_family("", theme);
120 let mut tile_labels = Vec::new();
121 for i in (0..data.nrows()).filter(|&i| part[i].key_str() == "tile") {
122 let ((l, t), (r, b)) = px(i);
123 let fill = fill_col
124 .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
125 .unwrap_or(self.fill);
126 let label = label_col
127 .map(|c| super::tip_value(&c[i]))
128 .filter(|s| !s.is_empty());
129 let name = label
130 .clone()
131 .or_else(|| sub_col.map(|c| super::tip_value(&c[i])))
132 .unwrap_or_default();
133 let tip = if name.is_empty() {
134 super::tip_value(&area_c[i])
135 } else {
136 format!("{name}: {}", super::tip_value(&area_c[i]))
137 };
138 super::set_mark(
139 backend,
140 Some(tip),
141 label.clone(),
142 super::series_key(data, i),
143 super::raw_value(&area_c[i]),
144 );
145 super::set_row_extras(backend, data, i);
146 backend.draw_rect(
147 (l, t),
148 (r, b),
149 &RectStyle {
150 fill: Some(fill),
151 stroke: Some(self.border),
152 stroke_width: self.border_width,
153 alpha: self.alpha,
154 clip: true,
155 corner_radius: 0.0,
156 },
157 )?;
158 if let Some(label) = label {
159 if let Some(size) =
160 fit_size(&label, r - l, b - t, self.label_size, self.min_label_size)
161 {
162 let pos = (l + 4.0, t + 3.0 + size / 2.0);
163 tile_labels.push((label, pos, size, contrast_text(fill)));
164 }
165 }
166 }
167 super::clear_row_extras(backend);
168 super::clear_mark(backend);
169
170 let groups: Vec<usize> = (0..data.nrows())
171 .filter(|&i| part[i].key_str() == "group")
172 .collect();
173 for &i in &groups {
174 let ((l, t), (r, b)) = px(i);
175 backend.draw_rect(
176 (l, t),
177 (r, b),
178 &RectStyle {
179 fill: None,
180 stroke: Some(self.border),
181 stroke_width: self.group_border_width,
182 alpha: 1.0,
183 clip: true,
184 corner_radius: 0.0,
185 },
186 )?;
187 }
188 for (label, pos, size, color) in tile_labels {
189 backend.draw_text(
190 &label,
191 pos,
192 &TextStyle {
193 color,
194 size,
195 anchor: TextAnchor::Start,
196 angle: 0.0,
197 family: family.clone(),
198 face: FontFace::Plain,
199 bg_colour: None,
200 bg_r: 0.0,
201 },
202 )?;
203 }
204 if self.group_label_size > 0.0 {
205 for &i in &groups {
206 let Some(name) = sub_col.map(|c| super::tip_value(&c[i])) else {
207 continue;
208 };
209 let ((l, t), (r, b)) = px(i);
210 if let Some(size) = fit_size(&name, r - l, b - t, self.group_label_size, 8.0) {
211 let center = ((l + r) / 2.0, (t + b) / 2.0);
212 backend.draw_text(
213 &name,
214 center,
215 &TextStyle {
216 color: (255, 255, 255),
217 size,
218 anchor: TextAnchor::Middle,
219 angle: 0.0,
220 family: family.clone(),
221 face: FontFace::Bold,
222 bg_colour: Some((40, 40, 40)),
223 bg_r: 2.0,
224 },
225 )?;
226 }
227 }
228 }
229 Ok(())
230 }
231
232 fn required_aes(&self) -> Vec<Aesthetic> {
233 vec![Aesthetic::Weight]
234 }
235
236 fn default_stat(&self) -> Box<dyn Stat> {
237 Box::new(StatTreemap {
238 aspect: self.aspect,
239 group_padding: self.group_padding,
240 })
241 }
242
243 fn default_position(&self) -> Box<dyn Position> {
244 Box::new(PositionIdentity)
245 }
246
247 fn default_params(&self) -> GeomParams {
248 GeomParams::default()
249 }
250
251 fn name(&self) -> &str {
252 "treemap"
253 }
254
255 fn consumes_tooltips(&self) -> bool {
256 true
257 }
258
259 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
260 if let (true, Some(paper)) = (self.border == (255, 255, 255), el.paper) {
261 self.border = paper;
262 }
263 }
264
265 fn set_series_color(&mut self, color: (u8, u8, u8)) {
266 self.fill = color;
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn label_fitting_and_contrast() {
276 assert_eq!(fit_size("abc", 200.0, 50.0, 11.0, 6.0), Some(11.0));
277 assert!(fit_size("a long label", 30.0, 50.0, 11.0, 6.0).is_none());
278 assert_eq!(contrast_text((255, 255, 200)), (30, 30, 30));
279 assert_eq!(contrast_text((20, 40, 90)), (255, 255, 255));
280 }
281}