plotchart/chart/
context.rs

1use std::borrow::Borrow;
2use std::fmt::Debug;
3use std::marker::PhantomData;
4use std::ops::Range;
5use std::sync::Arc;
6
7use super::dual_coord::DualCoordChartContext;
8use super::mesh::MeshStyle;
9use super::series::SeriesLabelStyle;
10
11use crate::coord::{
12    AsRangedCoord, CoordTranslate, MeshLine, Ranged, RangedCoord, ReverseCoordTranslate, Shift,
13};
14use crate::drawing::backend::{BackendCoord, DrawingBackend};
15use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
16use crate::element::{Drawable, DynElement, IntoDynElement, PathElement, PointCollection};
17use crate::style::text_anchor::{HPos, Pos, VPos};
18use crate::style::{AsRelative, FontTransform, ShapeStyle, SizeDesc, TextStyle};
19
20/// The annotations (such as the label of the series, the legend element, etc)
21/// When a series is drawn onto a drawing area, an series annotation object
22/// is created and a mutable reference is returned.
23#[allow(clippy::type_complexity)]
24pub struct SeriesAnno<'a, DB: DrawingBackend> {
25    label: Option<String>,
26    draw_func: Option<Box<dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord> + 'a>>,
27    phantom_data: PhantomData<DB>,
28}
29
30impl<'a, DB: DrawingBackend> SeriesAnno<'a, DB> {
31    pub(crate) fn get_label(&self) -> &str {
32        self.label.as_ref().map(|x| x.as_str()).unwrap_or("")
33    }
34
35    pub(crate) fn get_draw_func(
36        &self,
37    ) -> Option<&dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord>> {
38        self.draw_func.as_ref().map(|x| x.borrow())
39    }
40
41    fn new() -> Self {
42        Self {
43            label: None,
44            draw_func: None,
45            phantom_data: PhantomData,
46        }
47    }
48
49    /// Set the series label
50    /// - `label`: The string would be use as label for current series
51    pub fn label<L: Into<String>>(&mut self, label: L) -> &mut Self {
52        self.label = Some(label.into());
53        self
54    }
55
56    /// Set the legend element creator function
57    /// - `func`: The function use to create the element
58    /// *Note*: The creation function uses a shifted pixel-based coordinate system. And place the
59    /// point (0,0) to the mid-right point of the shape
60    pub fn legend<E: IntoDynElement<'a, DB, BackendCoord>, T: Fn(BackendCoord) -> E + 'a>(
61        &mut self,
62        func: T,
63    ) -> &mut Self {
64        self.draw_func = Some(Box::new(move |p| func(p).into_dyn()));
65        self
66    }
67}
68
69/// The context of the chart. This is the core object of Plotters.
70/// Any plot/chart is abstracted as this type, and any data series can be placed to the chart
71/// context.
72pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
73    pub(super) x_label_area: [Option<DrawingArea<DB, Shift>>; 2],
74    pub(super) y_label_area: [Option<DrawingArea<DB, Shift>>; 2],
75    pub(super) drawing_area: DrawingArea<DB, CT>,
76    pub(super) series_anno: Vec<SeriesAnno<'a, DB>>,
77    pub(super) drawing_area_pos: (i32, i32),
78}
79
80/// A chart context state - This is the data that is needed to reconstruct the chart context
81/// without actually drawing the chart. This is useful when we want to do realtime rendering and
82/// want to incrementally update the chart.
83///
84/// For each frame, instead of updating the entire backend, we are able to keep the keep the figure
85/// component like axis, labels untouched and make updates only in the plotting drawing area.
86pub struct ChartState<CT: CoordTranslate> {
87    drawing_area_pos: (i32, i32),
88    drawing_area_size: (u32, u32),
89    coord: CT,
90}
91
92impl<'a, CT: CoordTranslate + Clone> Clone for ChartState<CT> {
93    fn clone(&self) -> Self {
94        Self {
95            drawing_area_size: self.drawing_area_size,
96            drawing_area_pos: self.drawing_area_pos,
97            coord: self.coord.clone(),
98        }
99    }
100}
101
102impl<'a, DB: DrawingBackend, CT: CoordTranslate> From<ChartContext<'a, DB, CT>> for ChartState<CT> {
103    fn from(chart: ChartContext<'a, DB, CT>) -> ChartState<CT> {
104        ChartState {
105            drawing_area_pos: chart.drawing_area_pos,
106            drawing_area_size: chart.drawing_area.dim_in_pixel(),
107            coord: chart.drawing_area.into_coord_spec(),
108        }
109    }
110}
111
112impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
113    /// Convert a chart context into a chart state, by doing so, the chart context is consumed and
114    /// a saved chart state is created for later use.
115    pub fn into_chart_state(self) -> ChartState<CT> {
116        self.into()
117    }
118
119    /// Convert the chart context into a sharable chart state.
120    /// Normally a chart state can not be clone, since the coordinate spec may not be able to be
121    /// cloned. In this case, we can use an `Arc` get the coordinate wrapped thus the state can be
122    /// cloned and shared by multiple chart context
123    pub fn into_shared_chart_state(self) -> ChartState<Arc<CT>> {
124        ChartState {
125            drawing_area_pos: self.drawing_area_pos,
126            drawing_area_size: self.drawing_area.dim_in_pixel(),
127            coord: Arc::new(self.drawing_area.into_coord_spec()),
128        }
129    }
130}
131
132impl<'a, 'b, DB, CT> From<&ChartContext<'a, DB, CT>> for ChartState<CT>
133where
134    DB: DrawingBackend,
135    CT: CoordTranslate + Clone,
136{
137    fn from(chart: &ChartContext<'a, DB, CT>) -> ChartState<CT> {
138        ChartState {
139            drawing_area_pos: chart.drawing_area_pos,
140            drawing_area_size: chart.drawing_area.dim_in_pixel(),
141            coord: chart.drawing_area.as_coord_spec().clone(),
142        }
143    }
144}
145
146impl<'a, DB: DrawingBackend, CT: CoordTranslate + Clone> ChartContext<'a, DB, CT> {
147    /// Make the chart context, do not consume the chart context and clone the coordinate spec
148    pub fn to_chart_state(&self) -> ChartState<CT> {
149        self.into()
150    }
151}
152
153impl<CT: CoordTranslate> ChartState<CT> {
154    /// Restore the chart context on the given drawing area
155    ///
156    /// - `area`: The given drawing area where we want to restore the chart context
157    /// - **returns** The newly created chart context
158    pub fn restore<'a, DB: DrawingBackend>(
159        self,
160        area: &DrawingArea<DB, Shift>,
161    ) -> ChartContext<'a, DB, CT> {
162        let area = area
163            .clone()
164            .shrink(self.drawing_area_pos, self.drawing_area_size);
165        ChartContext {
166            x_label_area: [None, None],
167            y_label_area: [None, None],
168            drawing_area: area.apply_coord_spec(self.coord),
169            series_anno: vec![],
170            drawing_area_pos: self.drawing_area_pos,
171        }
172    }
173}
174
175impl<
176        'a,
177        DB: DrawingBackend,
178        XT: Debug,
179        YT: Debug,
180        X: Ranged<ValueType = XT>,
181        Y: Ranged<ValueType = YT>,
182    > ChartContext<'a, DB, RangedCoord<X, Y>>
183{
184    fn is_overlapping_drawing_area(&self, area: Option<&DrawingArea<DB, Shift>>) -> bool {
185        if let Some(area) = area {
186            let (x0, y0) = area.get_base_pixel();
187            let (w, h) = area.dim_in_pixel();
188            let (x1, y1) = (x0 + w as i32, y0 + h as i32);
189            let (dx0, dy0) = self.drawing_area.get_base_pixel();
190            let (w, h) = self.drawing_area.dim_in_pixel();
191            let (dx1, dy1) = (dx0 + w as i32, dy0 + h as i32);
192
193            let (ox0, ox1) = (x0.max(dx0), x1.min(dx1));
194            let (oy0, oy1) = (y0.max(dy0), y1.min(dy1));
195
196            ox1 > ox0 && oy1 > oy0
197        } else {
198            false
199        }
200    }
201
202    /// Initialize a mesh configuration object and mesh drawing can be finalized by calling
203    /// the function `MeshStyle::draw`
204    pub fn configure_mesh<'b>(&'b mut self) -> MeshStyle<'a, 'b, X, Y, DB> {
205        let base_tick_size = (5u32).percent().max(5).in_pixels(&self.drawing_area);
206
207        let mut x_tick_size = [base_tick_size, base_tick_size];
208        let mut y_tick_size = [base_tick_size, base_tick_size];
209
210        for idx in 0..2 {
211            if self.is_overlapping_drawing_area(self.x_label_area[idx].as_ref()) {
212                x_tick_size[idx] = -x_tick_size[idx];
213            }
214            if self.is_overlapping_drawing_area(self.y_label_area[idx].as_ref()) {
215                y_tick_size[idx] = -y_tick_size[idx];
216            }
217        }
218
219        MeshStyle {
220            parent_size: self.drawing_area.dim_in_pixel(),
221            axis_style: None,
222            x_label_offset: 0,
223            y_label_offset: 0,
224            draw_x_mesh: true,
225            draw_y_mesh: true,
226            draw_x_axis: true,
227            draw_y_axis: true,
228            n_x_labels: 10,
229            n_y_labels: 10,
230            line_style_1: None,
231            line_style_2: None,
232            x_label_style: None,
233            y_label_style: None,
234            format_x: &|x| format!("{:?}", x),
235            format_y: &|y| format!("{:?}", y),
236            target: Some(self),
237            _phantom_data: PhantomData,
238            x_desc: None,
239            y_desc: None,
240            axis_desc_style: None,
241            x_tick_size,
242            y_tick_size,
243        }
244    }
245}
246
247impl<'a, DB: DrawingBackend + 'a, CT: CoordTranslate> ChartContext<'a, DB, CT> {
248    /// Configure the styles for drawing series labels in the chart
249    pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT> {
250        SeriesLabelStyle::new(self)
251    }
252
253    /// Get a reference of underlying plotting area
254    pub fn plotting_area(&self) -> &DrawingArea<DB, CT> {
255        &self.drawing_area
256    }
257}
258
259impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
260    pub fn as_coord_spec(&self) -> &CT {
261        self.drawing_area.as_coord_spec()
262    }
263}
264
265impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
266    /// Convert the chart context into an closure that can be used for coordinate translation
267    pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
268        let coord_spec = self.drawing_area.into_coord_spec();
269        move |coord| coord_spec.reverse_translate(coord)
270    }
271}
272
273impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Arc<RangedCoord<X, Y>>> {
274    pub(super) fn draw_series_impl<E, R, S>(
275        &mut self,
276        series: S,
277    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
278    where
279        for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
280        E: Drawable<DB>,
281        R: Borrow<E>,
282        S: IntoIterator<Item = R>,
283    {
284        for element in series {
285            self.drawing_area.draw(element.borrow())?;
286        }
287        Ok(())
288    }
289
290    pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
291        let idx = self.series_anno.len();
292        self.series_anno.push(SeriesAnno::new());
293        &mut self.series_anno[idx]
294    }
295
296    /// Draw a data series. A data series in Plotters is abstracted as an iterator of elements
297    pub fn draw_series<E, R, S>(
298        &mut self,
299        series: S,
300    ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
301    where
302        for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
303        E: Drawable<DB>,
304        R: Borrow<E>,
305        S: IntoIterator<Item = R>,
306    {
307        self.draw_series_impl(series)?;
308        Ok(self.alloc_series_anno())
309    }
310}
311
312impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, RangedCoord<X, Y>> {
313    /// Get the range of X axis
314    pub fn x_range(&self) -> Range<X::ValueType> {
315        self.drawing_area.get_x_range()
316    }
317
318    /// Get range of the Y axis
319    pub fn y_range(&self) -> Range<Y::ValueType> {
320        self.drawing_area.get_y_range()
321    }
322
323    /// Maps the coordinate to the backend coordinate. This is typically used
324    /// with an interactive chart.
325    pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord {
326        self.drawing_area.map_coordinate(coord)
327    }
328
329    pub(super) fn draw_series_impl<E, R, S>(
330        &mut self,
331        series: S,
332    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
333    where
334        for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
335        E: Drawable<DB>,
336        R: Borrow<E>,
337        S: IntoIterator<Item = R>,
338    {
339        for element in series {
340            self.drawing_area.draw(element.borrow())?;
341        }
342        Ok(())
343    }
344
345    pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
346        let idx = self.series_anno.len();
347        self.series_anno.push(SeriesAnno::new());
348        &mut self.series_anno[idx]
349    }
350
351    /// Draw a data series. A data series in Plotters is abstracted as an iterator of elements
352    pub fn draw_series<E, R, S>(
353        &mut self,
354        series: S,
355    ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
356    where
357        for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
358        E: Drawable<DB>,
359        R: Borrow<E>,
360        S: IntoIterator<Item = R>,
361    {
362        self.draw_series_impl(series)?;
363        Ok(self.alloc_series_anno())
364    }
365
366    /// The actual function that draws the mesh lines.
367    /// It also returns the label that suppose to be there.
368    #[allow(clippy::type_complexity)]
369    fn draw_mesh_lines<FmtLabel>(
370        &mut self,
371        (r, c): (usize, usize),
372        (x_mesh, y_mesh): (bool, bool),
373        mesh_line_style: &ShapeStyle,
374        mut fmt_label: FmtLabel,
375    ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>>
376    where
377        FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
378    {
379        let mut x_labels = vec![];
380        let mut y_labels = vec![];
381        self.drawing_area.draw_mesh(
382            |b, l| {
383                let draw;
384                match l {
385                    MeshLine::XMesh((x, _), _, _) => {
386                        if let Some(label_text) = fmt_label(&l) {
387                            x_labels.push((x, label_text));
388                        }
389                        draw = x_mesh;
390                    }
391                    MeshLine::YMesh((_, y), _, _) => {
392                        if let Some(label_text) = fmt_label(&l) {
393                            y_labels.push((y, label_text));
394                        }
395                        draw = y_mesh;
396                    }
397                };
398                if draw {
399                    l.draw(b, mesh_line_style)
400                } else {
401                    Ok(())
402                }
403            },
404            r,
405            c,
406        )?;
407        Ok((x_labels, y_labels))
408    }
409
410    fn draw_axis(
411        &self,
412        area: &DrawingArea<DB, Shift>,
413        axis_style: Option<&ShapeStyle>,
414        orientation: (i16, i16),
415        inward_labels: bool,
416    ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> {
417        let (x0, y0) = self.drawing_area.get_base_pixel();
418        let (tw, th) = area.dim_in_pixel();
419
420        let mut axis_range = if orientation.0 == 0 {
421            self.drawing_area.get_x_axis_pixel_range()
422        } else {
423            self.drawing_area.get_y_axis_pixel_range()
424        };
425
426        /* At this point, the coordinate system tells us the pixel range
427         * after the translation.
428         * However, we need to use the logic coordinate system for drawing. */
429        if orientation.0 == 0 {
430            axis_range.start -= x0;
431            axis_range.end -= x0;
432        } else {
433            axis_range.start -= y0;
434            axis_range.end -= y0;
435        }
436
437        if let Some(axis_style) = axis_style {
438            let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 };
439            let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 };
440            let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 };
441            let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 };
442
443            if inward_labels {
444                if orientation.0 == 0 {
445                    if y0 == 0 {
446                        y0 = th as i32 - 1;
447                        y1 = th as i32 - 1;
448                    } else {
449                        y0 = 0;
450                        y1 = 0;
451                    }
452                } else if x0 == 0 {
453                    x0 = tw as i32 - 1;
454                    x1 = tw as i32 - 1;
455                } else {
456                    x0 = 0;
457                    x1 = 0;
458                }
459            }
460
461            if orientation.0 == 0 {
462                x0 = axis_range.start;
463                x1 = axis_range.end;
464            } else {
465                y0 = axis_range.start;
466                y1 = axis_range.end;
467            }
468
469            area.draw(&PathElement::new(
470                vec![(x0, y0), (x1, y1)],
471                axis_style.clone(),
472            ))?;
473        }
474
475        Ok(axis_range)
476    }
477
478    // TODO: consider make this function less complicated
479    #[allow(clippy::too_many_arguments)]
480    #[allow(clippy::cognitive_complexity)]
481    fn draw_axis_and_labels(
482        &self,
483        area: Option<&DrawingArea<DB, Shift>>,
484        axis_style: Option<&ShapeStyle>,
485        labels: &[(i32, String)],
486        label_style: &TextStyle,
487        label_offset: i32,
488        orientation: (i16, i16),
489        axis_desc: Option<(&str, &TextStyle)>,
490        tick_size: i32,
491    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
492        let area = if let Some(target) = area {
493            target
494        } else {
495            return Ok(());
496        };
497
498        let (x0, y0) = self.drawing_area.get_base_pixel();
499        let (tw, th) = area.dim_in_pixel();
500
501        /* This is the minimal distance from the axis to the box of the labels */
502        let label_dist = tick_size.abs() * 2;
503
504        /* Draw the axis and get the axis range so that we can do further label
505         * and tick mark drawing */
506        let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?;
507
508        /* To make the right label area looks nice, it's a little bit tricky, since for a that is
509         * very long, we actually prefer left alignment instead of right alignment.
510         * Otherwise, the right alignment looks better. So we estimate the max and min label width
511         * So that we are able decide if we should apply right alignment for the text. */
512        let label_width: Vec<_> = labels
513            .iter()
514            .map(|(_, text)| {
515                if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 {
516                    let ((x0, _), (x1, _)) = label_style
517                        .font
518                        .layout_box(text)
519                        .unwrap_or(((0, 0), (0, 0)));
520                    x1 - x0
521                } else {
522                    // Don't ever do the layout estimationfor the drawing area that is either not
523                    // the right one or the tick mark is inward.
524                    0
525                }
526            })
527            .collect();
528
529        let min_width = *label_width.iter().min().unwrap_or(&1).max(&1);
530        let max_width = *label_width
531            .iter()
532            .filter(|&&x| x < min_width * 2)
533            .max()
534            .unwrap_or(&min_width);
535        let right_align_width = (min_width * 2).min(max_width);
536
537        /* Then we need to draw the tick mark and the label */
538        for ((p, t), w) in labels.iter().zip(label_width.into_iter()) {
539            /* Make sure we are actually in the visible range */
540            let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 };
541
542            if rp < axis_range.start.min(axis_range.end)
543                || axis_range.end.max(axis_range.start) < rp
544            {
545                continue;
546            }
547
548            let (cx, cy, h_pos, v_pos) = if tick_size >= 0 {
549                match orientation {
550                    // Right
551                    (dx, dy) if dx > 0 && dy == 0 => {
552                        if w >= right_align_width {
553                            (label_dist, *p - y0, HPos::Left, VPos::Center)
554                        } else {
555                            (
556                                label_dist + right_align_width,
557                                *p - y0,
558                                HPos::Right,
559                                VPos::Center,
560                            )
561                        }
562                    }
563                    // Left
564                    (dx, dy) if dx < 0 && dy == 0 => {
565                        (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
566                    }
567                    // Bottom
568                    (dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
569                    // Top
570                    (dx, dy) if dx == 0 && dy < 0 => {
571                        (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
572                    }
573                    _ => panic!("Bug: Invalid orientation specification"),
574                }
575            } else {
576                match orientation {
577                    // Right
578                    (dx, dy) if dx > 0 && dy == 0 => {
579                        (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
580                    }
581                    // Left
582                    (dx, dy) if dx < 0 && dy == 0 => {
583                        (label_dist, *p - y0, HPos::Left, VPos::Center)
584                    }
585                    // Bottom
586                    (dx, dy) if dx == 0 && dy > 0 => {
587                        (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
588                    }
589                    // Top
590                    (dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
591                    _ => panic!("Bug: Invalid orientation specification"),
592                }
593            };
594
595            let (text_x, text_y) = if orientation.0 == 0 {
596                (cx + label_offset, cy)
597            } else {
598                (cx, cy + label_offset)
599            };
600
601            let label_style = &label_style.pos(Pos::new(h_pos, v_pos));
602            area.draw_text(&t, label_style, (text_x, text_y))?;
603
604            if tick_size != 0 {
605                if let Some(style) = axis_style {
606                    let xmax = tw as i32 - 1;
607                    let ymax = th as i32 - 1;
608                    let (kx0, ky0, kx1, ky1) = if tick_size > 0 {
609                        match orientation {
610                            (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0),
611                            (dx, dy) if dx < 0 && dy == 0 => {
612                                (xmax - tick_size, *p - y0, xmax, *p - y0)
613                            }
614                            (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size),
615                            (dx, dy) if dx == 0 && dy < 0 => {
616                                (*p - x0, ymax - tick_size, *p - x0, ymax)
617                            }
618                            _ => panic!("Bug: Invalid orientation specification"),
619                        }
620                    } else {
621                        match orientation {
622                            (dx, dy) if dx > 0 && dy == 0 => {
623                                (xmax, *p - y0, xmax + tick_size, *p - y0)
624                            }
625                            (dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0),
626                            (dx, dy) if dx == 0 && dy > 0 => {
627                                (*p - x0, ymax, *p - x0, ymax + tick_size)
628                            }
629                            (dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size),
630                            _ => panic!("Bug: Invalid orientation specification"),
631                        }
632                    };
633                    let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], style.clone());
634                    area.draw(&line)?;
635                }
636            }
637        }
638
639        if let Some((text, style)) = axis_desc {
640            let actual_style = if orientation.0 == 0 {
641                style.clone()
642            } else if orientation.0 == -1 {
643                style.transform(FontTransform::Rotate270)
644            } else {
645                style.transform(FontTransform::Rotate90)
646            };
647
648            let (x0, y0, h_pos, v_pos) = match orientation {
649                // Right
650                (dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top),
651                // Left
652                (dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top),
653                // Bottom
654                (dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom),
655                // Top
656                (dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top),
657                _ => panic!("Bug: Invalid orientation specification"),
658            };
659
660            let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos));
661            area.draw_text(&text, &actual_style, (x0 as i32, y0 as i32))?;
662        }
663
664        Ok(())
665    }
666
667    #[allow(clippy::too_many_arguments)]
668    pub(super) fn draw_mesh<FmtLabel>(
669        &mut self,
670        (r, c): (usize, usize),
671        mesh_line_style: &ShapeStyle,
672        x_label_style: &TextStyle,
673        y_label_style: &TextStyle,
674        fmt_label: FmtLabel,
675        x_mesh: bool,
676        y_mesh: bool,
677        x_label_offset: i32,
678        y_label_offset: i32,
679        x_axis: bool,
680        y_axis: bool,
681        axis_style: &ShapeStyle,
682        axis_desc_style: &TextStyle,
683        x_desc: Option<String>,
684        y_desc: Option<String>,
685        x_tick_size: [i32; 2],
686        y_tick_size: [i32; 2],
687    ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
688    where
689        FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
690    {
691        let (x_labels, y_labels) =
692            self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?;
693
694        for idx in 0..2 {
695            self.draw_axis_and_labels(
696                self.x_label_area[idx].as_ref(),
697                if x_axis { Some(axis_style) } else { None },
698                &x_labels[..],
699                x_label_style,
700                x_label_offset,
701                (0, -1 + idx as i16 * 2),
702                x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
703                x_tick_size[idx],
704            )?;
705
706            self.draw_axis_and_labels(
707                self.y_label_area[idx].as_ref(),
708                if y_axis { Some(axis_style) } else { None },
709                &y_labels[..],
710                y_label_style,
711                y_label_offset,
712                (-1 + idx as i16 * 2, 0),
713                y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
714                y_tick_size[idx],
715            )?;
716        }
717
718        Ok(())
719    }
720
721    /// Convert this chart context into a dual axis chart context
722    ///
723    /// - `x_coord`: The coordinate spec for the X axis
724    /// - `y_coord`: The coordinate spec for the Y axis
725    /// - **returns** The newly created dual spec chart context
726    #[allow(clippy::type_complexity)]
727    pub fn set_secondary_coord<SX: AsRangedCoord, SY: AsRangedCoord>(
728        self,
729        x_coord: SX,
730        y_coord: SY,
731    ) -> DualCoordChartContext<
732        'a,
733        DB,
734        RangedCoord<X, Y>,
735        RangedCoord<SX::CoordDescType, SY::CoordDescType>,
736    > {
737        let mut pixel_range = self.drawing_area.get_pixel_range();
738        pixel_range.1 = pixel_range.1.end..pixel_range.1.start;
739
740        DualCoordChartContext::new(self, RangedCoord::new(x_coord, y_coord, pixel_range))
741    }
742}
743
744#[cfg(test)]
745mod test {
746    use crate::prelude::*;
747
748    #[test]
749    fn test_chart_context() {
750        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
751
752        drawing_area.fill(&WHITE).expect("Fill");
753
754        let mut chart = ChartBuilder::on(&drawing_area)
755            .caption("Test Title", ("serif", 10))
756            .x_label_area_size(20)
757            .y_label_area_size(20)
758            .set_label_area_size(LabelAreaPosition::Top, 20)
759            .set_label_area_size(LabelAreaPosition::Right, 20)
760            .build_ranged(0..10, 0..10)
761            .expect("Create chart")
762            .set_secondary_coord(0.0..1.0, 0.0..1.0);
763
764        chart
765            .configure_mesh()
766            .x_desc("X")
767            .y_desc("Y")
768            .draw()
769            .expect("Draw mesh");
770        chart
771            .configure_secondary_axes()
772            .x_desc("X")
773            .y_desc("Y")
774            .draw()
775            .expect("Draw Secondary axes");
776
777        chart
778            .draw_series(std::iter::once(Circle::new((5, 5), 5, &RED)))
779            .expect("Drawing error");
780        chart
781            .draw_secondary_series(std::iter::once(Circle::new((0.3, 0.8), 5, &GREEN)))
782            .expect("Drawing error")
783            .label("Test label")
784            .legend(|(x, y)| Rectangle::new([(x - 10, y - 5), (x, y + 5)], &GREEN));
785
786        chart
787            .configure_series_labels()
788            .position(SeriesLabelPosition::UpperMiddle)
789            .draw()
790            .expect("Drawing error");
791    }
792}