FormidableLabs / victory-native-xl

A charting library for React Native with a focus on performance and customization.
https://commerce.nearform.com/open-source/victory-native
664 stars 49 forks source link

Unable to create custom Pie.Chart Slice with the useSlicePath hook #245

Closed jmcmullen closed 5 months ago

jmcmullen commented 5 months ago

Prerequisites

Describe Your Environment

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

What version of React and React Native are you using? react 18.2.0 react-native 0.73.6

What version of Reanimated and React Native Skia are you using? react-native-reanimated 3.6.3 @shopify/react-native-skia 1.2.3

Are you using Expo or React Native CLI? Expo

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

Describe the Problem

When following the documentation to useSlicePath I encounter a few errors.

Firstly, the type PieSliceData isn't exported from victory-native and I need to import from victory-native/dist/pie/PieSlice but then the useSlicePath hook actually wants a different type SlicePathArgs than what's specified in the documentation.

When I proceed with the wrong types, I get the error:

TypeError: Cannot read property 'radius' of undefined

This error is located at:
    in Unknown (created by PolarChartBase)
    in RCTView (created by CssInterop.View)
    in CssInterop.View (created by PolarChartBase)
    in PolarChartBase (created by PolarChart)
    in PolarChartProvider (created by PolarChart)

Expected behavior: I expected the documentation to be correct and allow me to create a custom slice for my Pie Chart.

Actual behavior: Error is thrown trying to read undefined property.

Additional Information

Off topic, but I am trying to create a Donut Chart where the pie slices start at 12 o'clock so it looks like a "Progress Ring". I think this would be a great addition to the documentation & example repo. If someone can point me in the right direction I'd be happy to contribute the changes needed.

jmcmullen commented 5 months ago

I've also just noticed the example code also passes props for PolarChart to Pie.Chart which I believe is also wrong.

jmcmullen commented 5 months ago

I managed to get it working, here's the code incase anyone else encounters this:

import { useMemo } from "react";
import type { DataSourceParam } from "@shopify/react-native-skia";
import { Path, Text, useFont } from "@shopify/react-native-skia";
import { Pie, PolarChart, useSlicePath } from "victory-native";
import type { PieSliceData } from "victory-native/dist/pie/PieSlice";
import { BRANDING } from "@sash/constants";
import inter from "../assets/fonts/inter.ttf";
import { View } from "./Layout";

interface ProgressProps {
  size: number;
  stroke: number;
  label: string;
  data: {
    value: number;
    label: string;
    color: string;
  }[];
}

const PieSlice = ({
  slice,
  data,
}: {
  slice: PieSliceData;
  data: ProgressProps["data"];
}) => {
  const total = data.reduce((acc, item) => acc + item.value, 0);
  let startAngle = 0;

  for (const item of data) {
    if (item.label === slice.label) break;
    startAngle += (item.value / total) * 360;
  }

  const endAngle = startAngle + (slice.value / total) * 360;

  const path = useSlicePath({
    slice: {
      ...slice,
      startAngle,
      endAngle,
    },
  });

  return <Path path={path} style="fill" {...slice} />;
};

export const Progress = ({ size, label, data }: ProgressProps) => {
  const half = size / 2;
  const font = useFont(inter as DataSourceParam, 16);

  const fixedData: ProgressProps["data"] = useMemo(() => {
    if (data[0]?.value === 0) {
      return [
        {
          value: 1,
          label: data[1]?.label ?? "Checked Out",
          color: data[1]?.color ?? "#181818",
        },
      ];
    }
    return data;
  }, [data]);

  const xOffset = label.length > 2 ? 16 : 9;

  return (
    <View style={{ width: size, height: size }}>
      <PolarChart
        data={fixedData}
        labelKey={"label"}
        valueKey={"value"}
        colorKey={"color"}
        containerStyle={{ transform: [{ rotate: "-90deg" }] }}
      >
        <Pie.Chart innerRadius={"80%"}>
          {({ slice }) => {
            return <PieSlice slice={slice} data={fixedData} />;
          }}
        </Pie.Chart>
        <Text
          color={BRANDING.COLORS.brand}
          x={half - xOffset}
          y={-half + 7}
          font={font}
          text={label}
          transform={[{ rotate: 1.57 }]}
        />
      </PolarChart>
    </View>
  );
};
OscarHgg commented 5 months ago

@jmcmullen I am currently trying to wrap my head around this component, and noticed the same problems in the sample code.

Are you also having inconsistent rendering performance? Sometimes it shows up, but then disappears after a re-render. Perhaps an Expo Go issue?

zibs commented 5 months ago

Hey @OscarHgg Can you provide more details or open a separate issue that has a reproducible example? I'd be happy to take a closer look. Feel free to look at the examples in the example app as well.

iM-GeeKy commented 4 months ago

@jmcmullen Thanks for sharing your workaround. It seemed to work pretty well for the most part, but we're you able to figure out a better way to center the text in the inside the pie chart? I was able to get the text centered relatively well on phone views, but it didn't work too well on tablets (iPad). I've resorted back to the older version since the VictoryLabel props textAnchor and verticalAnchor seemed to help with this.

@zibs Is there any plan to allow for centering text inside of the pie donut?