|
<!DOCTYPE html> |
|
<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: "\25BC"; |
|
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", 1e6); |
|
|
|
// setting up colors |
|
var color = params.color = d3.scaleOrdinal() |
|
.range(['#3E4095', '#FFCC29', '#00A859']); |
|
|
|
// 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> |