use leptos::prelude::*;
use orbital_motion::{OrbitalPresenceGroup, OrbitalPresenceGroupItem};
use crate::context::{
is_series_visible, use_chart_context, use_x_scale, use_y_scale, ChartInteractionContext,
};
use crate::engine::{
compute_grouped_bars, compute_stacked_bars, projected_for_plot_type, resolve_bar_color,
resolve_stack_config, stack_groups_from_series, stack_series, BandScale, LinearScale,
};
use crate::shared::marks::BarMark;
use crate::shared::motion::bar_entrance_motion;
use crate::{BarLabelConfig, ChartOrientation, ChartScale, ChartType, ScaleType};
#[component]
pub fn BarPlot(
#[prop(default = None)] orientation: Option<ChartOrientation>,
#[prop(default = None)] bar_label: Option<BarLabelConfig>,
#[prop(default = None)] corner_radius: Option<f64>,
) -> impl IntoView {
let ctx = use_chart_context();
let interaction = expect_context::<ChartInteractionContext>();
let orient = orientation.unwrap_or(ctx.orientation);
let skip = ctx.skip_animation;
let motion = ctx.motion.clone();
let on_item = ctx.on_item_click;
let highlight_scope = ctx.highlight_scope;
let bar_label = bar_label.or_else(|| ctx.series.first().and_then(|s| s.bar_label.clone()));
let radius = corner_radius.or_else(|| ctx.series.first().and_then(|s| s.corner_radius));
let y_domain = ctx.y_domain;
let projected = ctx.projected.clone();
let series_defs = ctx.series.clone();
let palette = ctx.palette.clone();
let x_axes = ctx.x_axes.clone();
let y_axes = ctx.y_axes.clone();
let (category_axis_id, value_axis_id) = match orient {
ChartOrientation::Vertical => ("x".to_string(), "y".to_string()),
ChartOrientation::Horizontal => ("y".to_string(), "x".to_string()),
};
let category_scale = match orient {
ChartOrientation::Vertical => use_x_scale(category_axis_id.clone()),
ChartOrientation::Horizontal => use_y_scale(category_axis_id.clone()),
};
let value_scale = match orient {
ChartOrientation::Vertical => use_y_scale(value_axis_id.clone()),
ChartOrientation::Horizontal => use_x_scale(value_axis_id.clone()),
};
let bar_gap = ctx
.x_axes
.iter()
.chain(ctx.y_axes.iter())
.find(|a| a.scale_type == ScaleType::Band)
.and_then(|a| a.bar_gap_ratio)
.unwrap_or(0.1);
let cat_scale = category_scale.clone();
let val_scale = value_scale.clone();
let projected_for_bars = projected.clone();
let series_defs_for_bars = series_defs.clone();
let bars = Memo::new(move |_| {
let Some(data) = projected_for_bars.as_ref() else {
return Vec::new();
};
let plot_data = projected_for_plot_type(data, &series_defs_for_bars, ChartType::Bar);
if plot_data.series.is_empty() {
return Vec::new();
}
let (band, linear) = extract_scales(&cat_scale, &val_scale);
let baseline = 0.0f64.min(y_domain.0);
let stack_groups = stack_groups_from_series(&series_defs_for_bars);
let has_stack = plot_data
.series
.iter()
.any(|s| stack_groups.contains_key(&s.id));
if has_stack {
let group = stack_groups
.values()
.next()
.cloned()
.unwrap_or_else(|| "stack".into());
let (offset, order) =
resolve_stack_config(&group, &series_defs_for_bars, ChartType::Bar);
let stacked = stack_series(&plot_data.series, &stack_groups, offset, order);
let mut bars = compute_stacked_bars(
orient,
&plot_data.categories,
&plot_data.series,
&stacked,
&stack_groups,
&series_defs_for_bars,
&band,
&linear,
offset,
order,
);
let unstacked: Vec<_> = plot_data
.series
.iter()
.filter(|s| !stack_groups.contains_key(&s.id))
.cloned()
.collect();
if !unstacked.is_empty() {
bars.extend(compute_grouped_bars(
orient,
&plot_data.categories,
&unstacked,
&band,
&linear,
bar_gap,
baseline,
));
}
bars
} else {
compute_grouped_bars(
orient,
&plot_data.categories,
&plot_data.series,
&band,
&linear,
bar_gap,
baseline,
)
}
});
Effect::new(move |_| {
interaction.plot_bars.set(bars.get());
});
let motion_preset = bar_entrance_motion(&motion);
let entrance = Signal::from(motion_preset);
let stagger = Signal::from(motion.stagger);
view! {
{move || {
let bar_list = bars.get();
if bar_list.is_empty() {
return ().into_any();
}
let categories = projected
.as_ref()
.map(|p| p.categories.clone())
.unwrap_or_default();
let category_axis = x_axes.first();
let value_axis = y_axes.first();
let bars_view = bar_list
.iter()
.enumerate()
.filter(|(_, bar)| is_series_visible(&bar.series_id))
.map(|(idx, bar)| {
let bar = bar.clone();
let series_idx = series_defs
.iter()
.position(|s| s.id == bar.series_id)
.unwrap_or(0);
let def = series_defs.get(series_idx).cloned().unwrap_or_default();
let scope = def.highlight_scope.or(highlight_scope);
let color = resolve_bar_color(
&bar,
series_idx,
&def,
&palette,
&categories,
category_axis,
value_axis,
);
let bar_view = view! {
<BarMark
x=bar.x
y=bar.y
width=bar.width
height=bar.height
fill=color
corner_radius=radius.unwrap_or(0.0)
orientation=orient
series_id=bar.series_id.clone()
data_index=bar.data_index
value=bar.value
bar_label=bar_label.clone()
highlight_scope=scope
on_item_click=on_item
/>
};
(idx, bar_view)
}).collect::<Vec<_>>();
if skip {
bars_view.into_iter().map(|(_, v)| v).collect_view().into_any()
} else {
view! {
<OrbitalPresenceGroup motion=entrance stagger=stagger>
{bars_view.into_iter().map(|(idx, bar_view)| {
view! {
<OrbitalPresenceGroupItem show=Signal::from(true) index=Signal::from(idx)>
{bar_view}
</OrbitalPresenceGroupItem>
}
}).collect_view()}
</OrbitalPresenceGroup>
}.into_any()
}
}}
}
}
fn extract_scales(category: &ChartScale, value: &ChartScale) -> (BandScale, LinearScale) {
let band = match category {
ChartScale::Band(b) => b.clone(),
_ => BandScale::new(vec![], (0.0, 100.0), 0.1),
};
let linear = match value {
ChartScale::Linear(l) => l.clone(),
_ => LinearScale::new((0.0, 1.0), (100.0, 0.0)),
};
(band, linear)
}