leptos_chartistry/series/line/
mod.rs

1mod interpolation;
2mod marker;
3pub use interpolation::{Interpolation, Step};
4pub use marker::{Marker, MarkerShape};
5
6use super::{ApplyUseSeries, IntoUseLine, SeriesAcc, UseData, UseY};
7use crate::{
8    colours::{Colour, DivergingGradient, LinearGradientSvg, SequentialGradient, BERLIN, LIPARI},
9    series::GetYValue,
10    ColourScheme, Tick,
11};
12use leptos::prelude::*;
13use std::sync::Arc;
14
15/// Suggested colour scheme for a linear gradient on a line. Uses darker colours for lower values and lighter colours for higher values. Assumes a light background.
16pub const LINEAR_GRADIENT: SequentialGradient = LIPARI;
17
18/// Suggested colour scheme for a diverging gradient on a line. Uses a blue for negative values, a dark central value and red for positive values. Assumes a light background.
19pub const DIVERGING_GRADIENT: DivergingGradient = BERLIN;
20
21/// Draws a line on the chart.
22///
23/// # Simple example
24/// With no legend names, lines can be a simple closure:
25/// ```rust
26/// # use leptos_chartistry::*;
27/// # struct MyData { x: f64, y1: f64, y2: f64 }
28/// let series = Series::new(|data: &MyData| data.x)
29///     .line(|data: &MyData| data.y1)
30///     .line(|data: &MyData| data.y2);
31/// ```
32/// See this in action with the [tick labels example](https://feral-dot-io.github.io/leptos-chartistry/examples.html#tick-labels).
33///
34/// # Example
35/// However, we can also set the name of the line which a legend can show:
36/// ```rust
37/// # use leptos_chartistry::*;
38/// # struct MyData { x: f64, y1: f64, y2: f64 }
39/// let series = Series::new(|data: &MyData| data.x)
40///     .line(Line::new(|data: &MyData| data.y1).with_name("pears"))
41///     .line(Line::new(|data: &MyData| data.y2).with_name("apples"));
42/// ```
43/// See this in action with the [legend example](https://feral-dot-io.github.io/leptos-chartistry/examples.html#legend).
44#[non_exhaustive]
45pub struct Line<T, Y> {
46    get_y: Arc<dyn GetYValue<T, Y>>,
47    /// Name of the line. Used in the legend.
48    pub name: RwSignal<String>,
49    /// Colour of the line. If not set, the next colour in the series will be used.
50    pub colour: RwSignal<Option<Colour>>,
51    /// Use a linear gradient (colour scheme) for the line. Default is `None` with fallback to the line colour.
52    pub gradient: RwSignal<Option<ColourScheme>>,
53    /// Width of the line.
54    pub width: RwSignal<f64>,
55    /// Interpolation method of the line, aka line smoothing (or not). Describes how the line is drawn between two points. Default is [Interpolation::Monotone].
56    pub interpolation: RwSignal<Interpolation>,
57    /// Marker at each point on the line.
58    pub marker: Marker,
59}
60
61#[derive(Clone, Debug, PartialEq)]
62pub struct UseLine {
63    colour: Signal<Colour>,
64    gradient: RwSignal<Option<ColourScheme>>,
65    width: RwSignal<f64>,
66    interpolation: RwSignal<Interpolation>,
67    marker: Marker,
68}
69
70impl<T, Y> Line<T, Y> {
71    /// Create a new line. The `get_y` function is used to extract the Y value from your struct.
72    ///
73    /// See the module documentation for examples.
74    pub fn new(get_y: impl Fn(&T) -> Y + Send + Sync + 'static) -> Self
75    where
76        Y: Tick,
77    {
78        Self {
79            get_y: Arc::new(get_y),
80            name: RwSignal::default(),
81            colour: RwSignal::default(),
82            gradient: RwSignal::default(),
83            width: RwSignal::new(1.0),
84            interpolation: RwSignal::default(),
85            marker: Marker::default(),
86        }
87    }
88
89    /// Set the name of the line. Used in the legend.
90    pub fn with_name(self, name: impl Into<String>) -> Self {
91        self.name.set(name.into());
92        self
93    }
94
95    /// Set the colour of the line. If not set, the next colour in the series will be used.
96    pub fn with_colour(self, colour: impl Into<Option<Colour>>) -> Self {
97        self.colour.set(colour.into());
98        self
99    }
100
101    /// Use a colour scheme for the line. Interpolated in SVG by the browser, overrides [Colour]. Default is `None` with fallback to the line colour.
102    ///
103    /// Suggested use with [LINEAR_GRADIENT] or [DIVERGING_GRADIENT] (for data with a zero value).
104    pub fn with_gradient(self, scheme: impl Into<ColourScheme>) -> Self {
105        self.gradient.set(Some(scheme.into()));
106        self
107    }
108
109    /// Set the width of the line.
110    pub fn with_width(self, width: impl Into<f64>) -> Self {
111        self.width.set(width.into());
112        self
113    }
114
115    /// Set the interpolation method of the line.
116    pub fn with_interpolation(self, interpolation: impl Into<Interpolation>) -> Self {
117        self.interpolation.set(interpolation.into());
118        self
119    }
120
121    /// Set the marker at each point on the line.
122    pub fn with_marker(mut self, marker: impl Into<Marker>) -> Self {
123        self.marker = marker.into();
124        self
125    }
126}
127
128impl<T, Y> Clone for Line<T, Y> {
129    fn clone(&self) -> Self {
130        Self {
131            get_y: self.get_y.clone(),
132            name: self.name,
133            colour: self.colour,
134            gradient: self.gradient,
135            width: self.width,
136            interpolation: self.interpolation,
137            marker: self.marker.clone(),
138        }
139    }
140}
141
142impl<T, Y: Tick, F: Fn(&T) -> Y + Send + Sync + 'static> From<F> for Line<T, Y> {
143    fn from(f: F) -> Self {
144        Self::new(f)
145    }
146}
147
148impl<T, Y: Tick, U: Fn(&T) -> Y + Send + Sync> GetYValue<T, Y> for U {
149    fn value(&self, t: &T) -> Y {
150        self(t)
151    }
152
153    fn stacked_value(&self, t: &T) -> Y {
154        self(t)
155    }
156}
157
158impl<T, Y> ApplyUseSeries<T, Y> for Line<T, Y> {
159    fn apply_use_series(self: Arc<Self>, series: &mut SeriesAcc<T, Y>) {
160        let colour = series.next_colour();
161        _ = series.push_line(colour, (*self).clone());
162    }
163}
164
165impl<T, Y> IntoUseLine<T, Y> for Line<T, Y> {
166    fn into_use_line(self, id: usize, colour: Memo<Colour>) -> (UseY, Arc<dyn GetYValue<T, Y>>) {
167        let override_colour = self.colour;
168        let colour = Signal::derive(move || override_colour.get().unwrap_or(colour.get()));
169        let line = UseY::new_line(
170            id,
171            self.name,
172            UseLine {
173                colour,
174                gradient: self.gradient,
175                width: self.width,
176                interpolation: self.interpolation,
177                marker: self.marker.clone(),
178            },
179        );
180        (line, self.get_y.clone())
181    }
182}
183
184#[component]
185pub fn RenderLine<X: Tick, Y: Tick>(
186    use_y: UseY,
187    line: UseLine,
188    data: UseData<X, Y>,
189    positions: Signal<Vec<(f64, f64)>>,
190    markers: Signal<Vec<(f64, f64)>>,
191) -> impl IntoView {
192    let path = move || positions.with(|positions| line.interpolation.get().path(positions));
193
194    // Line colour
195    let gradient_id = format!("line_{}_gradient", use_y.id);
196    let stroke = {
197        let colour = line.colour;
198        let gradient_id = gradient_id.clone();
199        Signal::derive(move || {
200            // Gradient takes precedence
201            if line.gradient.get().is_some() {
202                format!("url(#{gradient_id})")
203            } else {
204                colour.get().to_string()
205            }
206        })
207    };
208    let gradient = Signal::derive(move || {
209        line.gradient
210            .get()
211            .unwrap_or_else(|| LINEAR_GRADIENT.into())
212    });
213    let range_y = Signal::derive(move || data.range_y.read().positions());
214
215    let width = line.width;
216    view! {
217        <g
218            class="_chartistry_line"
219            stroke=stroke
220            stroke-linecap="round"
221            stroke-linejoin="bevel"
222            stroke-width=width>
223            <defs>
224                <Show when=move || line.gradient.get().is_some()>
225                    <LinearGradientSvg
226                        id=gradient_id.clone()
227                        scheme=gradient
228                        range_y=range_y />
229                </Show>
230            </defs>
231            <path d=path fill="none" />
232            <marker::LineMarkers line=line positions=markers />
233        </g>
234    }
235    .into_any()
236}