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