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