use crate::{bounds::Bounds, colours::Colour, debug::DebugRect, state::State, Tick};
use leptos::prelude::*;
use std::str::FromStr;
pub const GUIDE_LINE_COLOUR: Colour = Colour::from_rgb(0x9A, 0x9A, 0x9A);
macro_rules! impl_guide_line {
($name:ident) => {
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct $name {
/// Alignment of the guide line.
pub align: RwSignal<AlignOver>,
pub width: RwSignal<f64>,
pub colour: RwSignal<Colour>,
}
impl $name {
fn new(align: AlignOver) -> Self {
Self {
align: RwSignal::new(align.into()),
width: RwSignal::new(1.0),
colour: RwSignal::new(GUIDE_LINE_COLOUR),
}
}
pub fn over_mouse() -> Self {
Self::new(AlignOver::Mouse)
}
pub fn over_data() -> Self {
Self::new(AlignOver::Data)
}
pub fn with_colour(self, colour: impl Into<Colour>) -> Self {
self.colour.set(colour.into());
self
}
}
impl Default for $name {
fn default() -> Self {
Self::new(AlignOver::default())
}
}
};
}
impl_guide_line!(XGuideLine);
impl_guide_line!(YGuideLine);
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[non_exhaustive]
pub enum AlignOver {
#[default]
Mouse,
Data,
}
#[derive(Clone)]
pub struct UseXGuideLine(XGuideLine);
#[derive(Clone)]
pub struct UseYGuideLine(YGuideLine);
impl XGuideLine {
pub(crate) fn use_horizontal(self) -> UseXGuideLine {
UseXGuideLine(self)
}
}
impl YGuideLine {
pub(crate) fn use_vertical(self) -> UseYGuideLine {
UseYGuideLine(self)
}
}
impl std::fmt::Display for AlignOver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AlignOver::Mouse => write!(f, "mouse"),
AlignOver::Data => write!(f, "data"),
}
}
}
impl FromStr for AlignOver {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mouse" => Ok(AlignOver::Mouse),
"data" => Ok(AlignOver::Data),
_ => Err(format!("invalid align over: `{}`", s)),
}
}
}
#[component]
pub(super) fn XGuideLine<X: Tick, Y: Tick>(
line: UseXGuideLine,
state: State<X, Y>,
) -> impl IntoView {
let line = line.0;
let inner = state.layout.inner;
let mouse_chart = state.mouse_chart;
let nearest_pos_x = state.pre.data.nearest_position_x(state.hover_position_x);
let nearest_svg_x = Memo::new(move |_| {
nearest_pos_x
.get()
.map(|pos_x| state.projection.get().position_to_svg(pos_x, 0.0).0)
});
let pos = Signal::derive(move || {
let (mouse_x, _) = mouse_chart.get();
let x = match line.align.get() {
AlignOver::Data => nearest_svg_x.get().unwrap_or(mouse_x),
AlignOver::Mouse => mouse_x,
};
let inner = inner.get();
Bounds::from_points(x, inner.top_y(), x, inner.bottom_y())
});
view! {
<GuideLine id="x" width=line.width colour=line.colour state=state pos=pos />
}
}
#[component]
pub(super) fn YGuideLine<X: Tick, Y: Tick>(
line: UseYGuideLine,
state: State<X, Y>,
) -> impl IntoView {
let line = line.0;
let inner = state.layout.inner;
let mouse_chart = state.mouse_chart;
let pos = Signal::derive(move || {
let (_, mouse_y) = mouse_chart.get();
let inner = inner.get();
Bounds::from_points(inner.left_x(), mouse_y, inner.right_x(), mouse_y)
});
view! {
<GuideLine id="y" width=line.width colour=line.colour state=state pos=pos />
}
}
#[component]
fn GuideLine<X: Tick, Y: Tick>(
id: &'static str,
width: RwSignal<f64>,
colour: RwSignal<Colour>,
state: State<X, Y>,
pos: Signal<Bounds>,
) -> impl IntoView {
let debug = state.pre.debug;
let hover_inner = state.hover_inner;
let x1 = Memo::new(move |_| pos.get().left_x());
let y1 = Memo::new(move |_| pos.get().top_y());
let x2 = Memo::new(move |_| pos.get().right_x());
let y2 = Memo::new(move |_| pos.get().bottom_y());
let have_data = Signal::derive(move || {
!(x1.get().is_nan() || y1.get().is_nan() || x2.get().is_nan() || y2.get().is_nan())
});
view! {
<g
class=format!("_chartistry_{}_guide_line", id)
stroke=move || colour.get().to_string()
stroke-width=width>
<Show when=move || hover_inner.get() && have_data.get() >
<DebugRect label=format!("{}_guide_line", id) debug=debug />
<line
x1=x1
y1=y1
x2=x2
y2=y2
/>
</Show>
</g>
}
}