orbital_charts/shared/chart_stacking.rs
1//! Stacking preview — stacked bars and stacked areas.
2
3use leptos::prelude::*;
4use orbital_macros::component_doc;
5
6#[cfg(feature = "preview")]
7use crate::preview::fixtures::{
8 full_grid, month_categories, revenue_y_axis, stacked_bar_series, stacked_bar_x_axis,
9};
10use crate::shared::{AreaPlot, BarPlot, ChartContainer};
11
12/// Stack series when each category total is meaningful and segments show contribution.
13///
14/// **Decision guide:** Same `stack_group` id → stacked. Negative values → `StackOffset::Diverging`
15/// (bar default). Share of whole → `StackOffset::Expand`. Unsure about layer order → try
16/// `StackOrder::Ascending`.
17///
18/// # When to use
19///
20/// Assign the same `stack_group` on multiple series to stack segments per
21/// category. Bar stacks default to [`StackOffset::Diverging`] for signed data;
22/// lines and areas default to [`StackOffset::None`].
23///
24/// # Usage
25///
26/// 1. Assign the same `stack_group` string on series that should stack per category.
27/// 2. Set `stack_offset` on any member — `Diverging` for signed bars, `Expand` for 100% stacks.
28/// 3. Set `stack_order` when layer sequence at the baseline matters.
29/// 4. Use [`ChartStacking`] preview variants to compare bar vs area stacking side by side.
30///
31/// # Best Practices
32///
33/// ## Do's
34///
35/// * Include zero in the value axis domain for diverging stacks with negatives.
36/// * Use `stack_order: Ascending` when the smallest segment should anchor the baseline.
37/// * Link to `area-chart` percent stacked example for normalized area stacks.
38///
39/// ## Don'ts
40///
41/// * Do not stack unrelated series — they must share categories and a stack group id.
42/// * Do not use expand offset when absolute totals must remain readable.
43///
44/// # Examples
45///
46/// ## Stacked bar chart (diverging default)
47/// Three stacked segments per month including signed adjustments. Bar stacks default to
48/// diverging offset so negative segments mirror below the baseline.
49/// <!-- preview -->
50/// ```rust,ignore
51/// use crate::ChartStacking;
52/// use crate::preview::fixtures::{full_grid, revenue_y_axis, stacked_bar_series, stacked_bar_x_axis};
53/// view! {
54/// <div data-testid="chart-stacking-preview">
55/// <ChartStacking
56/// variant=ChartStackingVariant::StackedBar
57/// series=stacked_bar_series()
58/// x_axis=vec![stacked_bar_x_axis()]
59/// y_axis=vec![revenue_y_axis()]
60/// grid=full_grid()
61/// width=560.0
62/// height=320.0
63/// />
64/// </div>
65/// }
66/// ```
67///
68/// ## Stacked area chart
69/// Multiple filled series sharing a stack group. Compare with `area-chart-percent-preview`
70/// when you need `stack_offset: Expand` normalization.
71/// <!-- preview -->
72/// ```rust,ignore
73/// use crate::ChartStacking;
74/// use crate::preview::fixtures::{full_grid, month_categories, quarter_x_axis, revenue_y_axis, stacked_area_series};
75/// view! {
76/// <div data-testid="chart-stacking-area-preview">
77/// <ChartStacking
78/// variant=ChartStackingVariant::StackedArea
79/// series=stacked_area_series()
80/// x_axis=vec![{
81/// let mut a = quarter_x_axis();
82/// a.data = Some(month_categories());
83/// a
84/// }]
85/// y_axis=vec![revenue_y_axis()]
86/// grid=full_grid()
87/// width=560.0
88/// height=320.0
89/// />
90/// </div>
91/// }
92/// ```
93#[component_doc(
94 category = "Charts",
95 preview_slug = "chart-stacking",
96 preview_label = "Chart Stacking",
97 preview_icon = icondata::AiBlockOutlined,
98)]
99#[component]
100pub fn ChartStacking(
101 /// Which stacking example to render.
102 #[prop(default = ChartStackingVariant::StackedBar)]
103 variant: ChartStackingVariant,
104 /// Series with shared `stack_group` identifiers.
105 #[prop(default = stacked_bar_series())]
106 series: Vec<crate::SeriesDef>,
107 /// X-axis definitions.
108 #[prop(default = vec![stacked_bar_x_axis()])]
109 x_axis: Vec<crate::AxisDef>,
110 /// Y-axis definitions.
111 #[prop(default = vec![revenue_y_axis()])]
112 y_axis: Vec<crate::AxisDef>,
113 /// Background grid configuration.
114 #[prop(default = full_grid())]
115 grid: crate::GridConfig,
116 /// Chart width in pixels.
117 #[prop(default = 560.0)]
118 width: f64,
119 /// Chart height in pixels.
120 #[prop(default = 320.0)]
121 height: f64,
122 /// Optional CSS class on the root element.
123 #[prop(optional, into)]
124 class: MaybeProp<String>,
125) -> impl IntoView {
126 let _ = (&class, month_categories);
127
128 match variant {
129 ChartStackingVariant::StackedBar => view! {
130 <ChartContainer
131 series=Some(series)
132 x_axis=Some(x_axis)
133 y_axis=Some(y_axis)
134 grid=Some(grid)
135 width=Some(width)
136 height=Some(height)
137 >
138 <BarPlot />
139 </ChartContainer>
140 },
141 ChartStackingVariant::StackedArea => view! {
142 <ChartContainer
143 series=Some(series)
144 x_axis=Some(x_axis)
145 y_axis=Some(y_axis)
146 grid=Some(grid)
147 width=Some(width)
148 height=Some(height)
149 >
150 <AreaPlot />
151 </ChartContainer>
152 },
153 }
154}
155
156/// Stacking preview variants.
157#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
158pub enum ChartStackingVariant {
159 /// Diverging stacked bars with signed values.
160 #[default]
161 StackedBar,
162 /// Stacked area fills with gradient.
163 StackedArea,
164}