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.
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) + "...")
}
}
}