{
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);
const xAxisGroup = svg.append('g')
.attr('transform', `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x))
.call(g => g.select('.domain').remove())
.call(g => g.append('text')
.attr('x', margin.left + (width - margin.bottom - margin.top) / 2)
.attr('y', 40)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text('horsepower'));
const yAxisGroup = svg.append('g')
.attr('transform', `translate(${margin.left})`)
.call(d3.axisLeft(y))
.call(g => g.select('.domain').remove())
.call(g => g.append('text')
.attr('x', -40)
.attr('y', margin.top + (height - margin.top - margin.bottom) / 2)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.text('weight (lbs)'));
// grid based on https://observablehq.com/@d3/scatterplot
const yGrid = svg.append('g');
yGrid.selectAll('line')
.data(y.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', margin.left)
.attr('x2', width - margin.right)
.attr('y1', d => y(d))
.attr('y2', d => y(d));
const xGrid = svg.append('g');
xGrid.selectAll('line')
.data(x.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', d => x(d))
.attr('x2', d => x(d))
.attr('y1', margin.top)
.attr('y2', height - margin.bottom);
// clip path hides dots that go outside of the vis when zooming/panning
svg.append('clipPath')
.attr('id', 'border')
.append('rect')
.attr('x', margin.left)
.attr('y', margin.top)
.attr('width', width - margin.left - margin.right)
.attr('height', height - margin.top - margin.bottom)
.attr('fill', 'white');
const dotsGroup = svg.append('g')
.attr('clip-path', 'url(#border)');
// draw circles
const dots = dotsGroup.selectAll('circle')
.data(carData)
.join('circle')
.attr('cx', d => x(d.horsepower))
.attr('cy', d => y(d.weight))
.attr('fill', d => carColor(d.origin))
.attr('opacity', 1)
.attr('r', 3);
const zoom = d3.zoom()
.extent([
[margin.left, margin.top],
[width - margin.right, height - margin.bottom]
])
// determine how much you can zoom out and in
.scaleExtent([1, Infinity])
.on('zoom', onZoom);
svg.call(zoom);
function onZoom(event) {
// get updated scales
const xNew = event.transform.rescaleX(x);
const yNew = event.transform.rescaleY(y);
// update the position of the dots
dots.attr('cx', d => xNew(d.horsepower))
.attr('cy', d => yNew(d.weight));
// update the axes
xAxisGroup.call(d3.axisBottom(xNew))
.call(g => g.selectAll('.domain').remove());
yAxisGroup.call(d3.axisLeft(yNew))
.call(g => g.selectAll('.domain').remove());
// update the grid
yGrid.selectAll('line')
.data(yNew.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', margin.left)
.attr('x2', width - margin.right)
.attr('y1', d => 0.5 + yNew(d))
.attr('y2', d => 0.5 + yNew(d));
xGrid.selectAll('line')
.data(xNew.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('y1', margin.top)
.attr('y2', height - margin.bottom)
.attr('x1', d => 0.5 + xNew(d))
.attr('x2', d => 0.5 + xNew(d));
}
return svg.node();
}