plotters/chart/
context.rs

1use std::borrow::Borrow;
2use std::ops::Range;
3
4use super::axes3d::Axes3dStyle;
5use super::{DualCoordChartContext, MeshStyle, SeriesAnno, SeriesLabelStyle};
6
7use crate::coord::cartesian::{Cartesian2d, Cartesian3d, MeshLine};
8use crate::coord::ranged1d::{AsRangedCoord, KeyPointHint, Ranged, ValueFormatter};
9use crate::coord::ranged3d::{ProjectionMatrix, ProjectionMatrixBuilder};
10use crate::coord::{CoordTranslate, ReverseCoordTranslate, Shift};
11
12use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
13use crate::element::{
14    CoordMapper, Drawable, EmptyElement, PathElement, PointCollection, Polygon, Text,
15};
16use crate::style::text_anchor::{HPos, Pos, VPos};
17use crate::style::{ShapeStyle, TextStyle};
18
19use plotters_backend::{BackendCoord, DrawingBackend, FontTransform};
20
21/// The context of the chart. This is the core object of Plotters.
22/// Any plot/chart is abstracted as this type, and any data series can be placed to the chart
23/// context.
24///
25/// - To draw a series on a chart context, use [ChartContext::draw_series](struct.ChartContext.html#method.draw_series)
26/// - To draw a single element to the chart, you may want to use [ChartContext::plotting_area](struct.ChartContext.html#method.plotting_area)
27///
28pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
29    pub(super) x_label_area: [Option<DrawingArea<DB, Shift>>; 2],
30    pub(super) y_label_area: [Option<DrawingArea<DB, Shift>>; 2],
31    pub(super) drawing_area: DrawingArea<DB, CT>,
32    pub(super) series_anno: Vec<SeriesAnno<'a, DB>>,
33    pub(super) drawing_area_pos: (i32, i32),
34}
35
36impl<'a, DB, XT, YT, X, Y> ChartContext<'a, DB, Cartesian2d<X, Y>>
37where
38    DB: DrawingBackend,
39    X: Ranged<ValueType = XT> + ValueFormatter<XT>,
40    Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
41{
42    pub(crate) fn is_overlapping_drawing_area(
43        &self,
44        area: Option<&DrawingArea<DB, Shift>>,
45    ) -> bool {
46        if let Some(area) = area {
47            let (x0, y0) = area.get_base_pixel();
48            let (w, h) = area.dim_in_pixel();
49            let (x1, y1) = (x0 + w as i32, y0 + h as i32);
50            let (dx0, dy0) = self.drawing_area.get_base_pixel();
51            let (w, h) = self.drawing_area.dim_in_pixel();
52            let (dx1, dy1) = (dx0 + w as i32, dy0 + h as i32);
53
54            let (ox0, ox1) = (x0.max(dx0), x1.min(dx1));
55            let (oy0, oy1) = (y0.max(dy0), y1.min(dy1));
56
57            ox1 > ox0 && oy1 > oy0
58        } else {
59            false
60        }
61    }
62
63    /// Initialize a mesh configuration object and mesh drawing can be finalized by calling
64    /// the function `MeshStyle::draw`.
65    pub fn configure_mesh(&mut self) -> MeshStyle<'a, '_, X, Y, DB> {
66        MeshStyle::new(self)
67    }
68}
69
70impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
71    /// Convert the chart context into an closure that can be used for coordinate translation
72    pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
73        let coord_spec = self.drawing_area.into_coord_spec();
74        move |coord| coord_spec.reverse_translate(coord)
75    }
76}
77
78impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
79    /// Configure the styles for drawing series labels in the chart
80    pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT>
81    where
82        DB: 'a,
83    {
84        SeriesLabelStyle::new(self)
85    }
86
87    /// Get a reference of underlying plotting area
88    pub fn plotting_area(&self) -> &DrawingArea<DB, CT> {
89        &self.drawing_area
90    }
91
92    /// Cast the reference to a chart context to a reference to underlying coordinate specification.
93    pub fn as_coord_spec(&self) -> &CT {
94        self.drawing_area.as_coord_spec()
95    }
96
97    // TODO: All draw_series_impl is overly strict about lifetime, because we don't have stable HKT,
98    //       what we can ensure is for all lifetime 'b the element reference &'b E is a iterator
99    //       of points reference with the same lifetime.
100    //       However, this doesn't work if the coordinate doesn't live longer than the backend,
101    //       this is unnecessarily strict
102    pub(super) fn draw_series_impl<B, E, R, S>(
103        &mut self,
104        series: S,
105    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
106    where
107        B: CoordMapper,
108        for<'b> &'b E: PointCollection<'b, CT::From, B>,
109        E: Drawable<DB, B>,
110        R: Borrow<E>,
111        S: IntoIterator<Item = R>,
112    {
113        for element in series {
114            self.drawing_area.draw(element.borrow())?;
115        }
116        Ok(())
117    }
118
119    pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
120        let idx = self.series_anno.len();
121        self.series_anno.push(SeriesAnno::new());
122        &mut self.series_anno[idx]
123    }
124
125    /// Draw a data series. A data series in Plotters is abstracted as an iterator of elements
126    pub fn draw_series<B, E, R, S>(
127        &mut self,
128        series: S,
129    ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
130    where
131        B: CoordMapper,
132        for<'b> &'b E: PointCollection<'b, CT::From, B>,
133        E: Drawable<DB, B>,
134        R: Borrow<E>,
135        S: IntoIterator<Item = R>,
136    {
137        self.draw_series_impl(series)?;
138        Ok(self.alloc_series_anno())
139    }
140}
141
142impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d<X, Y>> {
143    /// Get the range of X axis
144    pub fn x_range(&self) -> Range<X::ValueType> {
145        self.drawing_area.get_x_range()
146    }
147
148    /// Get range of the Y axis
149    pub fn y_range(&self) -> Range<Y::ValueType> {
150        self.drawing_area.get_y_range()
151    }
152
153    /// Maps the coordinate to the backend coordinate. This is typically used
154    /// with an interactive chart.
155    pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord {
156        self.drawing_area.map_coordinate(coord)
157    }
158
159    /// The actual function that draws the mesh lines.
160    /// It also returns the label that suppose to be there.
161    #[allow(clippy::type_complexity)]
162    fn draw_mesh_lines<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
163        &mut self,
164        (r, c): (YH, XH),
165        (x_mesh, y_mesh): (bool, bool),
166        mesh_line_style: &ShapeStyle,
167        mut fmt_label: FmtLabel,
168    ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>>
169    where
170        FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
171    {
172        let mut x_labels = vec![];
173        let mut y_labels = vec![];
174        self.drawing_area.draw_mesh(
175            |b, l| {
176                let draw;
177                match l {
178                    MeshLine::XMesh((x, _), _, _) => {
179                        if let Some(label_text) = fmt_label(&l) {
180                            x_labels.push((x, label_text));
181                        }
182                        draw = x_mesh;
183                    }
184                    MeshLine::YMesh((_, y), _, _) => {
185                        if let Some(label_text) = fmt_label(&l) {
186                            y_labels.push((y, label_text));
187                        }
188                        draw = y_mesh;
189                    }
190                };
191                if draw {
192                    l.draw(b, mesh_line_style)
193                } else {
194                    Ok(())
195                }
196            },
197            r,
198            c,
199        )?;
200        Ok((x_labels, y_labels))
201    }
202
203    fn draw_axis(
204        &self,
205        area: &DrawingArea<DB, Shift>,
206        axis_style: Option<&ShapeStyle>,
207        orientation: (i16, i16),
208        inward_labels: bool,
209    ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> {
210        let (x0, y0) = self.drawing_area.get_base_pixel();
211        let (tw, th) = area.dim_in_pixel();
212
213        let mut axis_range = if orientation.0 == 0 {
214            self.drawing_area.get_x_axis_pixel_range()
215        } else {
216            self.drawing_area.get_y_axis_pixel_range()
217        };
218
219        /* At this point, the coordinate system tells us the pixel range
220         * after the translation.
221         * However, we need to use the logic coordinate system for drawing. */
222        if orientation.0 == 0 {
223            axis_range.start -= x0;
224            axis_range.end -= x0;
225        } else {
226            axis_range.start -= y0;
227            axis_range.end -= y0;
228        }
229
230        if let Some(axis_style) = axis_style {
231            let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 };
232            let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 };
233            let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 };
234            let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 };
235
236            if inward_labels {
237                if orientation.0 == 0 {
238                    if y0 == 0 {
239                        y0 = th as i32 - 1;
240                        y1 = th as i32 - 1;
241                    } else {
242                        y0 = 0;
243                        y1 = 0;
244                    }
245                } else if x0 == 0 {
246                    x0 = tw as i32 - 1;
247                    x1 = tw as i32 - 1;
248                } else {
249                    x0 = 0;
250                    x1 = 0;
251                }
252            }
253
254            if orientation.0 == 0 {
255                x0 = axis_range.start;
256                x1 = axis_range.end;
257            } else {
258                y0 = axis_range.start;
259                y1 = axis_range.end;
260            }
261
262            area.draw(&PathElement::new(
263                vec![(x0, y0), (x1, y1)],
264                axis_style.clone(),
265            ))?;
266        }
267
268        Ok(axis_range)
269    }
270
271    // TODO: consider make this function less complicated
272    #[allow(clippy::too_many_arguments)]
273    #[allow(clippy::cognitive_complexity)]
274    fn draw_axis_and_labels(
275        &self,
276        area: Option<&DrawingArea<DB, Shift>>,
277        axis_style: Option<&ShapeStyle>,
278        labels: &[(i32, String)],
279        label_style: &TextStyle,
280        label_offset: i32,
281        orientation: (i16, i16),
282        axis_desc: Option<(&str, &TextStyle)>,
283        tick_size: i32,
284    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
285        let area = if let Some(target) = area {
286            target
287        } else {
288            return Ok(());
289        };
290
291        let (x0, y0) = self.drawing_area.get_base_pixel();
292        let (tw, th) = area.dim_in_pixel();
293
294        /* This is the minimal distance from the axis to the box of the labels */
295        let label_dist = tick_size.abs() * 2;
296
297        /* Draw the axis and get the axis range so that we can do further label
298         * and tick mark drawing */
299        let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?;
300
301        /* To make the right label area looks nice, it's a little bit tricky, since for a that is
302         * very long, we actually prefer left alignment instead of right alignment.
303         * Otherwise, the right alignment looks better. So we estimate the max and min label width
304         * So that we are able decide if we should apply right alignment for the text. */
305        let label_width: Vec<_> = labels
306            .iter()
307            .map(|(_, text)| {
308                if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 {
309                    self.drawing_area
310                        .estimate_text_size(text, &label_style)
311                        .map(|(w, _)| w)
312                        .unwrap_or(0) as i32
313                } else {
314                    // Don't ever do the layout estimationfor the drawing area that is either not
315                    // the right one or the tick mark is inward.
316                    0
317                }
318            })
319            .collect();
320
321        let min_width = *label_width.iter().min().unwrap_or(&1).max(&1);
322        let max_width = *label_width
323            .iter()
324            .filter(|&&x| x < min_width * 2)
325            .max()
326            .unwrap_or(&min_width);
327        let right_align_width = (min_width * 2).min(max_width);
328
329        /* Then we need to draw the tick mark and the label */
330        for ((p, t), w) in labels.iter().zip(label_width.into_iter()) {
331            /* Make sure we are actually in the visible range */
332            let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 };
333
334            if rp < axis_range.start.min(axis_range.end)
335                || axis_range.end.max(axis_range.start) < rp
336            {
337                continue;
338            }
339
340            let (cx, cy, h_pos, v_pos) = if tick_size >= 0 {
341                match orientation {
342                    // Right
343                    (dx, dy) if dx > 0 && dy == 0 => {
344                        if w >= right_align_width {
345                            (label_dist, *p - y0, HPos::Left, VPos::Center)
346                        } else {
347                            (
348                                label_dist + right_align_width,
349                                *p - y0,
350                                HPos::Right,
351                                VPos::Center,
352                            )
353                        }
354                    }
355                    // Left
356                    (dx, dy) if dx < 0 && dy == 0 => {
357                        (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
358                    }
359                    // Bottom
360                    (dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
361                    // Top
362                    (dx, dy) if dx == 0 && dy < 0 => {
363                        (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
364                    }
365                    _ => panic!("Bug: Invalid orientation specification"),
366                }
367            } else {
368                match orientation {
369                    // Right
370                    (dx, dy) if dx > 0 && dy == 0 => {
371                        (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
372                    }
373                    // Left
374                    (dx, dy) if dx < 0 && dy == 0 => {
375                        (label_dist, *p - y0, HPos::Left, VPos::Center)
376                    }
377                    // Bottom
378                    (dx, dy) if dx == 0 && dy > 0 => {
379                        (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
380                    }
381                    // Top
382                    (dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
383                    _ => panic!("Bug: Invalid orientation specification"),
384                }
385            };
386
387            let (text_x, text_y) = if orientation.0 == 0 {
388                (cx + label_offset, cy)
389            } else {
390                (cx, cy + label_offset)
391            };
392
393            let label_style = &label_style.pos(Pos::new(h_pos, v_pos));
394            area.draw_text(&t, label_style, (text_x, text_y))?;
395
396            if tick_size != 0 {
397                if let Some(style) = axis_style {
398                    let xmax = tw as i32 - 1;
399                    let ymax = th as i32 - 1;
400                    let (kx0, ky0, kx1, ky1) = if tick_size > 0 {
401                        match orientation {
402                            (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0),
403                            (dx, dy) if dx < 0 && dy == 0 => {
404                                (xmax - tick_size, *p - y0, xmax, *p - y0)
405                            }
406                            (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size),
407                            (dx, dy) if dx == 0 && dy < 0 => {
408                                (*p - x0, ymax - tick_size, *p - x0, ymax)
409                            }
410                            _ => panic!("Bug: Invalid orientation specification"),
411                        }
412                    } else {
413                        match orientation {
414                            (dx, dy) if dx > 0 && dy == 0 => {
415                                (xmax, *p - y0, xmax + tick_size, *p - y0)
416                            }
417                            (dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0),
418                            (dx, dy) if dx == 0 && dy > 0 => {
419                                (*p - x0, ymax, *p - x0, ymax + tick_size)
420                            }
421                            (dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size),
422                            _ => panic!("Bug: Invalid orientation specification"),
423                        }
424                    };
425                    let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], style.clone());
426                    area.draw(&line)?;
427                }
428            }
429        }
430
431        if let Some((text, style)) = axis_desc {
432            let actual_style = if orientation.0 == 0 {
433                style.clone()
434            } else if orientation.0 == -1 {
435                style.transform(FontTransform::Rotate270)
436            } else {
437                style.transform(FontTransform::Rotate90)
438            };
439
440            let (x0, y0, h_pos, v_pos) = match orientation {
441                // Right
442                (dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top),
443                // Left
444                (dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top),
445                // Bottom
446                (dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom),
447                // Top
448                (dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top),
449                _ => panic!("Bug: Invalid orientation specification"),
450            };
451
452            let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos));
453            area.draw_text(&text, &actual_style, (x0 as i32, y0 as i32))?;
454        }
455
456        Ok(())
457    }
458
459    #[allow(clippy::too_many_arguments)]
460    pub(super) fn draw_mesh<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
461        &mut self,
462        (r, c): (YH, XH),
463        mesh_line_style: &ShapeStyle,
464        x_label_style: &TextStyle,
465        y_label_style: &TextStyle,
466        fmt_label: FmtLabel,
467        x_mesh: bool,
468        y_mesh: bool,
469        x_label_offset: i32,
470        y_label_offset: i32,
471        x_axis: bool,
472        y_axis: bool,
473        axis_style: &ShapeStyle,
474        axis_desc_style: &TextStyle,
475        x_desc: Option<String>,
476        y_desc: Option<String>,
477        x_tick_size: [i32; 2],
478        y_tick_size: [i32; 2],
479    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
480    where
481        FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
482    {
483        let (x_labels, y_labels) =
484            self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?;
485
486        for idx in 0..2 {
487            self.draw_axis_and_labels(
488                self.x_label_area[idx].as_ref(),
489                if x_axis { Some(axis_style) } else { None },
490                &x_labels[..],
491                x_label_style,
492                x_label_offset,
493                (0, -1 + idx as i16 * 2),
494                x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
495                x_tick_size[idx],
496            )?;
497
498            self.draw_axis_and_labels(
499                self.y_label_area[idx].as_ref(),
500                if y_axis { Some(axis_style) } else { None },
501                &y_labels[..],
502                y_label_style,
503                y_label_offset,
504                (-1 + idx as i16 * 2, 0),
505                y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
506                y_tick_size[idx],
507            )?;
508        }
509
510        Ok(())
511    }
512
513    /// Convert this chart context into a dual axis chart context and attach a second coordinate spec
514    /// on the chart context. For more detailed information, see documentation for [struct DualCoordChartContext](struct.DualCoordChartContext.html)
515    ///
516    /// - `x_coord`: The coordinate spec for the X axis
517    /// - `y_coord`: The coordinate spec for the Y axis
518    /// - **returns** The newly created dual spec chart context
519    #[allow(clippy::type_complexity)]
520    pub fn set_secondary_coord<SX: AsRangedCoord, SY: AsRangedCoord>(
521        self,
522        x_coord: SX,
523        y_coord: SY,
524    ) -> DualCoordChartContext<
525        'a,
526        DB,
527        Cartesian2d<X, Y>,
528        Cartesian2d<SX::CoordDescType, SY::CoordDescType>,
529    > {
530        let mut pixel_range = self.drawing_area.get_pixel_range();
531        pixel_range.1 = pixel_range.1.end..pixel_range.1.start;
532
533        DualCoordChartContext::new(self, Cartesian2d::new(x_coord, y_coord, pixel_range))
534    }
535}
536
537pub(super) struct KeyPoints3d<X: Ranged, Y: Ranged, Z: Ranged> {
538    pub(super) x_points: Vec<X::ValueType>,
539    pub(super) y_points: Vec<Y::ValueType>,
540    pub(super) z_points: Vec<Z::ValueType>,
541}
542
543#[derive(Clone, Debug)]
544pub(super) enum Coord3D<X, Y, Z> {
545    X(X),
546    Y(Y),
547    Z(Z),
548}
549
550impl<X, Y, Z> Coord3D<X, Y, Z> {
551    fn get_x(&self) -> &X {
552        match self {
553            Coord3D::X(ret) => ret,
554            _ => panic!("Invalid call!"),
555        }
556    }
557    fn get_y(&self) -> &Y {
558        match self {
559            Coord3D::Y(ret) => ret,
560            _ => panic!("Invalid call!"),
561        }
562    }
563    fn get_z(&self) -> &Z {
564        match self {
565            Coord3D::Z(ret) => ret,
566            _ => panic!("Invalid call!"),
567        }
568    }
569
570    fn build_coord([x, y, z]: [&Self; 3]) -> (X, Y, Z)
571    where
572        X: Clone,
573        Y: Clone,
574        Z: Clone,
575    {
576        (x.get_x().clone(), y.get_y().clone(), z.get_z().clone())
577    }
578}
579impl<'a, DB, X, Y, Z, XT, YT, ZT> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
580where
581    DB: DrawingBackend,
582    X: Ranged<ValueType = XT> + ValueFormatter<XT>,
583    Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
584    Z: Ranged<ValueType = ZT> + ValueFormatter<ZT>,
585{
586    pub fn configure_axes(&mut self) -> Axes3dStyle<'a, '_, X, Y, Z, DB> {
587        Axes3dStyle::new(self)
588    }
589}
590impl<'a, DB, X: Ranged, Y: Ranged, Z: Ranged> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
591where
592    DB: DrawingBackend,
593{
594    /// Override the 3D projection matrix. This function allows to override the default projection
595    /// matrix.
596    /// - `pf`: A function that takes the default projection matrix configuration and returns the
597    /// projection matrix. This function will allow you to adjust the pitch, yaw angle and the
598    /// centeral point of the projection, etc. You can also build a projection matrix which is not
599    /// relies on the default configuration as well.
600    pub fn with_projection<P: FnOnce(ProjectionMatrixBuilder) -> ProjectionMatrix>(
601        &mut self,
602        pf: P,
603    ) -> &mut Self {
604        let (actual_x, actual_y) = self.drawing_area.get_pixel_range();
605        self.drawing_area
606            .as_coord_spec_mut()
607            .set_projection(actual_x, actual_y, pf);
608        self
609    }
610
611    pub fn set_3d_pixel_range(&mut self, size: (i32, i32, i32)) -> &mut Self {
612        let (actual_x, actual_y) = self.drawing_area.get_pixel_range();
613        self.drawing_area
614            .as_coord_spec_mut()
615            .set_coord_pixel_range(actual_x, actual_y, size);
616        self
617    }
618}
619
620impl<'a, DB, X: Ranged, Y: Ranged, Z: Ranged> ChartContext<'a, DB, Cartesian3d<X, Y, Z>>
621where
622    DB: DrawingBackend,
623    X::ValueType: Clone,
624    Y::ValueType: Clone,
625    Z::ValueType: Clone,
626{
627    pub(super) fn get_key_points<XH: KeyPointHint, YH: KeyPointHint, ZH: KeyPointHint>(
628        &self,
629        x_hint: XH,
630        y_hint: YH,
631        z_hint: ZH,
632    ) -> KeyPoints3d<X, Y, Z> {
633        let coord = self.plotting_area().as_coord_spec();
634        let x_points = coord.logic_x.key_points(x_hint);
635        let y_points = coord.logic_y.key_points(y_hint);
636        let z_points = coord.logic_z.key_points(z_hint);
637        KeyPoints3d {
638            x_points,
639            y_points,
640            z_points,
641        }
642    }
643    pub(super) fn draw_axis_ticks(
644        &mut self,
645        axis: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
646        labels: &[(
647            [Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3],
648            String,
649        )],
650        tick_size: i32,
651        style: ShapeStyle,
652        font: TextStyle,
653    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
654        let coord = self.plotting_area().as_coord_spec();
655        let begin = coord.translate(&Coord3D::build_coord([
656            &axis[0][0],
657            &axis[0][1],
658            &axis[0][2],
659        ]));
660        let end = coord.translate(&Coord3D::build_coord([
661            &axis[1][0],
662            &axis[1][1],
663            &axis[1][2],
664        ]));
665        let axis_dir = (end.0 - begin.0, end.1 - begin.1);
666        let (x_range, y_range) = self.plotting_area().get_pixel_range();
667        let x_mid = (x_range.start + x_range.end) / 2;
668        let y_mid = (y_range.start + y_range.end) / 2;
669
670        let x_dir = if begin.0 < x_mid {
671            (-tick_size, 0)
672        } else {
673            (tick_size, 0)
674        };
675
676        let y_dir = if begin.1 < y_mid {
677            (0, -tick_size)
678        } else {
679            (0, tick_size)
680        };
681
682        let x_score = (x_dir.0 * axis_dir.0 + x_dir.1 * axis_dir.1).abs();
683        let y_score = (y_dir.0 * axis_dir.0 + y_dir.1 * axis_dir.1).abs();
684
685        let dir = if x_score < y_score { x_dir } else { y_dir };
686
687        for (pos, text) in labels {
688            let logic_pos = Coord3D::build_coord([&pos[0], &pos[1], &pos[2]]);
689            let mut font = font.clone();
690            if dir.0 < 0 {
691                font.pos = Pos::new(HPos::Right, VPos::Center);
692            } else if dir.0 > 0 {
693                font.pos = Pos::new(HPos::Left, VPos::Center);
694            };
695            if dir.1 < 0 {
696                font.pos = Pos::new(HPos::Center, VPos::Bottom);
697            } else if dir.1 > 0 {
698                font.pos = Pos::new(HPos::Center, VPos::Top);
699            };
700            let element = EmptyElement::at(logic_pos)
701                + PathElement::new(vec![(0, 0), dir], style.clone())
702                + Text::new(text.to_string(), (dir.0 * 2, dir.1 * 2), font.clone());
703            self.plotting_area().draw(&element)?;
704        }
705        Ok(())
706    }
707    pub(super) fn draw_axis(
708        &mut self,
709        idx: usize,
710        panels: &[[[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2]; 3],
711        style: ShapeStyle,
712    ) -> Result<
713        [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
714        DrawingAreaErrorKind<DB::ErrorType>,
715    > {
716        let coord = self.plotting_area().as_coord_spec();
717        let x_range = coord.logic_x.range();
718        let y_range = coord.logic_y.range();
719        let z_range = coord.logic_z.range();
720
721        let ranges: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 2]; 3] = [
722            [Coord3D::X(x_range.start), Coord3D::X(x_range.end)],
723            [Coord3D::Y(y_range.start), Coord3D::Y(y_range.end)],
724            [Coord3D::Z(z_range.start), Coord3D::Z(z_range.end)],
725        ];
726
727        let (start, end) = {
728            let mut start = [&ranges[0][0], &ranges[1][0], &ranges[2][0]];
729            let mut end = [&ranges[0][1], &ranges[1][1], &ranges[2][1]];
730
731            let mut plan = vec![];
732
733            for i in 0..3 {
734                if i == idx {
735                    continue;
736                }
737                start[i] = &panels[i][0][i];
738                end[i] = &panels[i][0][i];
739                for j in 0..3 {
740                    if i != idx && i != j && j != idx {
741                        for k in 0..2 {
742                            start[j] = &panels[i][k][j];
743                            end[j] = &panels[i][k][j];
744                            plan.push((start, end));
745                        }
746                    }
747                }
748            }
749            plan.into_iter()
750                .min_by_key(|&(s, e)| {
751                    let d = coord.projected_depth(s[0].get_x(), s[1].get_y(), s[2].get_z());
752                    let d = d + coord.projected_depth(e[0].get_x(), e[1].get_y(), e[2].get_z());
753                    let (_, y1) = coord.translate(&Coord3D::build_coord(s));
754                    let (_, y2) = coord.translate(&Coord3D::build_coord(e));
755                    let y = y1 + y2;
756                    (d, y)
757                })
758                .unwrap()
759        };
760
761        self.plotting_area().draw(&PathElement::new(
762            vec![Coord3D::build_coord(start), Coord3D::build_coord(end)],
763            style.clone(),
764        ))?;
765
766        Ok([
767            [start[0].clone(), start[1].clone(), start[2].clone()],
768            [end[0].clone(), end[1].clone(), end[2].clone()],
769        ])
770    }
771    pub(super) fn draw_axis_panels(
772        &mut self,
773        bold_points: &KeyPoints3d<X, Y, Z>,
774        light_points: &KeyPoints3d<X, Y, Z>,
775        panel_style: ShapeStyle,
776        bold_grid_style: ShapeStyle,
777        light_grid_style: ShapeStyle,
778    ) -> Result<
779        [[[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2]; 3],
780        DrawingAreaErrorKind<DB::ErrorType>,
781    > {
782        let mut r_iter = (0..3).map(|idx| {
783            self.draw_axis_panel(
784                idx,
785                bold_points,
786                light_points,
787                panel_style.clone(),
788                bold_grid_style.clone(),
789                light_grid_style.clone(),
790            )
791        });
792        Ok([
793            r_iter.next().unwrap()?,
794            r_iter.next().unwrap()?,
795            r_iter.next().unwrap()?,
796        ])
797    }
798    fn draw_axis_panel(
799        &mut self,
800        idx: usize,
801        bold_points: &KeyPoints3d<X, Y, Z>,
802        light_points: &KeyPoints3d<X, Y, Z>,
803        panel_style: ShapeStyle,
804        bold_grid_style: ShapeStyle,
805        light_grid_style: ShapeStyle,
806    ) -> Result<
807        [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 3]; 2],
808        DrawingAreaErrorKind<DB::ErrorType>,
809    > {
810        let coord = self.plotting_area().as_coord_spec();
811        let x_range = coord.logic_x.range();
812        let y_range = coord.logic_y.range();
813        let z_range = coord.logic_z.range();
814
815        let ranges: [[Coord3D<X::ValueType, Y::ValueType, Z::ValueType>; 2]; 3] = [
816            [Coord3D::X(x_range.start), Coord3D::X(x_range.end)],
817            [Coord3D::Y(y_range.start), Coord3D::Y(y_range.end)],
818            [Coord3D::Z(z_range.start), Coord3D::Z(z_range.end)],
819        ];
820
821        let (mut panel, start, end) = {
822            let a = [&ranges[0][0], &ranges[1][0], &ranges[2][0]];
823            let mut b = [&ranges[0][1], &ranges[1][1], &ranges[2][1]];
824            let mut c = a;
825            let d = b;
826
827            b[idx] = &ranges[idx][0];
828            c[idx] = &ranges[idx][1];
829
830            let (a, b) = if coord.projected_depth(a[0].get_x(), a[1].get_y(), a[2].get_z())
831                >= coord.projected_depth(c[0].get_x(), c[1].get_y(), c[2].get_z())
832            {
833                (a, b)
834            } else {
835                (c, d)
836            };
837
838            let mut m = a.clone();
839            m[(idx + 1) % 3] = b[(idx + 1) % 3];
840            let mut n = a.clone();
841            n[(idx + 2) % 3] = b[(idx + 2) % 3];
842
843            (
844                vec![
845                    Coord3D::build_coord(a),
846                    Coord3D::build_coord(m),
847                    Coord3D::build_coord(b),
848                    Coord3D::build_coord(n),
849                ],
850                a,
851                b,
852            )
853        };
854        self.plotting_area()
855            .draw(&Polygon::new(panel.clone(), panel_style.clone()))?;
856        panel.push(panel[0].clone());
857        self.plotting_area()
858            .draw(&PathElement::new(panel, bold_grid_style.clone()))?;
859
860        for (kps, style) in vec![
861            (light_points, light_grid_style),
862            (bold_points, bold_grid_style),
863        ]
864        .into_iter()
865        {
866            for idx in (0..3).filter(|&i| i != idx) {
867                let kps: Vec<_> = match idx {
868                    0 => kps.x_points.iter().map(|x| Coord3D::X(x.clone())).collect(),
869                    1 => kps.y_points.iter().map(|y| Coord3D::Y(y.clone())).collect(),
870                    _ => kps.z_points.iter().map(|z| Coord3D::Z(z.clone())).collect(),
871                };
872                for kp in kps.iter() {
873                    let mut kp_start = start;
874                    let mut kp_end = end;
875                    kp_start[idx] = kp;
876                    kp_end[idx] = kp;
877                    self.plotting_area().draw(&PathElement::new(
878                        vec![Coord3D::build_coord(kp_start), Coord3D::build_coord(kp_end)],
879                        style.clone(),
880                    ))?;
881                }
882            }
883        }
884
885        Ok([
886            [start[0].clone(), start[1].clone(), start[2].clone()],
887            [end[0].clone(), end[1].clone(), end[2].clone()],
888        ])
889    }
890}
891
892#[cfg(test)]
893mod test {
894    use crate::prelude::*;
895
896    #[test]
897    fn test_chart_context() {
898        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
899
900        drawing_area.fill(&WHITE).expect("Fill");
901
902        let mut chart = ChartBuilder::on(&drawing_area)
903            .caption("Test Title", ("serif", 10))
904            .x_label_area_size(20)
905            .y_label_area_size(20)
906            .set_label_area_size(LabelAreaPosition::Top, 20)
907            .set_label_area_size(LabelAreaPosition::Right, 20)
908            .build_cartesian_2d(0..10, 0..10)
909            .expect("Create chart")
910            .set_secondary_coord(0.0..1.0, 0.0..1.0);
911
912        chart
913            .configure_mesh()
914            .x_desc("X")
915            .y_desc("Y")
916            .draw()
917            .expect("Draw mesh");
918        chart
919            .configure_secondary_axes()
920            .x_desc("X")
921            .y_desc("Y")
922            .draw()
923            .expect("Draw Secondary axes");
924
925        chart
926            .draw_series(std::iter::once(Circle::new((5, 5), 5, &RED)))
927            .expect("Drawing error");
928        chart
929            .draw_secondary_series(std::iter::once(Circle::new((0.3, 0.8), 5, &GREEN)))
930            .expect("Drawing error")
931            .label("Test label")
932            .legend(|(x, y)| Rectangle::new([(x - 10, y - 5), (x, y + 5)], &GREEN));
933
934        chart
935            .configure_series_labels()
936            .position(SeriesLabelPosition::UpperMiddle)
937            .draw()
938            .expect("Drawing error");
939    }
940
941    #[test]
942    fn test_chart_context_3d() {
943        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
944
945        drawing_area.fill(&WHITE).expect("Fill");
946
947        let mut chart = ChartBuilder::on(&drawing_area)
948            .caption("Test Title", ("serif", 10))
949            .x_label_area_size(20)
950            .y_label_area_size(20)
951            .set_label_area_size(LabelAreaPosition::Top, 20)
952            .set_label_area_size(LabelAreaPosition::Right, 20)
953            .build_cartesian_3d(0..10, 0..10, 0..10)
954            .expect("Create chart");
955
956        chart.with_projection(|mut pb| {
957            pb.yaw = 0.5;
958            pb.pitch = 0.5;
959            pb.scale = 0.5;
960            pb.into_matrix()
961        });
962
963        chart.configure_axes().draw().expect("Drawing axes");
964
965        chart
966            .draw_series(std::iter::once(Circle::new((5, 5, 5), 5, &RED)))
967            .expect("Drawing error");
968    }
969}