Skip to main content

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}