feat(ui): let users choose local or UTC chart tooltip times

This commit is contained in:
Andras Bacsai
2026-10-24 13:26:03 +02:00
parent c0ae50f120
commit 4e6c0bfcb5
7 changed files with 67 additions and 49 deletions
@@ -208,6 +208,44 @@
</div>
</section>
<section class="application-settings-section">
<div class="application-settings-section-header">
<div>
<h2>Chart times</h2>
<p>Choose which times chart tooltips show.</p>
</div>
</div>
<div class="application-settings-section-body grid gap-3 sm:grid-cols-2">
@foreach ([
['value' => 'local', 'label' => 'Your time', 'description' => 'Show the time in the time zone of this browser.', 'lines' => ['Oct 24, 12:30:33']],
['value' => 'utc', 'label' => 'Your time and UTC', 'description' => 'Also show the UTC time. Use it to compare with server logs.', 'lines' => ['Your time: Oct 24, 12:30:33 GMT+2', 'UTC: Oct 24, 10:30:33 UTC']],
] as $option)
<button type="button" @click="setChartTimes('{{ $option['value'] }}')"
class="group overflow-hidden rounded-[10px] border border-neutral-200 bg-white text-left transition-[border-color,box-shadow] hover:border-neutral-300 hover:shadow-sm dark:border-white/[0.07] dark:bg-white/[0.05] dark:hover:border-white/[0.12]"
:class="chartTimes === '{{ $option['value'] }}'
? 'border-coollabs! bg-coollabs/[0.06]! ring-2 ring-coollabs/25 dark:border-brand-light! dark:bg-brand-light/[0.08]! dark:ring-brand-light/25'
: ''">
<div class="flex h-20 items-center justify-center border-b border-neutral-200 bg-neutral-50 px-4 dark:border-white/[0.07] dark:bg-black/15">
<div class="rounded-md border border-neutral-300 bg-white px-2.5 py-1.5 text-[11px] leading-4 dark:border-white/15 dark:bg-[#181818]">
<div class="text-black dark:text-fg">CPU: <span class="font-semibold">4.3%</span></div>
@foreach ($option['lines'] as $line)
<div class="text-neutral-500 dark:text-fg-dim">{{ $line }}</div>
@endforeach
</div>
</div>
<div class="p-3">
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-semibold text-black dark:text-fg">{{ $option['label'] }}</span>
<x-reicon name="check-circle" class="size-4 text-coollabs dark:text-brand-light"
x-show="chartTimes === '{{ $option['value'] }}'" x-cloak />
</div>
<p class="mt-1 text-xs leading-5 text-neutral-500 dark:text-fg-dim">{{ $option['description'] }}</p>
</div>
</button>
@endforeach
</div>
</section>
{{-- Custom theme dialog: presets, a preview, four colors, and a share code. Save applies the draft. --}}
<template x-teleport="body">
<div x-show="dialogOpen" x-cloak @keydown.escape.window="dialogOpen = false"
+24
View File
@@ -138,6 +138,25 @@
root.style.setProperty('--theme-accent-foreground', window.themeAccentForeground(localStorage.themeColor));
document.querySelector('meta[name=theme-color]')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
};
// Time lines for chart tooltips. By default only the browser time zone is shown;
// the "Chart times" option on the Appearance page adds the UTC time.
window.chartTooltipTime = (timestamp, { seconds = true } = {}) => {
const format = (timeZone) => new Date(timestamp).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
...(seconds ? { second: '2-digit' } : {}),
hourCycle: 'h23',
...(timeZone ? { timeZone, timeZoneName: 'short' } : {}),
});
if (localStorage.getItem('chartTimes') !== 'utc') {
return `<div class="apexcharts-tooltip-custom-title">${format()}</div>`;
}
const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return `<div class="apexcharts-tooltip-custom-title">Your time: ${format(localTimeZone)}</div>
<div class="apexcharts-tooltip-custom-title">UTC: ${format('UTC')}</div>`;
};
// 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.
@@ -187,6 +206,7 @@
pageWidth: localStorage.getItem('pageWidth') || 'centered',
navStyle: localStorage.getItem('navStyle') === 'floating' ? 'floating' : 'default',
navItems: localStorage.getItem('navItems') === 'flat' ? 'flat' : '3d',
chartTimes: localStorage.getItem('chartTimes') === 'utc' ? 'utc' : 'local',
presets: window.customThemePresets,
// The custom theme dialog edits a draft; Save applies it. A null draft is the built-in Dark theme.
dialogOpen: false,
@@ -272,6 +292,10 @@
localStorage.setItem('navItems', style);
document.documentElement.dataset.navItems = style;
},
setChartTimes(value) {
this.chartTimes = value;
localStorage.setItem('chartTimes', value);
},
});
document.addEventListener('livewire:navigated', window.applyStoredTheme);
@@ -92,23 +92,13 @@
const memory = series[1][dataPointIndex];
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
const formatPercent = value => Number.isFinite(value) ? `${Number(value.toFixed(1))}%` : '—';
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 dot = color => `<span style="display:inline-block;width:8px;height:8px;border-radius:9999px;margin-right:6px;flex:none;background:${color}"></span>`;
return `<div class="apexcharts-tooltip-custom">
<div class="apexcharts-tooltip-custom-value" style="display:flex;align-items:center">${dot('rgb(30, 144, 255)')}CPU: <span class="apexcharts-tooltip-value-bold" style="margin-left:4px">${formatPercent(cpu)}</span></div>
<div class="apexcharts-tooltip-custom-value" style="display:flex;align-items:center">${dot('rgb(168, 85, 247)')}Memory: <span class="apexcharts-tooltip-value-bold" style="margin-left:4px">${formatPercent(memory)}</span></div>
<div class="apexcharts-tooltip-custom-title">Your time: ${formatLocalTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">UTC: ${formatUtcTimestamp(timestamp)}</div>
${window.chartTooltipTime(timestamp)}
</div>`;
},
},
@@ -380,9 +380,6 @@ $serverListboxOptions = array_merge(
};
const fmtRate = v => `${fmtBytes(v)}/s`;
const fmtLocalTime = ts => new Date(ts).toLocaleString(undefined, { hour12: false, timeZoneName: 'short' });
const fmtUtcTime = ts => new Date(ts).toLocaleString(undefined, { hour12: false, timeZone: 'UTC', timeZoneName: 'short' });
// The app strips the default ApexCharts tooltip background (utilities.css), so
// every chart must render its own `apexcharts-tooltip-custom` markup.
const customTooltip = formatter => ({ series, dataPointIndex, w }) => {
@@ -394,8 +391,7 @@ $serverListboxOptions = array_merge(
rows += `<div class="apexcharts-tooltip-custom-value">${w.globals.seriesNames[i]}: <span class="apexcharts-tooltip-value-bold">${formatter(value)}</span></div>`;
});
return `<div class="apexcharts-tooltip-custom">${rows}
<div class="apexcharts-tooltip-custom-title">Your time: ${fmtLocalTime(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">UTC: ${fmtUtcTime(timestamp)}</div>
${window.chartTooltipTime(timestamp)}
</div>`;
};
@@ -125,15 +125,6 @@
};
const formatRate = value => `${formatBytes(value)}/s`;
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, formatter = formatPercent) => ({
chart: {
@@ -226,8 +217,7 @@
return `<div class="apexcharts-tooltip-custom">
${values.join('')}
<div class="apexcharts-tooltip-custom-title">Your time: ${formatLocalTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">UTC: ${formatUtcTimestamp(timestamp)}</div>
${window.chartTooltipTime(timestamp)}
</div>`;
},
},
@@ -47,15 +47,6 @@
// `24h` buckets are hourly, `7d`/`30d` daily — pick a matching axis/tooltip format.
const timeFormat = range => (range === '24h' ? 'HH:mm' : 'dd MMM');
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 chart = new ApexCharts(el, {
chart: {
@@ -108,8 +99,7 @@
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">Your time: ${formatLocalTimestamp(timestamp)}</div>
<div class="apexcharts-tooltip-custom-title">UTC: ${formatUtcTimestamp(timestamp)}</div>
${window.chartTooltipTime(timestamp, { seconds: false })}
</div>`;
},
},
@@ -65,15 +65,6 @@
init() {
if (typeof ApexCharts === 'undefined') { return; }
const o = this.opts(@js($initial), @js($initialCategories));
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 formatValue = value => {
if (value === null || value === undefined) { return '—'; }
const fmt = @js($format);
@@ -110,8 +101,7 @@
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'>Your time: ${formatLocalTimestamp(timestamp)}</div>
<div class='apexcharts-tooltip-custom-title'>UTC: ${formatUtcTimestamp(timestamp)}</div>
${window.chartTooltipTime(timestamp, { seconds: false })}
</div>`;
},
},