1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::DataFrame;
4use crate::position::identity::PositionIdentity;
5use crate::position::Position;
6use crate::render::backend::{DrawBackend, RectStyle};
7use crate::render::RenderError;
8use crate::scale::ScaleSet;
9use crate::stat::binhex::{HexLattice, StatBinHex};
10use crate::stat::Stat;
11use crate::theme::Theme;
12
13use super::{Geom, GeomParams};
14
15fn fallback_hex_size(xs: impl Iterator<Item = f64>, ys: impl Iterator<Item = f64>) -> (f64, f64) {
32 let res_x = super::continuous_bar_half_width(xs, 2.0, 1.0);
33 let res_y = super::continuous_bar_half_width(ys, 2.0, 1.0);
34 (2.0 * res_x, 2.0 * res_y / 3.0_f64.sqrt())
35}
36
37pub struct GeomHex {
43 pub color: (u8, u8, u8),
44 pub alpha: f64,
45 pub line_width: f64,
46}
47
48impl Default for GeomHex {
49 fn default() -> Self {
50 GeomHex {
51 color: (50, 50, 50),
52 alpha: 1.0,
53 line_width: 0.2,
54 }
55 }
56}
57
58impl GeomHex {
59 pub fn color(mut self, color: (u8, u8, u8)) -> Self {
60 self.color = color;
61 self
62 }
63
64 pub fn linewidth(mut self, width: f64) -> Self {
66 self.line_width = width;
67 self
68 }
69}
70
71impl Geom for GeomHex {
72 fn draw(
73 &self,
74 data: &DataFrame,
75 coord: &dyn Coord,
76 scales: &ScaleSet,
77 _theme: &Theme,
78 backend: &mut dyn DrawBackend,
79 ) -> Result<(), RenderError> {
80 let x_col = data
81 .column("x")
82 .ok_or(RenderError::MissingAesthetic("x".into()))?;
83 let y_col = data
84 .column("y")
85 .ok_or(RenderError::MissingAesthetic("y".into()))?;
86 let fill_col = data.column("fill");
87 let width_col = data.column("width");
88 let height_col = data.column("height");
89
90 let plot_area = backend.plot_area();
91 let x_scale = scales.get(&Aesthetic::X);
92 let y_scale = scales.get(&Aesthetic::Y);
93
94 let (fallback_w, fallback_h) = fallback_hex_size(
95 x_col.iter().filter_map(|v| v.as_f64()),
96 y_col.iter().filter_map(|v| v.as_f64()),
97 );
98
99 for i in 0..data.nrows() {
100 let (Some(cx), Some(cy)) = (x_col[i].as_f64(), y_col[i].as_f64()) else {
101 continue;
102 };
103 let width = width_col
104 .and_then(|wc| wc[i].as_f64())
105 .filter(|w| w.is_finite() && *w > 0.0)
106 .unwrap_or(fallback_w);
107 let height = height_col
108 .and_then(|hc| hc[i].as_f64())
109 .filter(|h| h.is_finite() && *h > 0.0)
110 .unwrap_or(fallback_h);
111
112 let fill_color = fill_col
113 .and_then(|fc| scales.map_color(&Aesthetic::Fill, &fc[i]))
114 .unwrap_or((97, 156, 255));
115
116 super::set_mark(
117 backend,
118 fill_col.map(|c| format!("n = {}", super::tip_value(&c[i]))),
119 None,
120 None,
121 fill_col.and_then(|c| super::raw_value(&c[i])),
122 );
123 super::set_mark_formatted(
124 backend,
125 scales,
126 None,
127 &Aesthetic::Fill,
128 fill_col.map(|c| &c[i]),
129 );
130 super::set_row_extras(backend, data, i);
131
132 let verts_data = HexLattice::new(width, height, (cx, cy)).vertices(0, 0);
135 let vertices: Vec<(f64, f64)> = verts_data
136 .iter()
137 .map(|&(vx, vy)| {
138 let nx = x_scale
139 .map(|s| s.map(&crate::data::Value::Float(vx)))
140 .unwrap_or(0.0);
141 let ny = y_scale
142 .map(|s| s.map(&crate::data::Value::Float(vy)))
143 .unwrap_or(0.0);
144 coord.transform((nx, ny), &plot_area)
145 })
146 .collect();
147
148 backend.draw_polygon(
149 &vertices,
150 &RectStyle {
151 fill: Some(fill_color),
152 stroke: Some(self.color),
153 stroke_width: self.line_width,
154 alpha: self.alpha,
155 clip: true,
156 corner_radius: 0.0,
157 },
158 )?;
159 }
160 super::clear_mark(backend);
161 super::clear_row_extras(backend);
162
163 Ok(())
164 }
165
166 fn required_aes(&self) -> Vec<Aesthetic> {
167 vec![Aesthetic::X, Aesthetic::Y]
168 }
169
170 fn default_stat(&self) -> Box<dyn Stat> {
171 Box::new(StatBinHex::default())
172 }
173
174 fn default_position(&self) -> Box<dyn Position> {
175 Box::new(PositionIdentity)
176 }
177
178 fn default_params(&self) -> GeomParams {
179 GeomParams::default()
180 }
181
182 fn name(&self) -> &str {
183 "hex"
184 }
185
186 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
187 if let (true, Some(ink)) = (self.color == (50, 50, 50), el.ink) {
188 self.color = ink;
189 }
190 }
191
192 fn consumes_tooltips(&self) -> bool {
193 true
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::fallback_hex_size;
200 use crate::data::Value;
201 use crate::prelude::*;
202
203 #[test]
206 fn fallback_size_is_twice_the_resolution_scaled_for_the_lattice() {
207 let (w, h) = fallback_hex_size([0.0, 2.0].into_iter(), [0.0, 3.0].into_iter());
208 assert!((w - 4.0).abs() < 1e-9, "w={w}");
209 let expected_h = 2.0 * 3.0 / 3.0_f64.sqrt();
210 assert!(
211 (h - expected_h).abs() < 1e-9,
212 "h={h}, expected={expected_h}"
213 );
214 }
215
216 #[test]
217 fn fallback_size_gives_regular_hexagons_on_a_square_lattice() {
218 let w_lattice = 5.0;
223 let res_x = w_lattice / 2.0;
224 let res_y = w_lattice * 3.0_f64.sqrt() / 2.0;
225 let (w, h) = fallback_hex_size([0.0, res_x].into_iter(), [0.0, res_y].into_iter());
226 assert!((w - w_lattice).abs() < 1e-9, "w={w}");
227 assert!((h - w_lattice).abs() < 1e-9, "h={h}");
228 }
229
230 #[test]
238 #[cfg(feature = "sf")]
239 fn geom_hex_without_width_height_columns_draws_regular_hexagons() {
240 let res_x = 5.0;
244 let res_y = res_x * 3.0_f64.sqrt();
245 let svg = GGPlot::new(vec![
246 ("x".into(), vec![Value::Float(0.0), Value::Float(res_x)]),
247 ("y".into(), vec![Value::Float(0.0), Value::Float(res_y)]),
248 ])
249 .aes(Aes::new().x("x").y("y"))
250 .geom_hex_with(GeomHex::default())
251 .stat(crate::stat::identity::StatIdentity)
252 .coord_sf()
253 .render_svg_native_with_size(400, 400)
254 .unwrap();
255
256 assert!(!svg.contains("NaN"));
257 let poly = svg.split("<polygon").nth(1).expect("a hex polygon");
258 let points_attr = poly
259 .split("points=\"")
260 .nth(1)
261 .unwrap()
262 .split('"')
263 .next()
264 .unwrap();
265 let pts: Vec<(f64, f64)> = points_attr
266 .split(' ')
267 .map(|p| {
268 let mut it = p.split(',');
269 let x: f64 = it.next().unwrap().parse().unwrap();
270 let y: f64 = it.next().unwrap().parse().unwrap();
271 (x, y)
272 })
273 .collect();
274 assert_eq!(pts.len(), 6);
275 let edge =
276 |a: (f64, f64), b: (f64, f64)| ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt();
277 let edges: Vec<f64> = (0..6).map(|i| edge(pts[i], pts[(i + 1) % 6])).collect();
278 let first = edges[0];
279 for e in &edges {
280 assert!((e - first).abs() < 0.05, "edges should be equal: {edges:?}");
281 }
282 }
283
284 #[test]
287 fn color_and_linewidth_builders_change_the_rendered_outline() {
288 let svg = GGPlot::new(vec![
289 ("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
290 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
291 ])
292 .aes(Aes::new().x("x").y("y"))
293 .geom_hex_with(GeomHex::default().color((10, 20, 30)).linewidth(3.0))
294 .render_svg_native_with_size(200, 200)
295 .unwrap();
296 assert!(svg.contains("stroke=\"#0A141E\""));
297 assert!(svg.contains("stroke-width=\"3.00\""));
298 }
299
300 #[test]
303 fn a_row_with_non_numeric_position_is_skipped() {
304 let svg = GGPlot::new(vec![
305 (
306 "x".into(),
307 vec![Value::Float(0.0), Value::Na, Value::Float(5.0)],
308 ),
309 (
310 "y".into(),
311 vec![Value::Float(0.0), Value::Float(1.0), Value::Float(5.0)],
312 ),
313 ])
314 .aes(Aes::new().x("x").y("y"))
315 .geom_hex_with(GeomHex::default())
316 .stat(crate::stat::identity::StatIdentity)
317 .render_svg_native_with_size(200, 200)
318 .unwrap();
319 assert!(!svg.contains("NaN"));
320 assert_eq!(svg.matches("<polygon").count(), 2);
321 }
322}