如何在 D3 图表的标签中包含换行符?

IT技术 javascript svg d3.js
2021-02-02 10:27:49

我正在使用 D3 生成条形图(我改编了此示例中的代码)。我在x轴上使用的标签每个都有几个字长,由于这会使所有标签重叠,我需要将这些标签分成几行。(如果我可以用换行符替换每个标签中的所有空格就好了。)

我最初通过用文字换行符 ( &#xA;)替换空格并设置xml:space="preserve"标签<text>元素来尝试此操作不幸的是,事实证明 SVG 不尊重这个属性。接下来,我尝试将每个单词包装成<tspan>我以后可以设计的样式。我通过这个函数传递了每个标签:

function (text) {
    return '<tspan>' + text.replace(/ /g, '</tspan><tspan>') + '</tspan>';
}

但这只是将文字<tspan>s 放入输出中。如何将我的文本标签包裹在tspans (或做其他事情)中,以便我的标签不重叠?

6个回答

我最终使用以下代码来打破每个x轴标签:

var insertLinebreaks = function (d) {
    var el = d3.select(this);
    var words = d.split(' ');
    el.text('');

    for (var i = 0; i < words.length; i++) {
        var tspan = el.append('tspan').text(words[i]);
        if (i > 0)
            tspan.attr('x', 0).attr('dy', '15');
    }
};

svg.selectAll('g.x.axis g text').each(insertLinebreaks);

请注意,这假定标签已经创建。(如果您遵循规范直方图示例,那么标签将按照您需要的方式设置。)也不存在任何真正的换行逻辑;该函数将每个空格转换为换行符。这很符合我的目的,但您可能需要编辑该split()行以使其更智能地将字符串的各个部分划分为行。

这对我使用 v4 有用。我在日期格式中添加了一个管道,因为这是我希望我的换行符出现的地方:var dateFormat = d3.timeFormat("%b %e|%H:%M"); ... var el = d3.select(this); var words = el.text().split('|'); el.text('');
2021-03-15 10:27:49
这正是我需要的,谢谢。对于那些可能处于类似情况的人,我应该指出这d是一个数据点,而不是您要格式化的字符串,因此.split()(至少对我而言)需要将d.description.split("\n");.
2021-03-24 10:27:49
对于使用它的其他人,发现我需要将变量 words 设置为 d.toString().split(' ')。
2021-04-01 10:27:49
除了@DanielQuinn 建议在换行符上拆分之外,对此的另一个改进:用于d3.select(this).text()获取实际元素文本而不是绑定数据值。这很重要有两个原因:首先,您的轴值可能是数字而不是字符串(在这种情况下此代码失败),其次,我的改进允许通过tickFormat轴生成器上的函数插入自定义文本
2021-04-02 10:27:49
工作精美。对于从事此工作的人,请确保您拥有正确的元素。
2021-04-04 10:27:49

SVG 文本元素不支持文本换行,因此有两种选择:

  • 将文本拆分为多个 SVG 文本元素
  • 在 SVG 顶部使用叠加 HTML div

这里查看 Mike Bostock 对此的评论

我发现有用的是使用“foreignObject”标签而不是文本或 tspan 元素。这允许 HTML 的简单嵌入,允许单词自然分解。需要注意的是,满足特定需求的对象的整体尺寸:

var myLabel = svg.append('foreignObject')
    .attr({
        height: 50,
        width: 100, // dimensions determined based on need
        transform: 'translate(0,0)' // put it where you want it...
     })
     .html('<div class"style-me"><p>My label or other text</p></div>');

以后可以使用 d3.select/selectAll 获取您放置在此对象内的任何元素,以动态更新文本值。

环顾四周后,我发现 Mike Bostock 提供了一个解决方案,使您能够将文本环绕。

http://bl.ocks.org/mbostock/7555321

在我的代码上实现它(我使用折叠树图)。我只是简单地复制了“wrap”方法。

然后附上以下内容

    // Standard code for a node    
    nodeEnter.append("text")
        .attr("x", function(d) { return d.children || d._children ? -10 : 10; })
        .attr("dy", ".35em")
        .text(function(d) { return d.text; })
        // New added line to call the function to wrap after a given width
        .call(wrap, 40);

我看不出有任何理由不适用于力导向、条形或任何其他模式

修正案 :

对于阅读此内容并使用可折叠图形的任何人,我已将 wrap 函数修改为以下内容。“x”属性的更改正确设置了对齐方式,在原始代码中指出问题时在单独的行上执行递增行号,并且“y”已硬设置为零,否则会出现行间距增加的问题每条线。

function wrap(text, width) {
    text.each(function() {
        var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        y = text.attr("y"),
        dy = parseFloat(text.attr("dy")),
        lineHeight = 1.1, // ems
        tspan = text.text(null).append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", y).attr("dy", dy + "em");     
        while (word = words.pop()) {
            line.push(word);
            tspan.text(line.join(" "));
            var textWidth = tspan.node().getComputedTextLength();
            if (tspan.node().getComputedTextLength() > width) {
                line.pop();
                tspan.text(line.join(" "));
                line = [word];
                ++lineNumber;
                tspan = text.append("tspan").attr("x", function(d) { return d.children || d._children ? -10 : 10; }).attr("y", 0).attr("dy", lineNumber * lineHeight + dy + "em").text(word);
            }
        }
    });
}

关于包装长标签也有这个答案。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.bar {
  fill: steelblue;
}

.bar:hover {
  fill: brown;
}

.title {
  font: bold 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
}

.axis {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>

var margin = {top: 80, right: 180, bottom: 80, left: 180},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.ordinal()
    .rangeRoundBands([0, width], .1, .3);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .ticks(8, "%");

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.tsv("data.tsv", type, function(error, data) {
  x.domain(data.map(function(d) { return d.name; }));
  y.domain([0, d3.max(data, function(d) { return d.value; })]);

  svg.append("text")
      .attr("class", "title")
      .attr("x", x(data[0].name))
      .attr("y", -26)
      .text("Why Are We Leaving Facebook?");

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis)
    .selectAll(".tick text")
      .call(wrap, x.rangeBand());

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis);

  svg.selectAll(".bar")
      .data(data)
    .enter().append("rect")
      .attr("class", "bar")
      .attr("x", function(d) { return x(d.name); })
      .attr("width", x.rangeBand())
      .attr("y", function(d) { return y(d.value); })
      .attr("height", function(d) { return height - y(d.value); });
});

function wrap(text, width) {
  text.each(function() {
    var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        lineHeight = 1.1, // ems
        y = text.attr("y"),
        dy = parseFloat(text.attr("dy")),
        tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
    while (word = words.pop()) {
      line.push(word);
      tspan.text(line.join(" "));
      if (tspan.node().getComputedTextLength() > width) {
        line.pop();
        tspan.text(line.join(" "));
        line = [word];
        tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
      }
    }
  });
}

function type(d) {
  d.value = +d.value;
  return d;
}

</script>

和数据文件“data.tsv”:

name    value
Family in feud with Zuckerbergs .17
Committed 671 birthdays to memory   .19
Ex is doing too well    .10
High school friends all dead now    .15
Discovered how to “like” things mentally    .27
Not enough politics .12