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 let num = |name: &str, i: usize, dflt: f64| {
147 data.column(name)
148 .and_then(|c| c[i].as_f64())
149 .filter(|v| v.is_finite())
150 .unwrap_or(dflt)
151 };
152
153 let plot_area = backend.plot_area();
154 let xm = super::support::XMapper::new(data, scales);
155 let y_scale = scales.get(&Aesthetic::Y);
156
157 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
158
159 for i in 0..data.nrows() {
160 let (hjust, vjust) = (num("hjust", i, self.hjust), num("vjust", i, self.vjust));
161 super::set_row_extras(backend, data, i);
163 backend.set_tooltip(None);
166 let nx = xm.map(&x_col[i], i);
167 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
168 let (px, py) = coord.transform((nx, ny), &plot_area);
169
170 let face = fontface_col
172 .map(|c| &c[i])
173 .filter(|v| !v.is_na())
174 .and_then(|v| super::parse_fontface(&v.to_group_key()))
175 .unwrap_or(self.fontface);
176
177 let text = label_col[i].to_group_key();
178 let paragraphs: Vec<String> = if text.is_empty() {
187 vec![text]
188 } else {
189 text.lines().map(str::to_string).collect()
190 };
191 let lines = match self.wrap_width {
192 Some(w) => paragraphs
193 .iter()
194 .flat_map(|p| wrap_text(p, w, self.size))
195 .collect::<Vec<_>>(),
196 None => paragraphs,
197 };
198 let lh = self.size * self.line_height;
199 let n = lines.len() as f64;
200 let block_h = if n <= 1.0 { self.size } else { lh * n };
208 let py_block = py + (vjust - 0.5) * block_h;
209 let w_text = wrapped_block_width(&lines, self.size);
213 let px = px
214 + if hjust < 0.0 {
215 -hjust * w_text
216 } else if hjust > 1.0 {
217 -(hjust - 1.0) * w_text
218 } else {
219 0.0
220 };
221
222 if self.check_overlap {
223 let w = wrapped_block_width(&lines, self.size);
224 let bbox = (
225 px - w / 2.0,
226 py_block - block_h / 2.0,
227 px + w / 2.0,
228 py_block + block_h / 2.0,
229 );
230 if bboxes_overlap(&bbox, &drawn_bboxes) {
231 continue;
232 }
233 drawn_bboxes.push(bbox);
234 }
235
236 let anchor = hjust_to_anchor(hjust);
237 let family = super::resolve_font_family(&self.fontfamily, theme);
238 let top = py_block - block_h / 2.0;
239 for (li, line) in lines.iter().enumerate() {
240 let line_y = if n <= 1.0 {
244 py_block
245 } else {
246 top + lh * (li as f64 + 0.5)
247 };
248 backend.draw_text(
249 line,
250 (px, line_y),
251 &TextStyle {
252 color: self.color,
253 size: self.size,
254 anchor: anchor.clone(),
255 angle: 0.0,
256 family: family.clone(),
257 face,
258 bg_colour: self.bg_colour,
259 bg_r: self.bg_r,
260 },
261 )?;
262 }
263 }
264 super::clear_row_extras(backend);
265
266 Ok(())
267 }
268
269 fn required_aes(&self) -> Vec<Aesthetic> {
270 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
271 }
272
273 fn default_stat(&self) -> Box<dyn Stat> {
274 Box::new(StatIdentity)
275 }
276 fn default_position(&self) -> Box<dyn Position> {
277 Box::new(PositionIdentity)
278 }
279 fn default_params(&self) -> GeomParams {
280 GeomParams::default()
281 }
282 fn name(&self) -> &str {
283 "text"
284 }
285 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
286 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
287 self.color = ink;
288 }
289 }
290}
291
292pub struct GeomLabel {
294 pub size: f64,
295 pub color: (u8, u8, u8),
296 pub fill: (u8, u8, u8),
297 pub alpha: f64,
298 pub padding: f64,
299 pub hjust: f64,
301 pub vjust: f64,
303 pub fontfamily: String,
305 pub check_overlap: bool,
307 pub wrap_width: Option<f64>,
313 pub line_height: f64,
317 pub radius: f64,
321 pub fontface: FontFace,
326}
327
328impl GeomLabel {
329 pub fn with_fontface(mut self, face: FontFace) -> Self {
331 self.fontface = face;
332 self
333 }
334 pub fn with_hjust(mut self, hjust: f64) -> Self {
335 self.hjust = hjust;
336 self
337 }
338
339 pub fn with_vjust(mut self, vjust: f64) -> Self {
340 self.vjust = vjust;
341 self
342 }
343
344 pub fn with_fontfamily(mut self, family: &str) -> Self {
345 self.fontfamily = family.to_string();
346 self
347 }
348
349 pub fn with_check_overlap(mut self, check: bool) -> Self {
350 self.check_overlap = check;
351 self
352 }
353
354 pub fn with_wrap(mut self, width_px: f64) -> Self {
357 self.wrap_width = Some(width_px);
358 self
359 }
360
361 pub fn with_line_height(mut self, line_height: f64) -> Self {
363 self.line_height = line_height;
364 self
365 }
366
367 pub fn with_radius(mut self, radius_px: f64) -> Self {
369 self.radius = radius_px;
370 self
371 }
372}
373
374impl Default for GeomLabel {
375 fn default() -> Self {
376 GeomLabel {
377 size: 10.0,
378 color: (0, 0, 0),
379 fill: (255, 255, 255),
380 alpha: 0.8,
381 padding: 3.0,
382 hjust: 0.5,
383 vjust: 0.5,
384 fontfamily: String::new(),
385 check_overlap: false,
386 wrap_width: None,
387 line_height: 1.2,
388 radius: 0.0,
389 fontface: FontFace::Plain,
390 }
391 }
392}
393
394impl Geom for GeomLabel {
395 fn draw(
396 &self,
397 data: &DataFrame,
398 coord: &dyn Coord,
399 scales: &ScaleSet,
400 theme: &Theme,
401 backend: &mut dyn DrawBackend,
402 ) -> Result<(), RenderError> {
403 let x_col = data
404 .column("x")
405 .ok_or(RenderError::MissingAesthetic("x".into()))?;
406 let y_col = data
407 .column("y")
408 .ok_or(RenderError::MissingAesthetic("y".into()))?;
409 let label_col = data
410 .column("label")
411 .ok_or(RenderError::MissingAesthetic("label".into()))?;
412 let fontface_col = data.column("fontface");
413
414 let plot_area = backend.plot_area();
415 let xm = super::support::XMapper::new(data, scales);
416 let y_scale = scales.get(&Aesthetic::Y);
417
418 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
419
420 for i in 0..data.nrows() {
421 super::set_row_extras(backend, data, i);
423 backend.set_tooltip(None);
426 let nx = xm.map(&x_col[i], i);
427 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
428 let (px, py) = coord.transform((nx, ny), &plot_area);
429
430 let face = fontface_col
432 .map(|c| &c[i])
433 .filter(|v| !v.is_na())
434 .and_then(|v| super::parse_fontface(&v.to_group_key()))
435 .unwrap_or(self.fontface);
436
437 let text = label_col[i].to_group_key();
438 let text_len = text.len();
447 let paragraphs: Vec<String> = if text.is_empty() {
448 vec![text]
449 } else {
450 text.lines().map(str::to_string).collect()
451 };
452 let (lines, half_w) = match self.wrap_width {
453 Some(w) => {
454 let lines = paragraphs
455 .iter()
456 .flat_map(|p| wrap_text(p, w, self.size))
457 .collect::<Vec<_>>();
458 let half_w = wrapped_block_width(&lines, self.size) / 2.0 + self.padding;
459 (lines, half_w)
460 }
461 None if paragraphs.len() <= 1 => {
468 let half_w = text_len as f64 * self.size * 0.6 / 2.0 + self.padding;
469 (paragraphs, half_w)
470 }
471 None => {
477 let half_w = wrapped_block_width(¶graphs, self.size) / 2.0 + self.padding;
478 (paragraphs, half_w)
479 }
480 };
481 let lh = self.size * self.line_height;
482 let n = lines.len() as f64;
483 let block_h = if n <= 1.0 { self.size } else { lh * n };
486 let py_block = py + (self.vjust - 0.5) * block_h;
487 let half_h = block_h / 2.0 + self.padding;
488
489 if self.check_overlap {
490 let bbox = (
491 px - half_w,
492 py_block - half_h,
493 px + half_w,
494 py_block + half_h,
495 );
496 if bboxes_overlap(&bbox, &drawn_bboxes) {
497 continue;
498 }
499 drawn_bboxes.push(bbox);
500 }
501
502 backend.draw_rect(
505 (px - half_w, py_block - half_h),
506 (px + half_w, py_block + half_h),
507 &RectStyle {
508 fill: Some(self.fill),
509 stroke: Some(self.color),
510 stroke_width: 0.5,
511 alpha: self.alpha,
512 clip: true,
513 corner_radius: self.radius,
514 },
515 )?;
516
517 let anchor = hjust_to_anchor(self.hjust);
519 let family = super::resolve_font_family(&self.fontfamily, theme);
520 let top = py_block - block_h / 2.0;
521 for (li, line) in lines.iter().enumerate() {
522 let line_y = if n <= 1.0 {
523 py_block
524 } else {
525 top + lh * (li as f64 + 0.5)
526 };
527 backend.draw_text(
528 line,
529 (px, line_y),
530 &TextStyle {
531 color: self.color,
532 size: self.size,
533 anchor: anchor.clone(),
534 angle: 0.0,
535 family: family.clone(),
536 face,
537 bg_colour: None,
538 bg_r: 0.0,
539 },
540 )?;
541 }
542 }
543 super::clear_row_extras(backend);
544
545 Ok(())
546 }
547
548 fn required_aes(&self) -> Vec<Aesthetic> {
549 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
550 }
551
552 fn default_stat(&self) -> Box<dyn Stat> {
553 Box::new(StatIdentity)
554 }
555 fn default_position(&self) -> Box<dyn Position> {
556 Box::new(PositionIdentity)
557 }
558 fn default_params(&self) -> GeomParams {
559 GeomParams::default()
560 }
561 fn name(&self) -> &str {
562 "label"
563 }
564 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
565 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
566 self.color = ink;
567 }
568 if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
569 self.fill = paper;
570 }
571 }
572}
573
574fn hjust_to_anchor(hjust: f64) -> TextAnchor {
576 if hjust < 0.25 {
577 TextAnchor::Start
578 } else if hjust > 0.75 {
579 TextAnchor::End
580 } else {
581 TextAnchor::Middle
582 }
583}
584
585fn bboxes_overlap(candidate: &(f64, f64, f64, f64), existing: &[(f64, f64, f64, f64)]) -> bool {
587 for b in existing {
588 if candidate.0 < b.2 && candidate.2 > b.0 && candidate.1 < b.3 && candidate.3 > b.1 {
590 return true;
591 }
592 }
593 false
594}