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::RenderError;
8use crate::scale::ScaleSet;
9use crate::stat::identity::StatIdentity;
10use crate::stat::Stat;
11use crate::theme::Theme;
12
13use super::{Geom, GeomParams};
14
15pub struct GeomText {
17 pub size: f64,
18 pub color: (u8, u8, u8),
19 pub alpha: f64,
20 pub hjust: f64,
22 pub vjust: f64,
24 pub fontfamily: String,
26 pub check_overlap: bool,
28}
29
30impl GeomText {
31 pub fn with_hjust(mut self, hjust: f64) -> Self {
32 self.hjust = hjust;
33 self
34 }
35
36 pub fn with_vjust(mut self, vjust: f64) -> Self {
37 self.vjust = vjust;
38 self
39 }
40
41 pub fn with_fontfamily(mut self, family: &str) -> Self {
42 self.fontfamily = family.to_string();
43 self
44 }
45
46 pub fn with_check_overlap(mut self, check: bool) -> Self {
47 self.check_overlap = check;
48 self
49 }
50}
51
52impl Default for GeomText {
53 fn default() -> Self {
54 GeomText {
55 size: 10.0,
56 color: (0, 0, 0),
57 alpha: 1.0,
58 hjust: 0.5,
59 vjust: 0.5,
60 fontfamily: String::new(),
61 check_overlap: false,
62 }
63 }
64}
65
66impl Geom for GeomText {
67 fn draw(
68 &self,
69 data: &DataFrame,
70 coord: &dyn Coord,
71 scales: &ScaleSet,
72 theme: &Theme,
73 backend: &mut dyn DrawBackend,
74 ) -> Result<(), RenderError> {
75 let x_col = data
76 .column("x")
77 .ok_or(RenderError::MissingAesthetic("x".into()))?;
78 let y_col = data
79 .column("y")
80 .ok_or(RenderError::MissingAesthetic("y".into()))?;
81 let label_col = data
82 .column("label")
83 .ok_or(RenderError::MissingAesthetic("label".into()))?;
84
85 let plot_area = backend.plot_area();
86 let xm = super::support::XMapper::new(data, scales);
87 let y_scale = scales.get(&Aesthetic::Y);
88
89 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
90
91 for i in 0..data.nrows() {
92 let nx = xm.map(&x_col[i], i);
93 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
94 let (px, py) = coord.transform((nx, ny), &plot_area);
95 let py = py + (self.vjust - 0.5) * self.size;
99
100 let text = label_col[i].to_group_key();
101
102 if self.check_overlap {
103 let w = text.len() as f64 * self.size * 0.6;
104 let h = self.size;
105 let bbox = (px - w / 2.0, py - h / 2.0, px + w / 2.0, py + h / 2.0);
106 if bboxes_overlap(&bbox, &drawn_bboxes) {
107 continue;
108 }
109 drawn_bboxes.push(bbox);
110 }
111
112 let anchor = hjust_to_anchor(self.hjust);
113 backend.draw_text(
114 &text,
115 (px, py),
116 &TextStyle {
117 color: self.color,
118 size: self.size,
119 anchor,
120 angle: 0.0,
121 family: super::resolve_font_family(&self.fontfamily, theme),
122 face: crate::render::backend::FontFace::Plain,
123 },
124 )?;
125 }
126
127 Ok(())
128 }
129
130 fn required_aes(&self) -> Vec<Aesthetic> {
131 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
132 }
133
134 fn default_stat(&self) -> Box<dyn Stat> {
135 Box::new(StatIdentity)
136 }
137 fn default_position(&self) -> Box<dyn Position> {
138 Box::new(PositionIdentity)
139 }
140 fn default_params(&self) -> GeomParams {
141 GeomParams::default()
142 }
143 fn name(&self) -> &str {
144 "text"
145 }
146 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
147 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
148 self.color = ink;
149 }
150 }
151}
152
153pub struct GeomLabel {
155 pub size: f64,
156 pub color: (u8, u8, u8),
157 pub fill: (u8, u8, u8),
158 pub alpha: f64,
159 pub padding: f64,
160 pub hjust: f64,
162 pub vjust: f64,
164 pub fontfamily: String,
166 pub check_overlap: bool,
168}
169
170impl GeomLabel {
171 pub fn with_hjust(mut self, hjust: f64) -> Self {
172 self.hjust = hjust;
173 self
174 }
175
176 pub fn with_vjust(mut self, vjust: f64) -> Self {
177 self.vjust = vjust;
178 self
179 }
180
181 pub fn with_fontfamily(mut self, family: &str) -> Self {
182 self.fontfamily = family.to_string();
183 self
184 }
185
186 pub fn with_check_overlap(mut self, check: bool) -> Self {
187 self.check_overlap = check;
188 self
189 }
190}
191
192impl Default for GeomLabel {
193 fn default() -> Self {
194 GeomLabel {
195 size: 10.0,
196 color: (0, 0, 0),
197 fill: (255, 255, 255),
198 alpha: 0.8,
199 padding: 3.0,
200 hjust: 0.5,
201 vjust: 0.5,
202 fontfamily: String::new(),
203 check_overlap: false,
204 }
205 }
206}
207
208impl Geom for GeomLabel {
209 fn draw(
210 &self,
211 data: &DataFrame,
212 coord: &dyn Coord,
213 scales: &ScaleSet,
214 theme: &Theme,
215 backend: &mut dyn DrawBackend,
216 ) -> Result<(), RenderError> {
217 let x_col = data
218 .column("x")
219 .ok_or(RenderError::MissingAesthetic("x".into()))?;
220 let y_col = data
221 .column("y")
222 .ok_or(RenderError::MissingAesthetic("y".into()))?;
223 let label_col = data
224 .column("label")
225 .ok_or(RenderError::MissingAesthetic("label".into()))?;
226
227 let plot_area = backend.plot_area();
228 let xm = super::support::XMapper::new(data, scales);
229 let y_scale = scales.get(&Aesthetic::Y);
230
231 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
232
233 for i in 0..data.nrows() {
234 let nx = xm.map(&x_col[i], i);
235 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
236 let (px, py) = coord.transform((nx, ny), &plot_area);
237 let py = py + (self.vjust - 0.5) * self.size;
241
242 let text = label_col[i].to_group_key();
243 let approx_width = text.len() as f64 * self.size * 0.6;
244 let half_w = approx_width / 2.0 + self.padding;
245 let half_h = self.size / 2.0 + self.padding;
246
247 if self.check_overlap {
248 let bbox = (px - half_w, py - half_h, px + half_w, py + half_h);
249 if bboxes_overlap(&bbox, &drawn_bboxes) {
250 continue;
251 }
252 drawn_bboxes.push(bbox);
253 }
254
255 backend.draw_rect(
257 (px - half_w, py - half_h),
258 (px + half_w, py + half_h),
259 &RectStyle {
260 fill: Some(self.fill),
261 stroke: Some(self.color),
262 stroke_width: 0.5,
263 alpha: self.alpha,
264 clip: true,
265 },
266 )?;
267
268 let anchor = hjust_to_anchor(self.hjust);
270 backend.draw_text(
271 &text,
272 (px, py),
273 &TextStyle {
274 color: self.color,
275 size: self.size,
276 anchor,
277 angle: 0.0,
278 family: super::resolve_font_family(&self.fontfamily, theme),
279 face: crate::render::backend::FontFace::Plain,
280 },
281 )?;
282 }
283
284 Ok(())
285 }
286
287 fn required_aes(&self) -> Vec<Aesthetic> {
288 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
289 }
290
291 fn default_stat(&self) -> Box<dyn Stat> {
292 Box::new(StatIdentity)
293 }
294 fn default_position(&self) -> Box<dyn Position> {
295 Box::new(PositionIdentity)
296 }
297 fn default_params(&self) -> GeomParams {
298 GeomParams::default()
299 }
300 fn name(&self) -> &str {
301 "label"
302 }
303 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
304 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
305 self.color = ink;
306 }
307 if let (true, Some(paper)) = (self.fill == (255, 255, 255), el.paper) {
308 self.fill = paper;
309 }
310 }
311}
312
313fn hjust_to_anchor(hjust: f64) -> TextAnchor {
315 if hjust < 0.25 {
316 TextAnchor::Start
317 } else if hjust > 0.75 {
318 TextAnchor::End
319 } else {
320 TextAnchor::Middle
321 }
322}
323
324fn bboxes_overlap(candidate: &(f64, f64, f64, f64), existing: &[(f64, f64, f64, f64)]) -> bool {
326 for b in existing {
327 if candidate.0 < b.2 && candidate.2 > b.0 && candidate.1 < b.3 && candidate.3 > b.1 {
329 return true;
330 }
331 }
332 false
333}