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 x_scale = scales.get(&Aesthetic::X);
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 = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
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: None,
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}
147
148pub struct GeomLabel {
150 pub size: f64,
151 pub color: (u8, u8, u8),
152 pub fill: (u8, u8, u8),
153 pub alpha: f64,
154 pub padding: f64,
155 pub hjust: f64,
157 pub vjust: f64,
159 pub fontfamily: String,
161 pub check_overlap: bool,
163}
164
165impl GeomLabel {
166 pub fn with_hjust(mut self, hjust: f64) -> Self {
167 self.hjust = hjust;
168 self
169 }
170
171 pub fn with_vjust(mut self, vjust: f64) -> Self {
172 self.vjust = vjust;
173 self
174 }
175
176 pub fn with_fontfamily(mut self, family: &str) -> Self {
177 self.fontfamily = family.to_string();
178 self
179 }
180
181 pub fn with_check_overlap(mut self, check: bool) -> Self {
182 self.check_overlap = check;
183 self
184 }
185}
186
187impl Default for GeomLabel {
188 fn default() -> Self {
189 GeomLabel {
190 size: 10.0,
191 color: (0, 0, 0),
192 fill: (255, 255, 255),
193 alpha: 0.8,
194 padding: 3.0,
195 hjust: 0.5,
196 vjust: 0.5,
197 fontfamily: String::new(),
198 check_overlap: false,
199 }
200 }
201}
202
203impl Geom for GeomLabel {
204 fn draw(
205 &self,
206 data: &DataFrame,
207 coord: &dyn Coord,
208 scales: &ScaleSet,
209 _theme: &Theme,
210 backend: &mut dyn DrawBackend,
211 ) -> Result<(), RenderError> {
212 let x_col = data
213 .column("x")
214 .ok_or(RenderError::MissingAesthetic("x".into()))?;
215 let y_col = data
216 .column("y")
217 .ok_or(RenderError::MissingAesthetic("y".into()))?;
218 let label_col = data
219 .column("label")
220 .ok_or(RenderError::MissingAesthetic("label".into()))?;
221
222 let plot_area = backend.plot_area();
223 let x_scale = scales.get(&Aesthetic::X);
224 let y_scale = scales.get(&Aesthetic::Y);
225
226 let mut drawn_bboxes: Vec<(f64, f64, f64, f64)> = Vec::new();
227
228 for i in 0..data.nrows() {
229 let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
230 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
231 let (px, py) = coord.transform((nx, ny), &plot_area);
232 let py = py + (self.vjust - 0.5) * self.size;
236
237 let text = label_col[i].to_group_key();
238 let approx_width = text.len() as f64 * self.size * 0.6;
239 let half_w = approx_width / 2.0 + self.padding;
240 let half_h = self.size / 2.0 + self.padding;
241
242 if self.check_overlap {
243 let bbox = (px - half_w, py - half_h, px + half_w, py + half_h);
244 if bboxes_overlap(&bbox, &drawn_bboxes) {
245 continue;
246 }
247 drawn_bboxes.push(bbox);
248 }
249
250 backend.draw_rect(
252 (px - half_w, py - half_h),
253 (px + half_w, py + half_h),
254 &RectStyle {
255 fill: Some(self.fill),
256 stroke: Some(self.color),
257 stroke_width: 0.5,
258 alpha: self.alpha,
259 clip: true,
260 },
261 )?;
262
263 let anchor = hjust_to_anchor(self.hjust);
265 backend.draw_text(
266 &text,
267 (px, py),
268 &TextStyle {
269 color: self.color,
270 size: self.size,
271 anchor,
272 angle: 0.0,
273 family: None,
274 face: crate::render::backend::FontFace::Plain,
275 },
276 )?;
277 }
278
279 Ok(())
280 }
281
282 fn required_aes(&self) -> Vec<Aesthetic> {
283 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Label]
284 }
285
286 fn default_stat(&self) -> Box<dyn Stat> {
287 Box::new(StatIdentity)
288 }
289 fn default_position(&self) -> Box<dyn Position> {
290 Box::new(PositionIdentity)
291 }
292 fn default_params(&self) -> GeomParams {
293 GeomParams::default()
294 }
295 fn name(&self) -> &str {
296 "label"
297 }
298}
299
300fn hjust_to_anchor(hjust: f64) -> TextAnchor {
302 if hjust < 0.25 {
303 TextAnchor::Start
304 } else if hjust > 0.75 {
305 TextAnchor::End
306 } else {
307 TextAnchor::Middle
308 }
309}
310
311fn bboxes_overlap(candidate: &(f64, f64, f64, f64), existing: &[(f64, f64, f64, f64)]) -> bool {
313 for b in existing {
314 if candidate.0 < b.2 && candidate.2 > b.0 && candidate.1 < b.3 && candidate.3 > b.1 {
316 return true;
317 }
318 }
319 false
320}