From 0fbf5a4a0142457919ddabfe4a8a2b6311abe794 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Tue, 18 Aug 2026 15:10:39 -0500 Subject: [PATCH 1/2] feat(#1543): adopt the DataJoint brand tier colors in diagram themes Retunes _DIAGRAM_THEMES to the brand palette: Imported on DataJoint Blue #00A0DF, Computed on DataJoint Orange #FF5113, brand grey Lookup, retuned Manual green; edges, cluster frames, and titles on brand navy #171C39. Dark theme derived by the existing rule (stroke hue kept, fills inverted toward navy-tinted darks, text brightened to AA on #161A21). Hue families are preserved, so tier recognition carries over; structural work from #1534 (shapes, clusters, underlines, edge weights, adaptive block) is unchanged. Two collision-free adjustments, commented in place: Part text #55585C (vs Lookup's #5A5C5F) and schema-cluster frame #171D3A (vs edge navy) so the adaptive mapping stays one-light-color-one-role. Adds test_theme_text_contrast_meets_aa (every text-on-fill pair >= 4.5:1 in both themes) and test_adaptive_mapping_is_collision_free; updates theme and mermaid expectations. Closes #1543 --- src/datajoint/diagram.py | 34 ++++++++++------- tests/integration/test_diagram_style.py | 49 +++++++++++++++++++++---- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/datajoint/diagram.py b/src/datajoint/diagram.py index 27e3d7a7f..9dc9e50cc 100644 --- a/src/datajoint/diagram.py +++ b/src/datajoint/diagram.py @@ -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", @@ -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 @@ -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 diff --git a/tests/integration/test_diagram_style.py b/tests/integration/test_diagram_style.py index b4c2e1821..a95f11ed6 100644 --- a/tests/integration/test_diagram_style.py +++ b/tests/integration/test_diagram_style.py @@ -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" @@ -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" @@ -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): @@ -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 @@ -150,7 +150,42 @@ 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).""" + light, dark = dj.diagram._DIAGRAM_THEMES["light"], dj.diagram._DIAGRAM_THEMES["dark"] + for idx, kind in ((0, "fill"), (1, "stroke"), (2, "fill")): + mapping = {} + 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}" + strokes = {} + for lv, dv in [ + (light["edge"].lower(), dark["edge"]), + (light["edge_renamed"].lower(), dark["edge_renamed"]), + (light["schema_cluster"][0].lower(), dark["schema_cluster"][0]), + ]: + assert strokes.setdefault(lv, dv) == dv, f"stroke collision on {lv}" From 411c8ad572da5326de4e06e2f922336bfafa93bd Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Tue, 18 Aug 2026 16:32:15 -0500 Subject: [PATCH 2/2] =?UTF-8?q?test:=20unify=20the=20adaptive-mapping=20co?= =?UTF-8?q?llision=20namespace=20per=20review=20=E2=80=94=20tier=20fills?= =?UTF-8?q?=20and=20texts=20share=20the=20[fill=3D]=20selector=20space;=20?= =?UTF-8?q?entity=5Ffill=20and=20cluster=20label=20included?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/integration/test_diagram_style.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/integration/test_diagram_style.py b/tests/integration/test_diagram_style.py index a95f11ed6..64864b449 100644 --- a/tests/integration/test_diagram_style.py +++ b/tests/integration/test_diagram_style.py @@ -175,17 +175,23 @@ def test_theme_text_contrast_meets_aa(): def test_adaptive_mapping_is_collision_free(): - """Every light color maps to exactly one dark counterpart per attribute (#1532 invariant).""" + """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")): - mapping = {} 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}" - strokes = {} - for lv, dv in [ - (light["edge"].lower(), dark["edge"]), - (light["edge_renamed"].lower(), dark["edge_renamed"]), - (light["schema_cluster"][0].lower(), dark["schema_cluster"][0]), + 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 strokes.setdefault(lv, dv) == dv, f"stroke collision on {lv}" + assert mapping.setdefault((kind, lkey.lower()), dkey) == dkey, f"collision on {kind} {lkey}"