Zoom - lennartdeknikker/frontend-data GitHub Wiki

Defined in zoom.js

This file exports two functions:

  • addZoomToSvg() used in index.js
  • adjustCirclesToZoomLevel() called on zoom events and used in datapoints.js to render the right circle radius when the points are rendered first.

Adding zoom to the SVG

AddZoomToSvg()

// adds zoom functionality to the svg using the functions above.
async function addZoomToSvg(settings, svg) {
	const svgWithZoom = createZoomObject(settings).then(zoom => {
		const zoomGroup = svg.call(zoom).append('g');
		const zoomGroupWithHandlers = addZoomHandlers(
			zoom,
			zoomGroup,
			settings,
			svg
		);
		return zoomGroupWithHandlers;
	});
	return svgWithZoom;
}

This function first creates an object zoom using the scaleExtent defined in settings.json.

async function createZoomObject(settings) {
	const zoom = d3.zoom().scaleExtent(settings.render.scaleExtent);
	return zoom;
}

Adding handlers

addZoomHandlers()

Then handlers are added to accommodate zoom functionality on events:

function addZoomHandlers(zoom, zoomGroup, settings, svg) {
	function zoomHandler() {
		zoomGroup.attr('transform', d3.event.transform);
		adjustCirclesToZoomLevel(d3.event.transform.k, zoomGroup, settings);
	}

	function clickToZoom(zoomStep) {
		svg
			.transition()
			.duration(500)
			.call(zoom.scaleBy, zoomStep);
	}

	function resetProjection() {
		svg
			.transition()
			.duration(500)
			.call(zoom.transform, d3.zoomIdentity);
	}

	zoom.on('zoom', zoomHandler);

	// makes it possible to zoom on click with adjustable steps,
	// and resets the projection when search input is changed.
	d3.select('#btn-zoom--in').on('click', () => clickToZoom(2));
	d3.select('#btn-zoom--out').on('click', () => clickToZoom(0.5));
	d3.select('#search_input').on('select', () => resetProjection());
	d3.select('#search_input').on('click', () => resetProjection());
	return zoomGroup;
}

The function clickToZoom() is used to make it possible to zoom by using the buttons as well as zooming in by scrolling. resetProjection makes the projection reset when the user goes back to using the search input to search for different objects.

Adjusting data point circles to the zoom level

adjustCirclesToZoomLevel()

As you can see in addZoomHandlers(), the before mentioned adjustCirclesToZoomLevel() function is called on each zoom event. This function has a few helper functions to obtain the extent in radius, depending on the zoom-level and to calculate the factor by which the data needs to be multiplied to get the right radius.

When those variables are calculated, the function then calls two functions to both change the radius for each circle as well as the opacity, based on how far a user has zoomed in/out at that moment.

// adjusts the shown datapoints to the zoomlevel using the functions above.
function adjustCirclesToZoomLevel(zoomLevel, zoomGroup, settings) {
	const [minRadius, maxRadius] = calculateRadiusExtent(zoomLevel);
	const maxZoomLevel = settings.render.scaleExtent[1];
	const factor = calculateFactor(minRadius, maxRadius, settings);

	changeCircleRadius(zoomGroup, factor, minRadius, maxRadius);
	changeCircleOpacity(zoomGroup, zoomLevel, maxZoomLevel);
}

Helper function 1: calculateRadiusExtent()

This first helper function uses a formula I broke my head on for an hour to calculate a minimum and maximum radius extent, based on the current zoom level. When the user zooms in 7 times or more, the radius of the circle returned, is either 1 or 2.5, depending on what side of the median the amount of objects is at. This results in smaller circles that make it easier to pinpoint the location when zoomed in at the maximum level.

// calculates the extent of the circle radius.
function calculateRadiusExtent(zoomLevel) {
	const min = zoomLevel / 3 < 2 ? 3 - zoomLevel / 3 : 1;
	const max = zoomLevel * 7 < 37.5 ? 40 - zoomLevel * 7 : 2.5;
	return [min, max];
}

Helper function 2: calculateFactor()

This second helper function calculates the factor by which the radius needs to be multiplied in order to get the right radius. This function uses the radius extent supplied by the previous helper function and the data extent provided in settings.json.

// calculates the factor on which to transform the size of the circles.
function calculateFactor(minRadius, maxRadius, settings) {
	return (
		(maxRadius - minRadius) /
		(settings.render.dataExtent[1] - settings.render.dataExtent[0])
	);
}

changing the radius of the data point circles

changeCircleRadius()

This function uses the factor and radius extent provided by the helper function to change the radius for all circles shown on the map. When the result is bigger or smaller than the min or max radius, respectively either is returned. I also added a transition to have the circles change in size gradually.

// changes the radius of the datapoint circles according to zoomlevel.
function changeCircleRadius(g, factor, minRadius, maxRadius) {
	g.selectAll('circle')
		.transition()
		.duration(500)
		.attr('r', d => {
			if (d.amount * factor < minRadius) {
				return minRadius;
			}
			if (d.amount * factor > maxRadius) {
				return maxRadius;
			}
			return d.amount * factor;
		});
}

Changing the opacity of the data point circles on zooming in

changeCircleOpacity()

This function then changes the opacity of all datapoints according to the zoom level. When zoomed in more than halfway the maximum zoomlevel, the opacity is set to 1 again. I did that for the same reason the radius gets smaller when zoomed in at the maximum. Users can then see clearly at what location the data is pinpointed.

// changes the opacity of the datapoint circles according to zoomlevel.
function changeCircleOpacity(g, zoomLevel, maxZoomLevel) {
	if (zoomLevel < maxZoomLevel / 2) {
		g.selectAll('.datapoint').attr('fill-opacity', 0.3 + 0.7 / zoomLevel);
	} else {
		g.selectAll('.datapoint').attr('fill-opacity', 1);
	}
}