1use chartml_core::data::DataTable;
2use chartml_core::element::{ChartElement, ElementData, Transform, ViewBox};
3use chartml_core::error::ChartError;
4use chartml_core::layout::margins::{calculate_margins, MarginConfig};
5use chartml_core::layout::stack::{StackLayout, StackOffset};
6use chartml_core::plugin::ChartConfig;
7use chartml_core::scales::{ScaleBand, ScaleLinear};
8use chartml_core::shapes::AreaGenerator;
9
10use chartml_core::layout::labels::{LabelStrategy, LabelStrategyConfig};
11
12use chartml_core::layout::legend::{calculate_legend_layout, LegendConfig};
13
14use crate::helpers::{GridConfig, generate_annotations, generate_x_axis, generate_y_axis_numeric, generate_legend, get_color_field, get_field_name, get_x_format, get_y_format, offset_element};
15
16pub fn render_area(data: &DataTable, config: &ChartConfig) -> Result<ChartElement, ChartError> {
17 let category_field = get_field_name(&config.visualize.columns)?;
18 let value_field = get_field_name(&config.visualize.rows)?;
19
20 let categories = data.unique_values(&category_field);
21 if categories.is_empty() {
22 return Err(ChartError::DataError("No category values found".into()));
23 }
24
25 let color_field = get_color_field(config);
26 let is_stacked = matches!(config.visualize.mode, Some(chartml_core::spec::ChartMode::Stacked));
27 let is_normalized = matches!(config.visualize.mode, Some(chartml_core::spec::ChartMode::Normalized));
28 let y_fmt = get_y_format(config);
29 let y_fmt_ref = y_fmt.as_deref();
30 let grid = GridConfig::from_config(config);
31 let left_axis_label = config.visualize.axes.as_ref()
32 .and_then(|a| a.left.as_ref())
33 .and_then(|a| a.label.as_deref());
34
35 let estimated_width = config.width - 80.0;
37 let x_format = get_x_format(config);
38 let x_strategy = LabelStrategy::determine(&categories, estimated_width, &LabelStrategyConfig::default());
39 let x_extra_margin = match &x_strategy {
40 LabelStrategy::Rotated { margin, .. } => *margin,
41 _ => 0.0,
42 };
43
44 let prelim_values: Vec<f64> = (0..data.num_rows()).filter_map(|i| data.get_f64(i, &value_field)).collect();
46 let prelim_min = prelim_values.iter().cloned().fold(f64::INFINITY, f64::min);
47 let prelim_max = prelim_values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
48 let prelim_max = if prelim_max <= 0.0 { 1.0 } else { prelim_max };
49 let prelim_domain_min = if prelim_min < 0.0 { prelim_min } else { 0.0 };
51 let (prelim_nice_min, prelim_nice_max) = crate::helpers::nice_domain(prelim_domain_min, prelim_max, 5);
52 let area_prelim_fmt = if is_normalized { Some(".0%") } else { y_fmt_ref };
53 let area_prelim_labels = vec![
54 crate::helpers::format_value(prelim_nice_min, area_prelim_fmt),
55 crate::helpers::format_value(prelim_nice_max, area_prelim_fmt),
56 ];
57
58 let has_x_axis_label = config.visualize.axes.as_ref()
60 .and_then(|a| a.x.as_ref())
61 .and_then(|a| a.label.as_ref())
62 .is_some();
63 let margin_config = MarginConfig {
64 has_title: config.title.is_some(),
65 has_legend: color_field.is_some(),
66 has_x_axis_label,
67 x_label_strategy_margin: x_extra_margin,
68 y_tick_labels: area_prelim_labels,
69 ..Default::default()
70 };
71 let margins = calculate_margins(&margin_config);
72
73 let inner_width = margins.inner_width(config.width);
74 let inner_height = margins.inner_height(config.height);
75
76 let band = ScaleBand::new(categories.clone(), (0.0, inner_width));
77 let bandwidth = band.bandwidth();
78
79 let mut children = Vec::new();
80
81 let area_gen = AreaGenerator::new().curve(chartml_core::shapes::CurveType::MonotoneX);
84 let mut area_elements = Vec::new();
85
86 let y_domain_min: f64;
88 let y_domain_max: f64;
89
90 if let Some(ref color_f) = color_field {
91 let series_names = data.unique_values(color_f);
92 let groups = data.group_by(color_f);
93
94 if (is_stacked || is_normalized) && series_names.len() > 1 {
95 let mut values_matrix: Vec<Vec<f64>> = Vec::new();
97 for series in &series_names {
98 let series_data = groups.get(series);
99 let mut series_vals = Vec::new();
100 for cat in &categories {
101 let val = series_data
102 .map(|sd| {
103 (0..sd.num_rows())
104 .find_map(|i| {
105 if sd.get_string(i, &category_field).as_deref() == Some(cat.as_str()) {
106 sd.get_f64(i, &value_field)
107 } else {
108 None
109 }
110 })
111 .unwrap_or(0.0)
112 })
113 .unwrap_or(0.0);
114 series_vals.push(val);
115 }
116 values_matrix.push(series_vals);
117 }
118
119 let stack = if is_normalized {
120 StackLayout::new().offset(StackOffset::Normalize)
121 } else {
122 StackLayout::new()
123 };
124 let stacked_points = stack.layout(&categories, &series_names, &values_matrix);
125
126 let (value_min, value_max, y_axis_fmt): (f64, f64, Option<&str>) = if is_normalized {
130 (0.0, 1.0, Some(".0%"))
131 } else {
132 let raw_value_max = stacked_points
133 .iter()
134 .map(|p| p.y1)
135 .fold(0.0_f64, f64::max);
136 let raw_value_max = if raw_value_max <= 0.0 { 1.0 } else { raw_value_max };
137 let (_, nice_max) = crate::helpers::nice_domain(0.0, raw_value_max, 5);
139 (0.0, nice_max, y_fmt_ref)
140 };
141 let linear = ScaleLinear::new((value_min, value_max), (inner_height, 0.0));
142 y_domain_min = value_min;
143 y_domain_max = value_max;
144
145 for (series_idx, series_name) in series_names.iter().enumerate() {
147 let mut series_points: Vec<(f64, f64, f64)> = Vec::new();
148 let mut dot_data: Vec<(String, f64, f64)> = Vec::new(); for cat in &categories {
151 let point = match stacked_points.iter().find(|p| {
152 p.key == *cat && p.series == *series_name
153 }) {
154 Some(p) => p,
155 None => continue,
156 };
157 let x = match band.map(cat) {
158 Some(x) => x + bandwidth / 2.0,
159 None => continue,
160 };
161 let y0 = linear.map(point.y0);
162 let y1 = linear.map(point.y1);
163 series_points.push((x, y0, y1));
164 let series_val = point.y1 - point.y0;
166 dot_data.push((cat.clone(), series_val, y1));
167 }
168
169 if series_points.is_empty() {
170 continue;
171 }
172
173 let path_d = area_gen.generate(&series_points);
174 let color = config
175 .colors
176 .get(series_idx)
177 .cloned()
178 .unwrap_or_else(|| "#2E7D9A".to_string());
179
180 area_elements.push(ChartElement::Path {
181 d: path_d,
182 fill: Some(color.clone()),
183 stroke: None,
184 stroke_width: None,
185 stroke_dasharray: None,
186 opacity: Some(0.6),
187 class: "area".to_string(),
188 data: Some(ElementData::new(series_name, "").with_series(series_name)),
189 });
190
191 let line_d = area_gen.generate_line(&series_points);
193 area_elements.push(ChartElement::Path {
194 d: line_d,
195 fill: None,
196 stroke: Some(color.clone()),
197 stroke_width: Some(2.0),
198 stroke_dasharray: None,
199 opacity: None,
200 class: "line".to_string(),
201 data: Some(ElementData::new(series_name, "").with_series(series_name)),
202 });
203
204 }
206
207 let bottom_axis_label = config.visualize.axes.as_ref()
209 .and_then(|a| a.x.as_ref())
210 .and_then(|a| a.label.as_deref());
211 let x_axis_result =
212 generate_x_axis(&crate::helpers::XAxisParams {
213 labels: &categories,
214 display_label_overrides: None,
215 range: (0.0, inner_width),
216 y_position: margins.top + inner_height,
217 available_width: inner_width,
218 x_format: x_format.as_deref(),
219 chart_height: Some(inner_height),
220 grid: &grid,
221 axis_label: bottom_axis_label,
222 });
223 let y_axis_elements =
224 generate_y_axis_numeric(&crate::helpers::YAxisNumericParams {
225 domain: (value_min, value_max),
226 range: (inner_height, 0.0),
227 x_position: margins.left,
228 fmt: y_axis_fmt,
229 tick_count: 5,
230 chart_width: Some(inner_width),
231 grid: &grid,
232 axis_label: left_axis_label,
233 });
234
235 children.push(ChartElement::Group {
236 class: "axes".to_string(),
237 transform: None,
238 children: {
239 let mut axes = Vec::new();
240 axes.extend(
241 x_axis_result.elements
242 .into_iter()
243 .map(|e| offset_element(e, margins.left, 0.0)),
244 );
245 axes.extend(
246 y_axis_elements
247 .into_iter()
248 .map(|e| offset_element(e, 0.0, margins.top)),
249 );
250 axes
251 },
252 });
253 } else {
254 let values: Vec<f64> = (0..data.num_rows())
256 .filter_map(|i| data.get_f64(i, &value_field))
257 .collect();
258 let raw_value_min = values.iter().cloned().fold(f64::INFINITY, f64::min);
259 let raw_value_max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
260 let raw_value_max = if raw_value_max <= 0.0 { 1.0 } else { raw_value_max };
261 let domain_min = if raw_value_min < 0.0 { raw_value_min } else { 0.0 };
263 let (value_min, value_max) = crate::helpers::nice_domain(domain_min, raw_value_max, 5);
264 let linear = ScaleLinear::new((value_min, value_max), (inner_height, 0.0));
265 y_domain_min = value_min;
266 y_domain_max = value_max;
267 let baseline = linear.map(0.0);
268
269 for (series_idx, series_name) in series_names.iter().enumerate() {
270 let series_data = match groups.get(series_name) {
271 Some(d) => d,
272 None => continue,
273 };
274
275 let mut points: Vec<(f64, f64, f64)> = Vec::new();
276 let mut dot_data: Vec<(String, f64)> = Vec::new();
277
278 for cat in &categories {
279 let row_i = match (0..series_data.num_rows()).find(|&i| {
280 series_data.get_string(i, &category_field).as_deref() == Some(cat.as_str())
281 }) {
282 Some(i) => i,
283 None => continue,
284 };
285 let val = match series_data.get_f64(row_i, &value_field) {
286 Some(v) => v,
287 None => continue,
288 };
289 let x = match band.map(cat) {
290 Some(x) => x + bandwidth / 2.0,
291 None => continue,
292 };
293 let y = linear.map(val);
294 points.push((x, baseline, y));
295 dot_data.push((cat.clone(), val));
296 }
297
298 if points.is_empty() {
299 continue;
300 }
301
302 let path_d = area_gen.generate(&points);
303 let color = config
304 .colors
305 .get(series_idx)
306 .cloned()
307 .unwrap_or_else(|| "#2E7D9A".to_string());
308
309 area_elements.push(ChartElement::Path {
310 d: path_d,
311 fill: Some(color.clone()),
312 stroke: None,
313 stroke_width: None,
314 stroke_dasharray: None,
315 opacity: Some(0.6),
316 class: "area".to_string(),
317 data: Some(ElementData::new(series_name, "").with_series(series_name)),
318 });
319
320 let line_d = area_gen.generate_line(&points);
322 area_elements.push(ChartElement::Path {
323 d: line_d,
324 fill: None,
325 stroke: Some(color.clone()),
326 stroke_width: Some(2.0),
327 stroke_dasharray: None,
328 opacity: None,
329 class: "line".to_string(),
330 data: Some(ElementData::new(series_name, "").with_series(series_name)),
331 });
332
333 }
334
335 let bottom_axis_label = config.visualize.axes.as_ref()
337 .and_then(|a| a.x.as_ref())
338 .and_then(|a| a.label.as_deref());
339 let x_axis_result =
340 generate_x_axis(&crate::helpers::XAxisParams {
341 labels: &categories,
342 display_label_overrides: None,
343 range: (0.0, inner_width),
344 y_position: margins.top + inner_height,
345 available_width: inner_width,
346 x_format: x_format.as_deref(),
347 chart_height: Some(inner_height),
348 grid: &grid,
349 axis_label: bottom_axis_label,
350 });
351 let y_axis_elements =
352 generate_y_axis_numeric(&crate::helpers::YAxisNumericParams {
353 domain: (value_min, value_max),
354 range: (inner_height, 0.0),
355 x_position: margins.left,
356 fmt: None,
357 tick_count: 5,
358 chart_width: Some(inner_width),
359 grid: &grid,
360 axis_label: left_axis_label,
361 });
362
363 children.push(ChartElement::Group {
364 class: "axes".to_string(),
365 transform: None,
366 children: {
367 let mut axes = Vec::new();
368 axes.extend(
369 x_axis_result.elements
370 .into_iter()
371 .map(|e| offset_element(e, margins.left, 0.0)),
372 );
373 axes.extend(
374 y_axis_elements
375 .into_iter()
376 .map(|e| offset_element(e, 0.0, margins.top)),
377 );
378 axes
379 },
380 });
381 }
382
383 let series_names_for_legend = data.unique_values(color_f);
385 let legend_config = LegendConfig::default();
386 let legend_layout = calculate_legend_layout(&series_names_for_legend, &config.colors, config.width, &legend_config);
387 let legend_y = config.height - legend_layout.total_height - 8.0;
388 let legend_elements = generate_legend(
389 &series_names_for_legend,
390 &config.colors,
391 config.width,
392 legend_y,
393 );
394 children.push(ChartElement::Group {
395 class: "legend".to_string(),
396 transform: None,
397 children: legend_elements,
398 });
399 } else {
400 let values: Vec<f64> = (0..data.num_rows())
402 .filter_map(|i| data.get_f64(i, &value_field))
403 .collect();
404 let raw_value_min = values.iter().cloned().fold(f64::INFINITY, f64::min);
405 let raw_value_max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
406 let raw_value_max = if raw_value_max <= 0.0 { 1.0 } else { raw_value_max };
407 let domain_min = if raw_value_min < 0.0 { raw_value_min } else { 0.0 };
409 let (value_min, value_max) = crate::helpers::nice_domain(domain_min, raw_value_max, 5);
410 let linear = ScaleLinear::new((value_min, value_max), (inner_height, 0.0));
411 y_domain_min = value_min;
412 y_domain_max = value_max;
413 let baseline = linear.map(0.0);
414
415 let mut points: Vec<(f64, f64, f64)> = Vec::new();
416
417 for cat in &categories {
418 let row_i = match (0..data.num_rows()).find(|&i| {
419 data.get_string(i, &category_field).as_deref() == Some(cat.as_str())
420 }) {
421 Some(i) => i,
422 None => continue,
423 };
424 let val = match data.get_f64(row_i, &value_field) {
425 Some(v) => v,
426 None => continue,
427 };
428 let x = match band.map(cat) {
429 Some(x) => x + bandwidth / 2.0,
430 None => continue,
431 };
432 let y = linear.map(val);
433 points.push((x, baseline, y));
434 }
435
436 if !points.is_empty() {
437 let path_d = area_gen.generate(&points);
438 let color = config
439 .colors
440 .first()
441 .cloned()
442 .unwrap_or_else(|| "#2E7D9A".to_string());
443
444 area_elements.push(ChartElement::Path {
445 d: path_d,
446 fill: Some(color.clone()),
447 stroke: None,
448 stroke_width: None,
449 stroke_dasharray: None,
450 opacity: Some(0.6),
451 class: "area".to_string(),
452 data: None,
453 });
454
455 let line_d = area_gen.generate_line(&points);
457 area_elements.push(ChartElement::Path {
458 d: line_d,
459 fill: None,
460 stroke: Some(color.clone()),
461 stroke_width: Some(2.0),
462 stroke_dasharray: None,
463 opacity: None,
464 class: "line".to_string(),
465 data: None,
466 });
467
468 }
470
471 let bottom_axis_label = config.visualize.axes.as_ref()
473 .and_then(|a| a.x.as_ref())
474 .and_then(|a| a.label.as_deref());
475 let x_axis_result =
476 generate_x_axis(&crate::helpers::XAxisParams {
477 labels: &categories,
478 display_label_overrides: None,
479 range: (0.0, inner_width),
480 y_position: margins.top + inner_height,
481 available_width: inner_width,
482 x_format: x_format.as_deref(),
483 chart_height: Some(inner_height),
484 grid: &grid,
485 axis_label: bottom_axis_label,
486 });
487 let y_axis_elements =
488 generate_y_axis_numeric(&crate::helpers::YAxisNumericParams {
489 domain: (value_min, value_max),
490 range: (inner_height, 0.0),
491 x_position: margins.left,
492 fmt: None,
493 tick_count: 5,
494 chart_width: Some(inner_width),
495 grid: &grid,
496 axis_label: left_axis_label,
497 });
498
499 children.push(ChartElement::Group {
500 class: "axes".to_string(),
501 transform: None,
502 children: {
503 let mut axes = Vec::new();
504 axes.extend(
505 x_axis_result.elements
506 .into_iter()
507 .map(|e| offset_element(e, margins.left, 0.0)),
508 );
509 axes.extend(
510 y_axis_elements
511 .into_iter()
512 .map(|e| offset_element(e, 0.0, margins.top)),
513 );
514 axes
515 },
516 });
517 }
518
519 children.push(ChartElement::Group {
520 class: "areas".to_string(),
521 transform: Some(Transform::Translate(margins.left, margins.top)),
522 children: area_elements,
523 });
524
525 if let Some(annotations) = config.visualize.annotations.as_deref() {
527 if !annotations.is_empty() {
528 use chartml_core::scales::ScaleLinear;
529 let ann_scale = ScaleLinear::new((y_domain_min, y_domain_max), (inner_height, 0.0));
530 let ann_elements = generate_annotations(
531 annotations,
532 &ann_scale,
533 0.0,
534 inner_width,
535 inner_height,
536 Some(&categories),
537 );
538 if !ann_elements.is_empty() {
539 children.push(ChartElement::Group {
540 class: "annotations".to_string(),
541 transform: Some(Transform::Translate(margins.left, margins.top)),
542 children: ann_elements,
543 });
544 }
545 }
546 }
547
548 Ok(ChartElement::Svg {
549 viewbox: ViewBox::new(0.0, 0.0, config.width, config.height),
550 width: Some(config.width),
551 height: Some(config.height),
552 class: "chartml-area".to_string(),
553 children,
554 })
555}
556