http://bl.ocks.org/mundhradevang/1387786
Something similar like this. But I have lots of data so the text is a LOT. So what is the best way to make sure each individual text wraps nicely inside the treemap cell?
I use https://github.com/iros/underscore.nest underscore.nest to nest my JSON data so it looks like this
{"children":[{"name":"Afghanistan",
"children":[{"name":"Qal eh-ye Now","value":2997},
{"name":"Mahmud-E Eraqi","value":7407}
But when drawing my treemap, the text are all over the place,
This is my D3 code to append the text :
cells
.append("text")
.attr("x", function (d) {
return d.x + d.dx / 2;
})
.attr("y", function (d) {
return d.y + d.dy / 2;
})
.attr("text-anchor", "middle")
.text(function (d) { return d.children ? null : d.name })
Full Dataset: https://api.myjson.com/bins/g9p4s
My Full Code :
var coordinates = { x: 0, y: 0 };
var margin = { top: 40, right: 10, bottom: 90, left: 20 }
var cfg = {
margin: { top: 40, right: 10, bottom: 90, left: 140 },
width: 960 - margin.left - margin.right,
height: 500 - margin.top - margin.bottom,
color: d3.scale.category20()
};
//Put all of the options into a variable called cfg
if ('undefined' !== typeof options) {
for (var i in options) {
if ('undefined' !== typeof options[i]) { cfg[i] = options[i]; }
}//for i
}
var treemap,
legendCategories,
uniqueCategories;
var half = cfg.height / 2;
var tool = d3.select("body").append("div").attr("class", "toolTip");
/////////////////////////////////////////////////////////
//////////// Create the container SVG and g /////////////
/////////////////////////////////////////////////////////
//Remove whatever chart with the same id/class was present before
d3.select(id).select("svg").remove();
//Initiate the radar chart SVG
var canvas = d3
.select(id)
.append("svg")
.attr("class", "chart")
.attr("width", cfg.width + cfg.margin.left + cfg.margin.right)
.attr("height", cfg.height + cfg.margin.top + cfg.margin.bottom)
.attr("id", "canvas");
var innercanvas = canvas
.append("g")
.attr("class", "innercanvas")
.attr("transform", "translate(" + cfg.margin.left + "," + cfg.margin.top + ")");
legendCategories = data.children.map(a => a.name);
uniqueCategories = legendCategories.filter(onlyUnique);
var categoryTitle = String(categoryKey);
categoryTitle = categoryTitle.substring(categoryTitle.indexOf("." + 1));
categoryScale = cfg.color;
categoryScale.domain(uniqueCategories);
verticalLegend = d3.svg
.legend()
.labelFormat("none")
.cellPadding(5)
.orientation("vertical")
.units(categoryTitle)
.cellWidth(20)
.cellHeight(10)
.place(coordinates)
.inputScale(categoryScale)
.cellStepping(10);
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding(.25)
.sticky(true)
.nodes(data);
var cells = innercanvas
.selectAll(".newcell")
.data(treemap)
.enter()
.append("g")
.attr("class", "newcell");
cells
.append("rect")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("id", "rectangle")
.attr("width", function (d) {
return d.dx;
})
.attr("height", function (d) {
return d.dy;
})
.style("fill", function (d) {
return d.children ? cfg.color(d.name) : null;
})
.attr("stroke", "#000000")
.attr('pointer-events', 'all')
.on("mousemove", function (d) {
tool.style("left", d3.event.pageX + 10 + "px")
tool.style("top", d3.event.pageY - 20 + "px")
tool.style("display", "inline-block");
tool.html(d.children ? null : d.name + "<br>" + d.value);
}).on("mouseout", function (d) {
tool.style("display", "none");
});
cells
.append("text")
.attr("x", function (d) {
return d.x + d.dx / 2;
})
.attr("y", function (d) {
return d.y + d.dy / 2;
})
.attr("text-anchor", "middle")
.text(function (d) { return d.children ? null : d.name })
canvas
.append("g")
.attr("transform", "translate(40,50)")
.attr("class", "legend")
.attr("id", "legend")
.call(verticalLegend);
function onlyUnique(value, index, self) {
return self.indexOf(value) === index;
}
There are a few ways to approach the problem of having a large dataset of strings of varying length that we need to fit into boxes of varying sizes.
One way would be to add a clipping path (clip-path
) to each rectangle element, but I think that would be over the top for a visualisation like this, so we'll use other means.
First, you could add a title element to each g
; the default action for most browsers is to show a tooltip on mousing over a title. So:
cells
.append('title')
.text(function(d){ return d.name });
Now let's look at the text elements. Set the font-family
and font-size
for the text nodes in your stylesheet or document head so that we are dealing with predictable text sizes.
.newcell text {
font-family: Arial, sans-serif;
font-size: 10px;
}
I would advise shifting the text down slightly as the current code sets the baseline of the text at the vertical centre of the cell, which is too high. Here I've added an offset of 0.35em
:
selection
.append("text")
.attr("x", function (d) {
return d.x + d.dx / 2;
})
.attr("y", function (d) {
return d.y + d.dy / 2;
})
.attr('dy', '.35em')
.attr("text-anchor", "middle")
.text(function (d) {
return d.children ? '' : d.name;
})
We can filter the visibility of the text nodes by altering the opacity (which means we can easily toggle the opacity to show / hide the text), using
cells
.style('opacity', function(d){
// some function returning 0 or 1
});
There are various different methods we could use to decide whether to show or hide the text. One would be to have a minimum width and minimum height that the cell must be to show text in it, e.g.
var minHeight = 12,
minWidth = 20;
cells
.style('opacity', function(d){
if ( d.dx <= minWidth || d.dy <= minHeight ) {
return 0
};
return 1;
});
Another way could be to calculate the approximate width of the word and test that against the box width. I found a table of average character widths for common fonts at https://www.math.utah.edu/~beebe/fonts/afm-widths.html, so we can guess the width of the word by multiplying the word length by the average character width (in points), and multiply that by the font size (in points) and the conversion factor for converting points to pixels
var pt_px = 0.75, // convert font size in pt into pixels
font_size = 10, // or whatever it is set to in the stylesheet
averageLetterWidth = 0.58344; // average character width for Arial
cells
.style('opacity', function(d){
if ( d.name.length * averageLetterWidth * pt_px * font_size >= d.dx ) {
return 0
}
return 1;
});
And yet another method would be to compare the size of the bounding box of the text element to the height and width of the cell:
cells
.style('opacity', function(d){
var bbox = this.getBBox();
if ( d.dx <= bbox.width || d.dy <= bbox.height ) {
return 0;
}
return 1;
});
You may also want to ensure there is some padding between the edge of the text and the box:
var h_pad = 2, // 2 pixels vertical padding
v_pad = 4; // 4 pixels of horizontal padding (2 px at each side)
cells
.style('opacity', function(d){
var bbox = this.getBBox();
if ( d.dx <= bbox.width + h_pad || d.dy <= bbox.height + v_pad ) {
return 0;
}
return 1;
});
You can choose whichever of these methods is most appropriate for your use case (or combine them all!). I have put together a block that demonstrates the different calculations and their effects.