149 lines
4.9 KiB
TypeScript
149 lines
4.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import Pie, { ProvidedProps, PieArcDatum } from '@visx/shape/lib/shapes/Pie';
|
|
import { scaleOrdinal } from '@visx/scale';
|
|
import { Group } from '@visx/group';
|
|
import { animated, useTransition, interpolate } from '@react-spring/web';
|
|
import LoadingSpinner from '@/components/ui/spinner';
|
|
|
|
// accessor functions
|
|
const frequency = (c) => c.percentage;
|
|
|
|
const defaultMargin = { top: 20, right: 20, bottom: 20, left: 20 };
|
|
|
|
export type PieProps = {
|
|
data: any
|
|
width: number;
|
|
height: number;
|
|
margin?: typeof defaultMargin;
|
|
animate?: boolean;
|
|
};
|
|
|
|
export default function PieChart({
|
|
data,
|
|
width,
|
|
height,
|
|
margin = defaultMargin,
|
|
animate = true,
|
|
}: PieProps) {
|
|
if (width < 10) return null;
|
|
|
|
const innerWidth = width - margin.left - margin.right;
|
|
const innerHeight = height - margin.top - margin.bottom;
|
|
const radius = Math.min(innerWidth, innerHeight) / 2;
|
|
const centerY = innerHeight / 2;
|
|
const centerX = innerWidth / 2;
|
|
|
|
// console.log(data);
|
|
|
|
return data.length == 0 ? <LoadingSpinner /> : (
|
|
<svg width={width} height={height}>
|
|
<rect rx={14} width={width} height={height} fill="transparent" />
|
|
<Group top={centerY + margin.top} left={centerX + margin.left}>
|
|
<Pie
|
|
data={data}
|
|
pieValue={frequency}
|
|
pieSort={(a, b) => b.categoryCode - a.categoryCode}
|
|
outerRadius={radius}
|
|
cornerRadius={0}
|
|
>
|
|
{(pie) => (
|
|
<AnimatedPie
|
|
{...pie}
|
|
animate={false}
|
|
getKey={(c) => c.data.categoryName}
|
|
onClickDatum={({ data: category }) =>
|
|
console.log('clicked: ', category.categoryName)
|
|
}
|
|
getColor={(arc) => {
|
|
return arc.data.categoryColor;
|
|
}}
|
|
/>
|
|
)}
|
|
</Pie>
|
|
</Group>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
// react-spring transition definitions
|
|
type AnimatedStyles = { startAngle: number; endAngle: number; opacity: number };
|
|
|
|
const fromLeaveTransition = ({ endAngle }: PieArcDatum<any>) => ({
|
|
// enter from 360° if end angle is > 180°
|
|
startAngle: endAngle > Math.PI ? 2 * Math.PI : 0,
|
|
endAngle: endAngle > Math.PI ? 2 * Math.PI : 0,
|
|
opacity: 0,
|
|
});
|
|
const enterUpdateTransition = ({ startAngle, endAngle }: PieArcDatum<any>) => ({
|
|
startAngle,
|
|
endAngle,
|
|
opacity: 1,
|
|
});
|
|
|
|
type AnimatedPieProps<Datum> = ProvidedProps<Datum> & {
|
|
animate?: boolean;
|
|
getKey: (d: PieArcDatum<Datum>) => string;
|
|
getColor: (d: PieArcDatum<Datum>) => string;
|
|
onClickDatum: (d: PieArcDatum<Datum>) => void;
|
|
delay?: number;
|
|
};
|
|
|
|
function AnimatedPie<Datum>({
|
|
animate,
|
|
arcs,
|
|
path,
|
|
getKey,
|
|
getColor,
|
|
onClickDatum,
|
|
}: AnimatedPieProps<Datum>) {
|
|
const transitions = useTransition<PieArcDatum<Datum>, AnimatedStyles>(arcs, {
|
|
from: animate ? fromLeaveTransition : enterUpdateTransition,
|
|
enter: enterUpdateTransition,
|
|
update: enterUpdateTransition,
|
|
leave: animate ? fromLeaveTransition : enterUpdateTransition,
|
|
keys: getKey,
|
|
});
|
|
return transitions((props, arc, { key }) => {
|
|
const [centroidX, centroidY] = path.centroid(arc);
|
|
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.1;
|
|
|
|
return (
|
|
<g key={key}>
|
|
<animated.path
|
|
// compute interpolated path d attribute from intermediate angle values
|
|
d={interpolate([props.startAngle, props.endAngle], (startAngle, endAngle) =>
|
|
path({
|
|
...arc,
|
|
startAngle,
|
|
endAngle,
|
|
}),
|
|
)}
|
|
fill={getColor(arc)}
|
|
onClick={() => onClickDatum(arc)}
|
|
onTouchStart={() => onClickDatum(arc)}
|
|
/>
|
|
{hasSpaceForLabel && false && (
|
|
<animated.g style={{ opacity: props.opacity }}>
|
|
<text
|
|
fill={getForeground(arc)}
|
|
x={centroidX}
|
|
y={centroidY}
|
|
dy=".33em"
|
|
fontSize={9}
|
|
textAnchor="middle"
|
|
pointerEvents="none"
|
|
>
|
|
{getKey(arc)}
|
|
</text>
|
|
</animated.g>
|
|
)}
|
|
</g>
|
|
);
|
|
});
|
|
|
|
function getForeground(arc: PieArcDatum<Datum>) {
|
|
return arc.data.categoryForeground;
|
|
}
|
|
|
|
}
|