References:
Beeswarm Inspired by Beeswarm
Toggle inspired by Pure CSS toggle buttons
Color palette inspired by Material Motion
Built with blockbuilder.org
| license: mit |
References:
Beeswarm Inspired by Beeswarm
Toggle inspired by Pure CSS toggle buttons
Color palette inspired by Material Motion
Built with blockbuilder.org
| <!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> |