1use std::f64::consts::PI;
8
9use crate::aes::Aesthetic;
10use crate::coord::Coord;
11use crate::data::DataFrame;
12use crate::position::identity::PositionIdentity;
13use crate::position::Position;
14use crate::render::backend::{DrawBackend, FontFace, RectStyle, TextAnchor, TextStyle};
15use crate::render::RenderError;
16use crate::scale::ScaleSet;
17use crate::stat::identity::StatIdentity;
18use crate::stat::Stat;
19use crate::theme::Theme;
20
21use super::treemap::{contrast_text, fit_size};
22use super::{Geom, GeomParams};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
26pub enum TileShape {
27 #[default]
28 Square,
29 Hexagon,
31 Circle,
32 Diamond,
33}
34
35pub struct GeomTilemap {
38 pub shape: TileShape,
39 pub gap: f64,
41 pub fill: (u8, u8, u8),
43 pub label_size: f64,
44}
45
46impl Default for GeomTilemap {
47 fn default() -> Self {
48 GeomTilemap {
49 shape: TileShape::Square,
50 gap: 0.08,
51 fill: (200, 200, 200),
52 label_size: 10.0,
53 }
54 }
55}
56
57fn centre(shape: TileShape, col: f64, row: f64) -> (f64, f64) {
59 match shape {
60 TileShape::Hexagon => {
61 let odd = (row.round() as i64).rem_euclid(2) == 1;
62 (col + if odd { 0.5 } else { 0.0 }, row * 3f64.sqrt() / 2.0)
63 }
64 _ => (col, row),
65 }
66}
67
68impl Geom for GeomTilemap {
69 fn draw(
70 &self,
71 data: &DataFrame,
72 _coord: &dyn Coord,
73 scales: &ScaleSet,
74 theme: &Theme,
75 backend: &mut dyn DrawBackend,
76 ) -> Result<(), RenderError> {
77 let col = |n: &str| {
78 data.column(n)
79 .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
80 };
81 let (xc, yc) = (col("x")?, col("y")?);
82 let (fill_col, label_col) = (data.column("fill"), data.column("label"));
83 let tiles: Vec<(usize, (f64, f64))> = (0..data.nrows())
84 .filter_map(|i| Some((i, centre(self.shape, xc[i].as_f64()?, yc[i].as_f64()?))))
85 .collect();
86 if tiles.is_empty() {
87 return Ok(());
88 }
89 let area = backend.plot_area();
90 let (mut x0, mut x1, mut y0, mut y1) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
91 for (_, (x, y)) in &tiles {
92 x0 = x0.min(*x);
93 x1 = x1.max(*x);
94 y0 = y0.min(*y);
95 y1 = y1.max(*y);
96 }
97 let (gw, gh) = (x1 - x0 + 1.0, y1 - y0 + 1.0);
99 let unit = (area.width / gw).min(area.height / gh);
100 let ox = area.x + (area.width - unit * gw) / 2.0 + unit / 2.0 - x0 * unit;
101 let oy = area.y + (area.height - unit * gh) / 2.0 + unit / 2.0 - y0 * unit;
102 let r = unit * (1.0 - self.gap) / 2.0;
103 let family = super::resolve_font_family("", theme);
104 let mut labels = Vec::new();
105 for (i, (x, y)) in tiles {
106 let c = (ox + x * unit, oy + y * unit);
107 let fill = fill_col
108 .and_then(|f| scales.map_color(&Aesthetic::Fill, &f[i]))
109 .unwrap_or(self.fill);
110 let name = label_col
111 .map(|l| super::tip_value(&l[i]))
112 .unwrap_or_default();
113 let value = fill_col
114 .map(|f| super::tip_value(&f[i]))
115 .unwrap_or_default();
116 let tip = match (name.is_empty(), value.is_empty()) {
117 (false, false) => format!("{name}: {value}"),
118 (false, true) => name.clone(),
119 _ => value.clone(),
120 };
121 super::set_mark(
122 backend,
123 (!tip.is_empty()).then_some(tip),
124 (!name.is_empty()).then(|| name.clone()),
125 super::series_key(data, i),
126 fill_col.and_then(|f| super::raw_value(&f[i])),
127 );
128 super::set_row_extras(backend, data, i);
129 let style = RectStyle {
130 fill: Some(fill),
131 stroke: None,
132 stroke_width: 0.0,
133 alpha: 1.0,
134 clip: true,
135 corner_radius: 0.0,
136 };
137 let poly = |k: usize, rad: f64, phase: f64| -> Vec<(f64, f64)> {
138 (0..k)
139 .map(|j| {
140 let a = phase + 2.0 * PI * j as f64 / k as f64;
141 (c.0 + rad * a.cos(), c.1 + rad * a.sin())
142 })
143 .collect()
144 };
145 match self.shape {
146 TileShape::Square => {
147 backend.draw_rect((c.0 - r, c.1 - r), (c.0 + r, c.1 + r), &style)?
148 }
149 TileShape::Hexagon => {
152 backend.draw_polygon(&poly(6, r * 2.0 / 3f64.sqrt(), PI / 6.0), &style)?
153 }
154 TileShape::Circle => backend.draw_polygon(&poly(48, r, 0.0), &style)?,
155 TileShape::Diamond => backend.draw_polygon(&poly(4, r, 0.0), &style)?,
156 }
157 if !name.is_empty() {
158 if let Some(size) = fit_size(&name, 1.6 * r, 1.6 * r, self.label_size, 5.0) {
159 labels.push((name, c, size, contrast_text(fill)));
160 }
161 }
162 }
163 super::clear_mark(backend);
164 super::clear_row_extras(backend);
165 for (text, pos, size, color) in labels {
166 backend.draw_text(
167 &text,
168 pos,
169 &TextStyle {
170 color,
171 size,
172 anchor: TextAnchor::Middle,
173 angle: 0.0,
174 family: family.clone(),
175 face: FontFace::Bold,
176 bg_colour: None,
177 bg_r: 0.0,
178 },
179 )?;
180 }
181 Ok(())
182 }
183
184 fn required_aes(&self) -> Vec<Aesthetic> {
185 vec![Aesthetic::X, Aesthetic::Y]
186 }
187 fn default_stat(&self) -> Box<dyn Stat> {
188 Box::new(StatIdentity)
189 }
190 fn default_position(&self) -> Box<dyn Position> {
191 Box::new(PositionIdentity)
192 }
193 fn default_params(&self) -> GeomParams {
194 GeomParams::default()
195 }
196 fn name(&self) -> &str {
197 "tilemap"
198 }
199 fn consumes_tooltips(&self) -> bool {
200 true
201 }
202
203 fn set_series_color(&mut self, color: (u8, u8, u8)) {
205 self.fill = color;
206 }
207}
208
209pub fn us_state_grid() -> Vec<(&'static str, i64, i64)> {
213 vec![
214 ("AK", 0, 0),
215 ("ME", 11, 0),
216 ("VT", 10, 1),
217 ("NH", 11, 1),
218 ("WA", 1, 2),
219 ("ID", 2, 2),
220 ("MT", 3, 2),
221 ("ND", 4, 2),
222 ("MN", 5, 2),
223 ("IL", 6, 2),
224 ("WI", 7, 2),
225 ("MI", 8, 2),
226 ("NY", 9, 2),
227 ("RI", 10, 2),
228 ("MA", 11, 2),
229 ("OR", 1, 3),
230 ("NV", 2, 3),
231 ("WY", 3, 3),
232 ("SD", 4, 3),
233 ("IA", 5, 3),
234 ("IN", 6, 3),
235 ("OH", 7, 3),
236 ("PA", 8, 3),
237 ("NJ", 9, 3),
238 ("CT", 10, 3),
239 ("CA", 1, 4),
240 ("UT", 2, 4),
241 ("CO", 3, 4),
242 ("NE", 4, 4),
243 ("MO", 5, 4),
244 ("KY", 6, 4),
245 ("WV", 7, 4),
246 ("VA", 8, 4),
247 ("MD", 9, 4),
248 ("DE", 10, 4),
249 ("AZ", 2, 5),
250 ("NM", 3, 5),
251 ("KS", 4, 5),
252 ("AR", 5, 5),
253 ("TN", 6, 5),
254 ("NC", 7, 5),
255 ("SC", 8, 5),
256 ("DC", 9, 5),
257 ("OK", 4, 6),
258 ("LA", 5, 6),
259 ("MS", 6, 6),
260 ("AL", 7, 6),
261 ("GA", 8, 6),
262 ("HI", 0, 7),
263 ("TX", 4, 7),
264 ("FL", 9, 7),
265 ]
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn grid_is_complete_and_unique() {
274 let g = us_state_grid();
275 assert_eq!(g.len(), 51);
276 let mut cells: Vec<(i64, i64)> = g.iter().map(|s| (s.1, s.2)).collect();
277 cells.sort();
278 cells.dedup();
279 assert_eq!(cells.len(), 51, "no two states share a cell");
280 assert_eq!(
281 centre(TileShape::Hexagon, 2.0, 1.0),
282 (2.5, 3f64.sqrt() / 2.0)
283 );
284 assert_eq!(centre(TileShape::Square, 2.0, 1.0), (2.0, 1.0));
285 }
286}