1 | use std::ops::Range; |
2 | |
3 | use plotters_backend::DrawingBackend; |
4 | |
5 | use crate::chart::ChartContext; |
6 | use crate::coord::{ |
7 | cartesian::{Cartesian2d, MeshLine}, |
8 | ranged1d::{KeyPointHint, Ranged}, |
9 | Shift, |
10 | }; |
11 | use crate::drawing::{DrawingArea, DrawingAreaErrorKind}; |
12 | use crate::element::PathElement; |
13 | use crate::style::{ |
14 | text_anchor::{HPos, Pos, VPos}, |
15 | FontTransform, ShapeStyle, TextStyle, |
16 | }; |
17 | |
18 | impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d<X, Y>> { |
19 | /// The actual function that draws the mesh lines. |
20 | /// It also returns the label that suppose to be there. |
21 | #[allow (clippy::type_complexity)] |
22 | fn draw_mesh_lines<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>( |
23 | &mut self, |
24 | (r, c): (YH, XH), |
25 | (x_mesh, y_mesh): (bool, bool), |
26 | mesh_line_style: &ShapeStyle, |
27 | mut fmt_label: FmtLabel, |
28 | ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>> |
29 | where |
30 | FmtLabel: FnMut(&X, &Y, &MeshLine<X, Y>) -> Option<String>, |
31 | { |
32 | let mut x_labels = vec![]; |
33 | let mut y_labels = vec![]; |
34 | let xr = self.drawing_area.as_coord_spec().x_spec(); |
35 | let yr = self.drawing_area.as_coord_spec().y_spec(); |
36 | self.drawing_area.draw_mesh( |
37 | |b, l| { |
38 | let draw = match l { |
39 | MeshLine::XMesh((x, _), _, _) => { |
40 | if let Some(label_text) = fmt_label(xr, yr, &l) { |
41 | x_labels.push((x, label_text)); |
42 | } |
43 | x_mesh |
44 | } |
45 | MeshLine::YMesh((_, y), _, _) => { |
46 | if let Some(label_text) = fmt_label(xr, yr, &l) { |
47 | y_labels.push((y, label_text)); |
48 | } |
49 | y_mesh |
50 | } |
51 | }; |
52 | if draw { |
53 | l.draw(b, mesh_line_style) |
54 | } else { |
55 | Ok(()) |
56 | } |
57 | }, |
58 | r, |
59 | c, |
60 | )?; |
61 | Ok((x_labels, y_labels)) |
62 | } |
63 | |
64 | fn draw_axis( |
65 | &self, |
66 | area: &DrawingArea<DB, Shift>, |
67 | axis_style: Option<&ShapeStyle>, |
68 | orientation: (i16, i16), |
69 | inward_labels: bool, |
70 | ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> { |
71 | let (x0, y0) = self.drawing_area.get_base_pixel(); |
72 | let (tw, th) = area.dim_in_pixel(); |
73 | |
74 | let mut axis_range = if orientation.0 == 0 { |
75 | self.drawing_area.get_x_axis_pixel_range() |
76 | } else { |
77 | self.drawing_area.get_y_axis_pixel_range() |
78 | }; |
79 | |
80 | // At this point, the coordinate system tells us the pixel range after the translation. |
81 | // However, we need to use the logic coordinate system for drawing. |
82 | if orientation.0 == 0 { |
83 | axis_range.start -= x0; |
84 | axis_range.end -= x0; |
85 | } else { |
86 | axis_range.start -= y0; |
87 | axis_range.end -= y0; |
88 | } |
89 | |
90 | if let Some(axis_style) = axis_style { |
91 | let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 }; |
92 | let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 }; |
93 | let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 }; |
94 | let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 }; |
95 | |
96 | if inward_labels { |
97 | if orientation.0 == 0 { |
98 | if y0 == 0 { |
99 | y0 = th as i32 - 1; |
100 | y1 = th as i32 - 1; |
101 | } else { |
102 | y0 = 0; |
103 | y1 = 0; |
104 | } |
105 | } else if x0 == 0 { |
106 | x0 = tw as i32 - 1; |
107 | x1 = tw as i32 - 1; |
108 | } else { |
109 | x0 = 0; |
110 | x1 = 0; |
111 | } |
112 | } |
113 | |
114 | if orientation.0 == 0 { |
115 | x0 = axis_range.start; |
116 | x1 = axis_range.end; |
117 | } else { |
118 | y0 = axis_range.start; |
119 | y1 = axis_range.end; |
120 | } |
121 | |
122 | area.draw(&PathElement::new(vec![(x0, y0), (x1, y1)], *axis_style))?; |
123 | } |
124 | |
125 | Ok(axis_range) |
126 | } |
127 | |
128 | // TODO: consider make this function less complicated |
129 | #[allow (clippy::too_many_arguments)] |
130 | #[allow (clippy::cognitive_complexity)] |
131 | fn draw_axis_and_labels( |
132 | &self, |
133 | area: Option<&DrawingArea<DB, Shift>>, |
134 | axis_style: Option<&ShapeStyle>, |
135 | labels: &[(i32, String)], |
136 | label_style: &TextStyle, |
137 | label_offset: i32, |
138 | orientation: (i16, i16), |
139 | axis_desc: Option<(&str, &TextStyle)>, |
140 | tick_size: i32, |
141 | ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> { |
142 | let area = if let Some(target) = area { |
143 | target |
144 | } else { |
145 | return Ok(()); |
146 | }; |
147 | |
148 | let (x0, y0) = self.drawing_area.get_base_pixel(); |
149 | let (tw, th) = area.dim_in_pixel(); |
150 | |
151 | /* This is the minimal distance from the axis to the box of the labels */ |
152 | let label_dist = tick_size.abs() * 2; |
153 | |
154 | /* Draw the axis and get the axis range so that we can do further label |
155 | * and tick mark drawing */ |
156 | let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?; |
157 | |
158 | /* To make the right label area looks nice, it's a little bit tricky, since for a that is |
159 | * very long, we actually prefer left alignment instead of right alignment. |
160 | * Otherwise, the right alignment looks better. So we estimate the max and min label width |
161 | * So that we are able decide if we should apply right alignment for the text. */ |
162 | let label_width: Vec<_> = labels |
163 | .iter() |
164 | .map(|(_, text)| { |
165 | if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 { |
166 | self.drawing_area |
167 | .estimate_text_size(text, label_style) |
168 | .map(|(w, _)| w) |
169 | .unwrap_or(0) as i32 |
170 | } else { |
171 | // Don't ever do the layout estimationfor the drawing area that is either not |
172 | // the right one or the tick mark is inward. |
173 | 0 |
174 | } |
175 | }) |
176 | .collect(); |
177 | |
178 | let min_width = *label_width.iter().min().unwrap_or(&1).max(&1); |
179 | let max_width = *label_width |
180 | .iter() |
181 | .filter(|&&x| x < min_width * 2) |
182 | .max() |
183 | .unwrap_or(&min_width); |
184 | let right_align_width = (min_width * 2).min(max_width); |
185 | |
186 | /* Then we need to draw the tick mark and the label */ |
187 | for ((p, t), w) in labels.iter().zip(label_width.into_iter()) { |
188 | /* Make sure we are actually in the visible range */ |
189 | let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 }; |
190 | |
191 | if rp < axis_range.start.min(axis_range.end) |
192 | || axis_range.end.max(axis_range.start) < rp |
193 | { |
194 | continue; |
195 | } |
196 | |
197 | let (cx, cy, h_pos, v_pos) = if tick_size >= 0 { |
198 | match orientation { |
199 | // Right |
200 | (dx, dy) if dx > 0 && dy == 0 => { |
201 | if w >= right_align_width { |
202 | (label_dist, *p - y0, HPos::Left, VPos::Center) |
203 | } else { |
204 | ( |
205 | label_dist + right_align_width, |
206 | *p - y0, |
207 | HPos::Right, |
208 | VPos::Center, |
209 | ) |
210 | } |
211 | } |
212 | // Left |
213 | (dx, dy) if dx < 0 && dy == 0 => { |
214 | (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center) |
215 | } |
216 | // Bottom |
217 | (dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top), |
218 | // Top |
219 | (dx, dy) if dx == 0 && dy < 0 => { |
220 | (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom) |
221 | } |
222 | _ => panic!("Bug: Invalid orientation specification" ), |
223 | } |
224 | } else { |
225 | match orientation { |
226 | // Right |
227 | (dx, dy) if dx > 0 && dy == 0 => { |
228 | (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center) |
229 | } |
230 | // Left |
231 | (dx, dy) if dx < 0 && dy == 0 => { |
232 | (label_dist, *p - y0, HPos::Left, VPos::Center) |
233 | } |
234 | // Bottom |
235 | (dx, dy) if dx == 0 && dy > 0 => { |
236 | (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom) |
237 | } |
238 | // Top |
239 | (dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top), |
240 | _ => panic!("Bug: Invalid orientation specification" ), |
241 | } |
242 | }; |
243 | |
244 | let (text_x, text_y) = if orientation.0 == 0 { |
245 | (cx + label_offset, cy) |
246 | } else { |
247 | (cx, cy + label_offset) |
248 | }; |
249 | |
250 | let label_style = &label_style.pos(Pos::new(h_pos, v_pos)); |
251 | area.draw_text(t, label_style, (text_x, text_y))?; |
252 | |
253 | if tick_size != 0 { |
254 | if let Some(style) = axis_style { |
255 | let xmax = tw as i32 - 1; |
256 | let ymax = th as i32 - 1; |
257 | let (kx0, ky0, kx1, ky1) = if tick_size > 0 { |
258 | match orientation { |
259 | (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0), |
260 | (dx, dy) if dx < 0 && dy == 0 => { |
261 | (xmax - tick_size, *p - y0, xmax, *p - y0) |
262 | } |
263 | (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size), |
264 | (dx, dy) if dx == 0 && dy < 0 => { |
265 | (*p - x0, ymax - tick_size, *p - x0, ymax) |
266 | } |
267 | _ => panic!("Bug: Invalid orientation specification" ), |
268 | } |
269 | } else { |
270 | match orientation { |
271 | (dx, dy) if dx > 0 && dy == 0 => { |
272 | (xmax, *p - y0, xmax + tick_size, *p - y0) |
273 | } |
274 | (dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0), |
275 | (dx, dy) if dx == 0 && dy > 0 => { |
276 | (*p - x0, ymax, *p - x0, ymax + tick_size) |
277 | } |
278 | (dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size), |
279 | _ => panic!("Bug: Invalid orientation specification" ), |
280 | } |
281 | }; |
282 | let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], *style); |
283 | area.draw(&line)?; |
284 | } |
285 | } |
286 | } |
287 | |
288 | if let Some((text, style)) = axis_desc { |
289 | let actual_style = if orientation.0 == 0 { |
290 | style.clone() |
291 | } else if orientation.0 == -1 { |
292 | style.transform(FontTransform::Rotate270) |
293 | } else { |
294 | style.transform(FontTransform::Rotate90) |
295 | }; |
296 | |
297 | let (x0, y0, h_pos, v_pos) = match orientation { |
298 | // Right |
299 | (dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top), |
300 | // Left |
301 | (dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top), |
302 | // Bottom |
303 | (dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom), |
304 | // Top |
305 | (dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top), |
306 | _ => panic!("Bug: Invalid orientation specification" ), |
307 | }; |
308 | |
309 | let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos)); |
310 | area.draw_text(text, actual_style, (x0 as i32, y0 as i32))?; |
311 | } |
312 | |
313 | Ok(()) |
314 | } |
315 | |
316 | #[allow (clippy::too_many_arguments)] |
317 | pub(crate) fn draw_mesh<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>( |
318 | &mut self, |
319 | (r, c): (YH, XH), |
320 | mesh_line_style: &ShapeStyle, |
321 | x_label_style: &TextStyle, |
322 | y_label_style: &TextStyle, |
323 | fmt_label: FmtLabel, |
324 | x_mesh: bool, |
325 | y_mesh: bool, |
326 | x_label_offset: i32, |
327 | y_label_offset: i32, |
328 | x_axis: bool, |
329 | y_axis: bool, |
330 | axis_style: &ShapeStyle, |
331 | axis_desc_style: &TextStyle, |
332 | x_desc: Option<String>, |
333 | y_desc: Option<String>, |
334 | x_tick_size: [i32; 2], |
335 | y_tick_size: [i32; 2], |
336 | ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> |
337 | where |
338 | FmtLabel: FnMut(&X, &Y, &MeshLine<X, Y>) -> Option<String>, |
339 | { |
340 | let (x_labels, y_labels) = |
341 | self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?; |
342 | |
343 | for idx in 0..2 { |
344 | self.draw_axis_and_labels( |
345 | self.x_label_area[idx].as_ref(), |
346 | if x_axis { Some(axis_style) } else { None }, |
347 | &x_labels[..], |
348 | x_label_style, |
349 | x_label_offset, |
350 | (0, -1 + idx as i16 * 2), |
351 | x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)), |
352 | x_tick_size[idx], |
353 | )?; |
354 | |
355 | self.draw_axis_and_labels( |
356 | self.y_label_area[idx].as_ref(), |
357 | if y_axis { Some(axis_style) } else { None }, |
358 | &y_labels[..], |
359 | y_label_style, |
360 | y_label_offset, |
361 | (-1 + idx as i16 * 2, 0), |
362 | y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)), |
363 | y_tick_size[idx], |
364 | )?; |
365 | } |
366 | |
367 | Ok(()) |
368 | } |
369 | } |
370 | |