leptos_chartistry/layout/
legend.rs1use 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#[derive(Clone, Debug, PartialEq)]
14#[non_exhaustive]
15pub struct Legend {
16 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 pub fn start() -> Legend {
29 Self::new(Anchor::Start)
30 }
31 pub fn middle() -> Legend {
33 Self::new(Anchor::Middle)
34 }
35 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 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}