Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I associate SVG elements generated by graphviz to elements in the DOT source code

So, I've generated an svg graph from the dot file, using viz.js.
Now, it's easy to select it's elements, using javascript, but I don't see any association to the original dot file. I don't see any object structure in viz.js library, that ties the generated svg chart elements to the dot source elements, so, that, if I select the svg element with the mouse, I would know that this svg element is correspondent to the dot element, that it was generated from. Is there a way to have a such feedback? I need this, so that, if I edit an element in svg (visually in the browser), I would be able to map the edit back to the dot file and reflect the change on the source.

Explanation

So, here is the example of a possible source GraphViz dot code:

digraph DB {
rankdir=LR
node [shape=record]

person [
    label="
        Person table|
        <id> Person ID|
        <fn> First Name|
        <mn> Middle Name|
        <ln> Last Name
    "
]

address [
    label="
        Addresses table|
        <id> Address ID|
        <pid> Person ID|
        <index> ZIP Code|
        <street> Street Name|
        <house> House Number|
        <town> City/Town/Village Name|
        <state> State Name|
        <district> County/District Name|
        <country> Country Name
    "
]

phone [
    label="
        Phone Number table|
        <pid> Person ID|
        <cc> Country Code|
        <ac> Area Code|
        <n> Phone Number
    "
]
{phone:pid address:pid} -> person:id
}

Here is the svg result, generated by Viz.js library (but, as for me, I don't care, if the same can be done by other library, I will use that other library):

<svg width="671pt" height="257pt" viewBox="0 0 671 257" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 253)">
<title>DB</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-253 666.8861,-253 666.8861,4 -4,4"></polygon>
<!-- person -->
<g id="node1" class="node">
<title>person</title>
<polygon fill="none" stroke="#000000" points="277.8566,-62.5 277.8566,-186.5 371.2234,-186.5 371.2234,-62.5 277.8566,-62.5"></polygon>
<text text-anchor="middle" x="324.54" y="-169.9" font-family="Times,serif" font-size="14.00" fill="#000000">Person table</text>
<polyline fill="none" stroke="#000000" points="277.8566,-161.7 371.2234,-161.7 "></polyline>
<text text-anchor="middle" x="324.54" y="-145.1" font-family="Times,serif" font-size="14.00" fill="#000000">Person ID</text>
<polyline fill="none" stroke="#000000" points="277.8566,-136.9 371.2234,-136.9 "></polyline>
<text text-anchor="middle" x="324.54" y="-120.3" font-family="Times,serif" font-size="14.00" fill="#000000">First Name</text>
<polyline fill="none" stroke="#000000" points="277.8566,-112.1 371.2234,-112.1 "></polyline>
<text text-anchor="middle" x="324.54" y="-95.5" font-family="Times,serif" font-size="14.00" fill="#000000">Middle Name</text>
<polyline fill="none" stroke="#000000" points="277.8566,-87.3 371.2234,-87.3 "></polyline>
<text text-anchor="middle" x="324.54" y="-70.7" font-family="Times,serif" font-size="14.00" fill="#000000">Last Name</text>
</g>
<!-- address -->
<g id="node2" class="node">
<title>address</title>
<polygon fill="none" stroke="#000000" points="504.1939,-.5 504.1939,-248.5 662.8861,-248.5 662.8861,-.5 504.1939,-.5"></polygon>
<text text-anchor="middle" x="583.54" y="-231.9" font-family="Times,serif" font-size="14.00" fill="#000000">Addresses table</text>
<polyline fill="none" stroke="#000000" points="504.1939,-223.7 662.8861,-223.7 "></polyline>
<text text-anchor="middle" x="583.54" y="-207.1" font-family="Times,serif" font-size="14.00" fill="#000000">Address ID</text>
<polyline fill="none" stroke="#000000" points="504.1939,-198.9 662.8861,-198.9 "></polyline>
<text text-anchor="middle" x="583.54" y="-182.3" font-family="Times,serif" font-size="14.00" fill="#000000">Person ID</text>
<polyline fill="none" stroke="#000000" points="504.1939,-174.1 662.8861,-174.1 "></polyline>
<text text-anchor="middle" x="583.54" y="-157.5" font-family="Times,serif" font-size="14.00" fill="#000000">ZIP Code</text>
<polyline fill="none" stroke="#000000" points="504.1939,-149.3 662.8861,-149.3 "></polyline>
<text text-anchor="middle" x="583.54" y="-132.7" font-family="Times,serif" font-size="14.00" fill="#000000">Street Name</text>
<polyline fill="none" stroke="#000000" points="504.1939,-124.5 662.8861,-124.5 "></polyline>
<text text-anchor="middle" x="583.54" y="-107.9" font-family="Times,serif" font-size="14.00" fill="#000000">House Number</text>
<polyline fill="none" stroke="#000000" points="504.1939,-99.7 662.8861,-99.7 "></polyline>
<text text-anchor="middle" x="583.54" y="-83.1" font-family="Times,serif" font-size="14.00" fill="#000000">City/Town/Village Name</text>
<polyline fill="none" stroke="#000000" points="504.1939,-74.9 662.8861,-74.9 "></polyline>
<text text-anchor="middle" x="583.54" y="-58.3" font-family="Times,serif" font-size="14.00" fill="#000000">State Name</text>
<polyline fill="none" stroke="#000000" points="504.1939,-50.1 662.8861,-50.1 "></polyline>
<text text-anchor="middle" x="583.54" y="-33.5" font-family="Times,serif" font-size="14.00" fill="#000000">County/District Name</text>
<polyline fill="none" stroke="#000000" points="504.1939,-25.3 662.8861,-25.3 "></polyline>
<text text-anchor="middle" x="583.54" y="-8.7" font-family="Times,serif" font-size="14.00" fill="#000000">Country Name</text>
</g>
<!-- address&#45;&gt;person -->
<g id="edge1" class="edge">
<title>address-&gt;person:id</title>
<path fill="none" stroke="#000000" d="M503.9959,-133.8802C457.4691,-139.3669 403.6776,-145.7102 381.6916,-148.3029"></path>
<polygon fill="#000000" stroke="#000000" points="381.0613,-144.8529 371.54,-149.5 381.8811,-151.8047 381.0613,-144.8529"></polygon>
</g>
<!-- phone -->
<g id="node3" class="node">
<title>phone</title>
<polygon fill="none" stroke="#000000" points="0,-62.5 0,-186.5 131.08,-186.5 131.08,-62.5 0,-62.5"></polygon>
<text text-anchor="middle" x="65.54" y="-169.9" font-family="Times,serif" font-size="14.00" fill="#000000">Phone Number table</text>
<polyline fill="none" stroke="#000000" points="0,-161.7 131.08,-161.7 "></polyline>
<text text-anchor="middle" x="65.54" y="-145.1" font-family="Times,serif" font-size="14.00" fill="#000000">Person ID</text>
<polyline fill="none" stroke="#000000" points="0,-136.9 131.08,-136.9 "></polyline>
<text text-anchor="middle" x="65.54" y="-120.3" font-family="Times,serif" font-size="14.00" fill="#000000">Country Code</text>
<polyline fill="none" stroke="#000000" points="0,-112.1 131.08,-112.1 "></polyline>
<text text-anchor="middle" x="65.54" y="-95.5" font-family="Times,serif" font-size="14.00" fill="#000000">Area Code</text>
<polyline fill="none" stroke="#000000" points="0,-87.3 131.08,-87.3 "></polyline>
<text text-anchor="middle" x="65.54" y="-70.7" font-family="Times,serif" font-size="14.00" fill="#000000">Phone Number</text>
</g>
<!-- phone&#45;&gt;person -->
<g id="edge2" class="edge">
<title>phone-&gt;person:id</title>
<path fill="none" stroke="#000000" d="M131.1663,-132.2389C180.2951,-138.0324 243.0276,-145.4301 267.307,-148.2933"></path>
<polygon fill="#000000" stroke="#000000" points="267.1989,-151.8047 277.54,-149.5 268.0187,-144.8529 267.1989,-151.8047"></polygon>
</g>
</g>
</svg>

Let's say, I want to edit the " City/Town/Village Name" in the source dot file not by editing source's text, but by visually clicking on the related generated svg representation of that dot source. I can write some JavaScript, that will allow me to click on "City/Town/Village Name" on the svg graphic, for example, and the block becomes active. Then, I edit it in-place, as I wish. The problem lies with saving the change back to the source. JavaScript should change the dot source accordingly, but the problem is that the svg, generated with viz.js doesn't have any ties to the source. I.e., if you look at the source of the generated svg, it doesn't add any ids or anything, that would indicate, that a particular svg element was generated from which dot element. There is no way to identify, which element was edited in order to pass the edited value back to the correct dot element for the change to be made in the source. There are some ways, that I can think of so solve my issue:

  • edit the viz.js library, so it puts some ids on the generated svg
  • tediously analyse the generated svg, in order to logically identify the correct source element, to the edited svg element

,but the above are too difficult jobs and would take a long time to accomplish, so, I am asking, if there is some feature in viz.js, that I've missed, that would allow me to accomplish my task, or maybe, there is some other library that I could use, that can do, what I require?

like image 454
igoryonya Avatar asked Nov 06 '17 22:11

igoryonya


2 Answers

In cases simpler that yours, the SVG <title> element can be used to refer back to nodes and edges. For nodes, the title is the "node_id" (not to be confused with the node attribute id) and for edges it is "node_id edgeop node_id", e.g. a -> b. From your SVG code:

<g id="node1" class="node"> <title>person</title>

person can be used to refer back to the DOT source line: person [....

In the general case, the Graphviz id attribute is your friend:

id

Allows the graph author to provide an id for graph objects which is to be included in the output. Normal "\N", "\E", "\G" substitutions are applied. If provided, it is the responsibility of the provider to keep its values sufficiently unique for its intended downstream use. Note, in particular, that "\E" does not provide a unique id for multi-edges. If no id attribute is provided, then a unique internal id is used. However, this value is unpredictable by the graph writer. An externally provided id is not used internally.

If the graph provides an id attribute, this will be used as a prefix for internally generated attributes. By making these distinct, the user can include multiple image maps in the same document.

In your case, you want to reference not only nodes, but also individual fields of record-based nodes.

Although the fields of record labels are defined with fieldId's, they do not seem to be intended to propagate to the generated SVG:

The first string in fieldId assigns a portname to the field and can be combined with the node name to indicate where to attach an edge to the node. (See portPos.)

To your rescue comes HTML-like labels:

The record-based shape has largely been superseded and greatly generalized by HTML-like labels. That is, instead of using shape=record, one might consider using shape=none, margin=0 and an HTML-like label.

With them you can create a node that is a table with rows and columns where you can use the ID attribute:

ID="value"

allows the user to specify a unique ID for a table or cell. See the id attribute for more information. Note that the "value" is treated as an escString similarly to the id attribute.

Unfortunately there is a bug in Graphviz (better described here) that causes this attribute to be ignored in the SVG output. Fortunately, there's a workaround.

Below is a solution which is based on d3-graphviz, which uses viz.js internally. You don't need to use d3-graphviz, though. You can achieve the same thing with viz.js directly.

If you keep your id's sufficiently unique and you have control of the formatting of the DOT source, you can use simple pattern replacement as in the presented solution.

If you don't have control over the formatting of the DOT source, you are probably better off feeding back the information to the application that generates it. An alternative, to avoid writing a full-fledged DOT parser, is to normalize the DOT source with viz.js by using 'dot' as output format and try to parse that.

<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/[email protected]/viz.js"></script>
<script src="https://unpkg.com/[email protected]/build/d3-graphviz.js"></script>
<div id="graph" style="text-align: center;"></div>
<script>

var dotSrc = `
digraph DB {
graph [label="Click on a cell to convert to upper/lower case" labelloc="t", fontsize="20.0" tooltip=" "]
rankdir=LR
node [shape=plain]

person [

    // NOTE: The use of HREF is a workaround for '[Dot] ID="value" fails to produce id string in svg:svg output for html nodes'
    //       See https://gitlab.com/graphviz/graphviz/issues/207
    //       For the workaorund and more info, see http://ftp.graphviz.org/mantisbt/view.php?id=2197

    label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">
              <TR><TD>Person table</TD></TR>
              <TR><TD ID="p.id" PORT="id" HREF=" ">Person ID</TD></TR>
              <TR><TD ID="p.fn" PORT="fn" HREF=" ">First Name</TD></TR>
              <TR><TD ID="p.mn" PORT="mn" HREF=" ">Middle Name</TD></TR>
              <TR><TD ID="p.ln" PORT="ln" HREF=" ">Last Name</TD></TR>
            </TABLE> >
]

address [
    label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">
        <TR><TD>Addresses table</TD></TR>
        <TR><TD ID="a.id" PORT="id" HREF=" ">Address ID</TD></TR>
        <TR><TD ID="a.pid" PORT="pid" HREF=" ">Person ID</TD></TR>
        <TR><TD ID="a.index" PORT="index" HREF=" ">ZIP Code</TD></TR>
        <TR><TD ID="a.street" PORT="street" HREF=" ">Street Name</TD></TR>
        <TR><TD ID="a.house" PORT="house" HREF=" ">House Number</TD></TR>
        <TR><TD ID="a.town" PORT="town" HREF=" ">City/Town/Village Name</TD></TR>
        <TR><TD ID="a.state" PORT="state" HREF=" ">State Name</TD></TR>
        <TR><TD ID="a.district" PORT="district" HREF=" ">County/District Name</TD></TR>
        <TR><TD ID="a.country" PORT="country" HREF=" ">Country Name</TD></TR>
      </TABLE> >
]

phone [
    label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">
        <TR><TD>Phone Number table</TD></TR>
        <TR><TD ID="n.pid" PORT="pid" HREF=" ">Person ID</TD></TR>
        <TR><TD ID="n.cc" PORT="cc" HREF=" ">Country Code</TD></TR>
        <TR><TD ID="n.ac" PORT="ac" HREF=" ">Area Code</TD></TR>
        <TR><TD ID="n.n" PORT="n" HREF=" ">Phone Number</TD></TR>
      </TABLE> >
]
{phone:pid address:pid} -> person:id

}
`;

var graphviz = d3.select("#graph").graphviz();
var dotSrcLines;

function render(dotSrc) {
//    console.log('DOT source =', dotSrc);
    dotSrcLines = dotSrc.split('\n');

    transition1 = d3.transition()
        .delay(100)
        .duration(1000);

    graphviz
        .transition(transition1)
        .renderDot(dotSrc);

    transition1
      .transition()
        .duration(0)
        .on("end", function () {
            nodes = d3.selectAll('.node,.edge');
            nodes
              .selectAll("g")
                .on("click", fieldClickHandler)
              .selectAll("a")
                // Remove the workaround attributes to avoid consuming the click events
                .attr("href", null)
                .attr("title", null);
        });
}

function fieldClickHandler () {
    var node = d3.select(this);
    var text = node.selectAll('text').text();
    var id = node.attr('id');
    var class1 = node.attr('class');
    dotElement = id.replace(/^a_/, '');
    console.log('Element id="%s" class="%s" text="%s" dotElement="%s"', id, class1, text, dotElement);
    console.log('Finding and deleting references to %s "%s" from the DOT source', class1, dotElement);
    for (i = 0; i < dotSrcLines.length; i++) {
        if (dotSrcLines[i].indexOf(dotElement) >= 0) {
            ucText = text.toUpperCase();
            lcText = text.toLowerCase();
            if (text != ucText) {
                newText = ucText;
            } else {
                newText = lcText;
            }
            console.log('Converting "%s" to "%s" on line %d: %s', text, newText, i, dotSrcLines[i]);
            dotSrcLines[i] = dotSrcLines[i].replace(text, newText);
        }
    }
    dotSrc = dotSrcLines.join('\n');
    render(dotSrc);
}

render(dotSrc);

</script>
like image 50
magjac Avatar answered Sep 19 '22 01:09

magjac


Graphviz accepts class attributes and outputs them as SVG class="foo". Example:

$ cat test.dot
digraph G {
  graph [class="cats"];

  subgraph cluster_big {
    graph [class="big_cats"];

    "Lion" [class="yellow social"];
    "Snow Leopard" [class="white solitary"];
  };
}

$ dot -Tsvg ~/test.dot | grep "<g"
<g id="graph0" class="graph cats" ...>
<g id="clust1" class="cluster big_cats">
<g id="node1" class="node yellow social">
<g id="node2" class="node white solitary">
like image 24
mhansen Avatar answered Sep 20 '22 01:09

mhansen