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, TextMetrics};
11
12use chartml_core::layout::legend::{calculate_legend_layout, LegendConfig};
13
14use crate::helpers::{GridConfig, emit_zero_line_if_crosses, 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 formatted_for_strategy = crate::helpers::format_display_labels(&categories, x_format.as_deref());
39 let x_strategy = LabelStrategy::determine(&formatted_for_strategy, estimated_width, &LabelStrategyConfig {
40 text_metrics: TextMetrics::from_theme_axis_label(&config.theme),
41 ..LabelStrategyConfig::default()
42 });
43 let x_extra_margin = match &x_strategy {
44 LabelStrategy::Rotated { margin, .. } => *margin,
45 _ => 0.0,
46 };
47
48 let prelim_values: Vec<f64> = (0..data.num_rows()).filter_map(|i| data.get_f64(i, &value_field)).collect();
50 let prelim_min = prelim_values.iter().cloned().fold(f64::INFINITY, f64::min);
51 let prelim_max = prelim_values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
52 let prelim_max = if prelim_max <= 0.0 { 1.0 } else { prelim_max };
53 let prelim_domain_min = if prelim_min < 0.0 { prelim_min } else { 0.0 };
55 let (prelim_nice_min, prelim_nice_max) = crate::helpers::nice_domain(prelim_domain_min, prelim_max, 5);
56 let area_prelim_fmt = if is_normalized { Some(".0%") } else { y_fmt_ref };
57 let area_prelim_labels = vec![
58 crate::helpers::format_value(prelim_nice_min, area_prelim_fmt),
59 crate::helpers::format_value(prelim_nice_max, area_prelim_fmt),
60 ];
61
62 let legend_height = if let Some(ref color_f) = color_field {
64 let series_names = data.unique_values(color_f);
65 let legend_config = LegendConfig {
66 text_metrics: TextMetrics::from_theme_legend(&config.theme),
67 ..LegendConfig::default()
68 };
69 calculate_legend_layout(&series_names, &config.colors, config.width, &legend_config).total_height
70 } else {
71 0.0
72 };
73
74 let has_x_axis_label = config.visualize.axes.as_ref()
76 .and_then(|a| a.x.as_ref())
77 .and_then(|a| a.label.as_ref())
78 .is_some();
79 let has_y_axis_label = config.visualize.axes.as_ref()
80 .and_then(|a| a.left.as_ref())
81 .and_then(|a| a.label.as_ref())
82 .is_some();
83 let margin_config = MarginConfig {
84 has_title: config.title.is_some(),
85 legend_height,
86 has_x_axis_label,
87 has_y_axis_label,
88 x_label_strategy_margin: x_extra_margin,
89 y_tick_labels: area_prelim_labels,
90 tick_value_metrics: TextMetrics::from_theme_tick_value(&config.theme),
91 axis_label_metrics: TextMetrics::from_theme_axis_label(&config.theme),
92 ..Default::default()
93 };
94 let margins = calculate_margins(&margin_config);
95
96 let inner_width = margins.inner_width(config.width);
97 let inner_height = margins.inner_height(config.height);
98
99 let band = ScaleBand::new(categories.clone(), (0.0, inner_width));
100 let bandwidth = band.bandwidth();
101
102 let mut children = Vec::new();
103
104 let area_gen = AreaGenerator::new().curve(chartml_core::shapes::CurveType::MonotoneX);
107 let mut area_elements = Vec::new();
108
109 let y_domain_min: f64;
111 let y_domain_max: f64;
112
113 if let Some(ref color_f) = color_field {
114 let series_names = data.unique_values(color_f);
115 let groups = data.group_by(color_f);
116
117 if (is_stacked || is_normalized) && series_names.len() > 1 {
118 let mut values_matrix: Vec<Vec<f64>> = Vec::new();
120 for series in &series_names {
121 let series_data = groups.get(series);
122 let mut series_vals = Vec::new();
123 for cat in &categories {
124 let val = series_data
125 .map(|sd| {
126 (0..sd.num_rows())
127 .find_map(|i| {
128 if sd.get_string(i, &category_field).as_deref() == Some(cat.as_str()) {
129 sd.get_f64(i, &value_field)
130 } else {
131 None
132 }
133 })
134 .unwrap_or(0.0)
135 })
136 .unwrap_or(0.0);
137 series_vals.push(val);
138 }
139 values_matrix.push(series_vals);
140 }
141
142 let stack = if is_normalized {
143 StackLayout::new().offset(StackOffset::Normalize)
144 } else {
145 StackLayout::new()
146 };
147 let stacked_points = stack.layout(&categories, &series_names, &values_matrix);
148
149 let (value_min, value_max, y_axis_fmt): (f64, f64, Option<&str>) = if is_normalized {
153 (0.0, 1.0, Some(".0%"))
154 } else {
155 let raw_value_max = stacked_points
156 .iter()
157 .map(|p| p.y1)
158 .fold(0.0_f64, f64::max);
159 let raw_value_max = if raw_value_max <= 0.0 { 1.0 } else { raw_value_max };
160 let (_, nice_max) = crate::helpers::nice_domain(0.0, raw_value_max, 5);
162 (0.0, nice_max, y_fmt_ref)
163 };
164 let linear = ScaleLinear::new((value_min, value_max), (inner_height, 0.0));
165 y_domain_min = value_min;
166 y_domain_max = value_max;
167
168 for (series_idx, series_name) in series_names.iter().enumerate() {
170 let mut series_points: Vec<(f64, f64, f64)> = Vec::new();
171 let mut dot_data: Vec<(String, f64, f64)> = Vec::new(); for cat in &categories {
174 let point = match stacked_points.iter().find(|p| {
175 p.key == *cat && p.series == *series_name
176 }) {
177 Some(p) => p,
178 None => continue,
179 };
180 let x = match band.map(cat) {
181 Some(x) => x + bandwidth / 2.0,
182 None => continue,
183 };
184 let y0 = linear.map(point.y0);
185 let y1 = linear.map(point.y1);
186 series_points.push((x, y0, y1));
187 let series_val = point.y1 - point.y0;
189 dot_data.push((cat.clone(), series_val, y1));
190 }
191
192 if series_points.is_empty() {
193 continue;
194 }
195
196 let path_d = area_gen.generate(&series_points);
197 let color = config
198 .colors
199 .get(series_idx)
200 .cloned()
201 .unwrap_or_else(|| "#2E7D9A".to_string());
202
203 area_elements.push(ChartElement::Path {
204 d: path_d,
205 fill: Some(color.clone()),
206 stroke: None,
207 stroke_width: None,
208 stroke_dasharray: None,
209 opacity: Some(0.6),
210 class: "chartml-area-path".to_string(),
211 data: Some(ElementData::new(series_name, "").with_series(series_name)),
212 animation_origin: None,
213 });
214
215 let line_d = area_gen.generate_line(&series_points);
217 area_elements.push(ChartElement::Path {
218 d: line_d,
219 fill: None,
220 stroke: Some(color.clone()),
221 stroke_width: Some(config.theme.series_line_weight as f64),
222 stroke_dasharray: None,
223 opacity: None,
224 class: "chartml-line-path series-line".to_string(),
225 data: Some(ElementData::new(series_name, "").with_series(series_name)),
226 animation_origin: None,
227 });
228
229 }
231
232 let bottom_axis_label = config.visualize.axes.as_ref()
234 .and_then(|a| a.x.as_ref())
235 .and_then(|a| a.label.as_deref());
236 let x_axis_result =
237 generate_x_axis(&crate::helpers::XAxisParams {
238 labels: &categories,
239 display_label_overrides: None,
240 range: (0.0, inner_width),
241 y_position: margins.top + inner_height,
242 available_width: inner_width,
243 x_format: x_format.as_deref(),
244 chart_height: Some(inner_height),
245 grid: &grid,
246 axis_label: bottom_axis_label,
247 theme: &config.theme,
248 });
249 let y_axis_elements =
250 generate_y_axis_numeric(&crate::helpers::YAxisNumericParams {
251 domain: (value_min, value_max),
252 range: (inner_height, 0.0),
253 x_position: margins.left,
254 fmt: y_axis_fmt,
255 tick_count: 5,
256 chart_width: Some(inner_width),
257 grid: &grid,
258 axis_label: left_axis_label,
259 theme: &config.theme,
260 });
261
262 children.push(ChartElement::Group {
263 class: "axes".to_string(),
264 transform: None,
265 children: {
266 let mut axes = Vec::new();
267 axes.extend(
268 x_axis_result.elements
269 .into_iter()
270 .map(|e| offset_element(e, margins.left, 0.0)),
271 );
272 axes.extend(
273 y_axis_elements
274 .into_iter()
275 .map(|e| offset_element(e, 0.0, margins.top)),
276 );
277 if let Some(zl) = emit_zero_line_if_crosses(
283 &config.theme,
284 (value_min, value_max),
285 inner_width,
286 inner_height,
287 false,
288 ) {
289 axes.push(offset_element(zl, margins.left, margins.top));
290 }
291 axes
292 },
293 });
294 } else {
295 let values: Vec<f64> = (0..data.num_rows())
297 .filter_map(|i| data.get_f64(i, &value_field))
298 .collect();
299 let raw_value_min = values.iter().cloned().fold(f64::INFINITY, f64::min);
300 let raw_value_max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
301 let raw_value_max = if raw_value_max <= 0.0 { 1.0 } else { raw_value_max };
302 let domain_min = if raw_value_min < 0.0 { raw_value_min } else { 0.0 };
304 let (value_min, value_max) = crate::helpers::nice_domain(domain_min, raw_value_max, 5);
305 let linear = ScaleLinear::new((value_min, value_max), (inner_height, 0.0));
306 y_domain_min = value_min;
307 y_domain_max = value_max;
308 let baseline = linear.map(0.0);
309
310 for (series_idx, series_name) in series_names.iter().enumerate() {
311 let series_data = match groups.get(series_name) {
312 Some(d) => d,
313 None => continue,
314 };
315
316 let mut points: Vec<(f64, f64, f64)> = Vec::new();
317 let mut dot_data: Vec<(String, f64)> = Vec::new();
318
319 for cat in &categories {
320 let row_i = match (0..series_data.num_rows()).find(|&i| {
321 series_data.get_string(i, &category_field).as_deref() == Some(cat.as_str())
322 }) {
323 Some(i) => i,
324 None => continue,
325 };
326 let val = match series_data.get_f64(row_i, &value_field) {
327 Some(v) => v,
328 None => continue,
329 };
330 let x = match band.map(cat) {
331 Some(x) => x + bandwidth / 2.0,
332 None => continue,
333 };
334 let y = linear.map(val);
335 points.push((x, baseline, y));
336 dot_data.push((cat.clone(), val));
337 }
338
339 if points.is_empty() {
340 continue;
341 }
342
343 let path_d = area_gen.generate(&points);
344 let color = config
345 .colors
346 .get(series_idx)
347 .cloned()
348 .unwrap_or_else(|| "#2E7D9A".to_string());
349
350 area_elements.push(ChartElement::Path {
351 d: path_d,
352 fill: Some(color.clone()),
353 stroke: None,
354 stroke_width: None,
355 stroke_dasharray: None,
356 opacity: Some(0.6),
357 class: "chartml-area-path".to_string(),
358 data: Some(ElementData::new(series_name, "").with_series(series_name)),
359 animation_origin: None,
360 });
361
362 let line_d = area_gen.generate_line(&points);
364 area_elements.push(ChartElement::Path {
365 d: line_d,
366 fill: None,
367 stroke: Some(color.clone()),
368 stroke_width: Some(config.theme.series_line_weight as f64),
369 stroke_dasharray: None,
370 opacity: None,
371 class: "chartml-line-path series-line".to_string(),
372 data: Some(ElementData::new(series_name, "").with_series(series_name)),
373 animation_origin: None,
374 });
375
376 }
377
378 let bottom_axis_label = config.visualize.axes.as_ref()
380 .and_then(|a| a.x.as_ref())
381 .and_then(|a| a.label.as_deref());
382 let x_axis_result =
383 generate_x_axis(&crate::helpers::XAxisParams {
384 labels: &categories,
385 display_label_overrides: None,
386 range: (0.0, inner_width),
387 y_position: margins.top + inner_height,
388 available_width: inner_width,
389 x_format: x_format.as_deref(),
390 chart_height: Some(inner_height),
391 grid: &grid,
392 axis_label: bottom_axis_label,
393 theme: &config.theme,
394 });
395 let y_axis_elements =
396 generate_y_axis_numeric(&crate::helpers::YAxisNumericParams {
397 domain: (value_min, value_max),
398 range: (inner_height, 0.0),
399 x_position: margins.left,
400 fmt: None,
401 tick_count: 5,
402 chart_width: Some(inner_width),
403 grid: &grid,
404 axis_label: left_axis_label,
405 theme: &config.theme,
406 });
407
408 children.push(ChartElement::Group {
409 class: "axes".to_string(),
410 transform: None,
411 children: {
412 let mut axes = Vec::new();
413 axes.extend(
414 x_axis_result.elements
415 .into_iter()
416 .map(|e| offset_element(e, margins.left, 0.0)),
417 );
418 axes.extend(
419 y_axis_elements
420 .into_iter()
421 .map(|e| offset_element(e, 0.0, margins.top)),
422 );
423 if let Some(zl) = emit_zero_line_if_crosses(
425 &config.theme,
426 (value_min, value_max),
427 inner_width,
428 inner_height,
429 false,
430 ) {
431 axes.push(offset_element(zl, margins.left, margins.top));
432 }
433 axes
434 },
435 });
436 }
437
438 let series_names_for_legend = data.unique_values(color_f);
440 let legend_config = LegendConfig {
441 text_metrics: TextMetrics::from_theme_legend(&config.theme),
442 ..LegendConfig::default()
443 };
444 let legend_layout = calculate_legend_layout(&series_names_for_legend, &config.colors, config.width, &legend_config);
445 let legend_y = config.height - legend_layout.total_height - 8.0;
446 let legend_elements = generate_legend(
447 &series_names_for_legend,
448 &config.colors,
449 config.width,
450 legend_y,
451 &config.theme,
452 );
453 children.push(ChartElement::Group {
454 class: "legend".to_string(),
455 transform: None,
456 children: legend_elements,
457 });
458 } else {
459 let values: Vec<f64> = (0..data.num_rows())
461 .filter_map(|i| data.get_f64(i, &value_field))
462 .collect();
463 let raw_value_min = values.iter().cloned().fold(f64::INFINITY, f64::min);
464 let raw_value_max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
465 let raw_value_max = if raw_value_max <= 0.0 { 1.0 } else { raw_value_max };
466 let domain_min = if raw_value_min < 0.0 { raw_value_min } else { 0.0 };
468 let (value_min, value_max) = crate::helpers::nice_domain(domain_min, raw_value_max, 5);
469 let linear = ScaleLinear::new((value_min, value_max), (inner_height, 0.0));
470 y_domain_min = value_min;
471 y_domain_max = value_max;
472 let baseline = linear.map(0.0);
473
474 let mut points: Vec<(f64, f64, f64)> = Vec::new();
475
476 for cat in &categories {
477 let row_i = match (0..data.num_rows()).find(|&i| {
478 data.get_string(i, &category_field).as_deref() == Some(cat.as_str())
479 }) {
480 Some(i) => i,
481 None => continue,
482 };
483 let val = match data.get_f64(row_i, &value_field) {
484 Some(v) => v,
485 None => continue,
486 };
487 let x = match band.map(cat) {
488 Some(x) => x + bandwidth / 2.0,
489 None => continue,
490 };
491 let y = linear.map(val);
492 points.push((x, baseline, y));
493 }
494
495 if !points.is_empty() {
496 let path_d = area_gen.generate(&points);
497 let color = config
498 .colors
499 .first()
500 .cloned()
501 .unwrap_or_else(|| "#2E7D9A".to_string());
502
503 area_elements.push(ChartElement::Path {
504 d: path_d,
505 fill: Some(color.clone()),
506 stroke: None,
507 stroke_width: None,
508 stroke_dasharray: None,
509 opacity: Some(0.6),
510 class: "chartml-area-path".to_string(),
511 data: None,
512 animation_origin: None,
513 });
514
515 let line_d = area_gen.generate_line(&points);
517 area_elements.push(ChartElement::Path {
518 d: line_d,
519 fill: None,
520 stroke: Some(color.clone()),
521 stroke_width: Some(config.theme.series_line_weight as f64),
522 stroke_dasharray: None,
523 opacity: None,
524 class: "chartml-line-path series-line".to_string(),
525 data: None,
526 animation_origin: None,
527 });
528
529 }
531
532 let bottom_axis_label = config.visualize.axes.as_ref()
534 .and_then(|a| a.x.as_ref())
535 .and_then(|a| a.label.as_deref());
536 let x_axis_result =
537 generate_x_axis(&crate::helpers::XAxisParams {
538 labels: &categories,
539 display_label_overrides: None,
540 range: (0.0, inner_width),
541 y_position: margins.top + inner_height,
542 available_width: inner_width,
543 x_format: x_format.as_deref(),
544 chart_height: Some(inner_height),
545 grid: &grid,
546 axis_label: bottom_axis_label,
547 theme: &config.theme,
548 });
549 let y_axis_elements =
550 generate_y_axis_numeric(&crate::helpers::YAxisNumericParams {
551 domain: (value_min, value_max),
552 range: (inner_height, 0.0),
553 x_position: margins.left,
554 fmt: None,
555 tick_count: 5,
556 chart_width: Some(inner_width),
557 grid: &grid,
558 axis_label: left_axis_label,
559 theme: &config.theme,
560 });
561
562 children.push(ChartElement::Group {
563 class: "axes".to_string(),
564 transform: None,
565 children: {
566 let mut axes = Vec::new();
567 axes.extend(
568 x_axis_result.elements
569 .into_iter()
570 .map(|e| offset_element(e, margins.left, 0.0)),
571 );
572 axes.extend(
573 y_axis_elements
574 .into_iter()
575 .map(|e| offset_element(e, 0.0, margins.top)),
576 );
577 if let Some(zl) = emit_zero_line_if_crosses(
579 &config.theme,
580 (value_min, value_max),
581 inner_width,
582 inner_height,
583 false,
584 ) {
585 axes.push(offset_element(zl, margins.left, margins.top));
586 }
587 axes
588 },
589 });
590 }
591
592 children.push(ChartElement::Group {
593 class: "areas".to_string(),
594 transform: Some(Transform::Translate(margins.left, margins.top)),
595 children: area_elements,
596 });
597
598 if let Some(annotations) = config.visualize.annotations.as_deref() {
600 if !annotations.is_empty() {
601 use chartml_core::scales::ScaleLinear;
602 let ann_scale = ScaleLinear::new((y_domain_min, y_domain_max), (inner_height, 0.0));
603 let ann_elements = generate_annotations(
604 annotations,
605 &ann_scale,
606 0.0,
607 inner_width,
608 inner_height,
609 Some(&categories),
610 &config.theme,
611 );
612 if !ann_elements.is_empty() {
613 children.push(ChartElement::Group {
614 class: "annotations".to_string(),
615 transform: Some(Transform::Translate(margins.left, margins.top)),
616 children: ann_elements,
617 });
618 }
619 }
620 }
621
622 Ok(ChartElement::Svg {
623 viewbox: ViewBox::new(0.0, 0.0, config.width, config.height),
624 width: Some(config.width),
625 height: Some(config.height),
626 class: "chartml-area".to_string(),
627 children,
628 })
629}
630