nidi3 / graphviz-java

Use graphviz with pure java
Apache License 2.0
934 stars 107 forks source link

how to construct nested graphs with edges crossing subgraphs? #216

Open NicolasRouquette opened 3 years ago

NicolasRouquette commented 3 years ago

I use the graphviz-java library from Scala.

I would like to construct a graph like the following instead of parsing it from dot.

The following example is adapted from this: https://renenyffenegger.ch/notes/tools/Graphviz/examples/index

import guru.nidi.graphviz.engine.{Format, Graphviz}
import guru.nidi.graphviz.model.MutableGraph
import guru.nidi.graphviz.parse.Parser

import java.io.File

object TestNestedGraph:

  def main(args: Array[String]): Unit =

    val dot =
      """
        |digraph Q {
        |
        |  node [shape=rectangle];
        |
        |
        |  nd_1   [label = "Node 1"];
        |  nd_2   [label = "Node 2"];
        |  nd_3_a [label = "Above Right Node 3"];
        |  nd_3_l [label = "Left of Node 3"];
        |  nd_3   [label = "Node 3"];
        |  nd_3_r [label = "Right of Node 3"];
        |  nd_4   [label = "Node 4"];
        |
        |
        |  nd_3_a -> nd_3_r;
        |  nd_1 -> nd_2 -> nd_3 -> nd_4;
        |
        |  subgraph cluster_R {
        |
        |    {rank=same nd_3_l nd_3 nd_3_r}
        |
        |    nd_3_l -> nd_3 -> nd_3_r [color=grey arrowhead=none];
        |
        |  }
        |
        |}
        |""".stripMargin

    val g: MutableGraph = new Parser().read(dot)

    val gv = Graphviz.fromGraph(g)
    gv.render(Format.DOT).toFile(new File("Subgraph-example.dot"))
    gv.render(Format.SVG_STANDALONE).toFile(new File("Subgraph-example.svg"))

The above produces the following dot result:

digraph "Q" {
"nd_1" ["shape"="rectangle","label"="Node 1"]
"nd_2" ["shape"="rectangle","label"="Node 2"]
"nd_3" ["shape"="rectangle","label"="Node 3"]
"nd_4" ["shape"="rectangle","label"="Node 4"]
"nd_3_a" ["shape"="rectangle","label"="Above Right Node 3"]
"nd_3_r" ["shape"="rectangle","label"="Right of Node 3"]
"nd_3_l" ["shape"="rectangle","label"="Left of Node 3"]
subgraph "cluster_R" {
{
graph ["rank"="same"]
"nd_3_l"
"nd_3"
"nd_3_r"
}
"nd_3_l" -> "nd_3" ["color"="grey","arrowhead"="none"]
"nd_3" -> "nd_3_r" ["color"="grey","arrowhead"="none"]
}
"nd_1" -> "nd_2"
"nd_2" -> "nd_3"
"nd_3" -> "nd_4"
"nd_3_a" -> "nd_3_r"
}

It is unclear to me how to programmatically construct such a graph.

I can create a subgraph with nodes and edges within the subgraph OK. However, as soon as I add an edge between nodes in different subgraphs or between a subgraph node and a toplevel node, I get some strange behavior where the dot output has many duplicated edges.

NicolasRouquette commented 3 years ago

I have tried the following:

import guru.nidi.graphviz.attribute.{Attributes,Label,Shape}
import guru.nidi.graphviz.engine.{Format, Graphviz}
import guru.nidi.graphviz.model.{Factory,MutableGraph}

import java.io.File

object ConstructNestedGraphExample:

  def main(args: Array[String]): Unit =
    val g: MutableGraph = Factory
      .graph("Q")
      .directed()
      .nodeAttr()
      .`with`(Shape.RECTANGLE)
      .toMutable

    val cr: MutableGraph = Factory
      .graph("cluster_R")
      .directed()
      .toMutable()
      .setCluster(true)

    g.add(cr)

    val nd1 = Factory.mutNode("nd_1").add(Label.of("Node 1"))
    g.add(nd1)

    val nd2 = Factory.mutNode("nd_2").add(Label.of("Node 2"))
    g.add(nd2)

    val nd3a = Factory.mutNode("nd_3_a").add(Label.of( "Above Right Node 3"))
    g.add(nd3a)

    val nd3l = Factory.mutNode("nd_3_l").add(Label.of("Left of Node 3"))
    cr.add(nd3l)

    val nd3 = Factory.mutNode("nd_3").add(Label.of("Node 3"))
    cr.add(nd3)

    val nd3r = Factory.mutNode("nd_3_r").add(Label.of("Right of Node 3"))
    cr.add(nd3r)

    val nd4 = Factory.mutNode("nd_4").add(Label.of("Node 4"))
    g.add(nd4)

    //  nd_3_a -> nd_3_r;
    nd3a.links().add(nd3a.linkTo(nd3r))

    //  nd_1 -> nd_2 -> nd_3 -> nd_4;
    nd1.links().add(nd1.linkTo(nd2))
    nd2.links().add(nd2.linkTo(nd3))
    nd3.links().add(nd3.linkTo(nd4))

    // how to add links within the cluster?
    // nd_3_l -> nd_3 -> nd_3_r [color=grey arrowhead=none];

    nd3l.links().add(nd3l.linkTo(nd3))
    nd3.links().add(nd3.linkTo(nd3r))

    val gv = Graphviz.fromGraph(g)
    gv.render(Format.DOT).toFile(new File("ConstructNestedGraphExample.dot"))
    gv.render(Format.SVG_STANDALONE).toFile(new File("ConstructNestedGraphExample.svg"))

I get a duplication of edges in the dot and svg only (with the debugger, I see that the links are not duplicated):

digraph "Q" {
node ["shape"="rectangle"]
"nd_1" ["label"="Node 1"]
"nd_2" ["label"="Node 2"]
"nd_3" ["label"="Node 3"]
"nd_4" ["label"="Node 4"]
"nd_3_r" ["label"="Right of Node 3"]
"nd_3_a" ["label"="Above Right Node 3"]
subgraph "cluster_cluster_R" {
"nd_3_l" ["label"="Left of Node 3"]
"nd_3" ["label"="Node 3"]
"nd_4" ["label"="Node 4"]
"nd_3_r" ["label"="Right of Node 3"]
"nd_3_l" -> "nd_3"
"nd_3" -> "nd_4"
"nd_3" -> "nd_3_r"
}
"nd_1" -> "nd_2"
"nd_2" -> "nd_3"
"nd_3" -> "nd_4"
"nd_3" -> "nd_3_r"
"nd_3_a" -> "nd_3_r"
}

In the above, nd_4 ends up in the cluster even though it was not assigned to it. Edges nd_3 -> nd_4 and nd_3 -> nd_3_r are duplicated even though they are created and added only once.

NicolasRouquette commented 3 years ago

With the immutable API, I managed to construct the desired topology with the following:

import guru.nidi.graphviz.attribute.{Attributes, Label, Shape}
import guru.nidi.graphviz.engine.{Format, Graphviz}
import guru.nidi.graphviz.model.{Factory, Graph}

import java.io.File

object ConstructNestedGraphExample:

  def main(args: Array[String]): Unit =
    val g1: Graph = Factory
      .graph("Q")
      .directed()
      .nodeAttr()
      .`with`(Shape.RECTANGLE)
      .`with`(Factory.node("nd_1").`with`(Label.of("Node 1")))
      .`with`(Factory.node("nd_2").`with`(Label.of("Node 2")))
      .`with`(Factory.node("nd_3_a").`with`(Label.of("Above Right Node 3")))
      .`with`(Factory.node("nd_4").`with`(Label.of("Node 4")))
      .`with`(Factory.node("nd_1").link(Factory.node("nd_2")))
      .`with`(Factory.node("nd_2").link(Factory.node("nd_3")))
      .`with`(Factory.node("nd_3").link(Factory.node("nd_4")))
      .`with`(Factory.node("nd_3_a").link(Factory.node("nd_3_r")))

    val cr: Graph = Factory
      .graph("cluster_R")
      .directed()
      .cluster()
      .`with`(Factory.node("nd_3_l").`with`(Label.of("Left of Node 3")))
      .`with`(Factory.node("nd_3").`with`(Label.of("Node 3")))
      .`with`(Factory.node("nd_3_r").`with`(Label.of("Right of Node 3")))
      .`with`(Factory.node("nd_3_l").link(Factory.node("nd_3")))
      .`with`(Factory.node("nd_3").link(Factory.node("nd_3_r")))

    val g2 = g1
      .`with`(cr)

    val gv = Graphviz.fromGraph(g2)
    gv.render(Format.DOT).toFile(new File("ConstructNestedGraphExample.dot"))
    gv.render(Format.SVG_STANDALONE).toFile(new File("ConstructNestedGraphExample.svg"))

This produces the following graph in dot:

digraph "Q" {
node ["shape"="rectangle"]
"nd_1" ["label"="Node 1"]
"nd_2" ["label"="Node 2"]
"nd_3_a" ["label"="Above Right Node 3"]
"nd_4" ["label"="Node 4"]
subgraph "cluster_cluster_R" {
"nd_3_l" ["label"="Left of Node 3"]
"nd_3" ["label"="Node 3"]
"nd_3_r" ["label"="Right of Node 3"]
"nd_3_l" -> "nd_3"
"nd_3" -> "nd_3_r"
}
"nd_1" -> "nd_2"
"nd_2" -> "nd_3"
"nd_3_a" -> "nd_3_r"
"nd_3" -> "nd_4"
}