Handling Dragging and Gestures

Hypocube aims to make it easy to change the chart viewbox in response to basic mouse and touch gestures, including swipe and pinch zoom. To see how this works, consider the following example:

const { onGesture, view, scrollToView } = usePannable([-10, -10, 20, 20]);
return (
<React.Fragment>
<p>
Left: {view.xMin.toFixed(2)} | Right: {view.xMax.toFixed(2)} | Top:{' '}
{view.yMax.toFixed(2)} | Bottom: {view.yMin.toFixed(2)}
</p>
<p>
<button type="button" onClick={() => scrollToView(view.zoom(0.5))}>
Zoom Out
</button>
<button type="button" onClick={() => scrollToView(view.zoom(2))}>
Zoom In
</button>
</p>
<TheChart isCanvas={isCanvas} onGesture={onGesture} view={view} />
</React.Fragment>
);

Left: -10.00 | Right: 10.00 | Top: 10.00 | Bottom: -10.00

Hypocube aims to make it easy to change the chart viewbox in response to basic mouse and touch gestures, including swipe and pinch zoom. To see how this works, consider the following example:

As you might have guessed, TheChart is not the interesting part here, it merely renders a bunch of concentric diamonds centered on the origin. This gives us a sense of what's happening. Try clicking and dragging, or, if you have a touch device, try swiping or pinch-zooming the chart.

The "magic" is made possible by the usePannable hook, which works a bit like useState. The initial view (in the form [xMin, yMin, width, height]) is passed as an argument, and the hook returns some stuff that leds us read and manipulate the view. view and onGesture are just passed down to the Chart as props. We can also manipulate the view directly with setView and scrollToView. To do that, we first have to discuss how views are handled within Hypocube.

Manipulating the Viewbox

When we create a chart, we generally pass a view prop consisting of an array of four numbers: [xMin, yMin, width, height]. Under the hood, Hypocube converts this to a Viewbox object that contains this data, and other useful properties (like the xMax). It also has a few useful methods for manipulating the view. (Note: Viewbox objects are immutable: the methods that return Viewboxes actually create new ones, leaving the original in tact).

To create a Viewbox, use the createViewbox function, which can accept: (1) an array in the form above, or the four numbers as separate arguments. You can also use createViewboxFromData, which accepts an array of Points, and will return the smallest viewbox that fits around all of them.

Viewbox properties

Props are public, but do not manipulate them directly:

xMin, xMax, yMin, yMax, width, height

Viewbox methods that return viewboxes (chainable)

setEdges({ xMin: number, yMin: number, xMax: number, yMax: number }) => Viewbox Replaces any edge (which is not undefinied), leaves the others in tact.

panX(distance: number) => Viewbox Move the viewbox horizontally to the right. To move left, use a negative number.

panY(distance: number) => Viewbox Move the viewbox vertically down. To move up, use a negative number.

zoom(factor: number, anchor?: [x, y]) => Viewbox Zoom in (factors greater than 1) or out (factors less than 1). anchor defaults to the center of the current viewbox.

interpolate(final: Viewbox, progress: number) => Viewbox. Returns a viewbox interpolated between final and the current view. progress would normally be between 0 and 1.

bound(boundingBox: Viewbox) => Viewbox. Slide the current viewbox to fit within the bounding box. If the current view is too big, it will shrink just enough to fit.

constrainZoom({ maxZoomX: number, maxZoomY: number }): Viewbox. Zoom the current viewbox out until its width is at least maxZoomX or its height is at least maxZoomY. Used to prevent the viewbox from getting zoomed in too far. To keep the viewbox from being zoomed out too far, use bound.

Other Viewbox methods

toPath(): [number, number][] Returns four points representing a rectangle around the current viewbox.

isEqual(test: Viewbox): boolean Returns true when the current viewbox is equivalent to (all four sides are the same as) test.

pointsWithinX(points: Point[]) => Point[]. Filter out all points that don't fall within the viewbox on the x-axis.

pointsWithinY(points: Point[]) => Point[]. Filter out all points that don't fall within the viewbox on the y-axis.

Viewbox manipulation examples

With these methods we can do some useful things. For example:

Panning example

Pan the viewbox right, but keep it within a bounding box. Then, rescale the y-axis to fit around the data in the new view.

const bounded = view.panX(view.width).bound(boundingBox);
const fitted = createViewboxFromData(next.pointsWithinX(data));
return fitted
? next.setEdges({
yMin: 0,
yMax: fitted.yMax + 10,
})
: bounded;

Pan to end example

Pan the viewbox to the right end of the bounding box, keeping the same y-axis and width:

return view.setEdges({
xMax: boundingBox.xMax,
xMin: view.xMax - view.width,
});

Zooming example

Zoom in by 200% on the xAxis, within reason, but keep the y-axis the same:

return view
.zoom(2)
.constrainZoom({
maxZoomX: MAX_ZOOM_X,
})
.setEdges({
yMin: view.yMin,
yMax: view.yMax,
});

usePannable

Now that we understand how to manipulate viewboxes, let's see how to use them in practice.

The usePannable hook acts like React's useState, but for viewboxes.

const { view, setView, scrollToView, onGesture, isPanning } = usePannable(
[0, 0, 10, 10],
options
);

The onGesture return value can be plugged directly into <Chart />

const { view, setView, scrollToView, onGesture, isPanning } = usePannable(
[0, 0, 10, 10],
options
);
<Chart view={view} onGesture={onGesture}></Chart>;

while setView and scrollToView are used to otherwise manipulate the viewbox (e.g., with buttons). isPanning is a boolean that tells you whether the chart is currently in motion.

Options

The following options are passed to usePannable as the second argument:

  • animationDuration: number Total time for animation when a swipe gesture occurs or when scrollToView is called. Default: 600 ms.

  • animationStepFunction: (progress: number) => number Easing function to for the animation. Default: d3's easeCubicOut

  • rescale: (viewbox: Viewbox, gestureData?: ChartGestureEvent) => Viewbox The onGesture event handler created by usePannable will automatically move the viewbox by in response to drag and swipe gestures. The rescale option can be used to apply constraints to this automatic movement. The default is the identity function view => view. The same function is also applied after setView and scrollToView. If desired, the entire ChartGestureEvent (see below) is also passed as a second argument when this is function is called in response to a gesture event (it is not passed if triggered by scrollToView).

onGesture

Similar to Chart pointer events, Hypocube rescales and remaps the event associated with gestures to provide useful information about the chart. While in most cases it will be enough to plug the onGesture function returned by usePannable directly into the chart, it is also possible to compose your own gesture event handlers. The ChartGestureEvent objects created by Hypocube have the following properties:

A function to update the viewbox based on its current value.