Skip to main content

orbital_charts/shared/chart_container/
mod.rs

1//! [`ChartContainer`] — fixed-size chart context provider.
2
3mod chart_root;
4mod custom_layer;
5mod keyboard_listener;
6mod overlay_layer;
7mod overlays;
8mod plot_area;
9mod resize;
10mod responsive;
11
12use std::sync::Arc;
13
14use leptos::prelude::*;
15use orbital_data::{ChartFieldBinding, Dataset};
16use orbital_macros::component_doc;
17use orbital_theme::use_theme_options;
18
19use crate::context::ChartKind;
20use crate::engine::default_plot_inset;
21#[cfg(feature = "preview")]
22use crate::preview::fixtures::{
23    cost_series, full_grid, processed_binding, processed_dataset, quarter_x_axis, revenue_series,
24    revenue_y_axis,
25};
26use crate::{
27    AxisClickData, AxisDef, AxisHighlightConfig, ChartEmbedMode, ChartFeatures, ChartItemId,
28    ChartMotion, ChartOrientation, GridConfig, HighlightScope, LegendConfig, OrbitalChartPalette,
29    OrbitalChartsTheme, OverlayMount, PlotInset, SeriesDef, TooltipConfig, ZoomWindow,
30    CHART_FEATURES_DEFAULT,
31};
32use leptos::callback::Callback;
33
34pub use chart_root::ChartRoot;
35pub use custom_layer::ChartCustomBaseline;
36pub use overlay_layer::{ChartOverlayLayer, ChartRootOverlayChrome};
37pub use plot_area::{compute_drawing_area, DrawingArea};
38pub use responsive::ResponsiveChartContainer;
39
40/// Ready-made chart layout root — bind a [`Dataset`], tune props, or compose plot children.
41///
42/// Use [`ChartContainer`] when you need explicit control over axes, series, plot inset,
43/// and child plot layers. For common chart types, prefer the `*Chart` convenience
44/// components (e.g. [`crate::BarChart`]).
45///
46/// # Chart type family
47///
48/// Pick the chart that matches the story you are telling:
49///
50/// - **Compare categories** — [`crate::BarChart`] when magnitude at a shared baseline matters.
51/// - **Show change over time** — [`crate::LineChart`] or [`crate::AreaChart`] for trends and stacked volume.
52/// - **Show parts of a whole** — [`crate::PieChart`] when proportions beat precise comparisons.
53/// - **Correlate two metrics** — [`crate::ScatterChart`] for point-by-point relationships.
54/// - **Inline trend glyph** — [`crate::Sparkline`] beside KPIs without axis chrome.
55/// - **Single-metric readout** — [`crate::Gauge`] for capacity or score at a glance.
56/// - **Intensity grid** — [`crate::Heatmap`] for two categorical dimensions plus a value.
57///
58/// # Customization layers
59///
60/// 1. **Single `*Chart` component** — bind a [`Dataset`] and optional `legend` / `tooltip` props.
61/// 2. **Configuration props** — axes, plot inset (`margin`), palette, highlight scope on the container.
62/// 3. **Composition children** — [`ChartContainer`] + explicit `BarPlot` / `LinePlot` / custom SVG layers.
63///    See `chart-composition` and `chart-stacking` previews.
64///
65/// Cross-cutting UX lives on dedicated previews: `charts-legend`, `charts-tooltip`,
66/// `charts-highlighting`, `charts-label`, `charts-styling`, `charts-axis`.
67///
68/// # When to use
69///
70/// - Mixed-type dashboards (bar + line overlay) or custom SVG annotations.
71/// - Full control over which plot children render and their z-order.
72/// - Processed [`Dataset`] output from a DataTable without a convenience `*Chart` wrapper.
73///
74/// # Usage
75///
76/// 1. Bind a shared [`Dataset`] with `binding` or field keys that match your DataTable columns.
77/// 2. Set explicit `width` and `height` for fixed tiles, or wrap with [`ResponsiveChartContainer`] for parent-fill layouts.
78/// 3. Add plot children (`BarPlot`, `LinePlot`, etc.) in composition mode; otherwise the container renders axes only.
79/// 4. Opt into `legend`, `tooltip`, and `highlight_scope` when the dashboard needs exploration — defaults are `None` for lightweight embeds.
80/// 5. Wrap previews in a native element with `data-testid` for E2E hooks.
81///
82/// # Best Practices
83///
84/// ## Do's
85///
86/// * Use the same field keys as [`orbital_data::FieldDef::key`] in your DataTable columns.
87/// * Leave `skip_animation` unset so reduced-motion preferences are honored automatically.
88/// * Set plot inset via `margin` when legends or long axis labels need extra room.
89/// * Link users to cross-cutting previews instead of repeating legend/tooltip setup on every chart type.
90/// * Follow [`ChartCompositionOrder`] for child z-order: grid → [`PlotClip`] → plots → axes → overlay chrome.
91/// * Set `embed_mode=ChartEmbedMode::ScrollHost` when the chart lives inside a [`ScrollArea`](orbital_core_components::ScrollArea) so tooltips escape clip.
92///
93/// ## Don'ts
94///
95/// * Do not reach for composition when a `*Chart` wrapper already covers your chart type.
96/// * Do not put `data-testid` on the chart component — wrap it in a native `div` or `span`.
97/// * Do not rely on absolute tooltip positioning in scroll hosts — use `ChartEmbedMode` instead.
98///
99/// # Examples
100///
101/// ## Basic container with axes and grid
102/// Minimal shell proving band x-axis, linear y-axis, and grid wiring. Start here before
103/// adding plot children or binding a live [`Dataset`] from a DataTable.
104/// <!-- preview -->
105/// ```rust
106/// use crate::ChartContainer;
107/// use crate::preview::fixtures::{full_grid, quarter_x_axis, revenue_series, revenue_y_axis};
108/// view! {
109///     <div data-testid="chart-container-preview" style="min-width: 560px; min-height: 320px;">
110///         <ChartContainer
111///             series=Some(vec![revenue_series()])
112///             x_axis=Some(vec![quarter_x_axis()])
113///             y_axis=Some(vec![revenue_y_axis()])
114///             grid=Some(full_grid())
115///             width=Some(520.0)
116///             height=Some(320.0)
117///         />
118///     </div>
119/// }
120/// ```
121///
122/// ## Loading overlay
123/// Sets `loading=true` while async data fetches run. The container shows [`Spinner`] and
124/// [`Skeleton`] chrome instead of an empty plot — pair with your data resource's pending state.
125/// <!-- preview -->
126/// ```rust
127/// use crate::ChartContainer;
128/// view! {
129///     <div data-testid="chart-container-loading-preview">
130///         <ChartContainer loading=Some(true) width=Some(400.0) height=Some(280.0) />
131///     </div>
132/// }
133/// ```
134///
135/// ## Custom layer via hooks
136/// Dashed baseline drawn in SVG space using [`crate::use_drawing_area`] and
137/// [`crate::use_y_scale`]. Use this pattern for annotations that must stay aligned on resize.
138/// <!-- preview -->
139/// ```rust
140/// use crate::{ChartContainer, ChartCustomBaseline};
141/// use crate::preview::fixtures::{quarter_x_axis, revenue_series, revenue_y_axis};
142/// view! {
143///     <div data-testid="chart-container-custom-layer-preview">
144///         <ChartContainer
145///             series=Some(vec![revenue_series()])
146///             x_axis=Some(vec![quarter_x_axis()])
147///             y_axis=Some(vec![revenue_y_axis()])
148///             width=Some(520.0)
149///             height=Some(320.0)
150///         >
151///             <ChartCustomBaseline />
152///         </ChartContainer>
153///     </div>
154/// }
155/// ```
156#[component_doc(
157    category = "Charts",
158    preview_slug = "chart-container",
159    preview_label = "Chart Container",
160    preview_icon = icondata::AiBorderOuterOutlined,
161)]
162#[component]
163pub fn ChartContainer(
164    /// Series definitions (explicit or derived from dataset + binding).
165    #[prop(default = None)]
166    series: Option<Vec<SeriesDef>>,
167    /// Tabular data — primary consumption mode.
168    #[prop(default = None)]
169    dataset: Option<Dataset>,
170    /// Shorthand field binding when using dataset.
171    #[prop(default = None)]
172    binding: Option<ChartFieldBinding>,
173    /// X-axis definitions.
174    #[prop(default = None)]
175    x_axis: Option<Vec<AxisDef>>,
176    /// Y-axis definitions.
177    #[prop(default = None)]
178    y_axis: Option<Vec<AxisDef>>,
179    /// Chart width in pixels.
180    #[prop(default = None)]
181    width: Option<f64>,
182    /// Chart height in pixels.
183    #[prop(default = None)]
184    height: Option<f64>,
185    /// Space between SVG border and plot. User docs: "plot inset".
186    #[prop(default = None)]
187    margin: Option<PlotInset>,
188    /// Background grid configuration.
189    #[prop(default = None)]
190    grid: Option<GridConfig>,
191    /// Whether the chart is in a loading state.
192    #[prop(default = None)]
193    loading: Option<bool>,
194    /// When true, skip all enter/update animations.
195    #[prop(default = None)]
196    skip_animation: Option<bool>,
197    /// Chart-level animation configuration.
198    #[prop(default = None)]
199    motion: Option<ChartMotion>,
200    /// Chart orientation for cartesian plots.
201    #[prop(default = ChartOrientation::Vertical)]
202    orientation: ChartOrientation,
203    /// Chart geometry family.
204    #[prop(default = ChartKind::Cartesian)]
205    chart_kind: ChartKind,
206    /// Highlight and fade scope.
207    #[prop(default = None)]
208    highlight_scope: Option<HighlightScope>,
209    /// Axis crosshair / band highlight configuration.
210    #[prop(default = None)]
211    axis_highlight: Option<AxisHighlightConfig>,
212    /// Legend configuration (`None` hides legend).
213    #[prop(default = None)]
214    legend: Option<LegendConfig>,
215    /// Tooltip configuration.
216    #[prop(default = None)]
217    tooltip: Option<TooltipConfig>,
218    /// Chart theme extension overrides.
219    #[prop(default = None)]
220    charts_theme: Option<OrbitalChartsTheme>,
221    /// Color palette override.
222    #[prop(default = None)]
223    palette: Option<OrbitalChartPalette>,
224    /// Controlled highlighted item.
225    #[prop(default = None)]
226    highlighted_item: Option<RwSignal<Option<ChartItemId>>>,
227    /// Fired when highlight changes.
228    #[prop(default = None)]
229    on_highlight_change: Option<Callback<(Option<ChartItemId>,), ()>>,
230    /// Fired when a legend entry is clicked.
231    #[prop(default = None)]
232    on_legend_click: Option<Callback<(String,), ()>>,
233    /// Fired when a bar/mark is clicked.
234    #[prop(default = None)]
235    on_item_click: Option<Callback<(ChartItemId,), ()>>,
236    /// Fired when a category axis band is clicked.
237    #[prop(default = None)]
238    on_axis_click: Option<Callback<(AxisClickData,), ()>>,
239    /// Opt-in capability flags (`ChartFeatures::ZOOM_PAN`, `ChartFeatures::ANIMATION`, `ChartFeatures::KEYBOARD_NAV`).
240    #[prop(default = CHART_FEATURES_DEFAULT)]
241    features: ChartFeatures,
242    /// Enable arrow-key navigation between marks (CH-22). Keyboard zoom (CH-24) is deferred.
243    #[prop(default = true)]
244    keyboard_navigation: bool,
245    /// When true, inferred line/area x-axes use [`crate::DomainLimit::Strict`].
246    #[prop(default = false)]
247    prefer_line_x_strict: bool,
248    /// Controlled zoom windows per axis (percent 0–100).
249    #[prop(default = None)]
250    zoom: Option<Vec<ZoomWindow>>,
251    /// Fired when the user or program changes zoom state.
252    #[prop(default = None)]
253    on_zoom_change: Option<Callback<(Vec<ZoomWindow>,), ()>>,
254    /// Custom loading overlay slot.
255    #[prop(default = None)]
256    loading_view: Option<Arc<dyn Fn() -> AnyView + Send + Sync>>,
257    /// Custom empty overlay slot.
258    #[prop(default = None)]
259    empty_view: Option<Arc<dyn Fn() -> AnyView + Send + Sync>>,
260    /// How the chart is embedded in its host (scroll, dialog, table cell).
261    #[prop(default = ChartEmbedMode::Inline)]
262    embed_mode: ChartEmbedMode,
263    /// Portal mount override for overlay chrome.
264    #[prop(default = OverlayMount::ChartLocal)]
265    overlay_mount: OverlayMount,
266    /// Optional CSS class on the root element.
267    #[prop(optional, into)]
268    class: MaybeProp<String>,
269    /// Composition children (plot, axis, legend layers).
270    #[prop(optional)]
271    children: Option<Children>,
272) -> impl IntoView {
273    let w = width.unwrap_or(520.0);
274    let h = height.unwrap_or(320.0);
275    let theme_options = use_theme_options();
276    let inset = margin.unwrap_or_else(|| default_plot_inset(theme_options.get_untracked().density));
277    let is_loading = loading.unwrap_or(false);
278    let root_class = class;
279
280    // Reference fixtures so preview doc examples compile in crate context.
281    #[cfg(feature = "preview")]
282    let _ = (
283        revenue_series,
284        cost_series,
285        quarter_x_axis,
286        revenue_y_axis,
287        full_grid,
288        processed_dataset,
289        processed_binding,
290    );
291
292    view! {
293        <ChartRoot
294            class=root_class
295            dataset=dataset
296            binding=binding
297            width=w
298            height=h
299            margin=inset
300            skip_animation=skip_animation
301            motion=motion
302            loading=is_loading
303            series=series
304            x_axis=x_axis
305            y_axis=y_axis
306            grid=grid
307            palette=palette
308            loading_view=loading_view
309            empty_view=empty_view
310            orientation=orientation
311            chart_kind=chart_kind
312            highlight_scope=highlight_scope
313            axis_highlight=axis_highlight
314            legend=legend
315            tooltip=tooltip
316            charts_theme=charts_theme
317            highlighted_item=highlighted_item
318            on_highlight_change=on_highlight_change
319            on_item_click=on_item_click
320            on_axis_click=on_axis_click
321            on_legend_click=on_legend_click
322            features=features
323            keyboard_navigation=keyboard_navigation
324            prefer_line_x_strict=prefer_line_x_strict
325            zoom=zoom
326            on_zoom_change=on_zoom_change
327            embed_mode=embed_mode
328            overlay_mount=overlay_mount
329        >
330            {children.map(|c| c())}
331        </ChartRoot>
332    }
333}