leptos_chartistry/layout/
tick_labels.rs1use super::{UseLayout, UseVerticalLayout};
2use crate::{
3 bounds::Bounds,
4 debug::DebugRect,
5 edge::Edge,
6 state::{PreState, State},
7 ticks::{
8 AlignedFloats, GeneratedTicks, HorizontalSpan, TickFormat, TickFormatFn, TickGen,
9 Timestamps, VerticalSpan,
10 },
11 Tick,
12};
13use chrono::prelude::*;
14use leptos::prelude::*;
15use std::sync::Arc;
16
17#[derive(Debug, PartialEq)]
21#[non_exhaustive]
22pub struct TickLabels<XY: Tick> {
23 pub min_chars: RwSignal<usize>,
27 pub format: RwSignal<Arc<TickFormatFn<XY>>>,
29 pub generator: RwSignal<Arc<dyn TickGen<Tick = XY> + Send + Sync>>,
31}
32
33#[derive(Clone)]
34pub struct UseTickLabels {
35 ticks: Signal<Vec<(f64, String)>>,
36}
37
38impl<XY: Tick> Clone for TickLabels<XY> {
39 fn clone(&self) -> Self {
40 Self {
41 min_chars: self.min_chars,
42 format: self.format,
43 generator: self.generator,
44 }
45 }
46}
47
48impl<XY: Tick> Default for TickLabels<XY> {
49 fn default() -> Self {
50 Self::from_generator(XY::tick_label_generator())
51 }
52}
53
54impl TickLabels<f64> {
55 pub fn aligned_floats() -> Self {
57 Self::from_generator(AlignedFloats::default())
58 }
59}
60
61impl<Tz> TickLabels<DateTime<Tz>>
62where
63 Tz: TimeZone + Send + Sync + 'static,
64 Tz::Offset: std::fmt::Display + Send + Sync,
65{
66 pub fn timestamps() -> Self {
68 Self::from_generator(Timestamps::default())
69 }
70}
71
72impl<XY: Tick> TickLabels<XY> {
73 pub fn from_generator(gen: impl TickGen<Tick = XY> + 'static) -> Self {
75 Self {
76 min_chars: RwSignal::default(),
77 format: RwSignal::new(HorizontalSpan::identity_format()),
78 generator: RwSignal::new(Arc::new(gen)),
79 }
80 }
81
82 pub fn with_min_chars(self, min_chars: usize) -> Self {
84 self.min_chars.set(min_chars);
85 self
86 }
87
88 pub fn with_format(
92 self,
93 format: impl Fn(&XY, &dyn TickFormat<Tick = XY>) -> String + Send + Sync + 'static,
94 ) -> Self {
95 self.format.set(Arc::new(format));
96 self
97 }
98
99 fn map_ticks(&self, gen: Memo<GeneratedTicks<XY>>) -> Signal<Vec<(f64, String)>> {
100 let format = self.format;
101 Signal::derive(move || {
102 let format = format.get();
103 gen.with(|GeneratedTicks { ticks, state }| {
104 ticks
105 .iter()
106 .map(|tick| (tick.position(), (format)(tick, state.as_ref())))
107 .collect()
108 })
109 })
110 }
111}
112
113impl<Gen, XY> From<Gen> for TickLabels<XY>
114where
115 Gen: TickGen<Tick = XY> + 'static,
116 XY: Tick,
117{
118 fn from(gen: Gen) -> Self {
119 Self::from_generator(gen)
120 }
121}
122
123impl<X: Tick> TickLabels<X> {
124 pub(crate) fn generate_x<Y: Tick>(
125 &self,
126 state: &PreState<X, Y>,
127 avail_width: Signal<f64>,
128 ) -> Memo<GeneratedTicks<X>> {
129 let font_width = state.font_width;
130 let padding = state.padding;
131 let range_x = state.data.range_x;
132 let TickLabels {
133 min_chars,
134 format,
135 generator,
136 } = self.clone();
137 Memo::new(move |_| {
138 range_x
139 .get()
140 .range()
141 .map(|(first, last)| {
142 let span = HorizontalSpan::new(
143 font_width.get(),
144 min_chars.get(),
145 padding.get().width(),
146 avail_width.get(),
147 format.get(),
148 );
149 generator.get().generate(first, last, &span)
150 })
151 .unwrap_or_else(GeneratedTicks::none)
152 })
153 }
154
155 pub(super) fn fixed_height<Y: Tick>(&self, state: &PreState<X, Y>) -> Signal<f64> {
156 let font_height = state.font_height;
157 let padding = state.padding;
158 Signal::derive(move || font_height.get() + padding.get().height())
159 }
160
161 pub(super) fn to_horizontal_use<Y: Tick>(
162 &self,
163 state: &PreState<X, Y>,
164 avail_width: Memo<f64>,
165 ) -> UseLayout {
166 UseLayout::TickLabels(UseTickLabels {
167 ticks: self.map_ticks(self.generate_x(state, avail_width.into())),
168 })
169 }
170}
171
172impl<Y: Tick> TickLabels<Y> {
173 pub(crate) fn generate_y<X: Tick>(
174 &self,
175 state: &PreState<X, Y>,
176 avail_height: Signal<f64>,
177 ) -> Memo<GeneratedTicks<Y>> {
178 let font_height = state.font_height;
179 let padding = state.padding;
180 let range_y = state.data.range_y;
181 let generator = self.generator;
182 Memo::new(move |_| {
183 range_y
184 .get()
185 .range()
186 .map(|(first, last)| {
187 let span = VerticalSpan::new(
188 font_height.get() + padding.get().height(),
189 avail_height.get(),
190 );
191 generator.get().generate(first, last, &span)
192 })
193 .unwrap_or_else(GeneratedTicks::none)
194 })
195 }
196
197 pub(super) fn to_vertical_use<X: Tick>(
198 &self,
199 state: &PreState<X, Y>,
200 avail_height: Memo<f64>,
201 ) -> UseVerticalLayout {
202 let ticks = self.map_ticks(self.generate_y(state, avail_height.into()));
203 UseVerticalLayout {
204 width: mk_width(self.min_chars, state, ticks),
205 layout: UseLayout::TickLabels(UseTickLabels { ticks }),
206 }
207 }
208}
209
210fn mk_width<X: Tick, Y: Tick>(
211 min_chars: RwSignal<usize>,
212 state: &PreState<X, Y>,
213 ticks: Signal<Vec<(f64, String)>>,
214) -> Signal<f64> {
215 let font_width = state.font_width;
216 let padding = state.padding;
217 Signal::derive(move || {
218 let longest_chars = ticks.with(|ticks| {
219 ticks
220 .iter()
221 .map(|(_, label)| label.len())
222 .max()
223 .unwrap_or_default()
224 .max(min_chars.get())
225 }) as f64;
226 font_width.get() * longest_chars + padding.get().width()
227 })
228}
229
230fn align_tick_labels(labels: Vec<String>) -> Vec<String> {
231 let min_label = labels
233 .iter()
234 .map(|label| label.len())
235 .max()
236 .unwrap_or_default();
237 labels
239 .into_iter()
240 .map(|mut label| {
241 let spaces = " ".repeat(min_label.saturating_sub(label.len()));
242 label.insert_str(0, &spaces);
243 label
244 })
245 .collect::<Vec<_>>()
246}
247
248#[component]
249pub(super) fn TickLabels<X: Tick, Y: Tick>(
250 ticks: UseTickLabels,
251 edge: Edge,
252 bounds: Memo<Bounds>,
253 state: State<X, Y>,
254) -> impl IntoView {
255 let ticks = move || {
256 let ticks = ticks.ticks.get();
258 let ticks = if edge.is_vertical() {
259 let (pos, labels): (Vec<f64>, Vec<String>) = ticks.into_iter().unzip();
260 let labels = align_tick_labels(labels);
261 pos.into_iter().zip(labels).collect::<Vec<_>>()
262 } else {
263 ticks
264 };
265 ticks
266 .into_iter()
267 .map(|tick| {
268 view! {
269 <TickLabel edge=edge outer=bounds state=state.clone() tick=tick />
270 }
271 })
272 .collect_view()
273 };
274 view! {
275 <g class="_chartistry_tick_labels">
276 {ticks}
277 </g>
278 }
279}
280
281#[component]
282fn TickLabel<X: Tick, Y: Tick>(
283 edge: Edge,
284 outer: Memo<Bounds>,
285 state: State<X, Y>,
286 tick: (f64, String),
287) -> impl IntoView {
288 let debug = state.pre.debug;
289 let font_height = state.pre.font_height;
290 let font_width = state.pre.font_width;
291 let padding = state.pre.padding;
292 let projection = state.projection;
293
294 let (position, label) = tick;
295 let label_len = label.len();
296 let bounds = Signal::derive(move || {
298 let padding = padding.get();
299 let width = font_width.get() * label_len as f64 + padding.width();
300 let height = font_height.get() + padding.height();
301
302 let proj = projection.get();
303 let outer = outer.get();
304 match edge {
305 Edge::Top | Edge::Bottom => {
306 let (x, _) = proj.position_to_svg(position, 0.0);
307 let x = x - width / 2.0;
308 Bounds::from_points(x, outer.top_y(), x + width, outer.bottom_y())
309 }
310
311 Edge::Left | Edge::Right => {
312 let (_, y) = proj.position_to_svg(0.0, position);
313 let y = y - height / 2.0;
314 Bounds::from_points(outer.left_x(), y, outer.right_x(), y + height)
315 }
316 }
317 });
318 let content = Memo::new(move |_| padding.get().apply(bounds.get()));
319
320 let text_position = Memo::new(move |_| {
322 let content = content.get();
323 match edge {
324 Edge::Top | Edge::Bottom => ("middle", content.centre_x()),
325
326 Edge::Left | Edge::Right => {
327 let (x, anchor) = if edge == Edge::Left {
328 (content.right_x(), "end")
329 } else {
330 (content.left_x(), "start")
331 };
332 (anchor, x)
333 }
334 }
335 });
336
337 view! {
338 <g
339 class="_chartistry_tick_label"
340 font-family="monospace">
341 <DebugRect label="tick" debug=debug bounds=vec![bounds, content.into()] />
342 <text
343 x=move || text_position.get().1
344 y=move || content.get().centre_y()
345 style="white-space: pre;"
346 font-size=move || font_height.get()
347 dominant-baseline="middle"
348 text-anchor=move || text_position.get().0>
349 {label.clone()}
350 </text>
351 </g>
352 }
353}