use crate::components::charts::area_chart::AreaChart;
use crate::components::charts::bar_chart::{BarChart, BarMode, BarOrientation};
use crate::components::charts::line_chart::LineChart;
use crate::components::charts::pie_chart::{PieChart, PieEntry};
use crate::components::charts::scatter_chart::ScatterChart;
use crate::components::svg::empty_state::{EmptyStateMessage, EmptyStateProps, ValidationWarning};
use leptos::prelude::*;
use lodviz_core::core::data::{BarDataset, Dataset};
use lodviz_core::core::encoding::Encoding;
use lodviz_core::core::mark::Mark;
use lodviz_core::core::spec::{ChartData, ChartSpec};
use lodviz_core::core::theme::ChartConfig;
use lodviz_core::core::validation::DataValidationError;
fn resolve_dataset(spec: &ChartSpec) -> Dataset {
match &spec.data {
ChartData::TimeSeries(ds) => ds.clone(),
ChartData::Table(table) => {
let Some(y_field) = &spec.y else {
return Dataset::new();
};
let enc = Encoding::new(spec.x.clone(), y_field.clone())
.with_color_opt(spec.color.clone())
.with_size_opt(spec.size.clone());
table.to_dataset(&enc)
}
ChartData::Categorical(_) => Dataset::new(),
}
}
fn resolve_bar_dataset(spec: &ChartSpec) -> BarDataset {
match &spec.data {
ChartData::Categorical(bd) => bd.clone(),
ChartData::Table(table) => {
let Some(y_field) = &spec.y else {
return BarDataset::new(vec![]);
};
let enc =
Encoding::new(spec.x.clone(), y_field.clone()).with_color_opt(spec.color.clone());
table.to_bar_dataset(&enc)
}
ChartData::TimeSeries(_) => BarDataset::new(vec![]),
}
}
fn resolve_pie_dataset(spec: &ChartSpec) -> Vec<PieEntry> {
match &spec.data {
ChartData::Table(table) => {
let Some(y_field) = &spec.y else {
return vec![];
};
let mut aggregated: std::collections::HashMap<String, f64> =
std::collections::HashMap::new();
for row in table.rows() {
let label = match row.get(&spec.x.name) {
Some(v) => v.to_string(),
None => "Unknown".to_string(),
};
let value = row
.get(&y_field.name)
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
*aggregated.entry(label).or_insert(0.0) += value;
}
let mut entries: Vec<PieEntry> = aggregated
.into_iter()
.map(|(label, value)| PieEntry { label, value })
.collect();
entries.sort_by(|a, b| {
b.value
.partial_cmp(&a.value)
.unwrap_or(std::cmp::Ordering::Equal)
});
entries
}
_ => vec![],
}
}
#[component]
pub fn SmartChart(
spec: Signal<ChartSpec>,
#[prop(optional)]
width: Option<u32>,
#[prop(optional)]
height: Option<u32>,
#[prop(optional)]
card_id: Option<String>,
#[prop(default = Signal::derive(|| ChartConfig::default()), into)]
config: Signal<ChartConfig>,
) -> impl IntoView {
use crate::components::layout::card_registry::get_card_transform_signal;
use crate::components::layout::draggable_card::CardId;
let effective_card_id = Signal::derive(move || {
if let Some(ref id) = card_id {
return Some(id.clone());
}
use_context::<CardId>().map(|cid| cid.0)
});
let resolved_config = Signal::derive(move || {
let s = spec.get();
let external = config.get();
let mut cfg = if external.title.is_some() || external.theme.is_some() {
external
} else {
s.config.clone()
};
if let Some(id) = effective_card_id.get() {
if let Some(transform) = get_card_transform_signal(id).get() {
cfg.width = Some((transform.width - 32.0).max(0.0) as u32);
cfg.height = Some((transform.height - 40.0).max(100.0) as u32);
}
}
if cfg.width.is_none() {
if let Some(w) = width {
cfg.width = Some(w);
}
}
if cfg.height.is_none() {
if let Some(h) = height {
cfg.height = Some(h);
}
}
cfg
});
let dataset = Signal::derive(move || resolve_dataset(&spec.get()));
let bar_dataset = Signal::derive(move || resolve_bar_dataset(&spec.get()));
let pie_dataset = Signal::derive(move || resolve_pie_dataset(&spec.get()));
let mark = Memo::new(move |_| spec.get().mark);
let validation_result = Signal::derive(move || {
let s = spec.get();
if let ChartData::Table(table) = &s.data {
let Some(y_field) = &s.y else {
return None;
};
let encoding = Encoding::new(s.x.clone(), y_field.clone())
.with_color_opt(s.color.clone())
.with_size_opt(s.size.clone());
Some(table.validate_for_mark(&encoding, s.mark))
} else {
None
}
});
view! {
<div style="width: 100%; height: 100%;">
<Show
when=move || {
validation_result.with(|v| { v.as_ref().map(|r| !r.is_valid).unwrap_or(false) })
}
fallback=move || {
view! {
<div style="width: 100%; height: 100%;">
{move || {
validation_result
.with(|v| {
if let Some(result) = v {
if !result.warnings.is_empty() {
return view! {
<ValidationWarning
warnings=result.warnings.clone()
theme=Signal::derive(move || {
resolved_config.get().theme.unwrap_or_default()
})
/>
}
.into_any();
}
}
().into_any()
})
}} {move || match mark.get() {
Mark::Line => {
view! { <LineChart data=dataset config=resolved_config /> }
.into_any()
}
Mark::Area => {
view! { <AreaChart data=dataset config=resolved_config /> }
.into_any()
}
Mark::Arc => {
view! { <PieChart data=pie_dataset config=resolved_config /> }
.into_any()
}
Mark::Bar => {
view! {
<BarChart
data=bar_dataset
orientation=BarOrientation::Vertical
mode=BarMode::Grouped
config=resolved_config
/>
}
.into_any()
}
Mark::Point | Mark::Circle => {
view! { <ScatterChart data=dataset config=resolved_config /> }
.into_any()
}
}}
</div>
}
.into_any()
}
>
{move || {
validation_result
.with(|v| {
if let Some(result) = v {
if let Some(error) = result.errors.first() {
let props = match error {
DataValidationError::RoleMismatch {
column,
axis,
expected_role,
..
} => {
EmptyStateProps::role_mismatch(
column.clone(),
axis.clone(),
format!("{:?}", expected_role),
)
}
DataValidationError::HighCardinality {
column,
cardinality,
threshold,
..
} => {
EmptyStateProps::high_cardinality(
column.clone(),
*cardinality,
*threshold,
format!("{:?}", mark.get()),
)
}
DataValidationError::MarkIncompatible {
mark,
axis,
required_types,
column,
..
} => {
let types_str = required_types
.iter()
.map(|t| format!("{:?}", t))
.collect::<Vec<_>>()
.join(" or ");
EmptyStateProps::mark_incompatible(
format!("{:?}", mark),
axis.clone(),
types_str,
column.clone(),
)
}
_ => EmptyStateProps::generic_error(&error.to_string()),
};
return view! {
<EmptyStateMessage
props=props
theme=Signal::derive(move || {
resolved_config.get().theme.unwrap_or_default()
})
/>
}
.into_any();
}
}
().into_any()
})
}}
</Show>
</div>
}
}