im building a chart and need to make to allow the user to pan and zoom.
I am using react-native-gesture-handler
, d3-zoom
, d3-scale
and @shopify/react-native-skia
.
So far, I can pan and zoom, however I can pan left into negative values and I can also pan right into infinity basically. So I basically need to find a nice way to limit the amount a user can translate.
Somethings I have tried already:
d3-zoom
would do this all for me, I think it has ascaleExtent
function where you can set the bounds, but it cannot be used with React Native, as you need to select the svg's path elements via CSS document selector, which isn't possible in RN. I am stuck to only being able to usezoomIdentity
.d3-scale
has aclamp()
function that is close to what I'd like, it basically ensures that the axis elements stay within range, the problem with this is if you pan left or right, it rescales the axis array until you have 1 element left, which is weird behaviour. I need to allow axis to go into negative values but not so much that the entire graph is no longer visible.
...
import { Canvas, Group, rect, useFont } from '@shopify/react-native-skia';
import { scaleLinear, scaleTime } from 'd3-scale';
import { zoomIdentity } from 'd3-zoom';
import { useRef, useState } from 'react';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
...
function Chart({
width,
height,
data,
pauses,
hrMin,
hrMax,
zones,
paddingLeft = 30,
paddingRight = 30,
paddingTop = 40,
paddingBottom = 25,
xAxisLabelTicks = 5,
showChartBounds = false,
showBackButton = false,
fullScreenMode = false,
isShareMode = false,
isHideYAxis = false,
onBackPress,
}: ChartProps) {
const theme = useTheme();
const zoomIdentityRef = useRef(zoomIdentity);
const [translationX, setTranslationX] = useState(0);
const [scale, setScale] = useState(1);
const colours = [
theme.colors.t0,
theme.colors.t1,
theme.colors.t2,
theme.colors.t3,
theme.colors.t4,
theme.colors.t5,
];
// Allow some space for the title
const canvasHeight = fullScreenMode ? height - CHART_TITLE_HEIGHT : height;
// Drawable bounds of the chart.
const left = paddingLeft;
const right = width - paddingRight;
const top = paddingTop;
const bottom = canvasHeight - paddingBottom;
// Axis data.
const valuesX = data.map(({ x }) => x.getTime());
//Need to consider the pauses while scaling
const pausesXValues = pauses.map((pause) => pause.getTime());
const minX = Math.min(...valuesX, ...pausesXValues);
const maxX = Math.max(...valuesX, ...pausesXValues);
// Scale + Rescale/Translate X
let scaleX = scaleTime()
.domain([minX, maxX])
.range([left - 1, width + 1]);
scaleX = zoomIdentityRef.current
.translate(translationX, 0)
.scale(scale)
.rescaleX(scaleX);
// Scale + Scale/Translate X time ticks
let scaleTimeTicks = scaleLinear()
.domain([minX, maxX])
.range([30, !fullScreenMode ? right - 15 : right - 25]);
scaleTimeTicks = zoomIdentityRef.current
.translate(translationX, 0)
.scale(scale)
.rescaleX(scaleTimeTicks);
// Scale Y
const scaleY = scaleLinear().domain([hrMin, hrMax]).range([bottom, top]);
// Pan handler
const panHandler = Gesture.Pan()
.onUpdate((event) => {
const { translationX: x } = event;
setTranslationX((prev) => prev + x);
})
.runOnJS(true);
// Pinch handler
const pinchHandler = Gesture.Pinch()
.onUpdate((event) => {
const { scale: pinchScale } = event;
setScale(pinchScale);
})
.runOnJS(true);
if (!scaleX || !scaleTimeTicks || !scaleY) {
return null;
}
return (
...