Skip to content

Commit

Permalink
Refresh pipeline directed graph colors
Browse files Browse the repository at this point in the history
The current pipeline directed graph nodes use dark gray as their
background color. It has been reported that it is difficult to read the
black text on the dark gray background. This ticket updates the directed
graph color scheme with an aim towards making node text easier to read.
In the process of exploring what color schemes would be optimal to
satisfy the aim of this ticket, it emerged that making use of the Rubin
visual identity colors may be desirable. This will help to make LSST
pipeline graphs more instantly recognizable as Rubin-associated
products. Colors: https://rubin.canto.com/g/RubinVisualIdentity
  • Loading branch information
leeskelvin committed Jun 14, 2023
1 parent 3e79529 commit a76e6e1
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 17 deletions.
6 changes: 6 additions & 0 deletions doc/changes/DM-39294.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
The current pipeline directed graph nodes use dark gray as their background color.
It has been reported that it is difficult to read the black text on the dark gray background.
This ticket updates the directed graph color scheme with an aim towards making node text easier to read.
In the process of exploring what color schemes would be optimal to satisfy the aim of this ticket, it emerged that making use of the Rubin visual identity colors may be desirable.
This will help to make LSST pipeline graphs more instantly recognizable as Rubin-associated products.
Colors: https://rubin.canto.com/g/RubinVisualIdentity
53 changes: 40 additions & 13 deletions python/lsst/ctrl/mpexec/dotTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
# -------------------------------
# Imports of standard modules --
# -------------------------------
import html
import io
import re
from collections.abc import Iterable
Expand All @@ -49,32 +50,52 @@
# Local non-exported definitions --
# ----------------------------------

# Node styles indexed by node type.
_STYLES = dict(
task=dict(shape="box", style="filled,bold", fillcolor="gray70"),
quantum=dict(shape="box", style="filled,bold", fillcolor="gray70"),
dsType=dict(shape="box", style="rounded,filled", fillcolor="gray90"),
dataset=dict(shape="box", style="rounded,filled", fillcolor="gray90"),
# Attributes applied to directed graph objects.
_NODELABELPOINTSIZE = 18
_ATTRIBS = dict(
defaultGraph=dict(splines="ortho", nodesep="0.5", ranksep="0.75", pad="0.5"),
defaultNode=dict(shape="box", fontname="Monospace", fontsize="14", margin="0.2,0.1", penwidth="3"),
defaultEdge=dict(color="black", arrowsize="1.5", penwidth="1.5"),
task=dict(style="filled", color="black", fillcolor="#B1F2EF"),
quantum=dict(style="filled", color="black", fillcolor="#B1F2EF"),
dsType=dict(style="rounded,filled,bold", color="#00BABC", fillcolor="#F5F5F5"),
dataset=dict(style="rounded,filled,bold", color="#00BABC", fillcolor="#F5F5F5"),
)


def _renderDefault(type: str, attribs: dict[str, str], file: io.TextIOBase) -> None:
"""Set default attributes for a given type."""
default_attribs = ", ".join([f'{key}="{val}"' for key, val in attribs.items()])
print(f"{type} [{default_attribs}];", file=file)


def _renderNode(file: io.TextIOBase, nodeName: str, style: str, labels: list[str]) -> None:
"""Render GV node"""
label = r"\n".join(labels)
attrib_dict = dict(_STYLES[style], label=label)
attrib = ", ".join([f'{key}="{val}"' for key, val in attrib_dict.items()])
label = r"</TD></TR><TR><TD>".join(labels)
attrib_dict = dict(_ATTRIBS[style], label=label)
pre = '<<TABLE BORDER="0" CELLPADDING="5"><TR><TD>'
post = "</TD></TR></TABLE>>"
attrib = ", ".join(
[
f'{key}="{html.escape(val)}"' if key != "label" else f"{key}={pre}{val}{post}"
for key, val in attrib_dict.items()
]
)
print(f'"{nodeName}" [{attrib}];', file=file)


def _renderTaskNode(nodeName: str, taskDef: TaskDef, file: io.TextIOBase, idx: Any = None) -> None:
"""Render GV node for a task"""
labels = [taskDef.label, taskDef.taskName]
labels = [
f'<B><FONT POINT-SIZE="{_NODELABELPOINTSIZE}">' + taskDef.label + "</FONT></B>",
taskDef.taskName,
]
if idx is not None:
labels.append(f"index: {idx}")
if taskDef.connections:
# don't print collection of str directly to avoid visually noisy quotes
dimensions_str = ", ".join(sorted(taskDef.connections.dimensions))
labels.append(f"dimensions: {dimensions_str}")
labels.append(f"<I>dimensions:</I>&nbsp;{dimensions_str}")
_renderNode(file, nodeName, "task", labels)


Expand All @@ -91,9 +112,9 @@ def _renderQuantumNode(

def _renderDSTypeNode(name: str, dimensions: list[str], file: io.TextIOBase) -> None:
"""Render GV node for a dataset type"""
labels = [name]
labels = [f'<B><FONT POINT-SIZE="{_NODELABELPOINTSIZE}">' + name + "</FONT></B>"]
if dimensions:
labels.append("Dimensions: " + ", ".join(sorted(dimensions)))
labels.append("<I>dimensions:</I>&nbsp;" + ", ".join(sorted(dimensions)))
_renderNode(file, name, "dsType", labels)


Expand Down Expand Up @@ -165,6 +186,9 @@ def graph2dot(qgraph: QuantumGraph, file: Any) -> None:
close = True

print("digraph QuantumGraph {", file=file)
_renderDefault("graph", _ATTRIBS["defaultGraph"], file)
_renderDefault("node", _ATTRIBS["defaultNode"], file)
_renderDefault("edge", _ATTRIBS["defaultEdge"], file)

Check warning on line 191 in python/lsst/ctrl/mpexec/dotTools.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/ctrl/mpexec/dotTools.py#L189-L191

Added lines #L189 - L191 were not covered by tests

allDatasetRefs: dict[str, str] = {}
for taskId, taskDef in enumerate(qgraph.taskGraph):
Expand Down Expand Up @@ -242,6 +266,9 @@ def expand_dimensions(connection: connectionTypes.BaseConnection) -> list[str]:
close = True

print("digraph Pipeline {", file=file)
_renderDefault("graph", _ATTRIBS["defaultGraph"], file)
_renderDefault("node", _ATTRIBS["defaultNode"], file)
_renderDefault("edge", _ATTRIBS["defaultEdge"], file)

allDatasets: set[str | tuple[str, str]] = set()
if isinstance(pipeline, Pipeline):
Expand Down
10 changes: 6 additions & 4 deletions tests/test_dotTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,12 @@ def testPipeline2dot(self):
# It's hard to validate complete output, just checking few basic
# things, even that is not terribly stable.
lines = file.getvalue().strip().split("\n")
nglobals = 3
ndatasets = 10
ntasks = 6
nedges = 16
nextra = 2 # graph header and closing
self.assertEqual(len(lines), ndatasets + ntasks + nedges + nextra)
self.assertEqual(len(lines), nglobals + ndatasets + ntasks + nedges + nextra)

# make sure that all node names are quoted
nodeRe = re.compile(r"^([^ ]+) \[.+\];$")
Expand All @@ -142,8 +143,9 @@ def testPipeline2dot(self):
match = nodeRe.match(line)
if match:
node = match.group(1)
self.assertEqual(node[0] + node[-1], '""')
continue
if node not in ["graph", "node", "edge"]:
self.assertEqual(node[0] + node[-1], '""')
continue
match = edgeRe.match(line)
if match:
for group in (1, 2):
Expand All @@ -152,7 +154,7 @@ def testPipeline2dot(self):
continue

# make sure components are connected appropriately
self.assertIn('"D" -> "D.C";', file.getvalue())
self.assertIn('"D" -> "D.C"', file.getvalue())

# make sure there is a connection created for metadata if someone
# tries to read it in
Expand Down

0 comments on commit a76e6e1

Please sign in to comment.