Files
homarr/packages/widgets/src/unifi-os/throughput-chart.tsx
T
2026-09-13 16:26:50 +02:00

138 lines
4.7 KiB
TypeScript

"use client";
import { AreaChart } from "@mantine/charts";
import { Box, Group, Paper, Stack, Text } from "@mantine/core";
import type { ThroughputPoint } from "./throughput-history";
/**
* Series colours.
*
* Not copied from the `systemResources` network chart, which pairs `orange.5`
* with `yellow.5`: that pair scores a normal-vision ΔE of 12.1, below the
* legibility floor of 15, so even readers with full colour vision struggle to
* tell the two lines apart. This pair was validated instead, and passes every
* check — lightness band, chroma, colour-vision-deficiency separation,
* normal-vision separation and surface contrast — against BOTH the light and
* the dark chart surface, so one pair serves both themes.
*/
const DOWN_COLOR = "blue.7";
const UP_COLOR = "orange.8";
/**
* A floor, not a height: the chart now takes the height the card has left over.
* `ResponsiveContainer` measures its parent and renders nothing at all when that
* parent collapses to zero, so the wrapper keeps a small non-zero minimum — on a
* very short tile the area stays legible instead of disappearing.
*/
const CHART_MIN_HEIGHT = 32;
/**
* A dot carries the series identity so the figure beside it can stay in muted
* ink: colour is never the only thing distinguishing up from down — the arrow
* glyph and the label do it too.
*/
const SeriesKey = ({ color, label }: { color: string; label: string }) => (
<Group gap={4} wrap="nowrap">
<Text c={color} size="xs" lh={1} aria-hidden>
●
</Text>
<Text c="dimmed" size="xs" ff="monospace" lh={1}>
{label}
</Text>
</Group>
);
/**
* Defined at module level rather than inline in the `content` prop: recharts
* clones this element and injects `payload`, so it keeps a stable identity
* across renders. The repo's other chart tooltips pass a closure instead and
* each carries a `no-unstable-nested-components` warning for it.
*/
const ThroughputTooltip = ({
payload,
formatRate,
upName,
downName,
}: {
payload?: { name?: string | number; value?: string | number }[];
formatRate: (bitsPerSecond: number) => string;
upName: string;
downName: string;
}) => {
if (!payload || payload.length === 0) return null;
return (
<Paper px="xs" py={4} shadow="md" withBorder>
<Stack gap={2}>
{payload.map((item) => (
<Text key={String(item.name)} size="xs" c="dimmed" ff="monospace">
{item.name === "up" ? `↑ ${upName}` : `↓ ${downName}`} {formatRate(Number(item.value))}
</Text>
))}
</Stack>
</Paper>
);
};
export interface ThroughputLegendProps {
/** Rendered beside the up marker, already formatted. */
upLabel: string;
/** Rendered beside the down marker, already formatted. */
downLabel: string;
}
/**
* Sits on the card's "Internet" header row rather than above the graph, so the
* figures read as a heading and detach from the area they summarise. Exported
* from here instead of being rewritten there: the dots take the very constants
* the chart's series use, so the two can never drift apart.
*/
export const ThroughputLegend = ({ upLabel, downLabel }: ThroughputLegendProps) => (
<Group gap="sm" wrap="nowrap" justify="flex-end">
<SeriesKey color={DOWN_COLOR} label={`↓ ${downLabel}`} />
<SeriesKey color={UP_COLOR} label={`↑ ${upLabel}`} />
</Group>
);
export interface ThroughputChartProps {
history: ThroughputPoint[];
/** Formats a raw bits-per-second value for the tooltip. */
formatRate: (bitsPerSecond: number) => string;
upName: string;
downName: string;
}
/**
* The wrapper is the flex child that claims the card's leftover height, and the
* chart fills it with `h="100%"`. Mantine turns that into an inline
* `height: 100%` on the chart root, which recharts' `ResponsiveContainer` then
* measures; a percentage resolves here because flex layout gives this wrapper a
* definite used height, the card above it being a flex item of a `h="100%"`
* stack inside a tile of fixed height.
*/
export const ThroughputChart = ({ history, formatRate, upName, downName }: ThroughputChartProps) => (
<Box style={{ flex: 1, minHeight: CHART_MIN_HEIGHT }}>
<AreaChart
h="100%"
data={history}
dataKey="index"
series={[
{ name: "down", label: downName, color: DOWN_COLOR },
{ name: "up", label: upName, color: UP_COLOR },
]}
curveType="monotone"
strokeWidth={2}
withDots={false}
withXAxis={false}
withYAxis={false}
gridAxis="none"
withLegend={false}
tooltipAnimationDuration={150}
tooltipProps={{
content: <ThroughputTooltip formatRate={formatRate} upName={upName} downName={downName} />,
}}
/>
</Box>
);