leptos_chartistry/layout/
legend.rs

1use super::{rotated_label::Anchor, UseLayout, UseVerticalLayout};
2use crate::{
3    bounds::Bounds,
4    debug::DebugRect,
5    edge::Edge,
6    series::{Snippet, UseY},
7    state::{PreState, State},
8    Padding, Tick,
9};
10use leptos::prelude::*;
11
12/// Builds a legend for the chart [series](crate::Series). Orientated along the axis of its placed edge. Drawn in HTML.
13#[derive(Clone, Debug, PartialEq)]
14#[non_exhaustive]
15pub struct Legend {
16    /// Anchor of the legend.
17    pub anchor: RwSignal<Anchor>,
18}
19
20impl Legend {
21    pub(crate) fn new(anchor: Anchor) -> Self {
22        Self {
23            anchor: RwSignal::new(anchor),
24        }
25    }
26
27    /// Creates a new legend placed at the start of the line layout.
28    pub fn start() -> Legend {
29        Self::new(Anchor::Start)
30    }
31    /// Creates a new legend placed in the middle of the line layout.
32    pub fn middle() -> Legend {
33        Self::new(Anchor::Middle)
34    }
35    /// Creates a new legend placed at the end of the line layout.
36    pub fn end() -> Legend {
37        Self::new(Anchor::End)
38    }
39
40    pub(crate) fn width<X: Tick, Y: Tick>(state: &PreState<X, Y>) -> Signal<f64> {
41        let font_height = state.font_height;
42        let font_width = state.font_width;
43        let padding = state.padding;
44        let series = state.data.series;
45        let snippet_bounds = UseY::snippet_width(font_height, font_width);
46        Signal::derive(move || {
47            let font_width = font_width.get();
48            let max_chars = series
49                .get()
50                .into_iter()
51                .map(|line| line.name.get().len() as f64 * font_width)
52                .reduce(f64::max)
53                .unwrap_or_default();
54            snippet_bounds.get() + max_chars + padding.get().width()
55        })
56    }
57
58    pub(crate) fn fixed_height<X: Tick, Y: Tick>(&self, state: &PreState<X, Y>) -> Signal<f64> {
59        let font_height = state.font_height;
60        let padding = state.padding;
61        Signal::derive(move || font_height.get() + padding.get().height())
62    }
63
64    pub(super) fn to_horizontal_use(&self) -> UseLayout {
65        UseLayout::Legend(self.clone())
66    }
67
68    pub(super) fn to_vertical_use<X: Tick, Y: Tick>(
69        &self,
70        state: &PreState<X, Y>,
71    ) -> UseVerticalLayout {
72        UseVerticalLayout {
73            width: Self::width(state),
74            layout: UseLayout::Legend(self.clone()),
75        }
76    }
77}
78
79#[component]
80pub(crate) fn Legend<X: Tick, Y: Tick>(
81    legend: Legend,
82    #[prop(into)] edge: Signal<Edge>,
83    bounds: Memo<Bounds>,
84    state: State<X, Y>,
85) -> impl IntoView {
86    let anchor = legend.anchor;
87    let debug = state.pre.debug;
88    let font_height = state.pre.font_height;
89    let padding = state.pre.padding;
90    let series = state.pre.data.series;
91
92    // Don't apply padding on the edges of our axis i.e., maximise the space we extend over
93    let padding = Memo::new(move |_| {
94        let padding = padding.get();
95        if edge.get().is_horizontal() {
96            Padding::sides(padding.top, 0.0, padding.bottom, 0.0)
97        } else {
98            Padding::sides(0.0, padding.right, 0.0, padding.left)
99        }
100    });
101    let inner = Signal::derive(move || padding.get().apply(bounds.get()));
102
103    let html = move || {
104        let edge = edge.get();
105        let body = if edge.is_horizontal() {
106            view! {<HorizontalBody series=series state=state.clone() />}.into_any()
107        } else {
108            view! {<VerticalBody series=series state=state.clone() />}.into_any()
109        };
110        view! {
111            <div
112                style="display: flex; height: 100%; overflow: auto;"
113                style:flex-direction={if edge.is_horizontal() { "row" } else { "column" }}
114                style:justify-content=move || anchor.get().css_justify_content()>
115                <table
116                    style="border-collapse: collapse; border-spacing: 0; margin: 0;"
117                    style:font-size=move || format!("{}px", font_height.get())>
118                    <tbody>
119                        {body}
120                    </tbody>
121                </table>
122            </div>
123        }
124        .into_any()
125    };
126
127    view! {
128        <g class="_chartistry_legend">
129            <DebugRect label="Legend" debug=debug bounds=vec![bounds.into(), inner] />
130            <foreignObject
131                x=move || bounds.get().left_x()
132                y=move || bounds.get().top_y()
133                width=move || bounds.get().width()
134                height=move || bounds.get().height()
135                style="overflow: visible;">
136                {html}
137            </foreignObject>
138        </g>
139    }
140}
141
142#[component]
143fn VerticalBody<X: Tick, Y: Tick>(series: Memo<Vec<UseY>>, state: State<X, Y>) -> impl IntoView {
144    let padding = move || {
145        let p = state.pre.padding.get();
146        format!("0 {}px 0 {}px", p.right, p.left)
147    };
148    view! {
149        <For
150            each=move || series.get()
151            key=|series| series.id
152            let:series>
153            <tr>
154                <td style:padding=padding>
155                    <Snippet series=series state=state.clone() />
156                </td>
157            </tr>
158        </For>
159    }
160}
161
162#[component]
163fn HorizontalBody<X: Tick, Y: Tick>(series: Memo<Vec<UseY>>, state: State<X, Y>) -> impl IntoView {
164    let padding_left = move |i| {
165        (i != 0)
166            .then_some(state.pre.padding.get().left)
167            .map(|p| format!("{}px", p))
168            .unwrap_or_default()
169    };
170    view! {
171        <tr>
172            <For
173                each=move || series.get().into_iter().enumerate()
174                key=|(_, series)| series.id
175                let:series>
176                <td style:padding-left=move || padding_left(series.0)>
177                    <Snippet series=series.1 state=state.clone() />
178                </td>
179            </For>
180        </tr>
181    }
182}