首页 > 解决方案 > 如何让 d3 树图单元格文本换行而不溢出其他单元格

问题描述

我的 d3 树形图中单元格的文本不会换行和溢出其他单元格。这是我的项目

我希望文本看起来像这个项目。我查看了他们的代码(以及许多其他代码),但我无法让它在我的项目中工作。

问题区域是:

svg.append('text')
.selectAll('tspan')
.data(root.leaves())
.enter()
.append('tspan')
.attr("x", (d) => d.x0 + 5)
.attr("y", (d) => d.y0 + 20)
.text( (d) => d.data.name)   //.html( (d) =>       d.data.name.replace(/\s/g, "<br>"))
.attr("font-size", "0.6em")
.attr("fill", "white");

我尝试在评论中使用 .html 而不是 .text 。在 Safari 和 Chrome 中,文本仍然会溢出单元格。在 Firefox 中,仅显示电影名称的第一个单词。

标签: javascriptd3.js

解决方案


我们有两个选项以与您提供的示例类似的方式显示文本。

第一种也是最简单的方法是保持您的代码结构,并按照提供的示例进行类似的拆分文本的过程:

d.data.name.split(/(?=[A-Z][^A-Z])/g)

所以让我们稍微改变一下你的代码:

  svg.selectAll('text')
      .data(root.leaves())
      .enter()
      .append('text')
      .selectAll('tspan')
      .data(d => {
          return d.data.name.split(/(?=[A-Z][^A-Z])/g) // split the name of movie
              .map(v => {
                  return {
                      text: v,
                      x0: d.x0,                        // keep x0 reference
                      y0: d.y0                         // keep y0 reference
                  }
              });
      })
      .enter()
      .append('tspan')
      .attr("x", (d) => d.x0 + 5)
      .attr("y", (d, i) => d.y0 + 15 + (i * 10))       // offset by index 
      .text((d) => d.text)
      .attr("font-size", "0.6em")
      .attr("fill", "white");

这应该完成所需的显示。我们必须考虑到标签是一种非常难以定位和显示的避免重叠的方式,因为它在构建时需要更多的计算。

第二种方法是稍微更改代码结构并创建单元格,就像提供的示例一样:

const cell = svg.selectAll('g')
    .data(root.leaves())                                    
    .enter()
    .append('g')                                              // create a group for each cell / movie
    .attr('transform', d => `translate(${d.x0},${d.y0})`)     // let the group element handle the general positioning
    .on('mousemove', d => {
      //...
    })
    .on('mouseout', d => {
      //...
    });

cell.append('rect')                                           // append rect for each cell / movie
    .attr('id', d => d.data.id)
    .attr('class', 'tile')
    .attr('data-name', d => d.data.name)
    .attr('data-value', d => d.data.value)
    .attr('data-category', d => d.data.category)
    .attr('width', d => d.x1 - d.x0)
    .attr('height', d => d.y1 - d.y0)
    .attr('fill', d => color(d.data.category));

cell.append('text')                                           // append text node for each cell / movie
    .selectAll('tspan')                                       
    .data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g))         // split the name and use that as data to create indiviual tspan elements
    .enter()
    .append('tspan')                                          // append tspan node for each element of the string which got split
    .attr('font-size', '8px')
    .attr('x', 4)
    .attr('y', (d, i) => 13 + 10 * i)                         // offset the y positioning with the index of the data
    .text(d => d);

方法 1 的 CodePen

方法 2 的 CodePen

方法1的完整代码:

// !! IMPORTANT README:

// You may add additional external JS and CSS as needed to complete the project, however the current external resource MUST remain in place for the tests to work. BABEL must also be left in place. 

const w = 960;
const h = 600;
const padding = 60;
const svg = d3.select("#container").append("svg")
              .attr("width", w).attr("height", h);
const legendsvg = d3.select("#legend").append("svg")
              .attr("width", 960).attr("height", 50);
const legendPadding = 10;

d3.json("https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/movie-data.json") 
.then(function(data) {
    var root = d3.hierarchy(data).sum(function(d){ return d.value});

    var treeMap = d3.treemap()
        .size([w, h])
        .paddingInner(1);

    treeMap(root);

    const toolTip = d3
        .select("#container")
        .append("div")
        .attr("class", "tooltip")
        .style("opacity", 0);

    var color = d3.scaleOrdinal()
        .domain(["Action", "Drama", "Adventure", "Family", "Animation", "Comedy", "Biography"])
        .range(["#db8a00", "#75b0ff", "#13ad37", "#5d6d00", "#757582", "#d37cff", "#f96868"])

    svg.selectAll("rect")
        .data(root.leaves())
        .enter().append("rect")
        .attr("class", "tile")
        .attr("data-name", (d) => d.data.name)
        .attr("data-category", (d) => d.data.category)
        .attr("data-value", (d) => d.data.value)
        .attr('x', (d) => d.x0)
        .attr('y', (d) => d.y0)
        .attr('width', (d) => d.x1 - d.x0)
        .attr('height', (d) => d.y1 - d.y0)
        .style("stroke", "black")
        .style("fill", (d) => color(d.parent.data.name))
        .on("mouseover", (d, i) => {
            toolTip
              .transition()
              .duration(0)
              .style("opacity", 0.8);
            toolTip
              .attr("id", "tooltip")
              .html(function() {
              return "<span>" + "Name: " + d.data.name + "<br />" + "Category: " + d.data.category + "<br />" + "Value: " + d.data.value + "</span>";
            })
              .style("left", d3.event.pageX - 87.5 + "px") // -87.5 is half width of tooltip in css
              .style("top", d3.event.pageY - 75 + "px")
              .attr("data-value", d.data.value);
          })
           .on("mouseout", function(d) {
            toolTip
              .transition()
              .duration(0)
              .style("opacity", 0);
           });

 svg.selectAll('text')
      .data(root.leaves())
      .enter()
      .append('text')
      .selectAll('tspan')
      .data(d => {
          return d.data.name.split(/(?=[A-Z][^A-Z])/g) // split the name of movie
              .map(v => {
                  return {
                      text: v,
                      x0: d.x0,                        // keep x0 reference
                      y0: d.y0                         // keep y0 reference
                  }
              });
      })
      .enter()
      .append('tspan')
      .attr("x", (d) => d.x0 + 5)
      .attr("y", (d, i) => d.y0 + 15 + (i * 10))       // offset by index 
      .text((d) => d.text)
      .attr("font-size", "0.6em")
      .attr("fill", "white");

console.log(root.leaves());
       /*svg.selectAll("text")
        .data(root.leaves())
        .enter()
        .append("text")
          .attr("x", function(d){ return d.x0+5})    
          .attr("y", function(d){ return d.y0+20})   
          .text(function(d){ return d.data.name })
          .attr("font-size", "0.6em")
          .attr("fill", "white")*/

      legendsvg.selectAll('rect')
          .data(root.children)
          .enter()
          .append('rect')
          .attr('class', 'legend-item')
          .style('stroke', 'white')
          .attr('x', (d,i) => i*140 )
          .attr('width', 130)
          .attr('height', 20)
          .style('fill', d => color(d.data.name))

     legendsvg.selectAll('text')
            .data(root.children)
            .enter()
            .append('text')
            .attr('x', (d,i) => i*140)
            .attr('y', 40)
            .text(d => d.data.name);

      //had to change the legend below because it wouldn't pass fcc test
  /*legendsvg.append("g").classed("legend", true).classed("legend-item", true);
  const legend = d3.legendColor().shape("rect")
    .shapeWidth(90).cells(7).orient("horizontal").scale(color);
  legendsvg.select(".legend").call(legend);*/ 
});

方法2的完整代码:

// !! IMPORTANT README:

// You may add additional external JS and CSS as needed to complete the project, however the current external resource MUST remain in place for the tests to work. BABEL must also be left in place. 

const w = 960;
const h = 600;
const padding = 60;
const svg = d3.select("#container").append("svg")
              .attr("width", w).attr("height", h);
const legendsvg = d3.select("#legend").append("svg")
              .attr("width", 960).attr("height", 50);
const legendPadding = 10;

d3.json("https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/movie-data.json") 
.then(function(data) {
    var root = d3.hierarchy(data).sum(function(d){ return d.value});

    var treeMap = d3.treemap()
        .size([w, h])
        .paddingInner(1);

    treeMap(root);

    const toolTip = d3
        .select("#container")
        .append("div")
        .attr("class", "tooltip")
        .style("opacity", 0);

    var color = d3.scaleOrdinal()
        .domain(["Action", "Drama", "Adventure", "Family", "Animation", "Comedy", "Biography"])
        .range(["#db8a00", "#75b0ff", "#13ad37", "#5d6d00", "#757582", "#d37cff", "#f96868"])

    const cell = svg.selectAll('g')
            .data(root.leaves())
            .enter()
            .append('g')
            .attr('transform', d => `translate(${d.x0},${d.y0})`)
            .on('mousemove', d => {
              toolTip.transition()
                      .duration(200)
                      .style('opacity', 0.75);
              toolTip.attr('data-value', d.data.value);
              toolTip.html(
                'Name: ' + d.data.name + '<br>' +
                'Category: ' + d.data.category + '<br>' +
                'Value: ' + d.data.value
              )
                .style('top', `${d3.event.pageY + 10}px`)
                .style('left', `${d3.event.pageX + 8}px`);
            })
            .on('mouseout', d => {
              toolTip.transition()
                      .duration(200)
                      .style('opacity', 0);
            });


      cell.append('rect')
          .attr('id', d => d.data.id)
          .attr('class', 'tile')
          .attr('data-name', d => d.data.name)
          .attr('data-value', d => d.data.value)
          .attr('data-category', d => d.data.category)
          .attr('width', d => d.x1 - d.x0)
          .attr('height', d => d.y1 - d.y0)
          .attr('fill', d => color(d.data.category));

      cell.append('text')
          .selectAll('tspan')
          .data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g))
          .enter()
          .append('tspan')
          .attr('font-size', '8px')
          .attr('x', 4)
          .attr('y', (d, i) => 13 + 10*i)
          .text(d => d);



      legendsvg.selectAll('rect')
          .data(root.children)
          .enter()
          .append('rect')
          .attr('class', 'legend-item')
          .style('stroke', 'white')
          .attr('x', (d,i) => i*140 )
          .attr('width', 130)
          .attr('height', 20)
          .style('fill', d => color(d.data.name))

     legendsvg.selectAll('text')
            .data(root.children)
            .enter()
            .append('text')
            .attr('x', (d,i) => i*140)
            .attr('y', 40)
            .text(d => d.data.name);

      //had to change the legend below because it wouldn't pass fcc test
  /*legendsvg.append("g").classed("legend", true).classed("legend-item", true);
  const legend = d3.legendColor().shape("rect")
    .shapeWidth(90).cells(7).orient("horizontal").scale(color);
  legendsvg.select(".legend").call(legend);*/ 
});

推荐阅读