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 let nx = xm.map(&x_col[i], i);
116 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
117 let (px, py) = coord.transform((nx, ny), &plot_area);
118
119 let text = label_col[i].to_group_key();
120 let lines = match self.wrap_width {
121 Some(w) => wrap_text(&text, w, self.size),
122 None => vec![text],
123 };
124 let lh = self.size * self.line_height;
125 let n = lines.len() as f64;
126 let block_h = if n <= 1.0 { self.size } else { lh * n };
134 let py_block = py + (self.vjust - 0.5) * block_h;
135
136 if self.check_overlap {
137 let w = wrapped_block_width(&lines, self.size);
138 let bbox = (
139 px - w / 2.0,
140 py_block - block_h / 2.0,
141 px + w / 2.0,
142 py_block + block_h / 2.0,
143 );
144 if bboxes_overlap(&bbox, &drawn_bboxes) {
145 continue;
146 }
147 drawn_bboxes.push(bbox);
148 }
149
150 let anchor = hjust_to_anchor(self.hjust);
151 let family = super::resolve_font_family(&self.fontfamily, theme);
152 let top = py_block - block_h / 2.0;
153 for (li, line) in lines.iter().enumerate() {
154 let line_y = if n <= 1.0 {
158 py_block
159 } else {
160 top + lh * (li as f64 + 0.5)
161 };
162 backend.draw_text(
163 line,
164 (px, line_y),
165 &TextStyle {
166 color: self.color,
167 size: self.size,
168 anchor: anchor.clone(),
169 angle: 0.0,
170 family: family.clone(),
171 face: crate::render::backend::FontFace::Plain,
172 },
173 )?;
174 }
175 }
176
177 Ok(())
178 }
179
180 fn required_aes(&self) -> Vec<Aesthetic> {
181 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
182 }
183
184 fn default_stat(&self) -> Box<dyn Stat> {
185 Box::new(StatIdentity)
186 }
187 fn default_position(&self) -> Box<dyn Position> {
188 Box::new(PositionIdentity)
189 }
190 fn default_params(&self) -> GeomParams {
191 GeomParams::default()
192 }
193 fn name(&self) -> &str {
194 "text"
195 }
196 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
197 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
198 self.color = ink;
199 }
200 }
201}
202
203pub struct GeomLabel {
205 pub size: f64,
206 pub color: (u8, u8, u8),
207 pub fill: (u8, u8, u8),
208 pub alpha: f64,
209 pub padding: f64,
210 pub hjust: f64,
212 pub vjust: f64,
214 pub fontfamily: String,
216 pub check_overlap: bool,
218 pub wrap_width: Option<f64>,
224 pub line_height: f64,
228 pub radius: f64,
232}
233
234impl GeomLabel {
235 pub fn with_hjust(mut self, hjust: f64) -> Self {
236 self.hjust = hjust;
237 self
238 }
239
240 pub fn with_vjust(mut self, vjust: f64) -> Self {
241 self.vjust = vjust;
242 self
243 }
244
245 pub fn with_fontfamily(mut self, family: &str) -> Self {
246 self.fontfamily = family.to_string();
247 self
248 }
249
250 pub fn with_check_overlap(mut self, check: bool) -> Self {
251 self.check_overlap = check;
252 self
253 }
254
255 pub fn with_wrap(mut self, width_px: f64) -> Self {
258 self.wrap_width = Some(width_px);
259 self
260 }
261
262 pub fn with_line_height(mut self, line_height: f64) -> Self {
264 self.line_height = line_height;
265 self
266 }
267
268 pub fn with_radius(mut self, radius_px: f64) -> Self {
270 self.radius = radius_px;
271 self
272 }
273}
274
275impl Default for GeomLabel {
276 fn default() -> Self {
277 GeomLabel {
278 size: 10.0,
279 color: (0, 0, 0),
280 fill: (255, 255, 255),
281 alpha: 0.8,
282 padding: 3.0,
283 hjust: 0.5,
284 vjust: 0.5,
285 fontfamily: String::new(),
286 check_overlap: false,
287 wrap_width: None,
288 line_height: 1.2,
289 radius: 0.0,
290 }
291 }
292}
293
294impl Geom for GeomLabel {
295 fn draw(
296 &self,
297 data: &DataFrame,
298 coord: &dyn Coord,
299 scales: &ScaleSet,
300 theme: &Theme,
301 backend: &mut dyn DrawBackend,
302 ) -> Result<(), RenderError> {
303 let x_col = data
304 .column("x")
305 .ok_or(RenderError::MissingAesthetic("x".into()))?;
306 let y_col = data
307 .column("y")
308 .ok_or(RenderError::MissingAesthetic("y".into()))?;
309 let label_col = data
310 .column("label")
311 .ok_or(RenderError::MissingAesthetic("label".into()))?;
312
313 let plot_area = backend.plot_area();
314 let xm = super::support::XMapper::new(data, scales);
315 let y_scale = scales.get(&Aesthetic::Y);
316
317 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
318
319 for i in 0..data.nrows() {
320 let nx = xm.map(&x_col[i], i);
321 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
322 let (px, py) = coord.transform((nx, ny), &plot_area);
323
324 let text = label_col[i].to_group_key();
325 let (lines, half_w) = match self.wrap_width {
332 Some(w) => {
333 let lines = wrap_text(&text, w, self.size);
334 let half_w = wrapped_block_width(&lines, self.size) / 2.0 + self.padding;
335 (lines, half_w)
336 }
337 None => {
338 let half_w = text.len() as f64 * self.size * 0.6 / 2.0 + self.padding;
339 (vec![text], half_w)
340 }
341 };
342 let lh = self.size * self.line_height;
343 let n = lines.len() as f64;
344 let block_h = if n <= 1.0 { self.size } else { lh * n };
347 let py_block = py + (self.vjust - 0.5) * block_h;
348 let half_h = block_h / 2.0 + self.padding;
349
350 if self.check_overlap {
351 let bbox = (
352 px - half_w,
353 py_block - half_h,
354 px + half_w,
355 py_block + half_h,
356 );
357 if bboxes_overlap(&bbox, &drawn_bboxes) {
358 continue;
359 }
360 drawn_bboxes.push(bbox);
361 }
362
363 backend.draw_rect(
366 (px - half_w, py_block - half_h),
367 (px + half_w, py_block + half_h),
368 &RectStyle {
369 fill: Some(self.fill),
370 stroke: Some(self.color),
371 stroke_width: 0.5,
372 alpha: self.alpha,
373 clip: true,
374 corner_radius: self.radius,
375 },
376 )?;
377
378 let anchor = hjust_to_anchor(self.hjust);
380 let family = super::resolve_font_family(&self.fontfamily, theme);
381 let top = py_block - block_h / 2.0;
382 for (li, line) in lines.iter().enumerate() {
383 let line_y = if n <= 1.0 {
384 py_block
385 } else {
386 top + lh * (li as f64 + 0.5)
387 };
388 backend.draw_text(
389 line,
390 (px, line_y),
391 &TextStyle {
392 color: self.color,
393 size: self.size,
394 anchor: anchor.clone(),
395 angle: 0.0,
396 family: family.clone(),
397 face: crate::render::backend::FontFace::Plain,
398 },
399 )?;
400 }
401 }
402
403 Ok(())
404 }
405
406 fn required_aes(&self) -> Vec<Aesthetic> {
407 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
408 }
409
410 fn default_stat(&self) -> Box<dyn Stat> {
411 Box::new(StatIdentity)
412 }
413 fn default_position(&self) -> Box<dyn Position> {
414 Box::new(PositionIdentity)
415 }
416 fn default_params(&self) -> GeomParams {
417 GeomParams::default()
418 }
419 fn name(&self) -> &str {
420 "label"
421 }
422 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
423 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
424 self.color = ink;
425 }
426 if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
427 self.fill = paper;
428 }
429 }
430}
431
432fn hjust_to_anchor(hjust: f64) -> TextAnchor {
434 if hjust < 0.25 {
435 TextAnchor::Start
436 } else if hjust > 0.75 {
437 TextAnchor::End
438 } else {
439 TextAnchor::Middle
440 }
441}
442
443fn bboxes_overlap(candidate: &(f64, f64, f64, f64), existing: &[(f64, f64, f64, f64)]) -> bool {
445 for b in existing {
446 if candidate.0 < b.2 && candidate.2 > b.0 && candidate.1 < b.3 && candidate.3 > b.1 {
448 return true;
449 }
450 }
451 false
452}