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, FontFace, 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 pub fontface: FontFace,
41 pub bg_colour: Option<(u8, u8, u8)>,
47 pub bg_r: f64,
50}
51
52impl GeomText {
53 pub fn with_fontface(mut self, face: FontFace) -> Self {
55 self.fontface = face;
56 self
57 }
58
59 pub fn bg_colour(mut self, colour: (u8, u8, u8)) -> Self {
63 self.bg_colour = Some(colour);
64 self
65 }
66
67 pub fn bg_r(mut self, r: f64) -> Self {
69 self.bg_r = r;
70 self
71 }
72
73 pub fn with_hjust(mut self, hjust: f64) -> Self {
74 self.hjust = hjust;
75 self
76 }
77
78 pub fn with_vjust(mut self, vjust: f64) -> Self {
79 self.vjust = vjust;
80 self
81 }
82
83 pub fn with_fontfamily(mut self, family: &str) -> Self {
84 self.fontfamily = family.to_string();
85 self
86 }
87
88 pub fn with_check_overlap(mut self, check: bool) -> Self {
89 self.check_overlap = check;
90 self
91 }
92
93 pub fn with_wrap(mut self, width_px: f64) -> Self {
95 self.wrap_width = Some(width_px);
96 self
97 }
98
99 pub fn with_line_height(mut self, line_height: f64) -> Self {
101 self.line_height = line_height;
102 self
103 }
104}
105
106impl Default for GeomText {
107 fn default() -> Self {
108 GeomText {
109 size: 10.0,
110 color: (0, 0, 0),
111 alpha: 1.0,
112 hjust: 0.5,
113 vjust: 0.5,
114 fontfamily: String::new(),
115 check_overlap: false,
116 wrap_width: None,
117 line_height: 1.2,
118 fontface: FontFace::Plain,
119 bg_colour: None,
120 bg_r: 0.0,
121 }
122 }
123}
124
125impl Geom for GeomText {
126 fn draw(
127 &self,
128 data: &DataFrame,
129 coord: &dyn Coord,
130 scales: &ScaleSet,
131 theme: &Theme,
132 backend: &mut dyn DrawBackend,
133 ) -> Result<(), RenderError> {
134 let x_col = data
135 .column("x")
136 .ok_or(RenderError::MissingAesthetic("x".into()))?;
137 let y_col = data
138 .column("y")
139 .ok_or(RenderError::MissingAesthetic("y".into()))?;
140 let label_col = data
141 .column("label")
142 .ok_or(RenderError::MissingAesthetic("label".into()))?;
143 let fontface_col = data.column("fontface");
144
145 let plot_area = backend.plot_area();
146 let xm = super::support::XMapper::new(data, scales);
147 let y_scale = scales.get(&Aesthetic::Y);
148
149 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
150
151 for i in 0..data.nrows() {
152 super::set_row_extras(backend, data, i);
154 backend.set_tooltip(None);
157 let nx = xm.map(&x_col[i], i);
158 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
159 let (px, py) = coord.transform((nx, ny), &plot_area);
160
161 let face = fontface_col
163 .map(|c| &c[i])
164 .filter(|v| !v.is_na())
165 .and_then(|v| super::parse_fontface(&v.to_group_key()))
166 .unwrap_or(self.fontface);
167
168 let text = label_col[i].to_group_key();
169 let lines = match self.wrap_width {
170 Some(w) => wrap_text(&text, w, self.size),
171 None => vec![text],
172 };
173 let lh = self.size * self.line_height;
174 let n = lines.len() as f64;
175 let block_h = if n <= 1.0 { self.size } else { lh * n };
183 let py_block = py + (self.vjust - 0.5) * block_h;
184
185 if self.check_overlap {
186 let w = wrapped_block_width(&lines, self.size);
187 let bbox = (
188 px - w / 2.0,
189 py_block - block_h / 2.0,
190 px + w / 2.0,
191 py_block + block_h / 2.0,
192 );
193 if bboxes_overlap(&bbox, &drawn_bboxes) {
194 continue;
195 }
196 drawn_bboxes.push(bbox);
197 }
198
199 let anchor = hjust_to_anchor(self.hjust);
200 let family = super::resolve_font_family(&self.fontfamily, theme);
201 let top = py_block - block_h / 2.0;
202 for (li, line) in lines.iter().enumerate() {
203 let line_y = if n <= 1.0 {
207 py_block
208 } else {
209 top + lh * (li as f64 + 0.5)
210 };
211 backend.draw_text(
212 line,
213 (px, line_y),
214 &TextStyle {
215 color: self.color,
216 size: self.size,
217 anchor: anchor.clone(),
218 angle: 0.0,
219 family: family.clone(),
220 face,
221 bg_colour: self.bg_colour,
222 bg_r: self.bg_r,
223 },
224 )?;
225 }
226 }
227 super::clear_row_extras(backend);
228
229 Ok(())
230 }
231
232 fn required_aes(&self) -> Vec<Aesthetic> {
233 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
234 }
235
236 fn default_stat(&self) -> Box<dyn Stat> {
237 Box::new(StatIdentity)
238 }
239 fn default_position(&self) -> Box<dyn Position> {
240 Box::new(PositionIdentity)
241 }
242 fn default_params(&self) -> GeomParams {
243 GeomParams::default()
244 }
245 fn name(&self) -> &str {
246 "text"
247 }
248 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
249 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
250 self.color = ink;
251 }
252 }
253}
254
255pub struct GeomLabel {
257 pub size: f64,
258 pub color: (u8, u8, u8),
259 pub fill: (u8, u8, u8),
260 pub alpha: f64,
261 pub padding: f64,
262 pub hjust: f64,
264 pub vjust: f64,
266 pub fontfamily: String,
268 pub check_overlap: bool,
270 pub wrap_width: Option<f64>,
276 pub line_height: f64,
280 pub radius: f64,
284 pub fontface: FontFace,
289}
290
291impl GeomLabel {
292 pub fn with_fontface(mut self, face: FontFace) -> Self {
294 self.fontface = face;
295 self
296 }
297 pub fn with_hjust(mut self, hjust: f64) -> Self {
298 self.hjust = hjust;
299 self
300 }
301
302 pub fn with_vjust(mut self, vjust: f64) -> Self {
303 self.vjust = vjust;
304 self
305 }
306
307 pub fn with_fontfamily(mut self, family: &str) -> Self {
308 self.fontfamily = family.to_string();
309 self
310 }
311
312 pub fn with_check_overlap(mut self, check: bool) -> Self {
313 self.check_overlap = check;
314 self
315 }
316
317 pub fn with_wrap(mut self, width_px: f64) -> Self {
320 self.wrap_width = Some(width_px);
321 self
322 }
323
324 pub fn with_line_height(mut self, line_height: f64) -> Self {
326 self.line_height = line_height;
327 self
328 }
329
330 pub fn with_radius(mut self, radius_px: f64) -> Self {
332 self.radius = radius_px;
333 self
334 }
335}
336
337impl Default for GeomLabel {
338 fn default() -> Self {
339 GeomLabel {
340 size: 10.0,
341 color: (0, 0, 0),
342 fill: (255, 255, 255),
343 alpha: 0.8,
344 padding: 3.0,
345 hjust: 0.5,
346 vjust: 0.5,
347 fontfamily: String::new(),
348 check_overlap: false,
349 wrap_width: None,
350 line_height: 1.2,
351 radius: 0.0,
352 fontface: FontFace::Plain,
353 }
354 }
355}
356
357impl Geom for GeomLabel {
358 fn draw(
359 &self,
360 data: &DataFrame,
361 coord: &dyn Coord,
362 scales: &ScaleSet,
363 theme: &Theme,
364 backend: &mut dyn DrawBackend,
365 ) -> Result<(), RenderError> {
366 let x_col = data
367 .column("x")
368 .ok_or(RenderError::MissingAesthetic("x".into()))?;
369 let y_col = data
370 .column("y")
371 .ok_or(RenderError::MissingAesthetic("y".into()))?;
372 let label_col = data
373 .column("label")
374 .ok_or(RenderError::MissingAesthetic("label".into()))?;
375 let fontface_col = data.column("fontface");
376
377 let plot_area = backend.plot_area();
378 let xm = super::support::XMapper::new(data, scales);
379 let y_scale = scales.get(&Aesthetic::Y);
380
381 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
382
383 for i in 0..data.nrows() {
384 super::set_row_extras(backend, data, i);
386 backend.set_tooltip(None);
389 let nx = xm.map(&x_col[i], i);
390 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
391 let (px, py) = coord.transform((nx, ny), &plot_area);
392
393 let face = fontface_col
395 .map(|c| &c[i])
396 .filter(|v| !v.is_na())
397 .and_then(|v| super::parse_fontface(&v.to_group_key()))
398 .unwrap_or(self.fontface);
399
400 let text = label_col[i].to_group_key();
401 let text_len = text.len();
410 let paragraphs: Vec<String> = if text.is_empty() {
411 vec![text]
412 } else {
413 text.lines().map(str::to_string).collect()
414 };
415 let (lines, half_w) = match self.wrap_width {
416 Some(w) => {
417 let lines = paragraphs
418 .iter()
419 .flat_map(|p| wrap_text(p, w, self.size))
420 .collect::<Vec<_>>();
421 let half_w = wrapped_block_width(&lines, self.size) / 2.0 + self.padding;
422 (lines, half_w)
423 }
424 None if paragraphs.len() <= 1 => {
431 let half_w = text_len as f64 * self.size * 0.6 / 2.0 + self.padding;
432 (paragraphs, half_w)
433 }
434 None => {
440 let half_w = wrapped_block_width(¶graphs, self.size) / 2.0 + self.padding;
441 (paragraphs, half_w)
442 }
443 };
444 let lh = self.size * self.line_height;
445 let n = lines.len() as f64;
446 let block_h = if n <= 1.0 { self.size } else { lh * n };
449 let py_block = py + (self.vjust - 0.5) * block_h;
450 let half_h = block_h / 2.0 + self.padding;
451
452 if self.check_overlap {
453 let bbox = (
454 px - half_w,
455 py_block - half_h,
456 px + half_w,
457 py_block + half_h,
458 );
459 if bboxes_overlap(&bbox, &drawn_bboxes) {
460 continue;
461 }
462 drawn_bboxes.push(bbox);
463 }
464
465 backend.draw_rect(
468 (px - half_w, py_block - half_h),
469 (px + half_w, py_block + half_h),
470 &RectStyle {
471 fill: Some(self.fill),
472 stroke: Some(self.color),
473 stroke_width: 0.5,
474 alpha: self.alpha,
475 clip: true,
476 corner_radius: self.radius,
477 },
478 )?;
479
480 let anchor = hjust_to_anchor(self.hjust);
482 let family = super::resolve_font_family(&self.fontfamily, theme);
483 let top = py_block - block_h / 2.0;
484 for (li, line) in lines.iter().enumerate() {
485 let line_y = if n <= 1.0 {
486 py_block
487 } else {
488 top + lh * (li as f64 + 0.5)
489 };
490 backend.draw_text(
491 line,
492 (px, line_y),
493 &TextStyle {
494 color: self.color,
495 size: self.size,
496 anchor: anchor.clone(),
497 angle: 0.0,
498 family: family.clone(),
499 face,
500 bg_colour: None,
501 bg_r: 0.0,
502 },
503 )?;
504 }
505 }
506 super::clear_row_extras(backend);
507
508 Ok(())
509 }
510
511 fn required_aes(&self) -> Vec<Aesthetic> {
512 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
513 }
514
515 fn default_stat(&self) -> Box<dyn Stat> {
516 Box::new(StatIdentity)
517 }
518 fn default_position(&self) -> Box<dyn Position> {
519 Box::new(PositionIdentity)
520 }
521 fn default_params(&self) -> GeomParams {
522 GeomParams::default()
523 }
524 fn name(&self) -> &str {
525 "label"
526 }
527 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
528 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
529 self.color = ink;
530 }
531 if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
532 self.fill = paper;
533 }
534 }
535}
536
537fn hjust_to_anchor(hjust: f64) -> TextAnchor {
539 if hjust < 0.25 {
540 TextAnchor::Start
541 } else if hjust > 0.75 {
542 TextAnchor::End
543 } else {
544 TextAnchor::Middle
545 }
546}
547
548fn bboxes_overlap(candidate: &(f64, f64, f64, f64), existing: &[(f64, f64, f64, f64)]) -> bool {
550 for b in existing {
551 if candidate.0 < b.2 && candidate.2 > b.0 && candidate.1 < b.3 && candidate.3 > b.1 {
553 return true;
554 }
555 }
556 false
557}