Skip to content

Commit 5a562c4

Browse files
committed
Bar chart tooltip
1 parent 0c7dbdc commit 5a562c4

3 files changed

Lines changed: 122 additions & 4 deletions

File tree

client/app.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,3 +1197,15 @@
11971197
max-width: var(--UI-Spacing-spacing-xxxl) !important;
11981198
margin-right: var(--UI-Spacing-spacing-s) !important;
11991199
}
1200+
1201+
/* ===== BAR CHART TOOLTIP ===== */
1202+
1203+
.pl-bar-chart-tooltip {
1204+
padding: var(--UI-Spacing-spacing-s) var(--UI-Spacing-spacing-ml);
1205+
font-family: var(--body-family);
1206+
font-size: var(--UI-Typography-body-small-size);
1207+
line-height: var(--UI-Typography-body-small-line-height);
1208+
color: var(--Colors-Text-Body-Default);
1209+
white-space: nowrap;
1210+
max-width: none;
1211+
}

client/src/probability-lab/ui/charts/bar-chart.js

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import withCanvas2d from './canvas.js';
33
import { getCssVar } from './colors.js';
44
import { clamp } from '../../../shared/math.js';
5-
import { formatProbability } from '../../../shared/format.js';
5+
import { formatProbability, formatCount } from '../../../shared/format.js';
66

7-
export default function drawBarChart(canvas, labels, values, theory) {
7+
export default function drawBarChart(canvas, labels, values, theory, counts = [], trials = 0) {
88
const primary = getCssVar('--Colors-Base-Primary-600', '#377DFF');
99
const grid = getCssVar('--Colors-Stroke-Default', '#DDE0EA');
1010
const text = getCssVar('--Colors-Text-Body-Medium', '#66718F');
@@ -99,3 +99,108 @@ export default function drawBarChart(canvas, labels, values, theory) {
9999
}
100100
});
101101
}
102+
103+
// Store cleanup function for event listeners
104+
let hoverCleanup = null;
105+
106+
export function setupBarChartHover(canvas, labels, counts, trials) {
107+
// Clean up previous listeners if they exist
108+
if (hoverCleanup) {
109+
hoverCleanup();
110+
hoverCleanup = null;
111+
}
112+
113+
// Get or create tooltip element
114+
let tooltip = document.querySelector('.pl-bar-chart-tooltip');
115+
if (!tooltip) {
116+
tooltip = document.createElement('div');
117+
tooltip.className = 'pl-bar-chart-tooltip box card';
118+
tooltip.style.display = 'none';
119+
tooltip.style.position = 'absolute';
120+
tooltip.style.pointerEvents = 'none';
121+
tooltip.style.zIndex = '1000';
122+
document.body.appendChild(tooltip);
123+
}
124+
125+
const padding = { top: 14, right: 12, bottom: 34, left: 44 };
126+
const tooltipOffset = 10; // Offset from cursor
127+
128+
function handleMouseMove(event) {
129+
if (trials === 0 || labels.length === 0) {
130+
tooltip.style.display = 'none';
131+
return;
132+
}
133+
134+
const rect = canvas.getBoundingClientRect();
135+
const x = event.clientX - rect.left;
136+
const y = event.clientY - rect.top;
137+
138+
const w = rect.width;
139+
const h = rect.height;
140+
const plotW = Math.max(1, w - padding.left - padding.right);
141+
const plotH = Math.max(1, h - padding.top - padding.bottom);
142+
143+
// Check if mouse is within plot area
144+
if (x < padding.left || x > padding.left + plotW || y < padding.top || y > padding.top + plotH) {
145+
tooltip.style.display = 'none';
146+
return;
147+
}
148+
149+
// Calculate which bar slot the mouse is over
150+
const gap = labels.length > 1 ? Math.min(14, plotW / (labels.length * 2)) : 0;
151+
const slotW = (plotW - gap * (labels.length - 1)) / labels.length;
152+
const relativeX = x - padding.left;
153+
const barIndex = Math.floor(relativeX / (slotW + gap));
154+
155+
// Check if mouse is over a valid bar
156+
if (barIndex < 0 || barIndex >= labels.length) {
157+
tooltip.style.display = 'none';
158+
return;
159+
}
160+
161+
// Check if mouse is within the bar slot bounds
162+
const slotX = padding.left + barIndex * (slotW + gap);
163+
if (x < slotX || x > slotX + slotW) {
164+
tooltip.style.display = 'none';
165+
return;
166+
}
167+
168+
// Get data for this bar
169+
const count = counts[barIndex] ?? 0;
170+
const label = labels[barIndex];
171+
const probability = trials > 0 ? count / trials : 0;
172+
173+
// Format tooltip content
174+
const tooltipText = `P(${label}) = ${formatCount(count)} / ${formatCount(trials)} = ${formatProbability(probability, 2)}`;
175+
tooltip.textContent = tooltipText;
176+
177+
// Position tooltip near cursor
178+
tooltip.style.display = 'block';
179+
tooltip.style.left = `${event.clientX + tooltipOffset}px`;
180+
tooltip.style.top = `${event.clientY + tooltipOffset}px`;
181+
182+
// Adjust if tooltip goes off screen
183+
requestAnimationFrame(() => {
184+
const tooltipRect = tooltip.getBoundingClientRect();
185+
if (tooltipRect.right > window.innerWidth) {
186+
tooltip.style.left = `${event.clientX - tooltipRect.width - tooltipOffset}px`;
187+
}
188+
if (tooltipRect.bottom > window.innerHeight) {
189+
tooltip.style.top = `${event.clientY - tooltipRect.height - tooltipOffset}px`;
190+
}
191+
});
192+
}
193+
194+
function handleMouseLeave() {
195+
tooltip.style.display = 'none';
196+
}
197+
198+
canvas.addEventListener('mousemove', handleMouseMove);
199+
canvas.addEventListener('mouseleave', handleMouseLeave);
200+
201+
// Store cleanup function
202+
hoverCleanup = () => {
203+
canvas.removeEventListener('mousemove', handleMouseMove);
204+
canvas.removeEventListener('mouseleave', handleMouseLeave);
205+
};
206+
}

client/src/probability-lab/ui/render-single.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Single-mode rendering
2-
import drawBarChart from './charts/bar-chart.js';
2+
import drawBarChart, { setupBarChartHover } from './charts/bar-chart.js';
33
import drawLineChart from './charts/line-chart.js';
44
import { buildFrequencyTableHtml } from './tables.js';
55
import { updateDeviceViewSingle } from './device-view.js';
@@ -71,7 +71,8 @@ export default function renderSingle(els, state) {
7171
// TODO: add nice die animation, e.g. https://github.com/3d-dice/dice-box
7272

7373
const rel = state.single.trials === 0 ? def.labels.map(() => 0) : state.single.counts.map((c) => c / state.single.trials);
74-
drawBarChart(els.barChart, def.labels, rel, def.probabilities);
74+
drawBarChart(els.barChart, def.labels, rel, def.probabilities, state.single.counts, state.single.trials);
75+
setupBarChartHover(els.barChart, def.labels, state.single.counts, state.single.trials);
7576

7677
const historyTrials = state.single.history.map((p) => p.trials);
7778
const historyEst = state.single.history.map((p) => Array.from(selectedIndices).reduce((acc, idx) => acc + (p.rel[idx] ?? 0), 0));

0 commit comments

Comments
 (0)