|
|
|
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" /> |
|
</> |
|
); |
|
} |
|
}; |
|
|
|
const usage = (d: SummaryItem) => d.usage; |
|
const frequency = (d: SummaryItemSocial) => d.usage; |
|
|
|
|
|
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> |
|
); |
|
}); |
|
} |
|
|