Stable packet analytics coloring. Closes #246.

This commit is contained in:
Jack Kingsman
2026-05-13 16:15:41 -07:00
parent f1eca53625
commit e95acecbfb
+38 -9
View File
@@ -31,7 +31,29 @@ import { createDecoderOptions } from '../utils/rawPacketInspector';
import { getContactDisplayName } from '../utils/pubkey'; import { getContactDisplayName } from '../utils/pubkey';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const TIMELINE_FILL_COLORS = ['#0ea5e9', '#10b981', '#f59e0b', '#f43f5e', '#8b5cf6'];
/**
* Build a stable name→color mapping so the same type always gets the same
* color regardless of sort order or appearance order.
*/
function buildColorMap(names: readonly string[]): Map<string, string> {
const map = new Map<string, string>();
for (let i = 0; i < names.length; i++) {
map.set(names[i], TIMELINE_FILL_COLORS[i % TIMELINE_FILL_COLORS.length]);
}
return map;
}
function colorForIndex(index: number, colorMap?: Map<string, string>, name?: string): string {
if (colorMap && name && colorMap.has(name)) {
return colorMap.get(name)!;
}
return TIMELINE_FILL_COLORS[index % TIMELINE_FILL_COLORS.length];
}
const KNOWN_PAYLOAD_TYPE_SET = new Set<string>(KNOWN_PAYLOAD_TYPES); const KNOWN_PAYLOAD_TYPE_SET = new Set<string>(KNOWN_PAYLOAD_TYPES);
const PAYLOAD_TYPE_COLOR_MAP = buildColorMap(KNOWN_PAYLOAD_TYPES);
function getPacketTypeName( function getPacketTypeName(
packet: RawPacket, packet: RawPacket,
@@ -74,8 +96,6 @@ const WINDOW_LABELS: Record<RawPacketStatsWindow, string> = {
session: 'Session', session: 'Session',
}; };
const TIMELINE_FILL_COLORS = ['#0ea5e9', '#10b981', '#f59e0b', '#f43f5e', '#8b5cf6'];
function formatTimestamp(timestampMs: number): string { function formatTimestamp(timestampMs: number): string {
return new Date(timestampMs).toLocaleString([], { return new Date(timestampMs).toLocaleString([], {
month: 'short', month: 'short',
@@ -245,11 +265,13 @@ function RankedBars({
items, items,
emptyLabel, emptyLabel,
formatter, formatter,
colorMap,
}: { }: {
title: string; title: string;
items: RankedPacketStat[]; items: RankedPacketStat[];
emptyLabel: string; emptyLabel: string;
formatter?: (item: RankedPacketStat) => string; formatter?: (item: RankedPacketStat) => string;
colorMap?: Map<string, string>;
}) { }) {
const data = items.map((item) => ({ const data = items.map((item) => ({
name: item.label, name: item.label,
@@ -289,8 +311,8 @@ function RankedBars({
formatter={(_v: any, _n: any, props: any) => [props.payload.detail, null]} formatter={(_v: any, _n: any, props: any) => [props.payload.detail, null]}
/> />
<Bar dataKey="value" radius={[0, 4, 4, 0]} maxBarSize={16}> <Bar dataKey="value" radius={[0, 4, 4, 0]} maxBarSize={16}>
{data.map((_, i) => ( {data.map((entry, i) => (
<Cell key={i} fill={TIMELINE_FILL_COLORS[i % TIMELINE_FILL_COLORS.length]} /> <Cell key={i} fill={colorForIndex(i, colorMap, entry.name)} />
))} ))}
</Bar> </Bar>
</BarChart> </BarChart>
@@ -367,7 +389,13 @@ function NeighborList({
); );
} }
function TimelineChart({ bins }: { bins: PacketTimelineBin[] }) { function TimelineChart({
bins,
colorMap,
}: {
bins: PacketTimelineBin[];
colorMap: Map<string, string>;
}) {
const typeOrder = Array.from(new Set(bins.flatMap((bin) => Object.keys(bin.countsByType)))).slice( const typeOrder = Array.from(new Set(bins.flatMap((bin) => Object.keys(bin.countsByType)))).slice(
0, 0,
TIMELINE_FILL_COLORS.length TIMELINE_FILL_COLORS.length
@@ -386,11 +414,11 @@ function TimelineChart({ bins }: { bins: PacketTimelineBin[] }) {
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-foreground">Traffic Timeline</h3> <h3 className="text-sm font-semibold text-foreground">Traffic Timeline</h3>
<div className="flex flex-wrap justify-end gap-2 text-[0.6875rem] text-muted-foreground"> <div className="flex flex-wrap justify-end gap-2 text-[0.6875rem] text-muted-foreground">
{typeOrder.map((type, i) => ( {typeOrder.map((type) => (
<span key={type} className="inline-flex items-center gap-1"> <span key={type} className="inline-flex items-center gap-1">
<span <span
className="h-2 w-2 rounded-full" className="h-2 w-2 rounded-full"
style={{ backgroundColor: TIMELINE_FILL_COLORS[i] }} style={{ backgroundColor: colorMap.get(type) ?? TIMELINE_FILL_COLORS[0] }}
/> />
<span>{type}</span> <span>{type}</span>
</span> </span>
@@ -422,7 +450,7 @@ function TimelineChart({ bins }: { bins: PacketTimelineBin[] }) {
key={type} key={type}
dataKey={type} dataKey={type}
stackId="packets" stackId="packets"
fill={TIMELINE_FILL_COLORS[i]} fill={colorMap.get(type) ?? TIMELINE_FILL_COLORS[0]}
radius={i === typeOrder.length - 1 ? [2, 2, 0, 0] : undefined} radius={i === typeOrder.length - 1 ? [2, 2, 0, 0] : undefined}
/> />
))} ))}
@@ -747,7 +775,7 @@ export function RawPacketFeedView({
</div> </div>
<div className="mt-4"> <div className="mt-4">
<TimelineChart bins={stats.timeline} /> <TimelineChart bins={stats.timeline} colorMap={PAYLOAD_TYPE_COLOR_MAP} />
</div> </div>
<div className="md:columns-2 md:gap-4"> <div className="md:columns-2 md:gap-4">
@@ -755,6 +783,7 @@ export function RawPacketFeedView({
title="Packet Types" title="Packet Types"
items={stats.payloadBreakdown} items={stats.payloadBreakdown}
emptyLabel="No packets in this window yet." emptyLabel="No packets in this window yet."
colorMap={PAYLOAD_TYPE_COLOR_MAP}
/> />
<RankedBars <RankedBars