Neato output for Mrecord produces significant overlap

61 views Asked by At

Given this demo graph, it renders (vaguely) well in the default engine, and awfully in Neato:

import graphviz

a = [
    [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]],
    [[0, 0], [0, 0], [0, 0], [0, 0], [0, 18.442211], [0, 1.5577889]],
]
e = [
    [0, 7.8787879, 15.353535, 0, 0, 31.212121],
    [0, 0, 0, 0, 0, 0],
    [11.392405, 0, 0, 22.025316, 46.582278, 0],
]
f = [88.607595, 12.121212, 64.646465, 37.974684, 84.97551, 67.23009]
w = [45.555556, 0, 0, 60, 150, 100]
Cuntreat = (100, 250, 80, 200, 150, 130)
Ctreat = (650, 200)
Cfresh = 1
plants = range(len(Cuntreat))
treatments = range(len(Ctreat))

graph = graphviz.Digraph(
    name='treatment_flow', format='svg', engine='neato',
    graph_attr={
        'rankdir': 'LR',
        'overlap': 'false', 'splines': 'true',
    },
)

graph.node(name='fresh', label='Freshwater')
graph.node(name='waste', label='Wastewater')
for plant in plants:
    graph.node(
        name=str(plant),
        shape='Mrecord',
        label=
        '{'
            '{'
                '<fresh_in> fresh|'
                '<untreat_in> untreated|'
                '<treat_in> treated'
            '}|'
            r'Plant \N|'
            '{'
                '<waste_out> waste|'
                '<untreat_out> untreated|'
                + '|'.join(
                    f'<treat_{treatment}_out> treatment {treatment}'
                    for treatment in treatments
                ) +
            '}'
        '}'
    )

for i, a_slice in enumerate(a):
    for j, a_row in enumerate(a_slice):
        for treatment, (contam, flow) in enumerate(zip(Ctreat, a_row)):
            if flow > 0:
                graph.edge(
                    tail_name=f'{i}:treat_{treatment}_out',
                    head_name=f'{j}:treat_in',
                    label=f'{flow:.1f} ({contam*flow:.1f})',
                )
for i, (e_row, contam) in enumerate(zip(e, Cuntreat)):
    for j, flow in enumerate(e_row):
        if flow > 0:
            graph.edge(
                tail_name=f'{i}:untreat_out',
                head_name=f'{j}:untreat_in',
                label=f'{flow:.1f} ({contam*flow:.1f})',
            )
for j, flow in enumerate(f):
    if flow > 0:
        graph.edge(
            tail_name='fresh',
            head_name=f'{j}:fresh_in',
            label=f'{flow:.1f} ({Cfresh*flow:.1f})',
        )
for i, (flow, contam) in enumerate(zip(w, Cuntreat)):
    if flow > 0:
        graph.edge(
            tail_name=f'{i}:waste_out',
            head_name='waste',
            label=f'{flow:.1f} ({contam*flow:.1f})',
        )

graph.view()

neato garbageheap

with output

digraph treatment_flow {
    graph [overlap=false rankdir=LR splines=true]
    fresh [label=Freshwater]
    waste [label=Wastewater]
    0 [label="{{<fresh_in> fresh|<untreat_in> untreated|<treat_in> treated}|Plant \N|{<waste_out> waste|<untreat_out> untreated|<treat_0_out> treatment 0|<treat_1_out> treatment 1}}" shape=Mrecord]
    1 [label="{{<fresh_in> fresh|<untreat_in> untreated|<treat_in> treated}|Plant \N|{<waste_out> waste|<untreat_out> untreated|<treat_0_out> treatment 0|<treat_1_out> treatment 1}}" shape=Mrecord]
    2 [label="{{<fresh_in> fresh|<untreat_in> untreated|<treat_in> treated}|Plant \N|{<waste_out> waste|<untreat_out> untreated|<treat_0_out> treatment 0|<treat_1_out> treatment 1}}" shape=Mrecord]
    3 [label="{{<fresh_in> fresh|<untreat_in> untreated|<treat_in> treated}|Plant \N|{<waste_out> waste|<untreat_out> untreated|<treat_0_out> treatment 0|<treat_1_out> treatment 1}}" shape=Mrecord]
    4 [label="{{<fresh_in> fresh|<untreat_in> untreated|<treat_in> treated}|Plant \N|{<waste_out> waste|<untreat_out> untreated|<treat_0_out> treatment 0|<treat_1_out> treatment 1}}" shape=Mrecord]
    5 [label="{{<fresh_in> fresh|<untreat_in> untreated|<treat_in> treated}|Plant \N|{<waste_out> waste|<untreat_out> untreated|<treat_0_out> treatment 0|<treat_1_out> treatment 1}}" shape=Mrecord]
    1:treat_1_out -> 4:treat_in [label="18.4 (3688.4)"]
    1:treat_1_out -> 5:treat_in [label="1.6 (311.6)"]
    0:untreat_out -> 1:untreat_in [label="7.9 (787.9)"]
    0:untreat_out -> 2:untreat_in [label="15.4 (1535.4)"]
    0:untreat_out -> 5:untreat_in [label="31.2 (3121.2)"]
    2:untreat_out -> 0:untreat_in [label="11.4 (911.4)"]
    2:untreat_out -> 3:untreat_in [label="22.0 (1762.0)"]
    2:untreat_out -> 4:untreat_in [label="46.6 (3726.6)"]
    fresh -> 0:fresh_in [label="88.6 (88.6)"]
    fresh -> 1:fresh_in [label="12.1 (12.1)"]
    fresh -> 2:fresh_in [label="64.6 (64.6)"]
    fresh -> 3:fresh_in [label="38.0 (38.0)"]
    fresh -> 4:fresh_in [label="85.0 (85.0)"]
    fresh -> 5:fresh_in [label="67.2 (67.2)"]
    0:waste_out -> waste [label="45.6 (4555.6)"]
    3:waste_out -> waste [label="60.0 (12000.0)"]
    4:waste_out -> waste [label="150.0 (22500.0)"]
    5:waste_out -> waste [label="100.0 (13000.0)"]
}

Among the specific problems I need to fix:

  • Edge heads and tails should not enter ports at nonsensical directions
  • At least some effort should be made to avoid edge-node overlap
  • Node placement needn't be as it's shown here. For example, moving plant 3 to the right would probably help avoid overlap.

I'm sure there isn't a magic bullet for all of these, but surely Neato should be able to do better?

3

There are 3 answers

1
J_H On

I can't reproduce the symptom. (I mean, it's bad, but not as bad.)

I'm using version 9:

$  brew info graphviz
==> graphviz: stable 9.0.0 (bottled), HEAD
Graph visualization software from AT&T and Bell Labs
https://graphviz.org/
/usr/local/Cellar/graphviz/9.0.0 (330 files, 12.8MB) *

water treatment

Layout should improve a bit if we display smaller plants, and especially if there's fewer of them.

With the dot layout engine, the usual way to fix a train wreck is to mess with
[constraint = false];
sometimes in combination with an edge that does impose a layout constraint but which is [style = invis].

Here is one example of that.

Switch to dot.

 graph = graphviz.Digraph(
     name="treatment_flow",
     format="svg",
-    engine="neato",
+    engine="dot",

Force the rank of selected nodes. Also, some edges won't affect rank.

         )
         + "}"
         "}",
     )
+graph.edge("1", "2", label="fake", style="invis")
+graph.edge("2", "3", label="fake", style="invis")
+graph.edge("4", "5", label="fake", style="invis")
 
 for i, a_slice in enumerate(a):
     for j, a_row in enumerate(a_slice):
         for treatment, (contam, flow) in enumerate(zip(Ctreat, a_row)):
             if flow > 0:
                 graph.edge(
                     tail_name=f"{i}:treat_{treatment}_out",
                     head_name=f"{j}:treat_in",
                     label=f"{flow:.1f} ({contam*flow:.1f})",
+                    constraint="false",
                 )
 for i, (e_row, contam) in enumerate(zip(e, Cuntreat)):
     for j, flow in enumerate(e_row):
         if flow > 0:
             graph.edge(
                 tail_name=f"{i}:untreat_out",
                 head_name=f"{j}:untreat_in",
                 label=f"{flow:.1f} ({contam*flow:.1f})",
+                constraint="false",
             )

It will still be a bit cluttered, but now one more lever is available for shuffling the clutter around.

dot treatment flow

1
sroush On

I'm pretty sure the above image is an output of the dot engine with splines=false - not neato. Note that the nodes are aligned in 3 columns (rankdir=LR), just as dot would produce. Neato ignores rankdir.
Here is the result from the python file provided above (neato) engine:
enter image description here

1
sroush On

Here is the output of the dot engine. Colored edges and labels might help.
enter image description here