Skip to content

Instantly share code, notes, and snippets.

@jonsadka
Last active March 16, 2018 14:55
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 jonsadka/ad1a3698615485a310f9228ed7ea93cd to your computer and use it in GitHub Desktop.
Save jonsadka/ad1a3698615485a310f9228ed7ea93cd to your computer and use it in GitHub Desktop.
Vertical Force Beeswarm with Voronoi
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body {
background-color: #1D1D1D;
bottom: 0;
font-family: 'Helvetica', 'Arial', sans-serif;
left: 0;
margin: 0;
position: fixed;
right: 0;
top: 0;
}
.value {
stroke: #C61661;
}
.voronoi {
stroke: #F4AFC9;
fill: none;
}
.axis .domain,
.axis .tick line {
stroke: #FFF;
}
.axis .tick text {
fill: #FFF;
}
.voronoi-toggle {
position: absolute;
right: 20px;
top: 20px;
}
.toggle-label {
color: #666;
display: inline-block;
padding-left: 5px;
vertical-align: top;
}
.voronoi-toggle .toggle + .toggle-button {
display: inline-block;
}
</style>
<style>
.toggle {
display: none;
}
.toggle, .toggle:after, .toggle:before, .toggle *, .toggle *:after, .toggle *:before, .toggle + .toggle-button {
box-sizing: border-box;
}
.toggle::-moz-selection, .toggle:after::-moz-selection, .toggle:before::-moz-selection, .toggle *::-moz-selection, .toggle *:after::-moz-selection, .toggle *:before::-moz-selection, .toggle + .toggle-button::-moz-selection {
background: none;
}
.toggle::selection, .toggle:after::selection, .toggle:before::selection, .toggle *::selection, .toggle *:after::selection, .toggle *:before::selection, .toggle + .toggle-button::selection {
background: none;
}
.toggle + .toggle-button {
outline: 0;
display: block;
width: 2.5em;
height: 1.25em;
position: relative;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.toggle + .toggle-button:after, .toggle + .toggle-button:before {
position: relative;
display: block;
content: "";
width: 50%;
height: 100%;
}
.toggle + .toggle-button:after {
left: 0;
}
.toggle + .toggle-button:before {
display: none;
}
.toggle:checked + .toggle-button:after {
left: 50%;
}
.toggle + .toggle-button {
background: #666;
border-radius: 1.25em;
padding: 2px;
-webkit-transition: all .4s ease;
transition: all .4s ease;
}
.toggle + .toggle-button:after {
border-radius: 50%;
background: #fff;
-webkit-transition: all .2s ease;
transition: all .2s ease;
}
.toggle:checked + .toggle-button {
background: #F4AFC9;
}
</style>
</head>
<body>
<div class="voronoi-toggle">
<input class="toggle" id="toggle-1" type="checkbox" onclick="toggleVoronoi()" />
<label class="toggle-button" for="toggle-1"></label>
<div class="toggle-label">Show voronoi</div>
</div>
<script>
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
function getRandomData(numberPoints) {
return d3.range(numberPoints).map(function(){
return {
value: Math.floor(Math.random() * 10 * 10 * 10 * 10 * 10),
label: makeLabel()
};
});
}
function makeLabel() {
return ALPHABET[Math.floor(Math.random() * ALPHABET.length)] +
ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
}
</script>
<script>
// DATA MANIPULATION
const data = getRandomData(50);
const extent = d3.extent(data, function(d){return d.value;});
// SETUP CHART AND FUNCTORS
const width = 960;
const height = 500;
const margin = {top: 50, bottom: 50};
const svg = d3.select('body').append('svg')
.attr('width', 960)
.attr('height', 500)
const g = svg.append('g')
.attr('transform', `translate(0,${margin.top})`)
const yScale = d3.scaleLog()
.domain(extent)
.rangeRound([height - margin.top - margin.bottom, 0]);
const yAxis = d3.axisLeft()
.scale(yScale)
.ticks(14, '.0s');
const simulation = d3.forceSimulation(data)
.force('x', d3.forceY(function(d) {return yScale(d.value);}).strength(1))
.force('y', d3.forceX(width / 2))
.force('collide', d3.forceCollide(8))
.stop();
for (var i = 0; i < 120; ++i){
simulation.tick();
};
const voronoiCells = d3.voronoi()
.extent([[0, -margin.top], [width, height + margin.top]])
.x(function(d) {return d.x;})
.y(function(d) {return d.y;})
.polygons(data);
// RENDER
g.append('g')
.attr('class', 'axis')
.attr('transform', `translate(${width * 0.4},0)`)
.call(yAxis);
const cells = g
.append('g').attr('class', 'cells')
.selectAll('.cells')
.data(voronoiCells).enter()
.append('g').attr('class', 'cell');
cells.append('circle')
.attr('class', 'value')
.attr('cx', function(d) {return d.data.x;})
.attr('cy', function(d) {return d.data.y;})
.attr('fill', 'none')
.attr('r', 6)
.attr('stroke-width', 2)
.on('mouseover', function(d){console.log(d.data)});
// OPTIONAL / DEBUGGING
cells.append('path')
.attr('class', 'voronoi')
.attr('opacity', 0)
.attr('d', function(d) {return 'M' + d.join('L') + 'Z';})
// FOR FUN
setInterval(function(){
const newData = getRandomData(50);
const newExtent = d3.extent(newData, function(d){return d.value;});
yScale.domain(newExtent);
yAxis.scale(yScale);
const newSimulation = d3.forceSimulation(newData)
.force('x', d3.forceY(function(d) {return yScale(d.value);}).strength(1))
.force('y', d3.forceX(width / 2))
.force('collide', d3.forceCollide(8))
.stop();
for (var i = 0; i < 120; ++i){
newSimulation.tick();
};
const newVoronoiCells = d3.voronoi()
.extent([[0, -margin.top], [width, height + margin.top]])
.x(function(d) {return d.x;})
.y(function(d) {return d.y;})
.polygons(newData);
// // RENDER
g.selectAll('.axis').transition().duration(1000)
.call(yAxis);
d3.selectAll('.cell .value').data(newVoronoiCells)
.transition().duration(1000)
.attr('cx', function(d) {return d.data.x;})
.attr('cy', function(d) {return d.data.y;})
d3.selectAll('.cell .voronoi').data(newVoronoiCells)
.transition().delay(500).duration(1)
.attr('d', function(d) {return 'M' + d.join('L') + 'Z';})
}, 1500)
</script>
<script>
function toggleVoronoi() {
d3.selectAll('.voronoi')
.transition()
.attr('opacity', function(){
return +this.getAttribute('opacity') ? 0 : 1;
})
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment