fix: legend CSS, SI prefix for x-axis ticks, table sort cleanup

This commit is contained in:
Sebastian Seedorf
2026-06-03 13:00:34 +02:00
parent 3506d1f6c5
commit 654d3849eb
6 changed files with 20 additions and 30 deletions

View File

@@ -14,4 +14,8 @@ body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: ui-sans-serif, system-ui, sans-serif; font-family: ui-sans-serif, system-ui, sans-serif;
}
.u-legend .u-series:first-child {
display: none;
} }

View File

@@ -46,7 +46,7 @@ export function CardShell({ title, onEdit, onDelete, empty, children, legendCont
{/* Legend scrolls independently, capped at 25% card height */} {/* Legend scrolls independently, capped at 25% card height */}
<div <div
ref={legendContainerRef} ref={legendContainerRef}
className="shrink-0 max-h-[25%] overflow-y-auto border-t border-gray-800 px-2 py-1 text-[10px] text-gray-400 [&_.u-legend]:w-full [&_.u-series]:flex [&_.u-series]:items-center [&_.u-series_th]:flex [&_.u-series_th]:items-center [&_.u-series_th]:gap-1 [&_.u-marker]:w-3 [&_.u-marker]:h-0.5 [&_.u-marker]:shrink-0 [&_.u-label]:truncate [&_.u-value]:ml-auto [&_.u-value]:font-mono [&_.u-value]:shrink-0 [&_.u-series:first-child]:hidden" className="shrink-0 max-h-[25%] overflow-y-auto border-t border-gray-800 px-2 py-1 text-[10px] text-gray-400 [&_.u-legend]:w-full [&_.u-series]:flex [&_.u-series]:items-center [&_.u-series_th]:flex [&_.u-series_th]:items-center [&_.u-series_th]:gap-1 [&_.u-marker]:w-3 [&_.u-marker]:h-0.5 [&_.u-marker]:shrink-0 [&_.u-label]:truncate [&_.u-value]:ml-auto [&_.u-value]:font-mono [&_.u-value]:shrink-0"
/> />
</> </>
)} )}

View File

@@ -14,27 +14,11 @@ interface Props {
export default function TableViz({ config, rows, onEdit, onDelete }: Props) { export default function TableViz({ config, rows, onEdit, onDelete }: Props) {
const { localeMap } = useApp(); const { localeMap } = useApp();
const sortCol = config.signal_type === 'red' ? 'red' : 'green';
const latest = new Map<string, { green?: number; red?: number }>(); const latest = new Map<string, { green?: number; red?: number }>();
for (const row of rows) { for (const row of rows) {
latest.set(`${row.combinator}::${row.item_key}`, { green: row.green, red: row.red }); latest.set(`${row.combinator}::${row.item_key}`, { green: row.green, red: row.red });
} }
const tableRows = [...latest.entries()] const tableRows = [...latest.entries()].slice(0, config.series_limit);
.sort((a, b) => {
const va = a[1][sortCol] ?? 0;
const vb = b[1][sortCol] ?? 0;
switch (config.order_by) {
case 'value_asc':
case 'delta_asc':
return va - vb;
case 'abs_desc':
return Math.abs(vb) - Math.abs(va);
default:
return vb - va;
}
})
.slice(0, config.series_limit);
return ( return (
<CardShell title={config.title} onEdit={onEdit} onDelete={onDelete} empty={rows.length === 0}> <CardShell title={config.title} onEdit={onEdit} onDelete={onDelete} empty={rows.length === 0}>
@@ -57,12 +41,12 @@ export default function TableViz({ config, rows, onEdit, onDelete }: Props) {
<td className="px-2 py-0.5 text-gray-500">{combinator}</td> <td className="px-2 py-0.5 text-gray-500">{combinator}</td>
{config.signal_type !== 'red' && ( {config.signal_type !== 'red' && (
<td className={`px-2 py-0.5 text-right font-mono ${(vals.green ?? 0) < 0 ? 'text-red-400' : 'text-green-400'}`}> <td className={`px-2 py-0.5 text-right font-mono ${(vals.green ?? 0) < 0 ? 'text-red-400' : 'text-green-400'}`}>
{vals.green != null ? formatSI(vals.green) : '--'} {vals.green != null ? formatSI(vals.green, undefined, 0) : '--'}
</td> </td>
)} )}
{config.signal_type !== 'green' && ( {config.signal_type !== 'green' && (
<td className="px-2 py-0.5 text-right font-mono text-orange-400"> <td className="px-2 py-0.5 text-right font-mono text-orange-400">
{vals.red != null ? formatSI(vals.red) : '--'} {vals.red != null ? formatSI(vals.red, undefined, 0) : '--'}
</td> </td>
)} )}
</tr> </tr>

View File

@@ -32,9 +32,9 @@ export default function UpsChart({ config, upsRows, timeMode, onEdit, onDelete }
const xAxis: uPlot.Axis = { const xAxis: uPlot.Axis = {
...AXIS_BASE, ...AXIS_BASE,
...(timeMode === 'real' && { values: timeMode === 'real'
values: (_u, vals) => vals.map(v => v == null ? '' : new Date(v * 1000).toLocaleTimeString()), ? (_u, vals) => vals.map(v => v == null ? '' : new Date(v * 1000).toLocaleTimeString())
}), : (_u, vals) => vals.map(v => v == null ? '' : formatSI(v)),
}; };
return new uPlot({ return new uPlot({

View File

@@ -169,10 +169,11 @@ export function makeSignalsAxes(timeMode: 'real' | 'tick', locale?: string): uPl
return [ return [
{ {
...AXIS_BASE, ...AXIS_BASE,
...(timeMode === 'real' && { values: timeMode === 'real'
values: (_u: uPlot, vals: (number | null)[]) => ? (_u: uPlot, vals: (number | null)[]) =>
vals.map(v => v == null ? '' : new Date(v * 1000).toLocaleTimeString()), vals.map(v => v == null ? '' : new Date(v * 1000).toLocaleTimeString())
}), : (_u: uPlot, vals: (number | null)[]) =>
vals.map(v => v == null ? '' : formatSI(v, locale)),
}, },
{ {
...AXIS_BASE, ...AXIS_BASE,

View File

@@ -4,19 +4,20 @@ const SI_THRESHOLDS = [
{ limit: 1_000, divisor: 1_000, suffix: 'K' }, { limit: 1_000, divisor: 1_000, suffix: 'K' },
] as const; ] as const;
export function formatSI(v: number, locale?: string): string { export function formatSI(v: number, locale?: string, fractionDigits?: number): string {
const abs = Math.abs(v); const abs = Math.abs(v);
const fd = fractionDigits ?? 3;
for (const { limit, divisor, suffix } of SI_THRESHOLDS) { for (const { limit, divisor, suffix } of SI_THRESHOLDS) {
if (abs >= limit) { if (abs >= limit) {
const formatted = new Intl.NumberFormat(locale, { const formatted = new Intl.NumberFormat(locale, {
maximumFractionDigits: 3, maximumFractionDigits: fd,
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format(v / divisor); }).format(v / divisor);
return `${formatted}${suffix}`; return `${formatted}${suffix}`;
} }
} }
return new Intl.NumberFormat(locale, { return new Intl.NumberFormat(locale, {
maximumFractionDigits: 0, maximumFractionDigits: fractionDigits != null ? fractionDigits : 0,
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format(v); }).format(v);
} }