Victory.js 사용하기
victory js를 사용해보았다.
워낙 문서가 잘 되어있어서 기본적인 차트 그리기에는 어려움이 없었지만 과제의 요구사항에 따라서 추가적으로 구현하려니 어려움을 겪었다.
여러 번의 삽질을 통해 일단 기본적인 레이아웃까지는 완성했다.
과제의 요구사항에서 첫 번째 그래프의 y축은 왼쪽에, 두 번째 그래프의 y축은 오른쪽에 표시하라고 되어 있었다. 둘의 단위가 비슷하다면 쉽게 구현했겠지만 하나는 백 단위고, 하나는 십 단위라서 한 그래프에 비슷하게 보여주는 것이 어려웠다. 😥
바로 코드 고고
일단 샘플로 사용할 데이터는 아래와 같다.
Chart.tsx
const data = [
{
click: 559,
conv: 37,
date: '2022-02-01',
},
{
click: 690,
conv: 34,
date: '2022-02-02',
},
{
click: 693,
conv: 53,
date: '2022-02-03',
},
{
click: 693,
conv: 50,
date: '2022-02-04',
},
]
types/dashboard.d.ts
export interface IDailyData {
click: number
conv: number
date: string
}
dashboard.d.ts 파일에 interface로 데이터의 타입을 만들어준다.
utils.ts
import { IDailyData } from 'types/dashboard'
type Data = {
x: string
y: number
}
export const convertData = (data: IDailyData[]) => {
const click: Data[] = []
const conv: Data[] = []
data.forEach((d) => {
click.push({
x: d.date,
y: d.click,
})
conv.push({
x: d.date,
y: d.conv,
})
})
return {
click,
conv,
}
}
먼저 Data 타입을 선언해준다. x축에 string 타입의 날짜가 들어갈 것이기 때문에 string, y축에 숫자값이 들어가기 때문에 number로 만들어 준다.
click과 conv 그래프를 날짜별로 각각 그려줘야하기 때문에 convertData라는 함수 안에 click, conv라는 배열을 선언한 후, 데이터를 forEach로 돌면서 각각의 배열에 넣어준다.
다시 Chart.tsx
type Data = {
x: string
y: number
}
방금 만들어준 배열을 써줘야 하기 때문에 아까와 같이 type을 만들어 준다.
import { IDailyData } from 'types/dashboard'
import { convertData } from './utils'
const { click, conv } = convertData(data as IDailyData[])
방금 만들어주었던 convertData 함수를 불러와 아까 선언해 준 더미 데이터를 넘겨준다. 이 때 이 데이터는 아까 만들어준 IDailyData들의 배열이므로 타입을 IDailyData[]로 지정해준다.
// 데이터의 y값 중 최대값 찾는 함수
const maxNum = (d: Data[]) => {
return d.reduce((max, p) => (p.y > max ? p.y : max), d[0].y)
}
// 입력받은 최대값을 가공한 후 리턴하는 함수
const maxima = (mNum: number) => {
const strMaxNum = mNum.toString()
const firstDigit = Number(strMaxNum.substring(0, 1)) + 1 // 첫 째 자리수를 구한 후 +1.
const squre = 10 ** (strMaxNum.length - 1) // 문자열의 길이만큼 제곱.
return firstDigit * squre
}
<VictoryChart theme={VictoryTheme.material} {...options} domain={{ y: [0, 1] }}>
<VictoryAxis />
<VictoryAxis
dependentAxis
offsetX={50}
tickLabelComponent={<VictoryLabel dy={15} textAnchor='start' />}
style={{
axis: { stroke: 'none' },
tickLabels: { fill: 'black' },
}}
tickValues={[0.2, 0.4, 0.6, 0.8, 1]}
tickFormat={(t) => (t * maxima(maxNum(click))).toLocaleString()}
/>
<VictoryAxis
dependentAxis
offsetX={910}
tickLabelComponent={<VictoryLabel dy={15} textAnchor='start' />}
style={{
axis: { stroke: 'none' },
tickLabels: { fill: 'black' },
}}
tickValues={[0.2, 0.4, 0.6, 0.8, 1]}
tickFormat={(t) => (t * maxima(maxNum(conv))).toLocaleString()}
/>
</VictoryChart>
일단 VictoryChart로 전체 차트를 감싸준 후, domain={{ y: [0, 1] }} 을 입력해 y의 범위를 지정해준다. (단위가 다른 두 개의 그래프를 한 차트에 같이 그려주기 위함이다.)
VictoryAxis를 그냥 써주면 알아서 날짜별로 x축을 만들어준다.
이제 문제는 y축인데.. 🥲
VictoryAxis에 dependentAxis 옵션을 주면 y축을 그려준다.
offsetX옵션에 숫자값을 주면 y축의 위치를 정할 수 있으니 여러 값을 입력해보면서 원하는 위치에 넣으면 된다.
이 때 중요한 건 tickValues와 tickFormat이다!
tickValues로 tick을 몇 개를 줄 지, 어떤 값을 줄 지 정할 수 있다. 초반에 domain으로 y 범위를 0~1로 제한했으므로 1보다 작은 값의 배열로 지정해주면 된다.
tickFormat을 이용해 tick을 어떻게 보여줄 지를 정할 수 있다.
아까 만든 maxNum 함수에 click을 넣어주어 click 배열의 y값의 최대값을 구한 후, 그 최대값을 maxima 함수에 넘겨주어 백단위로 깔끔하게 만들어준다. (593 -> 600으로)
그렇게 계산한 값을 t와(t는 아까 만든 tickValues 배열.) 곱해주면 된다.
그리고 숫자를 3자리 단위로 콤마를 찍어주기 위해 toLocaleString을 붙여준다.
<VictoryChart theme={VictoryTheme.material} {...options} domain={{ y: [0, 1] }}>
<VictoryAxis />
<VictoryAxis
dependentAxis
offsetX={50}
tickLabelComponent={<VictoryLabel dy={15} textAnchor='start' />}
style={{
axis: { stroke: 'none' },
tickLabels: { fill: 'black' },
}}
tickValues={[0.2, 0.4, 0.6, 0.8, 1]}
tickFormat={(t) => (t * maxima(maxNum(click))).toLocaleString()}
/>
<VictoryAxis
dependentAxis
offsetX={910}
tickLabelComponent={<VictoryLabel dy={15} textAnchor='start' />}
style={{
axis: { stroke: 'none' },
tickLabels: { fill: 'black' },
}}
tickValues={[0.2, 0.4, 0.6, 0.8, 1]}
tickFormat={(t) => (t * maxima(maxNum(conv))).toLocaleString()}
/>
<VictoryLine
name='click'
data={click}
animate={{
duration: 2000,
onLoad: { duration: 1000 },
}}
style={{
parent: { border: '1px solid #ccc' },
}}
y={(datum) => datum.y / maxima(maxNum(click)}
/>
<VictoryLine
name='conv'
data={conv}
animate={{
duration: 2000,
onLoad: { duration: 1000 },
}}
style={{
data: { stroke: 'red' },
parent: { border: '1px solid #ccc' },
}}
y={(datum) => datum.y / maxima(maxNum(conv)}
/>
</VictoryChart>
그 후 VictoryLine을 추가해 본격적으로 차트를 그려주면 된다.
data에 각각 click과 conv를 넣어준 후, y값을 조정해준다.
아까 tick에 maxima(maxNum(click)) 값을 곱해주었기 때문에 y값은 maxima(maxNum(click))으로 나누어주면 된다.
이렇게 하면 일단 완성!
전체 코드
Chart.tsx
import { VictoryAxis, VictoryChart, VictoryLabel, VictoryLine, VictoryTheme } from 'victory'
import { IDailyData } from 'types/dashboard.d'
import { convertData } from './utils'
const data = [
{
imp: 51479,
click: 559,
cost: 371790,
conv: 37,
convValue: 3668610,
ctr: 1.09,
cvr: 6.62,
cpc: 665.1,
cpa: 10048.38,
roas: 986.74,
date: '2022-02-01',
},
{
imp: 53385,
click: 690,
cost: 387181,
conv: 34,
convValue: 2870740,
ctr: 1.29,
cvr: 4.93,
cpc: 561.13,
cpa: 11387.68,
roas: 741.45,
date: '2022-02-02',
},
{
imp: 71403,
click: 693,
cost: 407050,
conv: 53,
convValue: 3065225,
ctr: 0.97,
cvr: 7.65,
cpc: 587.37,
cpa: 7680.19,
roas: 753.03,
date: '2022-02-03',
},
{
imp: 71010,
click: 693,
cost: 429057,
conv: 50,
convValue: 4190550,
ctr: 0.98,
cvr: 7.22,
cpc: 619.13,
cpa: 8581.14,
roas: 976.69,
date: '2022-02-04',
},
]
type Data = {
x: string
y: number
}
const Graph = (): JSX.Element => {
const { click, conv } = convertData(data as IDailyData[])
const options = {
width: 960,
height: 400,
}
const maxNum = (d: Data[]) => {
return d.reduce((max, p) => (p.y > max ? p.y : max), d[0].y)
}
const maxima = (mNum: number) => {
const strMaxNum = mNum.toString()
const firstDigit = Number(strMaxNum.substring(0, 1)) + 1
const squre = 10 ** (strMaxNum.length - 1)
return firstDigit * squre
}
return (
<div>
<VictoryChart theme={VictoryTheme.material} {...options} domain={{ y: [0, 1] }}>
<VictoryAxis />
<VictoryAxis
dependentAxis
offsetX={50}
tickLabelComponent={<VictoryLabel dy={15} textAnchor='start' />}
style={{
axis: { stroke: 'none' },
tickLabels: { fill: 'black' },
}}
tickValues={[0.2, 0.4, 0.6, 0.8, 1]}
tickFormat={(t) => (t * maxima(maxNum(click))).toLocaleString()}
/>
<VictoryAxis
dependentAxis
offsetX={910}
tickLabelComponent={<VictoryLabel dy={15} textAnchor='start' />}
style={{
axis: { stroke: 'none' },
tickLabels: { fill: 'black' },
}}
tickValues={[0.2, 0.4, 0.6, 0.8, 1]}
tickFormat={(t) => (t * maxima(maxNum(conv))).toLocaleString()}
/>
<VictoryLine
name='click'
data={click}
animate={{
duration: 2000,
onLoad: { duration: 1000 },
}}
style={{
parent: { border: '1px solid #ccc' },
}}
y={(datum) => datum.y / maxima(maxNum(click))}
/>
<VictoryLine
name='conv'
data={conv}
animate={{
duration: 2000,
onLoad: { duration: 1000 },
}}
style={{
data: { stroke: 'red' },
parent: { border: '1px solid #ccc' },
}}
y={(datum) => datum.y / maxima(maxNum(conv))}
/>
</VictoryChart>
</div>
)
}
export default Graph
아쉬운 부분도 있지만 일단 오늘은 여기까지 하고 내일은 초기 작업을 한 것을 토대로 코드도 수정하고, 다른 추가적인 기능도 해 볼 예정이다.
'Education > Wanted Pre-Onboarding FE Course' 카테고리의 다른 글
[TIL] 22.05.26 프리온보딩 Day 18 - Squash and merge (0) | 2022.05.27 |
---|---|
[TIL] 22.05.24 프리온보딩 Day 16 (0) | 2022.05.25 |
[TIL] 22.05.22 프리온보딩 Day 14 (0) | 2022.05.22 |
[TIL] 22.05.19 프리온보딩 Day 13 - Redux Toolkit (0) | 2022.05.20 |
[TIL] 22.05.18 프리온보딩 Day 12 - React Query (0) | 2022.05.19 |