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 paragraphs: Vec<String> = if text.is_empty() {
178 vec![text]
179 } else {
180 text.lines().map(str::to_string).collect()
181 };
182 let lines = match self.wrap_width {
183 Some(w) => paragraphs
184 .iter()
185 .flat_map(|p| wrap_text(p, w, self.size))
186 .collect::<Vec<_>>(),
187 None => paragraphs,
188 };
189 let lh = self.size * self.line_height;
190 let n = lines.len() as f64;
191 let block_h = if n <= 1.0 { self.size } else { lh * n };
199 let py_block = py + (self.vjust - 0.5) * block_h;
200
201 if self.check_overlap {
202 let w = wrapped_block_width(&lines, self.size);
203 let bbox = (
204 px - w / 2.0,
205 py_block - block_h / 2.0,
206 px + w / 2.0,
207 py_block + block_h / 2.0,
208 );
209 if bboxes_overlap(&bbox, &drawn_bboxes) {
210 continue;
211 }
212 drawn_bboxes.push(bbox);
213 }
214
215 let anchor = hjust_to_anchor(self.hjust);
216 let family = super::resolve_font_family(&self.fontfamily, theme);
217 let top = py_block - block_h / 2.0;
218 for (li, line) in lines.iter().enumerate() {
219 let line_y = if n <= 1.0 {
223 py_block
224 } else {
225 top + lh * (li as f64 + 0.5)
226 };
227 backend.draw_text(
228 line,
229 (px, line_y),
230 &TextStyle {
231 color: self.color,
232 size: self.size,
233 anchor: anchor.clone(),
234 angle: 0.0,
235 family: family.clone(),
236 face,
237 bg_colour: self.bg_colour,
238 bg_r: self.bg_r,
239 },
240 )?;
241 }
242 }
243 super::clear_row_extras(backend);
244
245 Ok(())
246 }
247
248 fn required_aes(&self) -> Vec<Aesthetic> {
249 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
250 }
251
252 fn default_stat(&self) -> Box<dyn Stat> {
253 Box::new(StatIdentity)
254 }
255 fn default_position(&self) -> Box<dyn Position> {
256 Box::new(PositionIdentity)
257 }
258 fn default_params(&self) -> GeomParams {
259 GeomParams::default()
260 }
261 fn name(&self) -> &str {
262 "text"
263 }
264 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
265 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
266 self.color = ink;
267 }
268 }
269}
270
271pub struct GeomLabel {
273 pub size: f64,
274 pub color: (u8, u8, u8),
275 pub fill: (u8, u8, u8),
276 pub alpha: f64,
277 pub padding: f64,
278 pub hjust: f64,
280 pub vjust: f64,
282 pub fontfamily: String,
284 pub check_overlap: bool,
286 pub wrap_width: Option<f64>,
292 pub line_height: f64,
296 pub radius: f64,
300 pub fontface: FontFace,
305}
306
307impl GeomLabel {
308 pub fn with_fontface(mut self, face: FontFace) -> Self {
310 self.fontface = face;
311 self
312 }
313 pub fn with_hjust(mut self, hjust: f64) -> Self {
314 self.hjust = hjust;
315 self
316 }
317
318 pub fn with_vjust(mut self, vjust: f64) -> Self {
319 self.vjust = vjust;
320 self
321 }
322
323 pub fn with_fontfamily(mut self, family: &str) -> Self {
324 self.fontfamily = family.to_string();
325 self
326 }
327
328 pub fn with_check_overlap(mut self, check: bool) -> Self {
329 self.check_overlap = check;
330 self
331 }
332
333 pub fn with_wrap(mut self, width_px: f64) -> Self {
336 self.wrap_width = Some(width_px);
337 self
338 }
339
340 pub fn with_line_height(mut self, line_height: f64) -> Self {
342 self.line_height = line_height;
343 self
344 }
345
346 pub fn with_radius(mut self, radius_px: f64) -> Self {
348 self.radius = radius_px;
349 self
350 }
351}
352
353impl Default for GeomLabel {
354 fn default() -> Self {
355 GeomLabel {
356 size: 10.0,
357 color: (0, 0, 0),
358 fill: (255, 255, 255),
359 alpha: 0.8,
360 padding: 3.0,
361 hjust: 0.5,
362 vjust: 0.5,
363 fontfamily: String::new(),
364 check_overlap: false,
365 wrap_width: None,
366 line_height: 1.2,
367 radius: 0.0,
368 fontface: FontFace::Plain,
369 }
370 }
371}
372
373impl Geom for GeomLabel {
374 fn draw(
375 &self,
376 data: &DataFrame,
377 coord: &dyn Coord,
378 scales: &ScaleSet,
379 theme: &Theme,
380 backend: &mut dyn DrawBackend,
381 ) -> Result<(), RenderError> {
382 let x_col = data
383 .column("x")
384 .ok_or(RenderError::MissingAesthetic("x".into()))?;
385 let y_col = data
386 .column("y")
387 .ok_or(RenderError::MissingAesthetic("y".into()))?;
388 let label_col = data
389 .column("label")
390 .ok_or(RenderError::MissingAesthetic("label".into()))?;
391 let fontface_col = data.column("fontface");
392
393 let plot_area = backend.plot_area();
394 let xm = super::support::XMapper::new(data, scales);
395 let y_scale = scales.get(&Aesthetic::Y);
396
397 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
398
399 for i in 0..data.nrows() {
400 super::set_row_extras(backend, data, i);
402 backend.set_tooltip(None);
405 let nx = xm.map(&x_col[i], i);
406 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
407 let (px, py) = coord.transform((nx, ny), &plot_area);
408
409 let face = fontface_col
411 .map(|c| &c[i])
412 .filter(|v| !v.is_na())
413 .and_then(|v| super::parse_fontface(&v.to_group_key()))
414 .unwrap_or(self.fontface);
415
416 let text = label_col[i].to_group_key();
417 let text_len = text.len();
426 let paragraphs: Vec<String> = if text.is_empty() {
427 vec![text]
428 } else {
429 text.lines().map(str::to_string).collect()
430 };
431 let (lines, half_w) = match self.wrap_width {
432 Some(w) => {
433 let lines = paragraphs
434 .iter()
435 .flat_map(|p| wrap_text(p, w, self.size))
436 .collect::<Vec<_>>();
437 let half_w = wrapped_block_width(&lines, self.size) / 2.0 + self.padding;
438 (lines, half_w)
439 }
440 None if paragraphs.len() <= 1 => {
447 let half_w = text_len as f64 * self.size * 0.6 / 2.0 + self.padding;
448 (paragraphs, half_w)
449 }
450 None => {
456 let half_w = wrapped_block_width(¶graphs, self.size) / 2.0 + self.padding;
457 (paragraphs, half_w)
458 }
459 };
460 let lh = self.size * self.line_height;
461 let n = lines.len() as f64;
462 let block_h = if n <= 1.0 { self.size } else { lh * n };
465 let py_block = py + (self.vjust - 0.5) * block_h;
466 let half_h = block_h / 2.0 + self.padding;
467
468 if self.check_overlap {
469 let bbox = (
470 px - half_w,
471 py_block - half_h,
472 px + half_w,
473 py_block + half_h,
474 );
475 if bboxes_overlap(&bbox, &drawn_bboxes) {
476 continue;
477 }
478 drawn_bboxes.push(bbox);
479 }
480
481 backend.draw_rect(
484 (px - half_w, py_block - half_h),
485 (px + half_w, py_block + half_h),
486 &RectStyle {
487 fill: Some(self.fill),
488 stroke: Some(self.color),
489 stroke_width: 0.5,
490 alpha: self.alpha,
491 clip: true,
492 corner_radius: self.radius,
493 },
494 )?;
495
496 let anchor = hjust_to_anchor(self.hjust);
498 let family = super::resolve_font_family(&self.fontfamily, theme);
499 let top = py_block - block_h / 2.0;
500 for (li, line) in lines.iter().enumerate() {
501 let line_y = if n <= 1.0 {
502 py_block
503 } else {
504 top + lh * (li as f64 + 0.5)
505 };
506 backend.draw_text(
507 line,
508 (px, line_y),
509 &TextStyle {
510 color: self.color,
511 size: self.size,
512 anchor: anchor.clone(),
513 angle: 0.0,
514 family: family.clone(),
515 face,
516 bg_colour: None,
517 bg_r: 0.0,
518 },
519 )?;
520 }
521 }
522 super::clear_row_extras(backend);
523
524 Ok(())
525 }
526
527 fn required_aes(&self) -> Vec<Aesthetic> {
528 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
529 }
530
531 fn default_stat(&self) -> Box<dyn Stat> {
532 Box::new(StatIdentity)
533 }
534 fn default_position(&self) -> Box<dyn Position> {
535 Box::new(PositionIdentity)
536 }
537 fn default_params(&self) -> GeomParams {
538 GeomParams::default()
539 }
540 fn name(&self) -> &str {
541 "label"
542 }
543 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
544 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
545 self.color = ink;
546 }
547 if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
548 self.fill = paper;
549 }
550 }
551}
552
553fn hjust_to_anchor(hjust: f64) -> TextAnchor {
555 if hjust < 0.25 {
556 TextAnchor::Start
557 } else if hjust > 0.75 {
558 TextAnchor::End
559 } else {
560 TextAnchor::Middle
561 }
562}
563
564fn bboxes_overlap(candidate: &(f64, f64, f64, f64), existing: &[(f64, f64, f64, f64)]) -> bool {
566 for b in existing {
567 if candidate.0 < b.2 && candidate.2 > b.0 && candidate.1 < b.3 && candidate.3 > b.1 {
569 return true;
570 }
571 }
572 false
573}