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<X: AsRangedCoord, Y: AsRangedCoord>(
289 &mut self,
290 x_spec: X,
291 y_spec: Y,
292 ) -> Result<
293 ChartContext<'a, 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<X: AsRangedCoord, Y: AsRangedCoord>(
310 &mut self,
311 x_spec: X,
312 y_spec: Y,
313 ) -> Result<
314 ChartContext<'a, 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<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<'a, 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