Built with blockbuilder.org
xxxxxxxxxx
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
position: relative;
}
.node {
box-sizing: border-box;
position: absolute;
overflow: hidden;
}
.node-label {
padding: 4px;
line-height: 1em;
white-space: pre;
}
.node-value {
color: rgba(0,0,0,0.9);
font-size: 9px;
margin-top: 1px;
}
.tooltip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.9);
color: #fff;
border-radius: 2px;
pointer-events:none !important;
}
/* Creates a small triangle extender for the tooltip */
.tooltip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.9);
content: "\90BC";
position: absolute;
text-align: center;
}
.rect {
opacity: 0.9;
}
/* Style northward tooltips differently */
.tooltip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}
</style>
<p> <input type="checkbox" id="myCheckbox"> Switch to cluster view </p>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="d3-tip.js"></script>
<script>
var dataFile = 'economia_estados.csv';
d3.csv(dataFile, function(error, data){
console.log(data);
var input = {'dataElem': formatJsonElements(data),
'dataClus': formatJsonClusters(data),
'width': 960, 'height': 600},
canvas = setUpSvgCanvas(input);
console.log(input.dataElem);
drawRects(input, canvas);
});
function drawRects(input, canvas) {
var params = {'input': input, 'canvas': canvas};
initialize(params);
}
function initialize(params){
// unpacking params
var canvas = params.canvas,
input = params.input;
// unpacking canvas
var svg = canvas.svg,
width = params.width = canvas.width,
height = params.height = canvas.height;
// transitions duration
params.transClusterDuration = 1000;
params.transZoomDuration = 500;
// value format
var format = params.format = d3.formatPrefix(",.0", 1e1);
// setting up colors
var color = params.color = d3.scaleOrdinal()
.range(['#3E4095', '#ff5a28', '#ffdc60']);
// setting up treemap
var treemap = params.treemap = d3.treemap()
.size([width, height])
.padding(1.2)
.round(true);
// unpacking data correctly formatted for element view and cluster view
var dataElem = params.input.dataElem,
dataClus = params.input.dataClus;
// setting up hierarchy for both views
var rootElem = params.rootElem = d3.hierarchy(dataElem)
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.height - a.height || b.value - a.value; });
var rootClus = params.rootClus = d3.hierarchy(dataClus)
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.height - a.height || b.value - a.value; });
var treeElem = params.treeElem = treemap(rootElem),
treeClus = params.treeClus = treemap(rootClus);
// setting up tip
var tip = params.tip = d3.tip()
.attr('class', 'tooltip')
.offset(function(d){
return [d.parent.y0 - d.y0 + 7, -(d.x0 + d.x1)/2 + d.parent.x0 - 4 + (d.parent.x1 - d.parent.x0)/2];})
.html(function(d) {
var tip = "<div>" + d.parent.data.name + "</div>";
d.parent.children.forEach(function(e){
if(e.data.value > 0){
tip += '<div> <span style="color:' + color(e.data.name) + '">' + e.data.name + ': ' + format(e.data.value) + "</span></div>";
}
})
return tip;
})
svg.call(tip);
// both cluster view and zoom view are not the default views
params.viewCluster = false;
params.viewZoom = false;
// building vector with element names, the "before-last" layer
// will be important for the drowdown menu
var elemNames = []
rootElem.children.forEach(function(d){ if(d.children){elemNames = elemNames.concat(d.children);}});
// creating group for individual blocks, each describing the cluster of an element
var g = params.g = svg
.selectAll("g")
.data(rootElem.leaves())
.enter()
.append('g');
// creating rectangles
g.append('rect')
.attr("class", "rect")
.attr('x', function(d) { return d.x0;})
.attr('y', function(d) { return d.y0;})
.attr("height", function(d) { return d.y1 - d.y0;})
.attr("width", function(d) { return d.x1 - d.x0;})
.style('opacity', 0.9)
.attr('stroke', 'black')
.attr('stroke-width', '0px')
.attr('fill', function(d){return d.value > 0 ? color(d.data.name) : 'transparent';})
.on('mouseover', function(d){
g.selectAll('.rect').filter(function(e){
return e.parent.data.name == d.parent.data.name;
}).style('opacity', 1);
tip.show(d);
})
.on('mouseout', function(d){
g.selectAll('.rect').filter(function(e){
return e.parent.data.name == d.parent.data.name;
}).style('opacity', 0.9);
tip.hide(d);
})
.on('click', function(d){
params.viewZoom ? dezoom(params) : zoom(d, params);
tip.hide(d);
});
// creating text with element names
// using foreignobject to insert a pure html element into svg
g.append('foreignObject')
.attr("class", "text")
.attr('x', function(d) { return d.parent.x0 + 5;})
.attr('y', function(d) { return d.parent.y0 + 2;})
.attr("height", function(d) { return d.parent.y1 - d.parent.y0;})
.attr("width", function(d) { return d.parent.x1 - d.parent.x0;})
.attr('color', 'white')
.attr('pointer-events', 'none')
.html(function(d){
var elemNameText = ' ';
if(d.value > 0){
elemNameText = '<div style="width:' + (d.parent.x1 - d.parent.x0).toString() + ';">' + d.parent.data.name.substring(d.parent.data.name.lastIndexOf(".") + 1).split(/(?=[A-Z][^A-Z])/g).join("\n") + '</div>';
}
return elemNameText;
});
// create drowdown menu
var select = d3.select("body")
.append("div")
.append("select")
.on("change", onchange)
// what happens when dropdown menu is changed?
function onchange() {
selectedValue = d3.select('select').property('value');
params.g.selectAll('.rect').filter(function(d){
if(params.viewCluster){
return d.data.name == selectedValue;
}
else{
return d.parent.data.name == selectedValue;
}})
.transition()
.duration(params.transZoomDuration)
.style('opacity', 1)
.transition()
.duration(params.transZoomDuration)
.style('opacity', 0.4)
.transition()
.duration(params.transZoomDuration)
.style('opacity', 1)
.transition()
.duration(params.transZoomDuration)
.style('opacity', 0.9);
};
// appending data to dropdown menu
select.selectAll("option")
.data(elemNames.sort(function(x,y){return d3.ascending(x.data.name, y.data.name)}))
.enter().append("option")
.attr("selectedValue", function(d) { return d.data.name; })
.text(function(d) { return d.data.name; });
// initialize checkbox options
d3.select("#myCheckbox").on("change",function(){update(params);});
}
// update alternates between element and cluster view
function update(params){
if(params.viewCluster){
updateInElements(params);
}
else{
updateInClusters(params);
}
}
function zoom(elem, params){
var selected = params.g.selectAll('.rect').filter(function(d){
return d.parent.data.name == elem.parent.data.name;
});
selected
.transition()
.duration(params.transZoomDuration)
.attr('x', function(d){ return (d.x0 - d.parent.x0) * params.width / (d.parent.x1 - d.parent.x0);})
.attr('y', function(d){ return (d.y0 - d.parent.y0) * params.height / (d.parent.y1 - d.parent.y0);})
.attr('height', function(d){ return (d.y1 - d.y0) * params.height / (d.parent.y1 - d.parent.y0);})
.attr('width', function(d){ return (d.x1 - d.x0) * params.width / (d.parent.x1 - d.parent.x0);})
// selecting other rectangles, the ones that will colapse on zooming
var otherRects = params.g.selectAll('.rect').filter(function(d){
return d.parent.data.name != elem.parent.data.name;})
// geometric variables to create zoom animation
// other Rects should move away radially from source
var centerElem = [(elem.x0 + elem.x1)/2, (elem.y0 + elem.y1)/2],
diag = Math.sqrt(params.height * params.height + params.width * params.width);
otherRects
.transition()
.duration(params.transZoomDuration)
.attr('x', function(d){
var centerRect = [(d.x0 + d.x1)/2, (d.y0 + d.y1)/2],
dist = [centerRect[0] - centerElem[0], centerRect[1] - centerElem[1]],
distDiag = 0.7 * Math.sqrt(dist[0] * dist[0] + dist[1] * dist[1]);
return d.x0 + diag * dist[0]/ distDiag;
})
.attr('y', function(d){
var centerRect = [(d.x0 + d.x1)/2, (d.y0 + d.y1)/2],
dist = [centerRect[0] - centerElem[0], centerRect[1] - centerElem[1]],
distDiag = Math.sqrt(dist[0] * dist[0] + dist[1] * dist[1]);
return d.y0 + diag * dist[1]/ distDiag;
})
if(params.viewCluster){
params.g.selectAll('.text').filter(function(d){
return d.parent.data.name == elem.parent.data.name;
})
.transition()
.duration(params.transZoomDuration)
.attr('x', function(d){ return (d.x0 - d.parent.x0) * params.width / (d.parent.x1 - d.parent.x0);})
.attr('y', function(d){ return (d.y0 - d.parent.y0) * params.height / (d.parent.y1 - d.parent.y0);})
}
else{
params.g.selectAll('.text').filter(function(d){
return d.parent.data.name == elem.parent.data.name;
})
.transition()
.duration(params.transZoomDuration)
.attr('x', 30)
.attr('y', 30)
}
if(params.viewCluster){
params.g.selectAll('.text').filter(function(d){
return d.parent.data.name != elem.parent.data.name;
})
.transition()
.duration(params.transZoomDuration)
.attr('x', function(d){
var centerRect = [(d.x0 + d.x1)/2, (d.y0 + d.y1)/2],
dist = [centerRect[0] - centerElem[0], centerRect[1] - centerElem[1]],
distDiag = Math.sqrt(dist[0] * dist[0] + dist[1] * dist[1]);
return d.x0 + diag * dist[0]/ distDiag;
})
.attr('y', function(d){
var centerRect = [(d.x0 + d.x1)/2, (d.y0 + d.y1)/2],
dist = [centerRect[0] - centerElem[0], centerRect[1] - centerElem[1]],
distDiag = Math.sqrt(dist[0] * dist[0] + dist[1] * dist[1]);
return d.y0 + diag * dist[1]/ distDiag;
})
}
else{
params.g.selectAll('.text').filter(function(d){
return d.parent.data.name != elem.parent.data.name;
})
.transition()
.duration(params.transZoomDuration)
.attr('x', function(d){
var centerRect = [(d.parent.x0 + d.parent.x1)/2, (d.parent.y0 + d.parent.y1)/2],
dist = [centerRect[0] - centerElem[0], centerRect[1] - centerElem[1]],
distDiag = Math.sqrt(dist[0] * dist[0] + dist[1] * dist[1]);
return d.parent.x0 + diag * dist[0]/ distDiag;
})
.attr('y', function(d){
var centerRect = [(d.parent.x0 + d.parent.x1)/2, (d.parent.y0 + d.parent.y1)/2],
dist = [centerRect[0] - centerElem[0], centerRect[1] - centerElem[1]],
distDiag = Math.sqrt(dist[0] * dist[0] + dist[1] * dist[1]);
return d.parent.y0 + diag * dist[1]/ distDiag;
})
}
params.viewZoom = true;
}
function dezoom(params){
params.g.selectAll('.rect')
.transition()
.duration(params.transZoomDuration)
.attr('x', function(d) { return d.x0;})
.attr('y', function(d) { return d.y0;})
.attr("height", function(d) { return d.y1 - d.y0;})
.attr("width", function(d) { return d.x1 - d.x0;})
if(params.viewCluster){
params.g.selectAll('.text')
.transition()
.duration(params.transZoomDuration)
.attr('x', function(d) { return d.x0;})
.attr('y', function(d) { return d.y0;})
}
else{
params.g.selectAll('.text')
.transition()
.duration(params.transZoomDuration)
.attr('x', function(d) { return d.parent.x0 + 5;})
.attr('y', function(d) { return d.parent.y0 + 2;})
}
params.viewZoom = false;
}
// converting view to Element view
// pretty much the same thing as initialize
function updateInElements(params){
params.viewCluster = false;
var treemap = params.treemap,
rootElem = params.rootElem,
svg = params.canvas.svg,
color = params.color,
g = params.g;
var tip = params.tip
.offset(function(d){
return [d.parent.y0 - d.y0 + 7, -(d.x0 + d.x1)/2 + d.parent.x0 - 4 + (d.parent.x1 - d.parent.x0)/2];})
.html(function(d) {
var tipText = "<div>" + d.parent.data.name + "</div>";
d.parent.children.forEach(function(e){
if(e.data.value > 0){
tipText += '<div> <span style="color:' + color(e.data.name) + '">' + e.data.name + ': ' + params.format(e.data.value) + "</span></div>";
}
})
return tipText;
})
svg.call(tip);
// updating group for individual blocks, each describing the cluster of an element
g.data(rootElem.leaves(), function(d){return d.data.id;})
.select('rect')
.on('mouseover', function(d){
g.filter(function(e){
return e.parent.data.name == d.parent.data.name;
}).select('rect').style('opacity', 1);
tip.show(d);
})
.on('mouseout', function(d){
g.filter(function(e){
return e.parent.data.name == d.parent.data.name;
}).select('rect').style('opacity', 0.9);
tip.hide(d);
})
.transition()
.duration(params.transClusterDuration)
.attr('x', function(d) { return d.x0;})
.attr('y', function(d) { return d.y0;})
.attr("height", function(d) { return d.y1 - d.y0;})
.attr("width", function(d) { return d.x1 - d.x0;})
// updating element name over block
// adding element name over block
g.select('.text')
.transition()
.duration(params.transClusterDuration)
.attr('x', function(d) { return d.parent.x0 + 5;})
.attr('y', function(d) { return d.parent.y0 + 2;})
.attr("height", function(d) { return d.parent.y1 - d.parent.y0;})
.attr("width", function(d) { return d.parent.x1 - d.parent.x0;})
}
// converting view to Cluster view
function updateInClusters(params){
var treemap = params.treemap,
rootClus = params.rootClus,
svg = params.canvas.svg,
color = params.color,
g = params.g;
params.viewCluster = true;
var tip = params.tip
.offset([0,10])
.html(function(d) {
var tipText = '<div> ' + d.data.name + ': ' + params.format(d.data.value) + '</div>';
return tipText;
})
svg.call(tip);
// updating group with element names
g.data(rootClus.leaves(), function(d){return d.data.id;})
.select('rect')
.on('mouseover', function(d){
g.filter(function(e){return e.data.name == d.data.name;}).select('rect')
.style('stroke', 'black')
.style('stroke-width', '2px')
.style('opacity', 0.9)
tip.show(d);
})
.on('mouseout', function(d){
tip.hide(d);
g.selectAll('rect').filter(function(e){return e.data.name == d.data.name;})
.style('stroke-width', '0px')
.style('opacity', 0.9);
})
.transition()
.duration(params.transClusterDuration)
.attr('x', function(d) { return d.x0;})
.attr('y', function(d) { return d.y0;})
.attr("height", function(d) { return d.y1 - d.y0;})
.attr("width", function(d) { return d.x1 - d.x0;})
.attr('fill', function(d){return d.value > 0 ? color(d.parent.data.name) : 'transparent';})
g.select('.text')
.transition()
.duration(params.transClusterDuration)
.attr('x', function(d) { return d.x0;})
.attr('y', function(d) { return d.y0;})
.attr("height", function(d) { return d.y1 - d.y0;})
.attr("width", function(d) { return d.x1 - d.x0;});
}
function setUpSvgCanvas(input) {
// Set up the svg canvas
var margin = {top: 20, right: 20, bottom: 20, left: 80},
width = input.width - margin.left -margin.right,
height = input.height - margin.top -margin.bottom;
var svg = d3.select('svg')
.attr('width', width + margin.left + margin.right )
.attr('height', height + margin.top +margin.bottom )
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
return {
svg: svg,
margin: margin,
width: width,
height: height
};
}
// these functions will change the initial data to a more d3-hierarchy friendly form
// I prefer torturing the original data with it rather than torturing the code to
// accept the original data
function formatJsonElements(json){
// first we store which is the largest cluster for this element
var clusterNames = d3.keys(json[0]).filter(function(key) {return key !== 'estado' && key !== 'clusterLargest'; });
for(var j=0; j < json.length; j++){
var compare = [];
for(var k=0; k < clusterNames.length; k++){
compare.push(parseFloat(json[j][clusterNames[k]]))
}
var indexLargest = compare.indexOf(d3.max(compare)),
clusterLargest = clusterNames[indexLargest];
json[j]['clusterLargest'] = clusterLargest;
}
// we proceed to create the correct hierarchy based in that
// HEAD -> Largest cluster -> Element -> values on clusters
var formattedData = [];
for(var i=0; i < clusterNames.length; i++){
formattedData.push({'name': clusterNames[i]})
formattedData[i]['children'] = [];
var l = -1
for(var j=0; j < json.length; j++){
if(json[j]['clusterLargest'] == clusterNames[i]){
formattedData[i]['children'].push({'name': json[j].estado})
l += 1;
formattedData[i]['children'][l]['children'] = []
for(var k=0; k < clusterNames.length; k++){
formattedData[i]['children'][l]['children'].push({'name': clusterNames[k], 'value': parseFloat(json[j][clusterNames[k]]), 'id': json[j].estado + clusterNames[k]});
}
}
}
}
return {'children': formattedData};
}
function formatJsonClusters(json){
var formattedData = [];
var clusterNames = d3.keys(json[0]).filter(function(key) {return key !== 'estado'; });
for(var i=0; i < clusterNames.length; i++){
formattedData.push({'name': clusterNames[i]})
}
for(var i=0; i < clusterNames.length; i++){
formattedData[i]['children'] = [];
for(var j=0; j < json.length; j++){
formattedData[i]['children'].push({'name': json[j].estado,
'value': parseFloat(json[j][clusterNames[i]]), 'id': json[j].estado + clusterNames[i]});
}
}
return {'children': formattedData};
}
</script>
https://d3js.org/d3.v4.js