반응형
[React] 리액트 차트 라이브러리 Recharts.js 적용기(React chart libray Recharts.js)
Recharts.js란?
Recharts.js란 리액트 기반 차트 라이브러리이다. 어쩌다보니 다니는 회사마다 CMS나 어드민 페이지만 주구장창 개발하게 되어서 그런지 그전에는 Chart.js를 사용했었는데 이번 회사에서는 Recharts.js를 사용하게 되었다.
공식문서 링크
설치방법
$ npm install recharts
적용예제
나의 경우 기간 설정에 따라 차트 데이터가 동적으로 Fetching 되어야 하고,
범례를 클릭할 때마다 차트가 전시/비전시 처리가 되어야 했다.
그리고 X / Y 축, 범례, 툴팁, 그리드 등을 ‘모.두’ 커스텀 해야 했어서 별거 아닌 듯 하지만 엄청 허덕였음 ㅠㅠ Recharts.js의 payload가 요소별로 넘어오는 구조가 다르다보니 삽질하면서 개발했다..!
1. Line/bar 차트
import React, { useState } from 'react';
import styled from 'styled-components';
import moment from 'moment';
import {
USER_CHART_LEGEND_FLUCTUATION,
USER_CHART_LEGEND_FLUCTUATION_DICTIONARY,
} from 'app.features/user/constants/userDashboardChartLegends';
import {
ResponsiveContainer,
ComposedChart,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
Legend,
Bar,
Line,
} from 'recharts';
import UserChartLegend from './UserChartLegend';
import UserChartTooltip from './UserChartTooltip';
const tickFormatX: any = (tickItem) => moment(tickItem).format('M/D');
const tickFormatY: any = (tickItem) => tickItem.toLocaleString();
const UserFluctuationChart = ({ data }) => {
const [disable, setDisable] = useState([]);
return (
<StyledWrapper>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={data}
margin={{
top: 20,
left: -25,
right: -10,
}}
>
<CartesianGrid strokeDasharray={0} vertical={false} orientation={0} />
<XAxis
dataKey="date"
style={chartStyle}
interval={data?.length >= 30 ? parseInt(`${data.length / 30}`) : 0}
tickLine={false}
axisLine={false}
tickFormatter={tickFormatX}
/>
<YAxis
yAxisId="left"
orientation="left"
style={chartStyle}
interval={0}
tickLine={false}
axisLine={false}
tickFormatter={tickFormatY}
/>
<YAxis
yAxisId="right"
orientation="right"
style={chartStyle}
interval={0}
tickLine={false}
axisLine={false}
tickFormatter={tickFormatY}
/>
<Tooltip
content={
<UserChartTooltip
options={USER_CHART_LEGEND_FLUCTUATION_DICTIONARY}
unit="명"
/>
}
/>
<Legend
content={
<UserChartLegend
disable={disable}
legendOptions={USER_CHART_LEGEND_FLUCTUATION}
setDisable={setDisable}
/>
}
payload={USER_CHART_LEGEND_FLUCTUATION}
/>
<CartesianGrid stroke="#f5f5f5" />
{disable.includes('create') ? null : (
<Bar
yAxisId="left"
dataKey="create"
barSize={20}
fill={USER_CHART_LEGEND_FLUCTUATION_DICTIONARY.create.color}
/>
)}
{disable.includes('delete') ? null : (
<Bar
yAxisId="left"
dataKey="delete"
barSize={20}
fill={USER_CHART_LEGEND_FLUCTUATION_DICTIONARY.delete.color}
/>
)}
{disable.includes('clientTotal') ? null : (
<Line
yAxisId="right"
dataKey="clientTotal"
stroke={
USER_CHART_LEGEND_FLUCTUATION_DICTIONARY.clientTotal.color
}
dot={false}
/>
)}
</ComposedChart>
</ResponsiveContainer>
</StyledWrapper>
);
};
export default UserFluctuationChart;
const StyledWrapper = styled.div`
height: 260px;
`;
const chartStyle = {
fontSize: '12px',
fill: '#b8b8b8',
fontWeight: 'bold',
};
2. bar 차트(vertical)
import styled from 'styled-components';
import { Tooltip } from 'antd';
import {
BarChart,
Bar,
YAxis,
XAxis,
Tooltip as RechartsTooltip,
ResponsiveContainer,
Cell,
} from 'recharts';
// 컬러 상수
const COLORS_DIC = {
uuid: '#0088FE',
naver: '#00C49F',
google: '#FFBB28',
apple: '#FF8042',
kakao: '#ba42ff',
facebook: '#ff429a',
};
// 가입 수단 체크 함수
const onChkMethod = (method) => {
let render = '';
switch (method) {
case 'uuid':
render = 'UUID';
break;
case 'google':
render = '구글';
break;
case 'naver':
render = '네이버';
break;
case 'apple':
render = '애플';
break;
case 'facebook':
render = '페이스북';
break;
case 'kakao':
render = '카카오';
break;
}
return render;
};
const UserJoinChart = ({ data }) => {
// 데이터가 없다면 렌더링 하지 않음
if (!data) return null;
const { cumulativeTotal, data: datasource } = data?.typeTotal;
return (
<StyledWrapper>
{/* 툴팁 영역 */}
<div className="user-dashboard-card-title small">
<Tooltip
title={<CutomTooltip />}
color={'white'}
overlayInnerStyle={{
padding: '16px',
color: '#000',
fontSize: '12px',
borderRadius: '4px',
}}
>
<span className="cursor">계(누적 가입 회원 수):</span>
</Tooltip>
<span>{` ${data ? cumulativeTotal?.toLocaleString() : 0}`}명</span>
</div>
{/* --툴팁 영역-- */}
{/* 차트 영역 */}
<ChartWrapper>
<ResponsiveContainer width="100%" height="100%">
<BarChart layout="vertical" data={datasource}>
<YAxis
dataKey="type"
type="category"
tickLine={false}
axisLine={false}
tick={<CustomYAxisLeft data={datasource} />}
/>
<YAxis
dataKey="avg"
type="category"
yAxisId="right"
orientation="right"
tickLine={false}
axisLine={false}
tick={<CustomYAxisRight data={datasource} />}
/>
<XAxis dataKey="avg" type="number" domain={[0, 100]} hide />
<RechartsTooltip
cursor={false}
content={<UserDashboardChartTooltip />}
/>
<Bar
dataKey="avg"
barSize={6}
background={{ fill: '#f3f3f3' }}
radius={[10, 10, 10, 10]}
>
{datasource?.map((entry, index) => {
return (
<Cell key={`cell-${index}`} fill={COLORS_DIC[entry.type]} />
);
})}
</Bar>
</BarChart>
</ResponsiveContainer>
</ChartWrapper>
{/* --차트 영역-- */}
</StyledWrapper>
);
};
export default UserJoinChart;
// 상단 툴팁 컴포넌트
const CutomTooltip = () => {
const strongStyle = {
display: 'inline-block',
fontWeight: 'bold',
};
return (
<div>
<strong style={strongStyle}>계(누적 가입 회원 수)</strong>
<div className="tooltip-content">탈퇴, 휴면 회원을 포함합니다.</div>
</div>
);
};
// 왼쪽 축 컴포넌트
const CustomYAxisLeft = (props: any) => {
const idx = props.index;
const method = props?.data[idx]?.type;
return (
<text
dy="0.355em"
x={props?.x}
y={props?.y}
fontSize="12"
fill="#999999"
fontWeight="bold"
textAnchor="end"
>
{onChkMethod(method)}
</text>
);
};
// 오른쪽 축 컴포넌트
const CustomYAxisRight = (props: any) => {
const idx = props.index;
const avg = props?.data[idx]?.avg;
return (
<text
dy="0.355em"
x={props?.x}
y={props?.y}
fontSize="12"
fill="#999999"
fontWeight="bold"
>
{`${avg}%`}
</text>
);
};
// 차트 툴팁 컴포넌트
const UserDashboardChartTooltip = (payload) => {
const { payload: currentPayload } = payload;
const type = currentPayload[0]?.payload?.type;
const avg = currentPayload[0]?.payload?.avg;
const count = currentPayload[0]?.payload?.count;
if (!payload) return null;
return (
<CustomTooltipWrapper>
<TooltipItem color={COLORS_DIC[type]}>
<div className="label">{onChkMethod(type)}</div>
<strong className="value">{`${avg}% (${count}명)`}</strong>
</TooltipItem>
</CustomTooltipWrapper>
);
};
// 전체를 감싼 styled 컴포넌트
const StyledWrapper = styled.div`
.user-dashboard-card-title {
display: block;
}
`;
// 차트를 감싼 styled 컴포넌트
const ChartWrapper = styled.div`
height: 260px;
`;
// 차트 툴팁 styled 컴포넌트
const CustomTooltipWrapper = styled.div`
padding: 16px;
background-color: #fff;
border-radius: 4px;
box-shadow: 2px 3px 8px #00000022;
`;
//차트 툴팁 payload item 컴포넌트
const TooltipItem = styled.div`
display: flex;
justify-content: left;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
border-radius: 50%;
background-color: ${(props) => `${props.color}`};
}
.label {
margin: 0 16px;
}
.value {
font-weight: bold;
}
`;
3. pie 차트
import styled from 'styled-components';
import { USER_CHART_LEGEND_AUTH } from 'app.features/user/constants/userDashboardChartLegends';
import { Tooltip } from 'antd';
import UserChartLegend from './UserChartLegend';
import {
PieChart,
Pie,
Cell,
Legend,
Tooltip as RechartsTooltip,
ResponsiveContainer,
} from 'recharts';
// 컬러 상수
const COLORS_DIC = {
y: '#5893E1',
n: '#FD9179',
};
const UserAuthChart = ({ data }) => {
if (!data) return null;
const { total, data: datasource } = data?.snsCertified;
return (
<StyledWrapper>
<div className="user-dashboard-card-title small">
<Tooltip
title={<CutomTooltip />}
color={'white'}
overlayInnerStyle={{
padding: '16px',
color: '#000',
fontSize: '12px',
borderRadius: '4px',
}}
>
<span className="cursor">계(SNS 가입 회원 수):</span>
</Tooltip>
<span>{` ${total.toLocaleString()}`}명</span>
</div>
<ChartWrapper>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
dataKey="avg"
data={datasource}
innerRadius={60}
outerRadius={80}
>
{datasource?.map((entry, index) => {
return (
<Cell key={`cell-${index}`} fill={COLORS_DIC[entry.type]} />
);
})}
</Pie>
<RechartsTooltip content={<UserDashboardChartTooltip />} />
<Legend
content={
<UserChartLegend legendOptions={USER_CHART_LEGEND_AUTH} />
}
/>
</PieChart>
</ResponsiveContainer>
</ChartWrapper>
</StyledWrapper>
);
};
export default UserAuthChart;
// 상단 툴팁 컴포넌트
const CutomTooltip = () => {
const strongStyle = {
display: 'inline-block',
fontWeight: 'bold',
};
return (
<div>
<strong style={strongStyle}>계(SNS 가입 회원 수)</strong>
<div className="tooltip-content">
탈퇴 회원은 제외합니다.
<br />
휴면 회원은 포함합니다.
</div>
</div>
);
};
// 차트 툴팁 컴포넌트
const UserDashboardChartTooltip = (payload) => {
const { payload: tooltipValue } = payload;
const type = tooltipValue[0]?.payload?.payload?.type;
const avg = tooltipValue[0]?.payload?.payload?.avg;
const count = tooltipValue[0]?.payload?.payload?.count;
if (!payload) return null;
return (
<CustomTooltipWrapper>
<TooltipItem color={COLORS_DIC[type]}>
<div className="label">{type}</div>
<strong className="value">{`${avg}% (${count}명)`}</strong>
</TooltipItem>
</CustomTooltipWrapper>
);
};
const StyledWrapper = styled.div`
width: 100%;
`;
// 차트를 감싼 styled 컴포넌트
const ChartWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 260px;
`;
// 차트 툴팁 styled 컴포넌트
const CustomTooltipWrapper = styled.div`
padding: 16px;
background-color: #fff;
border-radius: 4px;
box-shadow: 2px 3px 8px #00000022;
`;
//차트 툴팁 payload item 컴포넌트
const TooltipItem = styled.div`
display: flex;
justify-content: left;
position: relative;
text-transform: uppercase;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
border-radius: 50%;
background-color: ${(props) => `${props.color}`};
}
.label {
margin: 0 16px;
}
.value {
font-weight: bold;
}
`;
반응형
'Study > react.js' 카테고리의 다른 글
[React] React hooks 정리 part.2 useMemo(최적화) feat. React.memo (0) | 2023.02.28 |
---|---|
[React] React hooks 정리 part.1 - useState, useEffect, useRef, useContext + Context API (0) | 2023.02.20 |
[React] React Portal 기술을 이용하여 Modal을 만드는 방법(실습파일 하단 첨부) (0) | 2023.02.06 |
[React] React-query 정리 (0) | 2023.02.03 |
[React] 클래스형 vs 함수형 컴포넌트 차이 (class vs function component) (1) | 2022.12.08 |