Skip to main content

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}