1use std::borrow::Borrow;
2use std::ops::Range;
3
4use super::{DualCoordChartContext, MeshStyle, SeriesAnno, SeriesLabelStyle};
5
6use crate::coord::cartesian::{Cartesian2d, MeshLine};
7use crate::coord::ranged1d::{AsRangedCoord, KeyPointHint, Ranged, ValueFormatter};
8use crate::coord::{CoordTranslate, ReverseCoordTranslate, Shift};
9
10use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
11use crate::element::{Drawable, PathElement, PointCollection};
12use crate::style::text_anchor::{HPos, Pos, VPos};
13use crate::style::{ShapeStyle, TextStyle};
14
15use plotters_backend::{BackendCoord, DrawingBackend, FontTransform};
16
17pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
25 pub(super) x_label_area: [Option<DrawingArea<DB, Shift>>; 2],
26 pub(super) y_label_area: [Option<DrawingArea<DB, Shift>>; 2],
27 pub(super) drawing_area: DrawingArea<DB, CT>,
28 pub(super) series_anno: Vec<SeriesAnno<'a, DB>>,
29 pub(super) drawing_area_pos: (i32, i32),
30}
31
32impl<'a, DB, XT, YT, X, Y> ChartContext<'a, DB, Cartesian2d<X, Y>>
33where
34 DB: DrawingBackend,
35 X: Ranged<ValueType = XT> + ValueFormatter<XT>,
36 Y: Ranged<ValueType = YT> + ValueFormatter<YT>,
37{
38 pub(crate) fn is_overlapping_drawing_area(
39 &self,
40 area: Option<&DrawingArea<DB, Shift>>,
41 ) -> bool {
42 if let Some(area) = area {
43 let (x0, y0) = area.get_base_pixel();
44 let (w, h) = area.dim_in_pixel();
45 let (x1, y1) = (x0 + w as i32, y0 + h as i32);
46 let (dx0, dy0) = self.drawing_area.get_base_pixel();
47 let (w, h) = self.drawing_area.dim_in_pixel();
48 let (dx1, dy1) = (dx0 + w as i32, dy0 + h as i32);
49
50 let (ox0, ox1) = (x0.max(dx0), x1.min(dx1));
51 let (oy0, oy1) = (y0.max(dy0), y1.min(dy1));
52
53 ox1 > ox0 && oy1 > oy0
54 } else {
55 false
56 }
57 }
58
59 pub fn configure_mesh(&mut self) -> MeshStyle<'a, '_, X, Y, DB> {
62 MeshStyle::new(self)
63 }
64}
65
66impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
67 pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
69 let coord_spec = self.drawing_area.into_coord_spec();
70 move |coord| coord_spec.reverse_translate(coord)
71 }
72}
73
74impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
75 pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT>
77 where
78 DB: 'a,
79 {
80 SeriesLabelStyle::new(self)
81 }
82
83 pub fn plotting_area(&self) -> &DrawingArea<DB, CT> {
85 &self.drawing_area
86 }
87
88 pub fn as_coord_spec(&self) -> &CT {
90 self.drawing_area.as_coord_spec()
91 }
92
93 pub(super) fn draw_series_impl<E, R, S>(
99 &mut self,
100 series: S,
101 ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
102 where
103 for<'b> &'b E: PointCollection<'b, CT::From>,
104 E: Drawable<DB>,
105 R: Borrow<E>,
106 S: IntoIterator<Item = R>,
107 {
108 for element in series {
109 self.drawing_area.draw(element.borrow())?;
110 }
111 Ok(())
112 }
113
114 pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
115 let idx = self.series_anno.len();
116 self.series_anno.push(SeriesAnno::new());
117 &mut self.series_anno[idx]
118 }
119
120 pub fn draw_series<E, R, S>(
122 &mut self,
123 series: S,
124 ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
125 where
126 for<'b> &'b E: PointCollection<'b, CT::From>,
127 E: Drawable<DB>,
128 R: Borrow<E>,
129 S: IntoIterator<Item = R>,
130 {
131 self.draw_series_impl(series)?;
132 Ok(self.alloc_series_anno())
133 }
134}
135
136impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d<X, Y>> {
137 pub fn x_range(&self) -> Range<X::ValueType> {
139 self.drawing_area.get_x_range()
140 }
141
142 pub fn y_range(&self) -> Range<Y::ValueType> {
144 self.drawing_area.get_y_range()
145 }
146
147 pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord {
150 self.drawing_area.map_coordinate(coord)
151 }
152
153 #[allow(clippy::type_complexity)]
156 fn draw_mesh_lines<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
157 &mut self,
158 (r, c): (YH, XH),
159 (x_mesh, y_mesh): (bool, bool),
160 mesh_line_style: &ShapeStyle,
161 mut fmt_label: FmtLabel,
162 ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>>
163 where
164 FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
165 {
166 let mut x_labels = vec![];
167 let mut y_labels = vec![];
168 self.drawing_area.draw_mesh(
169 |b, l| {
170 let draw;
171 match l {
172 MeshLine::XMesh((x, _), _, _) => {
173 if let Some(label_text) = fmt_label(&l) {
174 x_labels.push((x, label_text));
175 }
176 draw = x_mesh;
177 }
178 MeshLine::YMesh((_, y), _, _) => {
179 if let Some(label_text) = fmt_label(&l) {
180 y_labels.push((y, label_text));
181 }
182 draw = y_mesh;
183 }
184 };
185 if draw {
186 l.draw(b, mesh_line_style)
187 } else {
188 Ok(())
189 }
190 },
191 r,
192 c,
193 )?;
194 Ok((x_labels, y_labels))
195 }
196
197 fn draw_axis(
198 &self,
199 area: &DrawingArea<DB, Shift>,
200 axis_style: Option<&ShapeStyle>,
201 orientation: (i16, i16),
202 inward_labels: bool,
203 ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> {
204 let (x0, y0) = self.drawing_area.get_base_pixel();
205 let (tw, th) = area.dim_in_pixel();
206
207 let mut axis_range = if orientation.0 == 0 {
208 self.drawing_area.get_x_axis_pixel_range()
209 } else {
210 self.drawing_area.get_y_axis_pixel_range()
211 };
212
213 if orientation.0 == 0 {
217 axis_range.start -= x0;
218 axis_range.end -= x0;
219 } else {
220 axis_range.start -= y0;
221 axis_range.end -= y0;
222 }
223
224 if let Some(axis_style) = axis_style {
225 let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 };
226 let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 };
227 let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 };
228 let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 };
229
230 if inward_labels {
231 if orientation.0 == 0 {
232 if y0 == 0 {
233 y0 = th as i32 - 1;
234 y1 = th as i32 - 1;
235 } else {
236 y0 = 0;
237 y1 = 0;
238 }
239 } else if x0 == 0 {
240 x0 = tw as i32 - 1;
241 x1 = tw as i32 - 1;
242 } else {
243 x0 = 0;
244 x1 = 0;
245 }
246 }
247
248 if orientation.0 == 0 {
249 x0 = axis_range.start;
250 x1 = axis_range.end;
251 } else {
252 y0 = axis_range.start;
253 y1 = axis_range.end;
254 }
255
256 area.draw(&PathElement::new(
257 vec![(x0, y0), (x1, y1)],
258 axis_style.clone(),
259 ))?;
260 }
261
262 Ok(axis_range)
263 }
264
265 #[allow(clippy::too_many_arguments)]
267 #[allow(clippy::cognitive_complexity)]
268 fn draw_axis_and_labels(
269 &self,
270 area: Option<&DrawingArea<DB, Shift>>,
271 axis_style: Option<&ShapeStyle>,
272 labels: &[(i32, String)],
273 label_style: &TextStyle,
274 label_offset: i32,
275 orientation: (i16, i16),
276 axis_desc: Option<(&str, &TextStyle)>,
277 tick_size: i32,
278 ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
279 let area = if let Some(target) = area {
280 target
281 } else {
282 return Ok(());
283 };
284
285 let (x0, y0) = self.drawing_area.get_base_pixel();
286 let (tw, th) = area.dim_in_pixel();
287
288 let label_dist = tick_size.abs() * 2;
290
291 let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?;
294
295 let label_width: Vec<_> = labels
300 .iter()
301 .map(|(_, text)| {
302 if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 {
303 let ((x0, _), (x1, _)) = label_style
304 .font
305 .layout_box(text)
306 .unwrap_or(((0, 0), (0, 0)));
307 x1 - x0
308 } else {
309 0
312 }
313 })
314 .collect();
315
316 let min_width = *label_width.iter().min().unwrap_or(&1).max(&1);
317 let max_width = *label_width
318 .iter()
319 .filter(|&&x| x < min_width * 2)
320 .max()
321 .unwrap_or(&min_width);
322 let right_align_width = (min_width * 2).min(max_width);
323
324 for ((p, t), w) in labels.iter().zip(label_width.into_iter()) {
326 let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 };
328
329 if rp < axis_range.start.min(axis_range.end)
330 || axis_range.end.max(axis_range.start) < rp
331 {
332 continue;
333 }
334
335 let (cx, cy, h_pos, v_pos) = if tick_size >= 0 {
336 match orientation {
337 (dx, dy) if dx > 0 && dy == 0 => {
339 if w >= right_align_width {
340 (label_dist, *p - y0, HPos::Left, VPos::Center)
341 } else {
342 (
343 label_dist + right_align_width,
344 *p - y0,
345 HPos::Right,
346 VPos::Center,
347 )
348 }
349 }
350 (dx, dy) if dx < 0 && dy == 0 => {
352 (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
353 }
354 (dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
356 (dx, dy) if dx == 0 && dy < 0 => {
358 (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
359 }
360 _ => panic!("Bug: Invalid orientation specification"),
361 }
362 } else {
363 match orientation {
364 (dx, dy) if dx > 0 && dy == 0 => {
366 (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
367 }
368 (dx, dy) if dx < 0 && dy == 0 => {
370 (label_dist, *p - y0, HPos::Left, VPos::Center)
371 }
372 (dx, dy) if dx == 0 && dy > 0 => {
374 (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
375 }
376 (dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
378 _ => panic!("Bug: Invalid orientation specification"),
379 }
380 };
381
382 let (text_x, text_y) = if orientation.0 == 0 {
383 (cx + label_offset, cy)
384 } else {
385 (cx, cy + label_offset)
386 };
387
388 let label_style = &label_style.pos(Pos::new(h_pos, v_pos));
389 area.draw_text(&t, label_style, (text_x, text_y))?;
390
391 if tick_size != 0 {
392 if let Some(style) = axis_style {
393 let xmax = tw as i32 - 1;
394 let ymax = th as i32 - 1;
395 let (kx0, ky0, kx1, ky1) = if tick_size > 0 {
396 match orientation {
397 (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0),
398 (dx, dy) if dx < 0 && dy == 0 => {
399 (xmax - tick_size, *p - y0, xmax, *p - y0)
400 }
401 (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size),
402 (dx, dy) if dx == 0 && dy < 0 => {
403 (*p - x0, ymax - tick_size, *p - x0, ymax)
404 }
405 _ => panic!("Bug: Invalid orientation specification"),
406 }
407 } else {
408 match orientation {
409 (dx, dy) if dx > 0 && dy == 0 => {
410 (xmax, *p - y0, xmax + tick_size, *p - y0)
411 }
412 (dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0),
413 (dx, dy) if dx == 0 && dy > 0 => {
414 (*p - x0, ymax, *p - x0, ymax + tick_size)
415 }
416 (dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size),
417 _ => panic!("Bug: Invalid orientation specification"),
418 }
419 };
420 let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], style.clone());
421 area.draw(&line)?;
422 }
423 }
424 }
425
426 if let Some((text, style)) = axis_desc {
427 let actual_style = if orientation.0 == 0 {
428 style.clone()
429 } else if orientation.0 == -1 {
430 style.transform(FontTransform::Rotate270)
431 } else {
432 style.transform(FontTransform::Rotate90)
433 };
434
435 let (x0, y0, h_pos, v_pos) = match orientation {
436 (dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top),
438 (dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top),
440 (dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom),
442 (dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top),
444 _ => panic!("Bug: Invalid orientation specification"),
445 };
446
447 let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos));
448 area.draw_text(&text, &actual_style, (x0 as i32, y0 as i32))?;
449 }
450
451 Ok(())
452 }
453
454 #[allow(clippy::too_many_arguments)]
455 pub(super) fn draw_mesh<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>(
456 &mut self,
457 (r, c): (YH, XH),
458 mesh_line_style: &ShapeStyle,
459 x_label_style: &TextStyle,
460 y_label_style: &TextStyle,
461 fmt_label: FmtLabel,
462 x_mesh: bool,
463 y_mesh: bool,
464 x_label_offset: i32,
465 y_label_offset: i32,
466 x_axis: bool,
467 y_axis: bool,
468 axis_style: &ShapeStyle,
469 axis_desc_style: &TextStyle,
470 x_desc: Option<String>,
471 y_desc: Option<String>,
472 x_tick_size: [i32; 2],
473 y_tick_size: [i32; 2],
474 ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
475 where
476 FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
477 {
478 let (x_labels, y_labels) =
479 self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?;
480
481 for idx in 0..2 {
482 self.draw_axis_and_labels(
483 self.x_label_area[idx].as_ref(),
484 if x_axis { Some(axis_style) } else { None },
485 &x_labels[..],
486 x_label_style,
487 x_label_offset,
488 (0, -1 + idx as i16 * 2),
489 x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
490 x_tick_size[idx],
491 )?;
492
493 self.draw_axis_and_labels(
494 self.y_label_area[idx].as_ref(),
495 if y_axis { Some(axis_style) } else { None },
496 &y_labels[..],
497 y_label_style,
498 y_label_offset,
499 (-1 + idx as i16 * 2, 0),
500 y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
501 y_tick_size[idx],
502 )?;
503 }
504
505 Ok(())
506 }
507
508 #[allow(clippy::type_complexity)]
515 pub fn set_secondary_coord<SX: AsRangedCoord, SY: AsRangedCoord>(
516 self,
517 x_coord: SX,
518 y_coord: SY,
519 ) -> DualCoordChartContext<
520 'a,
521 DB,
522 Cartesian2d<X, Y>,
523 Cartesian2d<SX::CoordDescType, SY::CoordDescType>,
524 > {
525 let mut pixel_range = self.drawing_area.get_pixel_range();
526 pixel_range.1 = pixel_range.1.end..pixel_range.1.start;
527
528 DualCoordChartContext::new(self, Cartesian2d::new(x_coord, y_coord, pixel_range))
529 }
530}
531
532#[cfg(test)]
533mod test {
534 use crate::prelude::*;
535
536 #[test]
537 fn test_chart_context() {
538 let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
539
540 drawing_area.fill(&WHITE).expect("Fill");
541
542 let mut chart = ChartBuilder::on(&drawing_area)
543 .caption("Test Title", ("serif", 10))
544 .x_label_area_size(20)
545 .y_label_area_size(20)
546 .set_label_area_size(LabelAreaPosition::Top, 20)
547 .set_label_area_size(LabelAreaPosition::Right, 20)
548 .build_cartesian_2d(0..10, 0..10)
549 .expect("Create chart")
550 .set_secondary_coord(0.0..1.0, 0.0..1.0);
551
552 chart
553 .configure_mesh()
554 .x_desc("X")
555 .y_desc("Y")
556 .draw()
557 .expect("Draw mesh");
558 chart
559 .configure_secondary_axes()
560 .x_desc("X")
561 .y_desc("Y")
562 .draw()
563 .expect("Draw Secondary axes");
564
565 chart
566 .draw_series(std::iter::once(Circle::new((5, 5), 5, &RED)))
567 .expect("Drawing error");
568 chart
569 .draw_secondary_series(std::iter::once(Circle::new((0.3, 0.8), 5, &GREEN)))
570 .expect("Drawing error")
571 .label("Test label")
572 .legend(|(x, y)| Rectangle::new([(x - 10, y - 5), (x, y + 5)], &GREEN));
573
574 chart
575 .configure_series_labels()
576 .position(SeriesLabelPosition::UpperMiddle)
577 .draw()
578 .expect("Drawing error");
579 }
580}