stats: bench the node-types donut until a real endpoint exists

The donut counted types among the top-10 nodes only, so the center
read '10 NODES' as if the region had ten nodes, and its center text
never sat quite right (ECharts titles anchor the main text, the
subtext hangs below). There's no /stats/node-types endpoint to feed it
properly yet (ticket filed), so the card shows 'Coming soon' and the
donut builder goes away rather than lingering as dead code.
This commit is contained in:
MrAlders0n
2026-06-10 08:44:13 -04:00
parent d1ff74cc1f
commit 9c93dac0f6
4 changed files with 9 additions and 84 deletions
+8 -16
View File
@@ -2,7 +2,7 @@ import { useMemo } from "react";
import { formatCount } from "../../lib/formatters";
import { useChartColors, type ChartColors } from "./chartTheme";
import { useStatsOverview, useStatsObservations, usePayloadBreakdown, useTopNodes, useTopObservers, useRadioPresets, useScopes } from "./useStats";
import { observationsAreaOption, leaderboardOption, typeBarOption, donutOption } from "./chartOptions";
import { observationsAreaOption, leaderboardOption, typeBarOption } from "./chartOptions";
import { Card, ChartCard, StatCard } from "./cards";
import { useLiveOverview } from "./useLiveStats";
import { aggregatePresets, formatPreset } from "./transforms";
@@ -90,20 +90,6 @@ export function MeshTab({ range, onSelectObserver, wsManager }: MeshTabProps) {
[observerIds, onSelectObserver],
);
const nodeTypeData = useMemo(() => {
const counts = new Map<string, number>();
for (const n of topNodes.data ?? []) counts.set(n.nodeTypeName, (counts.get(n.nodeTypeName) ?? 0) + 1);
return [...counts.entries()]
.map(([name, value]) => ({ name, value, color: nodeTypeColor(name, colors) }))
.sort((a, b) => b.value - a.value);
}, [topNodes.data, colors]);
const nodeTypeTotal = useMemo(() => nodeTypeData.reduce((a, d) => a + d.value, 0), [nodeTypeData]);
// few categories, so a donut reads fine here — payload types stays a bar chart (10+ categories)
const nodeTypeOption = useMemo(
() => donutOption(nodeTypeData, colors, String(nodeTypeTotal), "NODES"),
[nodeTypeData, nodeTypeTotal, colors],
);
const presetRows = useMemo(
() => aggregatePresets(radioPresets.data ?? []).slice(0, 8).map((r) => ({ name: formatPreset(r.preset), value: r.value, color: colors.primary })),
[radioPresets.data, colors],
@@ -151,7 +137,13 @@ export function MeshTab({ range, onSelectObserver, wsManager }: MeshTabProps) {
isEmpty={payloadItems.length === 0}
/>
<ChartCard title={<>Top observers · {range}</>} height={208} option={observersOption} isLoading={topObservers.isLoading} isError={topObservers.isError} isEmpty={observerRows.length === 0} onEvents={observerEvents} />
<ChartCard title="Node types · top" height={208} option={nodeTypeOption} isLoading={topNodes.isLoading} isError={topNodes.isError} isEmpty={nodeTypeData.length === 0} />
{/* needs a /stats/node-types endpoint (ticket filed) — the old donut counted types among
the top-10 nodes only, which read as the region's whole population */}
<Card title="Node types">
<div className="flex h-[208px] items-center justify-center font-mono text-[11px] text-text-dim">
Coming soon
</div>
</Card>
<ChartCard title="Radio presets" height={208} option={presetsOption} isLoading={radioPresets.isLoading} isError={radioPresets.isError} isEmpty={presetRows.length === 0} />
<Card title={<>Scopes · all regions</>}>
-51
View File
@@ -131,57 +131,6 @@ export function leaderboardOption(
};
}
// Donut for small category sets (node types — usually 3 or 4). The center total is a title block
// anchored at the ring's x with textAlign center, which ECharts centers properly — the old graphic
// text anchored its LEFT edge there and clipped against the ring.
export function donutOption(
items: { name: string; value: number; color?: string }[],
c: ChartColors,
centerValue: string,
centerLabel: string,
): EChartsOption {
return {
animation: false,
backgroundColor: "transparent",
tooltip: { trigger: "item", ...tooltipStyle(c), formatter: "{b}: {c} ({d}%)" },
title: {
text: centerValue,
subtext: centerLabel,
// anchor the block's center exactly on the pie's center (35%, 50%) — textAlign/-VerticalAlign
// make left/top the anchor point instead of the block's top-left corner
left: "35%",
top: "50%",
textAlign: "center",
textVerticalAlign: "middle",
itemGap: 2,
textStyle: { color: c.textBright, fontFamily: MONO, fontSize: 21, fontWeight: 700 },
subtextStyle: { color: c.textMuted, fontFamily: MONO, fontSize: 9 },
},
legend: {
orient: "vertical",
right: 10,
top: "middle",
itemWidth: 9,
itemHeight: 9,
itemGap: 7,
textStyle: { color: c.textNormal, fontFamily: MONO, fontSize: 10 },
inactiveColor: c.textDim,
},
series: [
{
type: "pie",
radius: ["46%", "68%"],
center: ["35%", "50%"],
avoidLabelOverlap: false,
itemStyle: { borderColor: c.bgSurface, borderWidth: 2, borderRadius: 4 },
label: { show: false },
emphasis: { scaleSize: 5 },
data: items.map((it, i) => ({ name: it.name, value: it.value, itemStyle: { color: it.color ?? c.series[i % c.series.length] } })),
},
],
};
}
// Vertical bars for the payload-type breakdown. Replaced the old donut: with 10+ slivers the legend
// needed scrolling, names truncated, and thin slices couldn't be compared by eye — bars label every
// category inline and need no legend at all.
-2
View File
@@ -6,7 +6,6 @@ import * as echarts from "echarts/core";
import { LineChart, BarChart, PieChart, GaugeChart } from "echarts/charts";
import {
GridComponent,
TitleComponent,
TooltipComponent,
LegendComponent,
GraphicComponent,
@@ -21,7 +20,6 @@ echarts.use([
PieChart,
GaugeChart,
GridComponent,
TitleComponent,
TooltipComponent,
LegendComponent,
GraphicComponent,
+1 -15
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any -- poking into loose ECharts option shapes */
import { describe, it, expect } from "vitest";
import { typeBarOption, leaderboardOption, donutOption } from "../../../src/features/stats/chartOptions";
import { typeBarOption, leaderboardOption } from "../../../src/features/stats/chartOptions";
import type { ChartColors } from "../../../src/features/stats/chartTheme";
const colors: ChartColors = {
@@ -51,20 +51,6 @@ describe("typeBarOption", () => {
});
});
describe("donutOption", () => {
it("centers the total on the ring via a textAlign-centered title at the pie's x", () => {
const opt = donutOption([{ name: "repeater", value: 3 }], colors, "10", "NODES") as Record<string, any>;
// the old graphic-text approach anchored its left edge at the ring center and clipped
expect(opt.graphic).toBeUndefined();
expect(opt.title.textAlign).toBe("center");
expect(opt.title.textVerticalAlign).toBe("middle");
expect(opt.title.text).toBe("10");
// anchor must sit exactly on the pie's center point
expect(opt.title.left).toBe(opt.series[0].center[0]);
expect(opt.title.top).toBe(opt.series[0].center[1]);
});
});
describe("leaderboardOption", () => {
it("left-aligns names at the card edge and truncates long ones to the label gutter", () => {
const rows = [{ name: "A very long observer name that overflows", value: 5, color: "#abc" }];