Skip to content

Commit 9346c20

Browse files
feat(#1532): adaptive 'auto' theme; brighten dark entity cluster
Add theme='auto': render the diagram in light colors and inject a prefers-color-scheme style block so a single SVG adapts to the viewer's light or dark mode (the two palettes are collision-free, so a per-color attribute-selector override is unambiguous). Exposed via Diagram.svg_string(); make_svg/_repr_svg_ use it. make_dot() gains an optional theme override. Also brighten the dark-theme entity-cluster fill so the master-part grouping box reads against the dark background.
1 parent c0abbe7 commit 9346c20

2 files changed

Lines changed: 72 additions & 8 deletions

File tree

‎src/datajoint/diagram.py‎

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,53 @@
9797
edge_renamed="#D68C4A",
9898
edge_alpha="C0",
9999
schema_cluster=("#606875", "#8A93A1"),
100-
entity_fill="#1E222B",
100+
entity_fill="#2A313D",
101101
),
102102
}
103103

104104

105+
def _adaptive_style_block() -> str:
106+
"""
107+
Build a ``<style>`` block that adapts a light-rendered diagram SVG to the
108+
viewer's dark mode via ``@media (prefers-color-scheme: dark)`` (#1532).
109+
110+
The two palettes are collision-free — every light color maps to exactly one
111+
role — so a per-color attribute-selector override is unambiguous. Graphviz
112+
emits lowercase hex, so selectors are lowercased.
113+
"""
114+
light, dark = _DIAGRAM_THEMES["light"], _DIAGRAM_THEMES["dark"]
115+
dark_edge_opacity = round(int(dark["edge_alpha"], 16) / 255, 3)
116+
rules = [
117+
f"svg {{ background-color: {dark['bg']}; }}",
118+
f'polygon[fill="white"] {{ fill: {dark["bg"]}; }}', # graph canvas
119+
]
120+
121+
def fill_rule(light_hex, dark_hex):
122+
return f'[fill="{light_hex.lower()}"] {{ fill: {dark_hex}; }}'
123+
124+
def stroke_rule(light_hex, dark_hex, opacity=None):
125+
extra = f" stroke-opacity: {opacity};" if opacity is not None else ""
126+
return f'[stroke="{light_hex.lower()}"] {{ stroke: {dark_hex};{extra} }}'
127+
128+
for tier, (lf, ls, lt) in light["palette"].items():
129+
df, ds, dt = dark["palette"][tier]
130+
rules += [fill_rule(lf, df), stroke_rule(ls, ds), fill_rule(lt, dt)]
131+
# edges (share the theme alpha)
132+
rules += [
133+
stroke_rule(light["edge"], dark["edge"], dark_edge_opacity),
134+
stroke_rule(light["edge_renamed"], dark["edge_renamed"], dark_edge_opacity),
135+
]
136+
# entity cluster fill + (borderless) stroke, and schema cluster border/label
137+
rules += [
138+
fill_rule(light["entity_fill"], dark["entity_fill"]),
139+
stroke_rule(light["entity_fill"], dark["entity_fill"]),
140+
stroke_rule(light["schema_cluster"][0], dark["schema_cluster"][0]),
141+
fill_rule(light["schema_cluster"][1], dark["schema_cluster"][1]),
142+
]
143+
body = "\n".join(" " + r for r in rules)
144+
return "<style>\n@media (prefers-color-scheme: dark) {\n" + body + "\n}\n</style>"
145+
146+
105147
class Diagram(nx.MultiDiGraph): # noqa: C901
106148
"""
107149
Schema diagram as a directed acyclic graph (DAG).
@@ -1396,7 +1438,7 @@ def _encapsulate_node_names(graph: nx.MultiDiGraph) -> None:
13961438
copy=False,
13971439
)
13981440

1399-
def make_dot(self):
1441+
def make_dot(self, theme=None):
14001442
"""
14011443
Generate a pydot graph object.
14021444
@@ -1468,9 +1510,12 @@ def make_dot(self):
14681510
schema_map[node] = data["schema_name"]
14691511

14701512
# 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"])
1513+
# theme-independent; only the fill/stroke/text colors change. `theme`
1514+
# (an explicit argument) overrides the configured default; "auto" is
1515+
# rendered in light colors here and made adaptive at the SVG layer, so
1516+
# it maps to the light palette.
1517+
theme_name = theme or self._connection._config.display.diagram_theme
1518+
theme = _DIAGRAM_THEMES.get(theme_name if theme_name != "auto" else "light", _DIAGRAM_THEMES["light"])
14741519
palette = theme["palette"]
14751520

14761521
# Build node_props by merging the structural attributes for each tier
@@ -1665,10 +1710,26 @@ def make_dot(self):
16651710

16661711
return dot
16671712

1713+
def svg_string(self) -> str:
1714+
"""
1715+
Render the diagram to an SVG string, honoring the color theme.
1716+
1717+
For ``theme="auto"`` the diagram is rendered in light colors and a
1718+
``prefers-color-scheme`` style block is injected so a single image
1719+
adapts to the viewer's light or dark mode. Other themes render directly.
1720+
"""
1721+
theme_name = self._connection._config.display.diagram_theme
1722+
if theme_name == "auto":
1723+
svg = self.make_dot(theme="light").create_svg().decode()
1724+
# Insert the adaptive style block right after the opening <svg ...>.
1725+
insert_at = svg.find(">", svg.find("<svg")) + 1
1726+
return svg[:insert_at] + "\n" + _adaptive_style_block() + svg[insert_at:]
1727+
return self.make_dot().create_svg().decode()
1728+
16681729
def make_svg(self):
16691730
from IPython.display import SVG
16701731

1671-
return SVG(self.make_dot().create_svg())
1732+
return SVG(self.svg_string())
16721733

16731734
def make_png(self):
16741735
return io.BytesIO(self.make_dot().create_png())

‎src/datajoint/settings.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,13 @@ class DisplaySettings(BaseSettings):
246246
validation_alias="DJ_DIAGRAM_DIRECTION",
247247
description="Default diagram layout direction: 'TB' (top-to-bottom) or 'LR' (left-to-right)",
248248
)
249-
diagram_theme: Literal["light", "dark"] = Field(
249+
diagram_theme: Literal["light", "dark", "auto"] = Field(
250250
default="light",
251251
validation_alias="DJ_DIAGRAM_THEME",
252-
description="Default diagram color theme: 'light' or 'dark' (dark background with adjusted palette)",
252+
description=(
253+
"Default diagram color theme: 'light', 'dark' (dark background with adjusted palette), "
254+
"or 'auto' (single SVG that adapts to the viewer's light/dark mode)"
255+
),
253256
)
254257

255258

0 commit comments

Comments
 (0)