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/**
13Specifies one of the four label positions around the figure.
14
15This is used to configure the label area size with function
16[`ChartBuilder::set_label_area_size()`].
17
18# Example
19
20```
21use plotters::prelude::*;
22let drawing_area = SVGBackend::new("label_area_position.svg", (300, 200)).into_drawing_area();
23drawing_area.fill(&WHITE).unwrap();
24let mut chart_builder = ChartBuilder::on(&drawing_area);
25chart_builder.set_label_area_size(LabelAreaPosition::Bottom, 60).set_label_area_size(LabelAreaPosition::Left, 35);
26let mut chart_context = chart_builder.build_cartesian_2d(0.0..4.0, 0.0..3.0).unwrap();
27chart_context.configure_mesh().x_desc("Spacious X label area").y_desc("Narrow Y label area").draw().unwrap();
28```
29
30The result is a chart with a spacious X label area and a narrow Y label area:
31
32![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@9ca6541/apidoc/label_area_position.svg)
33
34# See also
35
36[`ChartBuilder::set_left_and_bottom_label_area_size()`]
37*/
38#[derive(Copy, Clone)]
39pub enum LabelAreaPosition {
40    /// Top of the figure
41    Top = 0,
42    /// Bottom of the figure
43    Bottom = 1,
44    /// Left side of the figure
45    Left = 2,
46    /// Right side of the figure
47    Right = 3,
48}
49
50/**
51The helper object to create a chart context, which is used for the high-level figure drawing.
52
53With the help of this object, we can convert a basic drawing area into a chart context, which
54allows the high-level charting API being used on the drawing area.
55
56See [`ChartBuilder::on()`] for more information and examples.
57*/
58pub struct ChartBuilder<'a, 'b, DB: DrawingBackend> {
59    label_area_size: [u32; 4], // [upper, lower, left, right]
60    overlap_plotting_area: [bool; 4],
61    root_area: &'a DrawingArea<DB, Shift>,
62    title: Option<(String, TextStyle<'b>)>,
63    margin: [u32; 4],
64}
65
66impl<'a, 'b, DB: DrawingBackend> ChartBuilder<'a, 'b, DB> {
67    /**
68    Create a chart builder on the given drawing area
69
70    - `root`: The root drawing area
71    - Returns: The chart builder object
72
73    # Example
74
75    ```
76    use plotters::prelude::*;
77    let drawing_area = SVGBackend::new("chart_builder_on.svg", (300, 200)).into_drawing_area();
78    drawing_area.fill(&WHITE).unwrap();
79    let mut chart_builder = ChartBuilder::on(&drawing_area);
80    chart_builder.margin(5).set_left_and_bottom_label_area_size(35)
81    .caption("Figure title or caption", ("Calibri", 20, FontStyle::Italic, &RED).into_text_style(&drawing_area));
82    let mut chart_context = chart_builder.build_cartesian_2d(0.0..3.8, 0.0..2.8).unwrap();
83    chart_context.configure_mesh().draw().unwrap();
84    ```
85    The result is a chart with customized margins, label area sizes, and title:
86
87    ![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@42ecf52/apidoc/chart_builder_on.svg)
88
89    */
90    pub fn on(root: &'a DrawingArea<DB, Shift>) -> Self {
91        Self {
92            label_area_size: [0; 4],
93            root_area: root,
94            title: None,
95            margin: [0; 4],
96            overlap_plotting_area: [false; 4],
97        }
98    }
99
100    /**
101    Sets the size of the four margins of the chart.
102
103    - `size`: The desired size of the four chart margins in backend units (pixels).
104
105    See [`ChartBuilder::on()`] for more information and examples.
106    */
107    pub fn margin<S: SizeDesc>(&mut self, size: S) -> &mut Self {
108        let size = size.in_pixels(self.root_area).max(0) as u32;
109        self.margin = [size, size, size, size];
110        self
111    }
112
113    /**
114    Sets the size of the top margin of the chart.
115
116    - `size`: The desired size of the margin in backend units (pixels).
117
118    See [`ChartBuilder::on()`] for more information and examples.
119    */
120    pub fn margin_top<S: SizeDesc>(&mut self, size: S) -> &mut Self {
121        let size = size.in_pixels(self.root_area).max(0) as u32;
122        self.margin[0] = size;
123        self
124    }
125
126    /**
127    Sets the size of the bottom margin of the chart.
128
129    - `size`: The desired size of the margin in backend units (pixels).
130
131    See [`ChartBuilder::on()`] for more information and examples.
132    */
133    pub fn margin_bottom<S: SizeDesc>(&mut self, size: S) -> &mut Self {
134        let size = size.in_pixels(self.root_area).max(0) as u32;
135        self.margin[1] = size;
136        self
137    }
138
139    /**
140    Sets the size of the left margin of the chart.
141
142    - `size`: The desired size of the margin in backend units (pixels).
143
144    See [`ChartBuilder::on()`] for more information and examples.
145    */
146    pub fn margin_left<S: SizeDesc>(&mut self, size: S) -> &mut Self {
147        let size = size.in_pixels(self.root_area).max(0) as u32;
148        self.margin[2] = size;
149        self
150    }
151
152    /**
153    Sets the size of the right margin of the chart.
154
155    - `size`: The desired size of the margin in backend units (pixels).
156
157    See [`ChartBuilder::on()`] for more information and examples.
158    */
159    pub fn margin_right<S: SizeDesc>(&mut self, size: S) -> &mut Self {
160        let size = size.in_pixels(self.root_area).max(0) as u32;
161        self.margin[3] = size;
162        self
163    }
164
165    /**
166    Sets the size of the four label areas of the chart.
167
168    - `size`: The desired size of the four label areas in backend units (pixels).
169
170    See [`ChartBuilder::on()`] for more information and examples.
171    */
172    pub fn set_all_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
173        let size = size.in_pixels(self.root_area);
174        self.set_label_area_size(LabelAreaPosition::Top, size)
175            .set_label_area_size(LabelAreaPosition::Bottom, size)
176            .set_label_area_size(LabelAreaPosition::Left, size)
177            .set_label_area_size(LabelAreaPosition::Right, size)
178    }
179
180    /**
181    Sets the size of the left and bottom label areas of the chart.
182
183    - `size`: The desired size of the left and bottom label areas in backend units (pixels).
184
185    See [`ChartBuilder::on()`] for more information and examples.
186    */
187    pub fn set_left_and_bottom_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
188        let size = size.in_pixels(self.root_area);
189        self.set_label_area_size(LabelAreaPosition::Left, size)
190            .set_label_area_size(LabelAreaPosition::Bottom, size)
191    }
192
193    /**
194    Sets the size of the X label area at the bottom of the chart.
195
196    - `size`: The desired size of the X label area in backend units (pixels).
197      If set to 0, the X label area is removed.
198
199    See [`ChartBuilder::on()`] for more information and examples.
200    */
201    pub fn x_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
202        self.set_label_area_size(LabelAreaPosition::Bottom, size)
203    }
204
205    /**
206    Sets the size of the Y label area to the left of the chart.
207
208    - `size`: The desired size of the Y label area in backend units (pixels).
209      If set to 0, the Y label area is removed.
210
211    See [`ChartBuilder::on()`] for more information and examples.
212    */
213    pub fn y_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
214        self.set_label_area_size(LabelAreaPosition::Left, size)
215    }
216
217    /**
218    Sets the size of the X label area at the top of the chart.
219
220    - `size`: The desired size of the top X label area in backend units (pixels).
221      If set to 0, the top X label area is removed.
222
223    See [`ChartBuilder::on()`] for more information and examples.
224    */
225    pub fn top_x_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
226        self.set_label_area_size(LabelAreaPosition::Top, size)
227    }
228
229    /**
230    Sets the size of the Y label area to the right of the chart.
231
232    - `size`: The desired size of the Y label area in backend units (pixels).
233      If set to 0, the Y label area to the right is removed.
234
235    See [`ChartBuilder::on()`] for more information and examples.
236    */
237    pub fn right_y_label_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
238        self.set_label_area_size(LabelAreaPosition::Right, size)
239    }
240
241    /**
242    Sets the size of a chart label area.
243
244    - `pos`: The position of the desired label area to adjust
245    - `size`: The desired size of the label area in backend units (pixels).
246      If set to 0, the label area is removed.
247
248    See [`ChartBuilder::on()`] for more information and examples.
249    */
250    pub fn set_label_area_size<S: SizeDesc>(
251        &mut self,
252        pos: LabelAreaPosition,
253        size: S,
254    ) -> &mut Self {
255        let size = size.in_pixels(self.root_area);
256        self.label_area_size[pos as usize] = size.unsigned_abs();
257        self.overlap_plotting_area[pos as usize] = size < 0;
258        self
259    }
260
261    /**
262    Sets the title or caption of the chart.
263
264    - `caption`: The caption of the chart
265    - `style`: The text style
266
267    The title or caption will be centered at the top of the drawing area.
268
269    See [`ChartBuilder::on()`] for more information and examples.
270    */
271    pub fn caption<S: AsRef<str>, Style: IntoTextStyle<'b>>(
272        &mut self,
273        caption: S,
274        style: Style,
275    ) -> &mut Self {
276        self.title = Some((
277            caption.as_ref().to_string(),
278            style.into_text_style(self.root_area),
279        ));
280        self
281    }
282
283    /// This function has been renamed to [`ChartBuilder::build_cartesian_2d()`] and is to be removed in the future.
284    #[allow(clippy::type_complexity)]
285    #[deprecated(
286        note = "`build_ranged` has been renamed to `build_cartesian_2d` and is to be removed in the future."
287    )]
288    pub fn build_ranged<'c, X: AsRangedCoord, Y: AsRangedCoord>(
289        &mut self,
290        x_spec: X,
291        y_spec: Y,
292    ) -> Result<
293        ChartContext<'c, DB, Cartesian2d<X::CoordDescType, Y::CoordDescType>>,
294        DrawingAreaErrorKind<DB::ErrorType>,
295    > {
296        self.build_cartesian_2d(x_spec, y_spec)
297    }
298
299    /**
300    Builds a chart with a 2D Cartesian coordinate system.
301
302    - `x_spec`: Specifies the X axis range and data properties
303    - `y_spec`: Specifies the Y axis range and data properties
304    - Returns: A `ChartContext` object, ready to visualize data.
305
306    See [`ChartBuilder::on()`] and [`ChartContext::configure_mesh()`] for more information and examples.
307    */
308    #[allow(clippy::type_complexity)]
309    pub fn build_cartesian_2d<'c, X: AsRangedCoord, Y: AsRangedCoord>(
310        &mut self,
311        x_spec: X,
312        y_spec: Y,
313    ) -> Result<
314        ChartContext<'c, DB, Cartesian2d<X::CoordDescType, Y::CoordDescType>>,
315        DrawingAreaErrorKind<DB::ErrorType>,
316    > {
317        let mut label_areas = [None, None, None, None];
318
319        let mut drawing_area = DrawingArea::clone(self.root_area);
320
321        if *self.margin.iter().max().unwrap_or(&0) > 0 {
322            drawing_area = drawing_area.margin(
323                self.margin[0] as i32,
324                self.margin[1] as i32,
325                self.margin[2] as i32,
326                self.margin[3] as i32,
327            );
328        }
329
330        let (title_dx, title_dy) = if let Some((ref title, ref style)) = self.title {
331            let (origin_dx, origin_dy) = drawing_area.get_base_pixel();
332            drawing_area = drawing_area.titled(title, style.clone())?;
333            let (current_dx, current_dy) = drawing_area.get_base_pixel();
334            (current_dx - origin_dx, current_dy - origin_dy)
335        } else {
336            (0, 0)
337        };
338
339        let (w, h) = drawing_area.dim_in_pixel();
340
341        let mut actual_drawing_area_pos = [0, h as i32, 0, w as i32];
342
343        const DIR: [(i16, i16); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)];
344
345        for (idx, (dx, dy)) in (0..4).map(|idx| (idx, DIR[idx])) {
346            if self.overlap_plotting_area[idx] {
347                continue;
348            }
349
350            let size = self.label_area_size[idx] as i32;
351
352            let split_point = if dx + dy < 0 { size } else { -size };
353
354            actual_drawing_area_pos[idx] += split_point;
355        }
356
357        // Now the root drawing area is to be split into
358        //
359        // +----------+------------------------------+------+
360        // |    0     |    1 (Top Label Area)        |   2  |
361        // +----------+------------------------------+------+
362        // |    3     |                              |   5  |
363        // |  Left    |       4 (Plotting Area)      | Right|
364        // |  Labels  |                              | Label|
365        // +----------+------------------------------+------+
366        // |    6     |        7 (Bottom Labels)     |   8  |
367        // +----------+------------------------------+------+
368
369        let mut split: Vec<_> = drawing_area
370            .split_by_breakpoints(
371                &actual_drawing_area_pos[2..4],
372                &actual_drawing_area_pos[0..2],
373            )
374            .into_iter()
375            .map(Some)
376            .collect();
377
378        // Take out the plotting area
379        std::mem::swap(&mut drawing_area, split[4].as_mut().unwrap());
380
381        // Initialize the label areas - since the label area might be overlapping
382        // with the plotting area, in this case, we need handle them differently
383        for (src_idx, dst_idx) in [1, 7, 3, 5].iter().zip(0..4) {
384            if !self.overlap_plotting_area[dst_idx] {
385                let (h, w) = split[*src_idx].as_ref().unwrap().dim_in_pixel();
386                if h > 0 && w > 0 {
387                    std::mem::swap(&mut label_areas[dst_idx], &mut split[*src_idx]);
388                }
389            } else if self.label_area_size[dst_idx] != 0 {
390                let size = self.label_area_size[dst_idx] as i32;
391                let (dw, dh) = drawing_area.dim_in_pixel();
392                let x0 = if DIR[dst_idx].0 > 0 {
393                    dw as i32 - size
394                } else {
395                    0
396                };
397                let y0 = if DIR[dst_idx].1 > 0 {
398                    dh as i32 - size
399                } else {
400                    0
401                };
402                let x1 = if DIR[dst_idx].0 >= 0 { dw as i32 } else { size };
403                let y1 = if DIR[dst_idx].1 >= 0 { dh as i32 } else { size };
404
405                label_areas[dst_idx] = Some(
406                    drawing_area
407                        .clone()
408                        .shrink((x0, y0), ((x1 - x0), (y1 - y0))),
409                );
410            }
411        }
412
413        let mut pixel_range = drawing_area.get_pixel_range();
414        pixel_range.0.end -= 1;
415        pixel_range.1.end -= 1;
416        pixel_range.1 = pixel_range.1.end..pixel_range.1.start;
417
418        let mut x_label_area = [None, None];
419        let mut y_label_area = [None, None];
420
421        std::mem::swap(&mut x_label_area[0], &mut label_areas[0]);
422        std::mem::swap(&mut x_label_area[1], &mut label_areas[1]);
423        std::mem::swap(&mut y_label_area[0], &mut label_areas[2]);
424        std::mem::swap(&mut y_label_area[1], &mut label_areas[3]);
425
426        Ok(ChartContext {
427            x_label_area,
428            y_label_area,
429            drawing_area: drawing_area.apply_coord_spec(Cartesian2d::new(
430                x_spec,
431                y_spec,
432                pixel_range,
433            )),
434            series_anno: vec![],
435            drawing_area_pos: (
436                actual_drawing_area_pos[2] + title_dx + self.margin[2] as i32,
437                actual_drawing_area_pos[0] + title_dy + self.margin[0] as i32,
438            ),
439        })
440    }
441
442    /**
443    Builds a chart with a 3D Cartesian coordinate system.
444
445    - `x_spec`: Specifies the X axis range and data properties
446    - `y_spec`: Specifies the Y axis range and data properties
447    - `z_sepc`: Specifies the Z axis range and data properties
448    - Returns: A `ChartContext` object, ready to visualize data.
449
450    See [`ChartBuilder::on()`] and [`ChartContext::configure_axes()`] for more information and examples.
451    */
452    #[allow(clippy::type_complexity)]
453    pub fn build_cartesian_3d<'c, X: AsRangedCoord, Y: AsRangedCoord, Z: AsRangedCoord>(
454        &mut self,
455        x_spec: X,
456        y_spec: Y,
457        z_spec: Z,
458    ) -> Result<
459        ChartContext<'c, DB, Cartesian3d<X::CoordDescType, Y::CoordDescType, Z::CoordDescType>>,
460        DrawingAreaErrorKind<DB::ErrorType>,
461    > {
462        let mut drawing_area = DrawingArea::clone(self.root_area);
463
464        if *self.margin.iter().max().unwrap_or(&0) > 0 {
465            drawing_area = drawing_area.margin(
466                self.margin[0] as i32,
467                self.margin[1] as i32,
468                self.margin[2] as i32,
469                self.margin[3] as i32,
470            );
471        }
472
473        let (title_dx, title_dy) = if let Some((ref title, ref style)) = self.title {
474            let (origin_dx, origin_dy) = drawing_area.get_base_pixel();
475            drawing_area = drawing_area.titled(title, style.clone())?;
476            let (current_dx, current_dy) = drawing_area.get_base_pixel();
477            (current_dx - origin_dx, current_dy - origin_dy)
478        } else {
479            (0, 0)
480        };
481
482        let pixel_range = drawing_area.get_pixel_range();
483
484        Ok(ChartContext {
485            x_label_area: [None, None],
486            y_label_area: [None, None],
487            drawing_area: drawing_area.apply_coord_spec(Cartesian3d::new(
488                x_spec,
489                y_spec,
490                z_spec,
491                pixel_range,
492            )),
493            series_anno: vec![],
494            drawing_area_pos: (
495                title_dx + self.margin[2] as i32,
496                title_dy + self.margin[0] as i32,
497            ),
498        })
499    }
500}
501
502#[cfg(test)]
503mod test {
504    use super::*;
505    use crate::prelude::*;
506    #[test]
507    fn test_label_area_size() {
508        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
509        let mut chart = ChartBuilder::on(&drawing_area);
510
511        chart
512            .x_label_area_size(10)
513            .y_label_area_size(20)
514            .top_x_label_area_size(30)
515            .right_y_label_area_size(40);
516        assert_eq!(chart.label_area_size[1], 10);
517        assert_eq!(chart.label_area_size[2], 20);
518        assert_eq!(chart.label_area_size[0], 30);
519        assert_eq!(chart.label_area_size[3], 40);
520
521        chart.set_label_area_size(LabelAreaPosition::Left, 100);
522        chart.set_label_area_size(LabelAreaPosition::Right, 200);
523        chart.set_label_area_size(LabelAreaPosition::Top, 300);
524        chart.set_label_area_size(LabelAreaPosition::Bottom, 400);
525
526        assert_eq!(chart.label_area_size[0], 300);
527        assert_eq!(chart.label_area_size[1], 400);
528        assert_eq!(chart.label_area_size[2], 100);
529        assert_eq!(chart.label_area_size[3], 200);
530    }
531
532    #[test]
533    fn test_margin_configure() {
534        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
535        let mut chart = ChartBuilder::on(&drawing_area);
536
537        chart.margin(5);
538        assert_eq!(chart.margin[0], 5);
539        assert_eq!(chart.margin[1], 5);
540        assert_eq!(chart.margin[2], 5);
541        assert_eq!(chart.margin[3], 5);
542
543        chart.margin_top(10);
544        chart.margin_bottom(11);
545        chart.margin_left(12);
546        chart.margin_right(13);
547        assert_eq!(chart.margin[0], 10);
548        assert_eq!(chart.margin[1], 11);
549        assert_eq!(chart.margin[2], 12);
550        assert_eq!(chart.margin[3], 13);
551    }
552
553    #[test]
554    fn test_caption() {
555        let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
556        let mut chart = ChartBuilder::on(&drawing_area);
557
558        chart.caption("This is a test case", ("serif", 10));
559
560        assert_eq!(chart.title.as_ref().unwrap().0, "This is a test case");
561        assert_eq!(chart.title.as_ref().unwrap().1.font.get_name(), "serif");
562        assert_eq!(chart.title.as_ref().unwrap().1.font.get_size(), 10.0);
563        check_color(chart.title.as_ref().unwrap().1.color, BLACK.to_rgba());
564
565        chart.caption("This is a test case", ("serif", 10));
566        assert_eq!(chart.title.as_ref().unwrap().1.font.get_name(), "serif");
567    }
568
569    #[test]
570    fn test_zero_limit_with_log_scale() {
571        let drawing_area = create_mocked_drawing_area(640, 480, |_| {});
572
573        let mut chart = ChartBuilder::on(&drawing_area)
574            .build_cartesian_2d(0f32..10f32, (1e-6f32..1f32).log_scale())
575            .unwrap();
576
577        let data = vec![
578            (2f32, 1e-4f32),
579            (4f32, 1e-3f32),
580            (6f32, 1e-2f32),
581            (8f32, 1e-1f32),
582        ];
583
584        chart
585            .draw_series(
586                data.iter()
587                    .map(|&(x, y)| Rectangle::new([(x - 0.5, 0.0), (x + 0.5, y)], RED.filled())),
588            )
589            .unwrap();
590    }
591}