leptos_chartistry/overlay/
tooltip.rs1use crate::{
2 debug::DebugRect,
3 series::{Snippet, UseY},
4 state::State,
5 Tick, TickLabels, AXIS_MARKER_COLOUR,
6};
7use leptos::prelude::*;
8use std::cmp::{Ordering, Reverse};
9
10pub const TOOLTIP_CURSOR_DISTANCE: f64 = 10.0;
12
13#[derive(Clone, Debug, PartialEq)]
15#[non_exhaustive]
16pub struct Tooltip<X: Tick, Y: Tick> {
17 pub placement: RwSignal<TooltipPlacement>,
19 pub sort_by: RwSignal<TooltipSortBy>,
21 pub cursor_distance: RwSignal<f64>,
23 pub skip_missing: RwSignal<bool>,
25 pub show_x_ticks: RwSignal<bool>,
28 pub x_ticks: TickLabels<X>,
30 pub y_ticks: TickLabels<Y>,
32}
33
34#[derive(Copy, Clone, Debug, Default, PartialEq)]
36#[non_exhaustive]
37pub enum TooltipPlacement {
38 #[default]
40 Hide,
41 LeftCursor,
43}
44
45#[derive(Copy, Clone, Debug, Default, PartialEq)]
47#[non_exhaustive]
48pub enum TooltipSortBy {
49 #[default]
51 Lines,
52 Ascending,
54 Descending,
56}
57
58impl<X: Tick, Y: Tick> Tooltip<X, Y> {
59 pub fn new(
61 placement: impl Into<TooltipPlacement>,
62 x_ticks: impl Into<TickLabels<X>>,
63 y_ticks: impl Into<TickLabels<Y>>,
64 ) -> Self {
65 Self {
66 placement: RwSignal::new(placement.into()),
67 x_ticks: x_ticks.into(),
68 y_ticks: y_ticks.into(),
69 ..Default::default()
70 }
71 }
72
73 pub fn from_placement(placement: impl Into<TooltipPlacement>) -> Self {
75 Self::new(
76 placement,
77 TickLabels::from_generator(X::tooltip_generator()),
78 TickLabels::from_generator(Y::tooltip_generator()),
79 )
80 }
81
82 pub fn left_cursor() -> Self {
84 Self::from_placement(TooltipPlacement::LeftCursor)
85 }
86
87 pub fn with_sort_by(self, sort_by: impl Into<TooltipSortBy>) -> Self {
89 self.sort_by.set(sort_by.into());
90 self
91 }
92
93 pub fn with_cursor_distance(self, distance: impl Into<f64>) -> Self {
95 self.cursor_distance.set(distance.into());
96 self
97 }
98
99 pub fn skip_missing(self, skip_missing: impl Into<bool>) -> Self {
101 self.skip_missing.set(skip_missing.into());
102 self
103 }
104
105 pub fn show_x_ticks(self, show_x_ticks: impl Into<bool>) -> Self {
107 self.show_x_ticks.set(show_x_ticks.into());
108 self
109 }
110}
111
112impl<X: Tick, Y: Tick> Default for Tooltip<X, Y> {
113 fn default() -> Self {
114 Self {
115 placement: RwSignal::default(),
116 sort_by: RwSignal::default(),
117 cursor_distance: RwSignal::new(TOOLTIP_CURSOR_DISTANCE),
118 skip_missing: RwSignal::new(false),
119 show_x_ticks: RwSignal::new(true),
120 x_ticks: TickLabels::default(),
121 y_ticks: TickLabels::default(),
122 }
123 }
124}
125
126impl TooltipSortBy {
127 fn to_ord<Y: Tick>(y: &Option<Y>) -> Option<F64Ord> {
128 y.as_ref().map(|y| F64Ord(y.position()))
129 }
130
131 fn sort_values<Y: Tick>(&self, values: &mut [(UseY, Option<Y>)]) {
132 match self {
133 TooltipSortBy::Lines => values.sort_by_key(|(line, _)| line.name.get()),
134 TooltipSortBy::Ascending => values.sort_by_key(|(_, y)| Self::to_ord(y)),
135 TooltipSortBy::Descending => values.sort_by_key(|(_, y)| Reverse(Self::to_ord(y))),
136 }
137 }
138}
139
140#[derive(Copy, Clone, PartialEq)]
141struct F64Ord(f64);
142
143impl PartialOrd for F64Ord {
144 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
145 Some(self.cmp(other))
146 }
147}
148
149impl Ord for F64Ord {
150 fn cmp(&self, other: &Self) -> Ordering {
151 self.0.total_cmp(&other.0)
152 }
153}
154
155impl Eq for F64Ord {}
156
157impl std::fmt::Display for TooltipPlacement {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 TooltipPlacement::Hide => write!(f, "Hide"),
161 TooltipPlacement::LeftCursor => write!(f, "Left cursor"),
162 }
163 }
164}
165
166impl std::str::FromStr for TooltipPlacement {
167 type Err = String;
168
169 fn from_str(s: &str) -> Result<Self, Self::Err> {
170 match s.to_lowercase().as_str() {
171 "hide" => Ok(TooltipPlacement::Hide),
172 "left cursor" => Ok(TooltipPlacement::LeftCursor),
173 _ => Err(format!("invalid TooltipPlacement: `{}`", s)),
174 }
175 }
176}
177
178impl std::fmt::Display for TooltipSortBy {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 match self {
181 TooltipSortBy::Lines => write!(f, "Lines"),
182 TooltipSortBy::Ascending => write!(f, "Ascending"),
183 TooltipSortBy::Descending => write!(f, "Descending"),
184 }
185 }
186}
187
188impl std::str::FromStr for TooltipSortBy {
189 type Err = String;
190
191 fn from_str(s: &str) -> Result<Self, Self::Err> {
192 match s.to_lowercase().as_str() {
193 "lines" => Ok(TooltipSortBy::Lines),
194 "ascending" => Ok(TooltipSortBy::Ascending),
195 "descending" => Ok(TooltipSortBy::Descending),
196 _ => Err(format!("invalid SortBy: `{}`", s)),
197 }
198 }
199}
200
201#[component]
202pub(crate) fn Tooltip<X: Tick, Y: Tick>(
203 tooltip: Tooltip<X, Y>,
204 state: State<X, Y>,
205) -> impl IntoView {
206 let Tooltip {
207 placement,
208 sort_by,
209 skip_missing,
210 cursor_distance,
211 show_x_ticks,
212 x_ticks,
213 y_ticks,
214 } = tooltip;
215 let debug = state.pre.debug;
216 let font_height = state.pre.font_height;
217 let font_width = state.pre.font_width;
218 let padding = state.pre.padding;
219 let inner = state.layout.inner;
220
221 let x_body = {
222 let nearest_data_x = state.pre.data.nearest_data_x(state.hover_position_x);
223 let x_format = x_ticks.format;
224 let avail_width = Signal::derive(move || inner.read().width());
225 let x_ticks = x_ticks.generate_x(&state.pre, avail_width);
226 move || {
227 if !show_x_ticks.get() {
229 return "".to_string();
230 }
231 let x_format = x_format.get();
232 nearest_data_x.read().as_ref().map_or_else(
233 || "no data".to_string(),
234 |x_value| (x_format)(x_value, x_ticks.read().state.as_ref()),
235 )
236 }
237 };
238
239 let format_y_value = {
240 let avail_height = Signal::derive(move || inner.read().height());
241 let y_format = y_ticks.format;
242 let y_ticks = y_ticks.generate_y(&state.pre, avail_height);
243 move |y_value: Option<Y>| {
244 let y_format = y_format.get();
245 y_value.as_ref().map_or_else(
246 || "-".to_string(),
247 |y_value| (y_format)(y_value, y_ticks.read().state.as_ref()),
248 )
249 }
250 };
251
252 let nearest_y_values = {
253 let nearest_data_y = state.pre.data.nearest_data_y(state.hover_position_x);
254 Memo::new(move |_| {
255 let mut y_values = nearest_data_y.get();
256 if skip_missing.get() {
258 y_values = y_values
259 .into_iter()
260 .filter(|(_, y_value)| y_value.is_some())
261 .collect::<Vec<_>>()
262 }
263 sort_by.get().sort_values(&mut y_values);
265 y_values
266 })
267 };
268
269 let nearest_data_y = move || {
270 nearest_y_values
271 .get()
272 .into_iter()
273 .map(|(line, y_value)| {
274 let y_value = format_y_value(y_value);
275 (line, y_value)
276 })
277 .collect::<Vec<_>>()
278 };
279
280 let series_tr = {
281 let state = state.clone();
282 move |(series, y_value): (UseY, String)| {
283 view! {
284 <tr>
285 <td><Snippet series=series state=state.clone() /></td>
286 <td
287 style="white-space: pre; font-family: monospace; text-align: right;"
288 style:padding-top=move || format!("{}px", font_height.get() / 4.0)
289 style:padding-left=move || format!("{}px", font_width.get())>
290 {y_value}
291 </td>
292 </tr>
293 }
294 .into_any()
295 }
296 };
297
298 view! {
299 <Show when=move || state.hover_inner.get() && placement.get() != TooltipPlacement::Hide>
300 <DebugRect label="tooltip" debug=debug />
301 <aside
302 class="_chartistry_tooltip"
303 style="position: absolute; z-index: 1; width: max-content; height: max-content; transform: translateY(-50%); background-color: #fff; white-space: pre; font-family: monospace;"
304 style:border=format!("1px solid {}", AXIS_MARKER_COLOUR)
305 style:top=move || format!("calc({}px)", state.mouse_page.get().1)
306 style:right=move || format!("calc(100% - {}px + {}px)", state.mouse_page.get().0, cursor_distance.get())
307 style:padding=move || padding.get().to_css_style()>
308 <h2
309 style="margin: 0; text-align: center;"
310 style:font-size=move || format!("{}px", font_height.get())>
311 {x_body}
312 </h2>
313 <table
314 style="border-collapse: collapse; border-spacing: 0; margin: 0 0 0 auto; padding: 0;"
315 style:font-size=move || format!("{}px", font_height.get())>
316 <tbody>
317 <For
318 each=nearest_data_y
319 key=|(series, y_value)| (series.id, y_value.to_owned())
320 children=series_tr.clone()
321 />
322 </tbody>
323 </table>
324 </aside>
325 </Show>
326 }.into_any()
327}