如何使 svg 交互以在描绘的元素上收集 comments/annotations

How to make an svg interactive to gather comments/annotations on depicted elements

我在 networkx and nxv 的帮助下从 wikidata 创建了如下有向图。结果是一个 svg 文件,可能会嵌入到某些 html 页面中。

现在我希望每个节点和每条边都是“可点击的”,以便用户可以将他们的评论添加到图形的特定元素。我认为这可以通过弹出一个模式对话框来完成。这个对话框应该知道它是从哪个元素触发的,它应该通过 post 请求将文本区域的内容发送到某个 url。

实现此目标的最佳方法是什么?

据我所知,nxv 为每个节点生成一个 g 元素和 class “节点”,所有节点都嵌套在图 g 中。所以基本上你可以遍历主组内的所有 gs 元素,并在每个元素上附加一个点击事件侦听器。 (实际上,根据所需的行为,您可能希望将事件侦听器附加到 g 内部的形状,如下所示。要使形状内部可点击,它必须是 filled

点击后,它会更新 form,做几件事:更新其样式以将其显示为模式(提交后,表单应返回隐藏状态),并更新隐藏的输入与点击的 gtext 内容。

基本上是这样的:

<svg>Your nxv output goes here</svg>

<form style="display: none;">
  <input type="hidden" id="node_title">
  <textarea></textarea>
  <input type="submit" value="Send!">
</form>

<script>
const graph = document.querySelector("svg g");
const form = document.querySelector("form");
[...graph.querySelectorAll("g")].map(g => { //loop over each g element inside graph
  if (g.getAttribute("class") == "node") { //filter for nodes
    let target = "polygon";
    if (g.querySelector("polygon") === null) {
      target = "ellipse";
    }
    g.querySelector(target).addEventListener("click",() => {
      const node_title = g.querySelector("text").innerHTML;
      form.querySelector("#node_title").setAttribute("value", node_title);
      form.setAttribute("style","display: block;");
    });
  }
});

const submitForm = async (e) => { //function for handling form submission
  const endpoint = "path to your POST endpoint";
  const body = {
    source_node: form.querySelector("#node_title").value,
    textarea: form.querySelector("textarea").value
  }
  e.preventDefault(); //prevent the default form submission behavior
  let response = await fetch(endpoint, { method: "POST", body: JSON.stringify(body) });
  // you might wanna do something with the server response
  // if everything went ok, let's hide this form again & reset it
  form.querySelector("#node_title").value = "";
  form.querySelector("textarea").value = "";
  form.setAttribute("style","display: none;");
}
form.addEventListener("submit",submitForm);
</script>

包装在 W3C standard Web Component 中(所有现代浏览器都支持),您可以使它对任何 src="filename.svg"

通用
  • 简单例子:How to get SVG document data to be inserted into the DOM?

  • 更复杂的例子:

    <graphviz-svg-annotator src="https://graphviz.org/Gallery/directed/fsm.svg">
    </graphviz-svg-annotator>
    
  • SVG 使用异步获取加载

  • 在此 SO 代码段中可以单击节点和边

  • 添加你自己的、更好的模式,window并保存到数据库

  • 尝试以下 SVG:https://graphviz.org/Gallery/directed/Genetic_Programming.html

<graphviz-svg-annotator src="fsm.svg"></graphviz-svg-annotator>
<graphviz-svg-annotator src="Linux_kernel_diagram.svg"></graphviz-svg-annotator>
<style>
  svg .annotate { cursor:pointer }
</style>
<script>
  customElements.define('graphviz-svg-annotator', class extends HTMLElement {
    constructor() {
      let loadSVG = async ( src , container = this.shadowRoot ) => {
        container.innerHTML = `<style>:host { display:inline-block }
                               ::slotted(svg)  { width:100%;height:200px }
                               </style>
                               <slot name="svgonly">Loading ${src}</slot>`;
        this.innerHTML = await(await fetch(src)).text(); // load full XML in lightDOM
        let svg = this.querySelector("svg");
        svg.slot = "svgonly"; // show only SVG part in shadowDOM slot
        svg.querySelectorAll('g[id*="node"],g[id*="edge"]').forEach(g => {
          let label  = g.querySelector("text")?.innerHTML || "No label";
          let shapes = g.querySelectorAll("*:not(title):not(text)");
          let fill   = (color = "none") => shapes.forEach(x => x.style.fill = color);
          let prompt = "Please annotate: ID: " + g.id + " label: " + label; 
          g.classList.add("annotate");
          g.onmouseenter = evt => fill("lightgreen");
          g.onmouseleave = evt => fill();
          g.onclick = evt => g.setAttribute("annotation", window.prompt(prompt));
        })
      }
      super().attachShadow({ mode: 'open' });
      loadSVG("//graphviz.org/Gallery/directed/"+this.getAttribute("src"));
    }});
</script>

详细:

  • this.innerHTML = ... 在组件 ligthDOM
    中注入完整的 XML (因为元素有 shadowDOM,lightDOM 在浏览器中不可见)

  • 但你只想要 SVG 部分(graphviz XML 有太多数据)......你不想要屏幕闪烁;这就是为什么我把 XML .. invisible.. in lightDOM

  • ShadowDOM <slot> 仅用于 反映 <svg>

  • 使用此方法,<svg> 仍然可以从 全局 CSS 设置样式(参见 cursor:pointer

  • 屏幕上有多个 SVG <g> ID 值可能会发生冲突。
    完整的 SVG 可以移动到 shadowDOM 中:

     let svg = container.appendChild( this.querySelector("svg") );
    

    但是你不能再用全局 CSS 设置 SVG 的样式了,因为全局 CSS 不能设置 shadowDOM

    的样式