Skip to content

Commit c0abbe7

Browse files
feat(#1532): dark theme option; matched, higher-contrast edge density
Add a diagram color theme option (dj.config display.diagram_theme = light|dark). Refactor make_dot styling into theme-independent structure (_TIER_STRUCTURE) plus per-theme color sets (_DIAGRAM_THEMES): tier fill/stroke/text triples, background, edge colors, and cluster colors. The dark theme uses a deep-slate background with light text and brighter strokes. Edges share one alpha per theme, so a renamed (amber) edge sits at the same visual density as ordinary edges — differing only in hue — and both are given more contrast than the first pass. Also fix a single-underscore config.override example in the docstring (needs double underscore for nested keys).
1 parent 29b4786 commit c0abbe7

2 files changed

Lines changed: 86 additions & 90 deletions

File tree

src/datajoint/diagram.py

Lines changed: 80 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,61 @@
4747
logger = 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+
50105
class 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)

src/datajoint/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"database.create_tables": "DJ_CREATE_TABLES",
7171
"loglevel": "DJ_LOG_LEVEL",
7272
"display.diagram_direction": "DJ_DIAGRAM_DIRECTION",
73+
"display.diagram_theme": "DJ_DIAGRAM_THEME",
7374
}
7475

7576
Role = Enum("Role", "manual lookup imported computed job")
@@ -245,6 +246,11 @@ class DisplaySettings(BaseSettings):
245246
validation_alias="DJ_DIAGRAM_DIRECTION",
246247
description="Default diagram layout direction: 'TB' (top-to-bottom) or 'LR' (left-to-right)",
247248
)
249+
diagram_theme: Literal["light", "dark"] = Field(
250+
default="light",
251+
validation_alias="DJ_DIAGRAM_THEME",
252+
description="Default diagram color theme: 'light' or 'dark' (dark background with adjusted palette)",
253+
)
248254

249255

250256
class StoresSettings(BaseSettings):

0 commit comments

Comments
 (0)