본문 바로가기
Study/react.js

[React] 리액트 차트 라이브러리 Recharts.js 적용기(React chart library Recharts.js)

by 박히밍 2023. 6. 27.
반응형

[React] 리액트 차트 라이브러리 Recharts.js 적용기(React chart libray Recharts.js)

 
 
 

Recharts.js란?

Recharts.js란 리액트 기반 차트 라이브러리이다. 어쩌다보니 다니는 회사마다 CMS나 어드민 페이지만 주구장창 개발하게 되어서 그런지 그전에는 Chart.js를 사용했었는데 이번 회사에서는 Recharts.js를 사용하게 되었다.
 
 

리액트 차트 라이브러리 Recharts.js

 
 

공식문서 링크

https://recharts.org/en-US
 
 
 


 
 
 

설치방법

$ npm install recharts

 
 
 
 

적용예제

나의 경우 기간 설정에 따라 차트 데이터가 동적으로 Fetching 되어야 하고,
범례를 클릭할 때마다 차트가 전시/비전시 처리가 되어야 했다.
그리고 X / Y 축, 범례, 툴팁, 그리드 등을 ‘모.두’ 커스텀 해야 했어서 별거 아닌 듯 하지만 엄청 허덕였음 ㅠㅠ Recharts.js의 payload가 요소별로 넘어오는 구조가 다르다보니 삽질하면서 개발했다..!

 
 
 
 

1. Line/bar 차트

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)

 

vertical bar 차트 구현
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 차트

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;
  }
`;

 

반응형