block by curran 685fa8300650c4324d571c6b0ecc55de

Spinner with d3-component

Full Screen

This example demonstrates usage of d3-component to create a spinner component, and render it conditionally while something is loading. After loading finishes, the spinner goes away and is replaced by another component.

How often have you seen a data visualization show nothing but a blank screen while the data is loading? Showing a spinner with D3 and hiding it after data is loaded is not as easy as it seems. Here’s a solution for doing this, feel free to use it in your projects!

This demonstrates the following patterns supported by d3-component:

Built with blockbuilder.org

forked from curran‘s block: Posts with D3 Component

forked from curran‘s block: Spinny Loading Icon

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://unpkg.com/d3@4"></script>
  <script src="https://unpkg.com/d3-component@3.0.0"></script>
</head>
<body>
  <svg width="960" height="500"></svg>
  <script>
    // This function simulates fetching with a 2 second delay.
    // You can replace stuff here with e.g. d3.csv.
    function fetchData(callback){
      setTimeout(function (){
        callback([6, 5, 4, 3, 2, 1]);
      }, 2000);
    }
    
    // This function visualizes the data.
    function visualize(selection, data){
      var rects = selection
      	.selectAll("rect")
      	.data(data);
      rects.exit().remove();
      rects
        .enter().append("rect")
      		.attr("x", function (d, i){ return i * 100 + 182; })
          .attr("y", function (d){ return 400; })
          .attr("width", 50)
          .attr("height", 0)
        .merge(rects)
        .transition().duration(1000).ease(d3.easeBounce)
      		.delay(function (d, i){ return i * 500; })
          .attr("y", function (d){ return 400 - d * 50; })
          .attr("height", function (d){ return d * 50; });
    }
 
    // The stuff below uses d3-component to display a spinner
    // while the data loads, then render the visualization after loading.
    
    // This stateless component renders a static "wheel" made of circles,
    // and rotates it depending on the value of props.angle.
    var wheel = d3.component("g")
      .create(function (selection){
        var minRadius = 4,
            maxRadius = 10,
            numDots = 10,
            wheelRadius = 40,
            rotation = 0,
            rotationIncrement = 3, 
            radius = d3.scaleLinear()
              .domain([0, numDots - 1])
              .range([maxRadius, minRadius]),
            angle = d3.scaleLinear()
              .domain([0, numDots])
              .range([0, Math.PI * 2]);
        selection
          .selectAll("circle").data(d3.range(numDots))
          .enter().append("circle")
            .attr("cx", d => Math.round(Math.sin(angle(d)) * wheelRadius))
            .attr("cy", d => Math.round(Math.cos(angle(d)) * wheelRadius))
            .attr("r", d => Math.round(radius(d)));
      })
      .render(function (selection, d){
        selection.attr("transform", "rotate(" + d + ")");
      });
    
    // This component with a local timer makes the wheel spin.
    var spinner = (function (){
      var timer = d3.local();
      return d3.component("g")
        .create(function (selection, d){
          timer.set(selection.node(), d3.timer(function (elapsed){
            selection.call(wheel, elapsed * d.speed);
          }));
        })
        .render(function (selection, d){
          selection.attr("transform", "translate(" + d.x + "," + d.y + ")");
        })
        .destroy(function(selection, d){
          timer.get(selection.node()).stop();
        	return selection
          		.attr("fill-opacity", 1)
          	.transition().duration(3000)
          		.attr("transform", "translate(" + d.x + "," + d.y + ") scale(10)")
          		.attr("fill-opacity", 0);
        });
    }());
    
    // This component displays the visualization.
    var visualization = d3.component("g")
    	.render(function (selection, d){
        selection.call(visualize, d.data);
      });
    
    // This component manages an svg element, and
    // either displays a spinner or text,
    // depending on the value of the `loading` state.
    var app = d3.component("g")
      .render(function (selection, d){
        selection
            .call(spinner, !d.loading ? [] : {
              x: d.width / 2,
              y: d.height / 2,
              speed: 0.2
            })
            .call(visualization, d.loading ? [] : d);
      });
    
    // Kick off the app.
    function main(){
      var svg = d3.select("svg"),
          width = svg.attr("width"),
          height = svg.attr("height");

      // Initialize the app to be "loading".
      svg.call(app, {
        width: width,
        height: height,
        loading: true
      });

      // Invoke the data fetching logic.
      fetchData(function (data){
        svg.call(app, {
          width: width,
          height: height,
          loading: false,
          data: data
        });
      });
    }
    main();
  </script>
</body>