plotters/chart/
series.rs

1use super::ChartContext;
2use crate::coord::CoordTranslate;
3use crate::drawing::DrawingAreaErrorKind;
4use crate::element::{DynElement, EmptyElement, IntoDynElement, MultiLineText, Rectangle};
5use crate::style::{IntoFont, IntoTextStyle, ShapeStyle, SizeDesc, TextStyle, TRANSPARENT};
6
7use plotters_backend::{BackendCoord, DrawingBackend, DrawingErrorKind};
8
9type SeriesAnnoDrawFn<'a, DB> = dyn Fn(BackendCoord) -> DynElement<'a, DB, BackendCoord> + 'a;
10
11/// The annotations (such as the label of the series, the legend element, etc)
12/// When a series is drawn onto a drawing area, an series annotation object
13/// is created and a mutable reference is returned.
14pub struct SeriesAnno<'a, DB: DrawingBackend> {
15    label: Option<String>,
16    draw_func: Option<Box<SeriesAnnoDrawFn<'a, DB>>>,
17}
18
19impl<'a, DB: DrawingBackend> SeriesAnno<'a, DB> {
20    #[allow(clippy::option_as_ref_deref)]
21    pub(crate) fn get_label(&self) -> &str {
22        // TODO: Change this when we bump the MSRV
23        self.label.as_ref().map(|x| x.as_str()).unwrap_or("")
24    }
25
26    pub(crate) fn get_draw_func(&self) -> Option<&SeriesAnnoDrawFn<'a, DB>> {
27        self.draw_func.as_ref().map(|x| x.as_ref())
28    }
29
30    pub(crate) fn new() -> Self {
31        Self {
32            label: None,
33            draw_func: None,
34        }
35    }
36
37    /// Set the series label
38    /// - `label`: The string would be use as label for current series
39    pub fn label<L: Into<String>>(&mut self, label: L) -> &mut Self {
40        self.label = Some(label.into());
41        self
42    }
43
44    /// Set the legend element creator function
45    /// - `func`: The function use to create the element
46    /// *Note*: The creation function uses a shifted pixel-based coordinate system. And place the
47    /// point (0,0) to the mid-right point of the shape
48    pub fn legend<E: IntoDynElement<'a, DB, BackendCoord>, T: Fn(BackendCoord) -> E + 'a>(
49        &mut self,
50        func: T,
51    ) -> &mut Self {
52        self.draw_func = Some(Box::new(move |p| func(p).into_dyn()));
53        self
54    }
55}
56
57/// Describes where we want to put the series label
58pub enum SeriesLabelPosition {
59    UpperLeft,
60    MiddleLeft,
61    LowerLeft,
62    UpperMiddle,
63    MiddleMiddle,
64    LowerMiddle,
65    UpperRight,
66    MiddleRight,
67    LowerRight,
68    /// Force the series label drawn at the specific location
69    Coordinate(i32, i32),
70}
71
72impl SeriesLabelPosition {
73    fn layout_label_area(&self, label_dim: (i32, i32), area_dim: (u32, u32)) -> (i32, i32) {
74        use SeriesLabelPosition::*;
75        (
76            match self {
77                UpperLeft | MiddleLeft | LowerLeft => 5,
78                UpperMiddle | MiddleMiddle | LowerMiddle => {
79                    (area_dim.0 as i32 - label_dim.0 as i32) / 2
80                }
81                UpperRight | MiddleRight | LowerRight => area_dim.0 as i32 - label_dim.0 as i32 - 5,
82                Coordinate(x, _) => *x,
83            },
84            match self {
85                UpperLeft | UpperMiddle | UpperRight => 5,
86                MiddleLeft | MiddleMiddle | MiddleRight => {
87                    (area_dim.1 as i32 - label_dim.1 as i32) / 2
88                }
89                LowerLeft | LowerMiddle | LowerRight => area_dim.1 as i32 - label_dim.1 as i32 - 5,
90                Coordinate(_, y) => *y,
91            },
92        )
93    }
94}
95
96/// The struct to specify the series label of a target chart context
97pub struct SeriesLabelStyle<'a, 'b, DB: DrawingBackend, CT: CoordTranslate> {
98    target: &'b mut ChartContext<'a, DB, CT>,
99    position: SeriesLabelPosition,
100    legend_area_size: u32,
101    border_style: ShapeStyle,
102    background: ShapeStyle,
103    label_font: Option<TextStyle<'b>>,
104    margin: u32,
105}
106
107impl<'a, 'b, DB: DrawingBackend + 'a, CT: CoordTranslate> SeriesLabelStyle<'a, 'b, DB, CT> {
108    pub(super) fn new(target: &'b mut ChartContext<'a, DB, CT>) -> Self {
109        Self {
110            target,
111            position: SeriesLabelPosition::MiddleRight,
112            legend_area_size: 30,
113            border_style: (&TRANSPARENT).into(),
114            background: (&TRANSPARENT).into(),
115            label_font: None,
116            margin: 10,
117        }
118    }
119
120    /// Set the series label positioning style
121    /// `pos` - The positioning style
122    pub fn position(&mut self, pos: SeriesLabelPosition) -> &mut Self {
123        self.position = pos;
124        self
125    }
126
127    /// Set the margin of the series label drawing are
128    ///
129    /// - `value`: The size specification
130    pub fn margin<S: SizeDesc>(&mut self, value: S) -> &mut Self {
131        self.margin = value
132            .in_pixels(&self.target.plotting_area().dim_in_pixel())
133            .max(0) as u32;
134        self
135    }
136
137    /// Set the size of legend area
138    /// `size` - The size of legend area in pixel
139    pub fn legend_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
140        let size = size
141            .in_pixels(&self.target.plotting_area().dim_in_pixel())
142            .max(0) as u32;
143        self.legend_area_size = size;
144        self
145    }
146
147    /// Set the style of the label series area
148    /// `style` - The style of the border
149    pub fn border_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
150        self.border_style = style.into();
151        self
152    }
153
154    /// Set the background style
155    /// `style` - The style of the border
156    pub fn background_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
157        self.background = style.into();
158        self
159    }
160
161    /// Set the series label font
162    /// `font` - The font
163    pub fn label_font<F: IntoTextStyle<'b>>(&mut self, font: F) -> &mut Self {
164        self.label_font = Some(font.into_text_style(&self.target.plotting_area().dim_in_pixel()));
165        self
166    }
167
168    /// Draw the series label area
169    pub fn draw(&mut self) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
170        let drawing_area = self.target.plotting_area().strip_coord_spec();
171
172        // TODO: Issue #68 Currently generic font family doesn't load on OSX, change this after the issue
173        // resolved
174        let default_font = ("sans-serif", 12).into_font();
175        let default_style: TextStyle = default_font.into();
176
177        let font = {
178            let mut temp = None;
179            std::mem::swap(&mut self.label_font, &mut temp);
180            temp.unwrap_or(default_style)
181        };
182
183        let mut label_element = MultiLineText::<_, &str>::new((0, 0), &font);
184        let mut funcs = vec![];
185
186        for anno in self.target.series_anno.iter() {
187            let label_text = anno.get_label();
188            let draw_func = anno.get_draw_func();
189
190            if label_text == "" && draw_func.is_none() {
191                continue;
192            }
193
194            funcs.push(
195                draw_func.unwrap_or_else(|| &|p: BackendCoord| EmptyElement::at(p).into_dyn()),
196            );
197            label_element.push_line(label_text);
198        }
199
200        let (mut w, mut h) = label_element.estimate_dimension().map_err(|e| {
201            DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(Box::new(e)))
202        })?;
203
204        let margin = self.margin as i32;
205
206        w += self.legend_area_size as i32 + margin * 2;
207        h += margin * 2;
208
209        let (area_w, area_h) = drawing_area.dim_in_pixel();
210
211        let (label_x, label_y) = self.position.layout_label_area((w, h), (area_w, area_h));
212
213        label_element.relocate((
214            label_x + self.legend_area_size as i32 + margin,
215            label_y + margin,
216        ));
217
218        drawing_area.draw(&Rectangle::new(
219            [(label_x, label_y), (label_x + w, label_y + h)],
220            self.background.filled(),
221        ))?;
222        drawing_area.draw(&Rectangle::new(
223            [(label_x, label_y), (label_x + w, label_y + h)],
224            self.border_style.clone(),
225        ))?;
226        drawing_area.draw(&label_element)?;
227
228        for (((_, y0), (_, y1)), make_elem) in label_element
229            .compute_line_layout()
230            .map_err(|e| {
231                DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(Box::new(e)))
232            })?
233            .into_iter()
234            .zip(funcs.into_iter())
235        {
236            let legend_element = make_elem((label_x + margin, (y0 + y1) / 2));
237            drawing_area.draw(&legend_element)?;
238        }
239
240        Ok(())
241    }
242}