feat(charts): show local and UTC times in metric tooltips

Chart tooltips now list the viewer's local time and UTC with short
timezone names, instead of a UTC-only stamp. Also add a little left
padding on the traffic requests chart so axis labels are not clipped.
This commit is contained in:
Andras Bacsai
2026-09-08 13:31:40 +02:00
parent a25231683e
commit bd7d0b5de1
7 changed files with 70 additions and 28 deletions
@@ -92,15 +92,21 @@
const memory = series[1][dataPointIndex];
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
const formatPercent = value => Number.isFinite(value) ? `${Number(value.toFixed(1))}%` : '—';
const formatTimestamp = timestamp => `${new Date(timestamp).toLocaleString(undefined, {
timeZone: 'UTC',
const formatLocalTimestamp = timestamp => new Date(timestamp).toLocaleString(undefined, {
hour12: false,
})} UTC`;
timeZoneName: 'short',
});
const formatUtcTimestamp = timestamp => new Date(timestamp).toLocaleString(undefined, {
hour12: false,
timeZone: 'UTC',
timeZoneName: 'short',
});
return `<div class="apexcharts-tooltip-custom">
<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">${formatPercent(cpu)}</span></div>
<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">${formatPercent(memory)}</span></div>
<div class="apexcharts-tooltip-custom-title">${formatTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">Your time: ${formatLocalTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">UTC: ${formatUtcTimestamp(timestamp)}</div>
</div>`;
},
},
@@ -64,14 +64,15 @@
return `${Number(number.toFixed(precision))}%`;
};
const formatTimestamp = timestamp => {
const date = new Date(timestamp);
return `${date.toLocaleString(undefined, {
timeZone: 'UTC',
hour12: false
})} UTC`;
};
const formatLocalTimestamp = timestamp => new Date(timestamp).toLocaleString(undefined, {
hour12: false,
timeZoneName: 'short',
});
const formatUtcTimestamp = timestamp => new Date(timestamp).toLocaleString(undefined, {
hour12: false,
timeZone: 'UTC',
timeZoneName: 'short',
});
const chartOptions = (name, color, loadingText) => ({
chart: {
@@ -163,7 +164,8 @@
return `<div class="apexcharts-tooltip-custom">
<div class="apexcharts-tooltip-custom-value">${name}: <span class="apexcharts-tooltip-value-bold">${formatPercent(value)}</span></div>
<div class="apexcharts-tooltip-custom-title">${formatTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">Your time: ${formatLocalTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">UTC: ${formatUtcTimestamp(timestamp)}</div>
</div>`;
},
},
@@ -46,10 +46,15 @@
// `24h` buckets are hourly, `7d`/`30d` daily — pick a matching axis/tooltip format.
const timeFormat = range => (range === '24h' ? 'HH:mm' : 'dd MMM');
const formatTimestamp = timestamp => `${new Date(timestamp).toLocaleString(undefined, {
timeZone: 'UTC',
const formatLocalTimestamp = timestamp => new Date(timestamp).toLocaleString(undefined, {
hour12: false,
})} UTC`;
timeZoneName: 'short',
});
const formatUtcTimestamp = timestamp => new Date(timestamp).toLocaleString(undefined, {
hour12: false,
timeZone: 'UTC',
timeZoneName: 'short',
});
const chart = new ApexCharts(el, {
chart: {
@@ -84,7 +89,7 @@
},
grid: {
borderColor: gridColor(),
padding: { left: 0, right: 12, top: 12, bottom: 0 },
padding: { left: 6, right: 12, top: 12, bottom: 0 },
},
legend: { show: false },
noData: {
@@ -101,7 +106,8 @@
return `<div class="apexcharts-tooltip-custom">
<div class="apexcharts-tooltip-custom-value">Requests: <span class="apexcharts-tooltip-value-bold">${requests.toLocaleString()}</span></div>
<div class="apexcharts-tooltip-custom-title">${formatTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">Your time: ${formatLocalTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">UTC: ${formatUtcTimestamp(timestamp)}</div>
</div>`;
},
},
@@ -124,7 +130,7 @@
colors: [accent()],
grid: {
borderColor: gridColor(),
padding: { left: 0, right: 12, top: 12, bottom: 0 },
padding: { left: 6, right: 12, top: 12, bottom: 0 },
},
xaxis: {
type: 'datetime',
@@ -58,10 +58,15 @@
init() {
if (typeof ApexCharts === 'undefined') { return; }
const o = this.opts(@js($initial), @js($initialCategories));
const formatTimestamp = timestamp => `${new Date(timestamp).toLocaleString(undefined, {
timeZone: 'UTC',
const formatLocalTimestamp = timestamp => new Date(timestamp).toLocaleString(undefined, {
hour12: false,
})} UTC`;
timeZoneName: 'short',
});
const formatUtcTimestamp = timestamp => new Date(timestamp).toLocaleString(undefined, {
hour12: false,
timeZone: 'UTC',
timeZoneName: 'short',
});
const formatValue = value => {
if (@js($key) === 'bandwidthSpark') {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
@@ -95,7 +100,8 @@
const timestamp = w.globals.seriesX[0][dataPointIndex];
return `<div class='apexcharts-tooltip-custom'>
<div class='apexcharts-tooltip-custom-value'>${@js($label)}: <span class='apexcharts-tooltip-value-bold'>${formatValue(value)}</span></div>
<div class='apexcharts-tooltip-custom-title'>${formatTimestamp(timestamp)}</div>
<div class='apexcharts-tooltip-custom-title'>Your time: ${formatLocalTimestamp(timestamp)}</div>
<div class='apexcharts-tooltip-custom-title'>UTC: ${formatUtcTimestamp(timestamp)}</div>
</div>`;
},
},
@@ -79,7 +79,8 @@ it('configures the dashboard chart as a ten minute cpu and memory sparkline with
->not->toContain('w-full overflow-hidden rounded-b-xl')
->toContain('CPU:')
->toContain('Memory:')
->toContain('formatTimestamp(timestamp)')
->toContain('formatLocalTimestamp(timestamp)')
->toContain('formatUtcTimestamp(timestamp)')
->toContain("min: 0,\n max: 100,")
->toContain('labels: { show: false }');
});
@@ -29,3 +29,14 @@ test('metrics charts render custom tooltip content', function (string $view) {
'server metrics' => 'views/livewire/server/charts.blade.php',
'resource metrics' => 'views/livewire/project/shared/metrics.blade.php',
]);
test('server metric tooltips show local and UTC timestamps', function (string $view) {
expect(file_get_contents(resource_path($view)))
->toContain('formatLocalTimestamp(timestamp)')
->toContain('formatUtcTimestamp(timestamp)')
->toContain('Your time:')
->toContain('UTC:');
})->with([
'dashboard server metrics' => 'views/livewire/dashboard/server-metrics-chart.blade.php',
'server metrics' => 'views/livewire/server/charts.blade.php',
]);
@@ -55,7 +55,7 @@ it('keeps request values in a compact y-axis outside the plot', function () {
expect($partial)
->toContain('minWidth: 28')
->toContain('maxWidth: 28')
->toContain('padding: { left: 0, right: 12, top: 12, bottom: 0 }')
->toContain('padding: { left: 6, right: 12, top: 12, bottom: 0 }')
->not->toContain('floating: true')
->not->toContain('offsetX: 32');
});
@@ -110,18 +110,28 @@ it('keeps KPI sparklines axisless after live updates', function () {
->toContain('grid: { padding: { left: 4, right: 4, top: 0, bottom: 0 } }');
});
it('shows values and UTC timestamps in analytics chart tooltips', function () {
it('shows values with local and UTC timestamps in analytics chart tooltips', function () {
$sparkline = file_get_contents(base_path('resources/views/livewire/traffic/_sparkline.blade.php'));
$requestsChart = file_get_contents(base_path('resources/views/livewire/traffic/_requests-chart.blade.php'));
expect($sparkline)
->toContain('sparkCategories')
->toContain('apexcharts-tooltip-custom-value')
->toContain('formatTimestamp(timestamp)');
->toContain('formatLocalTimestamp(timestamp)')
->toContain('formatUtcTimestamp(timestamp)')
->toContain('Your time:')
->toContain('UTC:')
->toContain("timeZoneName: 'short'")
->toContain("timeZone: 'UTC'");
expect($requestsChart)
->toContain('apexcharts-tooltip-custom-value')
->toContain('formatTimestamp(timestamp)');
->toContain('formatLocalTimestamp(timestamp)')
->toContain('formatUtcTimestamp(timestamp)')
->toContain('Your time:')
->toContain('UTC:')
->toContain("timeZoneName: 'short'")
->toContain("timeZone: 'UTC'");
foreach ([
app_path('Livewire/Analytics.php'),