avatarLucas Jellema

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

10487

Abstract

egendBox"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"legend-box"</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-box"</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"zoompanel"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"zoomIn()"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Zoom In"</span>></span>+<span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"zoomOut()"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Zoom Out"</span>></span>-<span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"reset()"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Reset"</span>></span>Reset<span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span></pre></div><p id="73ef">And when the button has been clicked upon twice:</p><figure id="cab0"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*WgFJvbCJ3kvNj-_ReBQVbA.png"><figcaption>The effect of zooming in on the world map (factor of 1.5 ma</figcaption></figure><p id="4233">The functions zoomIn() and zoomOut() that make the actual zoom happen are as simple as can be:</p><div id="62a8"><pre><span class="hljs-keyword">const</span> <span class="hljs-title function_">zoomOut</span> = (<span class="hljs-params"></span>) => { countriesGroup .<span class="hljs-title function_">transition</span>() .<span class="hljs-title function_">call</span>(zoomBehavior.<span class="hljs-property">scaleBy</span>, <span class="hljs-number">0.667</span>); } <span class="hljs-keyword">const</span> <span class="hljs-title function_">zoomIn</span> = (<span class="hljs-params"></span>) => { countriesGroup .<span class="hljs-title function_">transition</span>() .<span class="hljs-title function_">call</span>(zoomBehavior.<span class="hljs-property">scaleBy</span>, <span class="hljs-number">1.5</span>); }</pre></div><p id="b922">They make use of the zoomBehavior we defined earlier on and apply a transition to it, multiplying the existing scale factor with either 0.667 (zoom out) or 1.5 (zoom in). The zoomBehavior knows it can not scale beyond 5 or below 1 and it respects those boundaries.</p><p id="0f52">Similar to programmatic zooming is panning. Using the zoomBehavior, we can easily move around our viewpoint of the map within the constraints set by the translate extent. Here I have added four buttons to move our vantage point over the map.</p><figure id="1ae8"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*nalM13be6pzHwDzvowIoYw.gif"><figcaption>The focus point for the map is moved by pressing the buttons (that cause a translation to be performed on the group that contains all country shapes)</figcaption></figure><p id="aa3e">The code for creating these four buttons:</p><div id="a288"><pre><div <span class="hljs-keyword">class</span>=<span class="hljs-string">"zoom-box"</span>> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"zoompanel"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"zoomIn()"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Zoom In"</span>></span>+<span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"zoomOut()"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Zoom Out"</span>></span>-<span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"reset()"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Reset"</span>></span>Reset<span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">br</span> /></span></span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"translatepanel"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"panMap('left')"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Translate Left"</span>></span><-<span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"panMap('up')"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Translate Up"</span>></span>^<span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"panMap('down')"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Translate Down"</span>></span>v<span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"panMap('right')"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"zoom-panel"</span> <span class="hljs-attr">title</span>=<span class="hljs-string">"Translate Right"</span>></span>-><span class="hljs-tag"></<span class="hljs-name">button</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"></<span class="hljs-name">div</span>></span></span></pre></div><p id="59ac">The code for function panMap():</p><div id="66e8"><pre><span class="hljs-keyword">const</span> <span class="hljs-title function_">panMap</span> = (<span class="hljs-params">direction</span>) => { <span class="hljs-keyword">const</span> panStep=<span class="hljs-number">50</span> <span class="hljs-keyword">let</span> horizontal = direction==<span class="hljs-string">"left"</span>? panStep : (direction==<span class="hljs-string">"right"</span>? -<span class="hljs-attr">panStep</span>:<span class="hljs-number">0</span>) <span class="hljs-keyword">let</span> vertical = direction==<span class="hljs-string">"up"</span>? panStep : (direction==<span class="hljs-string">"down"</span>? -<span class="hljs-attr">panStep</span>:<span class="hljs-number">0</span>)
countriesGroup .<span class="hljs-title function_">transition</span>() .<span class="hljs-title function_">call</span>(zoomBehavior.<span class="hljs-property">translateBy</span>, horizontal, vertical); }</pre></div><p id="c97f">The translateBy function on the zoom behavior calculates the new transform from the current one combined with the translation of the horizontal and vertical distances (one of which is always 0). This function respects the translate extent that was defined on the zoomBehavior.</p><p id="22a5">Note: to reset the zoom and translation — and return the map to how it looked before we did any zooming and panning — we can invoke function reset():</p><div id="4681"><pre><span class="hljs-keyword">const</span> <span class="hljs-title function_">reset</span> = (<span class="hljs-params"></span>) => { countriesGroup .<span class="hljs-title function_">transition</span>().<span class="hljs-title function_">duration</span>(<span class="hljs-number">800</span>) .<span class="hljs-title function_">call</span>(zoomBehavior.<span class="hljs-property">translateTo</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>) .<span class="hljs-title function_">call</span>(zoomBehavior.<span class="hljs-property">scaleTo</span>, <span class="hljs-number">1</span>); }</pre></div><p id="822c">The duration(800) call takes care of turning the transition from the current scale factor and translation back to the beginning into a smooth animation that lasts for 800 ms.</p><p id="b274">The <a href="https://github.com/lucasjellema/worldmap-visualization-part-four/blob/main/index2.html">status of the index.html</a> at this point. A <a href="https://lucasjellema.github.io/worldmap-visualization-part-four/index2.html">live demo of th

Options

e world map</a> application.</p><h1 id="37e9">Zoom In on Selected Countries</h1><p id="7b46">A special form of programmatic zoom action is the following: when a user selects a country, zoom in on that country (shape). When multiple countries are selected, zoom in on all the country shapes;. And when a country is deselected, adjust the zoom accordingly.</p><p id="5495">This zoom is triggered whenever countries are selected and deselected, And it must take into account the rectangle that fully encloses the selected country or countries. The center of this rectangle defines the required translation target. And the size of that rectangle relative to the total map viewbox size determines the appropriate scale factor.</p><p id="1ab5">For example, with Namibia, Tanzania and Madagascar selected, the automatic zoom we want to implement should bring the area indicated by the rectangle in the next figure into focus.</p><figure id="5ac6"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*Qq3XRNoWHWhZnuNxrC9izg.png"><figcaption>When these three countries are selected, the red rectangle indicates the area that the map should focus on</figcaption></figure><p id="f4f9">After we are done with this particular feature, the map is zoomed in like this:</p><figure id="37e8"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*cZ0EZeFQyC4JS5lfE90nPg.png"><figcaption>When zoom-in on selected countries is implemented, this is how the map should respond to the selection of Namibia, Tanzania and Madagascar</figcaption></figure><p id="07c2">The following code will zoom (pan and scale) the map based on the selected countries:</p><ol><li>find all GeoJSON features for the currently selected countries</li><li>process all features and find the lowest and highest X and Y coordinate for any of them; the bounds function on geoGenerator returns the four extremes (min and max for X and Y) for the corners of the rectangle that fully encloses the shape</li><li>determine the scale factor: how much smaller than the entire map is the enclosing rectangle? I have decided that the scale factor cannot be larger than 4.</li><li>construct a transform that first translates to the center, then applies the new scale factor and finally translates to the center of the rectangle</li><li>apply the transformation to the countriesGroup</li></ol><figure id="5d57"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*Axok9tMbOxZ14lHk5fmM5g.png"><figcaption></figcaption></figure><div id="9c7a"><pre><span class="hljs-keyword">const</span> <span class="hljs-title function_">zoomToSelectedCountries</span> = (<span class="hljs-params"></span>) => { <span class="hljs-keyword">const</span> selectedCountriesGeoJSON = geojsonCountryData.<span class="hljs-property">features</span>.<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">c</span>) =></span> selectedCountries[c.<span class="hljs-property">properties</span>.<span class="hljs-property">name</span>]) <span class="hljs-keyword">var</span> minX = <span class="hljs-title class_">Number</span>.<span class="hljs-property">POSITIVE_INFINITY</span>; <span class="hljs-keyword">var</span> minY = <span class="hljs-title class_">Number</span>.<span class="hljs-property">POSITIVE_INFINITY</span>; <span class="hljs-keyword">var</span> maxX = <span class="hljs-title class_">Number</span>.<span class="hljs-property">NEGATIVE_INFINITY</span>; <span class="hljs-keyword">var</span> maxY = <span class="hljs-title class_">Number</span>.<span class="hljs-property">NEGATIVE_INFINITY</span>; selectedCountriesGeoJSON.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">geojson</span>) =></span> { <span class="hljs-comment">// [process all GeoJSON polygons in all selected countries to find </span> <span class="hljs-keyword">const</span> bounds = geoGenerator.<span class="hljs-title function_">bounds</span>(geojson); minX = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">min</span>(minX, bounds[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]); minY = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">min</span>(minY, bounds[<span class="hljs-number">0</span>][<span class="hljs-number">1</span>]); maxX = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">max</span>(maxX, bounds[<span class="hljs-number">1</span>][<span class="hljs-number">0</span>]); maxY = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">max</span>(maxY, bounds[<span class="hljs-number">1</span>][<span class="hljs-number">1</span>]); }) <span class="hljs-keyword">const</span> dx = maxX - minX; <span class="hljs-keyword">const</span> dy = maxY - minY; <span class="hljs-keyword">const</span> x = (minX + maxX) / <span class="hljs-number">2</span>; <span class="hljs-keyword">const</span> y = (minY + maxY) / <span class="hljs-number">2</span>; <span class="hljs-keyword">const</span> newScale = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">max</span>(<span class="hljs-number">1</span>, <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">min</span>(<span class="hljs-number">4</span>, <span class="hljs-number">0.9</span> / <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">max</span>(dx / mapWidth, dy / mapHeight))); <span class="hljs-keyword">const</span> transform = d3.<span class="hljs-property">zoomIdentity</span> .<span class="hljs-title function_">translate</span>(mapWidth / <span class="hljs-number">2</span>, mapHeight / <span class="hljs-number">2</span>) <span class="hljs-comment">// translate to center of map</span> .<span class="hljs-title function_">scale</span>(newScale) <span class="hljs-comment">// scale to the newScale factor</span> .<span class="hljs-title function_">translate</span>(-x, -y) <span class="hljs-comment">// translate to the newly calculate focus point</span> countriesGroup.<span class="hljs-title function_">transition</span>().<span class="hljs-title function_">duration</span>(<span class="hljs-number">750</span>).<span class="hljs-title function_">call</span>(zoomBehavior.<span class="hljs-property">transform</span>, transform); }</pre></div><p id="68ff">This function is to be invoked whenever the selection of countries changes. All such changes are currently taken care of by function <i>handleCountryClick</i>, so that is where <i>zoomToSelectedCountries</i> should be invoked:</p><figure id="83eb"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*F7e-wvG5NmmjEdsgiR9zUA.png"><figcaption></figcaption></figure><p id="4b0a">The <a href="https://github.com/lucasjellema/worldmap-visualization-part-four/blob/main/index3.html">status of the index.html</a> at this point. A <a href="https://lucasjellema.github.io/worldmap-visualization-part-four/index3.html">live demo</a> of the world map web application.</p><figure id="77d1"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*Gybc1UOL0dAGz_9vm7D2Og.gif"><figcaption>As countries are selected and unselected, the zoom and pan of the map is automatically updated</figcaption></figure><h1 id="156c">Conclusion</h1><p id="cb62">In this article I have leveraged the rich support for zooming and panning in D3.js. The articles describes both user initiated zooming and panning as well as programmatic zooming and panning. By adding transitions to the transformations, we can provide the user with smooth animations that make the step between two states easier to absorb.</p><h1 id="871f">Resources</h1><p id="7aa4"><a href="https://github.com/lucasjellema/worldmap-visualization-part-four">GitHub Repository</a> with the code for this article.</p><p id="7246">My earlier articles on World Map data visualization:</p><p id="a9a3">1. Create Interactive World Map to Visualize Country Data — <a href="https://readmedium.com/create-interactive-world-map-to-visualize-country-data-baa5b242bfbb">https://readmedium.com/create-interactive-world-map-to-visualize-country-data-baa5b242bfbb</a> — introducing World Map Data Visualization with d3, SVG and JavaScript.</p><p id="ca23">2. Presenting the World in Data using World Map Visualization — <a href="https://readmedium.com/presenting-the-world-in-data-using-world-map-visualization-d3-kaggle-world-data-set-7c16ca207fb0">https://readmedium.com/presenting-the-world-in-data-using-world-map-visualization-d3-kaggle-world-data-set-7c16ca207fb0</a>— bring together a straightforward approach to data visualization (from article one) and a rich data set from Kaggle that provides many interesting details for all countries in the world, related to education, health, economy, demography, climate and more.</p><p id="3b86">3. Interactive Data Visualization in World Map-Zoom, Translate, Legend — <a href="https://readmedium.com/interactive-data-visualization-in-world-map-translate-select-legend-popup-4d28261110df">https://readmedium.com/interactive-data-visualization-in-world-map-translate-select-legend-popup-4d28261110df</a> — a closer look at adding some interactivity to the world map. In particular: changing the rotation of the map, select countries (by clicking on them), show country details popup window and show legend — color scale (mapping heatmap colors to numerical values)</p><p id="c246">Follow up:</p><p id="5115">5. Map Visualization of (office locations in) The Netherlands -using GeoJSON, D3 and SVG — <a href="https://readmedium.com/map-visualization-of-office-locations-in-the-netherlands-using-geojson-d3-and-svg-62ce923d747d">https://readmedium.com/map-visualization-of-office-locations-in-the-netherlands-using-geojson-d3-and-svg-62ce923d747d</a> — find a GeoJSON file with data for the 12 provinces in The Netherlands and visualize the country with its provinces using d3.js in a simple web application. Then add a custom GeoJSON file with locations of my choosing and draw markers on the map for these locations.</p><p id="9b0d"><i>Originally published at <a href="https://technology.amis.nl/frontend/world-map-data-visualization-with-d3-js-geojson-and-svg-zooming-and-panning-and-dragging/">https://technology.amis.nl</a> on January 1, 2024.</i></p></article></body>

World Map Data Visualization with d3.js, GeoJSON and SVG-zooming and panning and dragging

Zooming and Panning in action on the World Map data visualization

In several recent articles on Data Visualization using a Thematic World Map with color shades assigned to countries based on their value for a specific property in a world data set I have introduced the use of d3.js library for browser based visualization using SVG in combination with the GeoJSON data format in which many geographical definitions are available, from countries and states to cities, lakes and forests. I have shown how we can easily make the map switch between different properties and how we can add some degree of interactivity (allowing countries to be selected by clicking on them and changing the rotation or horizontal / east-west translation of the map.

In this article we will continue on with interactivity. We will discuss zooming in (and then out again) in two different ways: programmatically and user initiated. We will also discuss panning (or dragging) the map (when it is larger than the view box through which we look at it. Panning in d3 is a part of zooming and we get it more or less for free.

The final topic in this article is: zooming in on all selected countries — adjusting the zoom and translation whenever the selection changes.

User Initiated Zooming and Panning

D3 comes with rich support for zooming and panning (dragging, translating) SVG elements. It is clear that users have come to expect that graphs can be moved around and zoomed in upon — to the extent that I find myself sometimes making stretching and pinching finger movements on chart in the newspaper or some magazine.

In order to make the world map zoomable. we have to do a few simple things:

  • define a zoom behavior — a simple object that defines what should happen when the zoom event occurs; part of this behavior are the definition of the maximum and minimum zoom factor (how large of small can the map become) and how far the translation of the map can be done (aka how far the user can drag the map around)
  • attach the zoom behavior in such a way that every object (and every pixel) you want the user to be able to zoom on and drag with is enabled

In comparison to the code discussed in the earlier articles, I have now defined the height and width of the map using constants (1). I have also defined the maximum scaling factor (2): given the resolution of the GeoJSON data it does not make sense to allow users to zoom beyond factor 5.

I make use of a trick to ensure all pixels in the map are zoom-enabled: I create a group (3) that contains all country shapes and also an invisible rectangle. The zoom behavior will be associated with the group (and be inherited by all its children). However, a group is not represented by pixels. That where the invisible rectangle comes in (4): it makes sure that every pixel on the screen is part of an actual SVG object that has zoom behavior.

 const mapWidth = 1000, mapHeight = 400
 const svg = d3.select("#map-container")
                .append("svg")
                .attr("width", mapWidth)
                .attr("height", mapHeight);

 const maxScaleFactor = 5 // how far and zooming in continue? (what is the maximum magnification factor)

 const countriesGroup = svg.append('g'); // create an SVG group to only contain country shapes; the SVG root can contain other (groups of) elements
 // Append a <rect> element with fill and opacity 0 (invisible) to allow every pixel in the area to be zoomable and draggable 
 countriesGroup.append("rect")
                .attr("x", 0) // X-coordinate
                .attr("y", 0) // Y-coordinate
                .attr("width", mapWidth) // Width
                .attr("height", mapHeight) // Height
                .attr("opacity", 0.0) // Opacity set to 0 (invisible)
                .attr("fill", "white")

With this set up in place, adding the zoom behavior is quite straightforward.

  1. the zoomBehavior is associated with the countriesGroup
  2. the zoomBehavior is defined, using 1..maxScaleFactor as the zoom range and 0,0 to mapWidth, mapHeight as the translation (or panning/fragging) range (the latter means that the entire area between 0,0 and mapWidth, mapHeight will always be covered with a bit of map. When the zoom event occurs, function handleZoom is to be invoked
  3. Function handleZoom will simply apply the transformation associated with the zoom event (a combination of scale and translate) to all path elements in the countriesGroup (the shapes of all countries). Note: this is where you can implement funny things like a country that does not participate in zooming)
const handleZoom = (event) => {
                countriesGroup.selectAll('path').attr('transform', event.transform); // Apply the zoom transform to path elements in countriesGroup (i.e. the country shapes)
            }

const zoomBehavior = d3.zoom()
                .scaleExtent([1, maxScaleFactor])
                .translateExtent([[0, 0], [mapWidth, mapHeight]])
                .on("zoom", handleZoom)

// Attach the zoom behavior to the SVG group that contains all countries (as well as the rectangle that makes sure even every pixel not in a country shape is zoom=enabled) 
countriesGroup.call(zoomBehavior)

After zooming and panning, the map could present for example this focus view on South Asia:

World Map — zoomed in on and panned to South Asia

Note that the legend is not enlarged, nor is the H1 title element. Zoom only applied to the countriesGroup element and all is children.

Country selection and associated styling is included in zooming.

The highlighted borders are as much thicker as the map is larger. We could consider applying different CSS styles — with differing stroke-width values — for different zoom factors.

The current state of the index.html. A live demo of this functionality.

Programmatic Zoom — Buttons for zooming in and out

In addition to zooming and panning through the built in zoom event that is triggered by a user’s actions, we can also cause zoom (and translate) in a programmatic way. As a simple example, let’s add zoom-controls — buttons that allow zooming in and out (respecting the zoom range from 1..5.

This will look like this:

The code for this:

.zoom-box {
    position: absolute;
    top: 500px;
    left: 40px;
    background-color: antiquewhite;
    padding: 10;
}

.zoom-panel {
    font-size: 20px;
}
  </style>
</head>

<body>
  <h1>Life expectancy per country</h1>
  <div id="map-container"></div>
  <div id="legendBox" class="legend-box">
  </div>
  <div class="zoom-box">
    <div id="zoompanel" class="zoom-panel">
      <button onclick="zoomIn()" class="zoom-panel" title="Zoom In">+</button>
      <button onclick="zoomOut()" class="zoom-panel" title="Zoom Out">-</button>
      <button onclick="reset()" class="zoom-panel" title="Reset">Reset</button>
    </div>

And when the button has been clicked upon twice:

The effect of zooming in on the world map (factor of 1.5 ma

The functions zoomIn() and zoomOut() that make the actual zoom happen are as simple as can be:

const zoomOut = () => {
    countriesGroup
        .transition()
        .call(zoomBehavior.scaleBy, 0.667);
}
const zoomIn = () => {
    countriesGroup
        .transition()
        .call(zoomBehavior.scaleBy, 1.5);
}

They make use of the zoomBehavior we defined earlier on and apply a transition to it, multiplying the existing scale factor with either 0.667 (zoom out) or 1.5 (zoom in). The zoomBehavior knows it can not scale beyond 5 or below 1 and it respects those boundaries.

Similar to programmatic zooming is panning. Using the zoomBehavior, we can easily move around our viewpoint of the map within the constraints set by the translate extent. Here I have added four buttons to move our vantage point over the map.

The focus point for the map is moved by pressing the buttons (that cause a translation to be performed on the group that contains all country shapes)

The code for creating these four buttons:

<div class="zoom-box">
  <div id="zoompanel" class="zoom-panel">
      <button onclick="zoomIn()" class="zoom-panel" title="Zoom In">+</button>
      <button onclick="zoomOut()" class="zoom-panel" title="Zoom Out">-</button>
      <button onclick="reset()" class="zoom-panel" title="Reset">Reset</button>
  </div>
  <br />
  <div id="translatepanel" class="zoom-panel">
      <button onclick="panMap('left')" class="zoom-panel" title="Translate Left"><-</button>
      <button onclick="panMap('up')" class="zoom-panel" title="Translate Up">^</button>
      <button onclick="panMap('down')" class="zoom-panel" title="Translate Down">v</button>
      <button onclick="panMap('right')" class="zoom-panel" title="Translate Right">-></button>
  </div>
</div>

The code for function panMap():

const panMap = (direction) => {
    const panStep=50
    let horizontal = direction=="left"? panStep : (direction=="right"? -panStep:0)
    let vertical = direction=="up"? panStep : (direction=="down"? -panStep:0)                            
    countriesGroup
        .transition()
        .call(zoomBehavior.translateBy, horizontal, vertical);
}

The translateBy function on the zoom behavior calculates the new transform from the current one combined with the translation of the horizontal and vertical distances (one of which is always 0). This function respects the translate extent that was defined on the zoomBehavior.

Note: to reset the zoom and translation — and return the map to how it looked before we did any zooming and panning — we can invoke function reset():

const reset = () => {
    countriesGroup
        .transition().duration(800)
        .call(zoomBehavior.translateTo, 0, 0)
        .call(zoomBehavior.scaleTo, 1);
}

The duration(800) call takes care of turning the transition from the current scale factor and translation back to the beginning into a smooth animation that lasts for 800 ms.

The status of the index.html at this point. A live demo of the world map application.

Zoom In on Selected Countries

A special form of programmatic zoom action is the following: when a user selects a country, zoom in on that country (shape). When multiple countries are selected, zoom in on all the country shapes;. And when a country is deselected, adjust the zoom accordingly.

This zoom is triggered whenever countries are selected and deselected, And it must take into account the rectangle that fully encloses the selected country or countries. The center of this rectangle defines the required translation target. And the size of that rectangle relative to the total map viewbox size determines the appropriate scale factor.

For example, with Namibia, Tanzania and Madagascar selected, the automatic zoom we want to implement should bring the area indicated by the rectangle in the next figure into focus.

When these three countries are selected, the red rectangle indicates the area that the map should focus on

After we are done with this particular feature, the map is zoomed in like this:

When zoom-in on selected countries is implemented, this is how the map should respond to the selection of Namibia, Tanzania and Madagascar

The following code will zoom (pan and scale) the map based on the selected countries:

  1. find all GeoJSON features for the currently selected countries
  2. process all features and find the lowest and highest X and Y coordinate for any of them; the bounds function on geoGenerator returns the four extremes (min and max for X and Y) for the corners of the rectangle that fully encloses the shape
  3. determine the scale factor: how much smaller than the entire map is the enclosing rectangle? I have decided that the scale factor cannot be larger than 4.
  4. construct a transform that first translates to the center, then applies the new scale factor and finally translates to the center of the rectangle
  5. apply the transformation to the countriesGroup
const zoomToSelectedCountries = () => {
    const selectedCountriesGeoJSON = geojsonCountryData.features.filter((c) => selectedCountries[c.properties.name])
    var minX = Number.POSITIVE_INFINITY;
    var minY = Number.POSITIVE_INFINITY;
    var maxX = Number.NEGATIVE_INFINITY;
    var maxY = Number.NEGATIVE_INFINITY;
    selectedCountriesGeoJSON.forEach((geojson) => { // [process all GeoJSON polygons in all selected countries to find 
        const bounds = geoGenerator.bounds(geojson);
        minX = Math.min(minX, bounds[0][0]);
        minY = Math.min(minY, bounds[0][1]);
        maxX = Math.max(maxX, bounds[1][0]);
        maxY = Math.max(maxY, bounds[1][1]);
    })
    const dx = maxX - minX;
    const dy = maxY - minY;
    const x = (minX + maxX) / 2;
    const y = (minY + maxY) / 2;
    const newScale = Math.max(1, Math.min(4, 0.9 / Math.max(dx / mapWidth, dy / mapHeight)));
    const transform = d3.zoomIdentity
        .translate(mapWidth / 2, mapHeight / 2) // translate to center of map
        .scale(newScale)  // scale to the newScale factor
        .translate(-x, -y)   // translate to the newly calculate focus point
    countriesGroup.transition().duration(750).call(zoomBehavior.transform, transform);
}

This function is to be invoked whenever the selection of countries changes. All such changes are currently taken care of by function handleCountryClick, so that is where zoomToSelectedCountries should be invoked:

The status of the index.html at this point. A live demo of the world map web application.

As countries are selected and unselected, the zoom and pan of the map is automatically updated

Conclusion

In this article I have leveraged the rich support for zooming and panning in D3.js. The articles describes both user initiated zooming and panning as well as programmatic zooming and panning. By adding transitions to the transformations, we can provide the user with smooth animations that make the step between two states easier to absorb.

Resources

GitHub Repository with the code for this article.

My earlier articles on World Map data visualization:

1. Create Interactive World Map to Visualize Country Data — https://readmedium.com/create-interactive-world-map-to-visualize-country-data-baa5b242bfbb — introducing World Map Data Visualization with d3, SVG and JavaScript.

2. Presenting the World in Data using World Map Visualization — https://readmedium.com/presenting-the-world-in-data-using-world-map-visualization-d3-kaggle-world-data-set-7c16ca207fb0— bring together a straightforward approach to data visualization (from article one) and a rich data set from Kaggle that provides many interesting details for all countries in the world, related to education, health, economy, demography, climate and more.

3. Interactive Data Visualization in World Map-Zoom, Translate, Legend — https://readmedium.com/interactive-data-visualization-in-world-map-translate-select-legend-popup-4d28261110df — a closer look at adding some interactivity to the world map. In particular: changing the rotation of the map, select countries (by clicking on them), show country details popup window and show legend — color scale (mapping heatmap colors to numerical values)

Follow up:

5. Map Visualization of (office locations in) The Netherlands -using GeoJSON, D3 and SVG — https://readmedium.com/map-visualization-of-office-locations-in-the-netherlands-using-geojson-d3-and-svg-62ce923d747d — find a GeoJSON file with data for the 12 provinces in The Netherlands and visualize the country with its provinces using d3.js in a simple web application. Then add a custom GeoJSON file with locations of my choosing and draw markers on the map for these locations.

Originally published at https://technology.amis.nl on January 1, 2024.

D3js
World Map
Data Visualization
Interactive
JavaScript
Recommended from ReadMedium