Gooey scatterplot based on the Visual Cinnamon work. Many thanks to Nadieh Bremer for her excelent tutorials.
forked from maritrinez's block: Gooey scatterplot
license: mit |
Gooey scatterplot based on the Visual Cinnamon work. Many thanks to Nadieh Bremer for her excelent tutorials.
forked from maritrinez's block: Gooey scatterplot
//////// -- -- -- SETUP -- -- -- // | |
var delay = 300, | |
duration = 1000; | |
var nRadius = 10, | |
cRadius = 40; | |
var opacityMid = 0.8, | |
opacityLow = 0.3; | |
var margin = { top: 10, right: 30, bottom: 50, left: 50 }, | |
width = 800 - margin.left - margin.right, | |
height = 350 - margin.top - margin.bottom; | |
var svg = d3.select('#chart').append('svg') | |
.attr('width', width + margin.left + margin.right) | |
.attr('height', height + margin.top + margin.bottom) | |
.append('g') | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); | |
var defs = svg.append('defs'); | |
//////// -- -- -- DATA -- -- -- // | |
var n = 60, | |
c = 6; | |
var clusters = new Array(c); | |
var nodes = d3.range(n).map(function() { | |
var i = Math.floor(Math.random() * c), | |
d = { | |
cluster: i, | |
x: d3.randomUniform(10, 90)(), | |
y: d3.randomUniform(10, 90)(), | |
r: nRadius, | |
type: 'node' | |
}; | |
if (!clusters[i]) clusters[i] = { cluster: i, x: d3.randomUniform(25, 75)(), y: d3.randomUniform(25, 75)(), r: cRadius, type: 'cluster' } | |
return d; | |
}); | |
nodes = nodes.concat(clusters); | |
var nestNodes = d3.nest() | |
.key(function(d) { return d.cluster; }) | |
.entries(nodes); | |
//////// -- -- -- SCALES -- -- -- // | |
var xScale = d3.scaleLinear().domain([0, 100]).range([margin.left, width]); | |
var yScale = d3.scaleLinear().domain([0, 100]).range([height, margin.top]); | |
var colorScale = d3.scaleOrdinal(d3.schemeDark2); | |
//////// -- -- -- AXIS -- -- -- // | |
svg.append('g') | |
.attr('class', 'x axis') | |
.attr('transform', 'translate(' + 0 + ',' + height + ')') | |
.call(d3.axisBottom(xScale)); | |
svg.append('g') | |
.attr('class', 'y axis') | |
.attr('transform', 'translate(' + margin.left + ',' + 0 + ')') | |
.call(d3.axisLeft(yScale)); | |
//////// -- -- -- FILTERS -- -- -- // | |
// To avoid the cluster bubbles merging | |
// Create a filter and a group for each cluster | |
defs.selectAll('.filter') | |
.data(nestNodes) | |
.enter() | |
.append('filter') | |
.attr('class', 'filter') | |
.attr('id', function(d) { return 'gooey' + d.key; }); | |
// code taken from @nbremer tutorial | |
// https://www.visualcinnamon.com/2016/06/fun-data-visualizations-svg-gooey-effect.html | |
var onValues = '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -5', | |
offValues = '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 6 -5'; | |
var filters = defs.selectAll('.filter'); | |
filters.append('feGaussianBlur') | |
.attr('in','SourceGraphic') | |
.attr('stdDeviation','10') | |
.attr('color-interpolation-filters','sRGB') | |
.attr('result','blur'); | |
filters.append('feColorMatrix') | |
.attr('class','blurValues') | |
.attr('in','blur') | |
.attr('mode','matrix') | |
.attr('values', offValues) | |
.attr('result','gooey'); | |
filters.append('feBlend') | |
.attr('in','SourceGraphic') | |
.attr('in2','gooey') | |
.attr('operator','atop'); | |
//////// -- -- -- DRAWERS -- -- -- // | |
// Enter the groups and assign the filters | |
svg.selectAll('g.clusters') | |
.data(nestNodes) | |
.enter() | |
.append('g') | |
.attr('class', function(d) { return 'clusters cluster' + d.key; }) | |
.style('filter', function(d) { return 'url(#gooey' + d.key + ')'; }); | |
var gClusters = svg.selectAll('g.clusters'); | |
// Enter the circles (still undefined cx and cy) | |
gClusters.selectAll('circle') | |
.data(function(d) { return d.values; }) | |
.enter() | |
.append('circle') | |
.attr('class', function(d) { return d.type; }) | |
.attr('r', function(d) { return d.r; }) | |
.style('fill', function(d) { return colorScale(d.cluster); }); | |
var renderBubbles = {}; | |
renderBubbles.grouped = function () { | |
// for the first load: place the circles in their original place (ungrouped mode) | |
gClusters.selectAll('circle') | |
.attr('cx', function(d, i) { return xScale(d.x); }) | |
.attr('cy', function(d, i) { return yScale(d.y); }) | |
.style('opacity', opacityMid); | |
gClusters.selectAll('.cluster') | |
.style('opacity', opacityLow); | |
//// init the 'grouped' transition | |
// Activate the color matrix filter (melts the close bubbles) | |
d3.selectAll('.blurValues') | |
.transition() | |
.duration(duration) | |
.delay(function(d, i) { return d.key * delay; }) | |
.attrTween('values', function() { | |
return d3.interpolateString(offValues, onValues); | |
}); | |
// Turn on the cluster circles opacity | |
gClusters.selectAll('.cluster') | |
.transition() | |
.duration(duration) | |
.delay(function(d, i) { return d.cluster * delay; }) | |
.style('opacity', 1); | |
// Move the nodes to their clusters positions | |
gClusters.selectAll('.node') | |
.transition() | |
.duration(duration) | |
.delay(function(d, i) { return d.cluster * delay; }) | |
.ease(d3.easeExpIn) | |
.style('opacity', 1) | |
.attr('r', function(d) { return d.r; }) | |
.attr('cx', function(d, i) { var cluster = findCluster(nodes, d.cluster); return xScale(cluster.x); }) | |
.attr('cy', function(d, i) { var cluster = findCluster(nodes, d.cluster); return yScale(cluster.y); }); | |
} | |
renderBubbles.ungrouped = function () { | |
// for the first load: place the circles in their original place (grouped mode) | |
gClusters.selectAll('circle') | |
.attr('cx', function(d, i) { var cluster = findCluster(nodes, d.cluster); return xScale(cluster.x); }) | |
.attr('cy', function(d, i) { var cluster = findCluster(nodes, d.cluster); return yScale(cluster.y); }) | |
.style('opacity', 1); | |
//// init the 'grouped' transition | |
// Move the nodes to their original positions | |
gClusters.selectAll('.node') | |
.transition() | |
.duration(duration) | |
.delay(function(d, i) { return d.cluster * delay; }) | |
.ease(d3.easeExpOut) | |
.attr('r', function(d) { return d.r - 2; }) | |
.style('opacity', opacityMid) | |
.attr('cx', function(d, i) { return xScale(d.x); }) | |
.attr('cy', function(d, i) { return yScale(d.y); }); | |
// Turn off the cluster circles opacity | |
gClusters.selectAll('.cluster') | |
.transition() | |
.duration(duration) | |
.delay(function(d, i) { return d.cluster * delay; }) | |
.style('opacity', opacityLow); | |
// De-activate the color matrix filter (melts the close bubbles) | |
d3.selectAll('.blurValues') | |
.transition() | |
.duration(duration) | |
.delay(function(d, i) { return d.key * delay; }) | |
.attrTween('values', function() { | |
return d3.interpolateString(onValues, offValues); | |
}); | |
} | |
// Render thr bubbles | |
renderBubbles.grouped() | |
//////// -- -- -- FUNCTIONS -- -- -- // | |
function findCluster(data, cluster) { | |
return data.find(function(d) { return d.type == 'cluster' && d.cluster == cluster; }); | |
} | |
//////// -- -- -- INTERACTION -- -- -- // | |
var buttons = d3.selectAll('button'); | |
d3.selectAll('button') | |
.on('click', function() { | |
if (d3.event.target.classList.contains('selected')) return; | |
renderBubbles[d3.event.target.value](); | |
buttons.each(function() { this.classList.toggle('selected') }); | |
}); |
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<title>Gooey scatterplot</title> | |
<script src="https://d3js.org/d3.v4.js"></script> | |
<script src="https://d3js.org/d3-scale-chromatic.v0.3.min.js"></script> | |
<link rel="stylesheet" type="text/css" href="main.css"> | |
<body> | |
<div id='buttons'> | |
<button type="submit" class= "button selected" value="grouped">grouped</button> | |
<button type="submit" class= "button" value="ungrouped">ungrouped</button> | |
</div> | |
<div id="chart"></div> | |
<script src="gooey_scatterplot.js"></script> | |
</body> |
#buttons { | |
margin: 40px 110px; | |
letter-spacing: -0.31em; | |
} | |
button { | |
/*margin: 0 20px;*/ | |
font-size: 110%; | |
padding: .5em 1em; | |
color: rgba(0,0,0,.8); | |
border: 1px solid #999; | |
border: transparent; | |
background-color: #E6E6E6; | |
cursor: pointer; | |
border-radius: 2px; | |
font-weight: 100; | |
letter-spacing: 0.01em; | |
} | |
button:hover { | |
background-color: #c9c9c9; | |
} | |
button:focus { | |
outline-width: 0; | |
} | |
.button.selected { | |
background-color: #4A4A4A; | |
color: #ffffff; | |
} | |
.axis text { | |
fill: #a5a5a5; | |
} | |
.axis path, .axis line { | |
stroke: #a5a5a5; | |
} |