bubblechart-reduce.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import * as d3 from 'd3';
  2. // d3js module for module05: reduce bubble chart
  3. export default () => {
  4. // defaults
  5. const options = {
  6. width: 912,
  7. height: 350,
  8. radius: 5,
  9. numberOfNodes: 500,
  10. setData: () => {} // eslint-disable-line no-empty-function
  11. };
  12. // start
  13. const bubbleChart = selection => {
  14. const svg = selection.select('svg');
  15. // shuffle array
  16. const shuffle = array => {
  17. let currentIndex = array.length;
  18. let temporaryValue;
  19. let randomIndex;
  20. // While there remain elements to shuffle...
  21. while (currentIndex !== 0) {
  22. // Pick a remaining element...
  23. randomIndex = Math.floor(Math.random() * currentIndex);
  24. currentIndex -= 1;
  25. // And swap it with the current element.
  26. temporaryValue = array[currentIndex];
  27. array[currentIndex] = array[randomIndex];
  28. array[randomIndex] = temporaryValue;
  29. }
  30. return array;
  31. };
  32. // select `numberOfNodes` random
  33. const circles = svg.selectAll('.circle');
  34. const baseArr = [];
  35. for (let i = 0; i < circles.size(); i += 1) baseArr.push(i);
  36. // segmenting circles
  37. const shuffledArr = shuffle(baseArr);
  38. const falling = shuffledArr.slice(0, options.numberOfNodes);
  39. const keeping = shuffledArr.slice(options.numberOfNodes, shuffledArr.length);
  40. const circlesFalling = circles.filter(d => falling.includes(d.index));
  41. const circlesKeeping = circles.filter(d => keeping.includes(d.index));
  42. const factor = circlesKeeping.size() === 200 ? 0.3 : 0.07;
  43. // reorder remaining circles at the bottom
  44. const reorderRemaining = () => {
  45. const ticked = () => {
  46. circlesKeeping
  47. .attr('cx', d => (d.x = Math.max(options.radius, Math.min(options.width - options.radius, d.x))))
  48. .attr('cy', d => (d.y = Math.min(options.height - options.radius, d.y)));
  49. };
  50. // .. using a force simulation
  51. const simulation = d3.forceSimulation()
  52. .force('y', d3.forceY(() => (options.height))
  53. .strength(d => ((options.height - d.y) / (options.height) * factor)))
  54. .velocityDecay(0.6)
  55. .alphaDecay(0.08)
  56. .force('collide', d3.forceCollide(options.radius * 2)
  57. .strength(0.5)
  58. );
  59. // add timer
  60. const endTime = 1500;
  61. const transitionTimer = d3.timer(elapsed => {
  62. const dt = elapsed / endTime;
  63. if (dt >= 1.0) {
  64. transitionTimer.stop();
  65. simulation.stop();
  66. options.setData({ continue: true });
  67. }
  68. });
  69. // move circles
  70. simulation.nodes(circlesKeeping.data())
  71. .on('tick', ticked);
  72. };
  73. // change color of falling circles
  74. circlesFalling
  75. .transition()
  76. .ease(d3.easeSin)
  77. .duration(1000)
  78. .attr('fill', '#D0021B');
  79. // change y coordinate of falling circles
  80. // after they have been fallen down, remove them
  81. circlesFalling
  82. .transition()
  83. .delay(1000)
  84. .duration(d => ((options.height - d.y) * 5))
  85. .ease(d3.easeCircleIn)
  86. .attr('cy', options.height * 1.25)
  87. .remove();
  88. window.setTimeout(() => (reorderRemaining()), 1500);
  89. };
  90. // "setter"
  91. bubbleChart.options = input => {
  92. Object.assign(options, input);
  93. return bubbleChart;
  94. };
  95. return bubbleChart;
  96. };