diff --git a/README.md b/README.md
index 53a629d..12a36fd 100644
--- a/README.md
+++ b/README.md
@@ -29,13 +29,16 @@ switches/patch panels look the part: switches get draw.io's built-in network
switch icon, patch panels get a small rack-unit look (header strip + port
ticks) rather than a plain box.
-A **VLAN table** sits at the top of the diagram (VLAN / Name / Description /
-Subnet / Site). As of this writing NetBox has no VLANs defined here, so it
-renders as an explicit "not configured yet" placeholder rather than being
-silently omitted or -- worse -- filled in with invented VLAN numbers. Add
-VLANs and interface tagged/untagged assignments in NetBox and regenerate
-(no code changes needed): the table populates for real, and every connector
-whose port has VLAN config picks up a short suffix like `9·V10 ↔ 1·V10`.
+A **VLAN legend + table** sits at the top of the diagram: a colored
+"VLAN <id> / name" block per VLAN on the left (each color also used for
+that VLAN's connector tags and table swatch), and the detail table
+(VLAN / Name / Description / Subnet / Site) next to it. If NetBox has no
+VLANs defined, both render as an explicit "not configured yet" placeholder
+rather than being silently omitted or -- worse -- filled in with invented
+VLAN numbers. Add VLANs and interface tagged/untagged assignments in
+NetBox and regenerate (no code changes needed): both populate for real,
+and every connector whose port has VLAN config picks up a short suffix
+like `9·V214+215T ↔ 1`.
Deliberately left out: literal switch CLI config dumps -- those belong in
the switches' NetBox config-context/notes, not pasted into a topology
diagram where they'd immediately go stale.
diff --git a/topology/Field-Switch-Topology.drawio b/topology/Field-Switch-Topology.drawio
index 1112447..22bebc3 100644
--- a/topology/Field-Switch-Topology.drawio
+++ b/topology/Field-Switch-Topology.drawio
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/topology/Field-Switch-Topology.png b/topology/Field-Switch-Topology.png
index 3220943..ad69b6d 100644
Binary files a/topology/Field-Switch-Topology.png and b/topology/Field-Switch-Topology.png differ
diff --git a/topology/generate_drawio.py b/topology/generate_drawio.py
index c8cc280..87d13f0 100644
--- a/topology/generate_drawio.py
+++ b/topology/generate_drawio.py
@@ -23,9 +23,14 @@ devs = {d["id"]: d for d in data["devices"]}
edges = data["edges"] # [{a, a_port, a_vlan, b, b_port, b_vlan}, ...]
vlans = data.get("vlans", []) # [{vid, name, description, prefixes, site}, ...]
-VLAN_PALETTE = ["#D6603F", "#3C9D6B", "#E8A33D", "#5B7FD9", "#9A6BC9", "#3FAAB0", "#C2547E", "#7A8C3F"]
+VLAN_PALETTE = ["#D6603F", "#3C9D6B", "#C2547E", "#5B7FD9", "#E8A33D", "#3FAAB0", "#9A6BC9", "#7A8C3F"]
+# assigned by position among the VLANs actually present (sorted by vid, same
+# order they're listed/rendered in), not a hash -- guarantees no two VLANs
+# share a color as long as there are <= len(VLAN_PALETTE) of them, which a
+# hash-based assignment can't promise (two vids can hash to the same slot)
+_vlan_color_by_vid = {v["vid"]: VLAN_PALETTE[i % len(VLAN_PALETTE)] for i, v in enumerate(vlans)}
def vlan_color(vid):
- return VLAN_PALETTE[hash(vid) % len(VLAN_PALETTE)]
+ return _vlan_color_by_vid.get(vid, "#9AA0AA")
def vlan_tag(pv):
"""Short connector-label suffix for a port's VLAN config, e.g. 'V10' or
@@ -137,7 +142,17 @@ VLAN_HEADER_H = 30
VLAN_TABLE_W = 900
vlan_rows = max(1, len(vlans)) # the placeholder counts as one row
VLAN_TABLE_H = VLAN_HEADER_H + vlan_rows * VLAN_ROW_H + 16
-cursor_y = MARGIN + TITLE_H + LEGEND_ROW_H + VLAN_TABLE_H + 20
+
+# a dedicated colour-coded legend (VLAN id block + name block per row),
+# stacked to the left of the detail table -- this is the part that reads
+# as "legend" at a glance; the table next to it carries the detail
+VLAN_LEGEND_W = 260
+VLAN_LEGEND_ROW_H = 42
+VLAN_LEGEND_GAP = 6
+VLAN_LEGEND_H = vlan_rows * (VLAN_LEGEND_ROW_H + VLAN_LEGEND_GAP) - VLAN_LEGEND_GAP
+
+VLAN_SECTION_H = max(VLAN_TABLE_H, VLAN_LEGEND_H)
+cursor_y = MARGIN + TITLE_H + LEGEND_ROW_H + VLAN_SECTION_H + 20
w_main, h_main = place_section(depth_main, yslot_main, cursor_y)
main_top = cursor_y
@@ -160,7 +175,7 @@ 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
-PAGE_W = max(w_main, w_dmo, w_iso, 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
# ---------------- style palette ----------------
@@ -303,12 +318,40 @@ for i, (fill, stroke, label) in enumerate(legend_items):
)
lx += 260
-# ---------------- VLAN legend / mapping table ----------------
+# ---------------- VLAN legend (colour blocks) + mapping table ----------------
# Structure is always rendered; content depends on whether NetBox actually
# has VLANs defined yet (see export_topology.py). Nothing here is invented --
-# an empty table renders as an explicit "not configured" placeholder, not as
-# blank space or made-up VLAN numbers.
-vt_x, vt_y = MARGIN, MARGIN + TITLE_H + LEGEND_ROW_H
+# an empty legend/table renders as an explicit "not configured" placeholder,
+# not as blank space or made-up VLAN numbers.
+vlg_x, vlg_y = MARGIN, MARGIN + TITLE_H + LEGEND_ROW_H
+vlg_id_w, vlg_name_w = 70, VLAN_LEGEND_W - 70
+if not vlans:
+ add_cell(
+ f''
+ )
+else:
+ ly2 = vlg_y
+ for v in vlans:
+ color = vlan_color(v["vid"])
+ add_cell(
+ f''
+ f''
+ )
+ add_cell(
+ f''
+ f''
+ )
+ ly2 += VLAN_LEGEND_ROW_H + VLAN_LEGEND_GAP
+
+vt_x, vt_y = MARGIN + VLAN_LEGEND_W + 30, MARGIN + TITLE_H + LEGEND_ROW_H
vt_cols = [("VLAN", 60), ("Name", 130), ("Description", 300), ("Subnet", 160), ("Site", 170)]
add_cell(
f'