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    /**
38    Sets the series label for the current series.
39
40    See [`ChartContext::configure_series_labels()`] for more information and examples.
41    */
42    pub fn label<L: Into<String>>(&mut self, label: L) -> &mut Self {
43        self.label = Some(label.into());
44        self
45    }
46
47    /**
48    Sets the legend element creator function.
49
50    - `func`: The function use to create the element
51
52    # Note
53
54    The creation function uses a shifted pixel-based coordinate system, where the
55    point (0,0) is defined to the mid-right point of the shape.
56
57    # See also
58
59    See [`ChartContext::configure_series_labels()`] for more information and examples.
60    */
61    pub fn legend<E: IntoDynElement<'a, DB, BackendCoord>, T: Fn(BackendCoord) -> E + 'a>(
62        &mut self,
63        func: T,
64    ) -> &mut Self {
65        self.draw_func = Some(Box::new(move |p| func(p).into_dyn()));
66        self
67    }
68}
69
70/**
71Useful to specify the position of the series label.
72
73See [`ChartContext::configure_series_labels()`] for more information and examples.
74*/
75#[derive(Debug, Clone, PartialEq)]
76pub enum SeriesLabelPosition {
77    /// Places the series label at the upper left
78    UpperLeft,
79    /// Places the series label at the middle left
80    MiddleLeft,
81    /// Places the series label at the lower left
82    LowerLeft,
83    /// Places the series label at the upper middle
84    UpperMiddle,
85    /// Places the series label at the middle middle
86    MiddleMiddle,
87    /// Places the series label at the lower middle
88    LowerMiddle,
89    /// Places the series label at the upper right
90    UpperRight,
91    /// Places the series label at the middle right
92    MiddleRight,
93    /// Places the series label at the lower right
94    LowerRight,
95    /// Places the series label at the specific location in backend coordinates
96    Coordinate(i32, i32),
97}
98
99impl SeriesLabelPosition {
100    fn layout_label_area(&self, label_dim: (i32, i32), area_dim: (u32, u32)) -> (i32, i32) {
101        use SeriesLabelPosition::*;
102        (
103            match self {
104                UpperLeft | MiddleLeft | LowerLeft => 5,
105                UpperMiddle | MiddleMiddle | LowerMiddle => (area_dim.0 as i32 - label_dim.0) / 2,
106                UpperRight | MiddleRight | LowerRight => area_dim.0 as i32 - label_dim.0 - 5,
107                Coordinate(x, _) => *x,
108            },
109            match self {
110                UpperLeft | UpperMiddle | UpperRight => 5,
111                MiddleLeft | MiddleMiddle | MiddleRight => (area_dim.1 as i32 - label_dim.1) / 2,
112                LowerLeft | LowerMiddle | LowerRight => area_dim.1 as i32 - label_dim.1 - 5,
113                Coordinate(_, y) => *y,
114            },
115        )
116    }
117}
118
119/// The struct to specify the series label of a target chart context
120pub struct SeriesLabelStyle<'a, 'b, DB: DrawingBackend, CT: CoordTranslate> {
121    target: &'b mut ChartContext<'a, DB, CT>,
122    position: SeriesLabelPosition,
123    legend_area_size: u32,
124    border_style: ShapeStyle,
125    background: ShapeStyle,
126    label_font: Option<TextStyle<'b>>,
127    margin: u32,
128}
129
130impl<'a, 'b, DB: DrawingBackend + 'a, CT: CoordTranslate> SeriesLabelStyle<'a, 'b, DB, CT> {
131    pub(super) fn new(target: &'b mut ChartContext<'a, DB, CT>) -> Self {
132        Self {
133            target,
134            position: SeriesLabelPosition::MiddleRight,
135            legend_area_size: 30,
136            border_style: (&TRANSPARENT).into(),
137            background: (&TRANSPARENT).into(),
138            label_font: None,
139            margin: 10,
140        }
141    }
142
143    /**
144    Sets the series label positioning style
145
146    `pos` - The positioning style
147
148    See [`ChartContext::configure_series_labels()`] for more information and examples.
149    */
150    pub fn position(&mut self, pos: SeriesLabelPosition) -> &mut Self {
151        self.position = pos;
152        self
153    }
154
155    /**
156    Sets the margin of the series label drawing area.
157
158    - `value`: The size specification in backend units (pixels)
159
160    See [`ChartContext::configure_series_labels()`] for more information and examples.
161    */
162    pub fn margin<S: SizeDesc>(&mut self, value: S) -> &mut Self {
163        self.margin = value
164            .in_pixels(&self.target.plotting_area().dim_in_pixel())
165            .max(0) as u32;
166        self
167    }
168
169    /**
170    Sets the size of the legend area.
171
172    `size` - The size of legend area in backend units (pixels)
173
174    See [`ChartContext::configure_series_labels()`] for more information and examples.
175    */
176    pub fn legend_area_size<S: SizeDesc>(&mut self, size: S) -> &mut Self {
177        let size = size
178            .in_pixels(&self.target.plotting_area().dim_in_pixel())
179            .max(0) as u32;
180        self.legend_area_size = size;
181        self
182    }
183
184    /**
185    Sets the style of the label series area.
186
187    `style` - The style of the border
188
189    See [`ChartContext::configure_series_labels()`] for more information and examples.
190    */
191    pub fn border_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
192        self.border_style = style.into();
193        self
194    }
195
196    /**
197    Sets the background style of the label series area.
198
199    `style` - The style of the border
200
201    See [`ChartContext::configure_series_labels()`] for more information and examples.
202    */
203    pub fn background_style<S: Into<ShapeStyle>>(&mut self, style: S) -> &mut Self {
204        self.background = style.into();
205        self
206    }
207
208    /**
209    Sets the font for series labels.
210
211    `font` - Desired font
212
213    See [`ChartContext::configure_series_labels()`] for more information and examples.
214    */
215    pub fn label_font<F: IntoTextStyle<'b>>(&mut self, font: F) -> &mut Self {
216        self.label_font = Some(font.into_text_style(&self.target.plotting_area().dim_in_pixel()));
217        self
218    }
219
220    /**
221    Draws the series label area.
222
223    See [`ChartContext::configure_series_labels()`] for more information and examples.
224    */
225    pub fn draw(&mut self) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> {
226        let drawing_area = self.target.plotting_area().strip_coord_spec();
227
228        // TODO: Issue #68 Currently generic font family doesn't load on OSX, change this after the issue
229        // resolved
230        let default_font = ("sans-serif", 12).into_font();
231        let default_style: TextStyle = default_font.into();
232
233        let font = {
234            let mut temp = None;
235            std::mem::swap(&mut self.label_font, &mut temp);
236            temp.unwrap_or(default_style)
237        };
238
239        let mut label_element = MultiLineText::<_, &str>::new((0, 0), &font);
240        let mut funcs = vec![];
241
242        for anno in self.target.series_anno.iter() {
243            let label_text = anno.get_label();
244            let draw_func = anno.get_draw_func();
245
246            if label_text.is_empty() && draw_func.is_none() {
247                continue;
248            }
249
250            funcs.push(draw_func.unwrap_or(&|p: BackendCoord| EmptyElement::at(p).into_dyn()));
251            label_element.push_line(label_text);
252        }
253
254        let (mut w, mut h) = label_element.estimate_dimension().map_err(|e| {
255            DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(Box::new(e)))
256        })?;
257
258        let margin = self.margin as i32;
259
260        w += self.legend_area_size as i32 + margin * 2;
261        h += margin * 2;
262
263        let (area_w, area_h) = drawing_area.dim_in_pixel();
264
265        let (label_x, label_y) = self.position.layout_label_area((w, h), (area_w, area_h));
266
267        label_element.relocate((
268            label_x + self.legend_area_size as i32 + margin,
269            label_y + margin,
270        ));
271
272        drawing_area.draw(&Rectangle::new(
273            [(label_x, label_y), (label_x + w, label_y + h)],
274            self.background.filled(),
275        ))?;
276        drawing_area.draw(&Rectangle::new(
277            [(label_x, label_y), (label_x + w, label_y + h)],
278            self.border_style,
279        ))?;
280        drawing_area.draw(&label_element)?;
281
282        for (((_, y0), (_, y1)), make_elem) in label_element
283            .compute_line_layout()
284            .map_err(|e| {
285                DrawingAreaErrorKind::BackendError(DrawingErrorKind::FontError(Box::new(e)))
286            })?
287            .into_iter()
288            .zip(funcs.into_iter())
289        {
290            let legend_element = make_elem((label_x + margin, (y0 + y1) / 2));
291            drawing_area.draw(&legend_element)?;
292        }
293
294        Ok(())
295    }
296}