plotters_unstable/chart/
context.rs

1use std::borrow::Borrow;
2use std::ops::Range;
3
4use super::{DualCoordChartContext, MeshStyle, SeriesAnno, SeriesLabelStyle};
5
6use crate::coord::cartesian::{Cartesian2d, MeshLine};
7use crate::coord::ranged1d::{AsRangedCoord, KeyPointHint, Ranged, ValueFormatter};
8use crate::coord::{CoordTranslate, ReverseCoordTranslate, Shift};
9
10use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
11use crate::element::{Drawable, PathElement, PointCollection};
12use crate::style::text_anchor::{HPos, Pos, VPos};
13use crate::style::{ShapeStyle, TextStyle};
14
15use plotters_backend::{BackendCoord, DrawingBackend, FontTransform};
16
17/// The context of the chart. This is the core object of Plotters.
18/// Any plot/chart is abstracted as this type, and any data series can be placed to the chart
19/// context.
20///
21/// - To draw a series on a chart context, use [ChartContext::draw_series](struct.ChartContext.html#method.draw_series)
22/// - To draw a single element to the chart, you may want to use [ChartContext::plotting_area](struct.ChartContext.html#method.plotting_area)
23///
24pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
25    pub(super) x_label_area: [Option<DrawingArea<DB, Shift>>; 2],
26    pub(super) y_label_area: [Option<DrawingArea<DB, Shift>>; 2],
27    pub(super) drawing_area: DrawingArea<DB, CT>,
28    pub(super) series_anno: Vec<SeriesAnno<'a, DB>>,
29    pub(super) drawing_area_pos: (i32, i32),
30}
31
32impl<'a, DB, XT, YT, X, Y> ChartContext<'a, DB, Cartesian2d<X, Y>>
33where
34    DB: DrawingBackend,
35    X: Ranged<ValueType = XT> + ValueFormatter<XT>,
36    Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
37{
38    pub(crate) fn is_overlapping_drawing_area(
39        &self,
40        area: Option<&DrawingArea<DB, Shift>>,
41    ) -> bool {
42        if let Some(area) = area {
43            let (x0, y0) = area.get_base_pixel();
44            let (w, h) = area.dim_in_pixel();
45            let (x1, y1) = (x0 + w as i32, y0 + h as i32);
46            let (dx0, dy0) = self.drawing_area.get_base_pixel();
47            let (w, h) = self.drawing_area.dim_in_pixel();
48            let (dx1, dy1) = (dx0 + w as i32, dy0 + h as i32);
49
50            let (ox0, ox1) = (x0.max(dx0), x1.min(dx1));
51            let (oy0, oy1) = (y0.max(dy0), y1.min(dy1));
52
53            ox1 > ox0 && oy1 > oy0
54        } else {
55            false
56        }
57    }
58
59    /// Initialize a mesh configuration object and mesh drawing can be finalized by calling
60    /// the function `MeshStyle::draw`.
61    pub fn configure_mesh(&mut self) -> MeshStyle<'a, '_, X, Y, DB> {
62        MeshStyle::new(self)
63    }
64}
65
66impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
67    /// Convert the chart context into an closure that can be used for coordinate translation
68    pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
69        let coord_spec = self.drawing_area.into_coord_spec();
70        move |coord| coord_spec.reverse_translate(coord)
71    }
72}
73
74impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
75    /// Configure the styles for drawing series labels in the chart
76    pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT>
77    where
78        DB: 'a,
79    {
80        SeriesLabelStyle::new(self)
81    }
82
83    /// Get a reference of underlying plotting area
84    pub fn plotting_area(&self) -> &DrawingArea<DB, CT> {
85        &self.drawing_area
86    }
87
88    /// Cast the reference to a chart context to a reference to underlying coordinate specification.
89    pub fn as_coord_spec(&self) -> &CT {
90        self.drawing_area.as_coord_spec()
91    }
92
93    // TODO: All draw_series_impl is overly strict about lifetime, because we don't have stable HKT,
94    //       what we can ensure is for all lifetime 'b the element reference &'b E is a iterator
95    //       of points reference with the same lifetime.
96    //       However, this doesn't work if the coordinate doesn't live longer than the backend,
97    //       this is unnecessarily strict
98    pub(super) fn draw_series_impl<E, R, S>(
99        &mut self,
100        series: S,
101    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
102    where
103        for<'b> &'b E: PointCollection<'b, CT::From>,
104        E: Drawable<DB>,
105        R: Borrow<E>,
106        S: IntoIterator<Item = R>,
107    {
108        for element in series {
109            self.drawing_area.draw(element.borrow())?;
110        }
111        Ok(())
112    }
113
114    pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
115        let idx = self.series_anno.len();
116        self.series_anno.push(SeriesAnno::new());
117        &mut self.series_anno[idx]
118    }
119
120    /// Draw a data series. A data series in Plotters is abstracted as an iterator of elements
121    pub fn draw_series<E, R, S>(
122        &mut self,
123        series: S,
124    ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
125    where
126        for<'b> &'b E: PointCollection<'b, CT::From>,
127        E: Drawable<DB>,
128        R: Borrow<E>,
129        S: IntoIterator<Item = R>,
130    {
131        self.draw_series_impl(series)?;
132        Ok(self.alloc_series_anno())
133    }
134}
135
136impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d<X, Y>> {
137    /// Get the range of X axis
138    pub fn x_range(&self) -> Range<X::ValueType> {
139        self.drawing_area.get_x_range()
140    }
141
142    /// Get range of the Y axis
143    pub fn y_range(&self) -> Range<Y::ValueType> {
144        self.drawing_area.get_y_range()
145    }
146
147    /// Maps the coordinate to the backend coordinate. This is typically used
148    /// with an interactive chart.
149    pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord {
150        self.drawing_area.map_coordinate(coord)
151    }
152
153    /// The actual function that draws the mesh lines.
154    /// It also returns the label that suppose to be there.
155    #[allow(clippy::type_complexity)]
156    fn draw_mesh_lines<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
157        &mut self,
158        (r, c): (YH, XH),
159        (x_mesh, y_mesh): (bool, bool),
160        mesh_line_style: &ShapeStyle,
161        mut fmt_label: FmtLabel,
162    ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>>
163    where
164        FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
165    {
166        let mut x_labels = vec![];
167        let mut y_labels = vec![];
168        self.drawing_area.draw_mesh(
169            |b, l| {
170                let draw;
171                match l {
172                    MeshLine::XMesh((x, _), _, _) => {
173                        if let Some(label_text) = fmt_label(&l) {
174                            x_labels.push((x, label_text));
175                        }
176                        draw = x_mesh;
177                    }
178                    MeshLine::YMesh((_, y), _, _) => {
179                        if let Some(label_text) = fmt_label(&l) {
180                            y_labels.push((y, label_text));
181                        }
182                        draw = y_mesh;
183                    }
184                };
185                if draw {
186                    l.draw(b, mesh_line_style)
187                } else {
188                    Ok(())
189                }
190            },
191            r,
192            c,
193        )?;
194        Ok((x_labels, y_labels))
195    }
196
197    fn draw_axis(
198        &self,
199        area: &DrawingArea<DB, Shift>,
200        axis_style: Option<&ShapeStyle>,
201        orientation: (i16, i16),
202        inward_labels: bool,
203    ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> {
204        let (x0, y0) = self.drawing_area.get_base_pixel();
205        let (tw, th) = area.dim_in_pixel();
206
207        let mut axis_range = if orientation.0 == 0 {
208            self.drawing_area.get_x_axis_pixel_range()
209        } else {
210            self.drawing_area.get_y_axis_pixel_range()
211        };
212
213        /* At this point, the coordinate system tells us the pixel range
214         * after the translation.
215         * However, we need to use the logic coordinate system for drawing. */
216        if orientation.0 == 0 {
217            axis_range.start -= x0;
218            axis_range.end -= x0;
219        } else {
220            axis_range.start -= y0;
221            axis_range.end -= y0;
222        }
223
224        if let Some(axis_style) = axis_style {
225            let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 };
226            let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 };
227            let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 };
228            let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 };
229
230            if inward_labels {
231                if orientation.0 == 0 {
232                    if y0 == 0 {
233                        y0 = th as i32 - 1;
234                        y1 = th as i32 - 1;
235                    } else {
236                        y0 = 0;
237                        y1 = 0;
238                    }
239                } else if x0 == 0 {
240                    x0 = tw as i32 - 1;
241                    x1 = tw as i32 - 1;
242                } else {
243                    x0 = 0;
244                    x1 = 0;
245                }
246            }
247
248            if orientation.0 == 0 {
249                x0 = axis_range.start;
250                x1 = axis_range.end;
251            } else {
252                y0 = axis_range.start;
253                y1 = axis_range.end;
254            }
255
256            area.draw(&PathElement::new(
257                vec![(x0, y0), (x1, y1)],
258                axis_style.clone(),
259            ))?;
260        }
261
262        Ok(axis_range)
263    }
264
265    // TODO: consider make this function less complicated
266    #[allow(clippy::too_many_arguments)]
267    #[allow(clippy::cognitive_complexity)]
268    fn draw_axis_and_labels(
269        &self,
270        area: Option<&DrawingArea<DB, Shift>>,
271        axis_style: Option<&ShapeStyle>,
272        labels: &[(i32, String)],
273        label_style: &TextStyle,
274        label_offset: i32,
275        orientation: (i16, i16),
276        axis_desc: Option<(&str, &TextStyle)>,
277        tick_size: i32,
278    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
279        let area = if let Some(target) = area {
280            target
281        } else {
282            return Ok(());
283        };
284
285        let (x0, y0) = self.drawing_area.get_base_pixel();
286        let (tw, th) = area.dim_in_pixel();
287
288        /* This is the minimal distance from the axis to the box of the labels */
289        let label_dist = tick_size.abs() * 2;
290
291        /* Draw the axis and get the axis range so that we can do further label
292         * and tick mark drawing */
293        let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?;
294
295        /* To make the right label area looks nice, it's a little bit tricky, since for a that is
296         * very long, we actually prefer left alignment instead of right alignment.
297         * Otherwise, the right alignment looks better. So we estimate the max and min label width
298         * So that we are able decide if we should apply right alignment for the text. */
299        let label_width: Vec<_> = labels
300            .iter()
301            .map(|(_, text)| {
302                if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 {
303                    let ((x0, _), (x1, _)) = label_style
304                        .font
305                        .layout_box(text)
306                        .unwrap_or(((0, 0), (0, 0)));
307                    x1 - x0
308                } else {
309                    // Don't ever do the layout estimationfor the drawing area that is either not
310                    // the right one or the tick mark is inward.
311                    0
312                }
313            })
314            .collect();
315
316        let min_width = *label_width.iter().min().unwrap_or(&1).max(&1);
317        let max_width = *label_width
318            .iter()
319            .filter(|&&x| x < min_width * 2)
320            .max()
321            .unwrap_or(&min_width);
322        let right_align_width = (min_width * 2).min(max_width);
323
324        /* Then we need to draw the tick mark and the label */
325        for ((p, t), w) in labels.iter().zip(label_width.into_iter()) {
326            /* Make sure we are actually in the visible range */
327            let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 };
328
329            if rp < axis_range.start.min(axis_range.end)
330                || axis_range.end.max(axis_range.start) < rp
331            {
332                continue;
333            }
334
335            let (cx, cy, h_pos, v_pos) = if tick_size >= 0 {
336                match orientation {
337                    // Right
338                    (dx, dy) if dx > 0 && dy == 0 => {
339                        if w >= right_align_width {
340                            (label_dist, *p - y0, HPos::Left, VPos::Center)
341                        } else {
342                            (
343                                label_dist + right_align_width,
344                                *p - y0,
345                                HPos::Right,
346                                VPos::Center,
347                            )
348                        }
349                    }
350                    // Left
351                    (dx, dy) if dx < 0 && dy == 0 => {
352                        (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
353                    }
354                    // Bottom
355                    (dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
356                    // Top
357                    (dx, dy) if dx == 0 && dy < 0 => {
358                        (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
359                    }
360                    _ => panic!("Bug: Invalid orientation specification"),
361                }
362            } else {
363                match orientation {
364                    // Right
365                    (dx, dy) if dx > 0 && dy == 0 => {
366                        (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
367                    }
368                    // Left
369                    (dx, dy) if dx < 0 && dy == 0 => {
370                        (label_dist, *p - y0, HPos::Left, VPos::Center)
371                    }
372                    // Bottom
373                    (dx, dy) if dx == 0 && dy > 0 => {
374                        (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
375                    }
376                    // Top
377                    (dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
378                    _ => panic!("Bug: Invalid orientation specification"),
379                }
380            };
381
382            let (text_x, text_y) = if orientation.0 == 0 {
383                (cx + label_offset, cy)
384            } else {
385                (cx, cy + label_offset)
386            };
387
388            let label_style = &label_style.pos(Pos::new(h_pos, v_pos));
389            area.draw_text(&t, label_style, (text_x, text_y))?;
390
391            if tick_size != 0 {
392                if let Some(style) = axis_style {
393                    let xmax = tw as i32 - 1;
394                    let ymax = th as i32 - 1;
395                    let (kx0, ky0, kx1, ky1) = if tick_size > 0 {
396                        match orientation {
397                            (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0),
398                            (dx, dy) if dx < 0 && dy == 0 => {
399                                (xmax - tick_size, *p - y0, xmax, *p - y0)
400                            }
401                            (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size),
402                            (dx, dy) if dx == 0 && dy < 0 => {
403                                (*p - x0, ymax - tick_size, *p - x0, ymax)
404                            }
405                            _ => panic!("Bug: Invalid orientation specification"),
406                        }
407                    } else {
408                        match orientation {
409                            (dx, dy) if dx > 0 && dy == 0 => {
410                                (xmax, *p - y0, xmax + tick_size, *p - y0)
411                            }
412                            (dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0),
413                            (dx, dy) if dx == 0 && dy > 0 => {
414                                (*p - x0, ymax, *p - x0, ymax + tick_size)
415                            }
416                            (dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size),
417                            _ => panic!("Bug: Invalid orientation specification"),
418                        }
419                    };
420                    let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], style.clone());
421                    area.draw(&line)?;
422                }
423            }
424        }
425
426        if let Some((text, style)) = axis_desc {
427            let actual_style = if orientation.0 == 0 {
428                style.clone()
429            } else if orientation.0 == -1 {
430                style.transform(FontTransform::Rotate270)
431            } else {
432                style.transform(FontTransform::Rotate90)
433            };
434
435            let (x0, y0, h_pos, v_pos) = match orientation {
436                // Right
437                (dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top),
438                // Left
439                (dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top),
440                // Bottom
441                (dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom),
442                // Top
443                (dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top),
444                _ => panic!("Bug: Invalid orientation specification"),
445            };
446
447            let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos));
448            area.draw_text(&text, &actual_style, (x0 as i32, y0 as i32))?;
449        }
450
451        Ok(())
452    }
453
454    #[allow(clippy::too_many_arguments)]
455    pub(super) fn draw_mesh<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
456        &mut self,
457        (r, c): (YH, XH),
458        mesh_line_style: &ShapeStyle,
459        x_label_style: &TextStyle,
460        y_label_style: &TextStyle,
461        fmt_label: FmtLabel,
462        x_mesh: bool,
463        y_mesh: bool,
464        x_label_offset: i32,
465        y_label_offset: i32,
466        x_axis: bool,
467        y_axis: bool,
468        axis_style: &ShapeStyle,
469        axis_desc_style: &TextStyle,
470        x_desc: Option<String>,
471        y_desc: Option<String>,
472        x_tick_size: [i32; 2],
473        y_tick_size: [i32; 2],
474    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
475    where
476        FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
477    {
478        let (x_labels, y_labels) =
479            self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?;
480
481        for idx in 0..2 {
482            self.draw_axis_and_labels(
483                self.x_label_area[idx].as_ref(),
484                if x_axis { Some(axis_style) } else { None },
485                &x_labels[..],
486                x_label_style,
487                x_label_offset,
488                (0, -1 + idx as i16 * 2),
489                x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
490                x_tick_size[idx],
491            )?;
492
493            self.draw_axis_and_labels(
494                self.y_label_area[idx].as_ref(),
495                if y_axis { Some(axis_style) } else { None },
496                &y_labels[..],
497                y_label_style,
498                y_label_offset,
499                (-1 + idx as i16 * 2, 0),
500                y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
501                y_tick_size[idx],
502            )?;
503        }
504
505        Ok(())
506    }
507
508    /// Convert this chart context into a dual axis chart context and attach a second coordinate spec
509    /// on the chart context. For more detailed information, see documentation for [struct DualCoordChartContext](struct.DualCoordChartContext.html)
510    ///
511    /// - `x_coord`: The coordinate spec for the X axis
512    /// - `y_coord`: The coordinate spec for the Y axis
513    /// - **returns** The newly created dual spec chart context
514    #[allow(clippy::type_complexity)]
515    pub fn set_secondary_coord<SX: AsRangedCoord, SY: AsRangedCoord>(
516        self,
517        x_coord: SX,
518        y_coord: SY,
519    ) -> DualCoordChartContext<
520        'a,
521        DB,
522        Cartesian2d<X, Y>,
523        Cartesian2d<SX::CoordDescType, SY::CoordDescType>,
524    > {
525        let mut pixel_range = self.drawing_area.get_pixel_range();
526        pixel_range.1 = pixel_range.1.end..pixel_range.1.start;
527
528        DualCoordChartContext::new(self, Cartesian2d::new(x_coord, y_coord, pixel_range))
529    }
530}
531
532#[cfg(test)]
533mod test {
534    use crate::prelude::*;
535
536    #[test]
537    fn test_chart_context() {
538        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
539
540        drawing_area.fill(&WHITE).expect("Fill");
541
542        let mut chart = ChartBuilder::on(&drawing_area)
543            .caption("Test Title", ("serif", 10))
544            .x_label_area_size(20)
545            .y_label_area_size(20)
546            .set_label_area_size(LabelAreaPosition::Top, 20)
547            .set_label_area_size(LabelAreaPosition::Right, 20)
548            .build_cartesian_2d(0..10, 0..10)
549            .expect("Create chart")
550            .set_secondary_coord(0.0..1.0, 0.0..1.0);
551
552        chart
553            .configure_mesh()
554            .x_desc("X")
555            .y_desc("Y")
556            .draw()
557            .expect("Draw mesh");
558        chart
559            .configure_secondary_axes()
560            .x_desc("X")
561            .y_desc("Y")
562            .draw()
563            .expect("Draw Secondary axes");
564
565        chart
566            .draw_series(std::iter::once(Circle::new((5, 5), 5, &RED)))
567            .expect("Drawing error");
568        chart
569            .draw_secondary_series(std::iter::once(Circle::new((0.3, 0.8), 5, &GREEN)))
570            .expect("Drawing error")
571            .label("Test label")
572            .legend(|(x, y)| Rectangle::new([(x - 10, y - 5), (x, y + 5)], &GREEN));
573
574        chart
575            .configure_series_labels()
576            .position(SeriesLabelPosition::UpperMiddle)
577            .draw()
578            .expect("Drawing error");
579    }
580}