Built with blockbuilder.org
Last active
January 17, 2019 13:48
-
-
Save danharr/d2df74b87958e81387f0ec91bb737770 to your computer and use it in GitHub Desktop.
explaining a dataset
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment