cs-2022-class-profile/components/PieChart.tsx

195 lines
5.8 KiB
TypeScript

import { Group } from "@visx/group";
import Pie, { ProvidedProps } from "@visx/shape/lib/shapes/Pie";
import { Text } from "@visx/text";
import { withTooltip } from "@visx/tooltip";
import React from "react";
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper";
import styles from "./PieChart.module.css";
interface PieChartProps {
data: PieChartData[];
/** Width of the entire graph, including labels, in pixels. */
width: number;
/** Width of the outer ring of labels, in pixels. Label text may be cut off if specified value is too small. */
labelWidth: number;
/** Distance between pie slices, in pixels. */
padRadius?: number;
/** Distance of gap in center of pie graph, in pixels. */
innerRadius?: number;
/** Font size of labels outside the pie, in pixels. */
labelTextSize?: number;
/** X-axis offset of the label text, in pixels. */
labelTextXOffset?: number;
/** Y-axis offset of the label text, in pixels. */
labelTextYOffset?: number;
/** The radial offset of the label text, in pixels. */
labelTextRadialOffset?: number;
/** If set, the minimum width of this graph */
minWidth?: number;
/** Accessor function to get value to display as label text from datum. */
getLabelDisplayValueFromDatum?: (datum: PieChartData) => string;
className?: string;
}
interface PieChartData {
category: string;
value: number;
}
export const PieChart = withTooltip<PieChartProps>(
({
data,
width,
labelWidth,
padRadius = width * 0.25,
innerRadius = width * 0,
labelTextSize = 40,
labelTextXOffset = 0,
labelTextYOffset = 0,
labelTextRadialOffset = -20,
minWidth = 500,
getLabelDisplayValueFromDatum = (datum: PieChartData) =>
`${datum.category}`,
className,
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
hideTooltip,
showTooltip,
}) => {
if (minWidth) {
width = width < minWidth ? minWidth : width;
}
const pieWidth = width * 0.5 - labelWidth;
const cornerRadius = 0;
const padAngle = 0;
return (
<div>
<svg className={className} width={width} height={width}>
<Group top={width * 0.5} left={width * 0.5}>
<Pie
data={data}
pieValue={(d: PieChartData) => d.value}
cornerRadius={cornerRadius}
padAngle={padAngle}
padRadius={padRadius}
innerRadius={innerRadius}
outerRadius={pieWidth}
>
{({ arcs, path }) => {
return arcs.map((arc) => {
const pathArc = path(arc) as string;
return (
<Group
className={styles.group}
key={`arc-${arc.data.category}`}
>
<path
onMouseMove={(e) => {
const tooltipPos = getTooltipPosition(e);
showTooltip({
tooltipData: `${arc.data.category}: ${arc.data.value}%`,
tooltipLeft: tooltipPos.x,
tooltipTop: tooltipPos.y,
});
}}
onMouseOut={hideTooltip}
className={styles.piePath}
d={pathArc}
/>
</Group>
);
});
}}
</Pie>
<Pie
data={data}
pieValue={(d: PieChartData) => d.value}
innerRadius={pieWidth}
outerRadius={width * 0.5}
>
{(pie) => (
<PieSliceLabel
{...pie}
labelTextSize={labelTextSize}
labelTextXOffset={labelTextXOffset}
labelTextYOffset={labelTextYOffset}
labelTextRadialOffset={labelTextRadialOffset}
getLabelDisplayValueFromDatum={getLabelDisplayValueFromDatum}
/>
)}
</Pie>
</Group>
</svg>
{tooltipOpen && (
<TooltipWrapper
top={tooltipTop}
left={tooltipLeft}
header={tooltipData as string}
></TooltipWrapper>
)}
</div>
);
}
);
type PieSliceLabelProps<PieChartData> = ProvidedProps<PieChartData> & {
labelTextSize: number;
labelTextXOffset: number;
labelTextYOffset: number;
labelTextRadialOffset: number;
getLabelDisplayValueFromDatum: (datum: PieChartData) => string;
};
export function PieSliceLabel({
path,
arcs,
labelTextSize,
labelTextXOffset,
labelTextYOffset,
labelTextRadialOffset,
getLabelDisplayValueFromDatum,
}: PieSliceLabelProps<PieChartData>) {
return (
<>
{arcs.map((arc) => {
const [centroidX, centroidY] = path.centroid(arc);
const pathArc = path(arc) as string;
return (
<Group className={styles.group} key={`arc-${arc.data.category}`}>
<path className={styles.labelPath} d={pathArc} />
<Text
className={styles.labelText}
x={
(labelTextRadialOffset * centroidX) /
Math.sqrt(centroidX ** 2 + centroidY ** 2) +
centroidX +
labelTextXOffset
}
y={
(labelTextRadialOffset * centroidY) /
Math.sqrt(centroidX ** 2 + centroidY ** 2) +
centroidY +
labelTextYOffset
}
textAnchor={
centroidX > 100 ? "start" : centroidX < -100 ? "end" : "middle"
}
fontSize={labelTextSize}
>
{`${getLabelDisplayValueFromDatum(arc.data)}`}
</Text>
</Group>
);
})}
</>
);
}