Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions src/datajoint/diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,34 @@
bg=None,
palette={
None: ("#FFFDE7", "#C9BC5B", "#6B6420"),
Manual: ("#E7F3EC", "#2F7D5B", "#1B5138"),
Lookup: ("#F2F4F7", "#A9B1BD", "#495261"),
Computed: ("#FBEAEC", "#B23A48", "#7C2430"),
Imported: ("#E2ECFA", "#2A5FA5", "#123A6D"),
Part: ("#FFFFFF", "#9AA6B8", "#46536B"),
Manual: ("#E8F0E9", "#3E7A52", "#28513A"),
Lookup: ("#F0F0F1", "#808285", "#5A5C5F"),
Computed: ("#FFEDE5", "#FF5113", "#B23200"),
Imported: ("#E0F4FC", "#00A0DF", "#00537A"),
# Part text is #55585C rather than Lookup's #5A5C5F only so every
# light color maps to exactly one role (the adaptive block requires
# a collision-free mapping); visually identical.
Part: ("#FFFFFF", "#B9BBBE", "#55585C"),
"collapsed": ("#EDEEF0", "#808890", "#404040"),
},
edge="#3A424F",
# Edges and titles are brand navy. The schema-cluster frame is #171D3A —
# one step off navy for the same collision-free reason (edge and frame
# are both strokes and need distinct dark counterparts).
edge="#171C39",
edge_renamed="#C77D3A",
edge_alpha="9E",
schema_cluster=("gray", "gray"),
schema_cluster=("#171D3A", "#171C39"),
entity_fill="#F3F5F8",
),
"dark": dict(
bg="#161A21",
palette={
None: ("#3A3620", "#C9BC5B", "#EBE3A0"),
Manual: ("#16281F", "#4FA97F", "#BCE6CF"),
Lookup: ("#242832", "#8A93A1", "#C9CFD9"),
Computed: ("#331A1F", "#D0687A", "#F3C2CB"),
Imported: ("#152538", "#5E92D6", "#C3DAF6"),
Part: ("#1E232C", "#7B879B", "#C4CCDB"),
Manual: ("#16281F", "#6BBF94", "#BCE6CF"),
Lookup: ("#242832", "#9DA0A4", "#C9CFD9"),
Computed: ("#331B12", "#FF7A47", "#FFC7AE"),
Imported: ("#0F2433", "#33B8E8", "#BEE7F9"),
Part: ("#1E232C", "#8F959D", "#C8CED8"),
"collapsed": ("#242730", "#8890A0", "#C7CDD6"),
},
edge="#AEB6C2",
Expand Down Expand Up @@ -1811,7 +1817,7 @@ def make_mermaid(self) -> str:
--------
>>> print(dj.Diagram(schema).make_mermaid())
flowchart TB
classDef manual fill:#E7F3EC,stroke:#2F7D5B,color:#1B5138
classDef manual fill:#E8F0E9,stroke:#3E7A52,color:#28513A
...
subgraph my_pipeline["my_pipeline"]
Mouse[Mouse]:::manual
Expand All @@ -1820,7 +1826,7 @@ def make_mermaid(self) -> str:
end
Mouse --> Session
Session --> Neuron
linkStyle 0 stroke:#3A424F,stroke-width:1px
linkStyle 0 stroke:#171C39,stroke-width:1px
"""
graph = self._make_graph()
direction = self._connection._config.display.diagram_direction
Expand Down
55 changes: 48 additions & 7 deletions tests/integration/test_diagram_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ def test_light_theme_style(schema_by_backend):
ctx = _build(schema_by_backend)
svg = _svg(schema_by_backend, ctx, "light")
# tier fills
for fill in ("#e7f3ec", "#f2f4f7", "#e2ecfa", "#fbeaec", "#ffffff"):
for fill in ("#e8f0e9", "#f0f0f1", "#e0f4fc", "#ffede5", "#ffffff"):
assert fill in svg, f"light tier fill {fill} missing"
# a couple tier strokes
for stroke in ("#2f7d5b", "#b23a48"):
for stroke in ("#3e7a52", "#ff5113"):
assert stroke in svg, f"light tier stroke {stroke} missing"
# thick (1:1) and thin (multi) edge weights both present
assert 'stroke-width="2"' in svg, "thick (1:1) edge missing"
Expand All @@ -97,7 +97,7 @@ def test_dark_theme_style(schema_by_backend):
ctx = _build(schema_by_backend)
svg = _svg(schema_by_backend, ctx, "dark")
assert "#161a21" in svg, "dark background missing"
for fill in ("#16281f", "#152538", "#331a1f"):
for fill in ("#16281f", "#0f2433", "#331b12"):
assert fill in svg, f"dark tier fill {fill} missing"


Expand All @@ -108,7 +108,7 @@ def test_auto_theme_is_adaptive(schema_by_backend):
svg = _svg(schema_by_backend, ctx, "auto")
assert "@media (prefers-color-scheme: dark)" in svg, "auto theme must inject the adaptive media block"
# base render is light; the media block maps a light color to its dark counterpart
assert "#e7f3ec" in svg and "#16281f" in svg, "auto theme must carry both light base and dark override colors"
assert "#e8f0e9" in svg and "#16281f" in svg, "auto theme must carry both light base and dark override colors"


def test_mermaid_matches_modernized_notation(schema_by_backend):
Expand Down Expand Up @@ -138,8 +138,8 @@ class Analysis(dj.Computed):
mmd = dj.Diagram(schema, context=ctx).make_mermaid()

# shared light-theme palette (classDefs), not the old bright colors
assert "classDef manual fill:#E7F3EC,stroke:#2F7D5B" in mmd
assert "classDef computed fill:#FBEAEC,stroke:#B23A48" in mmd
assert "classDef manual fill:#E8F0E9,stroke:#3E7A52" in mmd
assert "classDef computed fill:#FFEDE5,stroke:#FF5113" in mmd
assert "#90EE90" not in mmd, "old bright palette must be gone"

# cardinality edge weights via linkStyle: thick 1:1 and thin one-to-many both present
Expand All @@ -150,7 +150,48 @@ class Analysis(dj.Computed):

# renamed FK takes the theme's amber; ordinary edges the slate
assert "stroke:#C77D3A" in mmd, "renamed-FK amber missing"
assert "stroke:#3A424F" in mmd, "ordinary edge slate missing"
assert "stroke:#171C39" in mmd, "ordinary edge navy missing"

# master-part group nests in an entity subgraph
assert "subgraph entity_" in mmd, "master-part entity nesting missing"


def _contrast(fg, bg):
def lum(h):
c = [int(h.lstrip("#")[i : i + 2], 16) / 255 for i in (0, 2, 4)]
c = [x / 12.92 if x <= 0.03928 else ((x + 0.055) / 1.055) ** 2.4 for x in c]
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]

hi, lo = sorted([lum(fg), lum(bg)], reverse=True)
return (hi + 0.05) / (lo + 0.05)


def test_theme_text_contrast_meets_aa():
"""Every tier's text-on-fill pair holds WCAG AA (>= 4.5:1) in both themes (#1543)."""
for name, theme in dj.diagram._DIAGRAM_THEMES.items():
for tier, (fill, _stroke, text) in theme["palette"].items():
ratio = _contrast(text, fill)
assert ratio >= 4.5, f"{name}/{tier}: {text} on {fill} = {ratio:.2f} < 4.5"


def test_adaptive_mapping_is_collision_free():
"""Every light color maps to exactly one dark counterpart per attribute (#1532 invariant).

One unified namespace per attribute kind: tier fills and tier texts both emit
[fill=...] selectors in the adaptive block, so they must not collide with each
other either; entity_fill and the schema-cluster label are fill rules too.
"""
light, dark = dj.diagram._DIAGRAM_THEMES["light"], dj.diagram._DIAGRAM_THEMES["dark"]
mapping = {}
for idx, kind in ((0, "fill"), (1, "stroke"), (2, "fill")):
for tier in light["palette"]:
lv, dv = light["palette"][tier][idx].lower(), dark["palette"][tier][idx]
assert mapping.setdefault((kind, lv), dv) == dv, f"collision on {kind} {lv}"
for lkey, dkey, kind in [
(light["edge"], dark["edge"], "stroke"),
(light["edge_renamed"], dark["edge_renamed"], "stroke"),
(light["schema_cluster"][0], dark["schema_cluster"][0], "stroke"),
(light["entity_fill"], dark["entity_fill"], "fill"),
(light["schema_cluster"][1], dark["schema_cluster"][1], "fill"),
]:
assert mapping.setdefault((kind, lkey.lower()), dkey) == dkey, f"collision on {kind} {lkey}"
Loading