Search code examples
javascriptd3.jsslicetruncatetreemap

D3 Text Slice based on the width of rect


I am building a tree map with D3 v4 and all good so far. However, some of the text within their respective rectangles goes out over the edge of the rectangle. I want to use text slice to cut off the text if it does this, and instead put in three dots.

As a test, I have been able to get the slice function to truncate text that goes beyond let's say 5 characters, but when I try to specify that I want the slice function to truncate based on the width of the corresponding rectangle, it doesn't work on all except one (which I think is because it goes out over the edge of the whole tree map.

I can't seem to find a way to pull in the width of the rectangles to the slice function in order to compare it to the width of the text.

  // set the dimensions and margins of the graph
    var margin = {top: 10, right: 10, bottom: 10, left: 10},
      width = 945 - margin.left - margin.right,
      height = 1145 - margin.top - margin.bottom;
    
    // append the svg object to the body of the page
    var svg = d3.select("#my_dataviz")
    .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 + ")");
    
    // Read data
    d3.csv('https://raw.githubusercontent.com/rootseire/survey/main/treemap-data.csv', function(data) {
    
      // stratify the data: reformatting for d3.js
      var root = d3.stratify()
        .id(function(d) { 
          return d.name; })   // Name of the entity (column name is name in csv)
        .parentId(function(d) { return d.parent; })   // Name of the parent (column name is parent in csv)
        (data);
      root.sum(function(d) { return +d.value })   // Compute the numeric value for each entity
    
      // Then d3.treemap computes the position of each element of the hierarchy
      // The coordinates are added to the root object above
      d3.treemap()
        .size([width, height])
        .padding(4)
        (root)
    
      // use this information to add rectangles:
      svg
        .selectAll("rect")
        .data(root.leaves())
        .enter()
        .append("rect")
          .attr('x', function (d) { return d.x0; })
          .attr('y', function (d) { return d.y0; })
          .attr('width', function (d) { return d.x1 - d.x0; })
          .attr('height', function (d) { return d.y1 - d.y0; })
          .style("stroke", "black")
          .style("fill", "#94C162")
      .attr("class", "label")
      .on("mouseover", function(d) {
  tip.style("opacity", 1)
     .html("Genre: " + d.data.name + "<br/> Number: " + d.value + "<br/>")
     .style("left", (d3.event.pageX-25) + "px")
     .style("top", (d3.event.pageY-25) + "px")
  })
  .on("mouseout", function(d) {
    tip.style("opacity", 0)
  });
 
      svg
        .selectAll("text")
        .data(root.leaves())
        .enter()
        .append("text")
          .attr("x", function(d){ return d.x0+6})    // +10 to adjust position (more right)
          .attr("y", function(d){ return d.y0+15})    // +20 to adjust position (lower)
      .attr('dy', 0) // here
          .text(function(d){ return d.data.name + ' (' + d.data.value +')' })
          .attr("font-size", "15px")
          .attr("fill", "black")
      .each(slice);
    })
 


  // Define the div for the tooltip
var tip = d3.select("#my_dataviz").append("div")
    .attr("class", "tooltip")
    .style("opacity", 0)
// Add events to circles

d3.selectAll('.label')
  .attr("x", function(t) {
    return Math.max(0, 100-this.textLength.baseVal.value);
  });


  function slice(d) { 
    var self = d3.select(this), 
        textLength = self.node().getComputedTextLength(), 
        text = self.text(); 
    
    while (textLength > text.getBoundingClientRect().width && text.length > 0) { 
      text = text.slice(0, 5); 
      self.text(text + '...'); 
      textLength = self.node().getComputedTextLength();
    } 
  }
.tooltip {
  position: absolute;
  pointer-events: none;
  background: #000;
  color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<!DOCTYPE html>

<head>
  <script type="text/javascript" src="https://raw.githubusercontent.com/rootseire/survey/main/word_wrap.js"></script>
</head>
<meta charset="utf-8">
<body>
<div id="my_dataviz"></div>
  </body>
</html>

Any help greatly appreciated.


Solution

  • If you're just looking for the width of the rectangles you can search for all rect elements on the page (optionally with the class 'label')

    E.g.,:

    let allBoxWidths = [];
    
    document.querySelectorAll("rect.label").forEach((rect) => {
        // and append each value an array
    
        // rect.getAttribute("width") is the box's width
        allBoxWidths.push(parseInt(rect.getAttribute("width")));
    });
    

    Overall, if you're comparing the boxes to the text inside and trying to deduce if you need to truncate it as it is overflowing you could write something like:

    function calcAllBoxWidths() {
        let allBoxWidths = [];
        let allTextWidths = [];
    
        let allTextElements = [];
    
        // Getting all of the rectangles's widths
        document.querySelectorAll("rect.label").forEach((rect) => {
            // and append each value an array
    
            // rect.getAttribute("width") is the box's width
            allBoxWidths.push(parseInt(rect.getAttribute("width")));
        });
    
        // Then getting all of the <text>'s widths
        document.querySelectorAll("g>text").forEach((text) => {
            // and append each value an array
    
            // text.getBBox() is the text width
            allTextWidths.push(parseInt(text.getBBox().width));
            allTextElements.push(text);
        });
    
        // We will loop over every box, and then use it's i/iteration value to get the corrisponding textWidth value
        for (let i = 0; i < allBoxWidths.length; i++) {
            let boxWidth = allBoxWidths[i];
            let textWidth = allTextWidths[i];
    
            let textElement = allTextElements[i];
            
            // ========= UNCOMMENT ME IF YOU WANT EXTRA INFO ===========
            // console.log('boxWidth:', allBoxWidths[i], 'textWidth:', allTextWidths[i])
            // console.log('textElement', allTextElements[i])
            // console.log('if statement is', (allTextWidths[i] >= allBoxWidths[i]))
            // ====================
    
            // If the text is wider than the box...
            if (allTextWidths[i] >= allBoxWidths[i]) {
                // Truncate it!
                console.log('Found a text element that is too big for their box. Hover the following to see it:', allTextElements[i])
                textElement.innerHTML = (textElement.innerHTML.slice(0, 4) + "...")
            }
        }
    }