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