plotters/chart/
builder.rs

1use super::context::ChartContext;
2
3use crate::coord::cartesian::{Cartesian2d, Cartesian3d};
4use crate::coord::ranged1d::AsRangedCoord;
5use crate::coord::Shift;
6
7use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
8use crate::style::{IntoTextStyle, SizeDesc, TextStyle};
9
10use plotters_backend::DrawingBackend;
11
12/// The enum used to specify the position of label area.
13/// This is used when we configure the label area size with the API
14/// [ChartBuilder::set_label_area_size](struct ChartBuilder.html#method.set_label_area_size)
15#[derive(Copy, Clone)]
16pub enum LabelAreaPosition {
17    Top = 0,
18    Bottom = 1,
19    Left = 2,
20    Right = 3,
21}
22
23/// The helper object to create a chart context, which is used for the high-level figure drawing.
24/// With the help of this object, we can convert a basic drawing area into a chart context, which
25/// allows the high-level charting API being used on the drawing area.
26pub struct ChartBuilder<'a, 'b, DB: DrawingBackend> {
27    label_area_size: [u32; 4], // [upper, lower, left, right]
28    overlap_plotting_area: [bool; 4],
29    root_area: &'a DrawingArea<DB, Shift>,
30    title: Option<(String, TextStyle<'b>)>,
31    margin: [u32; 4],
32}
33
34impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> {
35    /// Create a chart builder on the given drawing area
36    /// - `root`: The root drawing area
37    /// - Returns: The chart builder object
38    pub fn on(root: &'a DrawingArea<DB, Shift>) -> Self {
39        Self {
40            label_area_size: [0; 4],
41            root_area: root,
42            title: None,
43            margin: [0; 4],
44            overlap_plotting_area: [false; 4],
45        }
46    }
47
48    /// Set the margin size of the chart (applied for top, bottom, left and right at the same time)
49    /// - `size`: The size of the chart margin.
50    pub fn margin<S: SizeDesc>(&mut self, size: S) -> &mut Self {
51        let size = size.in_pixels(self.root_area).max(0) as u32;
52        self.margin = [size, size, size, size];
53        self
54    }
55
56    /// Set the top margin of current chart
57    /// - `size`: The size of the top margin.
58    pub fn margin_top<S: SizeDesc>(&mut self, size: S) -> &mut Self {
59        let size = size.in_pixels(self.root_area).max(0) as u32;
60        self.margin[0] = size;
61        self
62    }
63
64    /// Set the bottom margin of current chart
65    /// - `size`: The size of the bottom margin.
66    pub fn margin_bottom<S: SizeDesc>(&mut self, size: S) -> &mut Self {
67        let size = size.in_pixels(self.root_area).max(0) as u32;
68        self.margin[1] = size;
69        self
70    }
71
72    /// Set the left margin of current chart
73    /// - `size`: The size of the left margin.
74    pub fn margin_left<S: SizeDesc>(&mut self, size: S) -> &mut Self {
75        let size = size.in_pixels(self.root_area).max(0) as u32;
76        self.margin[2] = size;
77        self
78    }
79
80    /// Set the right margin of current chart
81    /// - `size`: The size of the right margin.
82    pub fn margin_right<S: SizeDesc>(&mut self, size: S) -> &mut Self {
83        let size = size.in_pixels(self.root_area).max(0) as u32;
84        self.margin[3] = size;
85        self
86    }
87
88    /// Set all the label area size with the same value
89    pub fn set_all_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
90        let size = size.in_pixels(self.root_area);
91        self.set_label_area_size(LabelAreaPosition::Top, size)
92            .set_label_area_size(LabelAreaPosition::Bottom, size)
93            .set_label_area_size(LabelAreaPosition::Left, size)
94            .set_label_area_size(LabelAreaPosition::Right, size)
95    }
96
97    /// Set the most commonly used label area size to the same value
98    pub fn set_left_and_bottom_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
99        let size = size.in_pixels(self.root_area);
100        self.set_label_area_size(LabelAreaPosition::Left, size)
101            .set_label_area_size(LabelAreaPosition::Bottom, size)
102    }
103
104    /// Set the size of X label area
105    /// - `size`: The height of the x label area, if x is 0, the chart doesn't have the X label area
106    pub fn x_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
107        self.set_label_area_size(LabelAreaPosition::Bottom, size)
108    }
109
110    /// Set the size of the Y label area
111    /// - `size`: The width of the Y label area. If size is 0, the chart doesn't have Y label area
112    pub fn y_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
113        self.set_label_area_size(LabelAreaPosition::Left, size)
114    }
115
116    /// Set the size of X label area on the top of the chart
117    /// - `size`: The height of the x label area, if x is 0, the chart doesn't have the X label area
118    pub fn top_x_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
119        self.set_label_area_size(LabelAreaPosition::Top, size)
120    }
121
122    /// Set the size of the Y label area on the right side
123    /// - `size`: The width of the Y label area. If size is 0, the chart doesn't have Y label area
124    pub fn right_y_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
125        self.set_label_area_size(LabelAreaPosition::Right, size)
126    }
127
128    /// Set a label area size
129    /// - `pos`: THe position where the label area located
130    /// - `size`: The size of the label area size
131    pub fn set_label_area_size<S: SizeDesc>(
132        &mut self,
133        pos: LabelAreaPosition,
134        size: S,
135    ) -> &mut Self {
136        let size = size.in_pixels(self.root_area);
137        self.label_area_size[pos as usize] = size.abs() as u32;
138        self.overlap_plotting_area[pos as usize] = size < 0;
139        self
140    }
141
142    /// Set the caption of the chart
143    /// - `caption`: The caption of the chart
144    /// - `style`: The text style
145    /// - Note: If the caption is set, the margin option will be ignored
146    pub fn caption<S: AsRef<str>, Style: IntoTextStyle<'b>>(
147        &mut self,
148        caption: S,
149        style: Style,
150    ) -> &mut Self {
151        self.title = Some((
152            caption.as_ref().to_string(),
153            style.into_text_style(self.root_area),
154        ));
155        self
156    }
157
158    #[allow(clippy::type_complexity)]
159    #[deprecated(
160        note = "`build_ranged` has been renamed to `build_cartesian_2d` and is to be removed in the future."
161    )]
162    pub fn build_ranged<X: AsRangedCoord, Y: AsRangedCoord>(
163        &mut self,
164        x_spec: X,
165        y_spec: Y,
166    ) -> Result<
167        ChartContext<'a, DB, Cartesian2d<X::CoordDescType, Y::CoordDescType>>,
168        DrawingAreaErrorKind<DB::ErrorType>,
169    > {
170        self.build_cartesian_2d(x_spec, y_spec)
171    }
172
173    /// Build the chart with a 2D Cartesian coordinate system. The function will returns a chart
174    /// context, where data series can be rendered on.
175    /// - `x_spec`: The specification of X axis
176    /// - `y_spec`: The specification of Y axis
177    /// - Returns: A chart context
178    #[allow(clippy::type_complexity)]
179    pub fn build_cartesian_2d<X: AsRangedCoord, Y: AsRangedCoord>(
180        &mut self,
181        x_spec: X,
182        y_spec: Y,
183    ) -> Result<
184        ChartContext<'a, DB, Cartesian2d<X::CoordDescType, Y::CoordDescType>>,
185        DrawingAreaErrorKind<DB::ErrorType>,
186    > {
187        let mut label_areas = [None, None, None, None];
188
189        let mut drawing_area = DrawingArea::clone(self.root_area);
190
191        if *self.margin.iter().max().unwrap_or(&0) > 0 {
192            drawing_area = drawing_area.margin(
193                self.margin[0] as i32,
194                self.margin[1] as i32,
195                self.margin[2] as i32,
196                self.margin[3] as i32,
197            );
198        }
199
200        let (title_dx, title_dy) = if let Some((ref title, ref style)) = self.title {
201            let (origin_dx, origin_dy) = drawing_area.get_base_pixel();
202            drawing_area = drawing_area.titled(title, style.clone())?;
203            let (current_dx, current_dy) = drawing_area.get_base_pixel();
204            (current_dx - origin_dx, current_dy - origin_dy)
205        } else {
206            (0, 0)
207        };
208
209        let (w, h) = drawing_area.dim_in_pixel();
210
211        let mut actual_drawing_area_pos = [0, h as i32, 0, w as i32];
212
213        const DIR: [(i16, i16); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)];
214
215        for (idx, (dx, dy)) in (0..4).map(|idx| (idx, DIR[idx])) {
216            if self.overlap_plotting_area[idx] {
217                continue;
218            }
219
220            let size = self.label_area_size[idx] as i32;
221
222            let split_point = if dx + dy < 0 { size } else { -size };
223
224            actual_drawing_area_pos[idx] += split_point;
225        }
226
227        // Now the root drawing area is to be split into
228        //
229        // +----------+------------------------------+------+
230        // |    0     |    1 (Top Label Area)        |   2  |
231        // +----------+------------------------------+------+
232        // |    3     |                              |   5  |
233        // |  Left    |       4 (Plotting Area)      | Right|
234        // |  Labels  |                              | Label|
235        // +----------+------------------------------+------+
236        // |    6     |        7 (Bottom Labels)     |   8  |
237        // +----------+------------------------------+------+
238
239        let mut split: Vec<_> = drawing_area
240            .split_by_breakpoints(
241                &actual_drawing_area_pos[2..4],
242                &actual_drawing_area_pos[0..2],
243            )
244            .into_iter()
245            .map(Some)
246            .collect();
247
248        // Take out the plotting area
249        std::mem::swap(&mut drawing_area, split[4].as_mut().unwrap());
250
251        // Initialize the label areas - since the label area might be overlapping
252        // with the plotting area, in this case, we need handle them differently
253        for (src_idx, dst_idx) in [1, 7, 3, 5].iter().zip(0..4) {
254            if !self.overlap_plotting_area[dst_idx] {
255                let (h, w) = split[*src_idx].as_ref().unwrap().dim_in_pixel();
256                if h > 0 && w > 0 {
257                    std::mem::swap(&mut label_areas[dst_idx], &mut split[*src_idx]);
258                }
259            } else if self.label_area_size[dst_idx] != 0 {
260                let size = self.label_area_size[dst_idx] as i32;
261                let (dw, dh) = drawing_area.dim_in_pixel();
262                let x0 = if DIR[dst_idx].0 > 0 {
263                    dw as i32 - size
264                } else {
265                    0
266                };
267                let y0 = if DIR[dst_idx].1 > 0 {
268                    dh as i32 - size
269                } else {
270                    0
271                };
272                let x1 = if DIR[dst_idx].0 >= 0 { dw as i32 } else { size };
273                let y1 = if DIR[dst_idx].1 >= 0 { dh as i32 } else { size };
274
275                label_areas[dst_idx] = Some(
276                    drawing_area
277                        .clone()
278                        .shrink((x0, y0), ((x1 - x0), (y1 - y0))),
279                );
280            }
281        }
282
283        let mut pixel_range = drawing_area.get_pixel_range();
284        pixel_range.1 = (pixel_range.1.end - 1)..(pixel_range.1.start - 1);
285
286        let mut x_label_area = [None, None];
287        let mut y_label_area = [None, None];
288
289        std::mem::swap(&mut x_label_area[0], &mut label_areas[0]);
290        std::mem::swap(&mut x_label_area[1], &mut label_areas[1]);
291        std::mem::swap(&mut y_label_area[0], &mut label_areas[2]);
292        std::mem::swap(&mut y_label_area[1], &mut label_areas[3]);
293
294        Ok(ChartContext {
295            x_label_area,
296            y_label_area,
297            drawing_area: drawing_area.apply_coord_spec(Cartesian2d::new(
298                x_spec,
299                y_spec,
300                pixel_range,
301            )),
302            series_anno: vec![],
303            drawing_area_pos: (
304                actual_drawing_area_pos[2] + title_dx + self.margin[2] as i32,
305                actual_drawing_area_pos[0] + title_dy + self.margin[0] as i32,
306            ),
307        })
308    }
309
310    /// Build a 3 dimensional cartesian chart. The function will returns a chart
311    /// context, where data series can be rendered on.
312    /// - `x_spec`: The specification of X axis
313    /// - `y_spec`: The specification of Y axis
314    /// - `z_sepc`: The specification of Z axis
315    /// - Returns: A chart context
316    pub fn build_cartesian_3d<X: AsRangedCoord, Y: AsRangedCoord, Z: AsRangedCoord>(
317        &mut self,
318        x_spec: X,
319        y_spec: Y,
320        z_spec: Z,
321    ) -> Result<
322        ChartContext<'a, DB, Cartesian3d<X::CoordDescType, Y::CoordDescType, Z::CoordDescType>>,
323        DrawingAreaErrorKind<DB::ErrorType>,
324    > {
325        let mut drawing_area = DrawingArea::clone(self.root_area);
326
327        if *self.margin.iter().max().unwrap_or(&0) > 0 {
328            drawing_area = drawing_area.margin(
329                self.margin[0] as i32,
330                self.margin[1] as i32,
331                self.margin[2] as i32,
332                self.margin[3] as i32,
333            );
334        }
335
336        let (title_dx, title_dy) = if let Some((ref title, ref style)) = self.title {
337            let (origin_dx, origin_dy) = drawing_area.get_base_pixel();
338            drawing_area = drawing_area.titled(title, style.clone())?;
339            let (current_dx, current_dy) = drawing_area.get_base_pixel();
340            (current_dx - origin_dx, current_dy - origin_dy)
341        } else {
342            (0, 0)
343        };
344
345        let pixel_range = drawing_area.get_pixel_range();
346
347        Ok(ChartContext {
348            x_label_area: [None, None],
349            y_label_area: [None, None],
350            drawing_area: drawing_area.apply_coord_spec(Cartesian3d::new(
351                x_spec,
352                y_spec,
353                z_spec,
354                pixel_range,
355            )),
356            series_anno: vec![],
357            drawing_area_pos: (
358                title_dx + self.margin[2] as i32,
359                title_dy + self.margin[0] as i32,
360            ),
361        })
362    }
363}
364
365#[cfg(test)]
366mod test {
367    use super::*;
368    use crate::prelude::*;
369    #[test]
370    fn test_label_area_size() {
371        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
372        let mut chart = ChartBuilder::on(&drawing_area);
373
374        chart
375            .x_label_area_size(10)
376            .y_label_area_size(20)
377            .top_x_label_area_size(30)
378            .right_y_label_area_size(40);
379        assert_eq!(chart.label_area_size[1], 10);
380        assert_eq!(chart.label_area_size[2], 20);
381        assert_eq!(chart.label_area_size[0], 30);
382        assert_eq!(chart.label_area_size[3], 40);
383
384        chart.set_label_area_size(LabelAreaPosition::Left, 100);
385        chart.set_label_area_size(LabelAreaPosition::Right, 200);
386        chart.set_label_area_size(LabelAreaPosition::Top, 300);
387        chart.set_label_area_size(LabelAreaPosition::Bottom, 400);
388
389        assert_eq!(chart.label_area_size[0], 300);
390        assert_eq!(chart.label_area_size[1], 400);
391        assert_eq!(chart.label_area_size[2], 100);
392        assert_eq!(chart.label_area_size[3], 200);
393    }
394
395    #[test]
396    fn test_margin_configure() {
397        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
398        let mut chart = ChartBuilder::on(&drawing_area);
399
400        chart.margin(5);
401        assert_eq!(chart.margin[0], 5);
402        assert_eq!(chart.margin[1], 5);
403        assert_eq!(chart.margin[2], 5);
404        assert_eq!(chart.margin[3], 5);
405
406        chart.margin_top(10);
407        chart.margin_bottom(11);
408        chart.margin_left(12);
409        chart.margin_right(13);
410        assert_eq!(chart.margin[0], 10);
411        assert_eq!(chart.margin[1], 11);
412        assert_eq!(chart.margin[2], 12);
413        assert_eq!(chart.margin[3], 13);
414    }
415
416    #[test]
417    fn test_caption() {
418        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
419        let mut chart = ChartBuilder::on(&drawing_area);
420
421        chart.caption("This is a test case", ("serif", 10));
422
423        assert_eq!(chart.title.as_ref().unwrap().0, "This is a test case");
424        assert_eq!(chart.title.as_ref().unwrap().1.font.get_name(), "serif");
425        assert_eq!(chart.title.as_ref().unwrap().1.font.get_size(), 10.0);
426        check_color(chart.title.as_ref().unwrap().1.color, BLACK.to_rgba());
427
428        chart.caption("This is a test case", ("serif", 10));
429        assert_eq!(chart.title.as_ref().unwrap().1.font.get_name(), "serif");
430    }
431}