Skip to main content

orbital_charts/shared/layers/
reference_line.rs

1//! Reference annotation line for cartesian charts.
2
3use leptos::prelude::*;
4
5use crate::context::{use_drawing_area, use_x_scale, use_y_scale};
6use crate::{ChartScale, ReferenceLineLabelAlign, ReferenceLineStyle};
7
8/// Horizontal or vertical reference guide line with optional label.
9#[component]
10pub fn ReferenceLine(
11    /// Vertical line at this x domain value.
12    #[prop(optional)]
13    x: Option<f64>,
14    /// Horizontal line at this y domain value.
15    #[prop(optional)]
16    y: Option<f64>,
17    /// Axis id for the value (`"x"` or `"y"`).
18    #[prop(optional, into)]
19    axis_id: Option<String>,
20    /// Label text displayed near the line.
21    #[prop(optional, into)]
22    label: Option<String>,
23    /// Label placement along the line.
24    #[prop(default = ReferenceLineLabelAlign::Middle)]
25    label_align: ReferenceLineLabelAlign,
26    /// Optional line styling overrides.
27    #[prop(optional)]
28    line_style: Option<ReferenceLineStyle>,
29) -> impl IntoView {
30    let area = use_drawing_area();
31    let x_scale = use_x_scale(axis_id.clone().unwrap_or_else(|| "x".into()));
32    let y_scale = use_y_scale(axis_id.clone().unwrap_or_else(|| "y".into()));
33    let style = line_style.unwrap_or_default();
34
35    let stroke = style
36        .color
37        .unwrap_or_else(|| "var(--orb-color-accent-primary, currentColor)".into());
38    let stroke_width = style.stroke_width.unwrap_or(1.5);
39    let dash = style.dash_array.unwrap_or_else(|| "6 4".into());
40    let opacity = style.opacity.unwrap_or(0.7);
41
42    view! {
43        <g class="orb-reference-line" aria-hidden="true">
44            {x.map(|xv| {
45                let px = scale_x(&x_scale, xv);
46                view! {
47                    <line
48                        class="orb-reference-line__line orb-reference-line__line--vertical"
49                        x1=px
50                        y1=0.0
51                        x2=px
52                        y2=area.plot_height
53                        stroke=stroke.clone()
54                        stroke-width=stroke_width
55                        stroke-dasharray=dash.clone()
56                        opacity=opacity
57                    />
58                }
59            })}
60            {y.map(|yv| {
61                let py = scale_y(&y_scale, yv);
62                view! {
63                    <line
64                        class="orb-reference-line__line orb-reference-line__line--horizontal"
65                        x1=0.0
66                        y1=py
67                        x2=area.plot_width
68                        y2=py
69                        stroke=stroke.clone()
70                        stroke-width=stroke_width
71                        stroke-dasharray=dash.clone()
72                        opacity=opacity
73                    />
74                }
75            })}
76            {label.map(|text| {
77                let (lx, ly, anchor) = label_position(
78                    x,
79                    y,
80                    &x_scale,
81                    &y_scale,
82                    &area,
83                    label_align,
84                );
85                view! {
86                    <text
87                        class="orb-reference-line__label"
88                        x=lx
89                        y=ly
90                        text-anchor=anchor
91                        dominant-baseline="middle"
92                        fill=stroke.clone()
93                        font-size="12"
94                        opacity=opacity
95                    >
96                        <title>{text.clone()}</title>
97                        {text}
98                    </text>
99                }
100            })}
101        </g>
102    }
103}
104
105fn scale_x(scale: &ChartScale, value: f64) -> f64 {
106    match scale {
107        ChartScale::Band(b) => b.scale(&value.to_string()).unwrap_or(0.0),
108        ChartScale::Linear(l) => l.scale(value),
109    }
110}
111
112fn scale_y(scale: &ChartScale, value: f64) -> f64 {
113    match scale {
114        ChartScale::Linear(l) => l.scale(value),
115        ChartScale::Band(b) => b.scale(&value.to_string()).unwrap_or(0.0),
116    }
117}
118
119fn label_position(
120    x: Option<f64>,
121    y: Option<f64>,
122    x_scale: &ChartScale,
123    y_scale: &ChartScale,
124    area: &crate::DrawingArea,
125    align: ReferenceLineLabelAlign,
126) -> (f64, f64, &'static str) {
127    let anchor = match align {
128        ReferenceLineLabelAlign::Start => "start",
129        ReferenceLineLabelAlign::Middle => "middle",
130        ReferenceLineLabelAlign::End => "end",
131    };
132
133    if let Some(yv) = y {
134        let py = scale_y(y_scale, yv);
135        let lx = match align {
136            ReferenceLineLabelAlign::Start => 4.0,
137            ReferenceLineLabelAlign::Middle => area.plot_width / 2.0,
138            ReferenceLineLabelAlign::End => area.plot_width - 4.0,
139        };
140        return (lx, py - 6.0, anchor);
141    }
142
143    if let Some(xv) = x {
144        let px = scale_x(x_scale, xv);
145        let ly = match align {
146            ReferenceLineLabelAlign::Start => 12.0,
147            ReferenceLineLabelAlign::Middle => area.plot_height / 2.0,
148            ReferenceLineLabelAlign::End => area.plot_height - 4.0,
149        };
150        return (px + 4.0, ly, anchor);
151    }
152
153    (0.0, 0.0, anchor)
154}