diff --git a/web/components/ChartCard/CardShell.tsx b/web/components/ChartCard/CardShell.tsx
index 5de90db..485dead 100644
--- a/web/components/ChartCard/CardShell.tsx
+++ b/web/components/ChartCard/CardShell.tsx
@@ -46,7 +46,7 @@ export function CardShell({ title, onEdit, onDelete, empty, children, legendCont
{/* Legend scrolls independently, capped at 25% card height */}
>
)}
diff --git a/web/components/ChartCard/SignalsChart.tsx b/web/components/ChartCard/SignalsChart.tsx
index ed1e3a7..c287261 100644
--- a/web/components/ChartCard/SignalsChart.tsx
+++ b/web/components/ChartCard/SignalsChart.tsx
@@ -23,6 +23,7 @@ interface Props {
export default function SignalsChart({ config, rows, sessions, alerts, timeMode, onEdit, onDelete }: Props) {
const { localeMap } = useApp();
+ const locale = typeof navigator !== 'undefined' ? navigator.language : 'de-DE';
const { containerRef, legendRef } = usePlot(
(el, w, h, lRef) => {
@@ -45,7 +46,7 @@ export default function SignalsChart({ config, rows, sessions, alerts, timeMode,
},
},
series: makeSignalsSeries(keys, timeMode, key => resolveName(key, localeMap)),
- axes: makeSignalsAxes(timeMode),
+ axes: makeSignalsAxes(timeMode, locale),
scales: {
x: { time: false },
y: makeYScale(config.y_min ?? null, config.y_max ?? null, config.y_scale),
diff --git a/web/components/ChartCard/TableViz.tsx b/web/components/ChartCard/TableViz.tsx
index 14875ba..d719a64 100644
--- a/web/components/ChartCard/TableViz.tsx
+++ b/web/components/ChartCard/TableViz.tsx
@@ -1,5 +1,6 @@
import { useApp } from '@/lib/context';
import { resolveName } from '@/lib/localization';
+import { formatSI } from '@/lib/formatNumber';
import { CardShell } from './CardShell';
import type { ChartConfig, SignalRow } from '@/lib/types';
@@ -13,11 +14,27 @@ interface Props {
export default function TableViz({ config, rows, onEdit, onDelete }: Props) {
const { localeMap } = useApp();
+ const sortCol = config.signal_type === 'red' ? 'red' : 'green';
+
const latest = new Map();
for (const row of rows) {
latest.set(`${row.combinator}::${row.item_key}`, { green: row.green, red: row.red });
}
- const tableRows = [...latest.entries()].sort((a, b) => (a[1].green ?? 0) - (b[1].green ?? 0));
+ const tableRows = [...latest.entries()]
+ .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 (
@@ -40,12 +57,12 @@ export default function TableViz({ config, rows, onEdit, onDelete }: Props) {
{combinator} |
{config.signal_type !== 'red' && (
- {vals.green?.toLocaleString() ?? '--'}
+ {vals.green != null ? formatSI(vals.green) : '--'}
|
)}
{config.signal_type !== 'green' && (
- {vals.red?.toLocaleString() ?? '--'}
+ {vals.red != null ? formatSI(vals.red) : '--'}
|
)}
diff --git a/web/components/ChartCard/UpsChart.tsx b/web/components/ChartCard/UpsChart.tsx
index e2d4ad0..f19887a 100644
--- a/web/components/ChartCard/UpsChart.tsx
+++ b/web/components/ChartCard/UpsChart.tsx
@@ -4,6 +4,7 @@ import 'uplot/dist/uPlot.min.css';
import uPlot from 'uplot';
import { CardShell } from './CardShell';
import { AXIS_BASE, CURSOR_NO_DRAG, makeYScale } from './plotHelpers';
+import { formatSI } from '@/lib/formatNumber';
import { usePlot } from './usePlot';
import type { ChartConfig, UpsRow } from '@/lib/types';
import type { TimeMode } from '@/lib/types';
@@ -49,7 +50,7 @@ export default function UpsChart({ config, upsRows, timeMode, onEdit, onDelete }
{ label: timeMode === 'tick' ? 'Tick' : 'Time' },
{ label: 'UPS', stroke: '#4ade80', width: 1.5 },
],
- axes: [xAxis, { ...AXIS_BASE }],
+ axes: [xAxis, { ...AXIS_BASE, values: (_u, vals) => vals.map(v => v == null ? '' : formatSI(v)) }],
scales: {
x: { time: false },
y: makeYScale(config.y_min ?? null, config.y_max ?? null, config.y_scale),
diff --git a/web/components/ChartCard/plotHelpers.ts b/web/components/ChartCard/plotHelpers.ts
index 84d4bd0..c7e1512 100644
--- a/web/components/ChartCard/plotHelpers.ts
+++ b/web/components/ChartCard/plotHelpers.ts
@@ -1,5 +1,6 @@
import uPlot from 'uplot';
import type { ChartConfig } from '@/lib/types';
+import { formatSI } from '@/lib/formatNumber';
// --- Color helpers ---
@@ -164,7 +165,7 @@ export function makeSignalsSeries(
];
}
-export function makeSignalsAxes(timeMode: 'real' | 'tick'): uPlot.Axis[] {
+export function makeSignalsAxes(timeMode: 'real' | 'tick', locale?: string): uPlot.Axis[] {
return [
{
...AXIS_BASE,
@@ -173,6 +174,10 @@ export function makeSignalsAxes(timeMode: 'real' | 'tick'): uPlot.Axis[] {
vals.map(v => v == null ? '' : new Date(v * 1000).toLocaleTimeString()),
}),
},
- { ...AXIS_BASE },
+ {
+ ...AXIS_BASE,
+ values: (_u: uPlot, vals: (number | null)[]) =>
+ vals.map(v => v == null ? '' : formatSI(v, locale)),
+ },
];
}
\ No newline at end of file
diff --git a/web/lib/formatNumber.ts b/web/lib/formatNumber.ts
new file mode 100644
index 0000000..5eda4e0
--- /dev/null
+++ b/web/lib/formatNumber.ts
@@ -0,0 +1,22 @@
+const SI_THRESHOLDS = [
+ { limit: 1_000_000_000, divisor: 1_000_000_000, suffix: 'G' },
+ { limit: 1_000_000, divisor: 1_000_000, suffix: 'M' },
+ { limit: 1_000, divisor: 1_000, suffix: 'K' },
+] as const;
+
+export function formatSI(v: number, locale?: string): string {
+ const abs = Math.abs(v);
+ for (const { limit, divisor, suffix } of SI_THRESHOLDS) {
+ if (abs >= limit) {
+ const formatted = new Intl.NumberFormat(locale, {
+ maximumFractionDigits: 3,
+ minimumFractionDigits: 0,
+ }).format(v / divisor);
+ return `${formatted}${suffix}`;
+ }
+ }
+ return new Intl.NumberFormat(locale, {
+ maximumFractionDigits: 0,
+ minimumFractionDigits: 0,
+ }).format(v);
+}
diff --git a/web/package-lock.json b/web/package-lock.json
index 36ebd5c..dbb6334 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -986,6 +986,7 @@
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
@@ -998,6 +999,7 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1795,6 +1797,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"pg-connection-string": "^2.12.0",
"pg-pool": "^3.13.0",
@@ -1968,6 +1971,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -1977,6 +1981,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},