import LineChartDefault from "@/components/charts/line-chart" import type { DataPoint } from "@/components/charts/line-chart" import { decimalString, formatMicroseconds, toFixedFloat } from "@/lib/utils" import { useLingui } from "@lingui/react/macro" import { ChartCard, FilterBar } from "../chart-card" import type { ChartData, NetworkProbeRecord, NetworkProbeStatsRecord } from "@/types" import { useMemo } from "react" import { atom } from "nanostores" import { useStore } from "@nanostores/react" const $filter = atom("") type ProbeChartProps = { probeStats: NetworkProbeStatsRecord[] grid?: boolean probes: NetworkProbeRecord[] chartData: ChartData empty: boolean showFilter?: boolean } type ProbeChartBaseProps = ProbeChartProps & { valueIndex: number title: string description: string tickFormatter: (value: number) => string contentFormatter: ({ value }: { value: number | string }) => string | number domain?: [number | "auto", number | "auto"] } function ProbeChart({ probeStats, grid, probes, chartData, empty, valueIndex, title, description, tickFormatter, contentFormatter, domain, showFilter = probes.length > 1, }: ProbeChartBaseProps) { const storedFilter = useStore($filter) const filter = showFilter ? storedFilter : "" const { dataPoints, visibleKeys } = useMemo(() => { const sortedProbes = [...probes].sort((a, b) => b.resAvg1h - a.resAvg1h) const count = sortedProbes.length const points: DataPoint[] = [] const visibleIDs: string[] = [] const filterTerms = filter ? filter .toLowerCase() .split(" ") .filter((term) => term.length > 0) : [] const dot = chartData.chartTime === "1m" for (let i = 0; i < count; i++) { const p = sortedProbes[i] const label = p.name || p.target const filtered = filterTerms.length > 0 && !filterTerms.some((term) => label.toLowerCase().includes(term)) if (filtered) { continue } visibleIDs.push(p.id) points.push({ order: i, label, dataKey: (record: NetworkProbeStatsRecord) => record.stats?.[p.id]?.[valueIndex] ?? "-", dot, color: count <= 5 ? i + 1 : `hsl(${(i * 360) / count}, var(--chart-saturation), var(--chart-lightness))`, }) } return { dataPoints: points, visibleKeys: visibleIDs } }, [probes, filter, valueIndex, chartData.chartTime]) const filteredProbeStats = useMemo(() => { if (!visibleKeys.length) return probeStats return probeStats.filter((record) => visibleKeys.some((id) => record.stats?.[id] != null)) }, [probeStats, visibleKeys]) const legend = dataPoints.length < 10 && dataPoints.length > 1 return ( : undefined} empty={empty} title={title} description={description} grid={grid} > ) } export function ResponseChart({ probeStats, grid, probes, chartData, empty }: ProbeChartProps) { const { t } = useLingui() return ( formatMicroseconds(value, false)} contentFormatter={({ value }) => { if (typeof value !== "number") { return value } return formatMicroseconds(value) }} /> ) } interface AvgMinMaxResponseChartProps { probeStats: NetworkProbeStatsRecord[] probe: NetworkProbeRecord | null chartData: ChartData empty: boolean } export function AvgMinMaxResponseChart({ probeStats, probe, chartData, empty }: AvgMinMaxResponseChartProps) { const { t } = useLingui() const { chartTime } = chartData const hasLongInterval = (probe?.interval ?? 61) > 60 // only one probe is relevant for this chart const dataPoints: DataPoint[] = useMemo(() => { const dataFn = (index: number) => (record: NetworkProbeStatsRecord) => record.stats?.[probe?.id ?? ""]?.[index] ?? "-" const avgPoint = { label: "Avg", dataKey: dataFn(0), color: 1, order: 0, } if (chartTime === "1m" || (hasLongInterval && chartTime === "1h")) { // avg, min, max are all the same for 1m interval, so just show avg return [avgPoint] } return [ { label: "Max", dataKey: dataFn(2), color: 3, order: 0, }, avgPoint, { label: "Min", dataKey: dataFn(1), color: 2, order: 2, }, ] }, [chartTime, hasLongInterval]) const data = useMemo(() => { if (!probe) return [] return probeStats.filter((record) => record.stats && probe.id in record.stats) }, [probe, probeStats]) const legend = dataPoints.length > 1 return ( formatMicroseconds(value, false)} contentFormatter={({ value }) => { if (typeof value !== "number") { return value } return formatMicroseconds(value) }} /> ) } export function LossChart({ probeStats, grid, probes, chartData, empty }: ProbeChartProps) { const { t } = useLingui() return ( `${toFixedFloat(value, value >= 10 ? 0 : 1)}%`} contentFormatter={({ value }) => { if (typeof value !== "number") { return value } return `${decimalString(value, 2)}%` }} /> ) }