회사에서 다음 프로젝트를 들어가기 전까지 여유시간이 생기면서 어떤걸 공부하지? 고민에 빠졌다. 회사의 서비스에는 차트가 많이 사용되는 만큼 빈 시간을 통해 '직접 SVG를 활용해서 차트를 구현하면 좋지 않을까' 생각하게 되었다. 여러 차트중 적은 데이터로 쉽게 나타낼 수 있는 파이차트 나아가 가운데 원이 생긴 도넛차트를 구현하기로 하였다.
먼저 원형에서 원하는 크기만큼 조각을 나누기 위해서는 무엇보다 삼각함수
가 중요하다.
삼각함수를 기본으로 storke의 특징을 이용해 나타내보았다.
삼각함수와 stroke에 대한 기본적인 이해는 해당 블로그를 통해 습득하였다.
storke를 통해 svg의 윤곽선을 그릴수 있다. stroke-dasharray
stroke-dashoffset
등 다양한 속성을 통해 윤관선을 원하는 형태로 변화시킬수 있다.
기본적인 소성을 통해 삼각함수를 그려보자
2*PI*radius
)이 3가지가 가장 핵심 흐름이라고 생각한다.
const radius = 80; // 차트 반지름
const diameter = 2 * Math.PI * radius;// 원 둘레
const colors = ["#e02e1e", "#281B1AFF", "#ff73cb", "#fff75c", "#93ff57"];
const dataset = [35, 8, 2, 3, 10];
// 데이터의 총 합
const total = dataset.reduce((r, v) => r + v, 0);
// 도넛 시작점을 기억하기 위한 누적값
const acc = dataset.reduce((result, value) => [...result, result[result.length - 1] + value], [0]);
const DrawCircle = (): JSX.Element => {
return <>{
dataset.map((v, i) => {
const radio = v / total; // 비율
const fillSpace = diameter * radio; // 실제 차지하는 비율
const emptySpace = diameter - fillSpace; // 전체에서 해당 데이터 차치 부분 빼고 나머지
const offset = (acc[i] / total) * diameter; // 밀어주기
return <path cx={50} cy={50} r={radius}
stroke={colors[i]}
strokeWidth={10}
fill={"transparent"}
strokeDasharray={`${fillSpace} ${emptySpace}`}
strokeDashoffset={-offset}/>
})
}
</>
};
데이터의 비율을 통해 각도를 구한다. 이후 strokeDashArray를 통해 실제로 해당하는 길이 만큼만 선을 표현한다. fillSpace
storkeDashoffset을 통해 다음 위치에 데이터를 그려줄 수 있도록 밀어준다. 기본이 시계반대 방향으로 그려지기 때문에 시계 방향으로 그려주기 위해 -
를 붙여주었다.
But, 데이터를 한번에 빠르게 나타낼 수있지만 각 데이터 마다 애니메이션을 나타내려면 한계가 발생한다.
참고 블로그
위의 특성을 사용하기 위해서는 삼각함수의 이해가 가장 중요하다. 이후 호를 그리기 위한 원호 명령어의 이해가 중요하다. 원호 명령어는 MDN 문서를 확인해보자.
위 그림이 삼각함수의 필요성을 나타내준다고 생각한다. 만약 현재 위치가 (x,y)라면 n도 만큼 떨어진 다음 점을 찾기위한 중요한 포인트이다.
// n도 벌어진 점의 좌표
const getCoordsOnCircle = ({x, y, radius, degree}: ArcData) => {
const radian = ((degree - 90) / 180) * Math.PI;
return {
x: x + radius * Math.cos(radian),
y: y + radius * Math.sin(radian)
}
};
처음에 데이터 순서대로 끝점을 기억하고 다음 호를 그릴 떄 그 점을 시작점으로 해야겠다라는 생각을 가지고 접근하였다. 하지만 그것보다 같은 점에 모든 데이터의 호를 그린후 전 데이터들의 각도를 누적을 회전하는 방법이 좀더 쉬울 것이라고 생각하였다.
// 호의 끝점과 시작점 얻기
const getArc = (data: ArcData) => {
const startCoord = getCoordsOnCircle({...data});
const finishCoord = getCoordsOnCircle({...data, degree: 0});
// 각의크기에 따라 LargeArc 속성 변화 주기
const {x, y, radius, degree} = data;
const isLargeArc = degree > 180 ? 1 : 0;
return `M ${startCoord.x} ${startCoord.y}
A ${radius} ${radius} 0 ${isLargeArc} 0 ${finishCoord.x} ${finishCoord.y}
L ${x} ${y} `;
};
// 호를 회전하기 위해 해당 데이터의 각도 누적값
function getStartRotate(idx: number) {
return dataset.filter((_, i) => i < idx).reduce((all, v) => v + all, 0);
}
const DrawPi = (): JSX.Element => {
return <>{
dataset.map((v, i) => {
const startRotate = getStartRotate(i);
const radio = v / total;
const rotateAngle = startRotate / total * MAX_DEGREE;
const d = getArc({x: 200, y: 200, radius, degree: radio * MAX_DEGREE});
const targetRad = 2 * Math.PI * radius * radio;
const targetRestRad = 2 * Math.PI * radius * (1 - radio);
return (
<path d={`${d}`}
key={i}
stroke={'transparent'}
transform={`rotate(${rotateAngle},200,200) `}
fill={colors[i]}/>
)
})
}
</>
};
이렇게 원하는 데이들이 모인 파이차트를 얻을 수있다.
여기까지 이해하고 왔다면 원형으로 그릴 수 있다고 생각한다. 현재 응용으로 spin 아이콘을 직접 svg를 통해 작성하고 원하는 대로 변형하고 있다
<article className={'chart'}>
<svg width={400} height={400} viewBox={"0 0 400 400"} id={'pi-svg'}>
<defs>
<mask id="myMask">
<circle cx={200} cy={200} r={200} fill={"white"}/>
<circle cx={200} cy={200} r={20} fill={"black"}/>
</mask>
</defs>
{/*<DrawCircle/>*/}
<g mask={"url(#myMask)"}>
<DrawPi/>
</g>
</svg>
</article>
<!-흰색 픽셀 아래의 모든 것이 표시됩니다 .-->
<!-검은 색 픽셀 아래의 모든 항목이 표시되지 않습니다.->
속성을 통해 나타내고 싶은 영역과 뚫고 싶은 영역을 표현할 수 있다.