1 | use super::context::ChartContext; |
2 | |
3 | use crate::coord::cartesian::{Cartesian2d, Cartesian3d}; |
4 | use crate::coord::ranged1d::AsRangedCoord; |
5 | use crate::coord::Shift; |
6 | |
7 | use crate::drawing::{DrawingArea, DrawingAreaErrorKind}; |
8 | use crate::style::{IntoTextStyle, SizeDesc, TextStyle}; |
9 | |
10 | use plotters_backend::DrawingBackend; |
11 | |
12 | /** |
13 | Specifies one of the four label positions around the figure. |
14 | |
15 | This is used to configure the label area size with function |
16 | [`ChartBuilder::set_label_area_size()`]. |
17 | |
18 | # Example |
19 | |
20 | ``` |
21 | use plotters::prelude::*; |
22 | let drawing_area = SVGBackend::new("label_area_position.svg" , (300, 200)).into_drawing_area(); |
23 | drawing_area.fill(&WHITE).unwrap(); |
24 | let mut chart_builder = ChartBuilder::on(&drawing_area); |
25 | chart_builder.set_label_area_size(LabelAreaPosition::Bottom, 60).set_label_area_size(LabelAreaPosition::Left, 35); |
26 | let mut chart_context = chart_builder.build_cartesian_2d(0.0..4.0, 0.0..3.0).unwrap(); |
27 | chart_context.configure_mesh().x_desc("Spacious X label area" ).y_desc("Narrow Y label area" ).draw().unwrap(); |
28 | ``` |
29 | |
30 | The 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)] |
39 | pub 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 | /** |
51 | The helper object to create a chart context, which is used for the high-level figure drawing. |
52 | |
53 | With the help of this object, we can convert a basic drawing area into a chart context, which |
54 | allows the high-level charting API being used on the drawing area. |
55 | |
56 | See [`ChartBuilder::on()`] for more information and examples. |
57 | */ |
58 | pub 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 | |
66 | impl<'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)] |
503 | mod 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 | |