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}