1use super::*;
4use crate::model::ChartSeries;
5
6pub struct AreaChartConfig {
8 pub show_grid: bool,
9 pub title: Option<String>,
10}
11
12pub fn build(
14 width: f64,
15 height: f64,
16 series: &[ChartSeries],
17 labels: &[String],
18 config: &AreaChartConfig,
19) -> Vec<ChartPrimitive> {
20 if series.is_empty() || labels.is_empty() {
21 return vec![];
22 }
23
24 let mut primitives = Vec::new();
25
26 let title_offset = if config.title.is_some() {
27 TITLE_HEIGHT
28 } else {
29 0.0
30 };
31
32 let plot_left = Y_AXIS_WIDTH;
33 let plot_top = title_offset;
34 let plot_right = width - LABEL_MARGIN;
35 let plot_bottom = height - X_AXIS_HEIGHT;
36 let plot_width = plot_right - plot_left;
37 let plot_height = plot_bottom - plot_top;
38
39 if plot_width <= 0.0 || plot_height <= 0.0 {
40 return vec![];
41 }
42
43 let max_value = series
44 .iter()
45 .flat_map(|s| s.data.iter())
46 .copied()
47 .fold(0.0_f64, f64::max);
48 let y_max = nice_number(max_value);
49 let y_ticks = 5;
50 let n_points = labels.len();
51
52 if config.show_grid {
54 for i in 0..=y_ticks {
55 let frac = i as f64 / y_ticks as f64;
56 let y = plot_bottom - frac * plot_height;
57 primitives.push(ChartPrimitive::Line {
58 x1: plot_left,
59 y1: y,
60 x2: plot_right,
61 y2: y,
62 stroke: GRID_COLOR,
63 width: 0.5,
64 });
65 }
66 }
67
68 for i in 0..=y_ticks {
70 let frac = i as f64 / y_ticks as f64;
71 let y = plot_bottom - frac * plot_height;
72 let value = y_max * frac;
73 primitives.push(ChartPrimitive::Label {
74 text: format_number(value),
75 x: plot_left - LABEL_MARGIN,
76 y: y + AXIS_LABEL_FONT * 0.35,
77 font_size: AXIS_LABEL_FONT,
78 color: LABEL_COLOR,
79 anchor: TextAnchor::Right,
80 });
81 }
82
83 primitives.push(ChartPrimitive::Line {
85 x1: plot_left,
86 y1: plot_top,
87 x2: plot_left,
88 y2: plot_bottom,
89 stroke: AXIS_COLOR,
90 width: 1.0,
91 });
92 primitives.push(ChartPrimitive::Line {
93 x1: plot_left,
94 y1: plot_bottom,
95 x2: plot_right,
96 y2: plot_bottom,
97 stroke: AXIS_COLOR,
98 width: 1.0,
99 });
100
101 for (i, label) in labels.iter().enumerate() {
103 let x = if n_points > 1 {
104 plot_left + (i as f64 / (n_points - 1) as f64) * plot_width
105 } else {
106 plot_left + plot_width / 2.0
107 };
108 primitives.push(ChartPrimitive::Label {
109 text: label.clone(),
110 x,
111 y: plot_bottom + AXIS_LABEL_FONT + LABEL_MARGIN,
112 font_size: AXIS_LABEL_FONT,
113 color: LABEL_COLOR,
114 anchor: TextAnchor::Center,
115 });
116 }
117
118 for (si, s) in series.iter().enumerate().rev() {
120 let color = resolve_color(s.color.as_deref(), si);
121 let mut line_points = Vec::new();
122
123 for (i, &value) in s.data.iter().enumerate() {
124 if i >= n_points {
125 break;
126 }
127 let x = if n_points > 1 {
128 plot_left + (i as f64 / (n_points - 1) as f64) * plot_width
129 } else {
130 plot_left + plot_width / 2.0
131 };
132 let y = if y_max > 0.0 {
133 plot_bottom - (value / y_max) * plot_height
134 } else {
135 plot_bottom
136 };
137 line_points.push((x, y));
138 }
139
140 if line_points.len() >= 2 {
141 let mut fill_points = line_points.clone();
143 fill_points.push((line_points.last().unwrap().0, plot_bottom));
145 fill_points.push((line_points[0].0, plot_bottom));
146
147 primitives.push(ChartPrimitive::FilledPath {
148 points: fill_points,
149 fill: color,
150 opacity: 0.3,
151 });
152
153 primitives.push(ChartPrimitive::Polyline {
154 points: line_points,
155 stroke: color,
156 width: 2.0,
157 });
158 }
159 }
160
161 if let Some(ref title) = config.title {
163 primitives.push(ChartPrimitive::Label {
164 text: title.clone(),
165 x: width / 2.0,
166 y: TITLE_FONT,
167 font_size: TITLE_FONT,
168 color: Color::BLACK,
169 anchor: TextAnchor::Center,
170 });
171 }
172
173 primitives
174}