Aligning elements within a subgraph/cluster

I’m trying to render a graph that looks like a state of an algorithm. For example: I want to render an array, and have another node that has an arrow pointing to an element index.

digraph {
      rankdir=TB;
// 1. Array of primitive types (horizontal)
    subgraph cluster_array1 {
        style=dashed;
        label="int_array";
        labelfontcolor=lightgray;

        array1_before [label="" shape="inviz" width=0 style=invisible margin="0" ];
        array1 [shape=record label="{ {<0>5|<1>4|<2>1|<3>3|<4>2} }" ];
        array1_after [label="" shape="inviz" width=0 style=invisible margin="0"];
    }
    # array1_j [label="j=-1"];

    array1_i [label="i=1"];
    array1_i -> array1:1

    array1_j [label="j=-1"]; # The order of the node arra1_j determines the position of array1_before.
    array1_j -> array1_before;
  


    subgraph cluster_array2 {
        style=dashed;
        label="str_array";
        labelfontcolor=lightgray;

        array2_before [label="" shape="inviz" width=0 style=invisible margin="0" ];
        array2 [shape=record label="{ <0>abc|<1>def|<2>ghi|<3>jkl|<4>mno }" ];
        array2_after [label="" shape="inviz" width=0 style=invisible margin="0"];

    }
    array2_p [label="p=-1"];

    array2_q [label="q=1"];
    array2_q -> array2:1

    array2_p -> array2_before;  
}

Code in viz-js

I need a way to render this correctly. I will be programmatically generating states, so I would want to avoid having to manually edit the text by visually inspecting it.

  1. I want a way to represent an array (horizontal and vertical) and tables, along with the variable name. So, the only reliable option I found was to define a subgraph cluster for each variable, have the label of the cluster as variable name. Then set node shape=“record”. I can easily make them look vertical or horizontal or have 2-D table, and I would be able to add direct link from an external node to each cell. This part works without an issue.
  2. I need a way to have an invisible node to which an external node can have a link to, to represent when the variable reaches outside the bounds of the array. (Like i=-1 or i=n where n is the length of they 0-indexed array). Here I am having issues.
  • For the record node shape, there seems to be no way to keep a zero width (or height for vertical) cells or invisible cells For example: array1 [shape=record label="{ {<.>|<0>5|<1>4|<2>1|<3>3|<4>2} }" ]; creates an empty non-zero width cell.
  • On horizontal 1-D array, I couldn’t find a way to keep the before or after node reliably. For example, to point to the case where the index is -1, I want the pointer arrow to point to element left of 0th element, but depending on when the source node of the edge is added, the before shows up after the array.
  • On vertical 1-D array, represented as record type with n rows of 1 column, here again, there seems to be no easy way to ensure the before/after nodes appear top or bottom of the array.
    rankdir=LR doesn’t work, because rankdir is the property of graph not subgraph.
    Adding a link from before to 0th element of the array and from nth elelemtn to the after node also doesn’t work because the arrow size is significantly higher making it visually horrible.

Please help me get this to work as expected.

This uses HTML-like labels instead of records. That allows cells with no boundaries (sides) and no wrestling with dot about where invisible nodes should be placed.
Use of bgcolor is optional, of course. Just trying to better define the out-of-bounds nature.

//  changed to HTML-like labels,
//    used sides to define out-of-bounds, colors to help location, and compass ports
digraph {
      rankdir=TB;

    subgraph cluster_array1 {
        style=dashed;
        label="int_array";
        labelfontcolor=lightgray;

        array1 [shape=plain label=<
	<table border="0" cellborder="1" cellspacing="0" cellpadding="6"><tr>
	  <td port="before" sides="r" bgcolor="lightpink"> </td>
	  <td port="0">5</td>
	  <td port="1">4</td>
	  <td port="2">1</td>
	  <td port="3">3</td>
	  <td port="4">2</td>
	  <td port="after" sides="l" bgcolor="lightpink"> </td>
	  </tr></table>> ];
    }
 
    array1_i [label="i=1"];
    array1_i -> array1:1
    array1_j [label="j=-1"]; 
    array1_j -> array1:before;

    subgraph cluster_array2 {
        style=dashed;
        label="str_array";
        labelfontcolor=lightgray;

         array2 [shape=plain label=<
 	 <table border="0" cellborder="1" cellspacing="0" cellpadding="6">
	  <tr><td port="before" sides="b" bgcolor="lightpink"> </td></tr>
	  <tr><td port="0">abc</td></tr>
	  <tr><td port="1">def</td></tr>
	  <tr><td port="2">ghi</td></tr>
	  <tr><td port="3">jhl</td></tr>
	  <tr><td port="4">mno</td></tr>
	  <tr><td port="after" sides="t" bgcolor="lightpink"> </td></tr>
	  </table>> ];

    }
    array2_p [label="p=-1"];
    array2_q [label="q=1"];
    ## added compass ports
    array2_q -> array2:1:e
    array2_p -> array2:before:e 
}

Giving:
useSides1

1 Like

Thanks. This works great. It looks like I could now avoid the unnecessary subgraph and have a row will colspan for the name, that gives better customization of the appearance.

One issue, I am not sure if this should be a separate question.

The variables i,j,p,q are all placed at much higher at the top than closer to the nodes. Similarly, for this example, it would have been better if p and q are placed left/right of str_array and have the arrows horizontally.

Although I might be able to manually tune with setting the ranks and rankdir, is there a better way to make the nodes be placed automatically and rankdir chosen for each edge independently to minimize the total length of the edges?

Sorry, one rankdir per graph.
Suggestions - from simplest to hardest:

Here are two graphs in one file (dot -O …) that created two output files that were then glued together with gvpack

//  changed to HTML-like labels,
//    used sides to define out-of-bounds, colors to help location, and compass ports
//  stopped using cluster for array1
//
// write postscript or use dot ... -O

// horizontal graphs
digraph H {
  rankdir=BT;  // note!  BT
  array1 [shape=plain label=<
          <table border="0" cellborder="1" cellspacing="0" cellpadding="6">
          <tr><td colspan="6" sides="b" >int_array</td></tr>
          <tr>
          <td port="before" sides="r" bgcolor="lightpink"> </td>
          <td port="0">5</td>
          <td port="1">4</td>
          <td port="2">1</td>
          <td port="3">3</td>
          <td port="4">2</td>
          <td port="after" sides="l" bgcolor="lightpink"> </td>
          </tr></table>> ];
 
    array1_i [label="i=1"];
    array1_j [label="j=-1"];
  
  array1_i -> array1:1
  array1_j -> array1:before;
}

// vertical graphs 
digraph V {
  rankdir=LR;

  array2 [shape=plain label=<
          <table border="0" cellborder="1" cellspacing="0" cellpadding="6">
          <tr><td colspan="1" sides="b" >str_array</td></tr>
          <tr><td port="before" sides="b" bgcolor="lightpink"> </td></tr>
          <tr><td port="0">abc</td></tr>
          <tr><td port="1">def</td></tr>
          <tr><td port="2">ghi</td></tr>
          <tr><td port="3">jhl</td></tr>
          <tr><td port="4">mno</td></tr>
          <tr><td port="after" sides="t" bgcolor="lightpink"> </td></tr>
          </table>> ];


  array2_p [label="p=-1"];
  array2_q [label="q=1"];
  
## added compass ports
  array2:1:e -> array2_q [dir=back] 
  array2_p -> array2:before:w
}

Giving:
o

Thanks.
I’m not sure about gvpack. But with gvpack would it be possible to add edges between that two graphs?
For example, if I want to add a link from the int_array[0] to the str_array[2]
Is there a JavaScript/browser implementation for gvpack?

Just some curious question: is this a design choice to not support rankdir at subgraph level or is it some implementation limitation that just was not addressed due to the lack of resources?

Yes, mostly a lack of programming resources and know-how at the time. The brittleness of the code doesn’t help.

I understand, and thanks for developing/maintaining this wonderful tool. Considering the initial release was 33 years old, I won’t be surprised the complexity.

Also, using C/C++ also doesn’t help either :frowning_face: (not complaining, that was the best option in the early 90s.).

Do you think it would be possible to support a way to build layout plugins in Rust/Go/JavaScript?
That would increase participation as well.

I am not sure if gvpack could handle edges between packed components, but here are two versions of the requested edges added to the output of gvpack and then piped to neato.
tweakmet
tweakmeb

Not that I am aware of.

My guess: Maybe you would get “best result” and “least heartburn” by explicitly assigning pos values to all the nodes and then just using neato (viz-js) to layout the edges & create the resultant image.
If there are only going to be a few nodes (say < 6-ish), a simple placement algorithm should not be too difficult. Determining the node sizes would be fiddley, but probably manageable (don’t allow infinite font selection).
Easy for me to say.

Or, if viz-js permits, run viz-js twice, behind the scenes. Once to size all the nodes, then your code would only have to determine node placement, and then neato to produce a finished graph.

That’s already supported today for languages/wrappers that expose a C ABI. If you can figure out how to build your plugin into a Shared Object and put it in the Graphviz plugin dir, and comprehend gvplugin_library_load in the Graphviz source, you should be able to wire it up.

Here is a list of known “bindings”, including Go & Rust. External Resources | Graphviz.

Also know that you probably would not have to sweat the edge layout algorithms. Every Graphviz layout engine, except for dot, uses the neato edge routines.

@smattr @steveroush I definitely want to try that. Last I used C/C++ were 25 years ago, when first learning to code before learning Java and others that I never looked back.

Would it be possible for any of you to help me with a tiny tutorial on how to create a layout plugin in any other language like Go, Javascript, Python or any newer languages. In Go, I know how to create a shared object (at least I can find a tutorial) but I don’t know how to get it to work with graphviz.

I am not sure if it is possible to create a plugin in Java or Python. The problem is that these are “managed” languages in the sense that they have a runtime.

Creating a plugin in Go should work, but I don’t know enough about Go to know how to do this. You could look at one of the newer simpler plugins in the tree like plugin/kitty/gvplugin_kitty.c to see what symbols you need to expose.

I would suggest first experimenting with how you call into Go from C. Start with a C file like:

extern int doStuff(const char *message);

int main(void) {
  return doStuff("hello world");
}

Then figure out how to implement doStuff in Go, compile it into a shared library, and then compile the C and have this use your shared library at runtime:

$ cc -std=c99 main.c -lmy_go_lib
$ env LD_LIBRARY_PATH=$(pwd) ./a.out

cat mylib.go

package main

import "C"
import "fmt"

//export doStuff
func doStuff(message *C.char) C.int {
    goMessage := C.GoString(message)
    fmt.Println("Go function received message:", goMessage)
    return 0 
}

Compiled using:
go build -o libmylib.so -buildmode=c-shared mylib.go

This created the .h and .so files

ls -l
total 2880
-rw-r--r--  1 jp  staff     1681 Sep  1 14:06 libmylib.h
-rw-r--r--  1 jp  staff  1458274 Sep  1 14:06 libmylib.so
-rw-r--r--  1 jp  staff      133 Sep  1 14:01 main.c
-rw-r--r--  1 jp  staff      223 Sep  1 14:06 mylib.go

cat main.c

#include <stdio.h>
#include "libmylib.h" // Include the generated Go header file

int main(void) {
    return doStuff("hello world");
}

Compiled using cc -std=c99 main.c -L. -lmylib
./a.out
Go function received message: hello world

Should this be moved into a separate thread, but still keep it public so act as a tutorial?

I’m fine to continue here, as we don’t know what side quests this exploration might go on.