leptos_chartistry/inner/
guide_line.rs

1use crate::{bounds::Bounds, colours::Colour, debug::DebugRect, state::State, Tick};
2use leptos::prelude::*;
3use std::str::FromStr;
4
5/// Default colour for guide lines.
6pub const GUIDE_LINE_COLOUR: Colour = Colour::from_rgb(0x9A, 0x9A, 0x9A);
7
8macro_rules! impl_guide_line {
9    ($name:ident) => {
10        /// Builds a mouse guide line. Aligned over the mouse position or nearest data.
11        #[derive(Clone, Debug, PartialEq)]
12        #[non_exhaustive]
13        pub struct $name {
14            /// Alignment of the guide line.
15            pub align: RwSignal<AlignOver>,
16            /// Width of the guide line.
17            pub width: RwSignal<f64>,
18            /// Colour of the guide line.
19            pub colour: RwSignal<Colour>,
20        }
21
22        impl $name {
23            fn new(align: AlignOver) -> Self {
24                Self {
25                    align: RwSignal::new(align.into()),
26                    width: RwSignal::new(1.0),
27                    colour: RwSignal::new(GUIDE_LINE_COLOUR),
28                }
29            }
30
31            /// Creates a new guide line aligned over the mouse position.
32            pub fn over_mouse() -> Self {
33                Self::new(AlignOver::Mouse)
34            }
35
36            /// Creates a new guide line aligned over the nearest data.
37            pub fn over_data() -> Self {
38                Self::new(AlignOver::Data)
39            }
40
41            /// Sets the colour of the guide line.
42            pub fn with_colour(self, colour: impl Into<Colour>) -> Self {
43                self.colour.set(colour.into());
44                self
45            }
46        }
47
48        impl Default for $name {
49            fn default() -> Self {
50                Self::new(AlignOver::default())
51            }
52        }
53    };
54}
55
56impl_guide_line!(XGuideLine);
57impl_guide_line!(YGuideLine);
58
59/// Align over mouse or nearest data.
60#[derive(Copy, Clone, Debug, Default, PartialEq)]
61#[non_exhaustive]
62pub enum AlignOver {
63    /// Align over the mouse position.
64    #[default]
65    Mouse,
66    /// Align over the nearest data. Creates a "snap to data" effect.
67    Data,
68}
69
70#[derive(Clone)]
71pub struct UseXGuideLine(XGuideLine);
72
73#[derive(Clone)]
74pub struct UseYGuideLine(YGuideLine);
75
76impl XGuideLine {
77    pub(crate) fn use_horizontal(self) -> UseXGuideLine {
78        UseXGuideLine(self)
79    }
80}
81
82impl YGuideLine {
83    pub(crate) fn use_vertical(self) -> UseYGuideLine {
84        UseYGuideLine(self)
85    }
86}
87
88impl std::fmt::Display for AlignOver {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            AlignOver::Mouse => write!(f, "mouse"),
92            AlignOver::Data => write!(f, "data"),
93        }
94    }
95}
96
97impl FromStr for AlignOver {
98    type Err = String;
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        match s {
101            "mouse" => Ok(AlignOver::Mouse),
102            "data" => Ok(AlignOver::Data),
103            _ => Err(format!("invalid align over: `{}`", s)),
104        }
105    }
106}
107
108#[component]
109pub(super) fn XGuideLine<X: Tick, Y: Tick>(
110    line: UseXGuideLine,
111    state: State<X, Y>,
112) -> impl IntoView {
113    let line = line.0;
114    let inner = state.layout.inner;
115    let mouse_chart = state.mouse_chart;
116
117    // Data alignment
118    let nearest_pos_x = state.pre.data.nearest_position_x(state.hover_position_x);
119    let nearest_svg_x = Memo::new(move |_| {
120        nearest_pos_x
121            .get()
122            .map(|pos_x| state.projection.get().position_to_svg(pos_x, 0.0).0)
123    });
124
125    let pos = Signal::derive(move || {
126        let (mouse_x, _) = mouse_chart.get();
127        let x = match line.align.get() {
128            AlignOver::Data => nearest_svg_x.get().unwrap_or(mouse_x),
129            AlignOver::Mouse => mouse_x,
130        };
131        let inner = inner.get();
132        Bounds::from_points(x, inner.top_y(), x, inner.bottom_y())
133    });
134
135    view! {
136        <GuideLine id="x" width=line.width colour=line.colour state=state pos=pos />
137    }
138}
139
140#[component]
141pub(super) fn YGuideLine<X: Tick, Y: Tick>(
142    line: UseYGuideLine,
143    state: State<X, Y>,
144) -> impl IntoView {
145    let line = line.0;
146    let inner = state.layout.inner;
147    let mouse_chart = state.mouse_chart;
148    // TODO align over
149    let pos = Signal::derive(move || {
150        let (_, mouse_y) = mouse_chart.get();
151        let inner = inner.get();
152        Bounds::from_points(inner.left_x(), mouse_y, inner.right_x(), mouse_y)
153    });
154    view! {
155        <GuideLine id="y" width=line.width colour=line.colour state=state pos=pos />
156    }
157}
158
159#[component]
160fn GuideLine<X: Tick, Y: Tick>(
161    id: &'static str,
162    width: RwSignal<f64>,
163    colour: RwSignal<Colour>,
164    state: State<X, Y>,
165    pos: Signal<Bounds>,
166) -> impl IntoView {
167    let debug = state.pre.debug;
168    let hover_inner = state.hover_inner;
169
170    let x1 = Memo::new(move |_| pos.get().left_x());
171    let y1 = Memo::new(move |_| pos.get().top_y());
172    let x2 = Memo::new(move |_| pos.get().right_x());
173    let y2 = Memo::new(move |_| pos.get().bottom_y());
174
175    // Don't render if any of the coordinates are NaN i.e., no data
176    let have_data = Signal::derive(move || {
177        !(x1.get().is_nan() || y1.get().is_nan() || x2.get().is_nan() || y2.get().is_nan())
178    });
179
180    view! {
181        <g
182            class=format!("_chartistry_{}_guide_line", id)
183            stroke=move || colour.get().to_string()
184            stroke-width=width>
185            <Show when=move || hover_inner.get() && have_data.get() >
186                <DebugRect label=format!("{}_guide_line", id) debug=debug />
187                <line
188                    x1=x1
189                    y1=y1
190                    x2=x2
191                    y2=y2
192                />
193            </Show>
194        </g>
195    }
196}