Adding a mini-map to your Konva canvas using react-konva
— JavaScript, Software Development — 3 min read
As part of building reciprocal.dev I’ve been working with Konva (and react-konva) to create an MVP of the user journey mapping tools in order to validate the idea.
Over time, the example map that I built for our MVP has become larger than the browser window and I’ve needed to introduce panning and zooming to allow the user to navigate this larger map.
With the introduction of moving the map it became crucial to give the user a means to locate themselves within the full map. To solve this problem I decided to include a mini-map.
The mini-map shows the entire map with an overlay of the user’s current view (what I’ll call the viewbox) so the user can see the position and zoom level of what they can currently see against the overall content on the canvas.
Interactivity can be added to the mini-map, allowing the user to update the position and zoom level of the current view without the need to scroll or pan.
Building the mini-map
The Reciprocal.dev UI layout is similar to Miro as it maximises the canvas space and has floating menu elements along the sides of the screen.
This UI layout means I need to render a second Konva stage in one of the floating menu elements which would show the full journey map and also show information such as zoom level and buttons to zoom in and out.
Laying out the entire map
I’m using a grid of predefined coordinates to position the screens on the full size map so I re-used that grid when laying out the mini-map as this meant I could ensure consistency between the two stages.
As the mini-map would only be for reference I didn’t want to render all the screens and connections that the full map displayed so I opted to just render a Rect
in the same dimensions as one of the screens in the main map which made it clear what it represented.
As I needed to render the full map for the mini-map I needed to know how big the entire map was in order to scale it down to fit the container. I did this by calling getClientRect
on the stage at a scale of 1
, this allowed me to use the returned width
value to calculate the scale I needed to render the mini-map at, based on the width of the container I was rendering it on in the UI.
Showing the user’s current view
With the screens added to the mini-map I then needed to represent the sub-section of the full map that the user could see in their browser.
I did this by capturing the innerWidth
and innerHeight
values of the user’s browser and the x
and y
values returned from calling getClientRect
on the stage whenever a zoom or pan ended (for improved performance over rendering every time the full map moved).
I then used a React context to share the values between the full map and the mini-map, allowing me to update the mini-maps representation of the user’s view when these values were updated.
Adding interactivity
Once I had the mini-map showing the user’s current view against the full map I wanted to add a means for them across the map by dragging the viewbox, essentially doing a pan but via the mini-map instead.
I also wanted to have a means for the user to zoom in and out on the full map from within the mini-map UI via pressing buttons to zoom.
Updating the view position
In order to make the viewbox draggable I needed to implement a few props which allow the position of the viewbox Rect
to be captured and sent via a React context back to the full map to update it’s position.
This worked really well but one issue I encountered was the fact that the axis were backwards, so panning up on the mini-map would pan the full map down. To fix this I multiplied the position values by -1
in order to reverse their position.
Updating the zoom level
Hooking up the zoom buttons was relatively trivial, when the button is clicked I just multiply the current scale by 1.25 to zoom in and 0.25 to zoom out and then updated the scale value on the shared React context to update the full map.
Handling zooming by just increasing the scale wasn’t a perfect approach however, as the zoom would have on the viewbox’s current position meaning the zoom was always against the top-left of the user’s screen which was a bit jolting.
In order to make the zoom experience better I had to find the center of the stage against the current position at the current scale and then recalculate it at the new scale.
Summary
By adding a mini-map to my Konva canvas I’ve given users of Reciprocal.dev a way to navigate complex user journey maps with ease and understand what they’re currently looking against the context of the bigger picture.
There’s a little bit of logic needed to calculate the difference in scales between the two maps but aside from figuring out how to center the view when pressing the zoom buttons there was nothing too hard to get my head around.