plotters/chart/context/cartesian2d/
draw_impl.rs

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}