D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
WeikerWT
Full window
Github gist
Top of funnel 6.26.18
Built with
blockbuilder.org
<!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: "\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