1use std::ops::Range;
2
3use plotters_backend::DrawingBackend;
4
5use crate::chart::ChartContext;
6use crate::coord::{
7 cartesian::{Cartesian2d, MeshLine},
8 ranged1d::{KeyPointHint, Ranged},
9 Shift,
10};
11use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
12use crate::element::PathElement;
13use crate::style::{
14 text_anchor::{HPos, Pos, VPos},
15 FontTransform, ShapeStyle, TextStyle,
16};
17
18impl<'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