Demonstration of a couple of possible methods for highlighting or labelling a group of elements in a treemap layout.
This treemap layout instantiation produces a g
element for each item in a hierarchical data structure; leaf nodes have a text element with the item name in it, and a black stroke around the edge; the g
node representing the parent term has a colour fill. Nodes are labelled with a class name derived from the item name with spaces and special characters removed, and leaf
, internal
, or root
, depending on their position in the hierarchy.
The default highlighting scheme in this layout adds a highlight
class to the g
element representing the parent node for a certain country; e.g. clicking on China in the legend highlights the g
element matching the selection criteria .internal.China
. It could easily be adapted to apply the class to all .China
nodes or .leaf.China
elements. CSS can be used to change the appearance of the elements.
A second method of highlighting the parent element is to create an overlay which takes its position and dimensions from the parent. This can be styled as desired.
For Stack Overflow question how to group g
elements in d3.
xxxxxxxxxx
<html lang="en">
<head>
<title>Treemap grouping</title>
<meta charset="UTF-8">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
<style>
.newcell text {
font-family: Arial, sans-serif;
font-size: 10px;
}
.newcell.leaf rect {
stroke: #000;
stroke-width: 0.2px;
}
.newcell.highlight rect {
stroke: #fff;
stroke-width: 2px;
fill-opacity: 0.5;
}
.oversize {
display: none;
}
.internal text {
opacity: 0
}
.internal.highlightAll text {
opacity: 1
}
.highlightAll.leaf rect {
opacity: 0.1
}
.highlightAll.leaf text {
opacity: 0
}
.highlighter rect {
fill: #000;
fill-opacity: 0.7;
stroke: deepskyblue;
stroke-width: 5px;
}
.highlighter text {
fill: deepskyblue;
opacity: 1
}
label {
display: inline-block;
padding-left: 5px;
}
ul {
list-style-type: none;
}
</style>
</head>
<body>
<div class="container">
<h2>Grouping the leaf cells of a treemap by parent node</h2>
<p>Click on a legend item to add a "highlight" class to the group in the treemap.</p>
<div id="controls"></div>
<div id="test"></div>
</div>
<script type="text/javascript">
d3.json('data.json', function(error, json) {
if (error) return console.warn(error);
visualizeit(json);
});
function visualizeit(data) {
var id = '#test';
var coordinates = { x: 0, y: 0 };
var margin = { top: 10, right: 15, bottom: 10, left: 15 }
var cfg = {
margin: { top: 10, right: 10, bottom: 10, left: 10 },
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,
half = cfg.height / 2;
/////////////////////////////////////////////////////////
//////////// 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 + ")");
var highlightG = canvas.append('g')
.attr("transform", "translate(" + cfg.margin.left + "," + cfg.margin.top + ")")
.append('g')
.classed('highlighter', true)
.attr('opacity',0);
highlightG.append('rect');
highlightG.append('text');
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding(1)
.sticky(true)
.nodes(data);
var parentNameValue = {};
data.children.forEach( function(a) {
parentNameValue.hasOwnProperty(a.name)
? parentNameValue[a.name] += a.value
: parentNameValue[a.name] = a.value;
})
var cells = innercanvas
.selectAll(".newcell")
.data(treemap)
.enter()
.append("g")
.attr("class", function (d,i) {
return 'newcell' + ' _' + i
+ ' cell-level-' + d.depth
+ ( d.name ? ' ' + safe_name(d.name) : '' )
+ ( ! d.children
? ' leaf'
: (d.depth === 0
? ' root'
: ' internal '));
})
cells
.append("rect")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("width", function (d) {
return d.dx;
})
.attr("height", function (d) {
return d.dy;
})
.style("fill", function (d) {
return d.children && d.parent
? cfg.color(d.name)
: 'none';
})
cells
.append('title')
.text(function(d){ return d.name });
cells
.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.parent ? d.name : '';
})
addText(cells);
addControls([{
name: 'overlay',
text: 'overlay a "g" element using the coordinates of the existing group'
},{
name: 'hilite_all',
text: 'apply the "highlight" class to all "g" elements in the group'
}]);
var current = '';
// create a legend for this chart
var legendData = Object.keys(parentNameValue)
.sort( function(a,b){
return parentNameValue[b] - parentNameValue[a]
})
.map(function(a){
return { name: a, value: parentNameValue[a] }
})
var legend = d3.select(id)
.append('svg')
.attr('width', cfg.width)
.attr('height', Math.ceil(25 * legendData.length/3))
.append('g')
.attr('legend', true);
var itemEnter = legend.selectAll('g.legendItem')
.data(legendData)
.enter()
.append('g')
.attr('class', function(d){
return 'legendItem ' + safe_name(d.name);
})
.attr('transform', function (d,i) {
return 'translate('
+ i%3 * Math.round(cfg.width/3) + ','
+ Math.floor(i/3)*25 + ')';
})
.on('click', legendClick)
itemEnter.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', '40')
.attr('height', '20')
.attr('fill', function(d){ return cfg.color(d.name); })
.attr('class', 'legendRect')
itemEnter.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('class', 'legendText')
.text(function(d){ return d.name })
.attr('transform', 'translate(45, 15)')
function legendClick (d) {
var values = {},
t = d3.transition()
.duration(250);
d3.select('#controls').selectAll('input')
.filter(function (o) {
if (o) {
return this.checked;
}
})
.each(function (option) {
values[option.name] = 1;
});
d3.selectAll('.highlightAll')
.classed('highlightAll', false)
d3.selectAll('.highlight')
.classed('highlight', false)
// repeat click on current node
if ( safe_name(d.name) === current ) {
current = ''
if ( values.overlay ) {
highlightG.transition(t)
.attr('opacity', 0)
}
}
else {
d3.select('.internal.' + safe_name(d.name))
.classed('highlight', true );
if ( values.hilite_all ) {
d3.selectAll('.' + safe_name(d.name))
.classed('highlightAll', true);
}
if ( values.overlay ) {
highlightDatum( d3.select('.internal.' + safe_name(d.name)).datum(), t );
current = safe_name(d.name);
}
}
}
function highlightDatum ( d, t ) {
highlightG
.transition(t)
.attr('opacity', 1)
highlightG.select('rect')
.transition(t)
.attr("x", d.x)
.attr("y", d.y)
.attr("width", d.dx)
.attr("height", d.dy)
highlightG
.select("text")
.transition(t)
.attr("x", d.x + d.dx / 2 )
.attr("y", d.y + d.dy / 2 )
.attr('dy', '.35em')
.attr("text-anchor", "middle")
.text(function () {
return d.name;
})
}
function addText( selection ) {
var v_pad = 2, // vertical padding
h_pad = 4 // horizontal padding
selection.selectAll('.leaf text')
.classed('oversize', function(d) {
if ( ! d.name ) {
return false;
}
var bbox = this.getBBox();
if ( d.dx <= bbox.width + h_pad || d.dy <= bbox.height + v_pad ) {
return true;
}
return false;
});
}
function addControls (types) {
// add in some controls
var controls = d3.select('#controls')
.append('ul')
.classed('control-group', true)
.on('change', cboxChange)
var options = controls.selectAll('li')
.data(types)
.enter()
.append('li');
options
.append('input')
.attr('type', 'checkbox')
.attr('value', d => d.name )
.attr('name', 'switcher')
.attr('id', function (d, i) {
return 'input_' + i;
});
options
.append('label')
.attr('for', function (d, i) {
return 'input_' + i;
})
.text( d => d.text );
}
function cboxChange () {
// reset all highlights
highlightG
.attr('opacity', 0)
d3.selectAll('.highlight')
.classed('highlight', false)
d3.selectAll('.highlightAll')
.classed('.highlightAll', false)
}
function safe_name (txt) {
return txt.replace(/\W/g, '_');
}
}
</script>
</body>
</html>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js