// Shared by the stats overview and the per-alert drilldown: the time-range // chips, the chart cards and the SVG column chart. import { h } from './ui.js'; const DAY_MS = 24 * 60 * 60 * 1000; // `days` counts back from today, inclusive; the server reads from/to as UTC // dates, so these are too. export const RANGES = [ { id: 'today', label: 'Today', days: 1 }, { id: '7d', label: '7d', days: 7 }, { id: '30d', label: '30d', days: 30 }, { id: '90d', label: '90d', days: 90 }, { id: 'all', label: 'All', days: null }, ]; // The chosen range outlives navigating between the overview and a drilldown. let range = '30d'; export const getRange = () => range; export const setRangeId = (id) => { range = id; }; const utcDate = (ms) => new Date(ms).toISOString().slice(0, 10); export function rangeQuery() { const r = RANGES.find((x) => x.id === range); if (!r.days) return {}; const now = Date.now(); return { from: utcDate(now - (r.days - 1) * DAY_MS), to: utcDate(now) }; } export function chips(onPick, ranges = RANGES) { return h('div', { class: 'chips', role: 'tablist', 'aria-label': 'Time range' }, ranges.map((r) => h('button', { class: 'chip', type: 'button', role: 'tab', 'aria-selected': String(r.id === range), onclick: () => onPick(r.id), text: r.label, }))); } export function card(title, content, note) { return h('section', { class: 'chart-card card card-pad' }, h('div', { class: 'chart-head' }, h('h3', { class: 'chart-title', text: title }), note && h('span', { class: 'chart-note', text: note })), content); } export const pad2 = (n) => String(n).padStart(2, '0'); export const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; // Monday first, as the mockup has it; server weekdays are 0=Sunday. export const MONDAY_FIRST = [1, 2, 3, 4, 5, 6, 0]; const SVG_NS = 'http://www.w3.org/2000/svg'; function svg(tag, attrs = {}, text) { const el = document.createElementNS(SVG_NS, tag); for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v)); if (text != null) el.textContent = text; return el; } // A column chart: one bar per item, the value in a tooltip, and a label under // the items marked `tick`. export function columns(items, label) { const W = 480; const H = 140; const base = H - 18; const step = W / items.length; const max = Math.max(...items.map((x) => x.value), 1); const root = svg('svg', { class: 'columns', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': label, }); root.appendChild(svg('line', { class: 'axis', x1: 0, x2: W, y1: base, y2: base })); items.forEach((it, i) => { const bh = it.value ? Math.max(2, (it.value / max) * (base - 6)) : 0; const x = i * step + step * 0.15; const g = svg('g', { class: 'col' }); g.appendChild(svg('title', {}, `${it.label}: ${it.value}`)); // A full-height transparent hit area, so a tiny bar is still hoverable. g.appendChild(svg('rect', { class: 'col-hit', x: i * step, y: 0, width: step, height: base })); if (bh) g.appendChild(svg('rect', { class: 'col-bar', x, y: base - bh, width: step * 0.7, height: bh, rx: 2 })); root.appendChild(g); if (it.tick) { root.appendChild(svg('text', { class: 'col-label', x: i * step + step / 2, y: H - 4, 'text-anchor': 'middle' }, it.label)); } }); return root; } // A 7×24 weekday-by-hour grid from `grid[weekday][hour]` (0=Sunday); cell // opacity steps with the count relative to the busiest cell. export function heatmap(grid) { const max = Math.max(...grid.flat(), 1); const level = (c) => (c === 0 ? 0 : Math.min(4, Math.ceil((c / max) * 4))); const rows = MONDAY_FIRST.map((w) => h('div', { class: 'heat-row' }, h('span', { class: 'heat-day', text: WEEKDAYS[w] }), h('div', { class: 'heat-cells' }, grid[w].map((c, hr) => h('div', { class: `heat-cell lv${level(c)}`, title: `${WEEKDAYS[w]} ${pad2(hr)}:00 UTC: ${c}`, }))))); const axis = h('div', { class: 'heat-row' }, h('span', { class: 'heat-day' }), h('div', { class: 'heat-cells' }, Array.from({ length: 24 }, (_, hr) => h('span', { class: 'heat-hour', text: hr % 3 === 0 ? pad2(hr) : '' })))); const peak = grid.flatMap((row, d) => row.map((c, hr) => ({ d, hr, c }))).reduce((a, b) => (b.c > a.c ? b : a)); return h('div', { class: 'heatmap', role: 'img', 'aria-label': peak.c ? `Heatmap of incidents by weekday and hour in UTC. The busiest slot is ${WEEKDAYS[peak.d]} at ${pad2(peak.hr)}:00 with ${peak.c}.` : 'Heatmap of incidents by weekday and hour in UTC. Nothing in this range.', }, rows, axis); }