A key feature of the interactive DC Metro map on https://dcmetropro.com is clicking on station markers in the SVG map and dragging / zooming around. Here’s a quick rundown of what it took to make that happen!

You might want to play with the map to get an idea of what it does before reading on: https://dcmetropro.com.

Starting off slowly now

Step 1:

Do you have an SVG file or content or something? For DC Metro Pro, I had to create the SVG of the DC Metro Map by hand (which I describe in this other post). Exporting from Sketch resulted in a big pile of svg content, which I put into a JS file like this:

When creating the file, I took special attention to name the layers and elements in a way that could be identified in code. Here’s a glimpse of the actual SVG content:

The <g id="Stations"> element wraps all of the individual station dots on the map, and the id of each dot corresponds to a metro stop ( C05 is Rosslyn for all you WMATA fans out there).

This is a key component in turning the visual map into an interactive one, because now our visual pieces have some semantic meaning attached to them that we can leverage in the codez.

A little bit faster now

Step 2:

We have to get the SVG rendered into the DOM, and since we’re using React, we have to play by React’s rules.

So here is the React Metro Map component, which uses that big HTML/SVG string from before. We render it using React’s dangerouslySetInnerHTML property, which will take our string and drop it right into the DOM without having React parse or update it on re-renders. This allows us to use d3 to get a “handle” on the element. We also wrap it in a <g> tag (used to group elements) so we can handle some mouse trickery later on.

And finally, the snippet shows a little bit of the logic for sizing and resizing the svg element based on the window dimensions and the content size of the svg (the hardcoded viewport values are the Artboard dimensions from my Sketch file).

Side Note: When you read through tutorials and overviews of code, things can seem really confusing; it’s partially because explaining a process that’s inherintely non-linear in a linear fashion, like a blog post, is hard. The code didn’t look like this when it was first written. I went through several iterations to get to this result. So if you caught yourself wondering, “how did he know to wrap that element?”, I want you to know that at first I didn’t! I ran into bugs with mouse events and text highlighting later on and this was how I fixed those problems. There’s a lot of trial and error that doesn’t get represented in posts like this. Continuing on...

Hey-Hey-A-Hey

Step 3:

Finally the d3.js + React code you were promised in the title! What is that saying about preparation?

A good preparation takes longer than the delivery. - E. Kim Nebeuts

Who is E. Kim Nebeuts? 🤔. Well, that’s not the quote I was thinking of, but close enough.

A snippet about clicking elements in SVG:

The comments in the snippet above walk through what each piece does.

Why do I append a circle element?

As it turns out, the visual circle in the svg is very tiny, and a bigger hit area makes for a better UX. The sizing functions aren’t shown, but they position the circle centered over the g tag they were appended to, and the radius of the circle is increased for what I call “major” stations (they’re bigger dots on the map). These circles are what you see when you hover over a station on the map.

Jump Up and Shout Now!

Step 4:

Time to drag the SVG. Alright, so clicking seems to work (proof is on the website!). Now how do we add dragging in a way that integrates well with the clicking?

The conceptual layout is:

The top layer is really just a conceptual layer, the dots are placed in the map (so they track the elements when the map is dragged). Shown this way to visualize how the pointer events pass through.

You can think of the mouse events as passing through these layers until they hit something. The circles themselves have pointer events (but not they layer the circles are on), so if the mouse is not over a circle the event passes through to the next layer that has pointer events, which is not the visual map, but our draggable background.

What this means is that clicking the circles get priority over dragging, but it also means you can’t start a drag on a circle. I’m sure there’s a better way to do this, but at this time I’d like to pull out my Good Enough™ stamp of approval.

Yeah it is.

Alright, enough funny business, let’s look at the code for dragging.

We really lean on d3 here to do the heavy lifting. Trust me, I’ve written custom zoom+dragging logic before and it can get complicated 😉.

For the curious: What’s translateExtent do?

Those are just bounding coordinates for how far you’re able to drag the map. Directly from the d3.js docs it “Sets the translate extent to the specified array of points [[x0, y0], [x1, y1]], where [x0, y0] is the top-left corner of the world and [x1, y1] is the bottom-right corner of the world.”

Conclusion

Yes, the sub headers were riffing on The Isely Brothers, Shout. Hat tip if you noticed!

So that was a little peek at how clicking + dragging svg in a react component can work. I hope you were able to glean something useful in how to augment the SVG and bend it to your will with d3. If you have a question, please drop it in a comment below!

If you enjoyed this article, you might also enjoy my musings on Twitter.

And finally, here’s one more link for the website that uses these techniques: https://www.dcmetropro.com.

Have a great day!