lifetracker/apps/web/components/dashboard/analytics/PieChart.tsx

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;
}
}