Basic interaction

Much like elsewhere in JavaScript Land, the principle for interaction is simple--attach an event listener to an element and do something when it's triggered. We add and remove listeners to and from selections with the .on() method, an event type (for instance, click), and a listener function that is executed when the event is triggered.

We can set a capture flag, which ensures that our listener is called first and all the other listeners wait for our listener to finish. Events bubbling up from children elements will not trigger our listener.

You can rely on the fact that there will only be a single listener for a particular event on an element because old listeners for the same event are removed when new ones are added. This is very useful for avoiding unpredictable behavior.

Just like other functions acting on element selections, event listeners get the current datum and index and set this context to the DOM element. The global d3.event will let you access the actual event object.

Let's create a function to add our buttons and wire this all up.

First, comment out everything in lib/chapter5/index.js and add the following:

import buttonPrisonChart from './buttonChart'; 
(async (enabled) => {
if (!enabled) return;
await buttonPrisonChart.resolveData();
await buttonPrisonChart.init();
buttonPrisonChart.addUIElements();
})(true);

This is like a simplified version of the last IIFE we wrote in index.js. We await the data and then initialize.

Create a new file, lib/chapter5/buttonChart.js. Add the following:

import * as d3 from 'd3'; 
import scenes from '../../data/prison_scenes.json';
import PrisonPopulationChart from './prisonChart';

const buttonPrisonPopulationChart = Object.create(PrisonPopulationChart);
buttonPrisonPopulationChart.scenes = scenes;
buttonPrisonPopulationChart.addUIElements = function addUI() {}

We import D3, a JSON file containing our descriptions for each chart state, and our last chart. Instead of creating a brand new chart with chartFactory, we use Object.create() to create a new object, using prisonChart as its prototype. This means that we have all methods and values of prisonChart available to us, and we can interact with its internal API by adding new methods or overloading the existing methods. We also instantiate a new function, addUIElements(), that we referenced earlier in index.js.

Add the following to addUIElements():

  // Add some room for buttons 
this.height -= 100;

// Needs to update y scale/axis
this.y.range([this.innerHeight(), 0]);
this.yAxisElement.call(this.yAxis);

// ...and the x scale/axis
this.xAxisElement.attr('transform', `translate(0, ${this.innerHeight()})`);

We set the height to 100 pixels smaller than what we used for the last chart (in essence, the entire screen height), and then update all the scales and axes accordingly. Now add the following:

  this.buttons = d3.select('body') 
.append('div')
.classed('buttons', true)
.selectAll('.button');

this.buttons.data(this.scenes)
.enter()
.append('button')
.classed('scene', true)
.text(d => d.label)
.on('click', d => this.loadScene(d))
.on('touchstart', d => this.loadScene(d));

this.words = d3.select('body').append('div');
this.words.classed('words', true);
this.loadScene(this.scenes[0]);
}; // This is the end of the addUI() method

This adds buttons to the page and sets the click and touchstart events to fire a method for loading in each scene.

We also create a div element to house our descriptions and assign that to the words' internal property. Lastly, we load the first entry state, which has nothing selected.

We're done with addUIElements(). It's time to add a few more functions--we want some functions to select individual bars, and we want some way of clearing that selection:

buttonPrisonPopulationChart.clearSelected = function clearSelected() { 
d3.timeout(() => {
d3.selectAll('.selected').classed('selected', false);
}, this.transitionSpeed);
};

buttonPrisonPopulationChart.selectBars = function selectBars(years) {
this.clearSelected();
d3.timeout(() => {
d3.select('.bars').selectAll('.bar')
.filter(d => years.indexOf(Number(d.year)) > -1)
.classed('selected', true);
}, this.transitionSpeed);
};

Here, we simply remove the selected class from all bars in our clearSelected() function and assign the same class to the bars when we pass a date range in selectBars(); pretty straightforward. We select the bars in a d3.timeout() call so that we're synchronized with the entry and exit bar transitions in prisonChart.update().

Lastly, we need to write our loadScene() method:

buttonPrisonPopulationChart.loadScene = function loadScene(scene) { 
const range = d3.range(scene.domain[0], scene.domain[1]);
this.update(this.data.filter(d => range.indexOf(Number(d.year)) > -1));

this.clearSelected();

if (scene.selected) {
const selected = scene.selected.range
? d3.range (...scene.selected.range) : scene.selected;
this.selectBars(selected);
}

this.words.html(scene.copy);

d3.selectAll('button.active').classed('active', false);
d3.select((d3.event && d3.event.target) ||
this.buttons.node()).classed('active', true);
};

In the preceding code, we first calculate the range of bars that we'll pass to update to set the new bars and axes. Then, if the selected scene property has a range property, we get an array of values using d3.range; otherwise we assume that it's already an array of values to be selected, which we then pass to selectBars(). If there's no selected bars, then it clears all the selected bars. We set the description to the copy provided by our JSON file, set all the buttons to inactive and, lastly, set our selected button to active.

Finally, we export our chart as such:

export default buttonPrisonPopulationChart;

Let's do our HTML layout using CSS. Create a new file, lib/chapter5/prisonChart.css. Add the following:

.selected { 
fill: red;
}

.buttons {
display: flex;
}

.buttons button {
flex-grow: 1;
color: white;
background: black;
border: 2px solid white;
transition: .5s background;
}

.buttons button.active {
background: steelblue;
}

.words {
padding: .5em;
text-align: center;
font-size: 1.5em;
}

This makes them stretch across the page and adds a basic CSS transition to when a button is selected. Import this into our prisonChart.js file at the top, as follows:

import './prisonChart.css';

Once again, we use Webpack's CSS and style loaders to pull our styles into our JavaScript module.

Your chart should now look like this:

And there you have it, your first interactive data visualization!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset