use std::collections::BTreeSet;
use crate::ast::node::ChartNode;
use crate::diagnostics::Diagnostic;
use super::shared::{
AnchorParentCtx, AnchorProps, TokenEnv, VisualProps, check_anchor, check_optional_dim,
check_style_ref, check_visual_props,
};
use super::suggest::check_unknown_props;
use crate::validate::check::nodes::WalkCtx;
use crate::validate::check::register_id;
pub(in crate::validate::check) fn check_chart(
c: &ChartNode,
ctx: WalkCtx,
seen_ids: &mut BTreeSet<String>,
referenced_token_ids: &mut BTreeSet<String>,
geom_required: bool,
parent_ctx: AnchorParentCtx,
diagnostics: &mut Vec<Diagnostic>,
) {
let WalkCtx {
resolved_tokens,
declared_style_ids,
zone_ids,
..
} = ctx;
register_id(&c.id, seen_ids, diagnostics);
check_style_ref(
&c.id,
c.style.as_deref(),
declared_style_ids,
c.source_span,
diagnostics,
);
let anchor_active = check_anchor(
&c.id,
AnchorProps {
anchor: c.anchor.as_deref(),
anchor_zone: c.anchor_zone.as_deref(),
anchor_sibling: c.anchor_sibling.as_deref(),
anchor_parent: c.anchor_parent == Some(true),
anchor_edge: c.anchor_edge.as_deref(),
anchor_gap: c.anchor_gap.as_ref(),
},
parent_ctx,
zone_ids,
c.source_span,
diagnostics,
);
let xy_required = geom_required && !anchor_active;
{
let mut tokens = TokenEnv {
referenced: referenced_token_ids,
resolved: resolved_tokens,
};
check_optional_dim(
&c.id,
"x",
c.x.as_ref(),
xy_required,
c.source_span,
&mut tokens,
diagnostics,
);
check_optional_dim(
&c.id,
"y",
c.y.as_ref(),
xy_required,
c.source_span,
&mut tokens,
diagnostics,
);
check_optional_dim(
&c.id,
"w",
c.w.as_ref(),
geom_required,
c.source_span,
&mut tokens,
diagnostics,
);
check_optional_dim(
&c.id,
"h",
c.h.as_ref(),
geom_required,
c.source_span,
&mut tokens,
diagnostics,
);
}
let props = VisualProps {
fill: c.fill.as_ref(),
stroke: c.stroke.as_ref(),
stroke_width: c.stroke_width.as_ref(),
stroke_dash: c.stroke_dash.as_ref(),
stroke_gap: c.stroke_gap.as_ref(),
stroke_linecap: c.stroke_linecap.as_deref(),
border_top: c.border_top.as_ref(),
border_bottom: c.border_bottom.as_ref(),
border_left: c.border_left.as_ref(),
border_right: c.border_right.as_ref(),
stroke_outer: c.stroke_outer.as_ref(),
border_width: c.border_width.as_ref(),
stroke_outer_width: c.stroke_outer_width.as_ref(),
blend_mode: c.blend_mode.as_deref(),
radius: c.radius.as_ref(),
radius_tl: c.radius_tl.as_ref(),
radius_tr: c.radius_tr.as_ref(),
radius_br: c.radius_br.as_ref(),
radius_bl: c.radius_bl.as_ref(),
shadow: c.shadow.as_ref(),
filter: c.filter.as_ref(),
mask: c.mask.as_ref(),
blur: c.blur.as_ref(),
};
check_visual_props(
"chart",
&c.id,
c.source_span,
props,
referenced_token_ids,
resolved_tokens,
diagnostics,
);
let kind_known = matches!(
c.kind.as_str(),
"bar" | "line" | "area" | "sparkline" | "pie" | "donut"
);
if !kind_known {
diagnostics.push(Diagnostic::error(
"chart.invalid_kind",
format!(
"chart '{}': kind '{}' is not recognized; \
expected \"bar\", \"line\", \"area\", \"sparkline\", \"pie\", or \"donut\"",
c.id, c.kind
),
c.source_span,
Some(c.id.clone()),
));
}
if let Some(bar_mode) = &c.bar_mode {
let bar_mode_known = matches!(bar_mode.as_str(), "grouped" | "stacked");
if !bar_mode_known {
diagnostics.push(Diagnostic::warning(
"chart.invalid_bar_mode",
format!(
"chart '{}': bar-mode '{}' is not recognized; \
expected \"grouped\" or \"stacked\"",
c.id, bar_mode
),
c.source_span,
Some(c.id.clone()),
));
}
}
if let Some(orientation) = &c.orientation {
let orientation_known = matches!(orientation.as_str(), "vertical" | "horizontal");
if !orientation_known {
diagnostics.push(Diagnostic::warning(
"chart.invalid_orientation",
format!(
"chart '{}': orientation '{}' is not recognized; \
expected \"vertical\" or \"horizontal\"",
c.id, orientation
),
c.source_span,
Some(c.id.clone()),
));
}
}
if let Some(point_placement) = &c.point_placement {
let point_placement_known = matches!(point_placement.as_str(), "edge" | "center");
if !point_placement_known {
diagnostics.push(Diagnostic::warning(
"chart.invalid_point_placement",
format!(
"chart '{}': point-placement '{}' is not recognized; \
expected \"edge\" or \"center\"",
c.id, point_placement
),
c.source_span,
Some(c.id.clone()),
));
}
}
if let Some(value_labels) = &c.value_labels {
let value_labels_known =
matches!(value_labels.as_str(), "auto" | "none" | "top" | "center");
if !value_labels_known {
diagnostics.push(Diagnostic::warning(
"chart.invalid_value_labels",
format!(
"chart '{}': value-labels '{}' is not recognized; \
expected \"auto\", \"none\", \"top\", or \"center\"",
c.id, value_labels
),
c.source_span,
Some(c.id.clone()),
));
}
}
if let Some(legend_position) = &c.legend_position {
let legend_position_known = matches!(
legend_position.as_str(),
"right" | "left" | "top" | "bottom"
);
if !legend_position_known {
diagnostics.push(Diagnostic::warning(
"chart.invalid_legend_position",
format!(
"chart '{}': legend-position '{}' is not recognized; \
expected \"right\", \"left\", \"top\", or \"bottom\"",
c.id, legend_position
),
c.source_span,
Some(c.id.clone()),
));
}
}
if let Some(legend_layout) = &c.legend_layout {
let legend_layout_known = matches!(legend_layout.as_str(), "wrapped" | "list");
if !legend_layout_known {
diagnostics.push(Diagnostic::warning(
"chart.invalid_legend_layout",
format!(
"chart '{}': legend-layout '{}' is not recognized; \
expected \"wrapped\" or \"list\"",
c.id, legend_layout
),
c.source_span,
Some(c.id.clone()),
));
}
}
if let Some(legend_align) = &c.legend_align {
let legend_align_known = matches!(legend_align.as_str(), "center" | "left" | "right");
if !legend_align_known {
diagnostics.push(Diagnostic::warning(
"chart.invalid_legend_align",
format!(
"chart '{}': legend-align '{}' is not recognized; \
expected \"center\", \"left\", or \"right\"",
c.id, legend_align
),
c.source_span,
Some(c.id.clone()),
));
}
}
if !c.categories.is_empty() {
let max_series_len = c.series.iter().map(|s| s.values.len()).max().unwrap_or(0);
if c.categories.len() != max_series_len {
diagnostics.push(Diagnostic::advisory(
"chart.category_count_mismatch",
format!(
"chart '{}': {} category labels but {} data points per series",
c.id,
c.categories.len(),
max_series_len,
),
c.source_span,
Some(c.id.clone()),
));
}
}
for s in &c.series {
if let Some(crate::ast::value::PropertyValue::TokenRef(token_id)) = &s.color {
referenced_token_ids.insert(token_id.clone());
}
if let Some(crate::ast::value::PropertyValue::TokenRef(token_id)) = &s.label_color {
referenced_token_ids.insert(token_id.clone());
}
}
if let Some(crate::ast::value::PropertyValue::TokenRef(token_id)) = &c.value_color {
referenced_token_ids.insert(token_id.clone());
}
for pv in &c.label_colors {
if let crate::ast::value::PropertyValue::TokenRef(token_id) = pv {
referenced_token_ids.insert(token_id.clone());
}
}
for pv in &c.slice_colors {
if let crate::ast::value::PropertyValue::TokenRef(token_id) = pv {
referenced_token_ids.insert(token_id.clone());
}
}
check_unknown_props("chart", &c.id, &c.unknown_props, c.source_span, diagnostics);
}