d3力导向图中的水平链接标签

5

我有一个带曲线链接的d3(v3)力导向图,看起来像这样:

enter image description here

我想要做到的是让链接的textPath元素为水平方向,因为它们是数字,而“81”需要与“18”不同。我还想在它们上面放置一些白色阴影/外发光/背景,因为我直接放置在链接上。我现在有一个白色描边,但由于有时一个数字的描边会侵入到下一个数字,所以它并不完美。

这里有一个可复制的示例,我承认这个示例是从其他SO答案中拼凑而成的:https://jsfiddle.net/2gbekL7m/

代码的相关部分如下:

var link_label = svg.selectAll(".link_label")
  .data(links)
  .enter()
  .append("text")
  .attr("class", "link_label")
  .attr("paint-order", "stroke")
  .attr("stroke", "white")
  .attr("stroke-width", 4)
  .attr("stroke-opacity", 1)
  .attr("stroke-linecap", "butt")
  .attr("stroke-linejoin", "miter")
  .style("fill", "black")
  .attr("dy", 5)
  .append("textPath")
  .attr("startOffset", "50%")
  .attr("xlink:href", function(d, i) {
    return "#link_" + i;
  })
  .text(function(d, i) {
    return d.n;
  });

有人知道我如何通过调整方向并添加背景框来改善链接标签的可读性吗?


谢谢,我已经看到了那个答案,但是不知什么原因在我的情况下它不起作用。链接标签根本没有出现。 - Devin
2个回答

5
首先,由于您没有发布您的代码,我的答案将针对您分享的JSFiddle中的代码。
让我们分别解决您的两个问题:

定位标签

我认为您想要将标签放在链接的中间,并始终水平,就像普通文本一样。如果是这样,解决方案是删除textPath,这将使您的选择更简单:
var link_label = svg.selectAll(".link_label")
    .data(links)
    .enter()
    .append("text")
    .text(function(d, i) {
        return d.n;
    });

现在的问题就是获取这些路径的中间位置,我们可以在tick函数中使用getTotalLength()getPointAtLength()来完成:

link_label.attr("x", function(d, i) {
        var pathLength = d3.select("#link_" + i).node().getTotalLength();
        d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
        return d.point.x
    })
    .attr("y", function(d) {
        return d.point.y
    })

这里我将数值存储在一个属性中,因为它对于x和y位置是相同的。这样我们就避免了不必要的重复计算。此外,有人可能会认为计算应该放在tick函数之外,只需一次获取路径长度:不幸的是,在模拟过程中,路径长度会不断变化,所以这并不适用。

文本阴影

这里有几种不同的方法。其中一个简单的方法,可能不是最美观的方法,是使用text-shadow。下面是一个简单的白色阴影,向上、向右、向下和向左:

.shadow {
    text-shadow: 2px 2px 0 #fff, 2px -2px 0 #fff, -2px 2px 0 #fff, -2px -2px 0 #fff
}

CSS在Chrome和FireFox上有效,但在Safari上无效。

综上所述,这是演示:

var nodes = [{
      "ix": 0
    },
    {
      "ix": 1
    },
    {
      "ix": 2
    },
    {
      "ix": 3
    }
  ];

  var links = [{
      "source": 0,
      "target": 2,
      "n": 12
    },
    {
      "source": 0,
      "target": 1,
      "n": 34
    },
    {
      "source": 1,
      "target": 2,
      "n": 56
    },
    {
      "source": 1,
      "target": 0,
      "n": 78
    },
    {
      "source": 0,
      "target": 3,
      "n": 90
    }
  ];

  var w = 400,
    h = 400;

  var force = d3.layout.force()
    .size([w, h])
    .nodes(nodes)
    .links(links)
    .gravity(1)
    .linkDistance(30)
    .charge(-20000)
    .linkStrength(1);

  force.start();

  var svg = d3.select("body").append("svg")
    .attr("width", w)
    .attr("height", h)
    .attr("preserveAspectRatio", "xMinYMin meet")
    .attr("viewBox", "0 0 " + w + " " + h)
    .append("g");

  var marker = svg.selectAll("marker")
    .append("svg:defs")
    .data(["end-arrow"])
    .enter()
    .append("svg:marker")
    .attr("id", String)
    .attr("viewBox", "0 -3 6 6")
    .attr("refX", 11.3)
    .attr("refY", -0.2)
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
    .append("svg:path")
    .attr("d", "M0,-3L6,0L0,3");

  var link = svg.selectAll("line.link")
    .data(links)
    .enter()
    .append("svg:path")
    .attr("id", function(d, i) {
      return "link_" + i;
    })
    .style("stroke", "black")
    .style("stroke-width", 2)
    .style("fill", "none")
    .attr("marker-end", "url(#end-arrow)");

  var link_label = svg.selectAll(".link_label")
    .data(links)
    .enter()
    .append("text")
    .style("text-anchor", "middle")
    .style("dominant-baseline", "central")
    .attr("class", "shadow")
    .text(function(d, i) {
      return d.n;
    });

  var node = svg.selectAll(".node")
    .data(force.nodes())
    .enter()
    .append("svg:g")
    .attr("class", "node");

  node.append("svg:circle")
    .attr("r", 10)
    .style("fill", "black");

  node.call(force.drag);

  force.on("tick", function() {
    link.attr("d", function(d) {
      var dx = d.target.x - d.source.x;
      var dy = d.target.y - d.source.y;
      var dr = Math.sqrt(dx * dx + dy * dy);

      return "M" +
        d.source.x +
        "," +
        d.source.y +
        "A" +
        dr +
        "," +
        dr +
        " 0 0,1 " +
        d.target.x +
        "," +
        d.target.y;
    });

    link_label.attr("x", function(d, i) {
        var pathLength = d3.select("#link_" + i).node().getTotalLength();
        d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
        return d.point.x
      })
      .attr("y", function(d) {
        return d.point.y
      })
    node.attr("transform", function(d) {
      return ("translate(" + d.x + "," + d.y + ")");
    });
  });
.shadow {
  text-shadow: 2px 2px 0 #fff, 2px -2px 0 #fff, -2px 2px 0 #fff, -2px -2px 0 #fff
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

编辑:请看Xavier的答案,他的白色圆形看起来比文字阴影好得多。


5

在使用getPointAtLength()的情况下重新利用Gerardo的答案,以下是基于白色圆圈的替代方案,用于第二部分问题,涉及标签的阴影/背景:

var nodes = [{
      "ix": 0
    },
    {
      "ix": 1
    },
    {
      "ix": 2
    },
    {
      "ix": 3
    }
  ];

  var links = [{
      "source": 0,
      "target": 2,
      "n": 12
    },
    {
      "source": 0,
      "target": 1,
      "n": 34
    },
    {
      "source": 1,
      "target": 2,
      "n": 56
    },
    {
      "source": 1,
      "target": 0,
      "n": 78
    },
    {
      "source": 0,
      "target": 3,
      "n": 90
    }
  ];

  var w = 400,
    h = 400;

  var force = d3.layout.force()
    .size([w, h])
    .nodes(nodes)
    .links(links)
    .gravity(1)
    .linkDistance(30)
    .charge(-20000)
    .linkStrength(1);

  force.start();

  var svg = d3.select("body").append("svg")
    .attr("width", w)
    .attr("height", h)
    .attr("preserveAspectRatio", "xMinYMin meet")
    .attr("viewBox", "0 0 " + w + " " + h)
    .append("g");

  var marker = svg.selectAll("marker")
    .append("svg:defs")
    .data(["end-arrow"])
    .enter()
    .append("svg:marker")
    .attr("id", String)
    .attr("viewBox", "0 -3 6 6")
    .attr("refX", 11.3)
    .attr("refY", -0.2)
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
    .append("svg:path")
    .attr("d", "M0,-3L6,0L0,3");

  var link = svg.selectAll("line.link")
    .data(links)
    .enter()
    .append("svg:path")
    .attr("id", function(d, i) {
      return "link_" + i;
    })
    .style("stroke", "black")
    .style("stroke-width", 2)
    .style("fill", "none")
    .attr("marker-end", "url(#end-arrow)");

  var link_label_shadow = svg.selectAll(".link_label_shadow")
    .data(links)
    .enter()
    .append("circle")
    .attr("r", 10)
    .style("fill", "white");

  var link_label = svg.selectAll(".link_label")
    .data(links)
    .enter()
    .append("text")
    .style("text-anchor", "middle")
    .style("dominant-baseline", "central")
    .attr("class", "shadow")
    .text(function(d, i) {
      return d.n;
    });

  var node = svg.selectAll(".node")
    .data(force.nodes())
    .enter()
    .append("svg:g")
    .attr("class", "node");

  node.append("svg:circle")
    .attr("r", 10)
    .style("fill", "black");

  node.call(force.drag);

  force.on("tick", function() {
    link.attr("d", function(d) {
      var dx = d.target.x - d.source.x;
      var dy = d.target.y - d.source.y;
      var dr = Math.sqrt(dx * dx + dy * dy);

      return "M" +
        d.source.x +
        "," +
        d.source.y +
        "A" +
        dr +
        "," +
        dr +
        " 0 0,1 " +
        d.target.x +
        "," +
        d.target.y;
    });

    link_label.attr("x", function(d, i) {
        var pathLength = d3.select("#link_" + i).node().getTotalLength();
        d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
        return d.point.x
      })
      .attr("y", function(d) {
        return d.point.y
      })
    node.attr("transform", function(d) {
      return ("translate(" + d.x + "," + d.y + ")");
    });

    link_label_shadow.attr("cx", function(d, i) {
        var pathLength = d3.select("#link_" + i).node().getTotalLength();
        d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
        return d.point.x
      })
      .attr("cy", function(d) {
        return d.point.y
      })
    node.attr("transform", function(d) {
      return ("translate(" + d.x + "," + d.y + ")");
    });
  });
  
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

同样的方式,使用getPointAtLength()设置标签时,我们可以在链接和文本标签之间的相同位置包含一个白色圆圈。
由于白色圆圈是在其关联的链接之后创建的,它们将被定位在链接上方,从而隐藏它们的中间部分。然后只插入标签,因此它们在白色圆圈之上。

白色圆圈看起来比“text-shadow”更好。 - Gerardo Furtado
1
谢谢,主要功劳应该归于你,因为你想出了 getPointAtLength 的解决方案 ;p - Xavier Guihot

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