Navigation UI for large canvases
This article is a five-part series on creating interactive infographics with plain Javascript.
So far, we’ve designed a schema and a view engine. However, the usability of an infographics design is still limited by the size of the browser window. To support a larger canvas, we can find ready answers from native Javascript.
Let’s create navigation features to browse a large canvas.
Think of infographics as a forest. With a wealth of information to present, designers may be tempted to squeeze the entire forest into a browser window. Texts, shapes, and images become too meshed up for the human eyes to see comfortably.
Going the other direction, designers may instead slice a layout into smaller parts and present each as separate pages. Visitors may see the trees but lose the forest.
Is it possible to see both the trees and the forest at the same time? Why not let users adjust the level of details as needed?
Imagine holding a “looking glass” over a map. Instead of shifting the looking glass to examine different parts of the map, we move the map itself. This effect is achieved through a CSS feature called overflow
. By manipulating its properties, we can create 4 powerful navigation effects:
Create a variable called canvas
to reference the map. This is the parent container.
var canvas = document.getElementById(“parentContainer
”);
Scroll vertically and horizontally
The first technique scroll
is created with a simple CSS:
div {overflow:scroll;}
overflow
controls how content is displayed. Think of this property as an imaginary overlay with a cutout window in its middle. overflow
is our virtual looking glass.
Beneath the glass is the forest (i.e. canvas
) itself. scroll
tells the browser to move the underlying canvas horizontally or vertically (or both via a trackpad). Only exposed parts of the forest can be observed through that window cutout.
We don’t want users to unintentionally move the canvas
when they are just browsing something else. You can prevent that with a basic HTML button to toggle between the UI states of scroll
and hidden
. You’ll probably want hidden
to be the default CSS behaviour (i.e. lock X and Y scroll).
canvas.style.overflow = "hidden";
When visitors are ready to explore the canvas
, let them toggle to scroll
.
canvas.style.overflow = "scroll";
scroll
is a default browser feature. You won’t have to write any custom handlers.
Let’s move on to the next navigation feature.
Pan by holding down the right mouse button and moving the mouse simultaneously
Regardless of canvas
size, it is possible to explore an entire canvas quickly without running off the mousepad. This feature is activated by holding down the right mouse button and moving the mouse simultaneously.
Use the position of the mouse cursor to calculate the new relative scroll value (see bold).
canvas.addEventListener("mousemove", handlerMove, false);function handlerMove(event) {if (event.which == 3) { // use right button to pancanvas.scrollTo( event.clientX , event.clientY );}}
mousemove
listens to any mouse buttons being pressed (and not released).event.which == 3
detects the the right mouse button. event.which == 1
detects the left. event.which == 2
detects the middle scroll button.event.clientX
and event.clientY
provides the current coordinates of the mouse cursor.Setting up the mouse buttons. Let’s reserve the right mouse button for “panning” navigation. As the browser shows a contextual menu by default for the right mouse button, we’ll need to instruct the browser to give us control like so:
canvas.addEventListener(‘contextmenu’,function(event) {event.preventDefault();},false);
Two concepts work in tandem to create this effect.
Action and reaction. Let’s place the canvas
on a tabletop and imagine the mouse cursor as our finger. When we hold a finger against the canvas and push it around, our finger is said to have moved a certain X and Y distance. Similarly, the canvas moves by the same X and Y distance. The finger has caused an action. The reaction is mirrored by how much the canvas has also moved.
The mouse cursor delta. We want to mirror this distance as a reaction on the canvas. In other words, the canvas should move by exactly how much the cursor has moved. Let’s call this value the mouse cursor delta.
Calculate with a surprisingly simple formula:
start coordinates - end coordinates
The result should update to a new scroll value in real time. Looking through our imaginary window cutout, a visitor would feel as if she’d dragged the canvas with her mouse cursor.
Tip: “mouse delta value” is just a label and is not the same as the _WheelEvent.deltaX_
browser event.
Implementation. Add an event listener mousedown
to detect the pressing of the left-mouse button.
canvas.addEventListener("mousedown", handlerGrab, false);
function handlerGrab(event) {if (event.which == 1) {mouseDownBoolean = true;// Find the initial scroll value// Capture the initial mouse cursor position...};}
event.which == 1
.mouseDownBoolean
tells other related functions that the left-mouse button is currently down or not.Add the mousemove
and mouseup
listeners in tandem to drag and release the canvas.
canvas.addEventListener("mousemove", elementDrag, false);
function elementDrag(event) {if (mouseDownBoolean){// Calculate the delta after the mouse cursor has moved// scroll to the new position...}};
canvas.addEventListener("mouseup", elementDragclose, false);
function elementDragclose() {if (mouseDownBoolean){mouseDownBoolean = true; // "releases" the drag// change the cursor icon dynamically}};
mousemove
triggers a custom function elementDrag
to drag the canvas.mouseDownBoolean
qualifies the action only if the left mouse button is also held-down (as set by handlerGrab
).mouseup
detects the end of a three-step sequence and call elementDragclose
to reset the UI state.Detecting all three actions as a sequence:
1. Press & hold the left mouse button2. Grab and drag the cursor some X and Y distances3. Release the mouse button
Within the code block if (event.which == 1) {…}
, we can find the start scroll value with getBoundingClientRect()
:
var distanceToTop = canvas.getBoundingClientRect().top;var distanceToLeft = canvas.getBoundingClientRect().left;
and capture the initial mouse cursor position (before it moves):
var posXdelta = 0, posYdelta = 0, posX = 0, posY = 0;
myBox = e || window.event;posX = myBox.clientX - distanceToLeft;posY = myBox.clientY - distanceToTop;
Within the code block elementDrag(event),
find the end coordinates and calculate the resultant delta value:
posXdelta = pos3 - ( myBox.clientX - distanceToLeft );posYdelta = pos4 - ( myBox.clientY - distanceToTop );
and command the browser to “scroll” to its new relative position:
var newX = canvas.scrollLeft + posXdelta;var newY = canvas.scrollTop + posYdelta;canvas.scrollTo( newX , newY );
mouseDownBoolean
, pos1
, pos2
, pos3
, pos4
, distanceToTop
, distanceToLeft
such that the above mentioned functions can access them.Tip: If you are showing a node element as an image with the _<img>_
tag, then prevent it from being accidentally dragged out of the window.
itemElementName[i].ondragstart = function(){ return false; };
Wouldn’t it feel weird to drag something with a pointy arrow cursor? How about changing it to a grab
cursor (i.e. hand icon) within the code block elementDrag(event)
?
canvas.style.cursor = "-webkit-grab";// supports Chrome, Safari and Opera
Better still, add an animation effect grabbing
whenever the mouse button is being pressed to show “grabbing in action”.
canvas.style.cursor = "-webkit-grabbing";
Remember to reset the UI state at code block elementDragclose()
as soon as the mouse button is released.
Zoom with mouse-scroll button
Use the scroll button for zooming. To zoom in and see the details, scroll up. To zoom out and see the bigger picture, scroll down. Use wheel
like so:
canvas.addEventListener("wheel", handlerWheel);
zoom
by manipulating the numerical value.
canvas.style.zoom = 1;
Here’s the structure:
canvas.addEventListener("wheel", handlerWheel);
function handlerWheel(event) {
if (zoomAllow) {
if (event.wheelDelta === 100) {
zoomLevel = zoomLevel + 0.1;
if (zoomLevel > 3){
...
}
} else if (event.wheelDelta === -100) {
zoomLevel = zoomLevel - 0.1;
if (zoomLevel < 0.3){
...
}
}
}
}
wheel
interaction. We should also differentiate window scroll
actions from zoom
actions. Use a boolean zoomAllow
to toggle between zoom and scroll “mode”.event.wheelDelta
to set the threshold for counting a zoom.zoomLevel
to zoom incrementally and smoothly. The smaller the value, the finer the zoom. Decimals are accepted. Consider adding an easing behaviour to animate smoothly.if (zoomLevel > myNumber)
to set a max and min zoom range.Maintaining focus. The absolute X and Y scroll value will remain the same after a zoom. This will “jump” the canvas to a new relative scroll position with every zoom. Let’s write a method to “remember” the original focus area.
var prevRatioX = (canvas.scrollLeft) / (canvas.scrollWidth - canvas.clientWidth);var prevRatioY = (canvas.scrollTop) / (canvas.scrollHeight - canvas.clientHeight);
var newX = prevRatioX * (canvas.scrollWidth - canvas.clientWidth);var newY = prevRatioY * (canvas.scrollHeight - canvas.clientHeight)
canvas.scrollTo( newX , newY );
prevRatioX
and prevRatioY
calculates the scroll ratio (before a zoom).newX
and newY
recalculates the new relative scroll values (after a zoom).scrollTo
tells the browser to go back to the previous relative position.We have enhanced the view engine with 4 navigation features.
Let’s take it to the next level with a navigation gadget.
Part-one sets the foundation for designing interactive infographics.
Part-two → You’re now here.
Part-three adds a dynamic mini-map to enhance navigation.
Part-four adds an inline UI to access layered content.
Part-five demonstrates why it is so easy to create UIs with a human touch.
If you enjoyed this story, you can find more at Pageii Studio.