SVG를 활용해 도넛차트 만들기

예리에르·2021년 11월 9일
1

Frontend

목록 보기
2/10
post-thumbnail

왜 도넛차트?

회사에서 다음 프로젝트를 들어가기 전까지 여유시간이 생기면서 어떤걸 공부하지? 고민에 빠졌다. 회사의 서비스에는 차트가 많이 사용되는 만큼 빈 시간을 통해 '직접 SVG를 활용해서 차트를 구현하면 좋지 않을까' 생각하게 되었다. 여러 차트중 적은 데이터로 쉽게 나타낼 수 있는 파이차트 나아가 가운데 원이 생긴 도넛차트를 구현하기로 하였다.

1. stroke 특징을 이용해보자.

먼저 원형에서 원하는 크기만큼 조각을 나누기 위해서는 무엇보다 삼각함수가 중요하다.
삼각함수를 기본으로 storke의 특징을 이용해 나타내보았다.

삼각함수와 stroke에 대한 기본적인 이해는 해당 블로그를 통해 습득하였다.

storke를 통해 svg의 윤곽선을 그릴수 있다. stroke-dasharray stroke-dashoffset 등 다양한 속성을 통해 윤관선을 원하는 형태로 변화시킬수 있다.

기본적인 소성을 통해 삼각함수를 그려보자

생각의 흐름

  1. 원의 둘레를 알아야한다. (2*PI*radius)
  2. 표현하려는 데이터의 비율을 알아야한다,
  3. 365도에서 차지하는 데이터의 각도를 알아야한다.

이 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, 데이터를 한번에 빠르게 나타낼 수있지만 각 데이터 마다 애니메이션을 나타내려면 한계가 발생한다.

2. path 를 활용해 호를 직접 그려보자.

참고 블로그
위의 특성을 사용하기 위해서는 삼각함수의 이해가 가장 중요하다. 이후 호를 그리기 위한 원호 명령어의 이해가 중요하다. 원호 명령어는 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]}/>
                )
            })
        }
        </>
    };

이렇게 원하는 데이들이 모인 파이차트를 얻을 수있다.

3. 마지막 구멍을 뚫어보자!

여기까지 이해하고 왔다면 원형으로 그릴 수 있다고 생각한다. 현재 응용으로 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>

<!-흰색 픽셀 아래의 모든 것이 표시됩니다 .-->
<!-검은 색 픽셀 아래의 모든 항목이 표시되지 않습니다.->
속성을 통해 나타내고 싶은 영역과 뚫고 싶은 영역을 표현할 수 있다.

profile
비전공 프론트엔드 개발자의 개발일기😈 ✍️

0개의 댓글