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)] |

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 | |

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)] |

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)] |

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 |