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