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 whenscrollToView
is called. Default: 600 ms.animationStepFunction: (progress: number) => number
Easing function to for the animation. Default: d3'seaseCubicOut
rescale: (viewbox: Viewbox, gestureData?: ChartGestureEvent) => Viewbox
TheonGesture
event handler created byusePannable
will automatically move the viewbox by in response to drag and swipe gestures. Therescale
option can be used to apply constraints to this automatic movement. The default is the identity functionview => view
. The same function is also applied aftersetView
andscrollToView
. 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 byscrollToView
).
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: