nolual's picture
Upload 55 files
0c20ea8
/* eslint-disable @typescript-eslint/no-use-before-define */
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 {
GradientPinkBlue,
GradientOrangeRed,
LinearGradient,
} from "@visx/gradient";
import { animated, useTransition, interpolate } from "@react-spring/web";
import {
generateSummary,
SummaryItem,
} from "../../../algorithm/DemographicsPieByGender";
import {
summarizeSocialGroups,
SummaryItemSocial,
} from "../../../algorithm/DemographicsPieBySocialGroup";
interface demographics {
age: string;
gender: string;
percentage: number;
social_group: string;
}
const defaultMargin = { top: 20, right: 20, bottom: 20, left: 20 };
export type PieProps = {
data: demographics[];
width: number;
height: number;
margin?: typeof defaultMargin;
animate?: boolean;
flag?: number;
};
export default function PieChart({
data,
width,
height,
margin = defaultMargin,
animate = true,
flag = 0,
}: PieProps) {
const [selectedDemographic, setSelectedDemographic] = useState<string | null>(
null
);
const [selectedSocialGroup, setSelectedSocialGroup] = useState<string | null>(
null
);
const proccessedData = generateSummary(data);
const proccessedSocialGroupData = summarizeSocialGroups(data);
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;
const donutThickness = 50;
const backgroundDefined = () => {
switch (flag) {
case 0:
return (
<>
<rect
width={width}
height={height}
fill="url('#visx-pie-gradient')"
/>
<LinearGradient
id="visx-pie-gradient"
from="#000000"
to="#3C3C3C"
rotate="-45"
/>
</>
);
case 1:
return (
<>
<rect width={width} height={height} fill="url('#men')" />
<GradientOrangeRed id="men" />
</>
);
case 2:
return (
<>
<rect width={width} height={height} fill="url('#women')" />
<GradientPinkBlue id="women" />
</>
);
}
};
// accessor functions
const usage = (d: SummaryItem) => d.usage;
const frequency = (d: SummaryItemSocial) => d.usage;
// color scales
const getBrowserColor = scaleOrdinal({
domain: proccessedData.map((d) => d.label),
range: [
"rgba(255,255,255,0.7)",
"rgba(255,255,255,0.6)",
"rgba(255,255,255,0.5)",
"rgba(255,255,255,0.4)",
"rgba(255,255,255,0.3)",
"rgba(255,255,255,0.2)",
"rgba(255,255,255,0.1)",
],
});
const getLetterFrequencyColor = scaleOrdinal({
domain: proccessedSocialGroupData.map((l) => l.label),
range: [
"rgba(93,30,91,1)",
"rgba(93,30,91,0.8)",
"rgba(93,30,91,0.6)",
"rgba(93,30,91,0.4)",
],
});
return (
<svg
width={width}
height={height}
style={{
borderBottomLeftRadius: "14px",
borderBottomRightRadius: "14px",
boxShadow: "0 0 8px rgba(0, 0, 0, 0.2)",
}}
>
{backgroundDefined()}
<Group top={centerY + margin.top} left={centerX + margin.left}>
<Pie
data={
selectedDemographic
? proccessedData.filter(
({ label }) => label === selectedDemographic
)
: proccessedData
}
pieValue={usage}
outerRadius={radius}
innerRadius={radius - donutThickness}
cornerRadius={3}
padAngle={0.005}
>
{(pie) => (
<AnimatedPie<SummaryItem>
{...pie}
animate={animate}
getKey={(arc) => arc.data.label}
onClickDatum={({ data: { label } }) =>
animate &&
setSelectedDemographic(
selectedDemographic && selectedDemographic === label
? null
: label
)
}
getColor={(arc) => getBrowserColor(arc.data.label)}
/>
)}
</Pie>
<Pie
data={
selectedSocialGroup
? proccessedSocialGroupData.filter(
({ label }) => label === selectedSocialGroup
)
: proccessedSocialGroupData
}
pieValue={frequency}
pieSortValues={() => -1}
outerRadius={radius - donutThickness * 1.3}
>
{(pie) => (
<AnimatedPie<SummaryItemSocial>
{...pie}
animate={animate}
getKey={({ data: { label } }) => label}
onClickDatum={({ data: { label } }) =>
animate &&
setSelectedSocialGroup(
selectedSocialGroup && selectedSocialGroup === label
? null
: label
)
}
getColor={({ data: { label } }) => getLetterFrequencyColor(label)}
/>
)}
</Pie>
</Group>
</svg>
);
}
type AnimatedStyles = { startAngle: number; endAngle: number; opacity: number };
const fromLeaveTransition = ({ endAngle }: PieArcDatum<any>) => ({
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: any, arc: PieArcDatum<Datum>, { key }: any) => {
const [centroidX, centroidY] = path.centroid(arc);
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.1;
return (
<g key={key}>
<animated.path
d={interpolate(
[props.startAngle, props.endAngle],
(startAngle: any, endAngle: any) =>
path({
...arc,
startAngle,
endAngle,
})
)}
fill={getColor(arc)}
onClick={() => onClickDatum(arc)}
onTouchStart={() => onClickDatum(arc)}
/>
{hasSpaceForLabel && (
<animated.g style={{ opacity: props.opacity }}>
<text
fill="white"
x={centroidX}
y={centroidY}
dy=".33em"
fontSize={9}
textAnchor="middle"
pointerEvents="none"
>
{getKey(arc)}
</text>
</animated.g>
)}
</g>
);
});
}