mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-11 03:03:03 +02:00
Stable packet analytics coloring. Closes #246.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user