1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
/// Tooltip overlay for waterfall charts with BandScale hit-testing
use leptos::prelude::*;
use lodviz_core::core::data::WaterfallKind;
use lodviz_core::core::scale::BandScale;
use lodviz_core::core::theme::Margin;
/// A single tooltip entry for a waterfall bar
#[derive(Clone, Debug, PartialEq)]
pub struct WaterfallTooltipEntry {
/// Category / step label
pub label: String,
/// Signed delta (original bar value)
pub value: f64,
/// Running cumulative total at this bar
pub running_total: f64,
/// The bar kind (Start, Delta, Total)
pub kind: WaterfallKind,
/// The CSS color of the bar
pub color: String,
}
/// Tooltip overlay for waterfall charts
#[component]
pub fn WaterfallTooltip(
/// Tooltip entries (one per waterfall bar, in order)
entries: Memo<Vec<WaterfallTooltipEntry>>,
/// Band scale for the X axis
band_scale: Memo<BandScale>,
/// Inner width of the chart area
inner_width: Memo<f64>,
/// Inner height of the chart area
inner_height: Memo<f64>,
/// Chart margins (to correct SVG offset coordinates)
margin: Memo<Margin>,
/// Tooltip background color
#[prop(into, optional)]
tooltip_bg: Option<Signal<String>>,
/// Tooltip text color
#[prop(into, optional)]
tooltip_text: Option<Signal<String>>,
) -> impl IntoView {
let tooltip_bg = move || {
tooltip_bg
.map(|s| s.get())
.unwrap_or_else(|| "rgba(0,0,0,0.85)".to_string())
};
let tooltip_text = move || {
tooltip_text
.map(|s| s.get())
.unwrap_or_else(|| "#ffffff".to_string())
};
let (mouse_pos, set_mouse_pos) = signal(None::<(f64, f64)>);
// Determine which bar index is hovered
let hovered_idx = Memo::new(move |_| {
let (mx, _) = mouse_pos.get()?;
let bs = band_scale.get();
let (r0, r1) = bs.range();
let step = bs.step();
if step <= 0.0 {
return None;
}
let range_min = r0.min(r1);
let range_max = r0.max(r1);
if mx < range_min || mx > range_max {
return None;
}
let idx = ((mx - range_min) / step).floor() as usize;
let n = bs.len();
if n == 0 {
return None;
}
Some(idx.min(n - 1))
});
view! {
// Transparent overlay to capture mouse events
<rect
width=move || inner_width.get()
height=move || inner_height.get()
fill="transparent"
style="pointer-events: all;"
on:mousemove=move |ev| {
let m = margin.get();
let x = ev.offset_x() as f64 - m.left;
let y = ev.offset_y() as f64 - m.top;
set_mouse_pos.set(Some((x, y)));
}
on:mouseleave=move |_| {
set_mouse_pos.set(None);
}
/>
// Tooltip rendering (pointer-events disabled so it doesn't block hover)
{move || {
let idx = hovered_idx.get()?;
let (mx, my) = mouse_pos.get()?;
let all_entries = entries.get();
let entry = all_entries.into_iter().nth(idx)?;
let bs = band_scale.get();
let w = inner_width.get();
let h = inner_height.get();
let hl_x = bs.map_index(idx);
let hl_w = bs.band_width();
let kind_label = match entry.kind {
WaterfallKind::Start => "Start",
WaterfallKind::Delta => "Δ",
WaterfallKind::Total => "Total",
};
let value_prefix = if entry.value >= 0.0 { "+" } else { "" };
let box_w = 175.0_f64;
let box_h = 70.0_f64;
let padding = 8.0_f64;
let row_h = 16.0_f64;
let box_x = if mx + box_w + 10.0 > w { mx - box_w - 10.0 } else { mx + 10.0 };
let box_y = if my + box_h + 10.0 > h { my - box_h - 10.0 } else { my + 10.0 };
Some(
// Highlight band covering full height of chart
// Kind badge text
// Value prefix sign
// Tooltip box sizing
// Auto-flip near edges
view! {
<g class="waterfall-tooltip-overlay" style="pointer-events: none;">
// Vertical band highlight
<rect
x=format!("{hl_x:.2}")
y="0"
width=format!("{hl_w:.2}")
height=format!("{h:.2}")
fill="white"
opacity="0.15"
/>
// Tooltip box background
<rect
x=format!("{box_x:.2}")
y=format!("{box_y:.2}")
width=box_w
height=box_h
rx="4"
fill=tooltip_bg()
/>
// Color indicator
<rect
x=format!("{:.2}", box_x)
y=format!("{box_y:.2}")
width="3"
height=box_h
rx="4"
fill=entry.color.clone()
/>
// Header: label + kind badge
<text
x=format!("{:.2}", box_x + padding)
y=format!("{:.2}", box_y + padding + 12.0)
font-size="11"
fill=tooltip_text()
font-family="'JetBrains Mono', monospace"
font-weight="bold"
>
{format!("{} [{}]", entry.label, kind_label)}
</text>
// Value row
<text
x=format!("{:.2}", box_x + padding)
y=format!("{:.2}", box_y + padding + 12.0 + row_h)
font-size="10"
fill="#ddd"
font-family="'JetBrains Mono', monospace"
>
{format!("Value: {value_prefix}{:.2}", entry.value)}
</text>
// Running total row
<text
x=format!("{:.2}", box_x + padding)
y=format!("{:.2}", box_y + padding + 12.0 + row_h * 2.0)
font-size="10"
fill="#ddd"
font-family="'JetBrains Mono', monospace"
>
{format!("Total: {:.2}", entry.running_total)}
</text>
</g>
},
)
}}
}
}