Using D3 and topoJSON to create a basic interactive floor map

Kami Lam
13 min readJul 14, 2024

--

Photo by Sven Mieke on Unsplash

D3.js is a light-weighted yet powerful Javascript library for data visualisation. Despite its common usage for creating various types of charts and even maps, it can be used for making an interactive floor map which we can see in some facilities like shopping malls, stations and airports.

In this article, I’d like to show you how to create a basic interactive floor map with topoJSON, D3.js and vanila Javascript (and the help of several useful online tools). By the end of the reading, you should be able to create your own topoJSON from a SVG file, fetching it in your script and having a floormap with hovering effect.

The repository related to this article is available in my Github: https://github.com/kamiviolet/d3-topojson-floormap

Level of Difficulty: Beginner ~ Intermediate

What is D3.js?

D3, short for Data-Driven Documents, is a free open source JS library for dynamic, interactive data visualisation in modern web browsers. Initially released in 2011, it offers various built-in functions from scaling, panning and zooming to transitioning, so that any developers can create different kinds of chart without the need to dive deep into the algorithms.

As for its syntax, since jQuery was omnipresent in web development back at the time, D3.js adopt the one very similar to jQuery, enables the developers to easily create, manipulate and style DOM elements using CSS selectors.

What is geoJSON?

GeoJSON, is an open standard formats to store geographic data. Unlike GIS shapefiles which are complex and storing data in binary, geoJSON is designed to represents smaller geographical features along with their non-spatial attributes, in the widely supported JSON format.

The basic format is as below:

{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [51.50382215630841, -0.11433874098846404]
},
"properties": {
"name": "marker",
"description": "I'm a marker near to London Waterloo."
}
}
]
}

What is topoJSON?

TopoJSON is an extension of geoJSON that encodes geospatial topology. Geometries in topoJSON are stitched together from shared line segments which are called arcs, rather than representing geometries discretely.

It is important not to get confused that, these “arcs” are different from the arcs in SVG (initiated by “A” or “a” command). In fact, neither geoJSON nor topoJSON supports svg arcs/ bezier curves. An arc here is an array of set(s) of [x,y] depending on the geometry type (which will be explained in the next section). Whenever we see curves, they are all plotted by several sets of [x,y].

If compared with the above-mentioned geoJSON format, topoJSON is like this:

{
"type": "Topology",
"objects": {
"object": {
"type": "Point",
"arcs": [[0]],
"properties": {
"name": "marker",
"description": "I'm a marker without real coordinates"
}
}
},
"arcs": [
[[100, 100]]
]
}

Now we have a general understanding about our tools, let’s get started with our project.

STEP 1 — Installation

Both D3.js and topoJSON can be installed using package manager like npm, yarn & pnpm, or load it in a script / HTML file by downloading the latest release, or from a CDN.

D3.js

If you have already installed Node in your environment, the easiest way for you is to install it via npm:

npm install d3

After then, you can import the library in your script using ES6 import module:

import * as d3 from "d3";

If you do not use any package managers, the official recommends using the CDN-hosted ES module bundle. They also provide a UMD bundle which allows you to download it as a plain script to use offline.

/* ESM + CDN */
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
...
</script>

/* UMD + CDN*/
<script src="https://cdn.jsdelivr.net/npm/d3@7" type="module"></script>
<script>
...
</script>

/* UMD + Local (Method 1 - HTML) */
<script src="./d3.v7.js" type="module"></script>
<script>
...
</script>

/* UMD + Local (Method 2 - JS) */
<script type="module">
import "./d3.v7.js";
...
</script>

topoJSON-client

Same as D3, you can run the following command in npm to install topoJSON-client:

npm install topojson-client

After then, import it by using ES6 import module:

import * as topojson from "topojson-client";

Otherwise, you can load it like this:

/* CDN */
<script src="https://unpkg.com/topojson-client@3" type="module"></script>
<script>
...
</script>

/* Local */
<script type="module">
import "./topojson-client.js";
...
</script>

The installation for D3 and topoJSON-client should be straightforward. In case you encounter any difficulties, I recommend to go to the official documentation and read it through or you can also leave a comment here to describe your issue.

STEP 2 — Data Entry

In this step you need to prepare the topoJSON file. In order to do so, you will need to:

  1. get the path of any shapes you want to draw; and,
  2. convert the path into polygon; and,
  3. store the path inside topoJSON as an array.

STEP 2.0 Draw the floor map as an SVG

Unless your floor map is very simple, you want to draw the SVG in order to get the arcs. For that, I recommend Inkscape as it is a free open source yet powerful SVG editor. It is also surprisingly easy to use (compared with what it can achieve).

If you never heard of this software or are not sure how to use it, you may want to get familiar with its interface to feel comfortable. There are plenty of tutorials available in the internet. Exploring Inkscape is a huge topic so I will skip this for now.

A simple one bedroom floor map done in Inkscape

Ultimately it does not matter which software you use or even if you prefer to plot [x,y] all from your head, our goal here is simple —

Get the set of paths we need to fill the topoJSON in the next step.

Upon the completion of the SVG, you will find the path details in the built-in XML editor in Inkscape. You should make sure all paths are in absolute values, i.e. commands are all in CAPITAL letter (for example M, H, V, Z, L). If they are not, you probably want to consider using the online tool in the optional step 2.0.2 below:

XML edition: the value of “d” is what you are after

STEP 2.0.1 (Optional)

After drawing the shapes and exporting them to SVG format, as a good practice, don’t forget to use SVGOMG or similar online SVG linter to clean the file.

With SVGOMG, the file is compressed from 1.2kb to 350b

STEP 2.0.2 (Optional)

Personally, I’d like to avoid any floats and keep the numbers as an integer unless it is unavoidable if the shape is curvy. If you are like me, you may also want to visit SvgPathEditor.

SvgPathEditor allows you to paste SVG path(s), then you can scale, round and edit each of them separately to ensure their relation stays intact after the rounding. Additionally, it also allows you to convert the commands between absolute (M, Z, L, H, V) and relative (m, z, l, h, v). All the changes you performed there will be immediately reflected on the right side of the screen.

SvgPathEditor is a good online tool to fine-tune SVG paths or debug if lines are not aligned

STEP 2.1 Convert path to correct format

In previous step, you collected all the paths which is similar to this string:

M 59, 94 H 1140 V 1097 H 59 Z

Both geoJSON and topoJSON do not support paths directly but the following 7 geometry types:

  • Point
  • LineString
  • Polygon
  • MultiPoint
  • MultiLineString
  • MultiPolygon
  • GeometryCollection

In order to make use of the paths you collected, you need to convert them into (Multi)Point, (Multi)Polygon or (Multi)LineString depending on what shape it is. Their names are quite self explanatory, the arc for Point is [[x,y]], for LineString is [[x0,y0], [x1,y1]] and for Polygon is [[x0,y0], [x1,y1]… [x0,y0]].

For the example floor map you are working on, you’d want to convert all paths into polygons. There are free online converters in the internet, like Path to Polygon Converter, which can be helpful. Another converter I highly recommend is this one (https://pjrclarke.github.io/SVG_path_to_polygon_JSON/) which allows you to convert the format even directly from Inkscape XML editor. Otherwise, you can also write your own function for the Path-Polygon conversion.

Using the above example path, after conversion it should be like:

[[59, 94], [1140, 94], [1140, 1097], [59, 1097], [59, 94]]

It is important that if a SVG path does not close itself explicitly, you need to manually do it on your own — make sure the 1st and the last index of the arc are identical.

STEP 2.2 Create a topoJSON file

Now that you have a collection of arrays which contains every coordinate you need for the floor map, the next step is to create the topoJSON file.

In topoJSON, there are 3 fields required: “type”, “objects” and “arcs”. The value of “type” is always “Topology”, the “objects” is an object containing a set of keys and their associated object, and lastly “arcs” which accepts a nested array to store all paths which are used inside “objects”. Apart from these fields, you can also add “bbox” and “transform”, depending on your needs.

You are highly recommended to check TopoJSON format specification when establishing your topoJSON file.

Return to the floor map you created in Inkscape, now you should first put all the polygon arrays inside “arcs”. Then, you will create objects and use those arrays for them.

There is no absolute way to organise the data, here I will divide the floor map into 3 objects — “floor”, “areas” and “entrances”. The completed topoJSON should look like this:

{
"type": "Topology",
"objects": {
"apartment": {
"type": "GeometryCollection",
"geometries": [
{
"type": "Polygon",
"arcs": [[0]],
"properties": {
"id": 0,
"type": "floor"
}
}
]
},
"areas": {
"type": "GeometryCollection",
"geometries": [
{
"type": "Polygon",
"arcs": [[1]],
"properties": {
"id": 2,
"type": "living room"
}
},{
"type": "Polygon",
"arcs": [[2]],
"properties": {
"id": 2,
"type": "bathroom"
}
},{
"type": "Polygon",
"arcs": [[3]],
"properties": {
"id": 3,
"type": "utility room"
}
},{
"type": "Polygon",
"arcs": [[4]],
"properties": {
"id": 4,
"type": "bedroom"
}
},{
"type": "Polygon",
"arcs": [[5]],
"properties": {
"id": 5,
"type": "kitchen"
}
},{
"type": "Polygon",
"arcs": [[6]],
"properties": {
"id": 6,
"type": "hallway"
}
}
]
},
"entrances": {
"type": "GeometryCollection",
"geometries": [
{
"type": "Polygon",
"arcs": [[7]],
"properties": {
"id": 2,
"type": "main entrance"
}
},{
"type": "Polygon",
"arcs": [[8]],
"properties": {
"id": 3,
"type": "bedroom door"
}
},{
"type": "Polygon",
"arcs": [[9]],
"properties": {
"id": 4,
"type": "utility room door"
}
},{
"type": "Polygon",
"arcs": [[10]],
"properties": {
"id": 5,
"type": "bathroom door"
}
}
]
}
},
"arcs": [
[[1, 1], [1082, 1], [1082, 1004], [1, 1004], [1, 1]],
[[31, 577], [446, 577], [446, 683], [461, 683], [461, 982], [31, 982], [31, 577]],
[[793, 19], [1054, 19], [1054, 479], [793, 479], [793, 19]],
[[619, 262], [764, 262], [764, 481], [619, 481], [619, 262]],
[[31, 19], [446, 19], [446, 563], [31, 563], [31, 19]],
[[606, 496], [1055, 496], [1055, 982], [606, 982], [606, 496]],
[[461, 19], [461, 982], [606, 982], [606, 243], [772, 243], [772, 19], [461, 19]],
[[486, 1], [486, 19], [587, 19], [587, 1], [486, 1]],
[[446, 119], [446, 197], [461, 197], [461, 119], [446, 119]],
[[606, 324], [606, 417], [619, 417], [619, 324], [606, 324]],
[[772, 101], [772, 184], [793, 184], [793, 101], [772, 101]]
]
}

STEP 3 — Writing script

With the data file ready, finally, it is time that you can start calling the functions from D3.js and topoJSON to see the effect.

STEP 3.0 Prepare a HTML file

During the installation, you probably have your index.html ready (if not, you should create one now) and it looks similar to the following block:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>Interactive Floor Map</title>
</head>
<body>
<-- It is up to you if you want to hard code a DIV element here-->
<div id="svg_container"></div>
<script src="./script.js" type="module"></script>
</body>
</html>

STEP 3.1 Declare core constants

In script.js, make sure both D3 and topoJSON-client are imported.

import "./lib/d3.v7.js";
import "./lib/topojson-client.js";

Below the imports, declare a constant “CANVAS”, which is an object to hold 2 properties — width and height of the SVG you are going to create.

const CANVAS = {
w: 1000,
h: 1000
};

Then, you will create another constant “data”, to store the value from fetching the json file. You can use either D3 fetch or native fetch API, or even XMLHttpRequest to do the fetching.

As a good practice, remember to chain a catch for any fetching failures as well.

const data = await d3
.json(`./one_bedroom.json`)
.catch(e => console.error(e.name));

Thanks to topoJSON we don’t need plot [x,y] like real coordinates for mapping. However, D3.js does not read topoJSON directly. Luckily you can easily convert it back to geoJSON with the build-in method topojson.feature(dataset, key) provided by topoJSON-client.

Since one-bedroom.json contains 3 objects inside “objects”, you will need to iterate them all.

// Create an empty object for storing data
const geoData = {};

// Get the array of ["apartment", "areas", "entrances"]
const arrOfKeys = Object.keys(topoData.objects);

// Loop through arrOfKeys and create a key for geoData to store the geojson info for that key
arrOfKeys.forEach(key => {
geoData[key] = topojson.feature(topoData, key);
})

STEP 3.2 Create D3 geoPath [important!]

After you fetched the data and converted to a format that D3.js can read, there is an important step to allow D3.js to parse the data to SVG paths — to create an instance of d3.geoPath, a generator which takes a geometry object and generates SVG path data.

The geographic path generator, geoPath, takes a given GeoJSON geometry or feature object and generates SVG path data string or renders to a Canvas. Paths can be used with projections or transforms, or they can be used to render planar geometry directly to Canvas or SVG. (Paths | D3 by Observable)

With d3.geoPath, more functionalities can be achieved, for example projections, transforms which can create an effect like panning and zooming.

// An identity to implement d3.projection methods
const d3Identity = d3.geoIdentity();

// This method sets the projection's scale to fit the object in the center of the given size.
const d3Projection = d3Identity
.fitSize([canvas.w, canvas.h], geoData["apartment"]);

// Pass the projection to the generator
const d3Path = d3.geoPath(d3Projection);

STEP 3.3 Draw the SVG and paths

Upon the setup of d3.geoPath and data fetched and stored, what is left is to create the SVG and its attributes.

// Create the SVG element
const svgContainer = d3
.select("#svg_container")
.append("svg")
.attr("viewBox", `0 0 ${CANVAS.w} ${CANVAS.h}`)
.classed("floormap", true)

// Create 3 g elements based on the keys of the "objects"
const groups = svgContainer
.selectAll("g")
.data(arrOfKeys)
.enter()
.append("g")
.attr("class", (d)=>d)

// Create all paths for the separate g element
const assets = groups
.selectAll("path")
.data(d=> geoData[d]?.features)
.enter()
.append("path")
.attr("d", d3Path)

Once you drew the SVG with D3.js and visit the browser.. however, it is like this?!

All black on screen?!

By default, SVG elements are with { fill: black }. One way to fix this is to carry on with the chain above to add basic styling using D3 so to see the paths just created. Sometimes this method is necessary when the value is dynamic. Otherwise, it is a better practice to style DOM elements (including SVG) in the stylesheet.

STEP 3.4 Update the stylesheet

Before creating the CSS file, remember to add <link> to index.html:

<head>
...
<link rel="stylesheet" href="./styles.css" type="text/css" />
</head>

To keep things simple, here is styles.css:

/* Reset default from UAs */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
display: grid;
place-items: center;
height: 100vh;
}

#svg_container {
width: 80vw;
height: 100%;
}

.floormap {
width: 100%;
height: 100%;
}

.apartment {
fill: gray;
}

.areas, .entrances {
fill: white;

&>*:hover, &>*:active {
fill: orange;
}
}

STEP 4 — Double check in the browser

Now if you return to the browser, you should be able to see the SVG image just like the one you drew in Inkscape. When you hover over it, different assets will be highlighted like below:

Hovering over the one-bedroom floor map

Conclusion

You may wonder why to use D3.js if one has to draw the image by hands first. At first, indeed it may take longer time to complete the data entry when using topoJSON and D3; however, thanks to topoJSON, you can arrange the associated non-spatial properties in a more organised fashion.

Needless to mention D3.js provides much more features that this article did not cover, including panning and zooming, responsive markers/icons, etc. Once you have the topoJSON file, it is easy to integrate the map with other external data (like json/ csv/ xml), which will be much harder to achieve if it is drawn as an image and loaded on the document.

Besides all, you can even upgrade the map to 3-dimentional. It is totally feasible to integrate D3.js with Three.js to create a 3D interactive floor map and add more features into it.

There are some downsides too, when using D3.js for creating a floor map. Among other limitations, one is about the scalability that when the project is growing larger, the SVG performance can be concerning.

Have you ever tried to use D3.js or topoJSON before? How do you feel about these tools I mentioned above? Feel free to share your thoughts below :)

On the other hand, I tried my best to the accuracy of my information but if you find any parts in this article you may want to correct or supplement, please let me know as well by leaving comment here.

Thank you for reading!

External Links

D3 — Official website

GeoJSON — Official website

https://geojson.org/

TopoJSON — Official Github channel

https://github.com/topojson

--

--

Kami Lam

Lifelong learner. Interested in Astronomy, MBTI, Astrology, Psychoanalysis and Philosophy.