Blog Park - 도넛 차트 만들기
왠 도넛 차트?
활동 로그를 캘린더로 만들었는데 굳이 또 차트가 필요한가 싶지만 이 유저의 전체 활동은 한눈에 안들어오는 문제가 있다. 일일히 날짜를 눌러보고 날짜에 어떤 활동을 했는지 확인하도록 하지 않고 차트를 보면 한눈에 어떤 활동을 했는지 확인이 가능해야 요약 캘린더도 의미가 있다고 생각한다.
여러 차트중에 왜 도넛차트냐면 막대 차트도 나쁘진 않지만 전체 활동중에 몇퍼센트에 해당하는지 한눈에 확인하기는 어렵다.
반면에 원형으로 차트를 만들게 되면 전체 활동중에 해당 활동의 비중이 얼마나 되는지 한눈에 확인이 가능하다. 그래서 나는 원형 모양의 차트, 도넛 차트를 적용하기로 했다.
도넛 차트 만드는 여러 방법
크게 세가지가 있다. svg, div, canvas를 사용하는 방법이다. 우선 canvas는 내가 거의 사용을 해본적이 없는 태그이기도 하고 해상도에 따른 픽셀 문제로 이미지가 깨지는 경우가 있어서 제외를 했다. 다음 div는 쉽게 사용이 가능하고 구현 가능하겠지만 여러 데이터의 비율 설정 까다롭고, 반응형에 취약하다는 단점이 있다.
그래서 나는 svg를 사용해보기로 했다. svg는 circle태그의 속성을 통해 차트를 구현할수 있고 반응형에도 좋은 결과를 보여준다. 물론 나는 canvas와 동일하게 직접 다뤄본적 없다. 하지만 항상 코드로 이뤄진 svg파일들을 보며 어떻게 이뤄지는지 궁금했기 때문에 겸사겸사 사용을 해보기로 했다.
svg로 도넛 차트 만들기
앞에서 svg의 circle 속성을 사용해서 도넛차트를 만든다고 했다. 어떻게 옵션을 사용하는지 알아보겠다.
strokeDasharray
strokeDasharray은 도형을 만들때 길이와 공백을 지정하는 속성이다. 만약 strokeDasharray(5)를 지정한다면 길이가 5, 공백이 5인 도형이 만들어진다.
strokeDasharray(5, 10)이렇게 두개의 값을 받게되는데 뒤에 오는 값은 공백의 크기다. 만약 위와 같이 한개의 값만 지정한다면 도형의 크기와 공백의 크기가 같은 모양이 만들어진다. 만약 두개의 값을 지정한다면
이렇게 만들어 진다.
strokeDashoffset
strokeDashoffset은 말그대로 strokeDasharray의 Offset이다. 그래서 만들어진 패턴에서 얼만큼 이동해서 보여줄지 지정이 가능하다.
도넛 차트에 필요한 값 설정
우선 우리는 도넛 차트로 3개의 값을 보여줄것이다. 포스트, 댓글, 좋아요를 나타낼 계획이기 때문에 svg내부에 circle은 3개가 필요하다. 그리고 각 3개마다 비율을 계산하고 dashArray와 offset을 지정하는 것이다.
const total = post + commnet + like; const radius = 90; // 원의 반지름 const circumference = 2 * Math.PI * radius; // 원 둘래
당연히 우리는 원으로 만들어진 차트를 구현할것이기 때문에 원 둘래 공식을 사용해야 strokeDasharray를 정확한 값으로 지정 가능해진다. 공식이 어려워 보이지만 어려운 것은 없다. 반지름은 내가 임의로 정한 반지름이고 circumference는 학교에서 많이 배운 2πr이다.
// 0일때 제외 const postRatio = total > 0 ? post / total : 0; const commentRatio = total > 0 ? commnet / total : 0; const likeRatio = total > 0 ? like / total : 0; // 각 부분의 비율에 따른 strokeDasharray 및 strokeDashoffset 계산 const dasharray1 = postRatio * circumference; const dasharray2 = commentRatio * circumference; const dasharray3 = likeRatio * circumference;
이제 포스트, 댓글, 좋아요의 비율을 계산해주면 된다. 0일때는 비율을 0으로 만들어 주도록 조건을 추가해주면 3개의 비율이 무조건 1로 떨어질 것이다. 그리고 이렇게 만들어진 비율을 circle에 적용시켜주면 된다.
<div onMouseLeave={resetState}> <svg viewBox='0 0 300 300' width='250' height='250'> {postRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#3b82f6' strokeWidth='20' strokeDasharray={`${dasharray1} ${circumference - dasharray1}`} /> )} {commentRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#ef4444' strokeWidth='20' strokeDasharray={`${dasharray2} ${circumference - dasharray2}`} /> )} {likeRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#10b981' strokeWidth='20' strokeDasharray={`${dasharray3} ${circumference - dasharray3}`} /> )} </svg> </div>
strokeDasharray에 적용하는 것을 보겠다. 왜 strokeDasharray에 그냥 계산한 값이 아니라 여러 값들이 들어가 있는지 알아보겠다. 위에서 계산한 값을 그대로 집어 넣는다면 길이가 그만큼의 도형이 생길것이다. 그래서 한개만 표시하기 위해 계산한 도형의 길이를 전체 길이를 뺀만큼의 값을 공백 값으로 넣는것이다.
실제 저런 모습은 아니고 다 겹쳐져서 내가 보기 쉽도록 크기를 좀 조정해놨다. strokeDasharray는 3시방향부터 시작하기 때문에 offset를 설정해줘야한다.
const offset = circumference * 0.25; // 위치 조정을 위한 offset
그래서 12시 방향에서 시작하도록 만들기 위해 전체 길이에서 0.25를 곱해서 12시로 이동시켜준다. 하지만 이동시켜도 3개의 circle이 전부 겹쳐있기 때문에 옮겨줘야한다.
{postRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#3b82f6' strokeWidth='20' strokeDasharray={`${dasharray1} ${circumference - dasharray1}`} strokeDashoffset={offset} /> )} {commentRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#ef4444' strokeWidth='20' strokeDasharray={`${dasharray2} ${circumference - dasharray2}`} strokeDashoffset={offset - dasharray1} /> )} {likeRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#10b981' strokeWidth='20' strokeDasharray={`${dasharray3} ${circumference - dasharray3}`} strokeDashoffset={offset - (dasharray1 + dasharray2)} /> )}
그래서 첫번째 circle만 offset을 그대로 적용시켜주고 뒤에 있는 circle은 앞에 있는 도형의 길이만큼을 더 빼준다. 그러면 도형 길이만큼 offset을 적용시켜서 겹치지 않도록 해준다.
도넛 차트 공식이 좀 어렵게 느껴지는데 간단하게 설명하자면 아래와 같다.
x = 원의 전체 둘레 길이 / a = 데이터에 해당하는 원의 둘레 길이
<circle strokeDasharray="<a> <x - a>" strokeDashoffset="<0.25 * x>" />
이 방법에 더해서 3개의 차트가 서로 겹치지 않게 하기 위해 offset에 바로 앞에 있는 도형의 길이를 추가로 넣어주는 것이다.
밋밋한 차트 꾸미기
약간 욕심이 생기는데 차트가 안이쁘다. 나는 좀더 동글동글하고 마우스 이벤트도 추가하고 싶다.
우선 circle의 양끝을 동글게 만들어 주겠다. strokeLinecap='round'을 지정해주면 양 끝이 동글게 만들어진다.
그런데 round를 지정하니 겹치는 부분이 생긴다. round 되는 부분까지 길이 지정되는 것이 아니라 round는 stroke의 width의 반지름만큼 커지는 것이다.
const dasharray1 = postRatio * circumference - 20; const dasharray2 = commentRatio * circumference - 20; const dasharray3 = likeRatio * circumference - 20; const offset = circumference * 0.25 - 10; // 위치 조정을 위한 offset
그래서 길이를 20만큼만 빼주게 되면 양끝 round되는 것을 포함해서 길이가 이전과 같은 길이가 된다. 그리고 12시 방향을 맞추기 위해 기존 offset에서 앞부분의 round값인 10을 빼주면 기존과 동일한 위치에 위치하게 된다.
{postRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#3b82f6' strokeWidth='20' strokeDasharray={`${dasharray1} ${circumference - dasharray1}`} strokeDashoffset={offset} /> )} {commentRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#ef4444' strokeWidth='20' strokeDasharray={`${dasharray2} ${circumference - dasharray2}`} strokeDashoffset={offset - (dasharray1 + 20)} /> )} {likeRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#10b981' strokeWidth='20' strokeDasharray={`${dasharray3} ${circumference - dasharray3}`} strokeDashoffset={offset - (dasharray1 + dasharray2 + 40)} /> )}
그리고 offset에서도 두번째 circle부터 offset에 round로 인해 추가된 값을 넣어주면 된다. 40인 이유는 앞에 있는 circle의 round와 현재 circle의 round의 값을 더해주는 것이다. 그래서 두번째 도형은 20, 세번째 도형은 40을 더해주는 것이다.
이제 round가 적용되고, 12시부터 차트가 시작되는 도넛 차트가 끝난것이다.
차트에 데이터 표시해주기
이제 각 부분에 마우스를 올렸을때 해당 영역이 어떤 값인지 확인하도록 만들어주겠다.
<text x='150' y='155' fontSize='20' fontWeight={600} fill='#4b5563' textAnchor='middle' > 전체 {total}회 </text>
우선 도넛 차트 중심에 전체 total값을 표시해주겠다. 그리고 각 영역에 마우스를 hover하면 해당 영역이 어떤 데이터이고 몇개의 활동을 했는지 확인하도록 하겠다.
type HoverArea = 'post' | 'comment' | 'like' | 'all'; const [isHover, setIsHover] = useState<HoverArea>('all'); ... const handleHover = (area: HoverArea) => { setIsHover(area); };
각 영역에 hover를 파악하기 위해 state를 만들어주겠다. 기본값은 all로 전체에 대한 정보를 알려주고 마우스를 호버했을때 각 영역이 어떤 영역인지 전달해주면 된다.
<svg viewBox='0 0 300 300' width='250' height='250'> {postRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#3b82f6' strokeWidth='20' strokeDasharray={`${dasharray1} ${circumference - dasharray1}`} strokeDashoffset={offset} className={`${ isHover === 'post' && 'scale-125' } origin-center sclae-100 cursor-pointer transition-all duration-300 ease-in-out`} strokeLinecap='round' onMouseOver={() => { handleHover('post'); }} /> )} {commentRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#ef4444' strokeWidth='20' strokeDasharray={`${dasharray2} ${circumference - dasharray2}`} strokeDashoffset={offset - (dasharray1 + 20)} onMouseOver={() => { handleHover('comment'); }} className={`${ isHover === 'comment' && 'scale-125' } origin-center sclae-100 cursor-pointer transition-all duration-300 ease-in-out`} strokeLinecap='round' /> )} {likeRatio !== 0 && ( <circle cx='150' cy='150' r={radius} fill='none' stroke='#10b981' strokeWidth='20' strokeDasharray={`${dasharray3} ${circumference - dasharray3}`} strokeDashoffset={offset - (dasharray1 + dasharray2 + 40)} onMouseOver={() => { handleHover('like'); }} className={`${ isHover === 'like' && 'scale-125' } origin-center sclae-100 cursor-pointer transition-all duration-300 ease-in-out`} strokeLinecap='round' /> )} {isHover !== 'all' ? ( <text x='150' y='155' fontSize='20' fontWeight={600} textAnchor='middle' > {isHover === 'post' && `포스트 ${post}회`} {isHover === 'comment' && `댓글 ${commnet}회`} {isHover === 'like' && `좋아요 ${like}회`} </text> ) : ( <text x='150' y='155' fontSize='20' fontWeight={600} fill='#4b5563' textAnchor='middle' > 전체 {total}회 </text> )} </svg>
각 영역에 hover여부를 파악해 스타일을 변경해주는 코드를 작성해줬다.
이제 마우스를 hover하면 해당 영역이 커지면서 데이터가 표시된다.
지금은 캡쳐를 하다보니 hover가 풀려서 이미지가 저렇게 표시된다. 실제로는 텍스트도 잘 표시된다.
추가적으로 마우스가 차트 밖으로 나갔을때 처리도 해주겠다.
const resetState = () => { setIsHover('all'); }; ... return ( <div onMouseLeave={resetState}> <svg viewBox='0 0 300 300' width='250' height='250'>
이렇게 svg 전체를 감싸서 마우스가 외부로 나가는 것을 감지해서 상태를 변경하도록 했다.
마무리
도넛 차트가 완성됬다. 뭔가 허전했던 프로필 창이 도넛 차트가 들어가니까 전보다 알찬 구성이 된것같다. 약간 아쉬운 것은 각 영역에 마우스 hover하면 옆 캘린더에서도 그 활동에 관련된 곳을 하이라이트해주면 좋긴 할것 같다. 그건 추후에 해봐야겠다.
이제 다음으론 github를 연동시켜서 커밋활동까지 포함시켜보겠다. 쉽지 않겠지만 해보겠다.
개의 댓글
1
왠 도넛 차트?
도넛 차트 만드는 여러 방법
svg로 도넛 차트 만들기
strokeDasharray
strokeDashoffset
도넛 차트에 필요한 값 설정
밋밋한 차트 꾸미기
차트에 데이터 표시해주기
마무리