Project๐Ÿ› ๏ธโš™๏ธ

[Project/ReactTS] Chart.js๋กœ ์›ํ˜• ์ฐจํŠธ(Doughnut Chart) ๊ตฌํ˜„

Gaeun Lee 2024. 1. 11. 16:59

์•„๋ž˜์™€ ๋™์ผํ•˜๊ฒŒ ์›ํ˜• ์ฐจํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ ์ž ํ•œ๋‹ค.

 

Setting

npm install chartjs
npm install --save chart.js react-chartjs-2

chartjs ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•œ๋‹ค

 

๊ธฐ๋ณธ์ ์ธ DoughnutChart ์ปดํฌ๋„ŒํŠธ ์ž‘์„ฑ

์ผ๋‹จ ์ •์  ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฐจํŠธ๋ฅผ ๊ทธ๋ ค๋ดค๋‹ค. ์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ๋Š” 'data' props๋ฅผ ํ•„์ˆ˜์ ์œผ๋กœ ํฌํ•จํ•ด์•ผ ํ•œ๋‹ค. ์ฐจํŠธ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋Š” string ๋ฐฐ์—ด 'labels'์™€ number ๋ฐฐ์—ด 'data'์ด๋‹ค. ์•„๋ž˜ ์˜ˆ์‹œ๋Š” ['๊ฐ€','๋‚˜','๋‹ค','๋ผ','๋งˆ'] ์™€ [1,2,3,4,5] ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์„ค์ •ํ•˜์˜€๋‹ค.  

DoughnutChart.tsx

import { Chart as ChartJS, ArcElement, Tooltip} from 'chart.js';
import { Doughnut } from 'react-chartjs-2';

export default function DoughnutChart() {
    ChartJS.register(ArcElement, Tooltip);
    const data = {
        labels: ['๊ฐ€','๋‚˜','๋‹ค','๋ผ','๋งˆ'],
        datasets: [
           {
              label: '๋™์˜ ์ˆ˜',
              data: [1,2,3,4,5],
              backgroundColor: ['#010101', '#868686', '#3A3A3A', '#C7C7C7', '#E9E9E9'],
              borderColor: ['#010101', '#868686', '#3A3A3A', '#C7C7C7', '#E9E9E9'],
           },
        ],
         },
     };
    return(<Doughnut data={data} />)
}

 

๋ฐ์ดํ„ฐ ๋ผ๋ฒจ์— ๋น„์œจ ์‚ฝ์ž…

chartjs-plugin-datalabels ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•œ๋‹ค.

npm install chartjs-plugin-datalabels --save

formatter๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ผ๋ฒจ์— ๋‚˜ํƒ€๋‚ผ ๊ฐ’์„ ๊ฐ€๊ณตํ•œ๋‹ค. ์•„๋ž˜ ์˜ˆ์‹œ๋Š” ๋น„์œจ์ด ๋‚˜ํƒ€๋‚˜๋„๋ก ํ•˜์˜€๋‹ค.

import { Chart as ChartJS, ArcElement, Tooltip} from 'chart.js';
import { Doughnut } from 'react-chartjs-2';
import ChartDataLabels from 'chartjs-plugin-datalabels'; // ์ถ”๊ฐ€

export default function DoughnutChart() {
    ChartJS.register(ArcElement, Tooltip, ChartDataLabels);  // ์ถ”๊ฐ€
    const data = {
        labels: ['๊ฐ€','๋‚˜','๋‹ค','๋ผ','๋งˆ'],
        datasets: [
           {
              label: '๋™์˜ ์ˆ˜',
              data: [1,2,3,4,5],
              backgroundColor: ['#010101', '#868686', '#3A3A3A', '#C7C7C7', '#E9E9E9'],
              borderColor: ['#010101', '#868686', '#3A3A3A', '#C7C7C7', '#E9E9E9'],
              datalabels: {  // ์ถ”๊ฐ€
                color: 'white',
                font: {
                   size: 15,
                },
                formatter: (value: number) => {
                    const percentage = `${Math.floor((value / 15) * 100)}%`;
                    return percentage;
                 },
             },
           },
        ],
     };
    return(<Doughnut data={data} />)
}

 

์ฐจํŠธ ์ค‘์•™์— ์ดํ•ฉ ํ‘œ์‹œ

์ฐจํŠธ ์ปดํฌ๋„ŒํŠธ์— 'plugins' props๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ์ฐจํŠธ ์ค‘์•™์— ํ…์ŠคํŠธ๋ฅผ ๋ฐฐ์น˜ํ•œ๋‹ค. 'plugins' props์˜ ํƒ€์ž…์€ ๋ฐฐ์—ด์ด๊ณ , textCenter ๊ฐ์ฒด ๋ณ€์ˆ˜๋ฅผ ๋”ฐ๋กœ ์„ ์–ธํ•˜์—ฌ plugins ๋ฐฐ์—ด์— ๋„ฃ์–ด์คฌ๋‹ค.

import { Chart as ChartJS, ArcElement, Tooltip} from 'chart.js';
import { Doughnut } from 'react-chartjs-2';
import ChartDataLabels from 'chartjs-plugin-datalabels';

export default function DoughnutChart() {
    ChartJS.register(ArcElement, Tooltip, ChartDataLabels);
    const data = {
        labels: ['๊ฐ€','๋‚˜','๋‹ค','๋ผ','๋งˆ'],
        datasets: [
           {
              label: '๋™์˜ ์ˆ˜',
              data: [1,2,3,4,5],
              backgroundColor: ['#010101', '#868686', '#3A3A3A', '#C7C7C7', '#E9E9E9'],
              borderColor: ['#010101', '#868686', '#3A3A3A', '#C7C7C7', '#E9E9E9'],
              datalabels:
                color: 'white',
                font: {
                   size: 15,
                },
                formatter: (value: number) => {
                    const percentage = `${Math.floor((value / 15) * 100)}%`;
                    return percentage;
                 },
             },
           },
        ],
     };
     const textCenter = {  // ์ถ”๊ฐ€
        id: 'textCenter',
        afterDatasetsDraw: (chart: ChartJS<'doughnut', number[], unknown>) => {
           const ctx = chart.ctx;
           const xCoor = chart.chartArea.left + (chart.chartArea.right - chart.chartArea.left) / 2;
           const yCoor = chart.chartArea.top + (chart.chartArea.bottom - chart.chartArea.top) / 2;
           let total = 0;
           chart.data.datasets[0].data.forEach((item) => {
              if (typeof item === 'number') {
                 total += item;
              }
           });
           ctx.save();
           ctx.font = 'bold 20px sans-serif';
           ctx.textAlign = 'center';
           ctx.textBaseline = 'middle';
           ctx.fillText(`${total}๋ช…`, xCoor, yCoor);
        },
     };
    return(<Doughnut data={data} plugins={[textCenter]}/>)  // ์ถ”๊ฐ€
}

 

์ฐจํŠธ ๋ฐ์ดํ„ฐ ๋ถ€๋ชจ๋กœ๋ถ€ํ„ฐ ๋ฐ›์•„์˜ค๊ธฐ

๋ถ€๋ชจ๋กœ๋ถ€ํ„ฐ ์ฐจํŠธ ๋ฐ์ดํ„ฐ์™€ ์ดํ•ฉ์„ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ์ž‘์„ฑํ•˜์˜€๋‹ค. ์ฐจํŠธ ๋ฐ์ดํ„ฐ๋Š” labels์™€ data ํ”„๋กœํผํ‹ฐ๋ฅผ ์ง€๋‹Œ ๊ฐ์ฒด๋‹ค.

DoughnutChart.tsx

import { Chart as ChartJS, ArcElement, Tooltip} from 'chart.js';
import { Doughnut } from 'react-chartjs-2';
import ChartDataLabels from 'chartjs-plugin-datalabels';

export interface IChartData {
    labels: string[];
    data: number[];
 }

export default function DoughnutChart({chartData, sum} : {chartData: IChartData, sum: number}) {
    ChartJS.register(ArcElement, Tooltip, ChartDataLabels);
    const data = {
        labels: chartData.labels,
        datasets: [
           {
              label: '๋™์˜ ์ˆ˜',
              data: chartData.data,
              backgroundColor: ['#010101', '#868686', '#3A3A3A', '#C7C7C7', '#E9E9E9'],
              borderColor: ['#010101', '#868686', '#3A3A3A', '#C7C7C7', '#E9E9E9'],
              datalabels: {
                color: 'white',
                font: {
                   size: 15,
                },
                formatter: (value: number) => {
                    const percentage = `${Math.floor((value / sum ) * 100)}%`;
                    return percentage;
                 },
             },
           },
        ],
     };
     const textCenter = {
        id: 'textCenter',
        afterDatasetsDraw: (chart: ChartJS<'doughnut', number[], unknown>) => {
           const ctx = chart.ctx;
           const xCoor = chart.chartArea.left + (chart.chartArea.right - chart.chartArea.left) / 2;
           const yCoor = chart.chartArea.top + (chart.chartArea.bottom - chart.chartArea.top) / 2;
           let total = 0;
           chart.data.datasets[0].data.forEach((item) => {
              if (typeof item === 'number') {
                 total += item;
              }
           });
           ctx.save();
           ctx.font = 'bold 20px sans-serif';
           ctx.textAlign = 'center';
           ctx.textBaseline = 'middle';
           ctx.fillText(`${total}๋ช…`, xCoor, yCoor);
        },
     };
    return(<Doughnut data={data} plugins={[textCenter]}/>)
}

App.tsx

import "./App.css";
import DoughnutChart from "./DoughnutChart";

function App() {
  const chartData = {
    labels: ['๊ฐ€','๋‚˜','๋‹ค','๋ผ','๋งˆ'],
    data: [1,2,3,4,5]
  }
  const getSum = (data: number[]) => {
    let sum = 0
    data.forEach((item) => {
      sum += item
    })
    return sum
  }
  return (
      <DoughnutChart chartData={chartData} sum={getSum(chartData.data)}/>
  );
}

export default App;

 

์ด์™€ ๊ฐ™์ด ์ •์ ์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฐจํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

 

๋™์ ์ธ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ฐจํŠธ ๊ตฌํ˜„

๋งŒ์•ฝ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์˜€์„ ๋•Œ ์ฐจํŠธ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋œ๋‹ค๋ฉด ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ฐจํŠธ์— ๋ฐ˜์˜๋˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ฆฌ๋ Œ๋”๋ง์ด ํ•„์š”ํ•˜๋‹ค.

์•ž์„œ ๋งŒ๋“  DoughnutChart ์ปดํฌ๋„ŒํŠธ๋Š” ๋ถ€๋ชจ๋กœ๋ถ€ํ„ฐ ๋ฐ›์•„์˜จ data๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ๋ฆฌ๋ Œ๋”๋ง๋˜๋ฏ€๋กœ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ ์•Œ๋งž๊ฒŒ data๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค.

๊ทธ๋ž˜์„œ ๋ฒ„ํŠผ์˜ ํด๋ฆญ์‹œ ์ฐจํŠธ ์—…๋ฐ์ดํŠธ๋ฅผ ์•Œ๋ฆฌ๊ธฐ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด ์ •์˜ํ•˜์˜€๋‹ค

const [updatePost, setUpdatePost] = useState(false);

 

๊ทธ๋ฆฌ๊ณ  useEffect Hook์„ ํ†ตํ•˜์—ฌ updatePost์˜ ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ ๋‹ค์‹œ post๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ํ•˜์—ฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ฐ˜์˜๋˜๊ฒŒ ํ•˜์˜€๋‹ค.

   useEffect(() => {
      if (post !== undefined) {
         getSum(post);
         processData(post.statisticList);
      }
   }, [post, updatePost]);

 

๋Œ€๋žต์ ์ธ ์ฝ”๋“œ (์ƒ๋žต ์กด์žฌ)

export default function PetitionDetail() {
   const [updatePost, setUpdatePost] = useState(false);
   const [chartData, setChartData] = useState({ labels: [''], data: [0] });
   const { post: petition, postId } = useFetchPost<IPetition>({
      api: API_PATH.POST.PETITION.ROOT,
      update: updatePost,
   });
   const [sum, setSum] = useState(0);

   // ์ดํ•ฉ ๊ตฌํ•˜๊ธฐ
   const getSum = (post: IPetition) => {
      setSum(0);
      return post.statisticList.forEach((item) => setSum((prev) => prev + item.agreeCount)); // ๋‹จ๊ณผ๋Œ€ ์ด ํˆฌํ‘œ์ˆ˜
   };

  // ์ฒ˜์Œ์— ์ฒญ์›๊ธ€ ๋ถˆ๋Ÿฌ์˜ฌ ๊ฒฝ์šฐ์™€ ์—…๋ฐ์ดํŠธ ๊ฐ’ ๋ณ€๊ฒฝ์‹œ ์‹คํ–‰
  useEffect(() => {
      if (petition !== undefined) {
         getSum(petition);
         processData(petition.statisticList);
      }
   }, [petition, updatePost]);

   // ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต
   const processData = (data: IPetitionStatistic[]) => {
      setChartData({ labels: [], data: [] }); // ์ฐจํŠธ ์ดˆ๊ธฐํ™”
      /* ๋‹จ๊ณผ๋Œ€ ํˆฌํ‘œ ๋ฐ์ดํ„ฐ ๊ฐ€๊ณต */
      data.forEach((item) => {
         setChartData((prev) => {
            return {
               labels: [...prev.labels, item.department],
               data: [...prev.data, item.agreeCount],
            };
         });
      });
   };

   // ๋™์˜ ๋ฒ„ํŠผ ํด๋ฆญ ๋ฐ์ดํ„ฐ ์ „์†ก
   const handlePostAgree = async () => {
      try {
         await post(`${API_PATH.POST.PETITION.AGREE.ID(postId!)}`, null, {
            authenticate: true,
         });
         setUpdatePost(true);
      } catch (error) {
         alert;
      }
   };

   // ๋™์˜ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ ํ•จ์ˆ˜
   const handleAgreeButtonClick = () => {
      if (petition?.agree) {
         alert('์ด๋ฏธ ๋™์˜ํ•˜์…จ์Šต๋‹ˆ๋‹ค');
      } else {
         handlePostAgree();
      }
   };

   return (
      <>
         {petition !== undefined && (
            <>
 
                     <PostBox>
                        <Text length={4}>์–ด๋–ค ๊ณผ์—์„œ ๊ฐ€์žฅ ๋™์˜๋ฅผ ๋งŽ์ด ํ–ˆ์„๊นŒ์š”?</Text>
                        <hr />
                        <DoughnutChart chartData={chartData} sum={sum} />
                        <PetitonChartList statisticList={petition.statisticList} sum={sum} />
                     </PostBox>
               {/* ํ”Œ๋กœํŒ… ๋ฒ„ํŠผ */}
               <FloatingButton
                  event={() => {
                     handleAgreeButtonClick();
                  }}
               >
                  {petition.agree ? (
                     <TbThumbUpFilled color='white' size={40} />
                  ) : (
                     <TbThumbUp color='white' size={40} />
                  )}
               </FloatingButton>
             </>
         )}
      </>
   );
}

์™„์„ฑ !!