feat: y-axis SI prefix, hide tick from legend, fix table sort
This commit is contained in:
@@ -46,7 +46,7 @@ export function CardShell({ title, onEdit, onDelete, empty, children, legendCont
|
||||
{/* Legend scrolls independently, capped at 25% card height */}
|
||||
<div
|
||||
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"
|
||||
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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<string, { green?: number; red?: number }>();
|
||||
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 (
|
||||
<CardShell title={config.title} onEdit={onEdit} onDelete={onDelete} empty={rows.length === 0}>
|
||||
@@ -40,12 +57,12 @@ export default function TableViz({ config, rows, onEdit, onDelete }: Props) {
|
||||
<td className="px-2 py-0.5 text-gray-500">{combinator}</td>
|
||||
{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'}`}>
|
||||
{vals.green?.toLocaleString() ?? '--'}
|
||||
{vals.green != null ? formatSI(vals.green) : '--'}
|
||||
</td>
|
||||
)}
|
||||
{config.signal_type !== 'green' && (
|
||||
<td className="px-2 py-0.5 text-right font-mono text-orange-400">
|
||||
{vals.red?.toLocaleString() ?? '--'}
|
||||
{vals.red != null ? formatSI(vals.red) : '--'}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user