leptos_chartistry/series/line/
marker.rs

1use super::UseLine;
2use crate::colours::Colour;
3use leptos::prelude::*;
4
5// Scales our marker (drawn -1 to 1) to a 1.0 line width
6const WIDTH_TO_MARKER: f64 = 8.0;
7
8/// Describes a line point marker.
9#[derive(Clone, Debug, PartialEq)]
10#[non_exhaustive]
11pub struct Marker {
12    /// Shape of the marker. Default is no marker.
13    pub shape: RwSignal<MarkerShape>,
14    /// Colour of the marker. Default is line colour.
15    pub colour: RwSignal<Option<Colour>>,
16    /// Size relative to the line width. Default is 1.0.
17    pub scale: RwSignal<f64>,
18    /// Colour of the border. Set to the same as the background to separate the marker from the line. Default is the line colour.
19    pub border: RwSignal<Option<Colour>>,
20    /// Width of the border. Zero removes the border. Default is zero.
21    pub border_width: RwSignal<f64>,
22}
23
24/// Shape of a line marker.
25#[derive(Copy, Clone, Debug, Default, PartialEq)]
26#[non_exhaustive]
27pub enum MarkerShape {
28    /// No marker.
29    #[default]
30    None,
31    /// Circle marker.
32    Circle,
33    /// Square marker.
34    Square,
35    /// Diamond marker.
36    Diamond,
37    /// Triangle marker.
38    Triangle,
39    /// Plus marker.
40    Plus,
41    /// Cross marker.
42    Cross,
43}
44
45impl Default for Marker {
46    fn default() -> Self {
47        Self {
48            shape: RwSignal::default(),
49            colour: RwSignal::default(),
50            scale: RwSignal::new(1.0),
51            border: RwSignal::default(),
52            border_width: RwSignal::new(0.0),
53        }
54    }
55}
56
57impl From<MarkerShape> for Marker {
58    fn from(shape: MarkerShape) -> Self {
59        Self::from_shape(shape)
60    }
61}
62
63impl Marker {
64    /// Create a new marker with the given shape.
65    pub fn from_shape(shape: impl Into<MarkerShape>) -> Self {
66        Self {
67            shape: RwSignal::new(shape.into()),
68            ..Default::default()
69        }
70    }
71
72    /// Set the colour of the marker. Default is line colour.
73    pub fn with_colour(self, colour: impl Into<Option<Colour>>) -> Self {
74        self.colour.set(colour.into());
75        self
76    }
77
78    /// Set the size of the marker relative to the line width. Default is 1.0.
79    pub fn with_scale(self, scale: impl Into<f64>) -> Self {
80        self.scale.set(scale.into());
81        self
82    }
83
84    /// Set the colour of the marker border. Set to the same as the background to separate the marker from the line. Default is white.
85    pub fn with_border(self, border: impl Into<Option<Colour>>) -> Self {
86        self.border.set(border.into());
87        self
88    }
89
90    /// Set the width of the marker border. Set to zero to remove the border. Default is zero.
91    pub fn with_border_width(self, border_width: impl Into<f64>) -> Self {
92        self.border_width.set(border_width.into());
93        self
94    }
95}
96
97#[component]
98pub(super) fn LineMarkers(line: UseLine, positions: Signal<Vec<(f64, f64)>>) -> impl IntoView {
99    let marker = line.marker.clone();
100
101    // Disable border if no marker
102    let border_width = Signal::derive(move || {
103        if marker.shape.get() == MarkerShape::None {
104            0.0
105        } else {
106            marker.border_width.get()
107        }
108    });
109
110    let markers = move || {
111        let shape = marker.shape.get();
112        // Size of our marker: proportionate to our line width
113        let line_width = line.width.get();
114        let diameter = line_width * WIDTH_TO_MARKER * marker.scale.get();
115
116        // Avoid the cost of empty nodes
117        if shape == MarkerShape::None {
118            return ().into_any();
119        };
120
121        positions.with(|positions| {
122            positions
123                .iter()
124                .filter(|(x, y)| !(x.is_nan() || y.is_nan()))
125                .map(|&(x, y)| {
126                    view! {
127                        <MarkerShape
128                            shape=shape
129                            x=x
130                            y=y
131                            diameter=diameter
132                            line_width=line_width />
133                    }
134                })
135                .collect_view()
136                .into_any()
137        })
138    };
139
140    view! {
141        <g
142            fill=move || marker.colour.get().unwrap_or_else(|| line.colour.get()).to_string()
143            stroke=move || marker.border.get().unwrap_or_else(|| line.colour.get()).to_string()
144            stroke-width=move || border_width.get() * 2.0 // Half of the stroke is inside
145            class="_chartistry_line_markers">
146            {markers}
147        </g>
148    }
149    .into_any()
150}
151
152/// Renders the marker shape in a square. They should all be similar in size and not just extend to the edge e.g., square is a rotated diamond.
153#[component]
154fn MarkerShape(
155    shape: MarkerShape,
156    x: f64,
157    y: f64,
158    diameter: f64,
159    line_width: f64,
160) -> impl IntoView {
161    let radius = diameter / 2.0;
162    match shape {
163        MarkerShape::None => ().into_any(),
164
165        MarkerShape::Circle => view! {
166            // Radius to fit inside our square / diamond -- not the viewbox rect
167            <circle
168                cx=x
169                cy=y
170                r=(45.0_f64).to_radians().sin() * radius
171                paint-order="stroke fill"
172            />
173        }
174        .into_any(),
175
176        MarkerShape::Square => view! {
177            <Diamond x=x y=y radius=radius rotate=45 />
178        }
179        .into_any(),
180
181        MarkerShape::Diamond => view! {
182            <Diamond x=x y=y radius=radius />
183        }
184        .into_any(),
185
186        MarkerShape::Triangle => view! {
187            <polygon
188                points=format!("{},{} {},{} {},{}",
189                    x, y - radius,
190                    x - radius, y + radius,
191                    x + radius, y + radius)
192                paint-order="stroke fill"/>
193        }
194        .into_any(),
195
196        MarkerShape::Plus => view! {
197            <PlusPath x=x y=y diameter=diameter leg=line_width />
198        }
199        .into_any(),
200
201        MarkerShape::Cross => view! {
202            <PlusPath x=x y=y diameter=diameter leg=line_width rotate=45 />
203        }
204        .into_any(),
205    }
206}
207
208#[component]
209fn Diamond(x: f64, y: f64, radius: f64, #[prop(into, optional)] rotate: f64) -> impl IntoView {
210    view! {
211        <polygon
212            transform=format!("rotate({rotate} {x} {y})")
213            paint-order="stroke fill"
214            points=format!("{},{} {},{} {},{} {},{}",
215                x, y - radius,
216                x - radius, y,
217                x, y + radius,
218                x + radius, y) />
219    }
220}
221
222// Outline of a big plus (like the Swiss flag) up against the edge (-1 to 1)
223#[component]
224fn PlusPath(
225    x: f64,
226    y: f64,
227    diameter: f64,
228    leg: f64,
229    #[prop(into, optional)] rotate: f64,
230) -> impl IntoView {
231    let radius = diameter / 2.0;
232    let half_leg = leg / 2.0;
233    let to_inner = radius - half_leg;
234    view! {
235        <path
236            transform=format!("rotate({rotate} {x} {y})")
237            paint-order="stroke fill"
238            d=format!("M {} {} h {} v {} h {} v {} h {} v {} h {} v {} h {} v {} h {} Z",
239                x - half_leg, y - radius, // Top-most left
240                leg, // Top-most right
241                to_inner,
242                to_inner, // Right-most top
243                leg, // Right-most bottom
244                -to_inner,
245                to_inner, // Bottom-most right
246                -leg, // Bottom-most left
247                -to_inner,
248                -to_inner, // Left-most bottom
249                -leg, // Left-most top
250                to_inner) />
251    }
252}
253
254impl std::str::FromStr for MarkerShape {
255    type Err = &'static str;
256
257    fn from_str(s: &str) -> Result<Self, Self::Err> {
258        match s.to_lowercase().as_str() {
259            "none" => Ok(MarkerShape::None),
260            "circle" => Ok(MarkerShape::Circle),
261            "triangle" => Ok(MarkerShape::Triangle),
262            "square" => Ok(MarkerShape::Square),
263            "diamond" => Ok(MarkerShape::Diamond),
264            "plus" => Ok(MarkerShape::Plus),
265            "cross" => Ok(MarkerShape::Cross),
266            _ => Err("unknown marker"),
267        }
268    }
269}
270
271impl std::fmt::Display for MarkerShape {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        match self {
274            MarkerShape::None => write!(f, "None"),
275            MarkerShape::Circle => write!(f, "Circle"),
276            MarkerShape::Triangle => write!(f, "Triangle"),
277            MarkerShape::Square => write!(f, "Square"),
278            MarkerShape::Diamond => write!(f, "Diamond"),
279            MarkerShape::Plus => write!(f, "Plus"),
280            MarkerShape::Cross => write!(f, "Cross"),
281        }
282    }
283}