2

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 a scaleExtent 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 use zoomIdentity.
  • d3-scale has a clamp() 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 (
  ...
squish
  • 846
  • 12
  • 24

0 Answers0