Ruby-Graphviz
Ruby-Graphviz copied to clipboard
parse_string graph object does not accept global settings
I have a dot output string that I would like to parse into a Ruby GraphViz object and then set graph/node/edge properties on it and add more nodes.
hello_goodnight = GraphViz::new( "" )
hello_goodnight.node[:style] = "rounded"
hello_goodnight.node[:shape] = "box"
hello_world = hello_goodnight.add_graph("hello_world")
hello = hello_world.add_nodes("Hello")
world = hello_world.add_nodes("World")
hello_world.add_edges(hello, world)
goodnight_all = hello_goodnight.add_graph("goodnight_all")
goodnight = goodnight_all.add_nodes("Goodnight")
all = goodnight_all.add_nodes("All")
goodnight_all.add_edges(goodnight, all)
#This gives an output with the style and shape in the global node section
puts hello_goodnight.output( :xdot => String )
#This does not bring in the style and shape
g = GraphViz.parse_string( hello_goodnight.output( :dot => String ) )
#Try setting it again
g.node[:style] = "rounded"
g.node[:shape] = "box"
hello_world = g.get_graph("hello_world")
foo = hello_world.add_nodes('Foo')
bar = hello_world.add_nodes('Bar')
hello_world.add_edges(foo, bar)
# The style and shape are not added to the global node section..
# the style/shape is listed inside the each previously created node
# Hello, World, Goodnight, All are still rounded box style
puts g.output( :xdot => String )
Foo and Bar do not have the rounded box style.
This, however, does work (by putting in the updated subgraph node settings):
g = GraphViz.parse_string( hello_goodnight.output( :dot => String ) )
hello_world = g.get_graph("hello_world")
hello_world.node[:style] = "rounded"
hello_world.node[:shape] = "box"
foo = hello_world.add_nodes('Foo')
bar = hello_world.add_nodes('Bar')
hello_world.add_edges(foo, bar)
puts g.output( :xdot => String )
But I would rather not set rounded box on every single subgraph. I'd rather set it globally.
Any ideas on how to get these properties to show up in the global node section?
OK, It's and it's not a bug !!!
TL;DR
I explain.
When you generate the first graph with :
hello_goodnight = GraphViz::new( "" )
hello_goodnight.node[:style] = "rounded"
hello_goodnight.node[:shape] = "box"
hello_world = hello_goodnight.add_graph("hello_world")
hello = hello_world.add_nodes("Hello")
world = hello_world.add_nodes("World")
hello_world.add_edges(hello, world)
goodnight_all = hello_goodnight.add_graph("goodnight_all")
goodnight = goodnight_all.add_nodes("Goodnight")
all = goodnight_all.add_nodes("All")
goodnight_all.add_edges(goodnight, all)
Ruby/GraphViz generate the following script :
digraph {
node[ style = "rounded" , shape = "box"];
subgraph hello_world {
Hello [label = "Hello"];
World [label = "World"];
Hello -> World;
}
subgraph goodnight_all {
Goodnight [label = "Goodnight"];
All [label = "All"];
Goodnight -> All;
}
}
Then, this graph is parsed (via GraphViz.parse_string
) and, internally, the graph script is :
digraph "%1" {
subgraph hello_world {
Hello [label = "Hello", shape = "box", style = "rounded"];
World [label = "World", shape = "box", style = "rounded"];
Hello -> World;
}
subgraph goodnight_all {
Goodnight [label = "Goodnight", shape = "box", style = "rounded"];
All [label = "All", shape = "box", style = "rounded"];
Goodnight -> All;
}
}
Now, don't forget that Ruby/GraphViz keep the order in which building informations of graph are given. So, when you add the nodes' attributes (attached to the root graph), internally, the script becomes :
digraph "%1" {
subgraph hello_world {
Hello [label = "Hello", shape = "box", style = "rounded"];
World [label = "World", shape = "box", style = "rounded"];
Hello -> World;
}
subgraph goodnight_all {
Goodnight [label = "Goodnight", shape = "box", style = "rounded"];
All [label = "All", shape = "box", style = "rounded"];
Goodnight -> All;
}
node[ style = "rounded" , shape = "box"];
}
Then, when you add the two nodes ("Foo" and "Bar") in the subgraph, the script becomes :
digraph "%1" {
subgraph hello_world {
Hello [label = "Hello", shape = "box", style = "rounded"];
World [label = "World", shape = "box", style = "rounded"];
Hello -> World;
Foo [label = "Foo"];
Bar [label = "Bar"];
Foo -> Bar;
}
subgraph goodnight_all {
Goodnight [label = "Goodnight", shape = "box", style = "rounded"];
All [label = "All", shape = "box", style = "rounded"];
Goodnight -> All;
}
node[ style = "rounded" , shape = "box"];
}
This explain why it "works" when you set the nodes' attributes in the subgraph. Indeed, in this case, the DOT script is :
digraph "%1" {
subgraph hello_world {
Hello [label = "Hello", shape = "box", style = "rounded"];
World [label = "World", shape = "box", style = "rounded"];
Hello -> World;
node[ style = "rounded" , shape = "box"];
Foo [label = "Foo"];
Bar [label = "Bar"];
Foo -> Bar;
}
subgraph goodnight_all {
Goodnight [label = "Goodnight", shape = "box", style = "rounded"];
All [label = "All", shape = "box", style = "rounded"];
Goodnight -> All;
}
}
As I said, at the beginning, there were a bug. But it had no effect in your particular example. (See commit 8c813bc)
So the question now is : Must we do something to change this ? What ? and How ?
We can't remove the fact that we keep the order in which the elements are given. But, maybe, we can give access to this order and allow the user to modify it, and add "elements" at a choosen index. All ideas are welcome...
Thanks for the reply @glejeune . I think it would be a nice feature to be able to modify/add elements of a chosen index, though I have no suggestions on implementation. Until that feature gets added, I've been able to incorporate adding the node attributes to the subgraphs into my code.
I have found another issue that has me stumped with parse_string. When I add an edge from a node to a node with a label, after I parse the dot back in, the label gets lost.
# Create the original graph
g = GraphViz::new( "" )
g["compound"] = "true"
graph_a = g.add_graph("graph_a", :rank => "same")
graph_a.add_nodes("a")
graph_b = g.add_graph("graph_b", :rank => "same")
graph_b.add_nodes("b")
graph_c = g.add_graph("graph_c", :rank => "same")
graph_c.add_nodes("c")
graph_d = g.add_graph("clusterd", :rank => "max")
graph_d.add_nodes("1", :label => "one")
g.add_edges("a", "b")
g.add_edges("b", "c")
# This breaks the label - attempt #1
g.add_edges("b", "1", "lhead" => "clusterd")
g.add_edges("c", "1")
# This does not break the label - attempt #2
# g.add_edges("1", "b", "ltail" => "clusterd", :dir => "back", :constraint => "false")
# g.add_edges("1", "c", :dir => "back", :constraint => "false")
output = g.output( :dot => String )
puts output
puts "------------------------------------------"
# Read in the graph and add to it
temp = GraphViz.parse_string( output )
puts temp.output( :dot => String )
Before parse_string After parse_string
I have noticed that if I switch the edges tail and head, as I have done under the comment # This does not break the label - attempt #2
, the label sticks. However, the layout is not the same as the desired layout from the first attempt.
Any ideas on why this is happening? Does this also have to do with keeping the original order? If I need to stick to the second attempt of switching the head/tail on the edges, how to I achieve the same layout as the first attempt? As you can see from the example above, I've tried using the rank and constraint to straighten out the layout, but neither worked.
Thanks for all the feedback! I appreciate all your work on this gem. It has definitely saved me a lot of time over having to directly modifying the dot file.
I wonder if using graphviz clusters aren't the solution to this issue?