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 key_col = data.column("key");
110 let sub_col = data.column("subgroup");
111 let area = backend.plot_area();
112 let px = |i: usize| {
113 let m = |aes: Aesthetic, v: &Value| scales.get(&aes).map(|s| s.map(v)).unwrap_or(0.0);
114 let (a, b) = (
115 coord.transform((m(Aesthetic::X, &x0c[i]), m(Aesthetic::Y, &y1c[i])), &area),
116 coord.transform((m(Aesthetic::X, &x1c[i]), m(Aesthetic::Y, &y0c[i])), &area),
117 );
118 ((a.0.min(b.0), a.1.min(b.1)), (a.0.max(b.0), a.1.max(b.1)))
119 };
120 let family = super::resolve_font_family("", theme);
121 let mut tile_labels = Vec::new();
122 for i in (0..data.nrows()).filter(|&i| part[i].key_str() == "tile") {
123 let ((l, t), (r, b)) = px(i);
124 let fill = fill_col
125 .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
126 .unwrap_or(self.fill);
127 let label = label_col
128 .map(|c| super::tip_value(&c[i]))
129 .filter(|s| !s.is_empty());
130 let name = label
131 .clone()
132 .or_else(|| sub_col.map(|c| super::tip_value(&c[i])))
133 .unwrap_or_default();
134 let tip = if name.is_empty() {
135 super::tip_value(&area_c[i])
136 } else {
137 format!("{name}: {}", super::tip_value(&area_c[i]))
138 };
139 let key = key_col
141 .map(|c| super::tip_value(&c[i]))
142 .filter(|s| !s.is_empty())
143 .or_else(|| label.clone());
144 super::set_mark(
145 backend,
146 Some(tip),
147 key,
148 super::series_key(data, i),
149 super::raw_value(&area_c[i]),
150 );
151 super::set_row_extras(backend, data, i);
152 backend.draw_rect(
153 (l, t),
154 (r, b),
155 &RectStyle {
156 fill: Some(fill),
157 stroke: Some(self.border),
158 stroke_width: self.border_width,
159 alpha: self.alpha,
160 clip: true,
161 corner_radius: 0.0,
162 },
163 )?;
164 if let Some(label) = label {
165 if let Some(size) =
166 fit_size(&label, r - l, b - t, self.label_size, self.min_label_size)
167 {
168 let pos = (l + 4.0, t + 3.0 + size / 2.0);
169 tile_labels.push((label, pos, size, contrast_text(fill)));
170 }
171 }
172 }
173 super::clear_row_extras(backend);
174 super::clear_mark(backend);
175
176 let groups: Vec<usize> = (0..data.nrows())
177 .filter(|&i| part[i].key_str() == "group")
178 .collect();
179 for &i in &groups {
180 let ((l, t), (r, b)) = px(i);
181 backend.draw_rect(
182 (l, t),
183 (r, b),
184 &RectStyle {
185 fill: None,
186 stroke: Some(self.border),
187 stroke_width: self.group_border_width,
188 alpha: 1.0,
189 clip: true,
190 corner_radius: 0.0,
191 },
192 )?;
193 }
194 for (label, pos, size, color) in tile_labels {
195 backend.draw_text(
196 &label,
197 pos,
198 &TextStyle {
199 color,
200 size,
201 anchor: TextAnchor::Start,
202 angle: 0.0,
203 family: family.clone(),
204 face: FontFace::Plain,
205 bg_colour: None,
206 bg_r: 0.0,
207 },
208 )?;
209 }
210 if self.group_label_size > 0.0 {
211 for &i in &groups {
212 let Some(name) = sub_col.map(|c| super::tip_value(&c[i])) else {
213 continue;
214 };
215 let ((l, t), (r, b)) = px(i);
216 if let Some(size) = fit_size(&name, r - l, b - t, self.group_label_size, 8.0) {
217 let center = ((l + r) / 2.0, (t + b) / 2.0);
218 backend.draw_text(
219 &name,
220 center,
221 &TextStyle {
222 color: (255, 255, 255),
223 size,
224 anchor: TextAnchor::Middle,
225 angle: 0.0,
226 family: family.clone(),
227 face: FontFace::Bold,
228 bg_colour: Some((40, 40, 40)),
229 bg_r: 2.0,
230 },
231 )?;
232 }
233 }
234 }
235 Ok(())
236 }
237
238 fn required_aes(&self) -> Vec<Aesthetic> {
239 vec![Aesthetic::Weight]
240 }
241
242 fn default_stat(&self) -> Box<dyn Stat> {
243 Box::new(StatTreemap {
244 aspect: self.aspect,
245 group_padding: self.group_padding,
246 })
247 }
248
249 fn default_position(&self) -> Box<dyn Position> {
250 Box::new(PositionIdentity)
251 }
252
253 fn default_params(&self) -> GeomParams {
254 GeomParams::default()
255 }
256
257 fn name(&self) -> &str {
258 "treemap"
259 }
260
261 fn consumes_tooltips(&self) -> bool {
262 true
263 }
264
265 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
266 if let (true, Some(paper)) = (self.border == (255, 255, 255), el.paper) {
267 self.border = paper;
268 }
269 }
270
271 fn set_series_color(&mut self, color: (u8, u8, u8)) {
272 self.fill = color;
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn label_fitting_and_contrast() {
282 assert_eq!(fit_size("abc", 200.0, 50.0, 11.0, 6.0), Some(11.0));
283 assert!(fit_size("a long label", 30.0, 50.0, 11.0, 6.0).is_none());
284 assert_eq!(contrast_text((255, 255, 200)), (30, 30, 30));
285 assert_eq!(contrast_text((20, 40, 90)), (255, 255, 255));
286 }
287}