D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
danharr
Full window
Github gist
explaining a dataset
Built with
blockbuilder.org
<!DOCTYPE html> <head> <meta charset="utf-8"> <script src="https://d3js.org/d3.v4.min.js"></script> <style> @import url('https://fonts.googleapis.com/css?family=Roboto|Material+Icons'); .customBIS.active { background-color: black; color:white; } .customBIS.button { margin:1px; } .customBIS.year { font-size:14px; font-family:'Open Sans', sans-serif; } .customBIS#sub-heading { font-family:'Open Sans', sans-serif; } .customBIS#heading { font-family: 'Roboto', sans-serif; } .customBIS.legend { font-family:'Open Sans', sans-serif; } .area-label { font-family: sans-serif; fill-opacity: 0.5; } .custom-visCS2-text { font-family:arial; } .button, a.button { display: inline-block; overflow: visible; font-size: 1em; line-height: 1.4; color: #000; text-decoration: none; padding: 1px 6px; border: 1px solid #bbb; cursor: pointer; background: #eee; white-space: nowrap; font-family: Arial, Helvetica, sans-serif; -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; } </style> </head> <body> <script> var width = 800; var height = 795; rawData = [{"headers":[{"name":"Argentina","tname":"Country"},{"name":"D","tname":"Content Type Level 2"},{"name":"N","tname":"Content Type Level 3"}],"values":[{"v":"8.6K","rv":100.41,"name":"Spend (EUR)"}],"colorInfo":[]}, {"headers":[{"name":"Argentina","tname":"Country"},{"name":"D","tname":"Content Type Level 2"},{"name":"G","tname":"Content Type Level 3"}],"values":[{"v":"1.8M","rv":100.32,"name":"Spend (EUR)"}],"colorInfo":[]}, {"headers":[{"name":"Argentina","tname":"Country"},{"name":"D","tname":"Content Type Level 2"},{"name":"A","tname":"Content Type Level 3"}],"values":[{"v":"2.5M","rv":100.68,"name":"Spend (EUR)"}],"colorInfo":[]}, {"headers":[{"name":"Argentina","tname":"Country"},{"name":"D","tname":"Content Type Level 2"},{"name":"V","tname":"Content Type Level 3"}],"values":[{"v":"180","rv":180.24,"name":"Spend (EUR)"}],"colorInfo":[]}, {"headers":[{"name":"Argentina","tname":"Country"},{"name":"D","tname":"Content Type Level 2"},{"name":"S","tname":"Content Type Level 3"}],"values":[{"v":"1.4M","rv":100.79,"name":"Spend (EUR)"}],"colorInfo":[]} ]; var VIZ = d3.select('body'); VIZ.append('p').html('How is spend distributed?').style('font-size', '22px').attr('id','heading') .attr('class','customBIS'); var digital_spend = d3.sum(rawData, function(d) { if (d.headers[1].name === "Physical") { return d.values[0].rv; } }); var total_spend = d3.sum(rawData, function(d) { return d.values[0].rv; }); var perc_digital = Math.floor(digital_spend / total_spend * 100); var perc_physical = 100 - perc_digital; var buttons = VIZ.append('div').attr('id', 'toolbar'); buttons.append('a').attr('href', '#').attr('id', 'all').attr('class', 'customBIS button active').html('All Spend').style('font-size', '14px'); buttons.append('a').attr('href', '#').attr('id', 'split').attr('class', 'customBIS button').html('P / D split').style('font-size', '14px'); buttons.append('a').attr('href', '#').attr('id', 'content').attr('class', 'customBIS button').html('Content Type').style('font-size', '14px'); buttons.append('a').attr('href', '#').attr('id', 'country').attr('class', 'customBIS button').html('Store').style('font-size', '14px'); //I want it fixed size var svg = VIZ.append('svg').attr('width', '100%').attr('height', height); var cols = Math.floor(width / 200); var center = { x: width / 2, y: height / 2 }; var ContentType2_xy = { "P": { x: width / 4, y: height / 2 }, "D": { x: 3 * width / 4, y: height / 2 } }; // X locations of the year titles. var ContentType2 = { "P": width / 4, "D": 3 * width / 4 }; // @v4 strength to apply to the position forces var forceStrength = 0.09; // These will be set in create_nodes and create_vis //var svg = null; var bubbles = null; var nodes = []; function charge(d) { return -Math.pow(d.values[0].radius, 2.0) * forceStrength; } var simulation = d3.forceSimulation() .velocityDecay(0.2) .force('x', d3.forceX().strength(forceStrength).x(center.x)) .force('y', d3.forceY().strength(forceStrength).y(center.y)) .force('charge', d3.forceManyBody().strength(charge)) .on('tick', ticked); // @v4 Force starts up automatically, // which we don't want as there aren't any nodes yet. simulation.stop(); const color = d3.scaleOrdinal(d3.schemeCategory10); //change rawData so it has radius var maxvalue = d3.max(rawData, function(d) { return d.values[0].rv; }); var radiusScale = d3.scalePow() .exponent(0.5) .range([2, 50]) .domain([0, maxvalue]); //order by country alphabet var countries = d3.map(rawData, function(d) { return d.headers[0].name; }).keys(); var country_pos = {}; var country_labels = []; countries.forEach(function(d, i) { country_pos[d] = { "pos": i, "row": (Math.floor(i / cols)), "col": (i % cols) }; country_labels.push({ "name": d, "pos": i, "row": (Math.floor(i / cols)), "col": (i % cols) }); }); rawData.forEach(function(d, i) { rawData[i].values[0].radius = radiusScale(rawData[i].values[0].rv); rawData[i].values[0].id = i; } ); //get list of content types so we can plot them in a nice circle var content_types = d3.map(rawData, function(d) { return d.headers[2].name; }).keys(); var num_ct = content_types.length; var circle_coords = []; var content_labels = []; content_types.forEach(function(d, i) { var x = (width / 2) + (height / 4) * Math.cos(2 * Math.PI * i / num_ct); var y = (height / 2.5) + (height / 4) * Math.sin(2 * Math.PI * i / num_ct); circle_coords[d] = { "x": x, "y": y }; content_labels.push({ "name": d, "x": x, "y": y, "sales": d3.sum(rawData, function(e) { if (e.headers[2].name === d) { return e.values[0].rv; } }) }); }); nodes = rawData; // Bind nodes data to what will become DOM elements to represent them. bubbles2 = svg.append("clipPath").attr('id','bubble-clip').selectAll('.bubble') .data(nodes, function(d) { return d.values[0].id; }); // Bind nodes data to what will become DOM elements to represent them. bubbles = svg.selectAll('.bubble') .data(nodes, function(d) { return d.values[0].id; }); var bubblesE = bubbles.enter().append('circle') .classed('bubble', true) .attr('r', 0) .attr('fill', function(d) { return color(d.headers[1].name); }) .attr('stroke', function(d) { return d3.rgb(color(d.headers[1].name)).darker(); }) .attr('stroke-width', 2) .on('mouseover', showDetail) .on('mouseout', hideDetail); var bubblesE2 = bubbles2.enter().append('circle') .classed('bubble', true) .attr('r', 0) .attr('stroke-width', 2); // @v4 Merge the original empty selection and the enter selection bubbles = bubbles.merge(bubblesE); bubbles2 = bubbles2.merge(bubblesE2); // Fancy transition to make bubbles appear, ending with the // correct radius bubbles.transition() .duration(2000) .attr('r', function(d) { return d.values[0].radius; }); bubbles2.transition() .duration(2000) .attr('r', function(d) { return d.values[0].radius; }); // Set the simulation's nodes to our newly created nodes array. // @v4 Once we set the nodes, the simulation will start running automatically! simulation.nodes(nodes); function ticked() { bubbles .attr('cx', function(d) { return d.x; }) .attr('cy', function(d) { return d.y; }); bubbles2 .attr('cx', function(d) { return d.x; }) .attr('cy', function(d) { return d.y; }); } /* * Provides a x value for each node to be used with the split by year * x force. */ function nodeYearPos(d) { return ContentType2_xy[d.headers[1].name].x; } function nodeCtyPosX(d) { return (country_pos[d.headers[0].name].col * 200) + 80; } function nodeCtyPosY(d) { return (country_pos[d.headers[0].name].row * 200) + 180; } function nodeContentPosX(d) { return (circle_coords[d.headers[2].name].x); } function nodeContentPosY(d) { return (circle_coords[d.headers[2].name].y); } /* * Sets visualization in "single group mode". * The year labels are hidden and the force layout * tick function is set to move all nodes to the * center of the visualization. */ function groupBubbles() { hideYearTitles(); hideCountryTitles(); hideContentTitles(); // @v4 Reset the 'x' force to draw the bubbles to the center. simulation.force('x', d3.forceX().strength(forceStrength).x(center.x)); simulation.force('y', d3.forceY().strength(forceStrength).y(center.y)); // @v4 We can reset the alpha value and restart the simulation simulation.alpha(1).restart(); } function splitBubbles() { showYearTitles(); hideCountryTitles(); hideContentTitles(); simulation.force('y', d3.forceY().strength(forceStrength).y(center.y)); // @v4 Reset the 'x' force to draw the bubbles to their year centers simulation.force('x', d3.forceX().strength(forceStrength).x(nodeYearPos)); // @v4 We can reset the alpha value and restart the simulation simulation.alpha(1).restart(); } function countryBubbles() { hideYearTitles(); hideContentTitles(); showCountryTitles(); // @v4 Reset the 'x' force to draw the bubbles to their year centers simulation.force('x', d3.forceX().strength(forceStrength).x(nodeCtyPosX)).force('y', d3.forceY().strength(forceStrength).y(nodeCtyPosY)); // @v4 We can reset the alpha value and restart the simulation simulation.alpha(1).restart(); } function contentBubbles() { hideYearTitles(); hideCountryTitles(); showContentTitles(); // @v4 Reset the 'x' force to draw the bubbles to their year centers simulation.force('x', d3.forceX().strength(forceStrength).x(nodeContentPosX)).force('y', d3.forceY().strength(forceStrength).y(nodeContentPosY)); // @v4 We can reset the alpha value and restart the simulation simulation.alpha(1).restart(); } function hideYearTitles() { svg.selectAll('.customBIS.year').remove(); } function showYearTitles() { // Another way to do this would be to create // the year texts once and then just hide them. var yearsData = d3.keys(ContentType2); var years = svg.selectAll('.customBIS.year') .data(yearsData); years.enter().append('text') .attr('class', 'customBIS year') .attr('x', function(d) { return ContentType2[d]; }) .attr('y', 100) .attr('text-anchor', 'middle') .text(function(d) { if (d === "P") { return d + ' (' + perc_physical + '%)'; } else { return d + ' (' + perc_digital + '%)'; } }); } function showCountryTitles() { var countries_text0 = svg.selectAll('.labels0') .data(country_labels); countries_text0.enter().append('text') .attr('class', 'labels0') .attr('x', function(d) { return (d.col * 200) + 80; }) .attr('y', function(d) { return (d.row * 200) + 180; }) .attr('text-anchor', 'middle') .text(function(d) { return d.name; }) .style('font-size','22px') .style('font-family','Roboto') .style('fill','black'); var countries_text = svg.selectAll('.labels') .data(country_labels); countries_text.enter().append('text') .attr('class', 'labels') .attr('x', function(d) { return (d.col * 200) + 80; }) .attr('y', function(d) { return (d.row * 200) + 180; }) .attr('text-anchor', 'middle') .attr("clip-path", "url(#bubble-clip)") .text(function(d) { return d.name; }) .style('font-size','22px') .style('font-family','Roboto') .style('fill','white'); } function hideCountryTitles() { svg.selectAll('.labels').remove(); svg.selectAll('.labels0').remove(); } function showContentTitles() { var content_text0 = svg.selectAll('.labels0') .data(content_labels); content_text0.enter().append('text') .attr('class', 'labels0') .attr('x', function(d) { return (d.x); }) .attr('y', function(d) { return (d.y); }) .attr('text-anchor', 'middle') .text(function(d) { return d.name; }) .style('font-size','22px') .style('font-family','Roboto') .style('fill','black'); var content_text = svg.selectAll('.labels') .data(content_labels); content_text.enter().append('text') .attr('class', 'labels') .attr('x', function(d) { return (d.x); }) .attr('y', function(d) { return (d.y); }) .attr('text-anchor', 'middle') .text(function(d) { return d.name; }) .attr("clip-path", "url(#bubble-clip)") .style('font-size','22px') .style('font-family','Roboto') .style('fill','white'); } function hideContentTitles() { svg.selectAll('.labels').remove(); svg.selectAll('.labels0').remove(); } /* * Function called on mouseover to display the * details of a bubble in the tooltip. */ function showDetail(d) { // change outline to indicate hover state. //d3.select(this).attr('stroke', 'black'); var _x = d3.select(this).attr('cx'); var _y = d3.select(this).attr('cy'); var g = svg.append('g').attr('transform', 'translate(' + _x + ',' + _y + ')').attr('id', 'tooltip'); g.append('rect').attr('width', 150).attr('height', 80).attr('x', 0).attr('y', 0).style('fill', 'white').style('stroke', 'black'); g.append('text').text(d.headers[0].tname + ': ' + d.headers[0].name).attr('x', 20).attr('y', 20); g.append('text').text(d.headers[1].name + ': ' + d.headers[2].name).attr('x', 20).attr('y', 40); g.append('text').text('Sales : €' + d.values[0].v).attr('x', 20).attr('y', 60); } /* * Hides tooltip */ function hideDetail(d) { d3.select('#tooltip').remove(); } /* * Sets up the layout buttons to allow for toggling between view modes. */ function setupButtons() { d3.select('#toolbar') .selectAll('.button') .on('click', function() { // Remove active class from all buttons d3.selectAll('.button').classed('active', false); // Find the button just clicked var button = d3.select(this); // Set it as the active button button.classed('active', true); // Get the id of the button var buttonId = button.attr('id'); chapter(buttonId); }); } setupButtons(); groupBubbles(); function chapter(x) { if (x === 'all') { legend(0); groupBubbles(); bubbles.transition().duration(2000).attr('fill', function(d) { return color(d.headers[1].name); }).attr('stroke', function(d) { return d3.rgb(color(d.headers[1].name)).darker(); }); } else if (x === 'split') { legend(1); splitBubbles(); bubbles.transition().duration(2000).attr('fill', function(d) { return color(d.headers[2].name); }).attr('stroke', function(d) { return d3.rgb(color(d.headers[2].name)).darker(); }); } else if (x === 'country') { countryBubbles(); } else if (x === 'content') { legend(0); contentBubbles(); bubbles.transition().duration(2000).attr('fill', function(d) { return color(d.headers[1].name); }).attr('stroke', function(d) { return d3.rgb(color(d.headers[1].name)).darker(); }); } } function legend(x) { if (x === 0) { d3.selectAll('.customBIS.legend').remove(); var legend_g = svg.selectAll('.legend').data(["P", "D"]) .enter().append('g') .attr('transform', function(d, i) { return 'translate(' + i * 120 + ',20)'; }).attr('class', 'customBIS legend'); legend_g.append('rect') .attr('x', 0) .attr('y', 0).attr('width', 120).attr('height', 20) .attr('fill', function(d) { return color(d); }); legend_g.append('text') .attr('x', 60) .attr('y', 15) .style('text-anchor', 'middle') .style('fill', 'white') .text(function(d) { return d; }); } else if (x === 1) { d3.selectAll('.customBIS.legend').remove(); var legend_g = svg.selectAll('.legend').data(["N", "D", "P", "G", "A", "V", "S"]) .enter().append('g') .attr('transform', function(d, i) { return 'translate(' + i * 120 + ',20)'; }).attr('class', 'customBIS legend'); legend_g.append('rect') .attr('x', 0) .attr('y', 0).attr('width', 120).attr('height', 20) .attr('fill', function(d) { return color(d); }); legend_g.append('text') .attr('x', 60) .attr('y', 15) .style('text-anchor', 'middle') .style('fill', 'white') .text(function(d) { return d; }); } } legend(0); </script> </body>
https://d3js.org/d3.v4.min.js