Instead of having the user click buttons in the last example, what if we just let them drag the chart area to see the UK's prison population change? It involves a bit more work on the user's behalf, but it also gives them the ability to freely navigate through the chart, which may be desirable in some circumstances.
Let's extend our prisonChart object again. Comment out everything in chapter5/index.js and add the following:
import draggablePrisonChart from './draggableChart';
(async (enabled) => {
if (!enabled) return;
await draggablePrisonChart.init();
draggablePrisonChart.addDragBehavior();
})(true);
Now, create a new file in lib/chapter5, called draggableChart.js:
import * as d3 from 'd3';
import PrisonPopulationChart from './prisonChart';
const draggablePrisonPopulationChart = Object.create(PrisonPopulationChart);
draggablePrisonPopulationChart.addDragBehavior = function addDrag() {};
This is the same way we started the other chart.
Inside of our addDrag function, add the following:
this.x.range([0, this.width * 4]);
this.update();
const bars = d3.select('.bars');
bars.attr('transform', 'translate(0,0)');
We set the x scale range to four times the screen size and update our chart to get the initial draw as well as set our axes. We also set an initial translate value on the bars, as we will use this value to ensure that it moves correctly.
Next, add the following, still inside our addDrag function:
const dragContainer = this.container.append('rect')
.classed('bar-container', true)
.attr('width', this.svg.node().getBBox().width)
.attr('height', this.svg.node().getBBox().height)
.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`)
.attr('x', 0)
.attr('y', 0)
.attr('fill-opacity', 0);
const xAxisTranslateY = d3.select('.axis.x').node().transform.baseVal[0].matrix.f;
We add a new, invisible element on top of everything, and we get the x axis translate value so that we can keep it at a constant Y position when we drag it. To do that, we get the x axis group element and then look at its transform.baseVal[0].matrix properties. The e property corresponds to y translate, and the f property corresponds to x translate.
Next, we set up our actual drag behavior and call it:
const drag = d3.drag().on('drag', () => {
const barsTranslateX = bars.node().transform.baseVal[0].matrix.e;
const barsWidth = bars.node().getBBox().width;
const xAxisTranslateX = d3.select('.axis.x').node()
.transform.baseVal[0].matrix.e;
const dx = d3.event.dx;
if (barsTranslateX + dx < 0 && barsTranslateX + dx >
-barsWidth + this.innerWidth()) {
bars.attr('transform', `translate(${barsTranslateX + dx}, 0)`);
d3.select('.axis.x').attr('transform',
`translate(${xAxisTranslateX + d3.event.dx}, ${xAxisTranslateY})`);
}
});
dragContainer.call(drag);
We attach the drag behavior to our dragContainer element. When a user drags a top of that element, it fires a drag event, which we use to set translate our bars and x axis horizontally. The d3.event object contains dx and dy, which are the horizontal and vertical distances dragged, respectively. The if-statement is used to prevent us from dragging past or ahead of our bars, meaning that they'll stop one dragged to the y axis or the last bar is dragged to the right of the x axis.
Outside of addDrag(), add the following to export our chart:
export default draggablePrisonPopulationChart;
Save, hit refresh, and you'll see your new chart: