In this article I am going to show you how to implement custom interactions in the Dygraphs javascript charting library specifically for mobile devices.
I was updating the backed reporting system of an exhibit this week that uses line graphs generated
by javascript to show real time stats regarding donations. I replaced the current charting library I was
using with Dygraphs and was very pleased. Dygraphs is a very capable and fast open source charting library.
Dygraphs comes with an out of the box HTML5 touch implementation for devices with touchscreens.
The problem with the out of the box implementation is that data points are not selectable.
In order to change how touches and clicks manipulate a dygraphs graph, the development team has exposed
what are called custom interactions. The custom interactions demo has a good general overview
for interactions with a mouse, but not touch.
After digging and digging, some experimentation I got point selection working on the default interaction
template with one finger touch. I tested this on an Ipad1, Ipad2, Android 2.2.3 Sony, Android 2.2.3 VM,
and Android 4 VM. Here's a demo:
OPEN IN NEW WINDOW
The Code
So in order to do this, you setup a custom interaction function to route the touchstart events through.
You can actually clone the default interaction model for other events you want to keep.
For further investigation you can see the
dygraph-interaction-model.js file on github .
So in order to re-route the touchstart event while keeping the default interaction model, in my dygraphs instantiation I have:
function init() {
// Create the chart
chart = new Dygraph(
document.getElementById("chartContainer"),
"userData.csv", // path to CSV file
{ ylabel: 'User Patience',
xlabel: 'Time',
hideOverlayOnMouseOut: false,
drawPoints: true,
animatedZooms:false,
fillGraph:true,
legend: "always",
interactionModel:{
mousedown: Dygraph.defaultInteractionModel.mousedown,
mousemove: Dygraph.defaultInteractionModel.mousemove,
mouseup: Dygraph.defaultInteractionModel.mouseup,
touchstart: newDygraphTouchstart,
touchend: Dygraph.defaultInteractionModel.touchend,
touchmove: Dygraph.defaultInteractionModel.touchmove
}
} // options
);
};
The important part is the interactionModel object. You'll see that I specify the default
interaction model for many of the events, except touchstart. We just create another javascript
function to route the touchstart events through.
My function in this example is called "newDygraphTouchstart". Here is that function.
Keep in mind this is just a copy of the default touchstart except the section under one touch where I changed three lines.
The function parameter "g" references the actual dygraph graph, so you can use the
dygraph api functions on it.
UPDATE: 4/30/2013 - I submitted a patch to dygraphs interaction model with what was previously seen below, and
Dan Vanderkam, the author of Dygraphs contacted me with a way better solution.
which can now be found below. I used strikethrough in the example below so you can see the original solution which is not recommended.
function newDygraphTouchstart(event, g, context) {
// This right here is what prevents IOS from doing its own zoom/touch behavior
// It stops the node from being selected too
event.preventDefault(); // touch browsers are all nice.
if (event.touches.length > 1) {
// If the user ever puts two fingers down, it's not a double tap.
context.startTimeForDoubleTapMs = null;
}
var touches = [];
for (var i = 0; i < event.touches.length; i++) {
var t = event.touches[i];
// we dispense with 'dragGetX_' because all touchBrowsers support pageX
touches.push({
pageX: t.pageX,
pageY: t.pageY,
dataX: g.toDataXCoord(t.pageX),
dataY: g.toDataYCoord(t.pageY)
// identifier: t.identifier
});
}
context.initialTouches = touches;
if (touches.length == 1) {
// This is just a swipe.
context.initialPinchCenter = touches[0];
context.touchDirections = { x: true, y: true };
// ADDITION - this needs to select the points
var closestTouchP = g.findClosestPoint(touches[0].pageX,touches[0].pageY);
if(closestTouchP) {
var selectionChanged = g.setSelection(closestTouchP.row, closestTouchP.seriesName);
}
g.mouseMove_(event);
} else if (touches.length >= 2) {
// It's become a pinch!
// In case there are 3+ touches, we ignore all but the "first" two.
// only screen coordinates can be averaged (data coords could be log scale).
context.initialPinchCenter = {
pageX: 0.5 * (touches[0].pageX + touches[1].pageX),
pageY: 0.5 * (touches[0].pageY + touches[1].pageY),
// TODO(danvk): remove
dataX: 0.5 * (touches[0].dataX + touches[1].dataX),
dataY: 0.5 * (touches[0].dataY + touches[1].dataY)
};
// Make pinches in a 45-degree swath around either axis 1-dimensional zooms.
var initialAngle = 180 / Math.PI * Math.atan2(
context.initialPinchCenter.pageY - touches[0].pageY,
touches[0].pageX - context.initialPinchCenter.pageX);
// use symmetry to get it into the first quadrant.
initialAngle = Math.abs(initialAngle);
if (initialAngle > 90) initialAngle = 90 - initialAngle;
context.touchDirections = {
x: (initialAngle < (90 - 45/2)),
y: (initialAngle > 45/2)
};
}
// save the full x & y ranges.
context.initialRange = {
x: g.xAxisRange(),
y: g.yAxisRange()
};
};
IOS Bonus
If you are still attentive after that mess; you deserve a BONUS! Lets discuss an IOS bonus in this example. I have made it so on
IOS the canvas is not highlighted when users touch. I didn't know this was possible when doing the
sample robot farm game.
You just set a series of CSS styles telling webkit to not highlight the canvas element. Awesome!
<style>
body {
padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
}
canvas {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline: none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0); /* mobile webkit */
}
</style>