Voronoi 单元在力导向布局中超出比例

Voronoi cells are off-scale in a force-directed layout

我有一个可折叠的力导向布局,我从 here 中引用了它。

我修改了代码,在图中的每个节点后面添加了 Voronoi 单元。目前,它看起来像是在工作,因为我可以看到基于折叠状态的 Voronoi 单元 appear/disappear。然而,它是超标的。

我认为它与这条特定的线有关

node.select("path")
    .data(voronoi.polygons(nodes))
    .attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z"; });

但我有点不知道该怎么办。 任何人都可以提供有关为什么 voronoi 细胞不按比例缩放的指示吗?我如何确保它们以节点为中心并正确缩放?

当前代码:https://jsfiddle.net/oc8xsm3L/

问题出在这里:

node.attr('transform', function(d) { return `translate(${d.x}, ${d.y})`; });

如您所见,您正在对 node 组应用翻译。这本身不是问题。问题是那些相同的组包含 Voronoi 路径。因此,当您稍后更改 Voronoi 坐标时...

node.select("path")
    .data(voronoi.polygons(nodes))
    .attr("d", function(d) { return d == null ? null : "M" + d.join("L") + "Z";
});

...您在已翻译的组内的路径上应用坐标。

解决方案:有很多不同的解决方案,比如为 Voronoi 路径创建不同的组。但是,这里最简单的解决方案是将翻译应用于圆圈,而不是包含组:

node.select("circle").attr('transform', function(d) {
    return `translate(${d.x}, ${d.y})`;
});

这是您的代码,仅进行了更改:

const svg = d3.select('#voronoiSvg');
const transform = d3.zoomIdentity;
const width = 600;
const height = 300;
let node, link, simulation, root, voronoi, nodes;
let i = 0;

function loadVoronoi() {
  const data = {
    "name": "flare",
    "children": [{
      "name": "analytics",
      "children": [{
          "name": "cluster",
          "children": [{
              "name": "AgglomerativeCluster",
              "size": 3938
            },
            {
              "name": "CommunityStructure",
              "size": 3812
            },
            {
              "name": "HierarchicalCluster",
              "size": 6714
            },
            {
              "name": "MergeEdge",
              "size": 743
            }
          ]
        },
        {
          "name": "graph",
          "children": [{
              "name": "BetweennessCentrality",
              "size": 3534
            },
            {
              "name": "LinkDistance",
              "size": 5731
            },
            {
              "name": "MaxFlowMinCut",
              "size": 7840
            },
            {
              "name": "ShortestPaths",
              "size": 5914
            },
            {
              "name": "SpanningTree",
              "size": 3416
            }
          ]
        },
        {
          "name": "optimization",
          "children": [{
            "name": "AspectRatioBanker",
            "size": 7074
          }]
        }
      ]
    }]
  }
  root = d3.hierarchy(data);

  svg
    .attr("width", width)
    .attr("height", height)
    .on('dblclick.zoom', null);

  simulation = d3.forceSimulation()
    .force('link', d3.forceLink().id(function(d) {
      return d.id;
    }))
    .force('charge', d3.forceManyBody().strength(-10).distanceMax(300))
    .force('center', d3.forceCenter(width / 2, height / 2))
    .alphaTarget(1)
    .on('tick', ticked);

  voronoi = d3.voronoi()
    .x(function(d) {
      return d.x;
    })
    .y(function(d) {
      return d.y;
    })
    .extent([
      [-1, 1],
      [width + 1, height + 1]
    ]);

  update(root);
}

function update(root) {
  nodes = flatten(root);
  const links = root.links();

  //links
  link = svg.selectAll('.link')
    .data(links, function(d) {
      return d.target.id;
    });
  link.exit().remove();
  const linkEnter = link.enter()
    .append('line')
    .attr('class', 'link')
    .style('stroke', '#132')
    .style('opacity', '1')
    .style('stroke-width', 2);
  link = linkEnter.merge(link);

  //nodes
  node = svg.selectAll('.node')
    .data(nodes, function(d) {
      return d.id;
    });
  node.exit().remove();
  const nodeEnter = node.enter()
    .append('g')
    .attr('class', 'node')
    .attr('stroke', '#666')
    .attr('stroke-width', 2)
    .style('fill', color)
    .style('opacity', 1)
    .on('click', clicked);
  nodeEnter.append('circle')
    .attr("r", 8)
    .style('text-anchor', function(d) {
      return d.children ? 'end' : 'start';
    })
    .text(function(d) {
      return d.data.name;
    });
  nodeEnter.append("path").attr("class", "path");
  nodeEnter.call(d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));
  node = nodeEnter.merge(node);

  simulation.nodes(nodes);
  simulation.force('link').links(links);
  // simulation.alpha(1).restart();
}

function color(d) {
  return d._children ? "#51A1DC" // collapsed package
    :
    d.children ? "#51A1DC" // expanded package
    :
    "#F94B4C"; // leaf node
}

function radius(d) {
  return d._children ? 8 :
    d.children ? 8 :
    4;
}

function ticked() {
  link
    .attr('x1', function(d) {
      return d.source.x;
    })
    .attr('y1', function(d) {
      return d.source.y;
    })
    .attr('x2', function(d) {
      return d.target.x;
    })
    .attr('y2', function(d) {
      return d.target.y;
    });

  node.select("circle").attr('transform', function(d) {
    return `translate(${d.x}, ${d.y})`;
  });

  node.select("path")
    .data(voronoi.polygons(nodes))
    .attr("d", function(d) {
      return d == null ? null : "M" + d.join("L") + "Z";
    });
}

function clicked(d) {
  if (!d3.event.defaultPrevented) {
    if (d.children) {
      d._children = d.children;
      d.children = null;
    } else {
      d.children = d._children;
      d._children = null;
    }

    console.log("clicked");
    update(root);
  }
}


function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}

function flatten(root) {
  const nodes = [];

  function recurse(node) {
    if (node.children) node.children.forEach(recurse);
    if (!node.id) node.id = ++i;
    else
      ++i;
    nodes.push(node);
  }
  recurse(root);
  return nodes;
}

function zoomed() {
  svg.attr('transform', d3.event.transform);
}

loadVoronoi();
.node circle {
  cursor: pointer;
  stroke: #3182bd;
  stroke-width: 1.5px;
}

.node text {
  font: 10px sans-serif;
  pointer-events: none;
  text-anchor: middle;
}

line.link {
  fill: none;
  stroke: #9ecae1;
  stroke-width: 1.5px;
}

path {
  pointer-events: all;
  fill: none;
  stroke: #666;
  stroke-opacity: 0.2;
}

.active path {
  fill: #111;
  opacity: 0.05;
}

svg {
  border: 1px solid #888;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="list-group voronoi_treemap" id="voronoiPanel">
  <svg id="voronoiSvg"></svg>
</div>