orbital_charts/charts/line_chart/root.rs
1//! [`LineChart`] root component.
2
3use leptos::callback::Callback;
4use leptos::prelude::*;
5use orbital_data::{ChartFieldBinding, Dataset};
6use orbital_macros::component_doc;
7
8use crate::shared::{ChartContainer, LinePlot};
9use crate::{
10 AxisClickData, AxisDef, AxisHighlightConfig, ChartFeatures, ChartItemId, ChartMotion,
11 GridConfig, HighlightScope, LegendConfig, OrbitalChartPalette, OrbitalChartsTheme, PlotInset,
12 SeriesDef, TooltipConfig, ZoomWindow, CHART_FEATURES_DEFAULT,
13};
14
15/// Show change over a continuous dimension — time, index, or ordered categories.
16///
17/// Use line charts when the story is about trend and rate of change, not category
18/// totals. Bind a shared [`Dataset`] via `x_field` and `y_fields`, or pass inline
19/// `series` and axis definitions for demos.
20///
21/// # When to use
22///
23/// - Revenue, throughput, or KPI trends across months or quarters.
24/// - Multiple series on one axis when metrics share the same x dimension.
25/// - Threshold annotations via composition children such as [`ReferenceLine`].
26///
27/// # Usage
28///
29/// 1. Bind a [`Dataset`] with `x_field` and `y_fields`, or pass inline `series` with aligned `x_axis` categories.
30/// 2. Set `show_markers: true` on series when points are sparse; use `connect_nulls` to bridge missing values.
31/// 3. Inferred x-axes use [`crate::DomainLimit::Strict`] by default — override via `AxisDef.domain_limit`.
32/// 4. Leave `skip_animation` unset to honor reduced-motion; path draw animation runs on enter/update otherwise.
33/// 5. Wrap the chart in a native element with `data-testid` for E2E hooks.
34///
35/// # Best Practices
36///
37/// ## Do's
38///
39/// * Prefer [`crate::AreaChart`] when filled volume or stacked composition matters more than the stroke alone.
40/// * Set `connect_nulls: true` on a series when null cells should not break the stroke.
41/// * Add [`ReferenceLine`] children for targets or limits instead of drawing ad-hoc SVG.
42///
43/// ## Don'ts
44///
45/// * Do not use lines for unordered categories — prefer [`crate::BarChart`].
46/// * Do not duplicate legend/tooltip demos here; link to `charts-legend` and `charts-tooltip`.
47/// * Do not put `data-testid` on the component itself — wrap with a native element.
48///
49/// # Related previews
50///
51/// Cross-cutting UX: `charts-legend`, `charts-tooltip`, `charts-highlighting`, `charts-label`, `charts-styling`, `charts-zoom-pan`.
52///
53/// # Examples
54///
55/// ## Two-series line chart
56/// Compare two metrics that share the same time axis. Markers are off by default; set `show_markers: true`
57/// when sparse points need visible marks. Enable `legend` and `tooltip` (see cross-cutting previews) for exploration.
58/// <!-- preview -->
59/// ```rust,ignore
60/// use crate::LineChart;
61/// use crate::preview::fixtures::{cost_series, full_grid, quarter_x_axis, revenue_series, revenue_y_axis};
62/// view! {
63/// <div data-testid="line-chart-preview">
64/// <LineChart
65/// series=vec![revenue_series(), cost_series()]
66/// x_axis=vec![quarter_x_axis()]
67/// y_axis=vec![revenue_y_axis()]
68/// grid=full_grid()
69/// width=560.0
70/// height=320.0
71/// />
72/// </div>
73/// }
74/// ```
75///
76/// ## Connect nulls across gaps
77/// Missing values (`f64::NAN` or null dataset cells) break the stroke by default. Set
78/// `connect_nulls: true` on the series to bridge gaps — useful for sparse telemetry feeds.
79/// <!-- preview -->
80/// ```rust,ignore
81/// use crate::LineChart;
82/// use crate::preview::fixtures::{full_grid, quarter_x_axis, revenue_y_axis, sparse_revenue_series};
83/// view! {
84/// <div data-testid="line-chart-connect-nulls-preview">
85/// <LineChart
86/// series=vec![sparse_revenue_series()]
87/// x_axis=vec![quarter_x_axis()]
88/// y_axis=vec![revenue_y_axis()]
89/// grid=full_grid()
90/// width=560.0
91/// height=320.0
92/// />
93/// </div>
94/// }
95/// ```
96///
97/// ## Reference line
98/// Horizontal threshold via [`ReferenceLine`] composition child. Place the line after
99/// [`LinePlot`] so it renders above the stroke; use when targets or limits must stay visible on resize.
100/// <!-- preview -->
101/// ```rust,ignore
102/// use crate::LineChart;
103/// use crate::preview::fixtures::{cost_series, full_grid, quarter_x_axis, revenue_series, revenue_y_axis};
104/// use crate::{LinePlot, ReferenceLine, ReferenceLineLabelAlign};
105/// view! {
106/// <div data-testid="line-chart-reference-preview">
107/// <LineChart
108/// series=vec![revenue_series(), cost_series()]
109/// x_axis=vec![quarter_x_axis()]
110/// y_axis=vec![revenue_y_axis()]
111/// grid=full_grid()
112/// width=560.0
113/// height=320.0
114/// >
115/// <LinePlot />
116/// <ReferenceLine y=520_000.0 label="Target" label_align=ReferenceLineLabelAlign::Middle />
117/// </LineChart>
118/// </div>
119/// }
120/// ```
121#[component_doc(
122 category = "Charts",
123 preview_slug = "line-chart",
124 preview_label = "Line Chart",
125 preview_icon = icondata::AiLineChartOutlined,
126)]
127#[component]
128pub fn LineChart(
129 /// Tabular data source.
130 #[prop(optional)]
131 dataset: Option<Dataset>,
132 /// Category field key when using dataset.
133 #[prop(optional, into)]
134 x_field: Option<String>,
135 /// Value field keys when using dataset.
136 #[prop(optional)]
137 y_fields: Option<Vec<String>>,
138 /// Explicit field binding (alternative to x_field/y_fields).
139 #[prop(optional)]
140 binding: Option<ChartFieldBinding>,
141 /// Inline or explicit series definitions.
142 #[prop(optional)]
143 series: Option<Vec<SeriesDef>>,
144 /// X-axis definitions.
145 #[prop(optional)]
146 x_axis: Option<Vec<AxisDef>>,
147 /// Y-axis definitions.
148 #[prop(optional)]
149 y_axis: Option<Vec<AxisDef>>,
150 /// Chart width in pixels.
151 #[prop(optional)]
152 width: Option<f64>,
153 /// Chart height in pixels.
154 #[prop(optional)]
155 height: Option<f64>,
156 /// Plot inset between SVG border and plot area.
157 #[prop(optional)]
158 margin: Option<PlotInset>,
159 /// Background grid configuration.
160 #[prop(optional)]
161 grid: Option<GridConfig>,
162 /// Whether the chart is loading.
163 #[prop(optional)]
164 loading: Option<bool>,
165 /// Skip enter/update animations.
166 #[prop(optional)]
167 skip_animation: Option<bool>,
168 /// Chart motion configuration.
169 #[prop(optional)]
170 motion: Option<ChartMotion>,
171 /// Highlight and fade scope.
172 #[prop(optional)]
173 highlight_scope: Option<HighlightScope>,
174 /// Axis highlight configuration.
175 #[prop(optional)]
176 axis_highlight: Option<AxisHighlightConfig>,
177 /// Legend configuration.
178 #[prop(optional)]
179 legend: Option<LegendConfig>,
180 /// Tooltip configuration.
181 #[prop(optional)]
182 tooltip: Option<TooltipConfig>,
183 /// Chart theme extension.
184 #[prop(optional)]
185 charts_theme: Option<OrbitalChartsTheme>,
186 /// Color palette override.
187 #[prop(optional)]
188 palette: Option<OrbitalChartPalette>,
189 /// Fired when a mark is clicked.
190 #[prop(optional)]
191 on_item_click: Option<Callback<(ChartItemId,), ()>>,
192 /// Fired when a category band is clicked.
193 #[prop(optional)]
194 on_axis_click: Option<Callback<(AxisClickData,), ()>>,
195 /// Fired when a legend entry is clicked.
196 #[prop(optional)]
197 on_legend_click: Option<Callback<(String,), ()>>,
198 /// Opt-in capability flags.
199 #[prop(default = CHART_FEATURES_DEFAULT)]
200 features: ChartFeatures,
201 /// Enable arrow-key navigation between marks (CH-22). Keyboard zoom (CH-24) is deferred.
202 #[prop(default = true)]
203 keyboard_navigation: bool,
204 /// Controlled zoom state.
205 #[prop(optional)]
206 zoom: Option<Vec<ZoomWindow>>,
207 /// Fired when zoom changes.
208 #[prop(optional)]
209 on_zoom_change: Option<Callback<(Vec<ZoomWindow>,), ()>>,
210 /// Optional CSS class on the root element.
211 #[prop(optional, into)]
212 class: MaybeProp<String>,
213 /// Composition children (e.g. [`LinePlot`], [`ReferenceLine`]).
214 #[prop(optional)]
215 children: Option<Children>,
216) -> impl IntoView {
217 let binding = binding.or_else(|| {
218 x_field
219 .zip(y_fields.clone())
220 .map(|(x, y)| ChartFieldBinding::new(x, y))
221 });
222 let w = width.unwrap_or(520.0);
223 let h = height.unwrap_or(320.0);
224 let grid = grid.or({
225 Some(GridConfig {
226 horizontal: true,
227 vertical: true,
228 })
229 });
230
231 view! {
232 <ChartContainer
233 class=class
234 dataset=dataset
235 binding=binding
236 series=series
237 x_axis=x_axis
238 y_axis=y_axis
239 width=Some(w)
240 height=Some(h)
241 margin=margin
242 grid=grid
243 loading=loading
244 skip_animation=skip_animation
245 motion=motion
246 highlight_scope=highlight_scope
247 axis_highlight=axis_highlight
248 legend=legend
249 tooltip=tooltip
250 charts_theme=charts_theme
251 palette=palette
252 on_item_click=on_item_click
253 on_axis_click=on_axis_click
254 on_legend_click=on_legend_click
255 features=features
256 keyboard_navigation=keyboard_navigation
257 prefer_line_x_strict=true
258 zoom=zoom
259 on_zoom_change=on_zoom_change
260 >
261 {match children {
262 Some(c) => c().into_any(),
263 None => view! { <LinePlot /> }.into_any(),
264 }}
265 </ChartContainer>
266 }
267}