Skip to content

Instantly share code, notes, and snippets.

@danharr
Last active January 17, 2019 13:48
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 danharr/d2df74b87958e81387f0ec91bb737770 to your computer and use it in GitHub Desktop.
Save danharr/d2df74b87958e81387f0ec91bb737770 to your computer and use it in GitHub Desktop.
explaining a dataset
license: mit
<!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