Search code examples
javascriptreact-nativeexporeact-native-svgreact-native-svg-charts

How to render labels along the circumference of PieChart component from react-native-svg-charts?


Here's a snapshot of my code:

  const data = filteredKeys.map((key, index) => {
    return {
      key,
      value: filteredValues[index],
      svg: { fill: colors[index] },
      arc: {
        outerRadius: 70 + filteredValues[index] + '%',
        padAngle: label === key ? 0.1 : 0,
      },
      onPress: () =>
        setSelectedSlice({ label: key, value: filteredValues[index] }),
    };
  });

  const Labels = ({ slices, height, width }) => {
    return slices.map((slice, index) => {
      console.log(slice);
      const { labelCentroid, pieCentroid, data } = slice;
      return (
        <Text
          key={index}
          x={labelCentroid[0]}
          y={labelCentroid[1]}
          fill={'#000'}
          textAnchor={'middle'}
          alignmentBaseline={'center'}
          fontSize={14}
          stroke={'black'}
          strokeWidth={0.2}
        >
          {data.key}
        </Text>
      );
    });
  };

  return (
    <View style={styles.container}>
      <PieChart
        style={{ height: 300 }}
        outerRadius={'100%'}
        innerRadius={'10%'}
        data={data}
        labelRadius={'85%'}
      >
        <Labels />
      </PieChart>
    </View>
  );

This is the result:

enter image description here

I want to labels to align like this:

enter image description here

The labels look very inconsistent on different pie charts, that's why I want to render the labels along the curve of their respective arcs (center of the arc is prefered).

I have tried TextPath and TSpan from react-native-svg, but couldn't figure out how to draw the TextPath when it is child of the component along which the path is to be drawn.


Solution

  • Hate to resurrect this but since I was wanting to do the same thing I figured I would show the math behind it.

    Given we know the center of the circle is 0,0 and we have the x,y of the labels current position we can calculate the adjacent angle which is what your looking for. Heres a simple diagram to illustrate .

    tx,ty = labelCentroid
    R = atan2(ty, tx)
    

    enter image description here

    Given that information the example code below gives the desired results.

    
    function TestPieChart() {
      const Labels: any = ({slices, height, width}) => {
        return slices.map((slice, index) => {
          const {labelCentroid, data} = slice;
          const labelAngle =
            Math.atan2(labelCentroid[1], labelCentroid[0]) + Math.PI / 2;
          return (
            <G key={index}>
              <Text
                transform={
                  `translate(${labelCentroid[0]}, ${labelCentroid[1]})` +
                  `rotate(${(360 * labelAngle) / (2 * Math.PI)})`
                }
                fill={'#000000'}
                textAnchor={'middle'}
                alignmentBaseline={'center'}
                fontSize={14}
                stroke={'black'}
                strokeWidth={0.2}
              >
                {data.key}
              </Text>
            </G>
          );
        });
      };
    
      return (
        <View style={{backgroundColor: 'white'}}>
          <PieChart
            data={[
              {key: 'arms', value: 20, svg: {fill: '#FAA'}},
              {key: 'legs', value: 20, svg: {fill: '#1bb234'}},
              {key: 'chest', value: 50, svg: {fill: '#8a5a00'}},
              {key: 'foot', value: 30, svg: {fill: '#00ffbb'}},
              {key: 'finger', value: 20, svg: {fill: '#223193'}},
            ]}
            style={{height: 270, marginTop: 0, padding: 10}}
            padAngle={0}
            outerRadius={'90%'}
            innerRadius={'80%'}
            labelRadius={'100%'}
          >
            <Labels></Labels>
          </PieChart>
        </View>
      );
    }
    

    enter image description here