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 sec_label = if let Ok(v) = label.parse::<f64>() {
332 let transformed = sec_axis.transform_value(v);
333 crate::scale::util::format_number(transformed)
334 } else {
335 label.clone()
336 };
337
338 let family = if theme.axis_text_y.family.is_empty() {
339 None
340 } else {
341 Some(theme.axis_text_y.family.clone())
342 };
343 backend.draw_text(
344 &sec_label,
345 (right_x + tick_len + theme.legend_spacing, py),
346 &TextStyle {
347 color: theme.axis_text_y.color,
348 size: theme.axis_text_y.size,
349 anchor: TextAnchor::Start,
350 angle: theme.axis_text_y.angle,
351 family,
352 face: theme.axis_text_y.face,
353 },
354 )?;
355 }
356 }
357
358 if !sec_axis.name.is_empty() && theme.axis_title_y.visible {
360 let title_x = right_x + tick_len + theme.axis_text_y.size * 3.5 + theme.legend_spacing;
361 let center_y = plot_area.y + plot_area.height / 2.0;
362 let family = if theme.axis_title_y.family.is_empty() {
363 None
364 } else {
365 Some(theme.axis_title_y.family.clone())
366 };
367 backend.draw_text(
368 &sec_axis.name,
369 (title_x, center_y),
370 &TextStyle {
371 color: theme.axis_title_y.color,
372 size: theme.axis_title_y.size,
373 anchor: TextAnchor::Middle,
374 angle: 90.0,
375 family,
376 face: theme.axis_title_y.face,
377 },
378 )?;
379 }
380
381 Ok(())
382}
383
384fn minor_breaks(major: &[(f64, String)]) -> Vec<f64> {
386 if major.len() < 2 {
387 return vec![];
388 }
389 let mut minors = Vec::with_capacity(major.len() - 1);
390 for pair in major.windows(2) {
391 minors.push((pair[0].0 + pair[1].0) / 2.0);
392 }
393 minors
394}
395
396pub fn draw_gridlines(
398 x_scale: &dyn Scale,
399 y_scale: &dyn Scale,
400 coord: &dyn Coord,
401 theme: &Theme,
402 plot_area: &Rect,
403 backend: &mut dyn DrawBackend,
404) -> Result<(), RenderError> {
405 let major_x = theme.get_panel_grid_major_x();
406 let major_y = theme.get_panel_grid_major_y();
407 let minor_x = theme.get_panel_grid_minor_x();
408 let minor_y = theme.get_panel_grid_minor_y();
409
410 let x_breaks = x_scale.breaks();
411 let y_breaks = y_scale.breaks();
412
413 if minor_x.visible {
415 for pos in minor_breaks(&x_breaks) {
416 let px = along_x(coord, pos, plot_area);
417 backend.draw_line(
418 &[(px, plot_area.y), (px, plot_area.y + plot_area.height)],
419 &LineStyle {
420 color: minor_x.color,
421 width: minor_x.width,
422 alpha: 1.0,
423 linetype: minor_x.linetype,
424 },
425 )?;
426 }
427 }
428
429 if minor_y.visible {
431 for pos in minor_breaks(&y_breaks) {
432 let py = along_y(coord, pos, plot_area);
433 backend.draw_line(
434 &[(plot_area.x, py), (plot_area.x + plot_area.width, py)],
435 &LineStyle {
436 color: minor_y.color,
437 width: minor_y.width,
438 alpha: 1.0,
439 linetype: minor_y.linetype,
440 },
441 )?;
442 }
443 }
444
445 if major_x.visible {
447 for (pos, _) in &x_breaks {
448 let px = along_x(coord, *pos, plot_area);
449 backend.draw_line(
450 &[(px, plot_area.y), (px, plot_area.y + plot_area.height)],
451 &LineStyle {
452 color: major_x.color,
453 width: major_x.width,
454 alpha: 1.0,
455 linetype: major_x.linetype,
456 },
457 )?;
458 }
459 }
460
461 if major_y.visible {
463 for (pos, _) in &y_breaks {
464 let py = along_y(coord, *pos, plot_area);
465 backend.draw_line(
466 &[(plot_area.x, py), (plot_area.x + plot_area.width, py)],
467 &LineStyle {
468 color: major_y.color,
469 width: major_y.width,
470 alpha: 1.0,
471 linetype: major_y.linetype,
472 },
473 )?;
474 }
475 }
476
477 Ok(())
478}