1use crate::axis::{format_number, Axis, AxisPosition, AxisScale};
5use crate::kernel::Coordinate;
6use crate::measure::{AxisOrientation, Bounds};
7use crate::scene::*;
8use crate::series::{Series, SeriesKind};
9use ferroui_base::{media::Color, Point, Rect, Size};
10use std::collections::BTreeMap;
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct Section {
14 pub x_axis: usize,
15 pub y_axis: usize,
16 pub x_min: Option<f64>,
17 pub x_max: Option<f64>,
18 pub y_min: Option<f64>,
19 pub y_max: Option<f64>,
20 pub fill: Option<Color>,
21 pub stroke: Option<Color>,
22 pub stroke_width: f64,
23}
24impl Default for Section {
25 fn default() -> Self {
26 Self {
27 x_axis: 0,
28 y_axis: 0,
29 x_min: None,
30 x_max: None,
31 y_min: None,
32 y_max: None,
33 fill: Some(Color::from_argb(35, 66, 99, 235)),
34 stroke: None,
35 stroke_width: 1.0,
36 }
37 }
38}
39
40#[derive(Clone, Debug, PartialEq)]
41pub struct CartesianOptions {
42 pub draw_margin: Option<Rect>,
44 pub sections: Vec<Section>,
45}
46impl Default for CartesianOptions {
47 fn default() -> Self {
48 Self {
49 draw_margin: None,
50 sections: Vec::new(),
51 }
52 }
53}
54
55#[derive(Clone, Debug, PartialEq)]
57pub struct CartesianMeasurement {
58 pub scene: Scene,
59 pub x_scales: Vec<AxisScale>,
60 pub y_scales: Vec<AxisScale>,
61 pub series_bounds: Vec<Option<CartesianSeriesBounds>>,
63}
64
65#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct CartesianSeriesBounds {
67 pub x: Bounds,
68 pub y: Bounds,
69}
70
71pub struct CartesianChartEngine;
72impl CartesianChartEngine {
73 pub fn measure(
74 size: Size,
75 series: &[Series],
76 x_axes: &[Axis],
77 y_axes: &[Axis],
78 options: &CartesianOptions,
79 ) -> Result<Scene, ChartError> {
80 Self::measure_with_axes(size, series, x_axes, y_axes, options).map(|m| m.scene)
81 }
82 pub fn measure_with_axes(
83 size: Size,
84 series: &[Series],
85 x_axes: &[Axis],
86 y_axes: &[Axis],
87 options: &CartesianOptions,
88 ) -> Result<CartesianMeasurement, ChartError> {
89 Self::measure_with_axes_and_labels(size, series, x_axes, y_axes, options, |text, size| {
90 Size::new(text.chars().count() as f64 * size * 0.6, size * 1.2)
91 })
92 }
93 pub fn measure_with_axes_and_labels(
94 size: Size,
95 series: &[Series],
96 x_axes: &[Axis],
97 y_axes: &[Axis],
98 options: &CartesianOptions,
99 mut measure_text: impl FnMut(&str, f64) -> Size,
100 ) -> Result<CartesianMeasurement, ChartError> {
101 validate_size(size)?;
102 let defaults = [Axis::default()];
103 let x_axes = if x_axes.is_empty() {
104 &defaults[..]
105 } else {
106 x_axes
107 };
108 let y_axes = if y_axes.is_empty() {
109 &defaults[..]
110 } else {
111 y_axes
112 };
113 let plot = plot_bounds(size, x_axes, y_axes, options.draw_margin)?;
114 let mut scene = Scene::new(size, plot);
115 let mut all_x = true;
118 let mut all_y = true;
119 for s in series.iter().filter(|s| s.visible) {
120 all_x &= !s.kind.is_row() && !matches!(s.kind, SeriesKind::Scatter | SeriesKind::Heat);
121 all_y &= s.kind.is_row();
122 }
123 scene.automatic_finding_strategy = if all_x {
124 FindingStrategy::CompareOnlyXTakeClosest
125 } else if all_y {
126 FindingStrategy::CompareOnlyYTakeClosest
127 } else {
128 FindingStrategy::CompareAllTakeClosest
129 };
130 let mut x_bounds = vec![(f64::INFINITY, f64::NEG_INFINITY); x_axes.len()];
131 let mut y_bounds = vec![(f64::INFINITY, f64::NEG_INFINITY); y_axes.len()];
132 let mut totals: BTreeMap<StackKey, (f64, f64)> = BTreeMap::new();
133 for (si, s) in series.iter().enumerate().filter(|(_, s)| s.visible) {
134 validate_series(s, x_axes.len(), y_axes.len())?;
135 scene.legend.push(LegendItem {
136 series: si,
137 name: if s.name.is_empty() {
138 format!("Series {}", si + 1)
139 } else {
140 s.name.clone()
141 },
142 color: s.color(si),
143 });
144 if s.kind.is_stacked() && s.stack_100_percent {
145 for c in s.values.iter().filter(|c| valid_point(s, c)) {
146 let t = totals.entry(stack_key(s, c)).or_default();
147 if c.primary_value >= 0.0 {
148 t.0 += c.primary_value;
149 } else {
150 t.1 -= c.primary_value;
151 }
152 }
153 }
154 }
155 let mut stacks: BTreeMap<StackKey, (f64, f64)> = BTreeMap::new();
156 let mut measured: Vec<Vec<Option<MeasuredPoint>>> = Vec::with_capacity(series.len());
157 let mut series_bounds = Vec::with_capacity(series.len());
158 for s in series {
159 let mut points = Vec::with_capacity(s.values.len());
160 if !s.visible {
161 measured.push(points);
162 series_bounds.push(None);
163 continue;
164 }
165 let mut own_x = (f64::INFINITY, f64::NEG_INFINITY);
166 let mut own_y = (f64::INFINITY, f64::NEG_INFINITY);
167 let category_half = if s.kind.is_bar() {
168 if s.kind.is_row() {
169 y_axes[s.y_axis].unit_width
170 } else {
171 x_axes[s.x_axis].unit_width
172 }
173 .abs()
174 * 0.5
175 } else {
176 0.0
177 };
178 for (index, c) in s.values.iter().copied().enumerate() {
179 if !valid_point(s, &c) {
180 points.push(None);
181 continue;
182 }
183 let mut top = c.primary_value;
184 let mut bottom = match s.kind {
185 SeriesKind::RangeColumn | SeriesKind::RangeRow | SeriesKind::RangeLine => {
186 c.tertiary_value
187 }
188 SeriesKind::Financial | SeriesKind::Box => c.quinary_value,
189 _ => s.pivot,
190 };
191 if s.kind.is_stacked() {
192 let key = stack_key(s, &c);
193 if s.stack_100_percent {
194 let total = totals.get(&key).copied().unwrap_or_default();
195 let denominator = if top >= 0.0 { total.0 } else { total.1 };
196 top = if denominator == 0.0 {
197 0.0
198 } else {
199 top / denominator * 100.0
200 };
201 }
202 let stack = stacks.entry(key).or_insert((s.pivot, s.pivot));
203 let offset = if top >= 0.0 {
204 &mut stack.0
205 } else {
206 &mut stack.1
207 };
208 bottom = *offset;
209 *offset += top;
210 top = *offset;
211 }
212 let (mut xmin, mut xmax, mut ymin, mut ymax) = if s.kind.is_row() {
213 (
214 top,
215 top,
216 c.secondary_value - category_half,
217 c.secondary_value + category_half,
218 )
219 } else {
220 (
221 c.secondary_value - category_half,
222 c.secondary_value + category_half,
223 top,
224 top,
225 )
226 };
227 if s.kind.is_bar() || s.kind.is_area() || s.fill.is_some() || s.fill_brush.is_some()
228 {
229 if s.kind.is_row() {
230 xmin = xmin.min(bottom);
231 xmax = xmax.max(bottom);
232 } else {
233 ymin = ymin.min(bottom);
234 ymax = ymax.max(bottom);
235 }
236 }
237 if !c.point_error.is_empty {
238 let (cx, cy) = if s.kind.is_row() {
239 (top, c.secondary_value)
240 } else {
241 (c.secondary_value, top)
242 };
243 xmin = xmin.min(cx - c.point_error.xi.abs());
244 xmax = xmax.max(cx + c.point_error.xj.abs());
245 ymin = ymin.min(cy - c.point_error.yj.abs());
246 ymax = ymax.max(cy + c.point_error.yi.abs());
247 }
248 for value in [
249 xmin,
250 xmax,
251 if s.kind.is_row() {
252 top
253 } else {
254 c.secondary_value
255 },
256 ] {
257 include_axis(&mut x_bounds[s.x_axis], value, &x_axes[s.x_axis]);
258 include_axis(&mut own_x, value, &x_axes[s.x_axis]);
259 }
260 for value in [
261 ymin,
262 ymax,
263 if s.kind.is_row() {
264 c.secondary_value
265 } else {
266 top
267 },
268 ] {
269 include_axis(&mut y_bounds[s.y_axis], value, &y_axes[s.y_axis]);
270 include_axis(&mut own_y, value, &y_axes[s.y_axis]);
271 }
272 points.push(Some(MeasuredPoint {
273 index,
274 coordinate: c,
275 bottom,
276 top,
277 }));
278 }
279 series_bounds.push(if own_x.0.is_finite() && own_y.0.is_finite() {
280 Some(CartesianSeriesBounds {
281 x: data_bounds(own_x),
282 y: data_bounds(own_y),
283 })
284 } else {
285 None
286 });
287 measured.push(points);
288 }
289 for (si, s) in series
292 .iter()
293 .enumerate()
294 .filter(|(_, s)| s.visible && s.kind == SeriesKind::Heat)
295 {
296 let dx = minimum_delta(
297 s.values
298 .iter()
299 .filter(|c| valid_point(s, c))
300 .map(|c| c.secondary_value),
301 )
302 .unwrap_or(x_axes[s.x_axis].unit_width.abs());
303 let dy = minimum_delta(
304 s.values
305 .iter()
306 .filter(|c| valid_point(s, c))
307 .map(|c| c.primary_value),
308 )
309 .unwrap_or(y_axes[s.y_axis].unit_width.abs());
310 for c in s.values.iter().filter(|c| valid_point(s, c)) {
314 for value in [c.secondary_value - dx * 0.5, c.secondary_value + dx * 0.5] {
315 include_axis(&mut x_bounds[s.x_axis], value, &x_axes[s.x_axis]);
316 if value.is_finite()
317 && (x_axes[s.x_axis].logarithmic_base.is_none() || value > 0.0)
318 {
319 if let Some(bounds) = &mut series_bounds[si] {
320 bounds.x.append_value(value);
321 }
322 }
323 }
324 for value in [c.primary_value - dy * 0.5, c.primary_value + dy * 0.5] {
325 include_axis(&mut y_bounds[s.y_axis], value, &y_axes[s.y_axis]);
326 if value.is_finite()
327 && (y_axes[s.y_axis].logarithmic_base.is_none() || value > 0.0)
328 {
329 if let Some(bounds) = &mut series_bounds[si] {
330 bounds.y.append_value(value);
331 }
332 }
333 }
334 }
335 }
336 let x_label_sizes = x_axes
337 .iter()
338 .zip(&x_bounds)
339 .map(|(axis, bounds)| maximum_label_size(axis, *bounds, &mut measure_text))
340 .collect::<Vec<_>>();
341 let y_label_sizes = y_axes
342 .iter()
343 .zip(&y_bounds)
344 .map(|(axis, bounds)| maximum_label_size(axis, *bounds, &mut measure_text))
345 .collect::<Vec<_>>();
346 let mut x_requests = vec![Bounds::default(); x_axes.len()];
347 let mut y_requests = vec![Bounds::default(); y_axes.len()];
348 for (si, s) in series.iter().enumerate().filter(|(_, s)| s.visible) {
349 let Some(bounds) = &mut series_bounds[si] else {
350 continue;
351 };
352 let geometry_size = if matches!(
353 s.kind,
354 SeriesKind::Line
355 | SeriesKind::StepLine
356 | SeriesKind::Area
357 | SeriesKind::StackedArea
358 | SeriesKind::StackedStepArea
359 | SeriesKind::RangeLine
360 ) {
361 (s.geometry_size.max(0.0)
362 + if s.geometry_size > 0.0 {
363 s.stroke_width.max(0.0)
364 } else {
365 0.0
366 })
367 * 0.5
368 } else {
369 0.0
370 };
371 set_padding_request(
372 &mut bounds.x,
373 &x_axes[s.x_axis],
374 size,
375 AxisOrientation::X,
376 x_label_sizes[s.x_axis],
377 s.data_padding.x,
378 geometry_size,
379 )?;
380 set_padding_request(
381 &mut bounds.y,
382 &y_axes[s.y_axis],
383 size,
384 AxisOrientation::Y,
385 y_label_sizes[s.y_axis],
386 s.data_padding.y,
387 geometry_size,
388 )?;
389 bounds.x.min_delta = minimum_delta(measured[si].iter().flatten().map(|p| {
390 if s.kind.is_row() {
391 p.top
392 } else {
393 p.coordinate.secondary_value
394 }
395 }))
396 .unwrap_or(f64::MAX);
397 bounds.y.min_delta = minimum_delta(measured[si].iter().flatten().map(|p| {
398 if s.kind.is_row() {
399 p.coordinate.secondary_value
400 } else {
401 p.top
402 }
403 }))
404 .unwrap_or(f64::MAX);
405 x_requests[s.x_axis].append_bounds(&bounds.x);
406 y_requests[s.y_axis].append_bounds(&bounds.y);
407 }
408 let all_columns = series
409 .iter()
410 .filter(|s| s.visible)
411 .all(|s| matches!(s.kind, SeriesKind::Column | SeriesKind::StackedColumn));
412 let x_scales = x_axes
413 .iter()
414 .zip(x_bounds.iter())
415 .zip(&x_requests)
416 .map(|((a, &(min, max)), request)| {
417 padded_scale(a, min, max, plot.x, plot.x + plot.width, request, false)
418 })
419 .collect::<Result<Vec<_>, _>>()?;
420 let y_scales = y_axes
421 .iter()
422 .zip(y_bounds.iter())
423 .zip(&y_requests)
424 .map(|((a, &(min, max)), request)| {
425 padded_scale(
426 a,
427 min,
428 max,
429 plot.y + plot.height,
430 plot.y,
431 request,
432 all_columns,
433 )
434 })
435 .collect::<Result<Vec<_>, _>>()?;
436 draw_sections(&mut scene, &options.sections, &x_scales, &y_scales)?;
437 draw_axes(
438 &mut scene,
439 x_axes,
440 y_axes,
441 &x_scales,
442 &y_scales,
443 &x_label_sizes,
444 &y_label_sizes,
445 );
446 for (si, s) in series.iter().enumerate().filter(|(_, s)| s.visible) {
447 let xs = x_scales[s.x_axis];
448 let ys = y_scales[s.y_axis];
449 let points = &measured[si];
450 match s.kind {
451 SeriesKind::Line
452 | SeriesKind::StepLine
453 | SeriesKind::Area
454 | SeriesKind::StackedArea
455 | SeriesKind::StackedStepArea
456 | SeriesKind::RangeLine => draw_line_series(
457 &mut scene,
458 si,
459 s,
460 points,
461 xs,
462 ys,
463 x_axes[s.x_axis].unit_width,
464 ),
465 SeriesKind::Column
466 | SeriesKind::Row
467 | SeriesKind::StackedColumn
468 | SeriesKind::StackedRow
469 | SeriesKind::RangeColumn
470 | SeriesKind::RangeRow => {
471 draw_bars(&mut scene, si, s, series, points, xs, ys, x_axes, y_axes)
472 }
473 SeriesKind::Scatter => draw_scatter(&mut scene, si, s, points, xs, ys),
474 SeriesKind::Heat => draw_heat(&mut scene, si, s, points, xs, ys, x_axes, y_axes),
475 SeriesKind::Financial | SeriesKind::Box => {
476 draw_financial(&mut scene, si, s, points, xs, ys, x_axes)
477 }
478 SeriesKind::Pie | SeriesKind::PolarLine => {
479 return Err(ChartError::InvalidData(
480 "a pie or polar series requires its matching chart engine".into(),
481 ));
482 }
483 }
484 }
485 Ok(CartesianMeasurement {
486 scene,
487 x_scales,
488 y_scales,
489 series_bounds,
490 })
491 }
492}
493
494fn data_bounds((min, max): (f64, f64)) -> Bounds {
495 Bounds {
496 min,
497 max,
498 is_empty: false,
499 ..Bounds::default()
500 }
501}
502fn maximum_label_size(
503 axis: &Axis,
504 bounds: (f64, f64),
505 measure: &mut impl FnMut(&str, f64) -> Size,
506) -> Size {
507 let min = axis
508 .min_limit
509 .unwrap_or(if bounds.0.is_finite() { bounds.0 } else { 0.0 });
510 let max = axis
511 .max_limit
512 .unwrap_or(if bounds.1.is_finite() { bounds.1 } else { 10.0 });
513 let mut maximum = Size::new(0.0, 0.0);
514 for i in 0..=25 {
516 let fraction = i as f64 / 25.0;
517 let value = min * (1.0 - fraction) + max * fraction;
518 let measured = measure(&axis.label(value), axis.text_size);
519 if measured.width.is_finite() && measured.height.is_finite() {
520 maximum.width = maximum
521 .width
522 .max(measured.width + axis.labels_padding * 2.0);
523 maximum.height = maximum
524 .height
525 .max(measured.height + axis.labels_padding * 2.0);
526 }
527 }
528 maximum
529}
530fn set_padding_request(
531 bounds: &mut Bounds,
532 axis: &Axis,
533 size: Size,
534 orientation: AxisOrientation,
535 labels: Size,
536 padding: f64,
537 geometry: f64,
538) -> Result<(), ChartError> {
539 let mut axis = axis.clone();
540 axis.padding = 0.0;
541 let scale = AxisScale::new(&axis, bounds.min, bounds.max, 0.0, 1.0)?;
542 let request = scale
543 .tick_with_metrics(&axis, size, orientation, labels)
544 .value
545 * padding;
546 bounds.padding_min = if request.is_finite() {
547 request.max(0.0)
548 } else {
549 0.0
550 };
551 bounds.padding_max = bounds.padding_min;
552 bounds.requested_geometry_size = geometry;
553 Ok(())
554}
555fn padded_scale(
556 axis: &Axis,
557 raw_min: f64,
558 raw_max: f64,
559 start: f64,
560 end: f64,
561 request: &Bounds,
562 columns: bool,
563) -> Result<AxisScale, ChartError> {
564 let mut plain = axis.clone();
565 plain.padding = 0.0;
566 let initial = AxisScale::new(&plain, raw_min, raw_max, start, end)?;
567 if axis.logarithmic_base.is_some() {
568 return Ok(initial);
569 }
570 let (mut min, mut max) = (initial.min, initial.max);
571 let percent = ((max - min) * axis.padding.max(0.0)).max(0.0);
572 let percent = if percent.is_finite() { percent } else { 0.0 };
573 let geometry_padding = |scale: AxisScale| {
574 if start == end {
575 return 0.0;
576 }
577 let value = (scale.to_chart_values(start + request.requested_geometry_size)
578 - scale.to_chart_values(start))
579 .abs();
580 if value.is_finite() {
581 value
582 } else {
583 0.0
584 }
585 };
586 if axis.min_limit.is_none() {
587 let amount = geometry_padding(initial)
588 .max(request.padding_min)
589 .max(percent);
590 if (min - amount).is_finite() {
591 min -= amount;
592 }
593 if columns && raw_min >= 0.0 {
594 min = min.max(0.0);
595 }
596 }
597 if axis.max_limit.is_none() {
598 let after_min = AxisScale::new(&plain, min, max, start, end)?;
600 let amount = geometry_padding(after_min)
601 .max(request.padding_max)
602 .max(percent);
603 if (max + amount).is_finite() {
604 max += amount;
605 }
606 if columns && raw_max <= 0.0 {
607 max = max.min(0.0);
608 }
609 }
610 AxisScale::new(&plain, min, max, start, end)
611}
612
613#[derive(Clone, Copy)]
614struct MeasuredPoint {
615 index: usize,
616 coordinate: Coordinate,
617 bottom: f64,
618 top: f64,
619}
620type StackKey = (usize, usize, u8, u32, u64);
621fn stack_key(s: &Series, c: &Coordinate) -> StackKey {
622 (
623 s.x_axis,
624 s.y_axis,
625 if s.kind.is_row() {
626 1
627 } else if s.kind.is_bar() {
628 2
629 } else {
630 3
631 },
632 s.stack_group,
633 if c.secondary_value == 0.0 {
634 0
635 } else {
636 c.secondary_value.to_bits()
637 },
638 )
639}
640fn include_axis(bounds: &mut (f64, f64), value: f64, axis: &Axis) {
641 if value.is_finite() && (axis.logarithmic_base.is_none() || value > 0.0) {
642 bounds.0 = bounds.0.min(value);
643 bounds.1 = bounds.1.max(value);
644 }
645}
646fn baseline(scale: AxisScale, value: f64) -> f64 {
647 scale.to_pixels(if scale.logarithmic_base.is_some() && value <= 0.0 {
648 scale.min
649 } else {
650 value
651 })
652}
653fn valid_point(s: &Series, c: &Coordinate) -> bool {
654 if c.is_empty || !c.primary_value.is_finite() || !c.secondary_value.is_finite() {
655 return false;
656 }
657 match s.kind {
658 SeriesKind::RangeColumn
659 | SeriesKind::RangeRow
660 | SeriesKind::RangeLine
661 | SeriesKind::Scatter
662 | SeriesKind::Heat => c.tertiary_value.is_finite(),
663 SeriesKind::Financial => {
664 c.tertiary_value.is_finite()
665 && c.quaternary_value.is_finite()
666 && c.quinary_value.is_finite()
667 }
668 SeriesKind::Box => {
669 c.tertiary_value.is_finite()
670 && c.quaternary_value.is_finite()
671 && c.quinary_value.is_finite()
672 && c.senary_value.is_finite()
673 }
674 _ => true,
675 }
676}
677fn validate_series(s: &Series, x: usize, y: usize) -> Result<(), ChartError> {
678 if s.x_axis >= x || s.y_axis >= y {
679 return Err(ChartError::InvalidAxis(
680 "series refers to a missing axis".into(),
681 ));
682 }
683 if [
684 s.pivot,
685 s.geometry_size,
686 s.min_geometry_size,
687 s.line_smoothness,
688 s.stroke_width,
689 s.opacity,
690 s.padding,
691 s.corner_radius,
692 s.max_bar_width,
693 s.data_labels_size,
694 s.data_padding.x,
695 s.data_padding.y,
696 ]
697 .iter()
698 .any(|v| !v.is_finite())
699 || s.dashes.iter().any(|d| !d.is_finite() || *d < 0.0)
700 || s.data_padding.x < 0.0
701 || s.data_padding.y < 0.0
702 {
703 return Err(ChartError::InvalidData(
704 "series dimensions and paint values must be finite".into(),
705 ));
706 }
707 Ok(())
708}
709fn plot_bounds(
710 size: Size,
711 x: &[Axis],
712 y: &[Axis],
713 explicit: Option<Rect>,
714) -> Result<Rect, ChartError> {
715 if let Some(r) = explicit {
716 if [r.x, r.y, r.width, r.height].iter().any(|v| !v.is_finite())
717 || r.width < 0.0
718 || r.height < 0.0
719 {
720 return Err(ChartError::InvalidData(
721 "draw margin must be finite with nonnegative size".into(),
722 ));
723 }
724 return Ok(r);
725 }
726 let left = y
727 .iter()
728 .filter(|a| a.visible && a.position == AxisPosition::Start)
729 .count() as f64
730 * 58.0
731 + 8.0;
732 let right = y
733 .iter()
734 .filter(|a| a.visible && a.position == AxisPosition::End)
735 .count() as f64
736 * 58.0
737 + 12.0;
738 let top = x
739 .iter()
740 .filter(|a| a.visible && a.position == AxisPosition::End)
741 .count() as f64
742 * 40.0
743 + 12.0;
744 let bottom = x
745 .iter()
746 .filter(|a| a.visible && a.position == AxisPosition::Start)
747 .count() as f64
748 * 40.0
749 + 8.0;
750 let sx = (size.width / (left + right).max(1.0)).min(1.0);
751 let sy = (size.height / (top + bottom).max(1.0)).min(1.0);
752 Ok(Rect::new(
753 left * sx,
754 top * sy,
755 (size.width - (left + right) * sx).max(0.0),
756 (size.height - (top + bottom) * sy).max(0.0),
757 ))
758}
759fn draw_axes(
760 scene: &mut Scene,
761 x: &[Axis],
762 y: &[Axis],
763 xs: &[AxisScale],
764 ys: &[AxisScale],
765 x_label_sizes: &[Size],
766 y_label_sizes: &[Size],
767) {
768 let r = scene.plot_bounds;
769 let mut x_offsets = [0.0, 0.0];
770 let mut y_offsets = [0.0, 0.0];
771 for ((axis, scale), labels) in x
772 .iter()
773 .zip(xs)
774 .zip(x_label_sizes)
775 .filter(|((a, _), _)| a.visible)
776 {
777 let side = usize::from(axis.position == AxisPosition::End);
778 let baseline = if side == 0 {
779 r.y + r.height + x_offsets[side]
780 } else {
781 r.y - x_offsets[side]
782 };
783 for value in scale.ticks_with_metrics(axis, scene.size, AxisOrientation::X, *labels) {
784 let px = scale.to_pixels(value);
785 if !px.is_finite() {
786 continue;
787 }
788 if axis.show_separator_lines {
789 scene.primitives.push(
790 Primitive::new(
791 Geometry::Line {
792 start: Point::new(px, r.y),
793 end: Point::new(px, r.y + r.height),
794 },
795 PaintStyle::stroke(axis.separators_color, 1.0),
796 )
797 .clipped(r),
798 );
799 }
800 scene.primitives.push(Primitive::new(
801 Geometry::Text {
802 origin: Point::new(
803 px,
804 if side == 0 {
805 baseline + 6.0
806 } else {
807 baseline - axis.text_size - 6.0
808 },
809 ),
810 text: axis.label(value),
811 size: axis.text_size,
812 rotation: axis.labels_rotation,
813 anchor: TextAnchor::Center,
814 },
815 PaintStyle::fill(axis.labels_color),
816 ));
817 }
818 if !axis.name.is_empty() {
819 scene.primitives.push(Primitive::new(
820 Geometry::Text {
821 origin: Point::new(
822 r.x + r.width * 0.5,
823 if side == 0 {
824 baseline + 23.0
825 } else {
826 baseline - 30.0
827 },
828 ),
829 text: axis.name.clone(),
830 size: axis.text_size,
831 rotation: 0.0,
832 anchor: TextAnchor::Center,
833 },
834 PaintStyle::fill(axis.labels_color),
835 ));
836 }
837 x_offsets[side] += 40.0;
838 }
839 for ((axis, scale), labels) in y
840 .iter()
841 .zip(ys)
842 .zip(y_label_sizes)
843 .filter(|((a, _), _)| a.visible)
844 {
845 let side = usize::from(axis.position == AxisPosition::End);
846 let baseline = if side == 0 {
847 r.x - y_offsets[side]
848 } else {
849 r.x + r.width + y_offsets[side]
850 };
851 for value in scale.ticks_with_metrics(axis, scene.size, AxisOrientation::Y, *labels) {
852 let py = scale.to_pixels(value);
853 if !py.is_finite() {
854 continue;
855 }
856 if axis.show_separator_lines {
857 scene.primitives.push(
858 Primitive::new(
859 Geometry::Line {
860 start: Point::new(r.x, py),
861 end: Point::new(r.x + r.width, py),
862 },
863 PaintStyle::stroke(axis.separators_color, 1.0),
864 )
865 .clipped(r),
866 );
867 }
868 scene.primitives.push(Primitive::new(
869 Geometry::Text {
870 origin: Point::new(
871 if side == 0 {
872 baseline - 8.0
873 } else {
874 baseline + 8.0
875 },
876 py - axis.text_size * 0.5,
877 ),
878 text: axis.label(value),
879 size: axis.text_size,
880 rotation: axis.labels_rotation,
881 anchor: if side == 0 {
882 TextAnchor::End
883 } else {
884 TextAnchor::Start
885 },
886 },
887 PaintStyle::fill(axis.labels_color),
888 ));
889 }
890 if !axis.name.is_empty() {
891 scene.primitives.push(Primitive::new(
892 Geometry::Text {
893 origin: Point::new(
894 if side == 0 {
895 baseline - 55.0
896 } else {
897 baseline + 48.0
898 },
899 r.y + r.height * 0.5,
900 ),
901 text: axis.name.clone(),
902 size: axis.text_size,
903 rotation: -90.0,
904 anchor: TextAnchor::Center,
905 },
906 PaintStyle::fill(axis.labels_color),
907 ));
908 }
909 y_offsets[side] += 58.0;
910 }
911}
912fn draw_sections(
913 scene: &mut Scene,
914 sections: &[Section],
915 xs: &[AxisScale],
916 ys: &[AxisScale],
917) -> Result<(), ChartError> {
918 let plot = scene.plot_bounds;
919 for section in sections {
920 let Some(x) = xs.get(section.x_axis) else {
921 return Err(ChartError::InvalidAxis(
922 "section refers to a missing X axis".into(),
923 ));
924 };
925 let Some(y) = ys.get(section.y_axis) else {
926 return Err(ChartError::InvalidAxis(
927 "section refers to a missing Y axis".into(),
928 ));
929 };
930 let left = section.x_min.map(|v| x.to_pixels(v)).unwrap_or(plot.x);
931 let right = section
932 .x_max
933 .map(|v| x.to_pixels(v))
934 .unwrap_or(plot.x + plot.width);
935 let top = section.y_max.map(|v| y.to_pixels(v)).unwrap_or(plot.y);
936 let bottom = section
937 .y_min
938 .map(|v| y.to_pixels(v))
939 .unwrap_or(plot.y + plot.height);
940 if [left, right, top, bottom, section.stroke_width]
941 .iter()
942 .any(|v| !v.is_finite())
943 {
944 return Err(ChartError::InvalidData(
945 "section endpoints must be finite and valid for their axes".into(),
946 ));
947 }
948 let style = PaintStyle {
949 fill: section.fill,
950 stroke: section.stroke,
951 stroke_width: section.stroke_width,
952 ..PaintStyle::default()
953 };
954 let geometry = if left == right || top == bottom {
955 Geometry::Line {
956 start: Point::new(left, top),
957 end: Point::new(right, bottom),
958 }
959 } else {
960 Geometry::Rectangle {
961 rect: Rect::new(
962 left.min(right),
963 top.min(bottom),
964 (right - left).abs(),
965 (bottom - top).abs(),
966 ),
967 radius: 0.0,
968 }
969 };
970 scene
971 .primitives
972 .push(Primitive::new(geometry, style).clipped(plot));
973 }
974 Ok(())
975}
976fn finite_point(p: Point) -> bool {
977 p.x.is_finite() && p.y.is_finite()
978}
979fn location(p: &MeasuredPoint, x: AxisScale, y: AxisScale) -> Point {
980 Point::new(
981 x.to_pixels(p.coordinate.secondary_value),
982 y.to_pixels(p.top),
983 )
984}
985fn append_hit(
986 scene: &mut Scene,
987 si: usize,
988 s: &Series,
989 p: &MeasuredPoint,
990 shape: HitShape,
991 center: Point,
992) {
993 let value = if s.kind == SeriesKind::Heat {
994 p.coordinate.tertiary_value
995 } else {
996 p.coordinate.primary_value
997 };
998 let text = match s.kind {
999 SeriesKind::RangeColumn | SeriesKind::RangeRow | SeriesKind::RangeLine => format!(
1000 "{} – {}",
1001 format_number(p.coordinate.tertiary_value),
1002 format_number(p.coordinate.primary_value)
1003 ),
1004 SeriesKind::Financial => format!(
1005 "O {} H {} L {} C {}",
1006 format_number(p.coordinate.tertiary_value),
1007 format_number(p.coordinate.primary_value),
1008 format_number(p.coordinate.quinary_value),
1009 format_number(p.coordinate.quaternary_value)
1010 ),
1011 SeriesKind::Box => format!(
1012 "Min {} Q1 {} Median {} Q3 {} Max {}",
1013 format_number(p.coordinate.quinary_value),
1014 format_number(p.coordinate.quaternary_value),
1015 format_number(p.coordinate.senary_value),
1016 format_number(p.coordinate.tertiary_value),
1017 format_number(p.coordinate.primary_value)
1018 ),
1019 _ => format_number(value),
1020 };
1021 scene.hits.push(HitRegion {
1022 id: PointId {
1023 series: si,
1024 point: p.index,
1025 },
1026 shape,
1027 center,
1028 value,
1029 label: format!(
1030 "{}{}{}",
1031 s.name,
1032 if s.name.is_empty() { "" } else { ": " },
1033 text
1034 ),
1035 });
1036 if s.data_labels {
1037 scene.primitives.push(
1038 Primitive::new(
1039 Geometry::Text {
1040 origin: Point::new(center.x + 4.0, center.y - s.data_labels_size - 4.0),
1041 text,
1042 size: s.data_labels_size,
1043 rotation: 0.0,
1044 anchor: TextAnchor::Start,
1045 },
1046 PaintStyle::fill(s.data_labels_color.unwrap_or_else(|| s.color(si))),
1047 )
1048 .clipped(scene.plot_bounds)
1049 .identified(PointId {
1050 series: si,
1051 point: p.index,
1052 }),
1053 );
1054 }
1055}
1056
1057pub(crate) fn line_path(points: &[Point], smoothness: f64, step: bool) -> Vec<PathCommand> {
1058 if points.is_empty() {
1059 return Vec::new();
1060 }
1061 let mut path = Vec::with_capacity(points.len() * if step { 2 } else { 1 });
1062 path.push(PathCommand::MoveTo(points[0]));
1063 for i in 1..points.len() {
1064 let current = points[i - 1];
1065 let next = points[i];
1066 if step {
1067 path.push(PathCommand::LineTo(Point::new(next.x, current.y)));
1068 path.push(PathCommand::LineTo(next));
1069 } else if smoothness <= 0.0 {
1070 path.push(PathCommand::LineTo(next));
1071 } else {
1072 let previous = points[i.saturating_sub(2)];
1073 let after = points[(i + 1).min(points.len() - 1)];
1074 let curve = crate::kernel::line_spline_math::control_points(
1075 previous, current, next, after, smoothness,
1076 );
1077 path.push(PathCommand::CubicTo {
1078 control1: curve.control1(),
1079 control2: curve.control2(),
1080 end: curve.end(),
1081 });
1082 }
1083 }
1084 path
1085}
1086fn draw_line_series(
1087 scene: &mut Scene,
1088 si: usize,
1089 s: &Series,
1090 points: &[Option<MeasuredPoint>],
1091 x: AxisScale,
1092 y: AxisScale,
1093 unit_width: f64,
1094) {
1095 let mut run = Vec::new();
1096 let flush = |scene: &mut Scene, run: &mut Vec<&MeasuredPoint>| {
1097 if run.is_empty() {
1098 return;
1099 }
1100 let top: Vec<_> = run.iter().map(|p| location(p, x, y)).collect();
1101 let step = matches!(s.kind, SeriesKind::StepLine | SeriesKind::StackedStepArea);
1102 let smooth = s.line_smoothness.clamp(0.0, 1.0);
1103 let mut path = line_path(&top, smooth, step);
1104 if s.kind.is_area() || s.fill.is_some() || s.fill_brush.is_some() {
1105 let bottom: Vec<_> = run
1106 .iter()
1107 .rev()
1108 .map(|p| {
1109 Point::new(
1110 x.to_pixels(p.coordinate.secondary_value),
1111 baseline(y, p.bottom),
1112 )
1113 })
1114 .collect();
1115 if let Some(first) = bottom.first() {
1116 path.push(PathCommand::LineTo(*first));
1117 }
1118 path.extend(line_path(&bottom, smooth, step).into_iter().skip(1));
1119 path.push(PathCommand::Close);
1120 let mut style = s.style(si, true);
1121 if s.fill.is_none() && s.fill_brush.is_none() {
1122 style.opacity *= 0.35;
1123 }
1124 style.stroke = None;
1125 style.stroke_brush = None;
1126 scene.primitives.push(
1127 Primitive::new(Geometry::Path(path), style)
1128 .clipped(scene.plot_bounds)
1129 .identified(PointId {
1130 series: si,
1131 point: usize::MAX,
1132 }),
1133 );
1134 }
1135 let mut style = s.style(si, false);
1136 style.fill = None;
1137 style.fill_brush = None;
1138 scene.primitives.push(
1139 Primitive::new(Geometry::Path(line_path(&top, smooth, step)), style.clone())
1140 .clipped(scene.plot_bounds)
1141 .identified(PointId {
1142 series: si,
1143 point: usize::MAX,
1144 }),
1145 );
1146 if s.kind == SeriesKind::RangeLine {
1147 let low: Vec<_> = run
1148 .iter()
1149 .map(|p| {
1150 Point::new(
1151 x.to_pixels(p.coordinate.secondary_value),
1152 baseline(y, p.bottom),
1153 )
1154 })
1155 .collect();
1156 scene.primitives.push(
1157 Primitive::new(Geometry::Path(line_path(&low, smooth, false)), style)
1158 .clipped(scene.plot_bounds)
1159 .identified(PointId {
1160 series: si,
1161 point: usize::MAX,
1162 }),
1163 );
1164 }
1165 run.clear();
1166 };
1167 for point in points {
1168 let Some(p) = point else {
1169 if s.enable_null_splitting {
1170 flush(scene, &mut run);
1171 }
1172 continue;
1173 };
1174 let center = location(p, x, y);
1175 if !finite_point(center) {
1176 flush(scene, &mut run);
1177 continue;
1178 }
1179 run.push(p);
1180 }
1181 flush(scene, &mut run);
1182
1183 for p in points.iter().flatten() {
1186 let center = location(p, x, y);
1187 if !finite_point(center) {
1188 continue;
1189 }
1190 let low = Point::new(center.x, baseline(y, p.bottom));
1191 let range = s.kind == SeriesKind::RangeLine;
1192 if range && !finite_point(low) {
1193 continue;
1194 }
1195 let marker_centers = [center, low];
1196 for marker in &marker_centers[..if range { 2 } else { 1 }] {
1197 if s.geometry_size > 0.0 {
1198 scene.primitives.push(
1199 Primitive::new(
1200 Geometry::Ellipse(Rect::new(
1201 marker.x - s.geometry_size / 2.0,
1202 marker.y - s.geometry_size / 2.0,
1203 s.geometry_size,
1204 s.geometry_size,
1205 )),
1206 s.style(si, true),
1207 )
1208 .clipped(scene.plot_bounds)
1209 .identified(PointId {
1210 series: si,
1211 point: p.index,
1212 }),
1213 );
1214 }
1215 }
1216 let marker_radius = (s.geometry_size / 2.0).max(0.5);
1217 let marker_shape = |center| HitShape::Circle {
1218 center,
1219 radius: marker_radius,
1220 };
1221 let exact = if range {
1222 HitShape::Compound(vec![marker_shape(center), marker_shape(low)])
1223 } else {
1224 marker_shape(center)
1225 };
1226 let slot = category_width(x, p.coordinate.secondary_value, unit_width).max(1.0);
1227 let hover_bounds = if range {
1228 Rect::new(
1229 center.x - slot * 0.5,
1230 center.y.min(low.y),
1231 slot,
1232 (center.y - low.y).abs().max(1.0),
1233 )
1234 } else {
1235 Rect::new(
1236 center.x - slot * 0.5,
1237 center.y - marker_radius,
1238 slot,
1239 marker_radius * 2.0,
1240 )
1241 };
1242 append_hit(
1243 scene,
1244 si,
1245 s,
1246 p,
1247 HitShape::WithHoverArea {
1248 shape: Box::new(exact),
1249 bounds: hover_bounds,
1250 },
1251 if range {
1252 Point::new(center.x, (center.y + low.y) * 0.5)
1253 } else {
1254 center
1255 },
1256 );
1257 if !range {
1258 draw_errors(scene, si, s, p, x, y);
1259 }
1260 }
1261}
1262fn category_width(scale: AxisScale, category: f64, unit: f64) -> f64 {
1263 (scale.to_pixels(category + unit.abs() * 0.5) - scale.to_pixels(category - unit.abs() * 0.5))
1264 .abs()
1265}
1266fn bar_group(si: usize, s: &Series, all: &[Series]) -> (usize, usize) {
1267 let mut keys = Vec::new();
1268 let mut own = 0;
1269 for (i, candidate) in all.iter().enumerate().filter(|(_, a)| {
1270 a.visible
1271 && a.kind.is_bar()
1272 && !matches!(a.kind, SeriesKind::Financial | SeriesKind::Box)
1273 && a.kind.is_row() == s.kind.is_row()
1274 && a.x_axis == s.x_axis
1275 && a.y_axis == s.y_axis
1276 }) {
1277 let key = if candidate.kind.is_stacked() {
1278 (true, candidate.stack_group as usize)
1279 } else {
1280 (false, i)
1281 };
1282 let pos = if let Some(pos) = keys.iter().position(|k| *k == key) {
1283 pos
1284 } else {
1285 keys.push(key);
1286 keys.len() - 1
1287 };
1288 if i == si {
1289 own = pos;
1290 }
1291 }
1292 (
1293 if s.ignores_bar_position {
1294 1
1295 } else {
1296 keys.len().max(1)
1297 },
1298 own,
1299 )
1300}
1301fn bar_layout(
1302 s: &Series,
1303 scale: AxisScale,
1304 category: f64,
1305 unit: f64,
1306 group: (usize, usize),
1307) -> (f64, f64) {
1308 let (count, own) = group;
1309 let raw = category_width(scale, category, unit);
1310 let group_width = (raw - s.padding.max(0.0)).max(1.0);
1311 let slot = (group_width / count as f64).min(s.max_bar_width.max(1.0));
1312 let width = (slot - s.padding.max(0.0)).max(1.0);
1313 let offset = if s.ignores_bar_position {
1314 0.0
1315 } else {
1316 (own as f64 - (count as f64 - 1.0) * 0.5) * slot
1317 };
1318 (width, offset)
1319}
1320fn draw_bars(
1321 scene: &mut Scene,
1322 si: usize,
1323 s: &Series,
1324 all: &[Series],
1325 points: &[Option<MeasuredPoint>],
1326 x: AxisScale,
1327 y: AxisScale,
1328 xa: &[Axis],
1329 ya: &[Axis],
1330) {
1331 let group = bar_group(si, s, all);
1332 for p in points.iter().flatten() {
1333 let row = s.kind.is_row();
1334 let category = p.coordinate.secondary_value;
1335 let scale = if row { y } else { x };
1336 let unit = if row {
1337 ya[s.y_axis].unit_width
1338 } else {
1339 xa[s.x_axis].unit_width
1340 };
1341 let (width, offset) = bar_layout(s, scale, category, unit, group);
1342 let center_category = scale.to_pixels(category) + offset;
1343 let value_scale = if row { x } else { y };
1344 let a = baseline(value_scale, p.bottom);
1345 let b = value_scale.to_pixels(p.top);
1346 if ![width, center_category, a, b].iter().all(|v| v.is_finite()) {
1347 continue;
1348 }
1349 let rect = if row {
1350 Rect::new(
1351 a.min(b),
1352 center_category - width / 2.0,
1353 (b - a).abs(),
1354 width,
1355 )
1356 } else {
1357 Rect::new(
1358 center_category - width / 2.0,
1359 a.min(b),
1360 width,
1361 (b - a).abs(),
1362 )
1363 };
1364 scene.primitives.push(
1365 Primitive::new(
1366 Geometry::Rectangle {
1367 rect,
1368 radius: s.corner_radius.max(0.0),
1369 },
1370 s.style(si, true),
1371 )
1372 .clipped(scene.plot_bounds)
1373 .identified(PointId {
1374 series: si,
1375 point: p.index,
1376 }),
1377 );
1378 let center = if row {
1379 Point::new(b, center_category)
1380 } else {
1381 Point::new(center_category, b)
1382 };
1383 let slot = category_width(scale, category, unit) / group.0 as f64;
1384 let hover_bounds = if row {
1385 Rect::new(
1386 rect.x,
1387 center_category - slot * 0.5,
1388 rect.width.max(1.0),
1389 slot,
1390 )
1391 } else {
1392 Rect::new(
1393 center_category - slot * 0.5,
1394 rect.y,
1395 slot,
1396 rect.height.max(1.0),
1397 )
1398 };
1399 append_hit(
1400 scene,
1401 si,
1402 s,
1403 p,
1404 HitShape::WithHoverArea {
1405 shape: Box::new(HitShape::Rectangle(rect)),
1406 bounds: hover_bounds,
1407 },
1408 center,
1409 );
1410 draw_errors(scene, si, s, p, x, y);
1411 }
1412}
1413fn draw_scatter(
1414 scene: &mut Scene,
1415 si: usize,
1416 s: &Series,
1417 points: &[Option<MeasuredPoint>],
1418 x: AxisScale,
1419 y: AxisScale,
1420) {
1421 let (mut min, mut max) = (f64::INFINITY, f64::NEG_INFINITY);
1422 for p in points.iter().flatten() {
1423 min = min.min(p.coordinate.tertiary_value);
1424 max = max.max(p.coordinate.tertiary_value);
1425 }
1426 for p in points.iter().flatten() {
1427 let center = location(p, x, y);
1428 if !finite_point(center) {
1429 continue;
1430 }
1431 let t = if max == min {
1432 1.0
1433 } else {
1434 (p.coordinate.tertiary_value - min) / (max - min)
1435 };
1436 let size = (s.min_geometry_size + (s.geometry_size - s.min_geometry_size) * t).max(0.0);
1437 scene.primitives.push(
1438 Primitive::new(
1439 Geometry::Ellipse(Rect::new(
1440 center.x - size * 0.5,
1441 center.y - size * 0.5,
1442 size,
1443 size,
1444 )),
1445 s.style(si, true),
1446 )
1447 .clipped(scene.plot_bounds)
1448 .identified(PointId {
1449 series: si,
1450 point: p.index,
1451 }),
1452 );
1453 append_hit(
1454 scene,
1455 si,
1456 s,
1457 p,
1458 HitShape::Circle {
1459 center,
1460 radius: (size * 0.5).max(5.0),
1461 },
1462 center,
1463 );
1464 draw_errors(scene, si, s, p, x, y);
1465 }
1466}
1467fn draw_heat(
1468 scene: &mut Scene,
1469 si: usize,
1470 s: &Series,
1471 points: &[Option<MeasuredPoint>],
1472 x: AxisScale,
1473 y: AxisScale,
1474 xa: &[Axis],
1475 ya: &[Axis],
1476) {
1477 let dx = minimum_delta(
1478 points
1479 .iter()
1480 .flatten()
1481 .map(|p| p.coordinate.secondary_value),
1482 )
1483 .unwrap_or(xa[s.x_axis].unit_width);
1484 let dy = minimum_delta(points.iter().flatten().map(|p| p.coordinate.primary_value))
1485 .unwrap_or(ya[s.y_axis].unit_width);
1486 let (mut min, mut max) = (f64::INFINITY, f64::NEG_INFINITY);
1487 for p in points.iter().flatten() {
1488 min = min.min(p.coordinate.tertiary_value);
1489 max = max.max(p.coordinate.tertiary_value);
1490 }
1491 for p in points.iter().flatten() {
1492 let c = p.coordinate;
1493 let a = Point::new(
1494 x.to_pixels(c.secondary_value - dx / 2.0),
1495 y.to_pixels(c.primary_value - dy / 2.0),
1496 );
1497 let b = Point::new(
1498 x.to_pixels(c.secondary_value + dx / 2.0),
1499 y.to_pixels(c.primary_value + dy / 2.0),
1500 );
1501 if !finite_point(a) || !finite_point(b) {
1502 continue;
1503 }
1504 let rect = Rect::new(
1505 a.x.min(b.x),
1506 a.y.min(b.y),
1507 (b.x - a.x).abs(),
1508 (b.y - a.y).abs(),
1509 );
1510 let t = if max == min {
1511 0.0
1512 } else {
1513 (c.tertiary_value - min) / (max - min)
1514 };
1515 let color = heat_color(&s.heat_map, t);
1516 scene.primitives.push(
1517 Primitive::new(
1518 Geometry::Rectangle { rect, radius: 0.0 },
1519 PaintStyle {
1520 opacity: s.opacity,
1521 ..PaintStyle::fill(color)
1522 },
1523 )
1524 .clipped(scene.plot_bounds)
1525 .identified(PointId {
1526 series: si,
1527 point: p.index,
1528 }),
1529 );
1530 append_hit(
1531 scene,
1532 si,
1533 s,
1534 p,
1535 HitShape::Rectangle(rect),
1536 Point::new((a.x + b.x) * 0.5, (a.y + b.y) * 0.5),
1537 );
1538 }
1539}
1540pub(crate) fn heat_color(colors: &[Color], t: f64) -> Color {
1541 if colors.is_empty() {
1542 return Color::from_argb(0, 0, 0, 0);
1543 }
1544 if colors.len() == 1 {
1545 return colors[0];
1546 }
1547 let pos = t.clamp(0.0, 1.0) * (colors.len() - 1) as f64;
1548 let i = (pos.floor() as usize).min(colors.len() - 2);
1549 let f = (pos - i as f64).clamp(0.0, 1.0);
1550 let (a, b) = (colors[i], colors[i + 1]);
1551 let mix = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * f) as u8;
1552 Color::from_argb(mix(a.a, b.a), mix(a.r, b.r), mix(a.g, b.g), mix(a.b, b.b))
1553}
1554fn draw_financial(
1555 scene: &mut Scene,
1556 si: usize,
1557 s: &Series,
1558 points: &[Option<MeasuredPoint>],
1559 x: AxisScale,
1560 y: AxisScale,
1561 xa: &[Axis],
1562) {
1563 for p in points.iter().flatten() {
1564 let c = p.coordinate;
1565 let center = x.to_pixels(c.secondary_value);
1566 let high = y.to_pixels(c.primary_value);
1567 let low = y.to_pixels(c.quinary_value);
1568 let open = y.to_pixels(c.tertiary_value);
1569 let close = y.to_pixels(c.quaternary_value);
1570 let category_width = category_width(x, c.secondary_value, xa[s.x_axis].unit_width);
1571 let width = category_width.min(s.max_bar_width.max(1.0));
1572 if ![center, high, low, open, close, width]
1573 .iter()
1574 .all(|v| v.is_finite())
1575 {
1576 continue;
1577 }
1578 let mut style = s.style(si, true);
1579 if s.kind == SeriesKind::Financial && s.fill.is_none() && s.fill_brush.is_none() {
1580 let color = if c.quaternary_value >= c.tertiary_value {
1581 Color::from_rgb(22, 163, 74)
1582 } else {
1583 Color::from_rgb(220, 38, 38)
1584 };
1585 style.fill = Some(color);
1586 style.stroke = Some(color);
1587 }
1588 let line_style = PaintStyle {
1589 fill: None,
1590 fill_brush: None,
1591 ..style.clone()
1592 };
1593 scene.primitives.push(
1594 Primitive::new(
1595 Geometry::Line {
1596 start: Point::new(center, high),
1597 end: Point::new(center, low),
1598 },
1599 line_style.clone(),
1600 )
1601 .clipped(scene.plot_bounds)
1602 .identified(PointId {
1603 series: si,
1604 point: p.index,
1605 }),
1606 );
1607 let body = Rect::new(
1608 center - width / 2.0,
1609 open.min(close),
1610 width,
1611 (close - open).abs().max(1.0),
1612 );
1613 scene.primitives.push(
1614 Primitive::new(
1615 Geometry::Rectangle {
1616 rect: body,
1617 radius: s.corner_radius.max(0.0),
1618 },
1619 style,
1620 )
1621 .clipped(scene.plot_bounds)
1622 .identified(PointId {
1623 series: si,
1624 point: p.index,
1625 }),
1626 );
1627 if s.kind == SeriesKind::Box {
1628 for value in [high, low, y.to_pixels(c.senary_value)] {
1629 scene.primitives.push(
1630 Primitive::new(
1631 Geometry::Line {
1632 start: Point::new(center - width / 2.0, value),
1633 end: Point::new(center + width / 2.0, value),
1634 },
1635 line_style.clone(),
1636 )
1637 .clipped(scene.plot_bounds)
1638 .identified(PointId {
1639 series: si,
1640 point: p.index,
1641 }),
1642 );
1643 }
1644 }
1645 let hit = Rect::new(
1646 center - width / 2.0,
1647 high.min(low),
1648 width,
1649 (low - high).abs(),
1650 );
1651 append_hit(
1652 scene,
1653 si,
1654 s,
1655 p,
1656 HitShape::WithHoverArea {
1657 shape: Box::new(HitShape::Rectangle(hit)),
1658 bounds: Rect::new(
1659 center - category_width * 0.5,
1660 hit.y,
1661 category_width,
1662 hit.height.max(1.0),
1663 ),
1664 },
1665 Point::new(center, (high + low) * 0.5),
1666 );
1667 }
1668}
1669fn draw_errors(
1670 scene: &mut Scene,
1671 si: usize,
1672 s: &Series,
1673 p: &MeasuredPoint,
1674 x: AxisScale,
1675 y: AxisScale,
1676) {
1677 let c = p.coordinate;
1678 let error = c.point_error;
1679 if error.is_empty {
1680 return;
1681 }
1682 let (vx, vy) = if s.kind.is_row() {
1683 (p.top, c.secondary_value)
1684 } else {
1685 (c.secondary_value, p.top)
1686 };
1687 let center = Point::new(x.to_pixels(vx), y.to_pixels(vy));
1688 let cap = 4.0;
1689 let style = PaintStyle::stroke(s.color(si), s.stroke_width.max(1.0));
1690 let left = x.to_pixels(vx - error.xi.abs());
1691 let right = x.to_pixels(vx + error.xj.abs());
1692 let top = y.to_pixels(vy + error.yi.abs());
1693 let bottom = y.to_pixels(vy - error.yj.abs());
1694 if ![left, right, top, bottom].iter().all(|v| v.is_finite()) {
1695 return;
1696 }
1697 for (a, b) in [
1698 (Point::new(left, center.y), Point::new(right, center.y)),
1699 (
1700 Point::new(left, center.y - cap),
1701 Point::new(left, center.y + cap),
1702 ),
1703 (
1704 Point::new(right, center.y - cap),
1705 Point::new(right, center.y + cap),
1706 ),
1707 (Point::new(center.x, top), Point::new(center.x, bottom)),
1708 (
1709 Point::new(center.x - cap, top),
1710 Point::new(center.x + cap, top),
1711 ),
1712 (
1713 Point::new(center.x - cap, bottom),
1714 Point::new(center.x + cap, bottom),
1715 ),
1716 ] {
1717 scene.primitives.push(
1718 Primitive::new(Geometry::Line { start: a, end: b }, style.clone())
1719 .clipped(scene.plot_bounds)
1720 .identified(PointId {
1721 series: si,
1722 point: p.index,
1723 }),
1724 );
1725 }
1726}
1727fn minimum_delta(values: impl Iterator<Item = f64>) -> Option<f64> {
1728 let mut values: Vec<_> = values.filter(|v| v.is_finite()).collect();
1729 values.sort_by(f64::total_cmp);
1730 values.dedup();
1731 values
1732 .windows(2)
1733 .map(|w| w[1] - w[0])
1734 .filter(|v| v.is_finite() && *v > 0.0)
1735 .min_by(f64::total_cmp)
1736}
1737
1738#[cfg(test)]
1739mod tests {
1740 use super::*;
1741 fn scene(series: &[Series]) -> Scene {
1742 CartesianChartEngine::measure(
1743 Size::new(640.0, 400.0),
1744 series,
1745 &[],
1746 &[],
1747 &CartesianOptions::default(),
1748 )
1749 .unwrap()
1750 }
1751 #[test]
1752 fn line_nulls_split_paths_and_keep_original_point_indices() {
1753 let mut s = Series::from_values(SeriesKind::Line, [1.0, 2.0, 3.0, 4.0]);
1754 s.values[1] = Coordinate::EMPTY;
1755 s.geometry_size = 0.0;
1756 let result = scene(&[s]);
1757 assert_eq!(
1758 result.hits.iter().map(|h| h.id.point).collect::<Vec<_>>(),
1759 vec![0, 2, 3]
1760 );
1761 assert_eq!(
1762 result
1763 .primitives
1764 .iter()
1765 .filter(|p| matches!(p.geometry, Geometry::Path(_)))
1766 .count(),
1767 2
1768 );
1769 }
1770 #[test]
1771 fn line_can_connect_across_nulls() {
1772 let mut s = Series::from_values(SeriesKind::Line, [1.0, 2.0, 3.0]);
1773 s.values[1] = Coordinate::EMPTY;
1774 s.enable_null_splitting = false;
1775 s.geometry_size = 0.0;
1776 assert_eq!(
1777 scene(&[s])
1778 .primitives
1779 .iter()
1780 .filter(|p| matches!(p.geometry, Geometry::Path(_)))
1781 .count(),
1782 1
1783 );
1784 }
1785 #[test]
1786 fn range_bounds_include_both_endpoints() {
1787 let mut s = Series::new(SeriesKind::RangeColumn);
1788 s.values = vec![Coordinate::range(0.0, 10.0, -50.0)];
1789 let m = CartesianChartEngine::measure_with_axes(
1790 Size::new(640.0, 400.0),
1791 &[s],
1792 &[],
1793 &[],
1794 &CartesianOptions::default(),
1795 )
1796 .unwrap();
1797 assert!(m.y_scales[0].min <= -50.0);
1798 assert!(m.y_scales[0].max >= 10.0);
1799 }
1800 #[test]
1801 fn stacked_positive_and_negative_bounds_are_independent() {
1802 let a = Series::from_values(SeriesKind::StackedColumn, [3.0, -2.0]);
1803 let b = Series::from_values(SeriesKind::StackedColumn, [4.0, -5.0]);
1804 let m = CartesianChartEngine::measure_with_axes(
1805 Size::new(640.0, 400.0),
1806 &[a, b],
1807 &[],
1808 &[],
1809 &CartesianOptions::default(),
1810 )
1811 .unwrap();
1812 assert!(m.y_scales[0].min <= -7.0);
1813 assert!(m.y_scales[0].max >= 7.0);
1814 assert_eq!(m.scene.hits.len(), 4);
1815 }
1816 #[test]
1817 fn stacked_hundred_percent_normalizes_categories() {
1818 let mut a = Series::from_values(SeriesKind::StackedColumn, [3.0, 1.0]);
1819 a.stack_100_percent = true;
1820 let mut b = Series::from_values(SeriesKind::StackedColumn, [1.0, 4.0]);
1821 b.stack_100_percent = true;
1822 let m = CartesianChartEngine::measure_with_axes(
1823 Size::new(640.0, 400.0),
1824 &[a, b],
1825 &[],
1826 &[],
1827 &CartesianOptions::default(),
1828 )
1829 .unwrap();
1830 assert!(m.y_scales[0].max >= 100.0 && m.y_scales[0].max < 110.0);
1831 }
1832 #[test]
1833 fn nonfinite_points_are_gaps_not_renderer_inputs() {
1834 let result = scene(&[Series::from_values(
1835 SeriesKind::Line,
1836 [f64::NAN, 1.0, f64::INFINITY, 2.0],
1837 )]);
1838 assert_eq!(result.hits.len(), 2);
1839 }
1840 #[test]
1841 fn missing_axis_is_an_error() {
1842 let mut s = Series::default();
1843 s.y_axis = 2;
1844 assert!(CartesianChartEngine::measure(
1845 Size::new(640.0, 400.0),
1846 &[s],
1847 &[],
1848 &[],
1849 &CartesianOptions::default()
1850 )
1851 .is_err());
1852 }
1853 #[test]
1854 fn grouped_bars_have_distinct_centers_and_shared_category() {
1855 let result = scene(&[
1856 Series::from_values(SeriesKind::Column, [3.0]),
1857 Series::from_values(SeriesKind::Column, [4.0]),
1858 ]);
1859 assert_ne!(result.hits[0].center.x, result.hits[1].center.x);
1860 }
1861 #[test]
1862 fn all_cartesian_series_emit_measured_hits() {
1863 for kind in [
1864 SeriesKind::Line,
1865 SeriesKind::StepLine,
1866 SeriesKind::Area,
1867 SeriesKind::StackedArea,
1868 SeriesKind::StackedStepArea,
1869 SeriesKind::Column,
1870 SeriesKind::Row,
1871 SeriesKind::StackedColumn,
1872 SeriesKind::StackedRow,
1873 SeriesKind::Scatter,
1874 SeriesKind::Heat,
1875 SeriesKind::RangeColumn,
1876 SeriesKind::RangeRow,
1877 SeriesKind::RangeLine,
1878 SeriesKind::Financial,
1879 SeriesKind::Box,
1880 ] {
1881 let mut s = Series::new(kind);
1882 s.values = vec![
1883 Coordinate::box_value(0.0, 10.0, 7.0, 3.0, 1.0, 5.0),
1884 Coordinate::box_value(1.0, 12.0, 8.0, 4.0, 2.0, 6.0),
1885 ];
1886 assert_eq!(scene(&[s]).hits.len(), 2, "{kind:?}");
1887 }
1888 }
1889 #[test]
1890 fn heat_cells_infer_datetime_spacing() {
1891 let mut s = Series::new(SeriesKind::Heat);
1892 s.values = vec![
1893 Coordinate::weighted(1_000_000.0, 1.0, 3.0),
1894 Coordinate::weighted(1_100_000.0, 2.0, 4.0),
1895 ];
1896 let result = scene(&[s]);
1897 let HitShape::Rectangle(rect) = result.hits[0].shape else {
1898 panic!("expected heat rect")
1899 };
1900 assert!(rect.width > 100.0);
1901 }
1902 #[test]
1903 fn numeric_extremes_scale_without_overflow() {
1904 let s = Series::from_values(SeriesKind::Line, [-1e308, 1e308]);
1905 let result = scene(&[s]);
1906 assert_eq!(result.hits.len(), 2);
1907 assert!(result.hits.iter().all(|h| finite_point(h.center)));
1908 }
1909 #[test]
1910 fn logarithmic_axes_round_trip() {
1911 let axis = Axis {
1912 min_limit: Some(1.0),
1913 max_limit: Some(1000.0),
1914 logarithmic_base: Some(10.0),
1915 ..Axis::default()
1916 };
1917 let s = AxisScale::new(&axis, 1.0, 1000.0, 0.0, 300.0).unwrap();
1918 assert!((s.to_pixels(10.0) - 100.0).abs() < 1e-8);
1919 assert!((s.to_chart_values(200.0) - 100.0).abs() < 1e-8);
1920 }
1921 #[test]
1922 fn inverted_axis_round_trips() {
1923 let axis = Axis {
1924 inverted: true,
1925 ..Axis::default()
1926 };
1927 let s = AxisScale::new(&axis, 0.0, 10.0, 0.0, 100.0).unwrap();
1928 assert!(s.to_pixels(0.0) > s.to_pixels(10.0));
1929 assert!((s.to_chart_values(s.to_pixels(3.0)) - 3.0).abs() < 1e-10);
1930 }
1931 #[test]
1932 fn automatic_hover_compares_the_shared_category_axis() {
1933 let result = scene(&[Series::from_values(SeriesKind::Line, [2.0, 8.0])]);
1936 let x = result.hits[0].center.x;
1937 let pointer = Point::new(x, result.plot_bounds.y + result.plot_bounds.height * 0.5);
1938 assert_eq!(
1939 result
1940 .hit_test(pointer, FindingStrategy::Automatic, 0.0)
1941 .len(),
1942 1
1943 );
1944 assert!(result
1945 .hit_test(pointer, FindingStrategy::CompareAll, 0.0)
1946 .is_empty());
1947 let rows = scene(&[Series::from_values(SeriesKind::Row, [2.0, 8.0])]);
1948 assert_eq!(
1949 rows.automatic_finding_strategy,
1950 FindingStrategy::CompareOnlyYTakeClosest
1951 );
1952 let mixed = scene(&[
1953 Series::from_values(SeriesKind::Column, [2.0]),
1954 Series::from_values(SeriesKind::Scatter, [4.0]),
1955 ]);
1956 assert_eq!(
1957 mixed.automatic_finding_strategy,
1958 FindingStrategy::CompareAllTakeClosest
1959 );
1960 }
1961 #[test]
1962 fn range_hover_covers_band_but_exact_match_targets_either_marker() {
1963 let mut range = Series::new(SeriesKind::RangeLine);
1966 range.values = vec![Coordinate::range(0.0, 10.0, 2.0)];
1967 let result = scene(&[range]);
1968 let hit = &result.hits[0];
1969 let HitShape::WithHoverArea { shape, .. } = &hit.shape else {
1970 panic!("range hover");
1971 };
1972 let HitShape::Compound(markers) = shape.as_ref() else {
1973 panic!("range endpoints");
1974 };
1975 let HitShape::Circle { center: low, .. } = markers[1] else {
1976 panic!("low endpoint");
1977 };
1978 assert_eq!(
1979 result.hit_test(low, FindingStrategy::ExactMatch, 0.0).len(),
1980 1
1981 );
1982 assert_eq!(
1983 result
1984 .hit_test(hit.center, FindingStrategy::CompareAll, 0.0)
1985 .len(),
1986 1
1987 );
1988 assert!(result
1989 .hit_test(hit.center, FindingStrategy::ExactMatch, 0.0)
1990 .is_empty());
1991 assert!(hit.label.contains('2') && hit.label.contains("10"));
1992 }
1993 #[test]
1994 fn narrow_column_hover_uses_category_slot_and_exact_match_uses_visual() {
1995 let mut column = Series::from_values(SeriesKind::Column, [4.0]);
1997 column.max_bar_width = 5.0;
1998 let result = scene(&[column]);
1999 let hit = &result.hits[0];
2000 let bounds = hit.shape.hover_bounds();
2001 let pointer = Point::new(
2002 bounds.x + bounds.width * 0.25,
2003 bounds.y + bounds.height * 0.5,
2004 );
2005 assert_eq!(
2006 result
2007 .hit_test(pointer, FindingStrategy::Automatic, 0.0)
2008 .len(),
2009 1
2010 );
2011 assert!(result
2012 .hit_test(pointer, FindingStrategy::ExactMatch, 0.0)
2013 .is_empty());
2014 }
2015 #[test]
2016 fn line_fill_is_behind_markers_and_labels() {
2017 let mut area = Series::from_values(SeriesKind::Area, [1.0, 3.0, 2.0]);
2018 area.fill = Some(Color::from_rgb(255, 0, 0));
2019 area.data_labels = true;
2020 let result = scene(&[area]);
2021 let last_path = result
2022 .primitives
2023 .iter()
2024 .rposition(|p| matches!(p.geometry, Geometry::Path(_)))
2025 .unwrap();
2026 let first_marker = result
2027 .primitives
2028 .iter()
2029 .position(|p| matches!(p.geometry, Geometry::Ellipse(_)))
2030 .unwrap();
2031 assert!(last_path < first_marker);
2032 }
2033 #[test]
2034 fn duplicate_heat_series_do_not_expand_shared_bounds_twice() {
2035 let mut heat = Series::new(SeriesKind::Heat);
2036 heat.values = vec![
2037 Coordinate::weighted(0.0, 1.0, 42.0),
2038 Coordinate::weighted(2.0, 3.0, 7.0),
2039 ];
2040 let measure = |series: &[Series]| {
2041 CartesianChartEngine::measure_with_axes(
2042 Size::new(640.0, 400.0),
2043 series,
2044 &[],
2045 &[],
2046 &CartesianOptions::default(),
2047 )
2048 .unwrap()
2049 };
2050 let single = measure(&[heat.clone()]);
2051 let duplicate = measure(&[heat.clone(), heat]);
2052 assert_eq!(single.x_scales, duplicate.x_scales);
2053 assert_eq!(single.y_scales, duplicate.y_scales);
2054 assert_eq!(single.scene.hits[0].value, 42.0);
2055 assert_eq!(single.scene.hits[0].label, "42");
2056 }
2057}