Creating interactive infographics with plain Javascript (Part-two)
Recap
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.
Objective
Let’s create navigation features to browse a large canvas.
Introduction
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?
Concept
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:
- Scroll (by X and Y scrollbar)
- Pan (by cursor’s relative position)
- Grab and drag (by cursor’s movement)
- Zoom (in or out)
Getting started—prepping the canvas
Create a variable called canvas
to reference the map. This is the parent container.
var canvas = document.getElementById(“parentContainer
”);
Setting up the mouse buttons. Use the left mouse button for “grabbing and dragging” navigation. Use 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);
Scroll
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.
Preventing accidental interaction
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
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 pan canvas.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
andevent.clientY
provides the current coordinates of the mouse cursor.
Grab and drag
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 theWheelEvent.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 ... }; }
- assign the left mouse button with
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 functionelementDrag
to drag the canvas.mouseDownBoolean
qualifies the action only if the left mouse button is also held-down (as set byhandlerGrab
).mouseup
detects the end of a three-step sequence and callelementDragclose
to reset the UI state.
Detecting all three actions as a sequence:
1. Press & hold the left mouse button 2. Grab and drag the cursor some X and Y distances 3. Release the mouse button
Within the code blockif (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 );
- Manage your variables
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; };
Don’t leave out the minor details
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
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){ ... } } }
}
- It’s a good practice to prevent accidental
wheel
interaction. We should also differentiate windowscroll
actions fromzoom
actions. Use a booleanzoomAllow
to toggle between zoom and scroll “mode”. - Use
event.wheelDelta
to set the threshold for counting a zoom. - Maintain a variable
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. - Use a conditional statement
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
andprevRatioY
calculates the scroll ratio (before a zoom).newX
andnewY
recalculates the new relative scroll values (after a zoom).scrollTo
tells the browser to go back to the previous relative position.
Next Steps
We have enhanced the view engine with 4 navigation features.
Let’s take it to the next level with a navigation gadget.
SOURCE: https://hackernoon.com/how-to-create-a-navigation-ui-bac94a9e51fa
WRITE BY