Packet diagrams from data
RFC-style header layouts from a flat field list - fields flow across 32-bit rows, and a field without a bit count fills the rest of its row via the stack's total sizing mode
import { type Field, PacketDiagram } from "./PacketDiagram";
// RFC-style header layouts as plain field lists; a field without bits
// fills the rest of its row.
const tcpHeader: Field[] = [
{ name: "Source Port", bits: 16 },
{ name: "Destination Port", bits: 16 },
{ name: "Sequence Number", bits: 32 },
{ name: "Acknowledgment Number", bits: 32 },
{ name: "Offset", bits: 4 },
{ name: "Rsvd", bits: 3 },
{ name: "Flags", bits: 9 },
{ name: "Window", bits: 16 },
{ name: "Checksum", bits: 16 },
{ name: "Urgent Pointer", bits: 16 },
{ name: "Options / Padding" },
];
const udpHeader: Field[] = [
{ name: "Source Port", bits: 16 },
{ name: "Destination Port", bits: 16 },
{ name: "Length", bits: 16 },
{ name: "Checksum", bits: 16 },
{ name: "Data" },
];
export function PacketDiagramExample() {
return (
<div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
<div>
<h3 style={{ margin: "0 0 0.5rem" }}>TCP header</h3>
<PacketDiagram fields={tcpHeader} />
</div>
<div>
<h3 style={{ margin: "0 0 0.5rem" }}>UDP header</h3>
<PacketDiagram fields={udpHeader} />
</div>
</div>
);
}
TCP header
UDP header
How the PacketDiagram component works
Rows are stackHs with a fixed total width; sized cells take bits x bitWidth, unsized cells share the leftover; names and bit ranges are aligned onto each cell
import { Graphic } from "@modular-svg/react";
export type Field = {
name: string;
/** field width in bits; omit to fill the rest of the row */
bits?: number;
};
export type PacketDiagramProps = {
fields: Field[];
bitsPerRow?: number;
/** pixels per bit; rows are bitsPerRow * bitWidth wide */
bitWidth?: number;
rowHeight?: number;
fontSize?: number;
};
type Cell = {
name: string;
startBit: number;
/** undefined fills the remainder of the row */
bits?: number;
continued: boolean;
};
// Flow fields into rows, splitting any field that crosses a row boundary
function toRows(fields: Field[], bitsPerRow: number): Cell[][] {
const rows: Cell[][] = [[]];
let bit = 0;
for (const field of fields) {
if (field.bits === undefined) {
rows[rows.length - 1].push({
name: field.name,
startBit: bit,
continued: false,
});
rows.push([]);
bit = Math.ceil((bit + 1) / bitsPerRow) * bitsPerRow;
continue;
}
let remaining = field.bits;
let continued = false;
while (remaining > 0) {
const space = bitsPerRow - (bit % bitsPerRow);
const take = Math.min(remaining, space);
rows[rows.length - 1].push({
name: field.name,
startBit: bit,
bits: take,
continued,
});
bit += take;
remaining -= take;
if (bit % bitsPerRow === 0) rows.push([]);
continued = true;
}
}
if (rows[rows.length - 1].length === 0) rows.pop();
return rows;
}
export function PacketDiagram({
fields,
bitsPerRow = 32,
bitWidth = 15,
rowHeight = 30,
fontSize = 12,
}: PacketDiagramProps) {
const rows = toRows(fields, bitsPerRow);
const cellId = (r: number, c: number) => `cell-${r}-${c}`;
return (
<Graphic
style={{
border: "1px solid #ddd",
borderRadius: "8px",
display: "inline-block",
}}
margin={12}
>
<group>
<stackV spacing={0} alignment="left">
{rows.map((row, r) => (
// Each row is exactly bitsPerRow * bitWidth wide: sized cells
// take bits * bitWidth, unsized cells share the leftover via
// the stack's total mode
<stackH
key={`row-${r}`}
spacing={0}
total={bitsPerRow * bitWidth}
alignment="top"
>
{row.map((cell, c) => (
<rect
key={cellId(r, c)}
width={
cell.bits === undefined ? undefined : cell.bits * bitWidth
}
height={rowHeight}
fill="#EDF2FB"
stroke="#333"
stroke-width={1}
/>
))}
</stackH>
))}
</stackV>
{rows.map((row, r) =>
row.map((cell, c) => {
const endBit =
cell.bits === undefined
? `${bitsPerRow - 1 + Math.floor(cell.startBit / bitsPerRow) * bitsPerRow}`
: `${cell.startBit + cell.bits - 1}`;
const range =
cell.bits === 1
? `${cell.startBit}`
: `${cell.startBit}-${endBit}`;
return (
<group key={`${cellId(r, c)}-labels`}>
<text key={`${cellId(r, c)}-name`} font-size={fontSize}>
{cell.continued ? `${cell.name} (cont.)` : cell.name}
</text>
<align alignment="center">
<ref target={cellId(r, c)} />
<ref target={`${cellId(r, c)}-name`} />
</align>
<text
key={`${cellId(r, c)}-range`}
font-size={fontSize - 4}
fill="#777"
>
{range}
</text>
<align alignment="topLeft">
<ref target={cellId(r, c)} />
<ref target={`${cellId(r, c)}-range`} />
</align>
</group>
);
}),
)}
</group>
</Graphic>
);
}