1 use std::ops::Range; 2 3 use plotters_backend::DrawingBackend; 4 5 use crate::chart::ChartContext; 6 use crate::coord::{ 7 cartesian::{Cartesian2d, MeshLine}, 8 ranged1d::{KeyPointHint, Ranged}, 9 Shift, 10 }; 11 use crate::drawing::{DrawingArea, DrawingAreaErrorKind}; 12 use crate::element::PathElement; 13 use crate::style::{ 14 text_anchor::{HPos, Pos, VPos}, 15 FontTransform, ShapeStyle, TextStyle, 16 }; 17 18 impl<'a, DB: DrawingBackend, X: Ranged, Y: Ranged> ChartContext<'a, DB, Cartesian2d<X, Y>> { 19 /// The actual function that draws the mesh lines. 20 /// It also returns the label that suppose to be there. 21 #[allow(clippy::type_complexity)] draw_mesh_lines<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>( &mut self, (r, c): (YH, XH), (x_mesh, y_mesh): (bool, bool), mesh_line_style: &ShapeStyle, mut fmt_label: FmtLabel, ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>> where FmtLabel: FnMut(&X, &Y, &MeshLine<X, Y>) -> Option<String>,22 fn draw_mesh_lines<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>( 23 &mut self, 24 (r, c): (YH, XH), 25 (x_mesh, y_mesh): (bool, bool), 26 mesh_line_style: &ShapeStyle, 27 mut fmt_label: FmtLabel, 28 ) -> Result<(Vec<(i32, String)>, Vec<(i32, String)>), DrawingAreaErrorKind<DB::ErrorType>> 29 where 30 FmtLabel: FnMut(&X, &Y, &MeshLine<X, Y>) -> Option<String>, 31 { 32 let mut x_labels = vec![]; 33 let mut y_labels = vec![]; 34 let xr = self.drawing_area.as_coord_spec().x_spec(); 35 let yr = self.drawing_area.as_coord_spec().y_spec(); 36 self.drawing_area.draw_mesh( 37 |b, l| { 38 let draw = match l { 39 MeshLine::XMesh((x, _), _, _) => { 40 if let Some(label_text) = fmt_label(xr, yr, &l) { 41 x_labels.push((x, label_text)); 42 } 43 x_mesh 44 } 45 MeshLine::YMesh((_, y), _, _) => { 46 if let Some(label_text) = fmt_label(xr, yr, &l) { 47 y_labels.push((y, label_text)); 48 } 49 y_mesh 50 } 51 }; 52 if draw { 53 l.draw(b, mesh_line_style) 54 } else { 55 Ok(()) 56 } 57 }, 58 r, 59 c, 60 )?; 61 Ok((x_labels, y_labels)) 62 } 63 draw_axis( &self, area: &DrawingArea<DB, Shift>, axis_style: Option<&ShapeStyle>, orientation: (i16, i16), inward_labels: bool, ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>>64 fn draw_axis( 65 &self, 66 area: &DrawingArea<DB, Shift>, 67 axis_style: Option<&ShapeStyle>, 68 orientation: (i16, i16), 69 inward_labels: bool, 70 ) -> Result<Range<i32>, DrawingAreaErrorKind<DB::ErrorType>> { 71 let (x0, y0) = self.drawing_area.get_base_pixel(); 72 let (tw, th) = area.dim_in_pixel(); 73 74 let mut axis_range = if orientation.0 == 0 { 75 self.drawing_area.get_x_axis_pixel_range() 76 } else { 77 self.drawing_area.get_y_axis_pixel_range() 78 }; 79 80 // At this point, the coordinate system tells us the pixel range after the translation. 81 // However, we need to use the logic coordinate system for drawing. 82 if orientation.0 == 0 { 83 axis_range.start -= x0; 84 axis_range.end -= x0; 85 } else { 86 axis_range.start -= y0; 87 axis_range.end -= y0; 88 } 89 90 if let Some(axis_style) = axis_style { 91 let mut x0 = if orientation.0 > 0 { 0 } else { tw as i32 - 1 }; 92 let mut y0 = if orientation.1 > 0 { 0 } else { th as i32 - 1 }; 93 let mut x1 = if orientation.0 >= 0 { 0 } else { tw as i32 - 1 }; 94 let mut y1 = if orientation.1 >= 0 { 0 } else { th as i32 - 1 }; 95 96 if inward_labels { 97 if orientation.0 == 0 { 98 if y0 == 0 { 99 y0 = th as i32 - 1; 100 y1 = th as i32 - 1; 101 } else { 102 y0 = 0; 103 y1 = 0; 104 } 105 } else if x0 == 0 { 106 x0 = tw as i32 - 1; 107 x1 = tw as i32 - 1; 108 } else { 109 x0 = 0; 110 x1 = 0; 111 } 112 } 113 114 if orientation.0 == 0 { 115 x0 = axis_range.start; 116 x1 = axis_range.end; 117 } else { 118 y0 = axis_range.start; 119 y1 = axis_range.end; 120 } 121 122 area.draw(&PathElement::new(vec![(x0, y0), (x1, y1)], *axis_style))?; 123 } 124 125 Ok(axis_range) 126 } 127 128 // TODO: consider make this function less complicated 129 #[allow(clippy::too_many_arguments)] 130 #[allow(clippy::cognitive_complexity)] draw_axis_and_labels( &self, area: Option<&DrawingArea<DB, Shift>>, axis_style: Option<&ShapeStyle>, labels: &[(i32, String)], label_style: &TextStyle, label_offset: i32, orientation: (i16, i16), axis_desc: Option<(&str, &TextStyle)>, tick_size: i32, ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>>131 fn draw_axis_and_labels( 132 &self, 133 area: Option<&DrawingArea<DB, Shift>>, 134 axis_style: Option<&ShapeStyle>, 135 labels: &[(i32, String)], 136 label_style: &TextStyle, 137 label_offset: i32, 138 orientation: (i16, i16), 139 axis_desc: Option<(&str, &TextStyle)>, 140 tick_size: i32, 141 ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> { 142 let area = if let Some(target) = area { 143 target 144 } else { 145 return Ok(()); 146 }; 147 148 let (x0, y0) = self.drawing_area.get_base_pixel(); 149 let (tw, th) = area.dim_in_pixel(); 150 151 /* This is the minimal distance from the axis to the box of the labels */ 152 let label_dist = tick_size.abs() * 2; 153 154 /* Draw the axis and get the axis range so that we can do further label 155 * and tick mark drawing */ 156 let axis_range = self.draw_axis(area, axis_style, orientation, tick_size < 0)?; 157 158 /* To make the right label area looks nice, it's a little bit tricky, since for a that is 159 * very long, we actually prefer left alignment instead of right alignment. 160 * Otherwise, the right alignment looks better. So we estimate the max and min label width 161 * So that we are able decide if we should apply right alignment for the text. */ 162 let label_width: Vec<_> = labels 163 .iter() 164 .map(|(_, text)| { 165 if orientation.0 > 0 && orientation.1 == 0 && tick_size >= 0 { 166 self.drawing_area 167 .estimate_text_size(text, label_style) 168 .map(|(w, _)| w) 169 .unwrap_or(0) as i32 170 } else { 171 // Don't ever do the layout estimationfor the drawing area that is either not 172 // the right one or the tick mark is inward. 173 0 174 } 175 }) 176 .collect(); 177 178 let min_width = *label_width.iter().min().unwrap_or(&1).max(&1); 179 let max_width = *label_width 180 .iter() 181 .filter(|&&x| x < min_width * 2) 182 .max() 183 .unwrap_or(&min_width); 184 let right_align_width = (min_width * 2).min(max_width); 185 186 /* Then we need to draw the tick mark and the label */ 187 for ((p, t), w) in labels.iter().zip(label_width.into_iter()) { 188 /* Make sure we are actually in the visible range */ 189 let rp = if orientation.0 == 0 { *p - x0 } else { *p - y0 }; 190 191 if rp < axis_range.start.min(axis_range.end) 192 || axis_range.end.max(axis_range.start) < rp 193 { 194 continue; 195 } 196 197 let (cx, cy, h_pos, v_pos) = if tick_size >= 0 { 198 match orientation { 199 // Right 200 (dx, dy) if dx > 0 && dy == 0 => { 201 if w >= right_align_width { 202 (label_dist, *p - y0, HPos::Left, VPos::Center) 203 } else { 204 ( 205 label_dist + right_align_width, 206 *p - y0, 207 HPos::Right, 208 VPos::Center, 209 ) 210 } 211 } 212 // Left 213 (dx, dy) if dx < 0 && dy == 0 => { 214 (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center) 215 } 216 // Bottom 217 (dx, dy) if dx == 0 && dy > 0 => (*p - x0, label_dist, HPos::Center, VPos::Top), 218 // Top 219 (dx, dy) if dx == 0 && dy < 0 => { 220 (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom) 221 } 222 _ => panic!("Bug: Invalid orientation specification"), 223 } 224 } else { 225 match orientation { 226 // Right 227 (dx, dy) if dx > 0 && dy == 0 => { 228 (tw as i32 - label_dist, *p - y0, HPos::Right, VPos::Center) 229 } 230 // Left 231 (dx, dy) if dx < 0 && dy == 0 => { 232 (label_dist, *p - y0, HPos::Left, VPos::Center) 233 } 234 // Bottom 235 (dx, dy) if dx == 0 && dy > 0 => { 236 (*p - x0, th as i32 - label_dist, HPos::Center, VPos::Bottom) 237 } 238 // Top 239 (dx, dy) if dx == 0 && dy < 0 => (*p - x0, label_dist, HPos::Center, VPos::Top), 240 _ => panic!("Bug: Invalid orientation specification"), 241 } 242 }; 243 244 let (text_x, text_y) = if orientation.0 == 0 { 245 (cx + label_offset, cy) 246 } else { 247 (cx, cy + label_offset) 248 }; 249 250 let label_style = &label_style.pos(Pos::new(h_pos, v_pos)); 251 area.draw_text(t, label_style, (text_x, text_y))?; 252 253 if tick_size != 0 { 254 if let Some(style) = axis_style { 255 let xmax = tw as i32 - 1; 256 let ymax = th as i32 - 1; 257 let (kx0, ky0, kx1, ky1) = if tick_size > 0 { 258 match orientation { 259 (dx, dy) if dx > 0 && dy == 0 => (0, *p - y0, tick_size, *p - y0), 260 (dx, dy) if dx < 0 && dy == 0 => { 261 (xmax - tick_size, *p - y0, xmax, *p - y0) 262 } 263 (dx, dy) if dx == 0 && dy > 0 => (*p - x0, 0, *p - x0, tick_size), 264 (dx, dy) if dx == 0 && dy < 0 => { 265 (*p - x0, ymax - tick_size, *p - x0, ymax) 266 } 267 _ => panic!("Bug: Invalid orientation specification"), 268 } 269 } else { 270 match orientation { 271 (dx, dy) if dx > 0 && dy == 0 => { 272 (xmax, *p - y0, xmax + tick_size, *p - y0) 273 } 274 (dx, dy) if dx < 0 && dy == 0 => (0, *p - y0, -tick_size, *p - y0), 275 (dx, dy) if dx == 0 && dy > 0 => { 276 (*p - x0, ymax, *p - x0, ymax + tick_size) 277 } 278 (dx, dy) if dx == 0 && dy < 0 => (*p - x0, 0, *p - x0, -tick_size), 279 _ => panic!("Bug: Invalid orientation specification"), 280 } 281 }; 282 let line = PathElement::new(vec![(kx0, ky0), (kx1, ky1)], *style); 283 area.draw(&line)?; 284 } 285 } 286 } 287 288 if let Some((text, style)) = axis_desc { 289 let actual_style = if orientation.0 == 0 { 290 style.clone() 291 } else if orientation.0 == -1 { 292 style.transform(FontTransform::Rotate270) 293 } else { 294 style.transform(FontTransform::Rotate90) 295 }; 296 297 let (x0, y0, h_pos, v_pos) = match orientation { 298 // Right 299 (dx, dy) if dx > 0 && dy == 0 => (tw, th / 2, HPos::Center, VPos::Top), 300 // Left 301 (dx, dy) if dx < 0 && dy == 0 => (0, th / 2, HPos::Center, VPos::Top), 302 // Bottom 303 (dx, dy) if dx == 0 && dy > 0 => (tw / 2, th, HPos::Center, VPos::Bottom), 304 // Top 305 (dx, dy) if dx == 0 && dy < 0 => (tw / 2, 0, HPos::Center, VPos::Top), 306 _ => panic!("Bug: Invalid orientation specification"), 307 }; 308 309 let actual_style = &actual_style.pos(Pos::new(h_pos, v_pos)); 310 area.draw_text(text, actual_style, (x0 as i32, y0 as i32))?; 311 } 312 313 Ok(()) 314 } 315 316 #[allow(clippy::too_many_arguments)] draw_mesh<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>( &mut self, (r, c): (YH, XH), mesh_line_style: &ShapeStyle, x_label_style: &TextStyle, y_label_style: &TextStyle, fmt_label: FmtLabel, x_mesh: bool, y_mesh: bool, x_label_offset: i32, y_label_offset: i32, x_axis: bool, y_axis: bool, axis_style: &ShapeStyle, axis_desc_style: &TextStyle, x_desc: Option<String>, y_desc: Option<String>, x_tick_size: [i32; 2], y_tick_size: [i32; 2], ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> where FmtLabel: FnMut(&X, &Y, &MeshLine<X, Y>) -> Option<String>,317 pub(crate) fn draw_mesh<FmtLabel, YH: KeyPointHint, XH: KeyPointHint>( 318 &mut self, 319 (r, c): (YH, XH), 320 mesh_line_style: &ShapeStyle, 321 x_label_style: &TextStyle, 322 y_label_style: &TextStyle, 323 fmt_label: FmtLabel, 324 x_mesh: bool, 325 y_mesh: bool, 326 x_label_offset: i32, 327 y_label_offset: i32, 328 x_axis: bool, 329 y_axis: bool, 330 axis_style: &ShapeStyle, 331 axis_desc_style: &TextStyle, 332 x_desc: Option<String>, 333 y_desc: Option<String>, 334 x_tick_size: [i32; 2], 335 y_tick_size: [i32; 2], 336 ) -> Result<(), DrawingAreaErrorKind<DB::ErrorType>> 337 where 338 FmtLabel: FnMut(&X, &Y, &MeshLine<X, Y>) -> Option<String>, 339 { 340 let (x_labels, y_labels) = 341 self.draw_mesh_lines((r, c), (x_mesh, y_mesh), mesh_line_style, fmt_label)?; 342 343 for idx in 0..2 { 344 self.draw_axis_and_labels( 345 self.x_label_area[idx].as_ref(), 346 if x_axis { Some(axis_style) } else { None }, 347 &x_labels[..], 348 x_label_style, 349 x_label_offset, 350 (0, -1 + idx as i16 * 2), 351 x_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)), 352 x_tick_size[idx], 353 )?; 354 355 self.draw_axis_and_labels( 356 self.y_label_area[idx].as_ref(), 357 if y_axis { Some(axis_style) } else { None }, 358 &y_labels[..], 359 y_label_style, 360 y_label_offset, 361 (-1 + idx as i16 * 2, 0), 362 y_desc.as_ref().map(|desc| (&desc[..], axis_desc_style)), 363 y_tick_size[idx], 364 )?; 365 } 366 367 Ok(()) 368 } 369 } 370