[React] Recharts 차트 라이브러리 커스텀하는 방법

이은진·2021년 1월 27일
7
post-thumbnail
post-custom-banner

1. 커스텀 전

const UsageAndBill = ({ tabButtonKey }) => {
  const [usageStatus, setUsageStatus] = useState([]);

  useEffect(() => {
    axios.get('/data/SmartView/usageAndBill.json')
      .then((res) => {
      const dataTemp = res.data[tabButtonKey].map((data) => {
        return {
          xAxis: data.MR_HHMI,
          usage: data.F_AP_QT,
          usageLast: data.LYEAR_F_AP_QT,
          bill: data.KWH_BILL,
        };
      });
      setUsageStatus([{}, ...dataTemp, {}]);
    });
  }, [tabButtonKey]);

  return (
    <ResponsiveContainer>
      <ComposedChart
        width={600}
        height={400}
        data={usageStatus}
        margin={{ top: 40, right: 40, bottom: 30, left: 40 }}
      >
        <CartesianGrid stroke="#f5f5f5" />
        <XAxis dataKey="xAxis" />
        <YAxis yAxisId="left" />
        <YAxis yAxisId="right" orientation="right" />
        <Tooltip />
        <Legend />
        <Bar yAxisId="left" dataKey="bill" barSize={30} fill="#7ac4c0" />
        <Line yAxisId="right" type="monotone" dataKey="usage" stroke="#ff7300" />
        <Line yAxisId="right" type="monotone" dataKey="usageLast" stroke="#8884d8" />
      </ComposedChart>
    </ResponsiveContainer>
  );
};

한눈에도 아쉬운 점이 눈에 보인다. 먼저 왼쪽, 오른쪽의 y축 단위가 명시돼 있지 않고, 분류별 x축 단위 또한 제대로 나와 있지 않고 백엔드에서 보내 주는 데이터 형식 그대로 되어 있다. 또 x축 마진을 주는 방법을 제대로 찾아보지 않고, 데이터의 앞뒤에 setUsageStatus([{}, ...dataTemp, {}]) 이렇게 빈 객체를 넣어서 야매로 구성했기 때문에, 엄밀히 말해 정확한 차트가 아니었다. 또 가격 천단위 콤마가 없는 것도 신경쓰이고, 범례도 날것 그대로의 키값으로 되어 있다.

이제 이것을 차례로 모두 커스텀해 볼 것이다.

2. 커스텀 과정

1. y축 단위를 원하는 위치에 추가하기

  <YAxis 
    yAxisId="left"
    label={{ value: '원', offset: 30, angle: 0, position: 'top' }}
  />
  <YAxis
    yAxisId="right"
    label={{ value: 'kWh', offset: 30, angle: 0, position: 'top' }}
  />

label 속성값으로 객체를 넣어 주면 된다. yAxisId는 왼쪽, 오른쪽 y축 단위라는 뜻인데 해당하는 축의 value에 원하는 단위를 넣어주고, 위치와 각도를 조절해준다. 옆으로 누운 형태로 만들고 싶다면 angle에 -90 또는 90 값을 준다.

2. x축 눈금 단위 설정하기, x/y축 천단위 콤마 찍기

  const formatXAxis = (tickItem) => {
    if (tickItem && tabButtonKey === "hourlyUsage") return `${tickItem.slice(0,2)}`
    else if (tickItem && tabButtonKey === "monthlyUsage") return `${tickItem}`
    else if (tickItem && tabButtonKey === "yearlyUsage") return `${tickItem}`
    else return tickItem
  };
  const formatYAxis = (tickItem) => tickItem.toLocaleString();
  const formatTooltip = (tickItem) => tickItem.toLocaleString();

formatXAxis, formatXAxis, formatTooltip 이라는 함수를 만들어준다. tickItem이라는 인자를 받아, 그 값으로 포맷팅을 한 상태로 리턴하는 함수다. 나는 차트 위에 있는 탭버튼의 값에 따라서 눈금 단위가 달라지기 때문에 if문으로 구분하여 각각 다른 값을 리턴하도록 했다.

<XAxis 
  dataKey="xAxis"
  tickFormatter={formatXAxis}
/>
<Tooltip
  formatter={formatTooltip}
  labelFormatter={formatXAxis}
/>

XAxis의 props로 formatXAxis 함수를 넣어주면 포맷팅이 완료된다.

3. x축 양끝 간격 설정하기

<XAxis 
  dataKey="xAxis"
  padding={tabButtonKey === "yearlyUsage" ? { left: 450, right: 450 } : { left: 40, right: 40 }} 
  tickFormatter={formatXAxis}
/>

XAxis 컴포넌트에 padding 속성값을 설정한다. 내 경우 년도별 버튼을 눌렀을 때와 아닐 때의 padding값을 다르게 설정하고 싶었기 때문에 삼항연산자를 사용해서 설정해주었다.

4. 범례 설정하기

  useEffect(() => {
    axios.get('/data/SmartView/usageAndBill.json')
      .then((res) => {
      const dataTemp = res.data[tabButtonKey].map((data) => {
        return {
          xAxis: data.MR_HHMI,
          '사용량(kWh)': data.F_AP_QT,
          '전년 사용량(kWh)': data.LYEAR_F_AP_QT,
          '요금(원)': data.KWH_BILL,
        };
      });
      setUsageStatus(dataTemp);
    });
  }, [tabButtonKey]);

useEffect에서 데이터를 state에 담아 올 때 객체의 key값을 내가 설정하고 싶은 범례 값으로 바꾸었다. 나중에 Bar, Line 등의 컴포넌트에 dataKey props를 설정해줄 때 자동으로 적용된다.

3. 완성된 코드

그 외 세세한 부분을 추가로 커스텀해서 완성해봤다.

const UsageAndBill = ({ tabButtonKey }) => {
  const classes = useStyles();
  const [usageStatus, setUsageStatus] = useState([]);

  useEffect(() => {
    axios.get('/data/SmartView/usageAndBill.json')
      .then((res) => {
      const dataTemp = res.data[tabButtonKey].map((data) => {
        return {
          xAxis: data.MR_HHMI,
          '사용량(kWh)': data.F_AP_QT,
          '전년 사용량(kWh)': data.LYEAR_F_AP_QT,
          '요금(원)': data.KWH_BILL,
        };
      });
      setUsageStatus(dataTemp);
    });
  }, [tabButtonKey]);

  const formatXAxis = (tickItem) => {
    if (tickItem && tabButtonKey === "hourlyUsage") return `${tickItem.slice(0,2)}`
    else if (tickItem && tabButtonKey === "monthlyUsage") return `${tickItem}`
    else if (tickItem && tabButtonKey === "yearlyUsage") return `${tickItem}`
    else return tickItem
  };
  const formatYAxis = (tickItem) => tickItem.toLocaleString();
  const formatTooltip = (tickItem) => tickItem.toLocaleString();

  return (
    <ResponsiveContainer className={classes.chartRoot}>
    <ComposedChart
      data={usageStatus}
      margin={{ top: 80, right: 40, bottom: 30, left: 40 }}
    >
    <CartesianGrid stroke="#f5f5f5" />
      <XAxis 
        dataKey="xAxis"
        padding={tabButtonKey === "yearlyUsage" ? { left: 450, right: 450 } : { left: 40, right: 40 }} 
        tickFormatter={formatXAxis}
      />
      <YAxis 
        type="number" 
        yAxisId="left"
        label={{ value: '원', offset: 30, angle: 0, position: 'top' }}
        tickFormatter={formatYAxis}
      />
      <YAxis
        yAxisId="right"
        orientation="right"
        label={{ value: 'kWh', offset: 30, angle: 0, position: 'top' }}
        tickFormatter={formatYAxis}
      />
      <Tooltip
        cursor={{ strokeDasharray: '3 3' }}
        formatter={formatTooltip}
        labelFormatter={formatXAxis}
      />
      <Legend />
      <Bar
        yAxisId="left"
        dataKey="요금(원)"
        barSize={tabButtonKey === "yearlyUsage" ? 150 : 30}
        fill="#7ac4c0"
      />
      <Line
        yAxisId="right"
        type="monotone"
        dataKey="사용량(kWh)"
        stroke="#fcac8d"
        strokeWidth="2"
        animationDuration="400"
      />
      <Line
        yAxisId="right"
        type="monotone"
        dataKey="전년 사용량(kWh)"
        stroke="#8fa3d1"
        hide={tabButtonKey === "yearlyUsage" && true}
        strokeWidth="2"
        animationDuration="500"
      />
    </ComposedChart>
  </ResponsiveContainer>
  );
};

커스텀 설정 설명이 그렇게 친절하지는 않지만 매우 다양하고 섬세한 편이다. 만족!

profile
빵굽는 프론트엔드 개발자
post-custom-banner

0개의 댓글