Skip to content

Instantly share code, notes, and snippets.

@micahstubbs
Last active April 2, 2017 00:14
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 micahstubbs/0e2b63921f5642f0f65f51e27cccd02f to your computer and use it in GitHub Desktop.
Save micahstubbs/0e2b63921f5642f0f65f51e27cccd02f to your computer and use it in GitHub Desktop.
spinner with d3-component | es2015
license: mit
border: no

an es2015 iteration on the block Spinner with d3-component from @currankelleher


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:

  • Use of local state and lifecycle hooks (spinner timer).
  • Composition of components with conditional rendering (app component).

Built with blockbuilder.org

forked from curran's block: Posts with D3 Component

forked from curran's block: Spinny Loading Icon

<!DOCTYPE html>
<head>
<meta charset='utf-8'>
<script src='https://unpkg.com/d3@4'></script>
<script src='https://unpkg.com/d3-component@3'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.23.1/babel.min.js'></script>
</head>
<body>
<svg width='960' height='500'></svg>
<script src='vis.js'></script>
</body>
# safe
lebab --replace vis.js --transform arrow
lebab --replace vis.js --transform for-of
lebab --replace vis.js --transform for-each
lebab --replace vis.js --transform arg-rest
lebab --replace vis.js --transform arg-spread
lebab --replace vis.js --transform obj-method
lebab --replace vis.js --transform obj-shorthand
lebab --replace vis.js --transform multi-var
# unsafe
lebab --replace vis.js --transform let
lebab --replace vis.js --transform template
/* global d3 */
// This function simulates fetching with a 2 second delay.
// You can replace stuff here with e.g. d3.csv.
function fetchData(callback) {
setTimeout(() => {
callback([6, 5, 4, 3, 2, 1]);
}, 2000);
}
// This function visualizes the data.
function visualize(selection, data) {
const rects = selection
.selectAll('rect')
.data(data);
rects.exit().remove();
rects
.enter().append('rect')
.attr('x', (d, i) => (i * 100) + 182)
.attr('y', d => 400)
.attr('width', 50)
.attr('height', 0)
.merge(rects)
.transition().duration(1000).ease(d3.easeBounce)
.delay((d, i) => i * 500)
.attr('y', d => 400 - (d * 50))
.attr('height', d => 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.
const wheel = d3.component('g')
.create(function (selection) {
const minRadius = 4;
const maxRadius = 10;
const numDots = 10;
const wheelRadius = 40;
const rotation = 0;
const rotationIncrement = 3;
const radius = d3.scaleLinear()
.domain([0, numDots - 1])
.range([maxRadius, minRadius]);
const 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.sin(angle(d)) * wheelRadius)
.attr('cy', d => Math.cos(angle(d)) * wheelRadius)
.attr('r', radius);
})
.render(function (selection, d) {
selection.attr('transform', `rotate(${d})`);
});
// This component with a local timer makes the wheel spin.
const spinner = ((() => {
const timer = d3.local();
return d3.component('g')
.create(function (selection, d) {
timer.set(selection.node(), d3.timer((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.
const 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.
const 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() {
const svg = d3.select('svg');
const width = svg.attr('width');
const height = svg.attr('height');
// Initialize the app to be "loading".
svg.call(app, {
width,
height,
loading: true,
});
// Invoke the data fetching logic.
fetchData((data) => {
svg.call(app, {
width,
height,
loading: false,
data,
});
});
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment