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, TextAnchor, TextStyle};
7use crate::render::text::{wrap_text, wrapped_block_width};
8use crate::render::RenderError;
9use crate::scale::ScaleSet;
10use crate::stat::identity::StatIdentity;
11use crate::stat::Stat;
12use crate::theme::Theme;
13
14use super::{Geom, GeomParams};
15
16pub struct GeomText {
18 pub size: f64,
19 pub color: (u8, u8, u8),
20 pub alpha: f64,
21 pub hjust: f64,
23 pub vjust: f64,
25 pub fontfamily: String,
27 pub check_overlap: bool,
29 pub wrap_width: Option<f64>,
33 pub line_height: f64,
37}
38
39impl GeomText {
40 pub fn with_hjust(mut self, hjust: f64) -> Self {
41 self.hjust = hjust;
42 self
43 }
44
45 pub fn with_vjust(mut self, vjust: f64) -> Self {
46 self.vjust = vjust;
47 self
48 }
49
50 pub fn with_fontfamily(mut self, family: &str) -> Self {
51 self.fontfamily = family.to_string();
52 self
53 }
54
55 pub fn with_check_overlap(mut self, check: bool) -> Self {
56 self.check_overlap = check;
57 self
58 }
59
60 pub fn with_wrap(mut self, width_px: f64) -> Self {
62 self.wrap_width = Some(width_px);
63 self
64 }
65
66 pub fn with_line_height(mut self, line_height: f64) -> Self {
68 self.line_height = line_height;
69 self
70 }
71}
72
73impl Default for GeomText {
74 fn default() -> Self {
75 GeomText {
76 size: 10.0,
77 color: (0, 0, 0),
78 alpha: 1.0,
79 hjust: 0.5,
80 vjust: 0.5,
81 fontfamily: String::new(),
82 check_overlap: false,
83 wrap_width: None,
84 line_height: 1.2,
85 }
86 }
87}
88
89impl Geom for GeomText {
90 fn draw(
91 &self,
92 data: &DataFrame,
93 coord: &dyn Coord,
94 scales: &ScaleSet,
95 theme: &Theme,
96 backend: &mut dyn DrawBackend,
97 ) -> Result<(), RenderError> {
98 let x_col = data
99 .column("x")
100 .ok_or(RenderError::MissingAesthetic("x".into()))?;
101 let y_col = data
102 .column("y")
103 .ok_or(RenderError::MissingAesthetic("y".into()))?;
104 let label_col = data
105 .column("label")
106 .ok_or(RenderError::MissingAesthetic("label".into()))?;
107
108 let plot_area = backend.plot_area();
109 let xm = super::support::XMapper::new(data, scales);
110 let y_scale = scales.get(&Aesthetic::Y);
111
112 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
113
114 for i in 0..data.nrows() {
115 super::set_row_extras(backend, data, i);
117 backend.set_tooltip(None);
120 let nx = xm.map(&x_col[i], i);
121 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
122 let (px, py) = coord.transform((nx, ny), &plot_area);
123
124 let text = label_col[i].to_group_key();
125 let lines = match self.wrap_width {
126 Some(w) => wrap_text(&text, w, self.size),
127 None => vec![text],
128 };
129 let lh = self.size * self.line_height;
130 let n = lines.len() as f64;
131 let block_h = if n <= 1.0 { self.size } else { lh * n };
139 let py_block = py + (self.vjust - 0.5) * block_h;
140
141 if self.check_overlap {
142 let w = wrapped_block_width(&lines, self.size);
143 let bbox = (
144 px - w / 2.0,
145 py_block - block_h / 2.0,
146 px + w / 2.0,
147 py_block + block_h / 2.0,
148 );
149 if bboxes_overlap(&bbox, &drawn_bboxes) {
150 continue;
151 }
152 drawn_bboxes.push(bbox);
153 }
154
155 let anchor = hjust_to_anchor(self.hjust);
156 let family = super::resolve_font_family(&self.fontfamily, theme);
157 let top = py_block - block_h / 2.0;
158 for (li, line) in lines.iter().enumerate() {
159 let line_y = if n <= 1.0 {
163 py_block
164 } else {
165 top + lh * (li as f64 + 0.5)
166 };
167 backend.draw_text(
168 line,
169 (px, line_y),
170 &TextStyle {
171 color: self.color,
172 size: self.size,
173 anchor: anchor.clone(),
174 angle: 0.0,
175 family: family.clone(),
176 face: crate::render::backend::FontFace::Plain,
177 },
178 )?;
179 }
180 }
181 super::clear_row_extras(backend);
182
183 Ok(())
184 }
185
186 fn required_aes(&self) -> Vec<Aesthetic> {
187 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
188 }
189
190 fn default_stat(&self) -> Box<dyn Stat> {
191 Box::new(StatIdentity)
192 }
193 fn default_position(&self) -> Box<dyn Position> {
194 Box::new(PositionIdentity)
195 }
196 fn default_params(&self) -> GeomParams {
197 GeomParams::default()
198 }
199 fn name(&self) -> &str {
200 "text"
201 }
202 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
203 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
204 self.color = ink;
205 }
206 }
207}
208
209pub struct GeomLabel {
211 pub size: f64,
212 pub color: (u8, u8, u8),
213 pub fill: (u8, u8, u8),
214 pub alpha: f64,
215 pub padding: f64,
216 pub hjust: f64,
218 pub vjust: f64,
220 pub fontfamily: String,
222 pub check_overlap: bool,
224 pub wrap_width: Option<f64>,
230 pub line_height: f64,
234 pub radius: f64,
238}
239
240impl GeomLabel {
241 pub fn with_hjust(mut self, hjust: f64) -> Self {
242 self.hjust = hjust;
243 self
244 }
245
246 pub fn with_vjust(mut self, vjust: f64) -> Self {
247 self.vjust = vjust;
248 self
249 }
250
251 pub fn with_fontfamily(mut self, family: &str) -> Self {
252 self.fontfamily = family.to_string();
253 self
254 }
255
256 pub fn with_check_overlap(mut self, check: bool) -> Self {
257 self.check_overlap = check;
258 self
259 }
260
261 pub fn with_wrap(mut self, width_px: f64) -> Self {
264 self.wrap_width = Some(width_px);
265 self
266 }
267
268 pub fn with_line_height(mut self, line_height: f64) -> Self {
270 self.line_height = line_height;
271 self
272 }
273
274 pub fn with_radius(mut self, radius_px: f64) -> Self {
276 self.radius = radius_px;
277 self
278 }
279}
280
281impl Default for GeomLabel {
282 fn default() -> Self {
283 GeomLabel {
284 size: 10.0,
285 color: (0, 0, 0),
286 fill: (255, 255, 255),
287 alpha: 0.8,
288 padding: 3.0,
289 hjust: 0.5,
290 vjust: 0.5,
291 fontfamily: String::new(),
292 check_overlap: false,
293 wrap_width: None,
294 line_height: 1.2,
295 radius: 0.0,
296 }
297 }
298}
299
300impl Geom for GeomLabel {
301 fn draw(
302 &self,
303 data: &DataFrame,
304 coord: &dyn Coord,
305 scales: &ScaleSet,
306 theme: &Theme,
307 backend: &mut dyn DrawBackend,
308 ) -> Result<(), RenderError> {
309 let x_col = data
310 .column("x")
311 .ok_or(RenderError::MissingAesthetic("x".into()))?;
312 let y_col = data
313 .column("y")
314 .ok_or(RenderError::MissingAesthetic("y".into()))?;
315 let label_col = data
316 .column("label")
317 .ok_or(RenderError::MissingAesthetic("label".into()))?;
318
319 let plot_area = backend.plot_area();
320 let xm = super::support::XMapper::new(data, scales);
321 let y_scale = scales.get(&Aesthetic::Y);
322
323 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
324
325 for i in 0..data.nrows() {
326 super::set_row_extras(backend, data, i);
328 backend.set_tooltip(None);
331 let nx = xm.map(&x_col[i], i);
332 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
333 let (px, py) = coord.transform((nx, ny), &plot_area);
334
335 let text = label_col[i].to_group_key();
336 let (lines, half_w) = match self.wrap_width {
343 Some(w) => {
344 let lines = wrap_text(&text, w, self.size);
345 let half_w = wrapped_block_width(&lines, self.size) / 2.0 + self.padding;
346 (lines, half_w)
347 }
348 None => {
349 let half_w = text.len() as f64 * self.size * 0.6 / 2.0 + self.padding;
350 (vec![text], half_w)
351 }
352 };
353 let lh = self.size * self.line_height;
354 let n = lines.len() as f64;
355 let block_h = if n <= 1.0 { self.size } else { lh * n };
358 let py_block = py + (self.vjust - 0.5) * block_h;
359 let half_h = block_h / 2.0 + self.padding;
360
361 if self.check_overlap {
362 let bbox = (
363 px - half_w,
364 py_block - half_h,
365 px + half_w,
366 py_block + half_h,
367 );
368 if bboxes_overlap(&bbox, &drawn_bboxes) {
369 continue;
370 }
371 drawn_bboxes.push(bbox);
372 }
373
374 backend.draw_rect(
377 (px - half_w, py_block - half_h),
378 (px + half_w, py_block + half_h),
379 &RectStyle {
380 fill: Some(self.fill),
381 stroke: Some(self.color),
382 stroke_width: 0.5,
383 alpha: self.alpha,
384 clip: true,
385 corner_radius: self.radius,
386 },
387 )?;
388
389 let anchor = hjust_to_anchor(self.hjust);
391 let family = super::resolve_font_family(&self.fontfamily, theme);
392 let top = py_block - block_h / 2.0;
393 for (li, line) in lines.iter().enumerate() {
394 let line_y = if n <= 1.0 {
395 py_block
396 } else {
397 top + lh * (li as f64 + 0.5)
398 };
399 backend.draw_text(
400 line,
401 (px, line_y),
402 &TextStyle {
403 color: self.color,
404 size: self.size,
405 anchor: anchor.clone(),
406 angle: 0.0,
407 family: family.clone(),
408 face: crate::render::backend::FontFace::Plain,
409 },
410 )?;
411 }
412 }
413 super::clear_row_extras(backend);
414
415 Ok(())
416 }
417
418 fn required_aes(&self) -> Vec<Aesthetic> {
419 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
420 }
421
422 fn default_stat(&self) -> Box<dyn Stat> {
423 Box::new(StatIdentity)
424 }
425 fn default_position(&self) -> Box<dyn Position> {
426 Box::new(PositionIdentity)
427 }
428 fn default_params(&self) -> GeomParams {
429 GeomParams::default()
430 }
431 fn name(&self) -> &str {
432 "label"
433 }
434 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
435 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
436 self.color = ink;
437 }
438 if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
439 self.fill = paper;
440 }
441 }
442}
443
444fn hjust_to_anchor(hjust: f64) -> TextAnchor {
446 if hjust < 0.25 {
447 TextAnchor::Start
448 } else if hjust > 0.75 {
449 TextAnchor::End
450 } else {
451 TextAnchor::Middle
452 }
453}
454
455fn bboxes_overlap(candidate: &(f64, f64, f64, f64), existing: &[(f64, f64, f64, f64)]) -> bool {
457 for b in existing {
458 if candidate.0 < b.2 && candidate.2 > b.0 && candidate.1 < b.3 && candidate.3 > b.1 {
460 return true;
461 }
462 }
463 false
464}