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