Dependency graph with colors (based on the organization) (#7052)

This commit is contained in:
Ondra Pelech 2022-10-17 19:25:02 +02:00 committed by GitHub
parent fda734c6b5
commit 01a501f4f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 127 additions and 248 deletions

View File

@ -30,86 +30,15 @@ THE SOFTWARE.
<meta charset="utf-8"> <meta charset="utf-8">
<title>Dependency Graph</title> <title>Dependency Graph</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> <script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> <script src="https://unpkg.com/@hpcc-js/wasm@0.3.15/dist/index.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre-d3/0.4.16/dagre-d3.min.js"></script> <script src="https://unpkg.com/d3-graphviz@3.2.0/build/d3-graphviz.js"></script>
<script src="https://dagrejs.github.io/project/graphlib-dot/v0.6.3/graphlib-dot.min.js"></script>
<script src="dependencies.dot.js"></script> <script src="dependencies.dot.js"></script>
<style> <body>
body { <div id="graph"></div>
margin: 0; </body>
overflow: hidden;
}
.node {
white-space: nowrap;
}
.node rect,
.node circle,
.node ellipse {
stroke: #333;
fill: #fff;
stroke-width: 1.5px;
}
.cluster rect {
stroke: #333;
fill: #000;
fill-opacity: 0.1;
stroke-width: 1.5px;
}
.edgePath path.path {
stroke: #333;
stroke-width: 1.5px;
fill: none;
}
</style>
<style>
h1, h2 {
color: #333;
}
</style>
<body onLoad="initialize()">
<svg width=1280 height=1024>
<g/>
</svg>
<script> <script>
function initialize() { d3.select("#graph").graphviz().renderDot(decodeURIComponent(data));
// Set up zoom support
var svg = d3.select("svg"),
inner = d3.select("svg g"),
zoom = d3.behavior.zoom().on("zoom", function() {
inner.attr("transform", "translate(" + d3.event.translate + ")" +
"scale(" + d3.event.scale + ")");
});
svg.attr("width", window.innerWidth);
svg.call(zoom);
// Create and configure the renderer
var render = dagreD3.render();
function tryDraw(inputGraph) {
var g;
{
g = graphlibDot.read(inputGraph);
g.graph().rankdir = "LR";
d3.select("svg g").call(render, g);
// Center the graph
var initialScale = 0.10;
zoom
.translate([(svg.attr("width") - g.graph().width * initialScale) / 2, 20])
.scale(initialScale)
.event(svg);
svg.attr('height', g.graph().height * initialScale + 40);
}
}
tryDraw(decodeURIComponent(data));
}
</script> </script>
</body>

View File

@ -11,22 +11,37 @@ package graph
package rendering package rendering
object DOT { object DOT {
val EvictedStyle = "stroke-dasharray: 5,5" val EvictedStyle = "dashed"
def dotGraph( def dotGraph(
graph: ModuleGraph, graph: ModuleGraph,
dotHead: String, dotHead: String,
nodeFormation: (String, String, String) => String, nodeFormation: (String, String, String) => String,
labelRendering: HTMLLabelRendering labelRendering: HTMLLabelRendering,
colors: Boolean
): String = { ): String = {
val nodes = { val nodes = {
for (n <- graph.nodes) yield { for (n <- graph.nodes) yield {
val style = if (n.isEvicted) EvictedStyle else ""
val label = nodeFormation(n.id.organization, n.id.name, n.id.version) val label = nodeFormation(n.id.organization, n.id.name, n.id.version)
""" "%s"[%s style="%s"]""".format( val style = if (n.isEvicted) EvictedStyle else ""
val penwidth = if (n.isEvicted) "3" else "5"
val color = if (colors) {
val orgHash = n.id.organization.hashCode
val r = (orgHash >> 16) & 0xFF
val g = (orgHash >> 8) & 0xFF
val b = (orgHash >> 0) & 0xFF
val r1 = (r * 0.90).toInt
val g1 = (g * 0.90).toInt
val b1 = (b * 0.90).toInt
(r1 << 16) | (g1 << 8) | (b1 << 0)
} else 0
s""" "%s"[shape=box %s style="%s" penwidth="%s" color="%s"]"""
.format(
n.id.idString, n.id.idString,
labelRendering.renderLabel(label), labelRendering.renderLabel(label),
style style,
penwidth,
f"#$color%06X",
) )
} }
}.sorted.mkString("\n") }.sorted.mkString("\n")

View File

@ -41,6 +41,9 @@ abstract class DependencyTreeKeys {
taskKey[File]("Creates a graphml file containing the dependency-graph for a project") taskKey[File]("Creates a graphml file containing the dependency-graph for a project")
val dependencyDotFile = val dependencyDotFile =
settingKey[File]("The location the dot file should be generated at") settingKey[File]("The location the dot file should be generated at")
val dependencyDotNodeColors = settingKey[Boolean](
"The boxes of nodes are painted with colors. Otherwise they're black."
)
val dependencyDotNodeLabel = settingKey[(String, String, String) => String]( val dependencyDotNodeLabel = settingKey[(String, String, String) => String](
"Returns a formated string of a dependency. Takes organization, name and version as parameters" "Returns a formated string of a dependency. Takes organization, name and version as parameters"
) )

View File

@ -109,15 +109,17 @@ object DependencyTreeSettings {
dependencyTreeModuleGraph0.value, dependencyTreeModuleGraph0.value,
dependencyDotHeader.value, dependencyDotHeader.value,
dependencyDotNodeLabel.value, dependencyDotNodeLabel.value,
rendering.DOT.AngleBrackets rendering.DOT.AngleBrackets,
dependencyDotNodeColors.value
), ),
dependencyDot := writeToFile(dependencyDot / asString, dependencyDotFile).value, dependencyDot := writeToFile(dependencyDot / asString, dependencyDotFile).value,
dependencyDotHeader := dependencyDotHeader :=
"""|digraph "dependency-graph" { """|digraph "dependency-graph" {
| graph[rankdir="LR"] | graph[rankdir="LR"; splines=polyline]
| edge [ | edge [
| arrowtail="none" | arrowtail="none"
| ]""".stripMargin, | ]""".stripMargin,
dependencyDotNodeColors := true,
dependencyDotNodeLabel := { (organization: String, name: String, version: String) => dependencyDotNodeLabel := { (organization: String, name: String, version: String) =>
"""%s<BR/><B>%s</B><BR/>%s""".format(organization, name, version) """%s<BR/><B>%s</B><BR/>%s""".format(organization, name, version)
}, },
@ -192,7 +194,8 @@ object DependencyTreeSettings {
graph, graph,
dependencyDotHeader.value, dependencyDotHeader.value,
dependencyDotNodeLabel.value, dependencyDotNodeLabel.value,
rendering.DOT.LabelTypeHtml rendering.DOT.AngleBrackets,
dependencyDotNodeColors.value
) )
val link = DagreHTML.createLink(dotGraph, target.value) val link = DagreHTML.createLink(dotGraph, target.value)
streams.value.log.info(s"HTML graph written to $link") streams.value.log.info(s"HTML graph written to $link")

View File

@ -17,36 +17,36 @@ lazy val test_project = project
val dotFile = (dependencyDot in Compile).value val dotFile = (dependencyDot in Compile).value
val expectedGraph = val expectedGraph =
"""digraph "dependency-graph" { """digraph "dependency-graph" {
| graph[rankdir="LR"] | graph[rankdir="LR"; splines=polyline]
| edge [ | edge [
| arrowtail="none" | arrowtail="none"
| ] | ]
| "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT"[label=<justadependencyproject<BR/><B>justadependencyproject_2.9.2</B><BR/>0.1-SNAPSHOT> style=""] | "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT"[shape=box label=<justadependencyproject<BR/><B>justadependencyproject_2.9.2</B><BR/>0.1-SNAPSHOT> style="" penwidth="5" color="#B6E316"]
| "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT"[label=<justatransitivedependencyproject<BR/><B>justatransitivedependencyproject_2.9.2</B><BR/>0.1-SNAPSHOT> style=""] | "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT"[shape=box label=<justatransitivedependencyproject<BR/><B>justatransitivedependencyproject_2.9.2</B><BR/>0.1-SNAPSHOT> style="" penwidth="5" color="#0E92BE"]
| "justatransivitedependencyendpointproject:justatransivitedependencyendpointproject_2.9.2:0.1-SNAPSHOT"[label=<justatransivitedependencyendpointproject<BR/><B>justatransivitedependencyendpointproject_2.9.2</B><BR/>0.1-SNAPSHOT> style=""] | "justatransivitedependencyendpointproject:justatransivitedependencyendpointproject_2.9.2:0.1-SNAPSHOT"[shape=box label=<justatransivitedependencyendpointproject<BR/><B>justatransivitedependencyendpointproject_2.9.2</B><BR/>0.1-SNAPSHOT> style="" penwidth="5" color="#9EAD1B"]
| "test_project:test_project_2.9.2:0.1-SNAPSHOT"[label=<test_project<BR/><B>test_project_2.9.2</B><BR/>0.1-SNAPSHOT> style=""] | "test_project:test_project_2.9.2:0.1-SNAPSHOT"[shape=box label=<test_project<BR/><B>test_project_2.9.2</B><BR/>0.1-SNAPSHOT> style="" penwidth="5" color="#C37661"]
| "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT" -> "justatransivitedependencyendpointproject:justatransivitedependencyendpointproject_2.9.2:0.1-SNAPSHOT" | "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT" -> "justatransivitedependencyendpointproject:justatransivitedependencyendpointproject_2.9.2:0.1-SNAPSHOT"
| "test_project:test_project_2.9.2:0.1-SNAPSHOT" -> "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT" | "test_project:test_project_2.9.2:0.1-SNAPSHOT" -> "justadependencyproject:justadependencyproject_2.9.2:0.1-SNAPSHOT"
| "test_project:test_project_2.9.2:0.1-SNAPSHOT" -> "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT" | "test_project:test_project_2.9.2:0.1-SNAPSHOT" -> "justatransitivedependencyproject:justatransitivedependencyproject_2.9.2:0.1-SNAPSHOT"
|} |}
""".stripMargin """.stripMargin
val graph : String = scala.io.Source.fromFile(dotFile.getAbsolutePath).mkString val graph: String = scala.io.Source.fromFile(dotFile.getAbsolutePath).mkString
val errors = compareByLine(graph, expectedGraph) val errors = compareByLine(graph, expectedGraph)
require(errors.isEmpty , errors.mkString("\n")) require(errors.isEmpty, errors.mkString("\n"))
() ()
} }
) )
def compareByLine(got : String, expected : String) : Seq[String] = { def compareByLine(got: String, expected: String): Seq[String] = {
val errors = ListBuffer[String]() val errors = ListBuffer[String]()
got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach { case((got_line : String, expected_line : String), i : Int) => got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach {
if(got_line != expected_line) { case ((got_line: String, expected_line: String), i: Int) =>
errors.append( if (got_line != expected_line) {
"""not matching lines at line %s errors.append("""not matching lines at line %s
|expected: %s |expected: %s
|got: %s |got: %s
|""".stripMargin.format(i,expected_line, got_line)) |""".stripMargin.format(i, expected_line, got_line))
} }
} }
errors errors

View File

@ -48,108 +48,37 @@ lazy val test_project = project
|<meta charset="utf-8"> |<meta charset="utf-8">
|<title>Dependency Graph</title> |<title>Dependency Graph</title>
| |
|<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> |<script src="https://d3js.org/d3.v5.min.js"></script>
|<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> |<script src="https://unpkg.com/@hpcc-js/wasm@0.3.15/dist/index.min.js"></script>
|<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre-d3/0.4.16/dagre-d3.min.js"></script> |<script src="https://unpkg.com/d3-graphviz@3.2.0/build/d3-graphviz.js"></script>
|<script src="https://dagrejs.github.io/project/graphlib-dot/v0.6.3/graphlib-dot.min.js"></script>
|<script src="dependencies.dot.js"></script> |<script src="dependencies.dot.js"></script>
| |
|<style> |<body>
| body { |<div id="graph"></div>
| margin: 0; |</body>
| overflow: hidden;
| }
| .node {
| white-space: nowrap;
| }
|
| .node rect,
| .node circle,
| .node ellipse {
| stroke: #333;
| fill: #fff;
| stroke-width: 1.5px;
| }
|
| .cluster rect {
| stroke: #333;
| fill: #000;
| fill-opacity: 0.1;
| stroke-width: 1.5px;
| }
|
| .edgePath path.path {
| stroke: #333;
| stroke-width: 1.5px;
| fill: none;
| }
|</style>
|
|<style>
| h1, h2 {
| color: #333;
| }
|</style>
|
|<body onLoad="initialize()">
|
|<svg width=1280 height=1024>
| <g/>
|</svg>
| |
|<script> |<script>
|function initialize() { | d3.select("#graph").graphviz().renderDot(decodeURIComponent(data));
| // Set up zoom support
| var svg = d3.select("svg"),
| inner = d3.select("svg g"),
| zoom = d3.behavior.zoom().on("zoom", function() {
| inner.attr("transform", "translate(" + d3.event.translate + ")" +
| "scale(" + d3.event.scale + ")");
| });
| svg.attr("width", window.innerWidth);
|
| svg.call(zoom);
| // Create and configure the renderer
| var render = dagreD3.render();
| function tryDraw(inputGraph) {
| var g;
| {
| g = graphlibDot.read(inputGraph);
| g.graph().rankdir = "LR";
| d3.select("svg g").call(render, g);
|
| // Center the graph
| var initialScale = 0.10;
| zoom
| .translate([(svg.attr("width") - g.graph().width * initialScale) / 2, 20])
| .scale(initialScale)
| .event(svg);
| svg.attr('height', g.graph().height * initialScale + 40);
| }
| }
| tryDraw(decodeURIComponent(data));
|}
|</script> |</script>
|</body>
| |
""".stripMargin """.stripMargin
val html : String = scala.io.Source.fromFile(htmlFile).mkString val html: String = scala.io.Source.fromFile(htmlFile).mkString
val errors = compareByLine(html, expectedHtml) val errors = compareByLine(html, expectedHtml)
require(errors.isEmpty , errors.mkString("\n")) require(errors.isEmpty, errors.mkString("\n"))
() ()
} }
) )
def compareByLine(got : String, expected : String) : Seq[String] = { def compareByLine(got: String, expected: String): Seq[String] = {
val errors = ListBuffer[String]() val errors = ListBuffer[String]()
got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach { case((got_line : String, expected_line : String), i : Int) => got.split("\n").zip(expected.split("\n").toSeq).zipWithIndex.foreach {
if(got_line != expected_line) { case ((got_line: String, expected_line: String), i: Int) =>
errors.append( if (got_line != expected_line) {
"""not matching lines at line %s errors.append("""not matching lines at line %s
|expected: %s |expected: %s
|got: %s |got: %s
|""".stripMargin.format(i,expected_line, got_line)) |""".stripMargin.format(i, expected_line, got_line))
} }
} }
errors errors