leptos_chartistry/series/line/
marker.rs1use super::UseLine;
2use crate::colours::Colour;
3use leptos::prelude::*;
4
5const WIDTH_TO_MARKER: f64 = 8.0;
7
8#[derive(Clone, Debug, PartialEq)]
10#[non_exhaustive]
11pub struct Marker {
12 pub shape: RwSignal<MarkerShape>,
14 pub colour: RwSignal<Option<Colour>>,
16 pub scale: RwSignal<f64>,
18 pub border: RwSignal<Option<Colour>>,
20 pub border_width: RwSignal<f64>,
22}
23
24#[derive(Copy, Clone, Debug, Default, PartialEq)]
26#[non_exhaustive]
27pub enum MarkerShape {
28 #[default]
30 None,
31 Circle,
33 Square,
35 Diamond,
37 Triangle,
39 Plus,
41 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 pub fn from_shape(shape: impl Into<MarkerShape>) -> Self {
66 Self {
67 shape: RwSignal::new(shape.into()),
68 ..Default::default()
69 }
70 }
71
72 pub fn with_colour(self, colour: impl Into<Option<Colour>>) -> Self {
74 self.colour.set(colour.into());
75 self
76 }
77
78 pub fn with_scale(self, scale: impl Into<f64>) -> Self {
80 self.scale.set(scale.into());
81 self
82 }
83
84 pub fn with_border(self, border: impl Into<Option<Colour>>) -> Self {
86 self.border.set(border.into());
87 self
88 }
89
90 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 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 let line_width = line.width.get();
114 let diameter = line_width * WIDTH_TO_MARKER * marker.scale.get();
115
116 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 class="_chartistry_line_markers">
146 {markers}
147 </g>
148 }
149 .into_any()
150}
151
152#[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 <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#[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, leg, to_inner,
242 to_inner, leg, -to_inner,
245 to_inner, -leg, -to_inner,
248 -to_inner, -leg, 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}