SVG로 Radar차트를 개발한 경험을 공유해보고자 합니다!
Chart.js 공식 문서 A radar chart is a way of showing multiple data points and the variation between them. They are often useful for comparing the points of two or more different data sets.
Radar Chart
Radar Chart는 3개 이상의 데이터를 각 축별로 비교할 때 사용하면 좋은 그래프이다. 비교하고 싶은 축의 개수(n)에 맞게 다각형이 결정된다. 예를들어, 비교하고 싶은 축이 3개라면 삼각형의 차트가, 6개라면 육각형의 차트가 생성된다. 단 3개 이상의 축이 존재하여야 도형이 만들어진다는 점만 알아두자.
Radar Chart를 만드는 방법은 간단하다. 각도계산과 각도를 기반으로한 x,y 좌표만 알면 된다.
SVG 태그를 찾아보던 중에 다각형을 표현하는 polygon
태그를 알게 되었다.
Polygon
polygon 은 첫 번째 점과 마지막 점이 연결되어있는 닫힌 도형을 말한다. Polygon은 points라는 속성이 존재한다.
<svg>
<polygon points="100,10 150,190 50,190" style="fill:lime;stroke:purple;stroke-width:3" />
</svg>
points에 x,y좌표들을 넣어주면 다각형이 그려진다. points를 구해보자.
<script lang="ts">
function getPolygonCoordinate(data: number[], radius: number) {
// label은 여러개의 축들을 의미한다. ex. ['sweet', 'price', 'color', 'fresh', 'good']
// 축을 기준으로 다각형을 그릴 것이므로 각 data가 차지하는 angle은 360 / labels.length 이다.
const _angle = 360 / labels.length;
const _data = labels.map((d, i) => data[i] || 0);
return _data.map((d, i) => {
// 각 축의 값 h는 (데이터의 값 / 설정한 max 값) * radius로 나타낼 수 있다.
const h = (d / _maxRange) * radius;
// angle은 이전 angle이 축적되어 더해지므로 index를 곱해주었다.
const angle = _angle * i;
return {
label: labels[i],
dot: [
// angle에서 90을 뺀 이유는 처음 시작하는 축이 y축에 와야 하기 때문이다.
h * Math.cos((Math.PI / 180) * (-90 + angle)),
h * Math.sin((Math.PI / 180) * (-90 + angle))
]
};
});
}
</script>
...
<!--svelte에서는 #each가 반복문을 표현하는 방식이다. map을 의미한다고 생각하면 된다.-->
{#each datasets as set, index}
<!--svelte에서는 @const를 사용하면 해당 스코프에서만 local하게 사용할 변수라는 것을 의미한다.-->
{@const polygon = getPolygonCoordinate(set?.data, radius)}
<g class="graph-container">
<polygon
class="graph"
points={polygon.points}
fill-opacity={0.2}
stroke-opacity={1}
stroke-width={1}
stroke-dasharray={2}
/>
</g>
{/each}
다각형은 export props인 label의 개수를 기준으로 몇 각형인지 결정된다. datasets의 data의 max length값으로 판단해도 되지만, 명시적으로 label을 입력하여 해당 개수 만큼 다각형을 그려야 datasets의 데이터에 예상치 못한 값이 들어가도 대응이 가능하다고 생각했다.
각 축의 값은 반지름의 높이로 값을 주었다. 예를들어, apple은 price(축)가 만점이라고 가정하였을때, 즉 _maxRange
와 같은 값일때 const h = (d / _maxRange) * radius
에서 h 값이 1 * radius
가 나와서 가장 큰 값을 가지게된다.
**깨알🫘 태그 지식 ** >
<g/>태그
는 svg 태그 요소들을 그룹하하는데 사용한다.<g/>태그
로 묶어 하위 요소들에게 스타일링을 일괄적으로 주는 게 가능하다. 공통적인 묶음이 되는 요소들은<g/>태그
로 묶어주는게 좋다.
맨처음에 _maxRange는 props로 export해서 사용하였다. 하지만 datasets에 _maxRange보다 큰 값이 들어온다면 범위를 초과하여 그래프가 그려진다.
위와 같은 경우를 막기 위해서 로직을 하나 더 추가해 주었다. _maxRange
에 주목해주길 바란다.
<script lang="ts">
export let datasets: Chart.RadarChartDataSetProps[] = [];
export let maxRange: number = 1000;
export let labels: { title: string; sub?: string }[] = [];
$: _maxRange = (() => {
const max = Math.max(
..._(datasets)
.map((d) => d.data)
.flatten()
.value()
);
return max > maxRange ? max : maxRange;
})();
</script>
datasets중에 가장 큰 값(max)을 구한 뒤에, max가 maxRange 보다 크다면 max, 아닌 경우 maxRange로 값을 설정하였다.
위 로직대로 라면 maxRange보다 큰 값이 들어왔을때, 그래프가 엇나가지 않고 max값이 가장 큰 기준이 되어 그려질 것이다.
가이드 라인 그리기
필자가 첨부한 사진에서는 가이드 라인이 보인다. 레이더 차트에서는 해당 축이 어떤 요소인지, 그리고 값이 어느정도에 위치하는지를 나타내는 가이드 라인이 필요하다.
가이드라인은 위에서 사용하였던 getPolygonCoordinate
함수를 사용하여 그릴 수 있다.
<script lang="ts">
...
export let maxRange: number = 1000;
export let minRange: number = 0;
export let baseline: number = 6;
$: polygonGuide = (() => {
const interval = Math.ceil((_maxRange - minRange) / baseline);
const guideRanges = _.range(interval, _maxRange + interval, interval);
return guideRanges.map((guide) => {
return getPolygonCoordinate(Array(labels.length).fill(guide), radius);
});
})();
</script>
<svg width="100%" height="100%">
<g>
{#each polygonGuide as guide}
<polygon
points={guide.points}
stroke-width={0.5}
stroke={'black'}
fill="transparent"
opacity={0.3}
/>
{/each}
</g>
</svg>
maxRange
, minRange
값으로 최소 얼마 부터 최대 얼마까지 표현할지를 정한다.
baseline
은 도형 내부에 몇개의 도형 라인이 그려질지를 의미한다.
const interval = Math.ceil((_maxRange - minRange) / baseline)
을 통해 각 라인의 폭을 정한다.
guideRanges
는 interval(가장 작은 폭)부터 _maxRange + interval
까지 interval
간격으로 배열을 만든다.
스타일링
각 polygon의 꼭짓점에 dot을 찍어서 디자인 적인 요소를 추가 하였다.
점을 찍을 때 circle
svg 태그를 사용하였다. 필자는 점에 그라데이션 스타일링을 주고 싶었으며 구글링중에 radialGradient
태그에 대해 알게 되었다.
radialGradient은 css의 태그중 radial-gradient
와 유사해보이지만, svg에만 사용할 수 있는 태그이다. 기능은 비슷해보이나, radial-gradient
와는 다르게 radialGradient는 태그로 사용한다.
<svg>
<defs>
<radialGradient id="radialG-1" cx="50%" cy="50%" ="50%" fx="20%" fy="20%">
<stop offset="0%" stop-color="#ffc054" />
<stop offset="100%" stop-color="#ff614b" />
</radialGradient>
</defs>
<rect
fill="url(#radialG-1)"
="0" ="0"
width="100" height="100" />
</svg>
radialGradient 태그는 id값을 부여한 뒤에 사용하고 싶은 태그에서 해당 id를 fill내부에 쓰면된다. stop
요소를 사용하여 그라데이션을 표현 할 수 있다.
유사한 태그로 LinearGradient가 있다.
radialGradient
과 같은 디자인적인? 태그들은 defs
태그 내부에 써주는게 좋다.
defs
요소 내에 선언된 그래픽은 바로 렌더링 되지 않는다. defs
를 나중에 사용될 그래픽 객체를 저장하는 용도이며, 참조될 요소로 간주되기에 문서의 전반적인 접근에 유리하다고 한다.
Tada-! 예쁜 레이터 차트 완성이다. 그리는거 너무 재밌다.. 재미져.. 예쁘게 컴포넌트가 나와서 기분이 참 좋다 .. ^ ^~