02 May 2015

Drag your GraphViz nodes with Inkscape!

GraphViz is a great graph layout engine, and GraphViz outputs to SVG. This allows us to edit GraphViz output in Inkscape.

Unfortunately, this editing is limited. The arcs are not "connectors" in the graphic-editing sense, so we cannot drag a node while maintaining arc connectivity.

But Inkscape does have draggable connectors. Solution: I wrote this Python script to convert GraphViz SVG output to have Inkscape-draggable connectors. Seems to work. In Inkscape, click the node to drag, not the arc. Your mileage may vary.  Open source code here.

# 2 May 2015, John F. Raffensperger.

from xml.dom import minidom

# 1. Read Graphviz.svg. This is output from Graphviz.
graphvizSVGFile= minidom.parse("graphviz_output.svg")

# 2. For each node, get id and title.
nodeTitles = {}
for s in graphvizSVGFile.getElementsByTagName('g'):
    if s.attributes['id'].value[:4] == "node":
        nodeTitles[s.getElementsByTagName('title')[0].firstChild.data] = s.attributes['id'].nodeValue

# 3. For each arc, parse the title, and match the corresponding ids of the nodes, add the ids to the arc,
edgeTitles = {}
for s in graphvizSVGFile.getElementsByTagName('g'):
    if s.attributes['id'].value[:4] == "edge":
        edgeTitle = s.getElementsByTagName('title')[0].firstChild.data
        edgeTitles [s.attributes['id'].nodeValue] = edgeTitle.split("->")
        
# 4. Add connector elements to Graphviz.svg.
# For each arc, delete the GraphViz arrow marker,
for s in graphvizSVGFile.getElementsByTagName('g'):
    if s.attributes['id'].value[:4] == "edge":
        # Remove child "polygon", which is the GraphViz arrow marker.
        for thing in s.childNodes: 
            if thing.nodeType == s.ELEMENT_NODE and thing.tagName == "polygon": 
                s.removeChild(thing) 
                break

# 5. To each edge path, add an Inkscape arrow marker in the attributes.
for s in graphvizSVGFile.getElementsByTagName('g'):
    if s.attributes['id'].value[:4] == "edge":
        for thing in s.childNodes: 
            if thing.nodeType == s.ELEMENT_NODE and thing.tagName == "path": 
                thing.setAttribute("inkscape:connector-type", "polyline")
                thing.setAttribute("inkscape:connector-curvature", "3")
                nodeID = nodeTitles[edgeTitles[s.attributes['id'].value][0]]
                thing.setAttribute("inkscape:connection-start", "#" + nodeID)
                nodeID = nodeTitles[edgeTitles[s.attributes['id'].value][1]]
                thing.setAttribute("inkscape:connection-end", "#" + nodeID)

# 6. Output should have draggable nodes in Inkscape, after ungrouping as needed.
SVGtextFile = open("inkscape_input.svg", "wb")
graphvizSVGFile.writexml(SVGtextFile)
SVGtextFile.close()
print "done"

3 comments:

Anonymous said...

Useful tool, thanks !

I use it with some patch.

There is a bug if you have a UTF8 text (ascii > 128), so use codecs

| import codecs

I remplaced the step 6 by

| with codecs.open("inkscape_input.svg", "w", "utf-8") as out:
| graphvizSVGFile.writexml(out)

I want arrow on the connector end, so I added this after thing.setAttribute("inkscape:connection-end", "#" + nodeID)

| thing.setAttribute("marker-end","url(#Arrow1Lend)")

But "Arrow1Lend" must be defined, so i use a bash script to add definition like this:

| MARKER=' '
| cat "inkscape_input.svg" | sed "s@@$MARKER@" > "inkscape_input2.svg"

Simon.

Anonymous said...

MARKER="$(echo "PG1hcmtlciBpbmtzY2FwZTpzdG9ja2lkPSJBcnJvdzFMZW5kIiBvcmllbnQ9ImF1dG8iIHJlZlk9
IjAuMCIgcmVmWD0iMC4wIiBpZD0iQXJyb3cxTGVuZCIgc3R5bGU9Im92ZXJmbG93OnZpc2libGU7
Ij4gPHBhdGggaWQ9InBhdGg2NjZ1bmlxdWVwYXRodmFsdWVpaG9wZSIgZD0iTSAwLjAsMC4wIEwg
NS4wLC01LjAgTCAtMTIuNSwwLjAgTCA1LjAsNS4wIEwgMC4wLDAuMCB6ICIgc3R5bGU9ImZpbGwt
cnVsZTpldmVub2RkO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxLjBwdDsiIHRyYW5zZm9y
bT0ic2NhbGUoMC44KSByb3RhdGUoMTgwKSB0cmFuc2xhdGUoMTIuNSwwKSIgLz4gPC9tYXJrZXI+
Cg==" | base64 -d)"

Anonymous said...

Works great! Have been looking for a long time for a solution...
Thanks
Martin