feat(ui): sync metric chart tooltips across charts on hover

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude Opus 5.5 committed 2026-10-24 13:24:57 +02:00
1 parent 1527917523
commit 407ac50439
4 files changed
+49 -2

No files matched your search

+42
View File
@@ -138,6 +138,48 @@
root.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(localStorage.themeColor));
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
};
// Shows the tooltip at the same time on every chart of a group while one of them is hovered.
// ApexCharts' own chart.group needs equal widths and equal x ranges, so it does not
// fit side-by-side cards. Instead, replay the hover as a mouse event at the matching x.
// Charts can join a group from separate scripts; charts removed from the page drop out.
window.chartSyncGroups ??= {};
window.syncChartTooltips = (group, charts) => {
const grid = chart => chart.el.querySelector('.apexcharts-grid')?.getBoundingClientRect();
const hoverArea = chart => chart.el.querySelector('.apexcharts-svg');
const hide = chart => hoverArea(chart)?.dispatchEvent(new MouseEvent('mouseout', { clientX: 0, clientY: -1 }));
const peers = source => window.chartSyncGroups[group].filter(chart => chart !== source && chart.el.isConnected);
window.chartSyncGroups[group] = [
...(window.chartSyncGroups[group] ?? []).filter(chart => chart.el.isConnected),
...charts,
];
charts.forEach(source => {
source.el.addEventListener('mousemove', event => {
// Replayed events are untrusted; skip them so charts do not echo each other.
const rect = grid(source);
const { minX, maxX } = source.w.globals;
if (!event.isTrusted || !rect?.width || !(maxX > minX)) {
return;
}
const timestamp = minX + (event.clientX - rect.left) / rect.width * (maxX - minX);
peers(source).forEach(chart => {
const target = grid(chart);
const { minX: min, maxX: max } = chart.w.globals;
if (!target?.width || !(max > min) || timestamp < min || timestamp > max) {
hide(chart);
return;
}
hoverArea(chart)?.dispatchEvent(new MouseEvent('mousemove', {
clientX: target.left + (timestamp - min) / (max - min) * target.width,
clientY: target.top + target.height / 2,
}));
});
});
source.el.addEventListener('mouseleave', () => peers(source).forEach(hide));
});
};
// Alpine state for the theme controls on the Appearance page (x-data="themeControls()").
window.themeControls = () => ({
theme: localStorage.getItem('theme') === 'purple' ? 'custom' : (localStorage.getItem('theme') || 'dark'),
@@ -433,6 +433,7 @@ $serverListboxOptions = array_merge(
chart.render();
charts[key] = chart;
}
window.syncChartTooltips('{!! $chartId !!}', Object.values(charts));
const renderLegends = specs => {
for (const [key, spec] of Object.entries(specs)) {
@@ -170,6 +170,7 @@
});
cpuChart.render();
window.syncChartTooltips('{!! $chartId !!}', [cpuChart]);
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', chartData => {
checkTheme();
@@ -326,6 +327,7 @@
});
memoryChart.render();
window.syncChartTooltips('{!! $chartId !!}', [memoryChart]);
Livewire.on('refreshChartData-{!! $chartId !!}-memory', chartData => {
checkTheme();
@@ -220,11 +220,12 @@
dataPointIndex,
w
}) => {
const value = series[seriesIndex][dataPointIndex];
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
const values = series.map((values, index) => values[dataPointIndex] === undefined ? '' :
`<div class="apexcharts-tooltip-custom-value">${w.globals.seriesNames?.[index] ?? name}: <span class="apexcharts-tooltip-value-bold">${formatter(values[dataPointIndex])}</span></div>`);
return `<div class="apexcharts-tooltip-custom">
<div class="apexcharts-tooltip-custom-value">${w.globals.seriesNames?.[seriesIndex] ?? name}: <span class="apexcharts-tooltip-value-bold">${formatter(value)}</span></div>
${values.join('')}
<div class="apexcharts-tooltip-custom-title">Your time: ${formatLocalTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">UTC: ${formatUtcTimestamp(timestamp)}</div>
</div>`;
@@ -268,6 +269,7 @@
diskChart.render();
networkChart.render();
loadChart.render();
window.syncChartTooltips('{!! $chartId !!}-metrics', [cpuChart, memoryChart, networkChart, loadChart, diskChart]);
});
Livewire.on('refreshChartData-{!! $chartId !!}-metrics', chartData => {