<p>d3.js is most flexible visualization library for data science based on a benchmark (<a href="http://datak.biz/blog/detail/15/" target="_blank">previous blog</a>). One of a downside is that it is still developing and every version update has major changes. Since I would like to have world map in an about page for myself on whom I did work with together in the world, I am researching latest coding to have world map with couple of animation such as blinking a dots, and tooltips. You will see actual code and visualization <a href="https://bl.ocks.org/Tak113/4a8caf75e1d3aa13132c8ad9a662a49b" target="_blank">here</a> at github gist and official landing page is <a href="http://datak.biz/about/" target="_blank">here</a> for my home page</p><p><br></p><h3>File Structure</h3><ol><li>index.html : index html page</li><li>cities.csv : unique city and company info who I worked with, having latitude and longitude</li><li>main.js : stores d3.js code</li><li>style.css : css</li><li>world-110m,json : topojson based world map (open source)</li></ol><p><br></p><h3>Index.html</h3><p>index has a link for each libraries I'm using. CDN for bootstrap, d3js v6, and additional json library to handle topojson with d3. TopoJSON is an extension of GeoJSON that encodes topology. Index file also have a overall site framework. In this example, I've put navigation bar using bootstrap v4.4 and put <div> inside of bootstrap container which has a "id" for d3 SVG.</p><pre><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- bootstrap only --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <!-- custom css --> <link rel="stylesheet" href="./style.css"> <title>Business in the world</title> </head> <body> <!-- nav bar --> <nav class="navbar navbar-expand-sm navbar-dark bg-secondary"> <div class="container"> <a href="#" class="navbar-brand">World Map</a> <button class="navbar-toggler" data-toggle="collapse" data-target="#navbarCollapse"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav"> <li class="nav-item"> <a href="#" class="nav-link active">Home</a> </li> <li class="nav-item"> <a href="#" class="nav-link">Test1</a> </li> <li class="nav-item"> <a href="#" class="nav-link">Test2</a> </li> </ul> </div> </div> </nav> <section> <div class="container py-5"> <!-- d3 graph --> <div id="map"></div> </div> </section> <!-- JavaScript Bundle with Popper --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script> <!-- d3 --> <script src="https://d3js.org/d3.v6.min.js"></script> <!-- topojson --> <script src="https://unpkg.com/topojson@3"></script> <!-- custom js --> <script src="./main.js"></script> </body> </html></pre><p><br></p><h3>cities.csv</h3><pre>category,company,city,country,lat,lon Customer,BtB Electronics,Gifu,Japan,35,137 Customer,LS Technology,Miyagi,Japan,38,141 Customer,Online Consultant,Kanagawa,Japan,35,140 Customer,BtB Electronics,Nagano,Japan,37,138 Customer,BtB Electornics,Busan,Korea,35,128 Customer,BtB Electronics,Taoyuan,Taiwan,25,121 Customer,BtB Electronics,Hinterburg,Austria,48,16 Customer,Intel Corporation,California,US,37,-122 MyCompany,Office,Arizona,US,33,-112</pre><p>Data table includes 6 objects and 9 records, 4 categorical and 2 numerical objects out of 6. Numerical data represents address in the world by latitude and longitude. </p><p><br></p><h3>world-110m.json</h3><p>this is given data set to render entire world</p><p><br></p><h3>style.css</h3><pre>#map path { stroke: none; stroke-width: 0.25px; fill: #6C757D; } #map rect { fill: #e9ecef; } #map circle { stroke: none; } #map div.tooltip { position: absolute; text-align: left; /*color: lightgreen;*/ padding: 3px; font: 12px sans-serif; background: black; border: 0px; border-radius: 3px; pointer-events: none; } #map span.tip-name { color: white; }</pre><p>Any static CSS features are in css file, while dynamic CSS features are written by javascript in main.js file</p><p><br></p><h3>main.js</h3><p>This is a main java script file where d3 code is written.</p><p>At first framing is defined selecting "#map" id from <div> tag set in index.html. Since map does not have x and y axis, svg does not need smaller g element nor translate. "responsivefy" function is explained later but mainly to enable fluid drawings.</p><pre>// framing const width = 950, height = 400; const svg = d3.select('#map') .append('svg') .attr('width', width) .attr('height', height) .call(responsivefy); //fluid svg function</pre><p><br></p><p>If you would like to set background color, we need an additional "rect" to make it colorize. width and height are 100% hence matched with svg defined above</p><pre>//background color svg.append("rect") .attr("width", "100%") .attr("height", "100%") .attr('rx', 8);</pre><p><br></p><p>Then projection model is set. This d3.geoMercator() has a method original map assumptions, such as where to set center (center([]), list stores x and y), magnitude (scale()), and rotation (rotate()). Then passing the projection model to SVG "path" for a map generation</p><pre>//set projection model let projection = d3.geoMercator() //define projection to use .center([0,5]) .scale(150) //set how far zoom in .rotate([-160,0]); //offset lon and lat //path generator(passing to SVG canvas generator) let path = d3.geoPath() .projection(projection);</pre><p><br></p><p>Additional <div> is created for a tooltip, <div> instead of <tooltip> would be flexible to set features. It starts with hidden hence opacity is 0, while other static CSS features are in style.css file </p><pre>//define div for tooltip let div = d3.select('#map').append("div") .attr("class", "tooltip") .style("opacity", 0);</pre><p><br></p><p>myColor object is created to draw with different color. In the category we have 2 factor hence put 2 colors in the list and passes to range object</p><pre>//define circle color let myColor = d3.scaleOrdinal() .domain(["Customer","MyCompany"]) .range(["lightblue","pink"])</pre><p><br></p><p>Then loading a data and display the world. Note data load function has been changed for v6. Loading uniques dots("circle") are a bit complex. In the circle event, append 'circle' and passing longitude and latitude information for each record by additional d(data) => {} function. Repeat event is created and assigned here, but location does not matter (for example, we can put repeat(); at the very last in this data loading blackets but it still works. </p><pre>//load and display the world d3.json('world-110m.json').then(topology => { console.log(topology); //load and display the cities, and tooltip d3.csv('cities.csv').then(data => { console.log(data); const circle = svg.selectAll('circle') .data(data) .enter() .append('circle') .attr('cx', d=>{ return projection([d.lon, d.lat])[0]; }) .attr('cy', d=>{ return projection([d.lon, d.lat])[1]; }) .style('fill', d=>{ return myColor(d.category); }); repeat();</pre><p><br></p><p>Since we have multiple event for a circle object such as blinking a dots and tooltip on mouseover, we split into each event. Combining all of method chain would not work and recommend to split. First event is blinking and we would like to repeat the blinking. We create anonymous function called repeat() and put all repeat animation, and put .on('end', repeat) method at the end of the chain. This anonymous function can be called just by repeat(); like above</p><pre> //blink function repeat() { circle .attr('r',1) .attr('opacity',1) .transition() .duration(2000) .attr('r',15) .attr('opacity',0) .on('end',repeat); };</pre><p><br></p><p>Next event is a tooltip. This is still a method chain from circle using .on() passing mouse over/out event. Each event passes data info so we could show unique information for each data record.</p><pre> //tooltip circle .on("mouseover", function(event,d) { div.transition() .duration(500) .style("opacity", .7); div.html( "<strong>Location</strong><span class='tip-name'> : " + d.city + ", " + d.country + "</span><br/><strong>Conpany</strong><span class='tip-name'> : " + d.company + "</span>" ) .style("left", (event.pageX + 10) + "px") //offset from dot x .style("top", (event.pageY - 40) + "px") //offset from dot y .style('color', myColor(d.category)); }) .on("mouseout", function(d) { div.transition() .duration(500) .style("opacity", 0); }); }); //display world map svg.selectAll('path') .data(topojson .feature(topology, topology.objects.countries) .features) .enter() .append('path') .attr('d',path); });</pre><p><br></p><p>d3.zoom() function is used for zoom in and out. In .on() event, we need to specify all SVG element which we would like to follow zooming event. In my case, we are passing 'path' for map drawing and 'circle' for each city dots.</p><pre>//zoom const zoom = d3.zoom() .scaleExtent([1, 8]) .on('zoom', event => { svg.selectAll('path') .attr('transform', event.transform); svg.selectAll('circle') .attr('transform', event.transform); }); svg.call(zoom);</pre><p><br></p><p>The last bucket is not a d3 function, but it would be a best result to combine this with d3 object. This bucket is to make SVG fluid on browser window size. Without these SVG keeps having original width and height we've set at an initial. We can call this function as responsivefy(), and we've put responsivefy() at the last of svg method as a calling.</p><pre>//fluid svg function function responsivefy(svg) { // container will be the DOM element the svg is appended to // we then measure the container and find its aspect ratio const container = d3.select(svg.node().parentNode), width = parseInt(svg.style('width'), 10), height = parseInt(svg.style('height'), 10), aspect = width / height; // add viewBox attribute and set its value to the initial size // add preserveAspectRatio attribute to specify how to scale // and call resize so that svg resizes on inital page load svg.attr('viewBox', `0 0 ${width} ${height}`) .attr('preserveAspectRatio', 'xMinYMid') .call(resize); // add a listener so the chart will be resized when the window resizes // to register multiple listeners for same event type, // you need to add namespace, i.e., 'click.foo' // necessary if you invoke this function for multiple svgs // api docs: https://github.com/mbostock/d3/wiki/Selections#on d3.select(window).on('resize.' + container.attr('id'), resize); // this is the code that actually resizes the chart // and will be called on load and in response to window resize // gets the width of the container and proportionally resizes the svg to fit function resize() { const targetWidth = parseInt(container.style('width')); svg.attr('width', targetWidth); svg.attr('height', Math.round(targetWidth / aspect)); } }</pre><p><br></p><h3>Appendix : What is a difference between TopoJSON and GeoJSON</h3><p>GeoJSON is a JSON file format to represent geographical information. Per wikipedia,</p><blockquote><p>The features include points (therefore addresses and locations), line strings (therefore streets, highways and boundaries), polygons (countries, provinces, tracts of land), and multi-part collections of these types. GeoJSON features need not represent entities of the physical world only; mobile routing and navigation apps, for example, might describe their service coverage using GeoJSON</p></blockquote><p>Meanwhile, TopoJSON is an extension of GeoJSON that encodes topology. That said, TopoJSON eliminates some of redundancy from GeoJSON so it's more simple and lighter.</p><blockquote><p>Rather than representing geometries discretely, geometries in TopoJSON files are stitched together from shared line segments called arcs.[6] Arcs are sequences of points, while line strings and polygons are defined as sequences of arcs. Each arc is defined only once, but can be referenced several times by different shapes, thus reducing redundancy and decreasing the file size.[7] In addition, TopoJSON facilitates applications that use topology, such as topology-preserving shape simplification, automatic map coloring, and cartograms.</p></blockquote><p><br></p><p>Hope this helps!</p><p><br></p><p>Below are reference site</p><p>map and point for d3 v6 : <a href="https://bl.ocks.org/d3noob/c056543aff74a0ac45bef099ee6f5ff4" target="_blank">https://bl.ocks.org/d3noob/c056543aff74a0ac45bef099ee6f5ff4</a><a href="https://bl.ocks.org/d3noob/c056543aff74a0ac45bef099ee6f5ff4" target="_blank"></a></p><p>tooltip for d3 v6 : <a href="https://bl.ocks.org/d3noob/180287b6623496dbb5ac4b048813af52" target="_blank">https://bl.ocks.org/d3noob/180287b6623496dbb5ac4b048813af52</a></p><p>looping animation for d3 v6 : <a href="https://bl.ocks.org/d3noob/bf44061b1d443f455b3f857f82721372" target="_blank">https://bl.ocks.org/d3noob/bf44061b1d443f455b3f857f82721372</a><span style="color: rgb(106, 115, 125); font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; font-size: 12px; white-space: pre;"></span></p><p><span style="color: rgb(106, 115, 125); font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; font-size: 12px; white-space: pre;"><br></span> </p>
<< Back to Blog Posts
Back to Home