4747logger = logging .getLogger (__name__ .split ("." )[0 ])
4848
4949
50+ # Structural node attributes per tier — shape, sizing, and whether the box has
51+ # rounded corners. These are theme-independent; only the colors change with the
52+ # theme. `_scale` matches the historical 1.2 scaling factor for fonts and boxes.
53+ _scale = 1.2
54+ _TIER_STRUCTURE = {
55+ None : dict (shape = "circle" , fontsize = round (_scale * 8 ), size = 0.4 * _scale , fixed = False , rounded = False ),
56+ Manual : dict (shape = "box" , fontsize = round (_scale * 10 ), size = 0.4 * _scale , fixed = False , rounded = True ),
57+ Lookup : dict (shape = "box" , fontsize = round (_scale * 8 ), size = 0.4 * _scale , fixed = False , rounded = True ),
58+ Computed : dict (shape = "ellipse" , fontsize = round (_scale * 10 ), size = 0.4 * _scale , fixed = False , rounded = False ),
59+ Imported : dict (shape = "ellipse" , fontsize = round (_scale * 10 ), size = 0.4 * _scale , fixed = False , rounded = False ),
60+ Part : dict (shape = "box" , fontsize = round (_scale * 8 ), size = 0.1 * _scale , fixed = False , rounded = True ),
61+ "collapsed" : dict (shape = "box3d" , fontsize = round (_scale * 10 ), size = 0.5 * _scale , fixed = False , rounded = False ),
62+ }
63+
64+ # Color themes (#1532). Each tier gets a (fill, stroke, text) triple. Edge colors
65+ # share a single alpha so a renamed (amber) edge sits at the same visual density
66+ # as ordinary edges, differing only in hue.
67+ _DIAGRAM_THEMES = {
68+ "light" : dict (
69+ bg = None ,
70+ palette = {
71+ None : ("#FFFDE7" , "#C9BC5B" , "#6B6420" ),
72+ Manual : ("#E7F3EC" , "#2F7D5B" , "#1B5138" ),
73+ Lookup : ("#F2F4F7" , "#A9B1BD" , "#495261" ),
74+ Computed : ("#FBEAEC" , "#B23A48" , "#7C2430" ),
75+ Imported : ("#E2ECFA" , "#2A5FA5" , "#123A6D" ),
76+ Part : ("#FFFFFF" , "#9AA6B8" , "#46536B" ),
77+ "collapsed" : ("#EDEEF0" , "#808890" , "#404040" ),
78+ },
79+ edge = "#3A424F" ,
80+ edge_renamed = "#C77D3A" ,
81+ edge_alpha = "9E" ,
82+ schema_cluster = ("gray" , "gray" ),
83+ entity_fill = "#F3F5F8" ,
84+ ),
85+ "dark" : dict (
86+ bg = "#161A21" ,
87+ palette = {
88+ None : ("#3A3620" , "#C9BC5B" , "#EBE3A0" ),
89+ Manual : ("#16281F" , "#4FA97F" , "#BCE6CF" ),
90+ Lookup : ("#242832" , "#8A93A1" , "#C9CFD9" ),
91+ Computed : ("#331A1F" , "#D0687A" , "#F3C2CB" ),
92+ Imported : ("#152538" , "#5E92D6" , "#C3DAF6" ),
93+ Part : ("#1E232C" , "#7B879B" , "#C4CCDB" ),
94+ "collapsed" : ("#242730" , "#8890A0" , "#C7CDD6" ),
95+ },
96+ edge = "#AEB6C2" ,
97+ edge_renamed = "#D68C4A" ,
98+ edge_alpha = "C0" ,
99+ schema_cluster = ("#606875" , "#8A93A1" ),
100+ entity_fill = "#1E222B" ,
101+ ),
102+ }
103+
104+
50105class Diagram (nx .MultiDiGraph ): # noqa: C901
51106 """
52107 Schema diagram as a directed acyclic graph (DAG).
@@ -92,7 +147,7 @@ class Diagram(nx.MultiDiGraph): # noqa: C901
92147 Layout direction is controlled via ``dj.config.display.diagram_direction``
93148 (default ``"TB"``). Use ``dj.config.override()`` to change temporarily::
94149
95- with dj.config.override(display_diagram_direction ="LR"):
150+ with dj.config.override(display__diagram_direction ="LR"):
96151 dj.Diagram(schema).draw()
97152 """
98153
@@ -1412,91 +1467,20 @@ def make_dot(self):
14121467 if data .get ("collapsed" ) and data .get ("schema_name" ):
14131468 schema_map [node ] = data ["schema_name" ]
14141469
1415- scale = 1.2 # scaling factor for fonts and boxes
1416- # Modernized tier palette (#1532): each tier gets a readable
1417- # fill / stroke / text triple in place of the old alpha-blended primary
1418- # fills. Shape stays load-bearing and unchanged so an existing diagram
1419- # reads without relearning: Manual = rounded rectangle, Imported and
1420- # Computed = ellipse, Lookup and Part = subtle (white/near-white) box.
1421- label_props = {
1422- None : dict (
1423- shape = "circle" ,
1424- fill = "#FFFDE7" ,
1425- stroke = "#C9BC5B" ,
1426- fontcolor = "#6B6420" ,
1427- fontsize = round (scale * 8 ),
1428- size = 0.4 * scale ,
1429- fixed = False ,
1430- rounded = False ,
1431- ),
1432- Manual : dict (
1433- shape = "box" ,
1434- fill = "#E7F3EC" ,
1435- stroke = "#2F7D5B" ,
1436- fontcolor = "#1B5138" ,
1437- fontsize = round (scale * 10 ),
1438- size = 0.4 * scale ,
1439- fixed = False ,
1440- rounded = True ,
1441- ),
1442- Lookup : dict (
1443- shape = "box" ,
1444- fill = "#F2F4F7" ,
1445- stroke = "#A9B1BD" ,
1446- fontcolor = "#495261" ,
1447- fontsize = round (scale * 8 ),
1448- size = 0.4 * scale ,
1449- fixed = False ,
1450- rounded = True ,
1451- ),
1452- Computed : dict (
1453- shape = "ellipse" ,
1454- fill = "#FBEAEC" ,
1455- stroke = "#B23A48" ,
1456- fontcolor = "#7C2430" ,
1457- fontsize = round (scale * 10 ),
1458- size = 0.4 * scale ,
1459- fixed = False ,
1460- rounded = False ,
1461- ),
1462- Imported : dict (
1463- shape = "ellipse" ,
1464- fill = "#E2ECFA" ,
1465- stroke = "#2A5FA5" ,
1466- fontcolor = "#123A6D" ,
1467- fontsize = round (scale * 10 ),
1468- size = 0.4 * scale ,
1469- fixed = False ,
1470- rounded = False ,
1471- ),
1472- Part : dict (
1473- shape = "box" ,
1474- fill = "#FFFFFF" ,
1475- stroke = "#9AA6B8" ,
1476- fontcolor = "#46536B" ,
1477- fontsize = round (scale * 8 ),
1478- size = 0.1 * scale ,
1479- fixed = False ,
1480- rounded = True ,
1481- ),
1482- "collapsed" : dict (
1483- shape = "box3d" ,
1484- fill = "#EDEEF0" ,
1485- stroke = "#808890" ,
1486- fontcolor = "#404040" ,
1487- fontsize = round (scale * 10 ),
1488- size = 0.5 * scale ,
1489- fixed = False ,
1490- rounded = False ,
1491- ),
1492- }
1493- # Build node_props, handling collapsed nodes specially
1470+ # Select the color theme (#1532). Structure (shape/size/rounded) is
1471+ # theme-independent; only the fill/stroke/text colors change.
1472+ theme_name = self ._connection ._config .display .diagram_theme
1473+ theme = _DIAGRAM_THEMES .get (theme_name , _DIAGRAM_THEMES ["light" ])
1474+ palette = theme ["palette" ]
1475+
1476+ # Build node_props by merging the structural attributes for each tier
1477+ # with the theme's (fill, stroke, text) colors. Collapsed nodes use the
1478+ # "collapsed" entry.
14941479 node_props = {}
14951480 for node , d in graph .nodes (data = True ):
1496- if d .get ("collapsed" ):
1497- node_props [node ] = label_props ["collapsed" ]
1498- else :
1499- node_props [node ] = label_props [d ["node_type" ]]
1481+ tier = "collapsed" if d .get ("collapsed" ) else d ["node_type" ]
1482+ fill , stroke , text = palette [tier ]
1483+ node_props [node ] = dict (_TIER_STRUCTURE [tier ], fill = fill , stroke = stroke , fontcolor = text )
15001484
15011485 # A renamed (aliased) FK is drawn as a distinctly-colored edge (there
15021486 # is no longer an intermediate "alias" node); describe the column
@@ -1515,6 +1499,8 @@ def make_dot(self):
15151499 self ._encapsulate_edge_attributes (graph )
15161500 dot = nx .drawing .nx_pydot .to_pydot (graph )
15171501 dot .set_rankdir (direction )
1502+ if theme ["bg" ]:
1503+ dot .set_bgcolor (theme ["bg" ])
15181504
15191505 # Master↔part grouping (#1532): map each part (class name "Master.Part")
15201506 # to its master ("Master"), and record which parts depend on a sibling
@@ -1588,8 +1574,11 @@ def make_dot(self):
15881574 multi = str (edge .get ("multi" )) == "True"
15891575 aliased = str (edge .get ("aliased" )) == "True"
15901576 # Renamed FK → a distinct, desaturated amber consistent with the
1591- # modernized palette (#1532); others → a light translucent slate.
1592- edge .set_color ("#C77D3A" if aliased else "#3A424F33" )
1577+ # modernized palette (#1532); others → a translucent slate. Both
1578+ # share the theme's edge alpha so the amber sits at the same visual
1579+ # density as ordinary edges, differing only in hue.
1580+ base = theme ["edge_renamed" ] if aliased else theme ["edge" ]
1581+ edge .set_color (base + theme ["edge_alpha" ])
15931582 edge .set_style ("solid" if primary else "dashed" )
15941583 # Line weight encodes cardinality, and only cardinality. `multi` is
15951584 # True when the child has primary-key attributes beyond those this
@@ -1626,12 +1615,13 @@ def make_dot(self):
16261615 # chain descends.
16271616 for schema_name , nodes in schemas .items ():
16281617 label = cluster_labels .get (schema_name , schema_name )
1618+ sc_color , sc_fontcolor = theme ["schema_cluster" ]
16291619 cluster = pydot .Cluster (
16301620 f"cluster_{ schema_name } " ,
16311621 label = label ,
16321622 style = "rounded,dashed" ,
1633- color = "gray" ,
1634- fontcolor = "gray" ,
1623+ color = sc_color ,
1624+ fontcolor = sc_fontcolor ,
16351625 )
16361626 node_by_name = {n .get_name ().strip ('"' ): n for n in nodes }
16371627 # masters in this schema that have at least one part present
@@ -1650,8 +1640,8 @@ def make_dot(self):
16501640 "cluster_entity_" + master_name .replace ("." , "_" ),
16511641 label = "" ,
16521642 style = "rounded,filled" ,
1653- fillcolor = "#F3F5F8" ,
1654- color = "#F3F5F8" ,
1643+ fillcolor = theme [ "entity_fill" ] ,
1644+ color = theme [ "entity_fill" ] ,
16551645 )
16561646 entity .add_node (node_by_name [master_name ])
16571647 grouped .add (master_name )
0 commit comments