use leptos::prelude::*;
use lodviz_core::core::theme::ChartTheme;
#[derive(Clone)]
pub struct EmptyStateProps {
pub title: String,
pub message: String,
pub suggestion: Option<String>,
}
impl EmptyStateProps {
pub fn no_data() -> Self {
Self {
title: "No Data Available".to_string(),
message: "The dataset is empty or contains no valid data points.".to_string(),
suggestion: Some("Please check your data source and column mappings.".to_string()),
}
}
pub fn missing_columns(columns: Vec<String>) -> Self {
Self {
title: "Missing Columns".to_string(),
message: format!("Required columns not found: {}", columns.join(", ")),
suggestion: Some("Please verify that your dataset contains these columns.".to_string()),
}
}
pub fn type_mismatch(column: String, expected: String) -> Self {
Self {
title: "Incompatible Data Type".to_string(),
message: format!(
"Column '{}' contains non-numeric data, but {} type is required.",
column, expected
),
suggestion: Some("Please select a column with numeric or date values.".to_string()),
}
}
pub fn insufficient_numeric_data(column: String, percent: f64) -> Self {
Self {
title: "Insufficient Numeric Data".to_string(),
message: format!(
"Column '{}' contains less than {}% numeric values.",
column,
percent.round()
),
suggestion: Some("Please clean your data or select a different column.".to_string()),
}
}
pub fn role_mismatch(column: String, axis: String, expected_role: String) -> Self {
let role_description = match expected_role.as_str() {
"Dimension" => "categorical (text/date)",
"Measure" => "numeric",
_ => "compatible",
};
Self {
title: "Column Incompatibility".to_string(),
message: format!(
"The {} axis expects a {} column, but '{}' has the wrong data role.",
axis, expected_role, column
),
suggestion: Some(format!(
"Try using a {} column instead, or swap the X and Y axes.",
role_description
)),
}
}
pub fn high_cardinality(
column: String,
cardinality: usize,
threshold: usize,
chart_type: String,
) -> Self {
Self {
title: "Too Many Categories".to_string(),
message: format!(
"Column '{}' has {} unique values (limit: {} for {} charts).",
column, cardinality, threshold, chart_type
),
suggestion: Some(
"Consider filtering your data, grouping smaller categories, or using a different chart type.".to_string()
),
}
}
pub fn mark_incompatible(
mark: String,
axis: String,
required_types: String,
column: String,
) -> Self {
Self {
title: format!("{} Chart Incompatibility", mark),
message: format!(
"The {} axis requires {} data, but column '{}' has an incompatible type.",
axis, required_types, column
),
suggestion: Some(format!(
"Try selecting a different chart type or changing the column mapping for the {} axis.",
axis
)),
}
}
pub fn generic_error(error: &str) -> Self {
Self {
title: "Validation Error".to_string(),
message: error.to_string(),
suggestion: Some("Please check your data and chart configuration.".to_string()),
}
}
}
#[component]
pub fn EmptyStateMessage(
props: EmptyStateProps,
#[prop(optional, into)]
theme: Signal<ChartTheme>,
) -> impl IntoView {
view! {
<div
class="empty-state-message"
style=move || {
let th = theme.get();
format!(
"display: flex; flex-direction: column; align-items: center; justify-content: center; \
height: 100%; padding: 40px; text-align: center; color: {}; font-family: {}; background-color: {};",
th.text_color,
th.font_family,
th.background_color,
)
}
>
<svg
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="opacity: 0.5; margin-bottom: 16px;"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<h3 style=move || {
let th = theme.get();
format!(
"margin: 0 0 8px 0; font-size: {}px; font-weight: {}; color: {};",
th.title_font_size,
th.title_font_weight,
th.text_color,
)
}>{props.title.clone()}</h3>
<p style=move || {
let th = theme.get();
format!(
"margin: 0 0 16px 0; font-size: {}px; color: {}; max-width: 400px; line-height: 1.5;",
th.axis_font_size,
th.text_color,
)
}>{props.message.clone()}</p>
{move || {
props
.suggestion
.clone()
.map(|suggestion| {
view! {
<p style=move || {
let th = theme.get();
format!(
"margin: 0; font-size: {}px; color: {}; opacity: 0.7; font-style: italic; max-width: 400px;",
th.font_size,
th.text_color,
)
}>{suggestion}</p>
}
})
}}
</div>
}
}
#[component]
pub fn ValidationWarning(
warnings: Vec<String>,
#[prop(optional, into)]
theme: Signal<ChartTheme>,
) -> impl IntoView {
if warnings.is_empty() {
return ().into_any();
}
view! {
<div
class="validation-warning-banner"
style=move || {
let th = theme.get();
format!(
"display: flex; align-items: flex-start; gap: 12px; padding: 12px 16px; \
margin-bottom: 16px; border-radius: 6px; \
background-color: rgba(255, 193, 7, 0.1); \
border-left: 4px solid #ffc107; \
color: {}; font-family: {}; font-size: 13px;",
th.text_color,
th.font_family,
)
}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="flex-shrink: 0; color: #ffc107; margin-top: 2px;"
>
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<div style="flex: 1;">
<div style="font-weight: 600; margin-bottom: 4px; color: #f57c00;">"Warning"</div>
<ul style="margin: 0; padding-left: 20px; list-style-type: disc;">
{warnings
.into_iter()
.map(|warning| {
view! { <li style="margin: 4px 0;">{warning}</li> }
})
.collect_view()}
</ul>
</div>
</div>
}
.into_any()
}