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