Skip to content

useChartPressState doesn't work well when having close points on a chart #637

Description

@janrabek

Prerequisites

Describe Your Environment

What version of victory-native-xl are you using?
41.20.2

What version of React and React Native are you using?
19.1.0, 0.81.5

What version of Reanimated and React Native Skia are you using?
Reanimated: 4.1.3
React Native Skia: 2.2.12

Are you using Expo or React Native CLI?
Expo

What platform are you on? (e.g., iOS, Android)
iOS

Describe the Problem

When I use "useChartPressState", it works fine for dots that have a large distance between each other, but when they have a small distance, I have to slide my finger very much to the left to target those points. Before my update to Expo 54 (and the update to the new architecture), the hook worked well and always focused the exact point I want to focus with my finger. When I switch apps in the video you can see it a bit (unfortunately finger touches are not visible in iPhone screen recordings:

https://youtube.com/shorts/roU8TBINt6o?feature=share

This is the code:

export default function WeightChartCard({
  weightPoints,
}: {
  weightPoints:
    | {
        x: string;
        y: number;
      }[]
    | undefined;
}) {
  const [selectedPeriod, setSelectedPeriod] = useState("Letzte 30 Tage");

  const allTransformedData = useMemo(() => {
    return weightPoints?.map((point) => ({
      date: new Date(point.x).getTime(),
      weight: point.y,
    }));
  }, [weightPoints]);

  // Filter data based on selectedPeriod
  const filteredData = useMemo(() => {
    if (!allTransformedData) return [];
    const startDate = getStartDate(selectedPeriod);
    if (!startDate) {
      return allTransformedData; // 'Seit Beginn' selected, show all data
    }
    const startTime = startDate.getTime();
    return allTransformedData.filter((point) => point.date >= startTime);
  }, [allTransformedData, selectedPeriod]);

  const pointInnerRadius = 5 - Math.min(filteredData.length / 75, 1) * 2;
  const pointOuterRadius = pointInnerRadius * 1.5;

  const font = useFont(uncutSans, 13);

  // Calculate min and max weights for domain based on filteredData
  const weights = filteredData?.map((d) => d.weight) ?? [];
  const minWeight = weights.length > 0 ? Math.min(...weights) : 80;
  const maxWeight = weights.length > 0 ? Math.max(...weights) : 90;
  const yDomain: [number, number] = [minWeight - 1, maxWeight + 1];

  const { state: chartPressState, isActive: isChartPressActive } =
    useChartPressState({ x: 0, y: { weight: 0 } });

  const selectedWeight = useDerivedValue(
    () =>
      chartPressState.y.weight.value.value
        ? `${chartPressState.y.weight.value.value.toFixed(1)}kg`
        : "",
    [chartPressState],
  );

  // Format timestamp to date string
  const formatDate = (timestamp: number) => {
    const date = new Date(timestamp);
    return `${date.getDate()}. ${GERMAN_MONTH_ABBREVIATIONS[date.getMonth()]}`;
  };

  const selectedDate = useDerivedValue(() => {
    const date =
      chartPressState.x.value && chartPressState.x.value.get()
        ? new Date(chartPressState.x.value.get())
        : null;
    return date
      ? `${date.getDate()}. ${GERMAN_MONTH_NAMES[date.getMonth()]}:`
      : "";
  }, [chartPressState]);

  return (
    <Card>
      <CardTitle>Gewichtsverlauf</CardTitle>
      <View style={styles.dropdownContainer}>
        <WeightChartDropDown
          selectedPeriod={selectedPeriod}
          onSelect={setSelectedPeriod}
        />
      </View>
      {filteredData.length === 0 ? (
        <NoResultsView />
      ) : (
        <View style={styles.chartContainer}>
          <CartesianChart
            data={filteredData}
            xKey="date"
            yKeys={["weight"]}
            domain={{ y: yDomain }}
            domainPadding={{ left: 10, right: 10 }}
            chartPressState={chartPressState}
            xAxis={{
              font,
              formatXLabel: formatDate,
              labelColor: "#667285",
              lineWidth: 0,
              tickCount: 3,
            }}
            yAxis={[
              {
                font,
                formatYLabel: (value) => `${value}kg`,
                labelColor: "#667285",
                lineColor: "#CDD6E9",
              },
            ]}
          >
            {({ points, chartBounds }) => (
              <React.Fragment
                key={`${points.weight.length}-${chartBounds.bottom}-${selectedPeriod}`}
              >
                <Area
                  points={points.weight}
                  y0={chartBounds.bottom}
                  curveType="cardinal"
                  animate={{ type: "timing", duration: 300 }}
                >
                  <LinearGradient
                    start={vec(0, maxWeight - 10)}
                    end={vec(0, chartBounds.bottom)}
                    colors={["#FA6F5B", "rgba(250, 111, 91, 0.00)"]}
                  />
                </Area>
                <Line
                  points={points.weight}
                  strokeWidth={2}
                  color="#FA6F5B"
                  curveType="cardinal"
                  animate={{ type: "timing", duration: 300 }}
                />
                {points.weight.map((point, index) => (
                  <PointsCircle
                    key={`point-${index}-${point.x}-${point.y}`}
                    index={index}
                    x={point.x}
                    y={point.y!}
                    innerRadius={pointInnerRadius}
                    outerRadius={pointOuterRadius}
                  />
                ))}
                {isChartPressActive && (
                  <>
                    <Rect
                      key="tooltip-rect"
                      x={chartBounds.right - 120}
                      y={chartBounds.top + 10}
                      width={110}
                      height={40}
                      color="white"
                    />
                    <PointsCircle
                      key="tooltip-point"
                      index={-1}
                      x={chartPressState.x.position}
                      y={chartPressState.y.weight.position}
                      innerRadius={pointInnerRadius * 1.5}
                      outerRadius={pointOuterRadius * 1.5}
                    />
                    <SkiaText
                      key="tooltip-date"
                      x={chartBounds.right - 100}
                      y={chartBounds.top + 20}
                      text={selectedDate}
                      font={font}
                      color="#667285"
                    />
                    <SkiaText
                      key="tooltip-weight"
                      x={chartBounds.right - 100}
                      y={chartBounds.top + 40}
                      text={selectedWeight}
                      font={font}
                      color="#000000"
                    />
                  </>
                )}
              </React.Fragment>
            )}
          </CartesianChart>
        </View>
      )}
    </Card>
  );
}

Expected behavior:
useChartPressState should target the exact point I'm hovering right now

Actual behavior:
It only targets the close points when I hover my finger way to the left, even outside of the actual chart.

Additional Information

This is a screenshot of my updated dependencies (updating Victory Native to 41.20.2 didn't solve the issue):
Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions