1use crate::coord::Coord;
2use crate::render::backend::{DrawBackend, LineStyle, TextAnchor, TextStyle};
3use crate::render::{Rect, RenderError};
4use crate::scale::Scale;
5use crate::theme::Theme;
6
7fn along_x(coord: &dyn Coord, pos: f64, area: &Rect) -> f64 {
11 let p = if coord.is_flipped() {
12 (0.0, pos)
13 } else {
14 (pos, 0.0)
15 };
16 coord.transform(p, area).0
17}
18
19fn along_y(coord: &dyn Coord, pos: f64, area: &Rect) -> f64 {
21 let p = if coord.is_flipped() {
22 (pos, 0.0)
23 } else {
24 (0.0, pos)
25 };
26 coord.transform(p, area).1
27}
28
29pub fn draw_x_axis(
31 scale: &dyn Scale,
32 coord: &dyn Coord,
33 theme: &Theme,
34 plot_area: &Rect,
35 backend: &mut dyn DrawBackend,
36) -> Result<(), RenderError> {
37 let breaks = scale.breaks();
38 let tick_len = theme.axis_ticks_length;
39 let axis_line = theme.get_axis_line_x();
40 let axis_ticks = theme.get_axis_ticks_x();
41
42 let top = scale.axis_position_opposite();
44 let edge_y = if top {
45 plot_area.y
46 } else {
47 plot_area.y + plot_area.height
48 };
49 let dir = if top { -1.0 } else { 1.0 };
50
51 if axis_line.visible {
53 backend.draw_line(
54 &[
55 (plot_area.x, edge_y),
56 (plot_area.x + plot_area.width, edge_y),
57 ],
58 &LineStyle {
59 color: axis_line.color,
60 width: axis_line.width,
61 alpha: 1.0,
62 linetype: axis_line.linetype,
63 },
64 )?;
65 }
66
67 let dodge = theme.axis_text_x_dodge.max(1);
69 for (i, (pos, label)) in breaks.iter().enumerate() {
70 let px = along_x(coord, *pos, plot_area);
71
72 if axis_ticks.visible {
73 backend.draw_line(
74 &[(px, edge_y), (px, edge_y + dir * tick_len)],
75 &LineStyle {
76 color: axis_ticks.color,
77 width: axis_ticks.width,
78 alpha: 1.0,
79 linetype: axis_ticks.linetype,
80 },
81 )?;
82 }
83
84 if theme.axis_text_x.visible {
85 let family = if theme.axis_text_x.family.is_empty() {
86 None
87 } else {
88 Some(theme.axis_text_x.family.clone())
89 };
90 let anchor = if theme.axis_text_x.angle.abs() > 10.0 {
92 TextAnchor::End
93 } else {
94 TextAnchor::Middle
95 };
96 let row_offset = (i % dodge) as f64 * (theme.axis_text_x.size + 2.0);
98 backend.draw_text(
99 label,
100 (
101 px,
102 edge_y
103 + dir
104 * (tick_len
105 + theme.legend_spacing / 2.0
106 + theme.axis_text_x.size / 2.0
107 + row_offset),
108 ),
109 &TextStyle {
110 color: theme.axis_text_x.color,
111 size: theme.axis_text_x.size,
112 anchor,
113 angle: theme.axis_text_x.angle,
114 family,
115 face: theme.axis_text_x.face,
116 },
117 )?;
118 }
119 }
120
121 if theme.axis_minor_ticks && axis_ticks.visible {
123 for pos in minor_breaks(&breaks) {
124 let px = along_x(coord, pos, plot_area);
125 backend.draw_line(
126 &[(px, edge_y), (px, edge_y + dir * tick_len * 0.5)],
127 &LineStyle {
128 color: axis_ticks.color,
129 width: axis_ticks.width,
130 alpha: 1.0,
131 linetype: axis_ticks.linetype,
132 },
133 )?;
134 }
135 }
136
137 let title = scale.name();
139 if !title.is_empty() && theme.axis_title_x.visible {
140 let hjust = theme.axis_title_x.hjust.clamp(0.0, 1.0);
141 let center_x = plot_area.x + hjust * plot_area.width;
142 let anchor = if hjust <= 0.02 {
143 TextAnchor::Start
144 } else if hjust >= 0.98 {
145 TextAnchor::End
146 } else {
147 TextAnchor::Middle
148 };
149 let title_y = edge_y
150 + dir * (tick_len + theme.axis_text_x.size + 8.0 + theme.axis_title_x.size / 2.0);
151 let family = if theme.axis_title_x.family.is_empty() {
152 None
153 } else {
154 Some(theme.axis_title_x.family.clone())
155 };
156 backend.draw_text(
157 title,
158 (center_x, title_y),
159 &TextStyle {
160 color: theme.axis_title_x.color,
161 size: theme.axis_title_x.size,
162 anchor,
163 angle: 0.0,
164 family,
165 face: theme.axis_title_x.face,
166 },
167 )?;
168 }
169
170 Ok(())
171}
172
173pub fn draw_y_axis(
175 scale: &dyn Scale,
176 coord: &dyn Coord,
177 theme: &Theme,
178 plot_area: &Rect,
179 backend: &mut dyn DrawBackend,
180) -> Result<(), RenderError> {
181 let breaks = scale.breaks();
182 let tick_len = theme.axis_ticks_length;
183 let axis_line = theme.get_axis_line_y();
184 let axis_ticks = theme.get_axis_ticks_y();
185
186 if axis_line.visible {
188 let top = (plot_area.x, plot_area.y);
189 let bottom = (plot_area.x, plot_area.y + plot_area.height);
190 backend.draw_line(
191 &[top, bottom],
192 &LineStyle {
193 color: axis_line.color,
194 width: axis_line.width,
195 alpha: 1.0,
196 linetype: axis_line.linetype,
197 },
198 )?;
199 }
200
201 for (pos, label) in &breaks {
203 let py = along_y(coord, *pos, plot_area);
204
205 if axis_ticks.visible {
206 backend.draw_line(
207 &[(plot_area.x - tick_len, py), (plot_area.x, py)],
208 &LineStyle {
209 color: axis_ticks.color,
210 width: axis_ticks.width,
211 alpha: 1.0,
212 linetype: axis_ticks.linetype,
213 },
214 )?;
215 }
216
217 if theme.axis_text_y.visible {
218 let family = if theme.axis_text_y.family.is_empty() {
219 None
220 } else {
221 Some(theme.axis_text_y.family.clone())
222 };
223 backend.draw_text(
224 label,
225 (plot_area.x - tick_len - theme.legend_spacing, py),
226 &TextStyle {
227 color: theme.axis_text_y.color,
228 size: theme.axis_text_y.size,
229 anchor: TextAnchor::End,
230 angle: theme.axis_text_y.angle,
231 family,
232 face: theme.axis_text_y.face,
233 },
234 )?;
235 }
236 }
237
238 if theme.axis_minor_ticks && axis_ticks.visible {
240 for pos in minor_breaks(&breaks) {
241 let py = along_y(coord, pos, plot_area);
242 backend.draw_line(
243 &[(plot_area.x - tick_len * 0.5, py), (plot_area.x, py)],
244 &LineStyle {
245 color: axis_ticks.color,
246 width: axis_ticks.width,
247 alpha: 1.0,
248 linetype: axis_ticks.linetype,
249 },
250 )?;
251 }
252 }
253
254 let title = scale.name();
256 if !title.is_empty() && theme.axis_title_y.visible {
257 let title_x = plot_area.x - tick_len - theme.axis_text_y.size * 3.5 - theme.legend_spacing;
258 let center_y = plot_area.y + plot_area.height / 2.0;
259 let family = if theme.axis_title_y.family.is_empty() {
260 None
261 } else {
262 Some(theme.axis_title_y.family.clone())
263 };
264 backend.draw_text(
265 title,
266 (title_x, center_y),
267 &TextStyle {
268 color: theme.axis_title_y.color,
269 size: theme.axis_title_y.size,
270 anchor: TextAnchor::Middle,
271 angle: 270.0,
272 family,
273 face: theme.axis_title_y.face,
274 },
275 )?;
276 }
277
278 Ok(())
279}
280
281pub fn draw_sec_y_axis(
283 primary_scale: &dyn Scale,
284 sec_axis: &crate::scale::sec_axis::SecAxis,
285 coord: &dyn Coord,
286 theme: &Theme,
287 plot_area: &Rect,
288 backend: &mut dyn DrawBackend,
289) -> Result<(), RenderError> {
290 let breaks = primary_scale.breaks();
291 let tick_len = theme.axis_ticks_length;
292 let axis_line = theme.get_axis_line_y();
293 let axis_ticks = theme.get_axis_ticks_y();
294
295 let right_x = plot_area.x + plot_area.width;
296
297 if axis_line.visible {
299 backend.draw_line(
300 &[
301 (right_x, plot_area.y),
302 (right_x, plot_area.y + plot_area.height),
303 ],
304 &LineStyle {
305 color: axis_line.color,
306 width: axis_line.width,
307 alpha: 1.0,
308 linetype: axis_line.linetype,
309 },
310 )?;
311 }
312
313 for (pos, label) in &breaks {
315 let py = along_y(coord, *pos, plot_area);
316
317 if axis_ticks.visible {
318 backend.draw_line(
319 &[(right_x, py), (right_x + tick_len, py)],
320 &LineStyle {
321 color: axis_ticks.color,
322 width: axis_ticks.width,
323 alpha: 1.0,
324 linetype: axis_ticks.linetype,
325 },
326 )?;
327 }
328
329 if theme.axis_text_y.visible {
330 let locale = primary_scale.locale();
332 let parsed = match locale {
333 Some(l) => l.parse_number(label),
334 None => label.parse::<f64>().ok(),
335 };
336 let sec_label = if let Some(v) = parsed {
337 let transformed = sec_axis.transform_value(v);
338 crate::scale::util::format_number_opt(transformed, locale)
339 } else {
340 label.clone()
341 };
342
343 let family = if theme.axis_text_y.family.is_empty() {
344 None
345 } else {
346 Some(theme.axis_text_y.family.clone())
347 };
348 backend.draw_text(
349 &sec_label,
350 (right_x + tick_len + theme.legend_spacing, py),
351 &TextStyle {
352 color: theme.axis_text_y.color,
353 size: theme.axis_text_y.size,
354 anchor: TextAnchor::Start,
355 angle: theme.axis_text_y.angle,
356 family,
357 face: theme.axis_text_y.face,
358 },
359 )?;
360 }
361 }
362
363 if !sec_axis.name.is_empty() && theme.axis_title_y.visible {
365 let title_x = right_x + tick_len + theme.axis_text_y.size * 3.5 + theme.legend_spacing;
366 let center_y = plot_area.y + plot_area.height / 2.0;
367 let family = if theme.axis_title_y.family.is_empty() {
368 None
369 } else {
370 Some(theme.axis_title_y.family.clone())
371 };
372 backend.draw_text(
373 &sec_axis.name,
374 (title_x, center_y),
375 &TextStyle {
376 color: theme.axis_title_y.color,
377 size: theme.axis_title_y.size,
378 anchor: TextAnchor::Middle,
379 angle: 90.0,
380 family,
381 face: theme.axis_title_y.face,
382 },
383 )?;
384 }
385
386 Ok(())
387}
388
389fn minor_breaks(major: &[(f64, String)]) -> Vec<f64> {
391 if major.len() < 2 {
392 return vec![];
393 }
394 let mut minors = Vec::with_capacity(major.len() - 1);
395 for pair in major.windows(2) {
396 minors.push((pair[0].0 + pair[1].0) / 2.0);
397 }
398 minors
399}
400
401pub fn draw_gridlines(
403 x_scale: &dyn Scale,
404 y_scale: &dyn Scale,
405 coord: &dyn Coord,
406 theme: &Theme,
407 plot_area: &Rect,
408 backend: &mut dyn DrawBackend,
409) -> Result<(), RenderError> {
410 let major_x = theme.get_panel_grid_major_x();
411 let major_y = theme.get_panel_grid_major_y();
412 let minor_x = theme.get_panel_grid_minor_x();
413 let minor_y = theme.get_panel_grid_minor_y();
414
415 let x_breaks = x_scale.breaks();
416 let y_breaks = y_scale.breaks();
417
418 if minor_x.visible {
420 for pos in minor_breaks(&x_breaks) {
421 let px = along_x(coord, pos, plot_area);
422 backend.draw_line(
423 &[(px, plot_area.y), (px, plot_area.y + plot_area.height)],
424 &LineStyle {
425 color: minor_x.color,
426 width: minor_x.width,
427 alpha: 1.0,
428 linetype: minor_x.linetype,
429 },
430 )?;
431 }
432 }
433
434 if minor_y.visible {
436 for pos in minor_breaks(&y_breaks) {
437 let py = along_y(coord, pos, plot_area);
438 backend.draw_line(
439 &[(plot_area.x, py), (plot_area.x + plot_area.width, py)],
440 &LineStyle {
441 color: minor_y.color,
442 width: minor_y.width,
443 alpha: 1.0,
444 linetype: minor_y.linetype,
445 },
446 )?;
447 }
448 }
449
450 if major_x.visible {
452 for (pos, _) in &x_breaks {
453 let px = along_x(coord, *pos, plot_area);
454 backend.draw_line(
455 &[(px, plot_area.y), (px, plot_area.y + plot_area.height)],
456 &LineStyle {
457 color: major_x.color,
458 width: major_x.width,
459 alpha: 1.0,
460 linetype: major_x.linetype,
461 },
462 )?;
463 }
464 }
465
466 if major_y.visible {
468 for (pos, _) in &y_breaks {
469 let py = along_y(coord, *pos, plot_area);
470 backend.draw_line(
471 &[(plot_area.x, py), (plot_area.x + plot_area.width, py)],
472 &LineStyle {
473 color: major_y.color,
474 width: major_y.width,
475 alpha: 1.0,
476 linetype: major_y.linetype,
477 },
478 )?;
479 }
480 }
481
482 Ok(())
483}