Skip to content

Instantly share code, notes, and snippets.

@ricardo-marino
Last active February 9, 2017 16:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ricardo-marino/d016daad162a8a84f19aee927dd01373 to your computer and use it in GitHub Desktop.
Save ricardo-marino/d016daad162a8a84f19aee927dd01373 to your computer and use it in GitHub Desktop.
Zoomable and versatile treemap
height: 750

Zoomable and versatile treemap

So I wanted to explore the new d3.hierarchy properties, and I learned a lot!

I think I finally understood the enter and update dynamics using data binded to groups inherited by their children. As it turns out, using var g = svg.selectAll('g').data(data).enter().append('g') creates groups g binded to data data. Their children, created by a simple var rect = g.append('rect') or var forObj = g.append('foreignobject'), inherit this data. Updating the data in groups g by using g.data(newData) does NOT transmit newData to rect nor forObj, which was a surprise! The .append method is responsible for this transmition in the enter stage, and in the update stage the counterpart to .append is .select! This means that after updating g.data(newData) I must call g.select('rect'), which correctly updates data attached to the rectangle in the group.

I wanted to create a visualization that could shift between view by "elements" (in this case, the Brazilian states) and by "clusters" (in this case, the economy sectors). d3.treemap is very handy to create x and y positions of structured data, so the first task was to refold data in a parent-children structure to please d3.hierarchy. To create different visualization forms, I created two functions that would fold the same data in different hierarchy structures, and the rest of the code is to shift between the two. Using d3 object permanence in the data attribution I was able to associate rectangles in one set of data with rectangles in another set of data, to create animations correctly.

Zooming was a personal challenge. I noticed that most zoomable treemaps use a rescaling of x and y axis to achive the zooming effect, so I tried something different and a little fancier. The result looks nice, but there are some glitches with names of small states.

When many variables are presented, it is hard to find one specific, so I added the dropdown menu to make the chosen variable blink.

To-do list:

  • A legend.
  • Fix captioning when zooming, which means fix precarious implementation of the zoom.
  • Add Goiás, that somehow I forgot to include in the data.

Data: Economy sectors (1: Agriculture, 2: Industry, 3: Services) of the Brazilian states. Source: deepask.com

// d3.tip
// Copyright (c) 2013 Justin Palmer
// ES6 / D3 v4 Adaption Copyright (c) 2016 Constantin Gavrilete
// Removal of ES6 for D3 v4 Adaption Copyright (c) 2016 David Gotz
//
// Tooltips for d3.js SVG visualizations
d3.functor = function functor(v) {
return typeof v === "function" ? v : function() {
return v;
};
};
d3.tip = function() {
var direction = d3_tip_direction,
offset = d3_tip_offset,
html = d3_tip_html,
node = initNode(),
svg = null,
point = null,
target = null
function tip(vis) {
svg = getSVGNode(vis)
point = svg.createSVGPoint()
document.body.appendChild(node)
}
// Public - show the tooltip on the screen
//
// Returns a tip
tip.show = function() {
var args = Array.prototype.slice.call(arguments)
if(args[args.length - 1] instanceof SVGElement) target = args.pop()
var content = html.apply(this, args),
poffset = offset.apply(this, args),
dir = direction.apply(this, args),
nodel = getNodeEl(),
i = directions.length,
coords,
scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
nodel.html(content)
.style('position', 'absolute')
.style('opacity', 1)
.style('pointer-events', 'all')
while(i--) nodel.classed(directions[i], false)
coords = direction_callbacks[dir].apply(this)
nodel.classed(dir, true)
.style('top', (coords.top + poffset[0]) + scrollTop + 'px')
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px')
return tip
}
// Public - hide the tooltip
//
// Returns a tip
tip.hide = function() {
var nodel = getNodeEl()
nodel
.style('opacity', 0)
.style('pointer-events', 'none')
return tip
}
// Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value.
//
// n - name of the attribute
// v - value of the attribute
//
// Returns tip or attribute value
tip.attr = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().attr(n)
} else {
var args = Array.prototype.slice.call(arguments)
d3.selection.prototype.attr.apply(getNodeEl(), args)
}
return tip
}
// Public: Proxy style calls to the d3 tip container. Sets or gets a style value.
//
// n - name of the property
// v - value of the property
//
// Returns tip or style property value
tip.style = function(n, v) {
// debugger;
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().style(n)
} else {
var args = Array.prototype.slice.call(arguments);
if (args.length === 1) {
var styles = args[0];
Object.keys(styles).forEach(function(key) {
return d3.selection.prototype.style.apply(getNodeEl(), [key, styles[key]]);
});
}
}
return tip
}
// Public: Set or get the direction of the tooltip
//
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
// sw(southwest), ne(northeast) or se(southeast)
//
// Returns tip or direction
tip.direction = function(v) {
if (!arguments.length) return direction
direction = v == null ? v : d3.functor(v)
return tip
}
// Public: Sets or gets the offset of the tip
//
// v - Array of [x, y] offset
//
// Returns offset or
tip.offset = function(v) {
if (!arguments.length) return offset
offset = v == null ? v : d3.functor(v)
return tip
}
// Public: sets or gets the html value of the tooltip
//
// v - String value of the tip
//
// Returns html value or tip
tip.html = function(v) {
if (!arguments.length) return html
html = v == null ? v : d3.functor(v)
return tip
}
// Public: destroys the tooltip and removes it from the DOM
//
// Returns a tip
tip.destroy = function() {
if(node) {
getNodeEl().remove();
node = null;
}
return tip;
}
function d3_tip_direction() { return 'n' }
function d3_tip_offset() { return [0, 0] }
function d3_tip_html() { return ' ' }
var direction_callbacks = {
n: direction_n,
s: direction_s,
e: direction_e,
w: direction_w,
nw: direction_nw,
ne: direction_ne,
sw: direction_sw,
se: direction_se
};
var directions = Object.keys(direction_callbacks);
function direction_n() {
var bbox = getScreenBBox()
return {
top: bbox.n.y - node.offsetHeight,
left: bbox.n.x - node.offsetWidth / 2
}
}
function direction_s() {
var bbox = getScreenBBox()
return {
top: bbox.s.y,
left: bbox.s.x - node.offsetWidth / 2
}
}
function direction_e() {
var bbox = getScreenBBox()
return {
top: bbox.e.y - node.offsetHeight / 2,
left: bbox.e.x
}
}
function direction_w() {
var bbox = getScreenBBox()
return {
top: bbox.w.y - node.offsetHeight / 2,
left: bbox.w.x - node.offsetWidth
}
}
function direction_nw() {
var bbox = getScreenBBox()
return {
top: bbox.nw.y - node.offsetHeight,
left: bbox.nw.x - node.offsetWidth
}
}
function direction_ne() {
var bbox = getScreenBBox()
return {
top: bbox.ne.y - node.offsetHeight,
left: bbox.ne.x
}
}
function direction_sw() {
var bbox = getScreenBBox()
return {
top: bbox.sw.y,
left: bbox.sw.x - node.offsetWidth
}
}
function direction_se() {
var bbox = getScreenBBox()
return {
top: bbox.se.y,
left: bbox.e.x
}
}
function initNode() {
var node = d3.select(document.createElement('div'))
node
.style('position', 'absolute')
.style('top', 0)
.style('opacity', 0)
.style('pointer-events', 'none')
.style('box-sizing', 'border-box')
return node.node()
}
function getSVGNode(el) {
el = el.node()
if(el.tagName.toLowerCase() === 'svg')
return el
return el.ownerSVGElement
}
function getNodeEl() {
if(node === null) {
node = initNode();
// re-add node to DOM
document.body.appendChild(node);
};
return d3.select(node);
}
// Private - gets the screen coordinates of a shape
//
// Given a shape on the screen, will return an SVGPoint for the directions
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
// sw(southwest).
//
// +-+-+
// | |
// + +
// | |
// +-+-+
//
// Returns an Object {n, s, e, w, nw, sw, ne, se}
function getScreenBBox() {
var targetel = target || d3.event.target;
while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) {
targetel = targetel.parentNode;
}
var bbox = {},
matrix = targetel.getScreenCTM(),
tbbox = targetel.getBBox(),
width = tbbox.width,
height = tbbox.height,
x = tbbox.x,
y = tbbox.y
point.x = x
point.y = y
bbox.nw = point.matrixTransform(matrix)
point.x += width
bbox.ne = point.matrixTransform(matrix)
point.y += height
bbox.se = point.matrixTransform(matrix)
point.x -= width
bbox.sw = point.matrixTransform(matrix)
point.y -= height / 2
bbox.w = point.matrixTransform(matrix)
point.x += width
bbox.e = point.matrixTransform(matrix)
point.x -= width / 2
point.y -= height / 2
bbox.n = point.matrixTransform(matrix)
point.y += height
bbox.s = point.matrixTransform(matrix)
return bbox
}
return tip
};
estado 1 2 3
SP 37163920 538876840 1282155240
RJ 3091021.33333333 249721986.666667 418182649.333333
MG 82661440 123992160 309980400
RS 28507318.6813187 68810769.2307692 144502615.384615
PR 41603665.3386454 104009163.346614 202471171.314741
SC 18539082.8025478 86515719.7452229 137498197.452229
BA 23064790 93378810 107486400
DF 0 23786987.9518072 173645012.048193
PE 6672817.20430108 43373311.827957 105096870.967742
ES 4711609.75609756 61250926.8292683 62821463.4146342
CE 8633835.61643836 32808575.3424658 84611589.0410959
PA 20764166.6666667 51218277.7777778 52602555.5555556
MT 28034307.6923077 21804461.5384615 51396230.7692308
AM 7738303.57142857 38691517.8571429 40239178.5714286
MS 17369000 22106000 39475000
MA 14292612 13754718 48794670
RN 1637060.60606061 18007666.6666667 34378272.7272727
PB 4072000 14252000 34612000
AL 6556000 9834000 24585000
PI 4191444.44444444 6287166.66666667 27244388.8888889
SE 3258434.7826087 13033739.1304348 21179826.0869565
RO 5373315.78947368 8955526.31578947 19702157.8947368
TO 5237800 6983733.33333333 13967466.6666667
AC 2243166.66666667 2243166.66666667 8972666.66666667
AP 0 3828571.42857143 9571428.57142857
RR 0 2436000 7308000
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment