b7019b33ac
Consistent graph scaling across every line-chart widget. A new shared frontend/src/lib/metricFormat.ts picks a decimal prefix (kB/MB/GB, kbps/Mbps, Gbps, …) from the series magnitude and formats values; LineSeriesChart accepts unit + scale and formats both the Y-axis ticks and the tooltip with the SAME prefix (one consistent unit per axis). MetricChartWidget (Prometheus) and QbittorrentSpeedWidget pass the widget config through; qBit speed defaults to bytes/sec → MB/s. WidgetConfigDialog now renders `enum` schema fields as a <Select> dropdown, so the backend's unit/scale Literal enums become consistent pickers in every graph widget's config (and any future enum option). Decimal (x1000) prefixes by default (matches Mbps/MB/s/Grafana). Tests: 13 new metricFormat tests (auto/fixed scaling, percent, seconds, nulls, trailing-zero trimming). 179/179 frontend tests pass; tsc + ESLint clean.
127 lines
3.0 KiB
TypeScript
127 lines
3.0 KiB
TypeScript
import {
|
|
LineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
} from "recharts";
|
|
import {
|
|
type MetricUnit,
|
|
type MetricScale,
|
|
metricScaleInfo,
|
|
formatScaled,
|
|
} from "../lib/metricFormat";
|
|
|
|
export interface SeriesPoint {
|
|
t: number;
|
|
v: number | null;
|
|
}
|
|
|
|
export interface ChartSeries {
|
|
label: string;
|
|
points: SeriesPoint[];
|
|
}
|
|
|
|
/** Merge multiple time-series into a single recharts-friendly array. */
|
|
function mergeSeries(series: ChartSeries[]): Record<string, unknown>[] {
|
|
const map = new Map<number, Record<string, unknown>>();
|
|
for (const s of series) {
|
|
for (const p of s.points) {
|
|
const existing = map.get(p.t) ?? { time: p.t };
|
|
existing[s.label] = p.v;
|
|
map.set(p.t, existing);
|
|
}
|
|
}
|
|
return [...map.values()].sort(
|
|
(a, b) => (a.time as number) - (b.time as number),
|
|
);
|
|
}
|
|
|
|
function formatTime(ms: number): string {
|
|
return new Date(ms).toLocaleTimeString([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
const CHART_COLORS = [
|
|
"var(--color-chart-1)",
|
|
"var(--color-chart-2)",
|
|
"var(--color-chart-3)",
|
|
"var(--color-chart-4)",
|
|
"var(--color-chart-5)",
|
|
];
|
|
|
|
interface LineSeriesChartProps {
|
|
series: ChartSeries[];
|
|
height?: number;
|
|
/** Display unit for the Y axis + tooltip (drives decimal-prefix scaling). */
|
|
unit?: MetricUnit;
|
|
/** "auto" picks a prefix from the data magnitude; k/m/g/t force one. */
|
|
scale?: MetricScale;
|
|
}
|
|
|
|
/** Shared recharts line-chart renderer used by PrometheusChart + qBit speed widgets. */
|
|
export function LineSeriesChart({
|
|
series,
|
|
height = 300,
|
|
unit = "none",
|
|
scale = "auto",
|
|
}: LineSeriesChartProps) {
|
|
// Choose ONE (divisor, suffix) from the series magnitude so the axis and
|
|
// tooltip stay consistent (e.g. all values shown in MB/s).
|
|
const maxAbs = series.reduce((m, s) => {
|
|
for (const p of s.points) {
|
|
const v = p.v == null ? 0 : Math.abs(p.v);
|
|
if (v > m) m = v;
|
|
}
|
|
return m;
|
|
}, 0);
|
|
const scaleInfo = metricScaleInfo(maxAbs, unit, scale);
|
|
const fmt = (v: number | null | undefined) =>
|
|
formatScaled(v, scaleInfo, unit);
|
|
|
|
return (
|
|
<ResponsiveContainer width="100%" height={height}>
|
|
<LineChart data={mergeSeries(series)}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis
|
|
dataKey="time"
|
|
tickFormatter={formatTime}
|
|
tick={{ fontSize: 11 }}
|
|
className="fill-muted-foreground"
|
|
/>
|
|
<YAxis
|
|
tickFormatter={fmt}
|
|
tick={{ fontSize: 11 }}
|
|
width={56}
|
|
className="fill-muted-foreground"
|
|
/>
|
|
<Tooltip
|
|
labelFormatter={(label) => formatTime(Number(label))}
|
|
formatter={(value) => fmt(Number(value))}
|
|
contentStyle={{
|
|
backgroundColor: "var(--color-popover)",
|
|
border: "1px solid var(--color-border)",
|
|
borderRadius: "0.5rem",
|
|
color: "var(--color-popover-foreground)",
|
|
}}
|
|
/>
|
|
{series.map((s, i) => (
|
|
<Line
|
|
key={s.label}
|
|
type="monotone"
|
|
dataKey={s.label}
|
|
stroke={CHART_COLORS[i % CHART_COLORS.length]}
|
|
dot={false}
|
|
strokeWidth={2}
|
|
connectNulls
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
);
|
|
}
|