From 57f235dd36348d033747c1c4a863251ce4524cdb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 12:18:29 +0000 Subject: [PATCH] Serverpuls: deutsche Zeitachse und Legende im Diagramm Co-Authored-By: Claude Opus 5.5 --- src/components/serverpuls/Chart.jsx | 10 +++++++-- src/lib/serverpulsFormat.js | 33 ++++++++++++++++++++++++++++ tests/unit/serverpulsFormat.test.mjs | 20 ++++++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/components/serverpuls/Chart.jsx b/src/components/serverpuls/Chart.jsx index 536a846..c943f06 100644 --- a/src/components/serverpuls/Chart.jsx +++ b/src/components/serverpuls/Chart.jsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from 'react' import uPlot from 'uplot' +import { formatAxisTick, formatLegendTime } from '../../lib/serverpulsFormat' import 'uplot/dist/uPlot.min.css' /** @@ -23,7 +24,12 @@ export default function Chart({ data, series, height = 280, yFmt, yRange }) { legend: { live: true }, scales: { y: { range: yr || ((u, min, max) => [Math.min(0, min), max === min ? min + 1 : max + (max - min) * 0.08]) } }, axes: [ - { stroke: '#9aa7b4', grid: { stroke: 'rgba(255,255,255,0.06)' }, ticks: { stroke: 'rgba(255,255,255,0.1)' } }, + { + stroke: '#9aa7b4', + grid: { stroke: 'rgba(255,255,255,0.06)' }, + ticks: { stroke: 'rgba(255,255,255,0.1)' }, + values: (u, splits, axisIdx, foundSpace, foundIncr) => splits.map((t) => formatAxisTick(t, foundIncr)), + }, { stroke: '#9aa7b4', grid: { stroke: 'rgba(255,255,255,0.06)' }, @@ -33,7 +39,7 @@ export default function Chart({ data, series, height = 280, yFmt, yRange }) { }, ], series: [ - {}, + { label: 'Zeit', value: (u, v) => formatLegendTime(v) }, ...s.map((x) => ({ label: x.label, stroke: x.color || '#10b981', diff --git a/src/lib/serverpulsFormat.js b/src/lib/serverpulsFormat.js index 01356e0..1d2309c 100644 --- a/src/lib/serverpulsFormat.js +++ b/src/lib/serverpulsFormat.js @@ -56,3 +56,36 @@ export function formatNumber(v, digits = 2) { if (v === null || v === undefined) return '–' return nf(digits).format(v) } + +// --- Zeitachsen (Europe/Berlin, de-DE) --- +const TZ = 'Europe/Berlin' +const dtf = (opts) => new Intl.DateTimeFormat('de-DE', { timeZone: TZ, ...opts }) +const F = { + time: dtf({ hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }), + day: dtf({ day: '2-digit', month: '2-digit' }), + month: dtf({ month: 'short', year: 'numeric' }), + year: dtf({ year: 'numeric' }), + legend: dtf({ day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }), +} + +// Welches Format passt zu einem Tick-Abstand (Sekunden)? +export function axisTimeKind(incrSec) { + if (incrSec < 86400) return 'time' + if (incrSec < 86400 * 28) return 'day' + if (incrSec < 86400 * 360) return 'month' + return 'year' +} + +// Tick (Sekunden) -> Text. Bei Tagesgrenzen innerhalb von Sub-Tages-Achsen das Datum zeigen. +export function formatAxisTick(sec, incrSec) { + const kind = axisTimeKind(incrSec) + const d = new Date(sec * 1000) + if (kind === 'time' && F.time.format(d) === '00:00') return F.day.format(d) + return F[kind].format(d) +} + +// Legende: "30.09.2026, 11:20" +export function formatLegendTime(sec) { + if (sec === null || sec === undefined) return '–' + return F.legend.format(new Date(sec * 1000)) +} diff --git a/tests/unit/serverpulsFormat.test.mjs b/tests/unit/serverpulsFormat.test.mjs index 68e4e99..d72ae46 100644 --- a/tests/unit/serverpulsFormat.test.mjs +++ b/tests/unit/serverpulsFormat.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test' import assert from 'node:assert/strict' import { loadModule } from './load.mjs' -const { formatBytes, formatDelta, ageLabel, usedPct } = await loadModule('src/lib/serverpulsFormat.js') +const { formatBytes, formatDelta, ageLabel, usedPct, axisTimeKind, formatAxisTick, formatLegendTime } = await loadModule('src/lib/serverpulsFormat.js') test('formatBytes: de-DE, Basis 1024', () => { assert.equal(formatBytes(1288490189), '1,2 GB') @@ -34,3 +34,21 @@ test('usedPct: RAM value ist frei', () => { assert.equal(usedPct(250, 1000), 75) assert.equal(usedPct(null, 1000), null) }) + +test('Zeitachse: Format nach Tick-Abstand', () => { + assert.equal(axisTimeKind(3600), 'time') + assert.equal(axisTimeKind(86400), 'day') + assert.equal(axisTimeKind(86400 * 30), 'month') + assert.equal(axisTimeKind(86400 * 365), 'year') +}) + +test('Zeitachse: deutsche Ausgabe in Europe/Berlin', () => { + const t = Date.UTC(2026, 8, 30, 9, 20) / 1000 // 11:20 Berlin (MESZ) + assert.equal(formatAxisTick(t, 3600), '11:20') + assert.equal(formatAxisTick(t, 86400 * 2), '30.09.') + assert.equal(formatAxisTick(Date.UTC(2026, 8, 29, 22, 0) / 1000, 3600 * 6), '30.09.') // Mitternacht Berlin + assert.match(formatAxisTick(t, 86400 * 30), /2026/) + assert.equal(formatAxisTick(t, 86400 * 365), '2026') + assert.equal(formatLegendTime(t), '30.09.2026, 11:20') + assert.equal(formatLegendTime(null), '–') +})