如何将由graphviz生成的SVG元素与DOT源代码中的元素关联起来?

7
所以,我已经使用viz.js从dot文件生成了一个svg图形。现在,使用javascript很容易选择它的元素,但我没有看到任何与原始dot文件相关的关联。我没有在viz.js库中看到任何对象结构,将生成的svg图表元素绑定到dot源元素,因此,如果我用鼠标选择svg元素,我会知道这个svg元素对应于生成自哪个dot元素。是否有一种方法可以获得这样的反馈?我需要这个,这样,如果我在浏览器中视觉上编辑svg中的一个元素,我将能够将编辑映射回dot文件并反映更改到源文件上。

解释

所以,这是可能的GraphViz dot代码示例:

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
}

以下是由Viz.js库生成的SVG结果(但对我来说,如果其他库也能做同样的事情,我不介意使用那个库):

<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>

假设我想编辑源点文件中的“城市/镇/村名称”,而不是编辑源文本,而是通过在相关生成的svg表示上进行视觉点击来进行编辑。我可以编写一些JavaScript,使我能够单击svg图形中的“城市/镇/村名称”,然后该块变为活动状态。然后,我可以根据需要就地编辑它。问题在于如何将更改保存回源代码。JavaScript应相应更改dot源代码,但问题在于由viz.js生成的svg与源代码没有任何联系。即,如果您查看生成的svg的源代码,则不会添加任何标识符或其他内容,以指示特定的svg元素是从哪个dot元素生成的。无法确定要修改的元素,以便将编辑后的值传递回正确的dot元素,以对源进行更改。有一些方法可以解决我的问题:
  • 编辑viz.js库,以便在生成的svg上放置一些标识符
  • 仔细分析生成的svg,以便逻辑上确定正确的源元素以编辑svg元素
但以上方法都非常困难,需要花费很长时间才能完成。因此,我想知道是否有我错过的viz.js功能可以实现我的任务,或者是否有其他库可以实现我的要求?

2
我想我理解了原帖想要做什么,而且我已经完成了。如果“等待审核”被解除,我会提供一个答案。 - magjac
magjac,也许我会等半天,如果他们不解除“挂起”状态,我可以创建另一个问题,然后你可以在那里回答? - igoryonya
1
magjac,我编辑了。 - igoryonya
我建议了一个更好的标题。希望现在“暂停”状态会被解除。你的代码比我想象的要复杂一些。你想能够引用单个标签记录字段。我会研究一下这个问题。同时,看一下这个块 https://bl.ocks.org/magjac/28a70231e2c9dddb84b3b20f450a215f,它展示了如何将节点和边缘SVG元素与DOT源元素相关联。它使用d3-graphviz(基于viz.js),但是同样可以直接使用viz.js完成。 - magjac
1
谢谢,这是一个很好的开端!我会试着使用它。所以,我需要使用D3库来完成这个任务。当他们取消保留并且您将评论提升为答案时,我会投票支持它。如果您愿意,我可以创建一个新问题,标题和内容相同,并删除此问题。您在那里发布您的答案,我会勾选它。 - igoryonya
显示剩余3条评论
2个回答

9
在比你的情况更简单的情况下,SVG <title>元素可用于引用节点和边。对于节点,标题是“node_id”(不要与节点属性id混淆),对于边,它是“node_id edgeop node_id”,例如a -> b。从您的SVG代码中:

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

person可用于引用DOT源行:person [...

在一般情况下,Graphviz id 属性是您的朋友:

id

允许图形作者为图形对象提供包含在输出中的 id。正常的 "\N"、"\E"、"\G" 替换将被应用。如果提供了该属性,则由提供者负责使其值足够唯一以满足其预期的下游使用。请注意,特别是 "\E" 不为多重边提供唯一的 id。如果未提供 id 属性,则使用唯一的内部 id。但是,该值对于图形编写者来说是不可预测的。外部提供的 id 不在内部使用。

如果图形提供了 id 属性,则将其用作内部生成属性的前缀。通过使这些属性不同,用户可以在同一文档中包含多个图像映射。

在您的情况下,您不仅要引用节点,还要引用 基于记录的节点 的各个字段。
尽管记录标签的字段是由fieldId定义的,但它们似乎不打算传播到生成的SVG中:

fieldId中的第一个字符串为字段分配了一个端口名称,并且可以与节点名称组合使用以指示在节点上附加边缘的位置。 (请参见portPos。)

类似HTML的标签来拯救你:

基于记录的形状已经被类似HTML的标签大量取代并广泛推广。 也就是说,可以考虑使用shape = none,margin = 0和类似HTML的标签,而不是使用shape = record。

有了它们,您可以创建一个带有行和列的表格节点,其中您可以使用ID属性:

ID =“value”

允许用户为表格或单元格指定唯一ID。有关详细信息,请参见id属性。请注意,“value”被视为与id属性类似的escString。

很不幸,Graphviz中存在一个漏洞(更好的描述在这里),导致该属性在SVG输出中被忽略。幸运的是,有一个解决方法
下面的解决方案基于d3-graphviz,它在内部使用viz.js。不过,您不需要使用d3-graphviz,直接使用viz.js也可以实现相同的功能。
如果您的id足够唯一,并且您可以控制DOT源代码的格式,您可以像所提供的解决方案中那样使用简单的模式替换。
如果您无法控制DOT源代码的格式,最好将信息反馈给生成它的应用程序。另一个选择是使用viz.js将DOT源代码标准化,输出格式使用“dot”,并尝试解析它,以避免编写全面的DOT解析器。

<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/viz.js@1.8.0/viz.js"></script>
<script src="https://unpkg.com/d3-graphviz@0.1.2/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>


谢谢您的帮助。事实上,我有一个想法,即如果所有的方法都失败了,我应该使用HTML在GraphViz源中,但我想把它留作最后的选择,因为它会无情地膨胀GraphViz源并使其难以阅读。我希望在实际的viz.js、d3.js或其他一些js库中有一些功能,能够生成带有id的svg,以便进一步将生成的图形与源代码绑定,或者这些库创建了一个对象,将源graphviz和生成的svg绑定在一起,这是我不知道的。 - igoryonya
1
但是,另一方面,当我阅读您的答案并到达您建议在graphviz源中使用HTML的时候,我恍然大悟 - 我可以拥有无HTML的graphviz源码,然后在将其传递给viz.js之前,我可以预处理它与HTML一起,并且当需要将SVG上的更改保存回源graphviz时,它们实际上将被保存到无HTML版本中,完全绕过HTML版本。谢谢! - igoryonya

5

Graphviz接受 class 属性 并将其作为 SVG class="foo" 输出。示例:

$ 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">

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接