Drawing stacked rectangles/boxes

Hi,
I am looking for a way to show stacked rectangles to indicate 1 or more of some node. Something like this image, but with the node labels inside the top box.

image

Although traditionally shows with arrow heads, I want to use stacked rectangle shape for the node. I couldn’t find that in the list of polygon based shapes. What is the easiest way to do that?

  • The easiest way is to create an image with some other tool (like inkscape) and include it as an image. (see cow below)
  • The second easiest is to explicitly overlap 3 nodes and use neato -n (not dot) to maintain that positioning. (see read me below)
  • Harder still, you can define a new node in postscript (see Node Shapes | Graphviz), but then you have to use postscript as your output format
  • Finally, you can write definition(s) for new node shape(s). (See Node Shapes | Graphviz and FAQ | Graphviz)

Easiest:

graph I {
  subgraph cluster_1 {
    MyCoolNode [ label="" image="image_dir/cow.png"]  
  }
}

Giving:
imageProblem1

Next easiest (neato -n)

graph shift {
  node [shape=box style=filled fillcolor=white fixedsize=true width=.95 height=1.4]
   b1 [pos="100,100"]
   b2 [pos="120,80"]
   b3 [pos="140,60" label="read me"]
}

Giving:
shiftedRectangles

Thanks. This seemed to with with local image, but when using with JavaScript on browser, it doesn’t work with a remote URL.
Or did I miss something?

You are correct. It does not work.
(See urls for image (#1664) · Issues · graphviz / graphviz · GitLab for a discussion of the issues).

Thanks.

For now, I am using this workaround of explicitly listing multiple boxes with a vertical ellipsis.

digraph {
  node [shape="box"]
    rankdir="LR"

    external1 [label="" shape="none"]
    external1 -> "Coordinator" [label="Write"]
  
    # external2 [label="" shape="none"]
    # external2 -> "Coordinator" [label="Read"]

    internal1 [label="" shape="none"]
    {rank="same" internal1;"Coordinator"}
    internal1 -> "Coordinator" [label="Timeout" ]

    internal2 [label="Cleanup" shape="none"]
    internal2 -> "Participant#0" [label=""]
    

    "Participant#0" 
    "Participant#1" [label="⋮" shape="none"]
    "Participant#2" 
    "Participant#0" -> "Participant#1" -> "Participant#2" [style="invisible" arrowhead="none"]
    {rank="same" "internal2" "Participant#0";"Participant#1";"Participant#2";}

    "Coordinator" -> "Participant#0" [label="Prepare, Abort, Commit" tooltip="asdfasf" URL="https://fizzbee.io"]

    "Coordinator" -> "Participant#2" [label="Prepare, Abort, Commit" tooltip="naothetr " edgeURL="https://fizzbee.io"]
  
    

}

[dot]
digraph {
node [shape=“box”]
rankdir=“LR”

external1 [label="" shape="none"]
external1 -> "Coordinator" [label="Write"]

# external2 [label="" shape="none"]
# external2 -> "Coordinator" [label="Read"]

internal1 [label="" shape="none"]
{rank="same" internal1;"Coordinator"}
internal1 -> "Coordinator" [label="Timeout" ]

internal2 [label="Cleanup" shape="none"]
internal2 -> "Participant#0" [label=""]


"Participant#0" 
"Participant#1" [label="⋮" shape="none"]
"Participant#2" 
"Participant#0" -> "Participant#1" -> "Participant#2" [style="invisible" arrowhead="none"]
{rank="same" "internal2" "Participant#0";"Participant#1";"Participant#2";}

"Coordinator" -> "Participant#0" [label="Prepare, Abort, Commit" tooltip="asdfasf" URL="https://fizzbee.io"]

"Coordinator" -> "Participant#2" [label="Prepare, Abort, Commit" tooltip="naothetr " edgeURL="https://fizzbee.io"]

}
[/dot]

This mostly works.
The incoming arrows from the left implies actions/requests from external service, the incoming arrows from the top implies internal operations.

I want to be sure, if this pattern of placing 3 nodes vertically will be consistent, if I add more nodes and edges into these participants. So this doesn’t get too ugly. Here,
"Participant#0" -> "Participant#1" -> "Participant#2" [style="invisible" arrowhead="none"]
line seems to ensure this works.

I tried another variant by grouping the Participants into a cluster.

digraph {
  node [shape="box"]
    rankdir="LR"

    external1 [label="" shape="none"]
    external1 -> "Coordinator" [label="Write"]
  
    # external2 [label="" shape="none"]
    # external2 -> "Coordinator" [label="Read"]

    internal1 [label="" shape="none"]
    {rank="same" internal1;"Coordinator"}
    internal1 -> "Coordinator" [label="Timeout" ]

    internal2 [label="" shape="none"]
    internal2 -> "Participant#0" [label="Cleanup"]

    subgraph "cluster_Participant" {
      style="dashed"
      "Participant#0" 
      "Participant#1" [label="⋮" shape="none"]
      "Participant#2" 
      
      # Adding this messes up the order of the placeholder.
      # "Participant#0" -> "Participant#1" -> "Participant#2" [style="invisible" arrowhead="none"]
      
      {rank="min" "Participant#0";"Participant#1";"Participant#2";}
    }

      "Coordinator" -> "Participant#0" [label="Prepare, Abort, Commit" tooltip="asdfasf" URL="https://fizzbee.io"]

    "Coordinator" -> "Participant#2" [label="Prepare, Abort, Commit" tooltip="naothetr " edgeURL="https://fizzbee.io"]
  

}

[dot]
digraph {
node [shape=“box”]
rankdir=“LR”

external1 [label="" shape="none"]
external1 -> "Coordinator" [label="Write"]

# external2 [label="" shape="none"]
# external2 -> "Coordinator" [label="Read"]

internal1 [label="" shape="none"]
{rank="same" internal1;"Coordinator"}
internal1 -> "Coordinator" [label="Timeout" ]

internal2 [label="" shape="none"]
internal2 -> "Participant#0" [label="Cleanup"]

subgraph "cluster_Participant" {
  style="dashed"
  "Participant#0" 
  "Participant#1" [label="⋮" shape="none"]
  "Participant#2" 
  
  # Adding this messes up the order of the placeholder. Commenting this line works
  "Participant#0" -> "Participant#1" -> "Participant#2" [style="invisible" arrowhead="none"]
  
  {rank="min" "Participant#0";"Participant#1";"Participant#2";}
}

  "Coordinator" -> "Participant#0" [label="Prepare, Abort, Commit" tooltip="asdfasf" URL="https://fizzbee.io"]

"Coordinator" -> "Participant#2" [label="Prepare, Abort, Commit" tooltip="naothetr " edgeURL="https://fizzbee.io"]

}
[/dot]
In this version, the order of the elipses node is messed up. The same line that was required in the previous version, is breaking the order here.
"Participant#0" -> "Participant#1" -> "Participant#2" [style="invisible" arrowhead="none"]
If I comment this line out, it works.

Again, I want to understand, why this happens? Second, is there any way to guarantee the order of nodes within a subgraph

One minor issue is, I am not able to get the Cleanup action from the top. rank=“same” does not work when the nodes are across clusters. Is there a way to ensure it works?


A node shape like stackedbox would make it a lot simpler, I am not sure how much effort it is to add that. Is there a way I can get some help on that?

Btw, thanks for all the answers on this and the other thread. To give a context, I am working on a design specification language (formal methods) for distributed systems. In addition to model checking the design for correctness and performance evaluation at the design phase before the implementation phase, I want to generate these beautiful visualizations automatically. So that can be shared with the reviewers and teammates.
With your answers, I rolled out the new feature today.

This replaces the cluster & contents w/ a table, giving better control of positioning.
I just had a thought on stacked boxes. Need to do some experimenting.

/**********************************************************************

  from:  https://forum.graphviz.org/t/drawing-stacked-rectangles-boxes/2412/6

  replaced cluster & contents with a table to force positions

**********************************************************************/
digraph {
  node [shape="box"]
    rankdir="LR"
    ranksep=1.4     // space ranks out
    splines=false  // personal preference

    external1 [label="" shape="none"]
    external1 -> "Coordinator" [label="Write"]
    # external2 [label="" shape="none"]
    # external2 -> "Coordinator" [label="Read"]

    internal1 [label="" shape="none"]
    {rank="same" internal1;"Coordinator"}
    internal1 -> "Coordinator" [label="Timeout" ]

    internal2 [label="" shape="none"]
    internal2 -> cluster_Participant:p1:nw [label="Cleanup"]  // nw corber of cell

    "cluster_Participant" [shape=none label=<
      <table cellpadding="14" cellspacing="8" style="dashed">
      <tr><td port="p1">Participant #0</td></tr>
      <tr><td border="0">&#x022EE;</td></tr>
      <tr><td port="p2">Participant #2</td></tr>
      </table>>]

      "Coordinator" -> cluster_Participant:p1 [label="Prepare, Abort, Commit" tooltip="asdfasf" URL="https://fizzbee.io"]

      "Coordinator" -> cluster_Participant:p2 [label="Prepare, Abort, Commit" tooltip="naothetr " edgeURL="https://fizzbee.io"]
}

Giving:

1 Like

Stacked boxes - using a single table!

digraph {

    stack [shape=none label=<
      <table cellpadding="8" cellspacing="0" border="0" cellborder="1">
      <tr>
        <td sides="tl" ></td><td sides="t" ></td><td sides="tr" ></td><td border="0" ></td><td border="0" ></td>
      </tr>
      <tr>
        <td sides="l" ></td><td sides="tl" ></td><td sides="t" ></td><td sides="tr" ></td><td border="0" ></td>
      </tr>
      <tr>
        <td sides="bl" ></td><td sides="lr" ></td><td sides="tl" ></td><td sides="t" ></td><td sides="tr" ></td>
      </tr>
      <tr>
        <td sides="r" ></td><td sides="rb" ></td><td colspan="3" rowspan="3" sides="lbr" >read me</td>
      </tr>	
      </table>>]
}

Giving:
stackedBox1

1 Like

I found another workaround. As my target is to run this on the browser, I am planning to edit the SVG in javascript.

For that, I started with the used usual node definition and added a class fizzbee.

digraph {
  pad = "0.16" // around 11.5 points to allow when changing box to overlapping box
  node [shape="box"]
    rankdir="LR"

    external1 [label="" shape="none"]
    external1 -> "Coordinator" [label="Write"]
  
    # external2 [label="" shape="none"]
    # external2 -> "Coordinator" [label="Read"]

    internal1 [label="" shape="none"]
    {rank="same" internal1;"Coordinator"}
    internal1 -> "Coordinator" [label="Timeout" ]

    internal2 [label="Cleanup" shape="none"]
    {rank="same" internal2;"Participant"}
    internal2 -> "Participant" [label=""]

    "Participant" [class="fizzbee" ]
    
    "Coordinator" -> "Participant" [label="Prepare, Abort, Commit"]

}

[dot]
digraph {
pad = “0.16” // around 11.5 points to allow when changing box to overlapping box
node [shape=“box”]
rankdir=“LR”

external1 [label="" shape="none"]
external1 -> "Coordinator" [label="Write"]

# external2 [label="" shape="none"]
# external2 -> "Coordinator" [label="Read"]

internal1 [label="" shape="none"]
{rank="same" internal1;"Coordinator"}
internal1 -> "Coordinator" [label="Timeout" ]

internal2 [label="Cleanup" shape="none"]
{rank="same" internal2;"Participant"}
internal2 -> "Participant" [label=""]

"Participant" [class="fizzbee" ]

"Coordinator" -> "Participant" [label="Prepare, Abort, Commit"]

}
[/dot]
The generated SVG contains something like,

<g xmlns="http://www.w3.org/2000/svg" id="node5" class="node fizzbee">
<title>Participant</title>
<polygon fill="none" stroke="black" points="453.48,-36 376.83,-36 376.83,0 453.48,0 453.48,-36"/>
<text text-anchor="middle" x="415.16" y="-13.8" font-family="Times,serif" font-size="14.00">Participant</text>
</g>

I just have to replicate the polygon, and I get the image I wanted.
Javascript code looks like this

const nodes = document.querySelectorAll('g.node.fizzbee');
nodes.forEach((node) => {
  // Find the polygon within each node
  const polygon = node.querySelector('polygon');

  if (polygon) {
    // Get the original points attribute
    const originalPoints = polygon.getAttribute('points');
    const pointsArray = originalPoints.split(' ').map(point => {
      const [x, y] = point.split(',');
      return { x: parseFloat(x), y: parseFloat(y) };
    });

    // Function to create a polygon with an offset
    const createOffsetPolygon = (offset) => {
      const newPolygon = polygon.cloneNode();
      const newPoints = pointsArray.map(({ x, y }) => {
        return `${x + offset},${y + offset}`;
      }).join(' ');

      newPolygon.setAttribute('points', newPoints);
      newPolygon.setAttribute('fill', 'white');  // Set the fill to white
      return newPolygon;
    };

    // Create the first and second offset polygons
    const offsetPolygon1 = createOffsetPolygon(4);
    const offsetPolygon2 = createOffsetPolygon(8);

    // Update the original polygon's fill to white
    polygon.setAttribute('fill', 'white');

    // Append the new polygons to the node
    node.insertBefore(offsetPolygon2, polygon);
    node.insertBefore(offsetPolygon1, polygon);
  }
});

This gives, the desired image. The extra box goes out of the viewport, so I had to add the padding. (Ideally, it would be wonderful if that could be done per node instead, but this works so good for now)

image-overlapping-boxes

Thanks. The table trick is wonderful. I will probably use that.

But if I increase the length of the string, the sizes get a bit messed up. But that is a small thing though.

[dot]
digraph {

stack [shape=none label=<
  <table cellpadding="8" cellspacing="0" border="0" cellborder="1">
  <tr>
    <td sides="tl" ></td><td sides="t" ></td><td sides="tr" ></td><td border="0" ></td><td border="0" ></td>
  </tr>
  <tr>
    <td sides="l" ></td><td sides="tl" ></td><td sides="t" ></td><td sides="tr" ></td><td border="0" ></td>
  </tr>
  <tr>
    <td sides="bl" ></td><td sides="lr" ></td><td sides="tl" ></td><td sides="t" ></td><td sides="tr" ></td>
  </tr>
  <tr>
    <td sides="r" ></td><td sides="rb" ></td><td colspan="3" rowspan="3" sides="lbr" >read me, <br/>this is a much longer text, <br/> with multiple lines</td>
  </tr>	
  </table>>]

}
[/dot]

This is probably not a bug issue, it just gives a different observer perspective

You might be able to repurpose contrib/dot_url_resolve.py · f3eff48ed8b4967042028abdcf0208c56f1b9ae5 · graphviz / graphviz · GitLab somewhere in your workflow.