Realistic rack-equipment icons for switches and patch panels

Swapped the abstract mxgraph.networks.switch pictogram and the hand-drawn
patch-panel box for draw.io's built-in Rack shape library -- real 19"
front-panel silhouettes (verified against the actual stencil source on
GitHub, not guessed): mxgraph.rack.general.switches_1 for switches,
...cat5e_rack_mount_patch_panel_24_ports for patch panels.

No stencil library covers FS.com/AFL/Primex/Clearview/FIS (all niche
fiber/telecom brands draw.io doesn't ship icons for), so per user's choice
brand/model accuracy is carried as text instead: every node's caption is
now 'hostname' + the real NetBox device_type.model on a second line, e.g.
'ls-bsls-sw01 / IES3110-8TFP-R'.

Patch panels also lost their hand-built 7-cell composition (body + strip +
6 port-tick cells) now that a real stencil exists -- one cell each,
cell count 530 -> 271.

Fixed a spacing regression this surfaced: the new realistic icons are much
shorter/flatter than the old ones, and COL_W (210) left only ~100px
between adjacent columns -- not enough room for a ~150-180px connector
label, so port/VLAN tags were overlapping neighboring icons. COL_W -> 320.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NM3nivbufxEWiKKmatEtEe
This commit is contained in:
dcstephenson
2026-09-15 14:36:23 -05:00
parent 4222da4a9b
commit 76391fadcf
4 changed files with 63 additions and 61 deletions
+10 -4
View File
@@ -24,10 +24,16 @@ both fabrics for a documented reason (spare/inventory, decommissioning, or
planned/not-yet-installed) -- those are called out in their own box on the planned/not-yet-installed) -- those are called out in their own box on the
diagram rather than silently dropped. diagram rather than silently dropped.
Every connector is labeled with the port at each end (e.g. `9 ↔ 1`), and Every connector is labeled with the port at each end (e.g. `9 ↔ 1`).
switches/patch panels look the part: switches get draw.io's built-in network Switches and patch panels use draw.io's built-in **Rack** shape library
switch icon, patch panels get a small rack-unit look (header strip + port (`mxgraph.rack.general.switches_1` / `...cat5e_rack_mount_patch_panel_24_ports`)
ticks) rather than a plain box. -- real 19"-rack front-panel silhouettes, not the old abstract pictogram /
hand-drawn box. There's no stencil library for FS.com, AFL, Primex,
Clearview, or FIS specifically (those are niche fiber/telecom brands, not
in any icon set draw.io ships), so brand accuracy is carried as text
instead of guessed at in the art: every node's caption is its hostname
*and* its real NetBox `device_type.model` (e.g. `IES3110-8TFP-R`, `xPAK
6-Port SC/APC`), not just the hostname.
A **VLAN legend + table** sits at the top of the diagram: a colored A **VLAN legend + table** sits at the top of the diagram: a colored
"VLAN &lt;id&gt; / name" block per VLAN on the left (each color also used for "VLAN &lt;id&gt; / name" block per VLAN on the left (each color also used for
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

+49 -53
View File
@@ -5,8 +5,7 @@ Render topo_full.json (produced by export_topology.py) as a draw.io
Edges reference shapes by id (source=/target=), which is how draw.io's Edges reference shapes by id (source=/target=), which is how draw.io's
native glue works -- no hand-rolled formulas needed: dragging a shape in native glue works -- no hand-rolled formulas needed: dragging a shape in
the app keeps every attached line attached. Patch panel port ticks/accent the app keeps every attached line attached.
strips are parented to the panel's body cell, so they move with it too.
Usage: Usage:
python3 generate_drawio.py [topo_full.json] [Field-Switch-Topology.drawio] python3 generate_drawio.py [topo_full.json] [Field-Switch-Topology.drawio]
@@ -104,22 +103,26 @@ depth_main, yslot_main = layout_tree(1, main_comp)
depth_dmo, yslot_dmo = layout_tree(7, dmo_comp) depth_dmo, yslot_dmo = layout_tree(7, dmo_comp)
# ---------------- geometry constants (px) ---------------- # ---------------- geometry constants (px) ----------------
SWITCH_ICON = 46 # icon box; label renders below it # Realistic rack-equipment silhouettes (draw.io's built-in Rack shape
SWITCH_LABEL_H = 30 # library) instead of the old abstract pictogram / hand-drawn box -- both
PANEL_W, PANEL_H = 132, 42 # switches and patch panels are real 19" rack-mount gear, so both get the
COL_W = 200 # same width and a below-icon caption; only the icon height (and which
ROW_H = 86 # stencil) differs, matching each one's actual flat 1U proportions.
NODE_W = 108
SWITCH_H = 20 # ~108:20, close to the stencil's native ~5.3:1
PANEL_H = 12 # ~108:12, close to the stencil's native ~10.1:1
CAPTION_H = 46 # hostname + model, wrapped, below the icon
COL_W = 320 # NODE_W + clearance for the connector's own port/VLAN label text
ROW_H = 78
PAD = 40 PAD = 40
LABEL_STRIP = 34 LABEL_STRIP = 34
SECTION_GAP = 70 SECTION_GAP = 70
MARGIN = 40 MARGIN = 40
def footprint(dev): def icon_h(dev):
if dev["role"] == "patch-panel": return PANEL_H if dev["role"] == "patch-panel" else SWITCH_H
return PANEL_W, PANEL_H
return SWITCH_ICON, SWITCH_ICON + SWITCH_LABEL_H
pos = {} # device_id -> (x, y) TOP-LEFT of the node's own bounding box (not center) pos = {} # device_id -> (x, y, w, h) of the node's own ICON (caption sits below it)
def place_section(depth_map, yslot_map, y_offset): def place_section(depth_map, yslot_map, y_offset):
ys = list(yslot_map.values()) ys = list(yslot_map.values())
@@ -127,12 +130,12 @@ def place_section(depth_map, yslot_map, y_offset):
max_x = max(depth_map.values()) max_x = max(depth_map.values())
for node, d in depth_map.items(): for node, d in depth_map.items():
y = yslot_map[node] y = yslot_map[node]
w, h = footprint(devs[node]) h = icon_h(devs[node])
cx = MARGIN + PAD + d * COL_W + SWITCH_ICON / 2 cx = MARGIN + PAD + d * COL_W + NODE_W / 2
cy = y_offset + PAD + LABEL_STRIP + (y - min_y) * ROW_H + SWITCH_ICON / 2 top = y_offset + PAD + LABEL_STRIP + (y - min_y) * ROW_H
pos[node] = (cx - w / 2, cy - h / 2, w, h) pos[node] = (cx - NODE_W / 2, top, NODE_W, h)
width = MARGIN * 2 + PAD * 2 + max_x * COL_W + SWITCH_ICON width = MARGIN * 2 + PAD * 2 + max_x * COL_W + NODE_W
height = PAD * 2 + LABEL_STRIP + (max(ys) - min_y) * ROW_H + SWITCH_ICON + SWITCH_LABEL_H height = PAD * 2 + LABEL_STRIP + (max(ys) - min_y) * ROW_H + SWITCH_H + CAPTION_H
return width, height return width, height
TITLE_H = 60 TITLE_H = 60
@@ -167,13 +170,13 @@ iso_top = cursor_y
iso_sorted = sorted(iso, key=lambda d: devs[d]["name"]) iso_sorted = sorted(iso, key=lambda d: devs[d]["name"])
for i, node in enumerate(iso_sorted): for i, node in enumerate(iso_sorted):
r, c = divmod(i, ISO_COLS) r, c = divmod(i, ISO_COLS)
w, h = footprint(devs[node]) h = icon_h(devs[node])
cx = MARGIN + PAD + c * COL_W + SWITCH_ICON / 2 cx = MARGIN + PAD + c * COL_W + NODE_W / 2
cy = iso_top + PAD + LABEL_STRIP + r * ROW_H + SWITCH_ICON / 2 top = iso_top + PAD + LABEL_STRIP + r * ROW_H
pos[node] = (cx - w / 2, cy - h / 2, w, h) pos[node] = (cx - NODE_W / 2, top, NODE_W, h)
iso_rows = (len(iso_sorted) + ISO_COLS - 1) // ISO_COLS iso_rows = (len(iso_sorted) + ISO_COLS - 1) // ISO_COLS
w_iso = MARGIN * 2 + PAD * 2 + (ISO_COLS - 1) * COL_W + SWITCH_ICON w_iso = MARGIN * 2 + PAD * 2 + (ISO_COLS - 1) * COL_W + NODE_W
h_iso = PAD * 2 + LABEL_STRIP + (iso_rows - 1) * ROW_H + SWITCH_ICON + SWITCH_LABEL_H h_iso = PAD * 2 + LABEL_STRIP + (iso_rows - 1) * ROW_H + SWITCH_H + CAPTION_H
PAGE_W = max(w_main, w_dmo, w_iso, VLAN_LEGEND_W + 30 + VLAN_TABLE_W + MARGIN * 2) PAGE_W = max(w_main, w_dmo, w_iso, VLAN_LEGEND_W + 30 + VLAN_TABLE_W + MARGIN * 2)
PAGE_H = cursor_y + h_iso + MARGIN PAGE_H = cursor_y + h_iso + MARGIN
@@ -247,29 +250,23 @@ add_cell(
) )
# ---------------- node shapes ---------------- # ---------------- node shapes ----------------
# Switches: a real network-switch icon, hostname caption below it. # Realistic 19" rack-equipment silhouettes from draw.io's built-in Rack
# Patch panels: understated -- pale body, a thin hairline where a rack unit's # shape library (verified against the actual stencil source, not guessed):
# seam would be, and a row of faint port outlines. Reads as "passive # a real network-switch front panel for switches, a real 24-port patch
# hardware, not the point of this diagram" rather than competing with the # panel front panel for patch panels. Neither is FS.com/AFL/Primex/etc.
# switches for attention. Built from cells parented to the panel's own body # branded art -- no such stencils exist anywhere, this vendor mix is far
# cell, so the whole unit moves together when dragged. # too niche -- so "by model" is carried as text: every node's caption is
def panel_shape(did, x, y, w, h, name, st): # its hostname plus its actual NetBox device_type.model, wrapped below the
body_id = f"n{did}" # icon, instead of pretending the icon itself is model-specific.
vertex(body_id, name, x, y, w, h, SWITCH_STENCIL = "mxgraph.rack.general.switches_1"
f'rounded=1;arcSize=6;whiteSpace=wrap;html=1;fillColor={st["fill"]};strokeColor={st["stroke"]};' PANEL_STENCIL = "mxgraph.rack.general.cat5e_rack_mount_patch_panel_24_ports"
f'fontColor={st["font"]};fontSize=9;verticalAlign=bottom;spacingBottom=5;')
# hairline seam, relative to the body def caption(name, model):
vertex(f"{body_id}-strip", "", 5, 6, w - 10, 1, # single-quote the inner HTML attribute -- this whole string is itself
f'rounded=0;fillColor={st["stroke"]};strokeColor=none;', parent=body_id) # going into an XML value="..." attribute (vertex() escapes it), and a
# faint port outlines along the top-middle, evenly spaced # literal " from a double-quoted style="..." here would prematurely
n_ticks = 6 # close that outer XML attribute
tick_w, tick_h = 9, 6 return f"{escape(name)}<br><font style='font-size:9px'>{escape(model)}</font>"
usable = w - 16
gap = usable / n_ticks
for k in range(n_ticks):
tx = 8 + gap * k + (gap - tick_w) / 2
vertex(f"{body_id}-t{k}", "", tx, 14, tick_w, tick_h,
f'rounded=0;fillColor=none;strokeColor={st["stroke"]};strokeWidth=0.75;', parent=body_id)
for did, dev in devs.items(): for did, dev in devs.items():
if did not in pos: if did not in pos:
@@ -277,15 +274,14 @@ for did, dev in devs.items():
x, y, w, h = pos[did] x, y, w, h = pos[did]
st = style_for(dev) st = style_for(dev)
dashed = "dashed=1;dashPattern=4 3;" if st["dashed"] else "" dashed = "dashed=1;dashPattern=4 3;" if st["dashed"] else ""
if dev["role"] == "patch-panel": stencil = PANEL_STENCIL if dev["role"] == "patch-panel" else SWITCH_STENCIL
panel_shape(did, x, y, w, h, dev["name"], st) font = st["font"] if st["font"] != "#FFFFFF" else "#1A2233"
else:
style = ( style = (
'shape=mxgraph.networks.switch;html=1;verticalLabelPosition=bottom;verticalAlign=top;' f'shape={stencil};html=1;whiteSpace=wrap;verticalLabelPosition=bottom;verticalAlign=top;'
f'labelBackgroundColor=none;fontSize=10;fontColor={st["font"] if st["font"] != "#FFFFFF" else "#1A2233"};' f'labelBackgroundColor=none;fontSize=11;fontColor={font};'
f'fillColor={st["fill"]};strokeColor={st["stroke"]};fontStyle={st["bold"]};{dashed}' f'fillColor={st["fill"]};strokeColor={st["stroke"]};fontStyle={st["bold"]};{dashed}'
) )
vertex(f"n{did}", dev["name"], x, y, SWITCH_ICON, SWITCH_ICON, style) vertex(f"n{did}", caption(dev["name"], dev.get("device_type", "")), x, y, w, h, style)
# ---------------- connectors, labeled with the port at each end ---------------- # ---------------- connectors, labeled with the port at each end ----------------
for idx, e in enumerate(edges): for idx, e in enumerate(edges):