How can I create named edge "types" in Graphviz/dot/neato?
Asked Answered
D

3

7

I need to draw a diagram with graphviz/dot where there are common edge types between nodes and am trying to find a way to define a label for each type of edge and then use that label multiple times in the diagram.

For example imagine the traditional ceiling fan FSM example where it's initially in state OFF and every time someone pulls the cord it changes to a new state based on the speed of the fan:

     Pull         Pull        Pull
OFF ------> HIGH ------> MED ------> LOW
 ^                                    |
 |                Pull                |
 +------------------------------------+

Every edge is named "Pull" and I can define that in dot by using:

digraph fan {
    OFF  -> HIGH [label="Pull"];
    HIGH -> MED  [label="Pull"];
    MED  -> LOW  [label="Pull"];
    LOW  -> OFF  [label="Pull"];
}

BUT I don't want to keep specifying the same textual label every time because

  1. My labels can get quite long so that's error-prone, and
  2. My edges have other attributes like color in addition to label, and
  3. I have a selection of multiple different types of edge so I want to make SURE that edge type "A" used in different contexts in the diagram always has all the same attributes.

I expected dot to have a syntax that would let me define names for my edge types, something like:

digraph fan {
    edge_a [label="Pull"];

    OFF  -> HIGH edge_a;
    HIGH -> MED  edge_a;
    MED  -> LOW  edge_a;
    LOW  -> OFF  edge_a;
}

but of course what the really does is create a node named "Pull" and unlabeled edges.

I've been searching online for a few hours with no success. Anyone know how to define edge types up front to be used in multiple locations?

Update: @vaettchen had suggested defining an edge type then listing all of the transitions for that edge type, then defining the next edge type followed by it's transitions. While that would technically solve my problem, it would introduce a couple of others because my graphs today can look like:

digraph {
    subgraph cluster_1 {
        a -> b [label="type x", color=red, style=solid];
        b -> a [label="type y", color=green, style=dashed];

        b -> c [label="type x", color=red, style=solid];
        c -> b [label="type y", color=green, style=dashed];

        c -> d [label="type z", color=blue, style=dotted];
    }

    subgraph cluster_2 {
        d -> e [label="type x", color=red, style=solid];
        e -> d [label="type y", color=green, style=dashed];

        e -> f [label="type x", color=red, style=solid];
        f -> e [label="type y", color=green, style=dashed];

        f -> c [label="type z", color=blue, style=dotted];
    }
}

and to rearrange that by edge type I'd lose the immediate visual clarity in the code of having the bidirectional edges next to each other (a->b and b->a) and I'd have to explicitly list the nodes within each subgraph and I'd have to pull the subgraph-internal edge definitions up into the main graph:

digraph {
    edge [label="type x", color=red, style=solid];
    a -> b;
    b -> c;
    d -> e;
    e -> f;

    edge [label="type y", color=green, style=dashed];
    b -> a;
    c -> b;
    e -> d;
    f -> e;

    edge [label="type z", color=blue, style=dotted];
    c -> d;
    f -> c;

    subgraph cluster_1 {
        a; b; c;
    }

    subgraph cluster_2 {
        d; e; f;
    }
}

So while it would solve the problem I asked about and I appreciate the suggestion, I'm not sure it's worth the tradeoff as you end up with the equivalent of a C program where you had to define all of your variables outside of the functions and organize them by their type rather than logical associations.

To be clear, given the above example what I was really hoping for would look like the following if such an "edge_type" definition keyword existed:

digraph {
    edge_type edge_x [label="type x", color=red, style=solid];
    edge_type edge_y [label="type y", color=green, style=dashed];
    edge_type edge_z [label="type z", color=blue, style=dotted];

    subgraph cluster_1 {
        a -> b edge_x;
        b -> a edge_y;

        b -> c edge_x;
        c -> b edge_y;

        c -> d edge_z;
    }

    subgraph cluster_2 {
        d -> e edge_x;
        e -> d edge_y;

        e -> f edge_x;
        f -> e edge_y;

        f -> c edge_z;
    }
}
Dibasic answered 5/10, 2017 at 21:31 Comment(2)
Thanks again, I'll take a look. Talking of github, I was hoping I could find a way to include .dot specs in a README.md and have the graph rendered automatically when someone was viewing it on github. So if I had a textual model of a system in .dot, I didn't have to manually keep an image of it up to date too. I found a site (github.com/TLmaK0/gravizo) I could link to with a reference to my .dot file and that actually worked BUT the github site I'm working on is proprietary so that external site can't actually see my internal .dot files. Have you come across anything similar?Dibasic
Basically it'd be some software I could install in a github repo which does what grazivo does (but now could use your m4 preprocessor too!)Dibasic
S
8

Not really an answer but "food for thought" as I don't think named labels exist in graphviz: You could define a default label for the following edges. That works well if your workflow allows to define edges all in one place. Example:

digraph rs
{
    node[ shape = box, style = rounded]

    edge[ label = "pull" ];
    { A B } -> C;
    G -> H;
    C -> D[ label = "stop" ];
    edge[ label = "push"];
    D -> { E F };
    edge[ color = red, fontcolor = red ];
    { E F } -> G;
}

which yields

enter image description here

I have also tried to implement your diagramm with

digraph fan 
{
    splines = ortho;
    node [ shape=box ]

    edge [ xlabel = "Pull", minlen = 4 ];
    { rank = same; OFF  -> HIGH -> LOW; }
    LOW:s -> OFF:s;
}

which produces

enter image description here

so it looks good but with all the tweaking is difficult to expand.

Sarawak answered 6/10, 2017 at 9:39 Comment(4)
Thanks for the suggestions. I could technically do what you suggest wrt defining edge characteristics and then listing all of the transitions of that type, but right now I have edges going in both directions between most nodes (e.g. Off->On and On->Off) plus I have subgraphs where edge type A occurs in multiple subgraphs so I'd be adding some more work in listing nodes explicitly in each subgraph, pulling the internal edge definitions up out of the subgraphs and I'd lose the legibility symmetry check of having the to/from edges for each pair of nodes collocated.Dibasic
Yeah I thought it's more complicated than the fan thing... -- Do you know gvpr? That's too much for me but you may find ways to put it at your service.Sarawak
gvpr is interesting, thanks for the suggestion. I had been considering writing an awk program to preprocess a dot-like file augmented with edge type definitions if dot doesn't support that natively but maybe gvpr would be a more robust approach. I need to do a bit more reading...Dibasic
Looks like though this isn't the answer I was hoping for, it seems to be the right answer so I'm accepting it, thanks again!Dibasic
S
9

I think I got your solution, using m4 (thanks to Simon). Using and adapting your sample, I created a file called gv.m4:

digraph {
    define(`edge_x',`[label="type x", color=red, style=solid]')
    define(`edge_y',`[label="type y", color=green, style=dashed]')
    define(`edge_z',`[label="type z", color=blue, style=dotted]')

    subgraph cluster_1 {
        a -> b edge_x;
        b -> a edge_y;

        b -> c edge_x;
        c -> b edge_y;

        c -> d edge_z;
    }

    subgraph cluster_2 {
        d -> e edge_x;
        e -> d edge_y;

        e -> f edge_x;
        f -> e edge_y;

        f -> c edge_z;
    }
}

and converted it with the simple command

m4 gv.m4 > gv.dot

which now contains your defined edges

digraph {

    subgraph cluster_1 {
        a -> b [label="type x", color=red, style=solid];
        b -> a [label="type y", color=green, style=dashed];

        b -> c [label="type x", color=red, style=solid];
        c -> b [label="type y", color=green, style=dashed];

        c -> d [label="type z", color=blue, style=dotted];
    }

    subgraph cluster_2 {
        d -> e [label="type x", color=red, style=solid];
        e -> d [label="type y", color=green, style=dashed];

        e -> f [label="type x", color=red, style=solid];
        f -> e [label="type y", color=green, style=dashed];

        f -> c [label="type z", color=blue, style=dotted];
    }
}

and yields the expected graph:

enter image description here

You can do much more with m4 - stuff that is missing in graphViz, like maintaining and (even conditionally) including subfiles. For example, if you put your two subgraphs into two separate files gv1.txt and gv2.txt, this would work nicely:

digraph incl
{
    define(`edge_x',`[label="type x", color=red, style=solid]')
    define(`edge_y',`[label="type y", color=green, style=dashed]')
    define(`edge_z',`[label="type z", color=blue, style=dotted]')
    include(gv1.txt)
    include(gv2.txt)
     e -> d[ color = yellow, label = "this is new!"];
}

enter image description here

Sarawak answered 22/10, 2017 at 9:29 Comment(2)
Interesting, thanks. In your define lines you use a mixture of backticks and apostrophes (e.g. `edge_x' = backtick edge apostrophe) - is that REALLY the syntax that needs to be used or is that a typo? I don't have m4 available to test with...Dibasic
That's really the syntax required. The code is copied & pasted from from the actual test which produced the graphs.Sarawak
S
8

Not really an answer but "food for thought" as I don't think named labels exist in graphviz: You could define a default label for the following edges. That works well if your workflow allows to define edges all in one place. Example:

digraph rs
{
    node[ shape = box, style = rounded]

    edge[ label = "pull" ];
    { A B } -> C;
    G -> H;
    C -> D[ label = "stop" ];
    edge[ label = "push"];
    D -> { E F };
    edge[ color = red, fontcolor = red ];
    { E F } -> G;
}

which yields

enter image description here

I have also tried to implement your diagramm with

digraph fan 
{
    splines = ortho;
    node [ shape=box ]

    edge [ xlabel = "Pull", minlen = 4 ];
    { rank = same; OFF  -> HIGH -> LOW; }
    LOW:s -> OFF:s;
}

which produces

enter image description here

so it looks good but with all the tweaking is difficult to expand.

Sarawak answered 6/10, 2017 at 9:39 Comment(4)
Thanks for the suggestions. I could technically do what you suggest wrt defining edge characteristics and then listing all of the transitions of that type, but right now I have edges going in both directions between most nodes (e.g. Off->On and On->Off) plus I have subgraphs where edge type A occurs in multiple subgraphs so I'd be adding some more work in listing nodes explicitly in each subgraph, pulling the internal edge definitions up out of the subgraphs and I'd lose the legibility symmetry check of having the to/from edges for each pair of nodes collocated.Dibasic
Yeah I thought it's more complicated than the fan thing... -- Do you know gvpr? That's too much for me but you may find ways to put it at your service.Sarawak
gvpr is interesting, thanks for the suggestion. I had been considering writing an awk program to preprocess a dot-like file augmented with edge type definitions if dot doesn't support that natively but maybe gvpr would be a more robust approach. I need to do a bit more reading...Dibasic
Looks like though this isn't the answer I was hoping for, it seems to be the right answer so I'm accepting it, thanks again!Dibasic
A
1

I struggled to download m4 on my machine and so opted for using graphviz through the python API where you can define a style as a dictionary and apply to nodes / edges as desired.

import graphviz

dot = graphviz.Digraph(comment='Test File')


nodeAttr_statement = dot.node_attr = {"shape": 'box', "style": 'filled', "fillcolor":"red"}
nodeAttr_question = dot.node_attr = {"shape": 'diamond', "style": 'filled', "fillcolor":"blue"}

dot.edge_attr

edge_Attr_sample = dot.edge_attr = {"arrowhead":'vee',"color":"yellow"}
edge_Attr_sample2 = dot.edge_attr = {"arrowhead": 'diamond', "color": "green"}


dot.node("A", "A", nodeAttr_statement)
dot.node("B", "B", nodeAttr_question )



dot.edge("A", "B", _attributes=edge_Attr_sample)
dot.edge("B", "A", _attributes=edge_Attr_sample2)
dot.format = 'pdf'
dot.render('test', view=True)

Output

// Test File
digraph {
    node [fillcolor=blue shape=diamond style=filled]
    edge [arrowhead=diamond color=green]
    A [label=A fillcolor=red shape=box style=filled]
    B [label=B fillcolor=blue shape=diamond style=filled]
    A -> B [arrowhead=vee color=yellow]
    B -> A [arrowhead=diamond color=green]
}

Output image from python script:

enter image description here

Anachronism answered 18/11, 2021 at 16:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.