From 9c93dac0f6b65b4f7301b6024d907ec220f384da Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Wed, 10 Jun 2026 08:44:13 -0400 Subject: [PATCH] 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. --- src/features/stats/MeshTab.tsx | 24 ++++------ src/features/stats/chartOptions.ts | 51 ---------------------- src/features/stats/echarts-setup.ts | 2 - tests/features/stats/chart-options.test.ts | 16 +------ 4 files changed, 9 insertions(+), 84 deletions(-) diff --git a/src/features/stats/MeshTab.tsx b/src/features/stats/MeshTab.tsx index 2c4b5f1..5b4440c 100644 --- a/src/features/stats/MeshTab.tsx +++ b/src/features/stats/MeshTab.tsx @@ -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(); - 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} /> Top observers · {range}} height={208} option={observersOption} isLoading={topObservers.isLoading} isError={topObservers.isError} isEmpty={observerRows.length === 0} onEvents={observerEvents} /> - + {/* 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 */} + +
+ Coming soon +
+
Scopes · all regions}> diff --git a/src/features/stats/chartOptions.ts b/src/features/stats/chartOptions.ts index b1c6d05..284fc94 100644 --- a/src/features/stats/chartOptions.ts +++ b/src/features/stats/chartOptions.ts @@ -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. diff --git a/src/features/stats/echarts-setup.ts b/src/features/stats/echarts-setup.ts index 319b6f5..e703520 100644 --- a/src/features/stats/echarts-setup.ts +++ b/src/features/stats/echarts-setup.ts @@ -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, diff --git a/tests/features/stats/chart-options.test.ts b/tests/features/stats/chart-options.test.ts index 9f56bcd..031100e 100644 --- a/tests/features/stats/chart-options.test.ts +++ b/tests/features/stats/chart-options.test.ts @@ -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; - // 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" }];