1use std::borrow::Borrow;
2use std::fmt::Debug;
3use std::marker::PhantomData;
4use std::ops::Range;
5use std::sync::Arc;
6
7use super::dual_coord::DualCoordChartContext;
8use super::mesh::MeshStyle;
9use super::series::SeriesLabelStyle;
10
11use crate::coord::{
12 AsRangedCoord, CoordTranslate, MeshLine, Ranged, RangedCoord, ReverseCoordTranslate, Shift,
13};
14use crate::drawing::backend::{BackendCoord, DrawingBackend};
15use crate::drawing::{DrawingArea, DrawingAreaErrorKind};
16use crate::element::{Drawable, DynElement, IntoDynElement, PathElement, PointCollection};
17use crate::style::text_anchor::{HPos, Pos, VPos};
18use crate::style::{AsRelative, FontTransform, ShapeStyle, SizeDesc, TextStyle};
19
20#[allow(clippy::type_complexity)]
24pub struct SeriesAnno<'a, DB: DrawingBackend> {
25 label: Option<String>,
26 draw_func: Option<Box<dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord> + 'a>>,
27 phantom_data: PhantomData<DB>,
28}
29
30impl<'a, DB: DrawingBackend> SeriesAnno<'a, DB> {
31 pub(crate) fn get_label(&self) -> &str {
32 self.label.as_ref().map(|x| x.as_str()).unwrap_or("")
33 }
34
35 pub(crate) fn get_draw_func(
36 &self,
37 ) -> Option<&dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord>> {
38 self.draw_func.as_ref().map(|x| x.borrow())
39 }
40
41 fn new() -> Self {
42 Self {
43 label: None,
44 draw_func: None,
45 phantom_data: PhantomData,
46 }
47 }
48
49 pub fn label<L: Into<String>>(&mut self, label: L) -> &mut Self {
52 self.label = Some(label.into());
53 self
54 }
55
56 pub fn legend<E: IntoDynElement<'a, DB, BackendCoord>, T: Fn(BackendCoord) -> E + 'a>(
61 &mut self,
62 func: T,
63 ) -> &mut Self {
64 self.draw_func = Some(Box::new(move |p| func(p).into_dyn()));
65 self
66 }
67}
68
69pub struct ChartContext<'a, DB: DrawingBackend, CT: CoordTranslate> {
73 pub(super) x_label_area: [Option<DrawingArea<DB, Shift>>; 2],
74 pub(super) y_label_area: [Option<DrawingArea<DB, Shift>>; 2],
75 pub(super) drawing_area: DrawingArea<DB, CT>,
76 pub(super) series_anno: Vec<SeriesAnno<'a, DB>>,
77 pub(super) drawing_area_pos: (i32, i32),
78}
79
80pub struct ChartState<CT: CoordTranslate> {
87 drawing_area_pos: (i32, i32),
88 drawing_area_size: (u32, u32),
89 coord: CT,
90}
91
92impl<'a, CT: CoordTranslate + Clone> Clone for ChartState<CT> {
93 fn clone(&self) -> Self {
94 Self {
95 drawing_area_size: self.drawing_area_size,
96 drawing_area_pos: self.drawing_area_pos,
97 coord: self.coord.clone(),
98 }
99 }
100}
101
102impl<'a, DB: DrawingBackend, CT: CoordTranslate> From<ChartContext<'a, DB, CT>> for ChartState<CT> {
103 fn from(chart: ChartContext<'a, DB, CT>) -> ChartState<CT> {
104 ChartState {
105 drawing_area_pos: chart.drawing_area_pos,
106 drawing_area_size: chart.drawing_area.dim_in_pixel(),
107 coord: chart.drawing_area.into_coord_spec(),
108 }
109 }
110}
111
112impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
113 pub fn into_chart_state(self) -> ChartState<CT> {
116 self.into()
117 }
118
119 pub fn into_shared_chart_state(self) -> ChartState<Arc<CT>> {
124 ChartState {
125 drawing_area_pos: self.drawing_area_pos,
126 drawing_area_size: self.drawing_area.dim_in_pixel(),
127 coord: Arc::new(self.drawing_area.into_coord_spec()),
128 }
129 }
130}
131
132impl<'a, 'b, DB, CT> From<&ChartContext<'a, DB, CT>> for ChartState<CT>
133where
134 DB: DrawingBackend,
135 CT: CoordTranslate + Clone,
136{
137 fn from(chart: &ChartContext<'a, DB, CT>) -> ChartState<CT> {
138 ChartState {
139 drawing_area_pos: chart.drawing_area_pos,
140 drawing_area_size: chart.drawing_area.dim_in_pixel(),
141 coord: chart.drawing_area.as_coord_spec().clone(),
142 }
143 }
144}
145
146impl<'a, DB: DrawingBackend, CT: CoordTranslate + Clone> ChartContext<'a, DB, CT> {
147 pub fn to_chart_state(&self) -> ChartState<CT> {
149 self.into()
150 }
151}
152
153impl<CT: CoordTranslate> ChartState<CT> {
154 pub fn restore<'a, DB: DrawingBackend>(
159 self,
160 area: &DrawingArea<DB, Shift>,
161 ) -> ChartContext<'a, DB, CT> {
162 let area = area
163 .clone()
164 .shrink(self.drawing_area_pos, self.drawing_area_size);
165 ChartContext {
166 x_label_area: [None, None],
167 y_label_area: [None, None],
168 drawing_area: area.apply_coord_spec(self.coord),
169 series_anno: vec![],
170 drawing_area_pos: self.drawing_area_pos,
171 }
172 }
173}
174
175impl<
176 'a,
177 DB: DrawingBackend,
178 XT: Debug,
179 YT: Debug,
180 X: Ranged<ValueType = XT>,
181 Y: Ranged<ValueType = YT>,
182 > ChartContext<'a, DB, RangedCoord<X, Y>>
183{
184 fn is_overlapping_drawing_area(&self, area: Option<&DrawingArea<DB, Shift>>) -> bool {
185 if let Some(area) = area {
186 let (x0, y0) = area.get_base_pixel();
187 let (w, h) = area.dim_in_pixel();
188 let (x1, y1) = (x0 + w as i32, y0 + h as i32);
189 let (dx0, dy0) = self.drawing_area.get_base_pixel();
190 let (w, h) = self.drawing_area.dim_in_pixel();
191 let (dx1, dy1) = (dx0 + w as i32, dy0 + h as i32);
192
193 let (ox0, ox1) = (x0.max(dx0), x1.min(dx1));
194 let (oy0, oy1) = (y0.max(dy0), y1.min(dy1));
195
196 ox1 > ox0 && oy1 > oy0
197 } else {
198 false
199 }
200 }
201
202 pub fn configure_mesh<'b>(&'b mut self) -> MeshStyle<'a, 'b, X, Y, DB> {
205 let base_tick_size = (5u32).percent().max(5).in_pixels(&self.drawing_area);
206
207 let mut x_tick_size = [base_tick_size, base_tick_size];
208 let mut y_tick_size = [base_tick_size, base_tick_size];
209
210 for idx in 0..2 {
211 if self.is_overlapping_drawing_area(self.x_label_area[idx].as_ref()) {
212 x_tick_size[idx] = -x_tick_size[idx];
213 }
214 if self.is_overlapping_drawing_area(self.y_label_area[idx].as_ref()) {
215 y_tick_size[idx] = -y_tick_size[idx];
216 }
217 }
218
219 MeshStyle {
220 parent_size: self.drawing_area.dim_in_pixel(),
221 axis_style: None,
222 x_label_offset: 0,
223 y_label_offset: 0,
224 draw_x_mesh: true,
225 draw_y_mesh: true,
226 draw_x_axis: true,
227 draw_y_axis: true,
228 n_x_labels: 10,
229 n_y_labels: 10,
230 line_style_1: None,
231 line_style_2: None,
232 x_label_style: None,
233 y_label_style: None,
234 format_x: &|x| format!("{:?}", x),
235 format_y: &|y| format!("{:?}", y),
236 target: Some(self),
237 _phantom_data: PhantomData,
238 x_desc: None,
239 y_desc: None,
240 axis_desc_style: None,
241 x_tick_size,
242 y_tick_size,
243 }
244 }
245}
246
247impl<'a, DB: DrawingBackend + 'a, CT: CoordTranslate> ChartContext<'a, DB, CT> {
248 pub fn configure_series_labels<'b>(&'b mut self) -> SeriesLabelStyle<'a, 'b, DB, CT> {
250 SeriesLabelStyle::new(self)
251 }
252
253 pub fn plotting_area(&self) -> &DrawingArea<DB, CT> {
255 &self.drawing_area
256 }
257}
258
259impl<'a, DB: DrawingBackend, CT: CoordTranslate> ChartContext<'a, DB, CT> {
260 pub fn as_coord_spec(&self) -> &CT {
261 self.drawing_area.as_coord_spec()
262 }
263}
264
265impl<'a, DB: DrawingBackend, CT: ReverseCoordTranslate> ChartContext<'a, DB, CT> {
266 pub fn into_coord_trans(self) -> impl Fn(BackendCoord) -> Option<CT::From> {
268 let coord_spec = self.drawing_area.into_coord_spec();
269 move |coord| coord_spec.reverse_translate(coord)
270 }
271}
272
273impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Arc<RangedCoord<X, Y>>> {
274 pub(super) fn draw_series_impl<E, R, S>(
275 &mut self,
276 series: S,
277 ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
278 where
279 for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
280 E: Drawable<DB>,
281 R: Borrow<E>,
282 S: IntoIterator<Item = R>,
283 {
284 for element in series {
285 self.drawing_area.draw(element.borrow())?;
286 }
287 Ok(())
288 }
289
290 pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
291 let idx = self.series_anno.len();
292 self.series_anno.push(SeriesAnno::new());
293 &mut self.series_anno[idx]
294 }
295
296 pub fn draw_series<E, R, S>(
298 &mut self,
299 series: S,
300 ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
301 where
302 for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
303 E: Drawable<DB>,
304 R: Borrow<E>,
305 S: IntoIterator<Item = R>,
306 {
307 self.draw_series_impl(series)?;
308 Ok(self.alloc_series_anno())
309 }
310}
311
312impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, RangedCoord<X, Y>> {
313 pub fn x_range(&self) -> Range<X::ValueType> {
315 self.drawing_area.get_x_range()
316 }
317
318 pub fn y_range(&self) -> Range<Y::ValueType> {
320 self.drawing_area.get_y_range()
321 }
322
323 pub fn backend_coord(&self, coord: &(X::ValueType, Y::ValueType)) -> BackendCoord {
326 self.drawing_area.map_coordinate(coord)
327 }
328
329 pub(super) fn draw_series_impl<E, R, S>(
330 &mut self,
331 series: S,
332 ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
333 where
334 for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
335 E: Drawable<DB>,
336 R: Borrow<E>,
337 S: IntoIterator<Item = R>,
338 {
339 for element in series {
340 self.drawing_area.draw(element.borrow())?;
341 }
342 Ok(())
343 }
344
345 pub(super) fn alloc_series_anno(&mut self) -> &mut SeriesAnno<'a, DB> {
346 let idx = self.series_anno.len();
347 self.series_anno.push(SeriesAnno::new());
348 &mut self.series_anno[idx]
349 }
350
351 pub fn draw_series<E, R, S>(
353 &mut self,
354 series: S,
355 ) -> Result<&mut SeriesAnno<'a, DB>, DrawingAreaErrorKind<DB::ErrorType>>
356 where
357 for<'b> &'b E: PointCollection<'b, (X::ValueType, Y::ValueType)>,
358 E: Drawable<DB>,
359 R: Borrow<E>,
360 S: IntoIterator<Item = R>,
361 {
362 self.draw_series_impl(series)?;
363 Ok(self.alloc_series_anno())
364 }
365
366 #[allow(clippy::type_complexity)]
369 fn draw_mesh_lines<FmtLabel>(
370 &mut self,
371 (r, c): (usize, usize),
372 (x_mesh, y_mesh): (bool, bool),
373 mesh_line_style: &ShapeStyle,
374 mut fmt_label: FmtLabel,
375 ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>>
376 where
377 FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
378 {
379 let mut x_labels = vec![];
380 let mut y_labels = vec![];
381 self.drawing_area.draw_mesh(
382 |b, l| {
383 let draw;
384 match l {
385 MeshLine::XMesh((x, _), _, _) => {
386 if let Some(label_text) = fmt_label(&l) {
387 x_labels.push((x, label_text));
388 }
389 draw = x_mesh;
390 }
391 MeshLine::YMesh((_, y), _, _) => {
392 if let Some(label_text) = fmt_label(&l) {
393 y_labels.push((y, label_text));
394 }
395 draw = y_mesh;
396 }
397 };
398 if draw {
399 l.draw(b, mesh_line_style)
400 } else {
401 Ok(())
402 }
403 },
404 r,
405 c,
406 )?;
407 Ok((x_labels, y_labels))
408 }
409
410 fn draw_axis(
411 &self,
412 area: &DrawingArea<DB, Shift>,
413 axis_style: Option<&ShapeStyle>,
414 orientation: (i16, i16),
415 inward_labels: bool,
416 ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> {
417 let (x0, y0) = self.drawing_area.get_base_pixel();
418 let (tw, th) = area.dim_in_pixel();
419
420 let mut axis_range = if orientation.0 == 0 {
421 self.drawing_area.get_x_axis_pixel_range()
422 } else {
423 self.drawing_area.get_y_axis_pixel_range()
424 };
425
426 if orientation.0 == 0 {
430 axis_range.start -= x0;
431 axis_range.end -= x0;
432 } else {
433 axis_range.start -= y0;
434 axis_range.end -= y0;
435 }
436
437 if let Some(axis_style) = axis_style {
438 let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 };
439 let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 };
440 let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 };
441 let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 };
442
443 if inward_labels {
444 if orientation.0 == 0 {
445 if y0 == 0 {
446 y0 = th as i32 - 1;
447 y1 = th as i32 - 1;
448 } else {
449 y0 = 0;
450 y1 = 0;
451 }
452 } else if x0 == 0 {
453 x0 = tw as i32 - 1;
454 x1 = tw as i32 - 1;
455 } else {
456 x0 = 0;
457 x1 = 0;
458 }
459 }
460
461 if orientation.0 == 0 {
462 x0 = axis_range.start;
463 x1 = axis_range.end;
464 } else {
465 y0 = axis_range.start;
466 y1 = axis_range.end;
467 }
468
469 area.draw(&PathElement::new(
470 vec![(x0, y0), (x1, y1)],
471 axis_style.clone(),
472 ))?;
473 }
474
475 Ok(axis_range)
476 }
477
478 #[allow(clippy::too_many_arguments)]
480 #[allow(clippy::cognitive_complexity)]
481 fn draw_axis_and_labels(
482 &self,
483 area: Option<&DrawingArea<DB, Shift>>,
484 axis_style: Option<&ShapeStyle>,
485 labels: &[(i32, String)],
486 label_style: &TextStyle,
487 label_offset: i32,
488 orientation: (i16, i16),
489 axis_desc: Option<(&str, &TextStyle)>,
490 tick_size: i32,
491 ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
492 let area = if let Some(target) = area {
493 target
494 } else {
495 return Ok(());
496 };
497
498 let (x0, y0) = self.drawing_area.get_base_pixel();
499 let (tw, th) = area.dim_in_pixel();
500
501 let label_dist = tick_size.abs() * 2;
503
504 let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?;
507
508 let label_width: Vec<_> = labels
513 .iter()
514 .map(|(_, text)| {
515 if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 {
516 let ((x0, _), (x1, _)) = label_style
517 .font
518 .layout_box(text)
519 .unwrap_or(((0, 0), (0, 0)));
520 x1 - x0
521 } else {
522 0
525 }
526 })
527 .collect();
528
529 let min_width = *label_width.iter().min().unwrap_or(&1).max(&1);
530 let max_width = *label_width
531 .iter()
532 .filter(|&&x| x < min_width * 2)
533 .max()
534 .unwrap_or(&min_width);
535 let right_align_width = (min_width * 2).min(max_width);
536
537 for ((p, t), w) in labels.iter().zip(label_width.into_iter()) {
539 let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 };
541
542 if rp < axis_range.start.min(axis_range.end)
543 || axis_range.end.max(axis_range.start) < rp
544 {
545 continue;
546 }
547
548 let (cx, cy, h_pos, v_pos) = if tick_size >= 0 {
549 match orientation {
550 (dx, dy) if dx > 0 && dy == 0 => {
552 if w >= right_align_width {
553 (label_dist, *p - y0, HPos::Left, VPos::Center)
554 } else {
555 (
556 label_dist + right_align_width,
557 *p - y0,
558 HPos::Right,
559 VPos::Center,
560 )
561 }
562 }
563 (dx, dy) if dx < 0 && dy == 0 => {
565 (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
566 }
567 (dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
569 (dx, dy) if dx == 0 && dy < 0 => {
571 (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
572 }
573 _ => panic!("Bug: Invalid orientation specification"),
574 }
575 } else {
576 match orientation {
577 (dx, dy) if dx > 0 && dy == 0 => {
579 (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center)
580 }
581 (dx, dy) if dx < 0 && dy == 0 => {
583 (label_dist, *p - y0, HPos::Left, VPos::Center)
584 }
585 (dx, dy) if dx == 0 && dy > 0 => {
587 (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom)
588 }
589 (dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top),
591 _ => panic!("Bug: Invalid orientation specification"),
592 }
593 };
594
595 let (text_x, text_y) = if orientation.0 == 0 {
596 (cx + label_offset, cy)
597 } else {
598 (cx, cy + label_offset)
599 };
600
601 let label_style = &label_style.pos(Pos::new(h_pos, v_pos));
602 area.draw_text(&t, label_style, (text_x, text_y))?;
603
604 if tick_size != 0 {
605 if let Some(style) = axis_style {
606 let xmax = tw as i32 - 1;
607 let ymax = th as i32 - 1;
608 let (kx0, ky0, kx1, ky1) = if tick_size > 0 {
609 match orientation {
610 (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0),
611 (dx, dy) if dx < 0 && dy == 0 => {
612 (xmax - tick_size, *p - y0, xmax, *p - y0)
613 }
614 (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size),
615 (dx, dy) if dx == 0 && dy < 0 => {
616 (*p - x0, ymax - tick_size, *p - x0, ymax)
617 }
618 _ => panic!("Bug: Invalid orientation specification"),
619 }
620 } else {
621 match orientation {
622 (dx, dy) if dx > 0 && dy == 0 => {
623 (xmax, *p - y0, xmax + tick_size, *p - y0)
624 }
625 (dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0),
626 (dx, dy) if dx == 0 && dy > 0 => {
627 (*p - x0, ymax, *p - x0, ymax + tick_size)
628 }
629 (dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size),
630 _ => panic!("Bug: Invalid orientation specification"),
631 }
632 };
633 let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], style.clone());
634 area.draw(&line)?;
635 }
636 }
637 }
638
639 if let Some((text, style)) = axis_desc {
640 let actual_style = if orientation.0 == 0 {
641 style.clone()
642 } else if orientation.0 == -1 {
643 style.transform(FontTransform::Rotate270)
644 } else {
645 style.transform(FontTransform::Rotate90)
646 };
647
648 let (x0, y0, h_pos, v_pos) = match orientation {
649 (dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top),
651 (dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top),
653 (dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom),
655 (dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top),
657 _ => panic!("Bug: Invalid orientation specification"),
658 };
659
660 let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos));
661 area.draw_text(&text, &actual_style, (x0 as i32, y0 as i32))?;
662 }
663
664 Ok(())
665 }
666
667 #[allow(clippy::too_many_arguments)]
668 pub(super) fn draw_mesh<FmtLabel>(
669 &mut self,
670 (r, c): (usize, usize),
671 mesh_line_style: &ShapeStyle,
672 x_label_style: &TextStyle,
673 y_label_style: &TextStyle,
674 fmt_label: FmtLabel,
675 x_mesh: bool,
676 y_mesh: bool,
677 x_label_offset: i32,
678 y_label_offset: i32,
679 x_axis: bool,
680 y_axis: bool,
681 axis_style: &ShapeStyle,
682 axis_desc_style: &TextStyle,
683 x_desc: Option<String>,
684 y_desc: Option<String>,
685 x_tick_size: [i32; 2],
686 y_tick_size: [i32; 2],
687 ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>
688 where
689 FmtLabel: FnMut(&MeshLine<X, Y>) -> Option<String>,
690 {
691 let (x_labels, y_labels) =
692 self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?;
693
694 for idx in 0..2 {
695 self.draw_axis_and_labels(
696 self.x_label_area[idx].as_ref(),
697 if x_axis { Some(axis_style) } else { None },
698 &x_labels[..],
699 x_label_style,
700 x_label_offset,
701 (0, -1 + idx as i16 * 2),
702 x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
703 x_tick_size[idx],
704 )?;
705
706 self.draw_axis_and_labels(
707 self.y_label_area[idx].as_ref(),
708 if y_axis { Some(axis_style) } else { None },
709 &y_labels[..],
710 y_label_style,
711 y_label_offset,
712 (-1 + idx as i16 * 2, 0),
713 y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)),
714 y_tick_size[idx],
715 )?;
716 }
717
718 Ok(())
719 }
720
721 #[allow(clippy::type_complexity)]
727 pub fn set_secondary_coord<SX: AsRangedCoord, SY: AsRangedCoord>(
728 self,
729 x_coord: SX,
730 y_coord: SY,
731 ) -> DualCoordChartContext<
732 'a,
733 DB,
734 RangedCoord<X, Y>,
735 RangedCoord<SX::CoordDescType, SY::CoordDescType>,
736 > {
737 let mut pixel_range = self.drawing_area.get_pixel_range();
738 pixel_range.1 = pixel_range.1.end..pixel_range.1.start;
739
740 DualCoordChartContext::new(self, RangedCoord::new(x_coord, y_coord, pixel_range))
741 }
742}
743
744#[cfg(test)]
745mod test {
746 use crate::prelude::*;
747
748 #[test]
749 fn test_chart_context() {
750 let drawing_area = create_mocked_drawing_area(200, 200, |_| {});
751
752 drawing_area.fill(&WHITE).expect("Fill");
753
754 let mut chart = ChartBuilder::on(&drawing_area)
755 .caption("Test Title", ("serif", 10))
756 .x_label_area_size(20)
757 .y_label_area_size(20)
758 .set_label_area_size(LabelAreaPosition::Top, 20)
759 .set_label_area_size(LabelAreaPosition::Right, 20)
760 .build_ranged(0..10, 0..10)
761 .expect("Create chart")
762 .set_secondary_coord(0.0..1.0, 0.0..1.0);
763
764 chart
765 .configure_mesh()
766 .x_desc("X")
767 .y_desc("Y")
768 .draw()
769 .expect("Draw mesh");
770 chart
771 .configure_secondary_axes()
772 .x_desc("X")
773 .y_desc("Y")
774 .draw()
775 .expect("Draw Secondary axes");
776
777 chart
778 .draw_series(std::iter::once(Circle::new((5, 5), 5, &RED)))
779 .expect("Drawing error");
780 chart
781 .draw_secondary_series(std::iter::once(Circle::new((0.3, 0.8), 5, &GREEN)))
782 .expect("Drawing error")
783 .label("Test label")
784 .legend(|(x, y)| Rectangle::new([(x - 10, y - 5), (x, y + 5)], &GREEN));
785
786 chart
787 .configure_series_labels()
788 .position(SeriesLabelPosition::UpperMiddle)
789 .draw()
790 .expect("Drawing error");
791 }
792}