이전에 css conic-gradient + property속성 조합을 통한 파이차트를 구현했었는데, 각 부분 섹션 경계선마다 aliasing이 일어난다길래, 이참에 svg로 새로 구현했습니다.
만들면서 호버 이펙트 같은 것도 알아서 넣어달라길래.. 호버 시, 각 섹션들이 얼만큼 차지하는지 비율을 보여주는 이펙트도 넣어봤습니다.
Precondition
1. Data
type PieChartData = {
value : number,
color : string
}
호버 이펙트에 대해서 결정 권한을 완전히 저한테 맡기기도 했고, 호버 이펙트가 그냥 있었으면 좋겠다고만 이야기해서, 굳이 라벨에 대한 프로퍼티는 넘기지 않기로 했습니다.
데이터 인터페이스로서, 딱 위 정보만 받고, 나머지 필요한 값들에 대해선, Piechart 컨포넌트 내에서, 어댑터 함수 거치면서, 생성시키기로 결정 했습니다.
Process
1. 부채꼴 그리기
2. 부채꼴 회전
3. 텍스트 배치
크게 3가지 과정으로 분리해봤습니다.
1. 부채꼴 그리기
data의 개수 만큼 <circle> 을 중앙 배치 후, stroke-dasharray 속성을 통해, 해당 data의 값이 차지하는 비율 * 원의 둘레만큼 dash 값을 설정하고 , gap 값은, 원의 둘레 만큼 설정해주면 될것 같습니다.
2. 부채꼴 회전
<svg width='200' height='200'viewBox='0 0 100 100'>
<circle cx='50' cy='50' r='25' fill='transparent' stroke='red' strokeWidth='50' strokeDasharray='100 1000'/>
</svg>
부채꼴은 만들었는데, 문제가 생겼습니다. 제가 원하는 건, 데카르트 좌표계 y축에서 부터 dash가 시작되길 원하는데, x축에서부터 시작합니다.
간단하게 -90만큼 회전시켜서 바로 잡아줍시다.
<svg width='200' height='200'viewBox='0 0 100 100'>
<circle cx='50' cy='50' r='25' fill='transparent' stroke='red' strokeWidth='50' strokeDasharray='100 1000' transform='rotate(-90)'/>
</svg>
이제 대강 준비가 끝났으니, 실제 데이터를 맵핑 해주겠습니다.
먼저, input 데이터를 쉽게 쓸 수 있게 바꾸겠습니다.
type AdaptedPieChartData ={
color : string,
acc : number,
share : number
}
portion들에게 색상값을 넘겨주기 위해 color
당연하지만, 해당 데이터가 전체에 대해 얼마만큼 차지하기 위한 데이터 share(부분 데이터 값 / 전체 데이터 값)
portion의 회전을 그 portion의 share만큼 하는게 아니라, 누적된 share만큼 회전하니, acc(accumulated share)
// adapt!!
const adapt = () => {
let sum = 0;
const tmp : Array<AdaptedPieChartData> = [];
data.forEach((d) => {
sum += d.value;
});
data.forEach((d) => {
const share = (d.value / sum);
tmp.push({
color : d.color,
acc : tmp.length === 0 ? share : tmp[tmp.length - 1].acc + share ,
share,
})
});
};
이제 지금까지를 토대로 렌더링을 해보겠습니다
const PieChart = ({data , ...rest} : PieChartProps) => {
const RADIUS = 50; // 원의 반지름
// ....
// ....
// 실제 코드에서 지우면서 코드 블럭 삽입해서 조금 지저분 합니다..!
return <Svg viewBox='0 0 120 120'>
{
values.map((d,idx) => {
return <Circle cx='60' cy='60' r={RADIUS / 2} fill='transparent' stroke={d.color} strokeWidth={RADIUS} percent={d.share}/>
})
}
</Svg>
}
3. 텍스트 배치
텍스트 배치같은 경우, 제가 원하는 것은, 각 portion의 중앙에 해당 portion이 전체 비율에 대해 몇 퍼센트 차지하고 있는지 나타내고 싶었습니다.
여러 방법이 있겠지만, 저는 각 <circle>(portion)들을 <g>를 통해 <text>와 묶고, portion의 점유율에 따라, 각 텍스트들의 좌표값을 구해 원점에 대해서 움직이는 방식으로 구현했습니다.
반지름과 각도를 알면, 삼각좌표의 형태로 점의 위치를 표현할 수 있습니다.
단, 제가 제시하는 각도는, y축을 기점으로 시계방향으로 증가하는 사잇각이기에, 이 부분을 고려하여, 사잇각의 기준 축을 변경 하여, 좌표를 구했습니다.
const caculateTextCoordinate = (accumulatedPercent : number , percent : number) => {
let rotationDeg = 360 * (accumulatedPercent - (percent/2));
if(rotationDeg <= 90){
rotationDeg = 90 - rotationDeg
}else if(rotationDeg <= 180 && rotationDeg > 90){
rotationDeg = 360 - rotationDeg + 90;
}else if(rotationDeg > 180 && rotationDeg <= 270){
rotationDeg = 270 - rotationDeg + 180
}else{
rotationDeg = 360 - rotationDeg + 90
console.log(rotationDeg)
}
//deg -> radian 1rad = 1deg * pi / 180
const rad = degToRad(rotationDeg);
const x_offset = Math.cos(rad);
const y_offset = Math.sin(rad);
// 브라우저에서 y축 방향은 아래 방향이므로, dy의 방향을 y_offset의 반대로 설정합니다
return {
dx : x_offset,
dy : -y_offset
}
}
적용 전, <text>의 좌표 기준점이 좌측 하단이므로, dx , dy속성에 기본 값을 부여해, 중앙 배치를 시킵니다.
const PieChart = ({data , ...rest} : PieChartProps) => {
const [values , setValues] = useState<Array<AdaptedPieChartData>>([]);
const RADIUS = 50;
const TEXT_X_OFFSET = -3;
const TEXT_Y_OFFSET = 3;
return <Svg viewBox='0 0 120 120'>
{
values.map((d,idx) => {
return <Group id={'pie-group' + idx} rotate={(d.acc - d.share)*360 }>
<Circle cx='60' cy='60' r={RADIUS / 2} fill='transparent' stroke={d.color} strokeWidth={RADIUS} percent={d.share}/>
<Text fill='white' x='60' y='60' dx={TEXT_X_OFFSET+RADIUS/2*caculateTextCoordinate(d.acc,d.share).dx} dy={TEXT_Y_OFFSET+RADIUS/2*caculateTextCoordinate(d.acc,d.share).dy} rot={90 - ((d.acc-d.share)*360)}>{Math.ceil(d.share * 100)}%</Text>
</Group>
})
}
<Text fill='white' x='60' y='60' rot={0}>Orgin</Text>
</Svg>
}
그런데 결과물이 조금 이상합니다.(텍스트 위치의 경우, TEXT_OFFSET들 잘 조절하면 딱 이쁘게 될겁니다)
노란색 portion의 텍스트가 클리핑되는거 보이시나요? SVG쌓임맥락상, 연한 노랑색 portion이 더 위에 쌓여서 그렇습니다.
아쉽게도 svg에는 css처럼 직접적으로 z-index를 조절하는 방법이 없으므로, 쌓임 맥락을 조절해주는 함수를 만들어, 각 portion에 hover시, 가장 높은 쌓임 맥락을 가질 수 있겠금 하겠습니다.
방법은 간단합니다, hover시, 해당 노드를 가장 뒤에 배치하면 됩니다.
const moveToFront = (id : string) =>{
const portion = document.querySelector(id)
portion?.parentNode?.appendChild(portion);
}
// .... (중략)
<Group id={'pie-group' + idx} rotate={(d.acc - d.share)*360 } onMouseEnter={() => moveToFront('#pie-group' + idx)}>
최종 코드는 아래 링크에서 확인하실 수 있습니다 :)
https://github.com/dovigod/Mega-component/blob/master/src/components/PieChart/index.tsx
'React' 카테고리의 다른 글
Fiber reconciler (outdated) (1) | 2023.09.26 |
---|---|
[React] Reconciliation - (feat, Diffing 알고리즘) (0) | 2023.06.26 |
React Element vs Component vs Component Instance vs React Node (0) | 2023.06.25 |
[React] Bar chart 만들기 (0) | 2022.12.03 |