|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js"></script> |
|
<script src="https://npmcdn.com/babel-core@5.8.34/browser.min.js"></script> |
|
<script type="text/javascript" src="http://gka.github.io/chroma.js/vendor/chroma-js/chroma.min.js"></script> |
|
<style> |
|
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; |
|
} |
|
svg { |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
text { |
|
font-family: courier; |
|
} |
|
|
|
/* blend options taken from visual cinnamon tutorial: http://www.visualcinnamon.com/2016/05/beautiful-color-blending-svg-d3.html */ |
|
/*Set isolate on the group element*/ |
|
svg {isolation: isolate;} |
|
/*Set blend mode on SVG element: e.g. screen, multiply*/ |
|
/* path { mix-blend-mode: multiply; } */ |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<svg></svg> |
|
<script type="text/babel"> |
|
var width = 800; |
|
var height = 550; |
|
|
|
var yellow = chroma('#FFFEDE').saturate(2.5); |
|
var navy = '#000e20'; |
|
var blue = chroma(navy).brighten(.5).saturate(); |
|
var white = chroma(navy).brighten(4); |
|
var colors = chroma.scale([blue, navy]) |
|
.mode('hsl') |
|
.colors(6); |
|
|
|
d3.csv('data.csv', (data) => { |
|
var attendees = _.map(data, d => { |
|
return { |
|
name: d['First Name'], |
|
first: d.first.split(', '), |
|
favorite: d.favorite.split(', '), |
|
version: parseInt(d.version.replace('v', '')), |
|
}; |
|
}); |
|
|
|
// create links between the different types |
|
// of firsts, favorites, and versions |
|
var firsts = _.chain(attendees) |
|
.map('first').flatten() |
|
.countBy().toPairs() |
|
.sortBy(d => -d[1]) |
|
.filter(d => d[0] !== 'N/A') |
|
.map(0).value(); |
|
|
|
var favorites = _.chain(attendees) |
|
.map('favorite').flatten() |
|
.countBy().toPairs() |
|
.sortBy(d => -d[1]) |
|
.filter(d => d[0]) |
|
.map(0).value(); |
|
|
|
var versions = _.chain(attendees) |
|
.map('version').flatten() |
|
.countBy().toPairs() |
|
// .sortBy(d => -d[1]) |
|
.filter(d => d[1] > 1) |
|
.map(0).value(); |
|
|
|
/************************************** |
|
** calculate nodes and links |
|
**************************************/ |
|
|
|
// width should be attendee's first experience with dataviz |
|
var startWidth = width * .1; |
|
var perWidth = width / firsts.length * .8; |
|
var xScale = d3.scaleOrdinal().domain(firsts) |
|
.range(_.times(firsts.length, (i) => |
|
// start at the middle and wrap the points around |
|
((i + firsts.length * .25) % firsts.length) * |
|
perWidth + startWidth)); |
|
// height is their current fav d3 API |
|
var startHeight = height * .2; |
|
var perHeight = height / favorites.length * .6; |
|
var yScale = d3.scaleOrdinal().domain(favorites) |
|
.range(_.times(favorites.length, (i) => |
|
// start at the middle and wrap the points around |
|
((i + favorites.length * .25) % favorites.length) * |
|
perHeight + startHeight)); |
|
|
|
// YAH LOOPS |
|
var points = []; |
|
_.each(attendees, attendee => { |
|
_.each(attendee.favorite, favorite => { |
|
var x = xScale(favorite); |
|
_.each(attendee.first, first => { |
|
var y = yScale(first); |
|
points.push({ |
|
name: attendee.name, |
|
favorite, |
|
first, |
|
version: attendee.version, |
|
focusX: x, |
|
focusY: y, |
|
}); |
|
}); |
|
}); |
|
}); |
|
// also loop through the favorites to |
|
// make an actual circle |
|
var outside = []; |
|
var times = 8; |
|
var perWidth = width / times; |
|
var perHeight = height / times; |
|
_.times((times + 1), i => { |
|
outside.push({ |
|
fx: 0, |
|
fy: i * perHeight, |
|
}); |
|
outside.push({ |
|
fx: width, |
|
fy: i * perHeight, |
|
}); |
|
}); |
|
_.times((times + 1), i => { |
|
outside.push({ |
|
fx: i * perWidth, |
|
fy: 0, |
|
}); |
|
outside.push({ |
|
fx: i * perWidth, |
|
fy: height, |
|
}); |
|
}); |
|
|
|
var nodes = _.union(points, outside); |
|
var simulation = d3.forceSimulation(nodes) |
|
.force('charge', d3.forceManyBody().strength(-25)) |
|
.force("collide", d3.forceCollide(2)) |
|
.force("x", d3.forceX().x(d => d.focusX)) |
|
.force("y", d3.forceY().y(d => d.focusY)) |
|
.on("tick", ticked); |
|
var voronoi = d3.voronoi() |
|
.x(d => d.x) |
|
.y(d => d.y); |
|
|
|
/************************************** |
|
** draw the circles and links |
|
**************************************/ |
|
var svg = d3.select('svg') |
|
.append('g').attr('transform', 'translate(20, 20)'); |
|
|
|
// motion blur taken from http://www.visualcinnamon.com/2016/05/real-life-motion-effects-d3-visualization.html |
|
var defs = svg.append("defs"); |
|
defs.append("filter") |
|
.attr("id", "motionFilter") |
|
.attr('width', '300%') |
|
.attr('height', '300%') |
|
.attr('x', '-100%') |
|
.attr('y', '-100%') |
|
.append("feGaussianBlur") |
|
.attr("in", "SourceGraphic") |
|
.attr("stdDeviation", "2"); |
|
|
|
var pathContainer = svg.append('g'); |
|
var paths, triangles; |
|
|
|
var circles = svg.selectAll('g') |
|
.data(points) |
|
.enter().append('g'); |
|
// the actual star |
|
circles.append('circle') |
|
.attr('fill', yellow) |
|
.attr('r', d => (4 / d.version - 1) * 1.5); |
|
// the blur |
|
circles.append('circle') |
|
.attr('fill', yellow) |
|
.attr('r', d => (4 / d.version - 1) * 4) |
|
.attr('opacity', .25) |
|
.style("filter", "url(#motionFilter)"); |
|
|
|
// add in text |
|
var fontSize = 18; |
|
var fontPadding = 5; |
|
var text = svg.append('g') |
|
.attr('opacity', .75); |
|
text.append('text') |
|
.attr('text-anchor', 'end') |
|
.attr('dy', '.35em') |
|
.attr('y', height - fontSize - fontPadding) |
|
.attr('x', width - 3 * fontPadding) |
|
.attr('fill', white) |
|
.attr('font-size', fontSize) |
|
.text('d3.unconf 2016'); |
|
|
|
function ticked() { |
|
circles.attr('transform', (d) => |
|
'translate(' + [d.x, d.y] + ')'); |
|
|
|
triangles = voronoi.triangles(nodes); |
|
|
|
// now create the triangles |
|
paths = pathContainer.selectAll('path') |
|
.data(triangles); |
|
|
|
paths.exit().remove(); |
|
paths.enter().append('path') |
|
.merge(paths) |
|
.attr('d', d => { |
|
return 'M' + _.map(d, function(point) { |
|
return point.x + ',' + point.y; |
|
}).join(' L') + 'Z'; |
|
}).attr('fill', (d, i) => colors[i % 6]) |
|
.attr('stroke', (d, i) => colors[i % 6]) |
|
.attr('opacity', .85); |
|
} |
|
|
|
}); |
|
</script> |
|
</body> |