본문 바로가기
  • 개발 / 공부 / 일상
Node.js

(Node.js) 서버에서 차트 그려서 이미지로 변환

by JJeongHyun 2024. 2. 25.
반응형

● 순서


1. 요지

2. 기능 설명

3. 라이브러리 설치

4. 코드 설명

5. 마무리

 

 

1. 요지

  • 특정 데이터에 대한 차트를 매번 그려서 브라우저에 과부하를 주기보다는 가능하다면 서버에서 차트를 그린다
  • 그린 차트를 이미지로 만들어서 보내줄 수 있는지 없는지 있다면, 그렇게 해준다면 브라우저에 과부하를 막자
  • 원하는 차트를 빠르게 출력할 수 있지 않을까 라는 ..... 작은... 생각...

몇 번의 검색과 OpenAI에게 물어본 결과로는 가능한 이야기였다

 

2. 기능 설명

총 2가지의 라이브러리가 도입됐다

    1. chart.js : 데이터로 차트를 그리는 라이브러리
    2. chartjs-node-canvas : chartjs를 노드(서버)에서 그릴 수 있게 도와주는 라이브러리
  • 차트를 이미지로 변환하기에 애니메이션, 마우스 hover효과 기능이 없다
  • 이미지의 크기를 정할 수 있다
  • 차트의 모양을 정할 수 있다
  • 차트의 라벨지, X, Y축의 라벨지, 범례 등 설정 가능
  • 차트의 색상 등 CSS효과를 넣을 수 있다

API 작동 순서 요약
1. 원하는 속성에 대한 값을 요청받는다
1-1. 차트의 모양, 각 라벨지들의 이름, 보일지 말지, 선의 색깔, 내부 색깔, 크기, 확장자 등등
2. chartJS로 데이터에 해당하는 녀석들을 종합해서 그린다
3. 그린 차트를 buffer형식으로 변환한다
3-1. chart-node-canvas라이브러리를 통해서 변환
4. 변환된 buffer형식의 차트를 원하는 확장자의 이미지로 응답한다

 

라고 생각하고 만들어 보려고 한다

결론부터 말하면 조금.. 힘들었던 거 같다... 허허

 

3. 라이브러리 설치

npm i chart.js chartjs-node-canvas

 

4. 코드 설명

controller.ts

const generateLineChart = async (req: Request, res: Response) => {
  try {
   let chartObj.type = 'line';
	/*
    // 데이터 형식
    chartObj.data = [
      { x: 1, y: 2 },
      { x: 2, y: 3 },
      { x: 4, y: 5 },
    ];
    */
    chartObj.options = setupOptions('line', 0, 45, 0, 50);

    const image = await generateImage(chartObj);

    res
      .writeHead(200, {
        'Content-Type': 'image/png',
        'Content-Length': image.length,
      })
      .end(image);
  } catch (error) {
    console.log(error);
    res.status(500).json(error);
  }
};
  • 차트의 모양은 선 그래프를 기준으로 만든다고 가정 (type = 'line')
  • 차트를 그릴 데이터의 형식은 하나의 객체에 x, y축에 넣을 값을 배열형식으로 정의 (data)
    • x축, y축의 데이터 형식이 숫자가 아니어도 된다. 다만,  편의상 숫자로 구현

option.ts

// ... 생략 ...
switch(chartOption){
	option = {
          plugins: {
            //x,y축에 대한 범례 표시 안함
            legend: { display: false },
            datalabels: { display: false },
          },
          scales: {
            // x축에 대한 설정
            x: {
              // 선형의 형식
              type: 'linear',
              // x축의 수직된 기준선 제거
              grid: { display: false },
              min: 0, // x축에서의 최소값
              max: 45, // x축에서의 최대값
            },
            y: {
              type: 'linear',
              min: 0,
              max: 50,
              // y축에 표시하는 값들의 구간 설정
              ticks: {
                stepSize: 10,
              },
            },
          },
        }
	break;
}      
// ... 생략 ...
  • 차트에 대한 옵션을 설정 (option)
    •  차트에 대한 전체적인 제목, 부제목, x&y축에 대한 범례 설정
    • 각 x, y축 마다의 기준선 표시 여부
    • 최소/최대 범위
    • 표시되는 틱마다의 간격
    • x,y축 표시 여부 
    • 기타 등등

 

createImage.ts

import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
import { IChart, INodeCanvas } from '../interfaces';

const generateImage = async ({
  type,
  data,
  options,
}: {
  type: IChart['type'];
  data?: IChart['data'];
  options?: IChart['options'];
}): Promise<Buffer> => {
  let chartJSNodeCanvas: INodeCanvas = new ChartJSNodeCanvas({
    width: 292,
    height: 180,
  });

  const image: Buffer = await chartJSNodeCanvas.renderToBuffer({
    type,
    data,
    options,
  });
  return image;
};

export { generateImage };
  • chartNodeCanvas : 쉽게 말해서 차트의 너비와 높이를 설정

 

위 속성들에 따른 그래프 결과 값(데이터 다름주의)

5. 마무리

차트(그래프)를 생성하고 그리는 것은 온전히 프론트엔드의 영역인 줄 알았다

하지만 서버에서도 차트를 생성하여 랜더링 후 이미지 형태로 용이하게 클라이언트에서 사용할 수 있는 경험 할 수 있었던 거 같다

 

※ 차트를 생성 후 이미지로 변환하는 API (type 별 다양한 그래프 그려버리기~)
const generateChartToImage = async (req: Request, res: Response) => {
  try {
    const { type, step, horizon, combo, multi } = req.query;

    const checkType: string = Object.values(CHART_TYPES).find(
      (item) => item == type,
    );

    // 제공되는 차트의 형식이 아닌 문자열이 넘어 왔을 때 에러반환
    if (!checkType) // ... 생략 ...
    else {
      // 차트 데이터와 그의 속성(옵션), 차트에 대한 전반적인 속성(옵션)들을 담은 객체 선언
      let chartObj: IChartObj = setupChartObject(type.toString());

      let setExtractData: IData[] | number[] =
        horizon || combo || multi
          ? // 조건에 맞는 데이터 형식
          : // 조건에 맞는 데이터 형식2

      // 그래프의 x,y축에 범례의 최소값과 최대값을 구하기 위함
      const { minX, maxX, minY, maxY }: IMinMax =
        calculateMinMaxValues(// 차트 데이터);

      // 그래프 형식에 따른 조건 분류
      switch (type) {
        // 선 그래프 일 경우 꺾은선(No step), 계단식 그래프(step) 분류
        case 'line':
          if (step) {
          	 // ... 생략 ...
          } else {
            // ... 생략 ...
          }
          break;

        // 막대 그래프일 경우 수직, 수평으로 분류
        case 'bar':
          if (horizon) {
            // ... 생략 ...
          } else if (combo) {
            // ... 생략 ...
          } else {
            // ... 생략 ...
          }
          break;

        // pie형식과 도넛형식은 옵션이 같으므로 아래와 같이 설정
        case 'pie':
        case 'doughnut':
          if (multi) {
           // ... 생략 ...
          } else {
            // ... 생략 ...
          }
          break;
        case 'bubble':
          // ... 생략 ...
          break;
        case 'polarArea':
          // ... 생략 ...
          break;
        case 'radar':
         // ... 생략 ...
          break;
        case 'scatter':
          // ... 생략 ...
          break;
      }
      const image = await generateImage(chartObj);
      res
        .writeHead(200, {
          'Content-Type': 'image/png',
          'Content-Length': image.length,
        })
        .end(image);
    }
  } catch (error) {
 	// ... 생략 ...
  }
};

 

계단식 선 그래프 / 파이 그래프 / 도넛 그래프 / scatter 그래프
레이더 그래프 / 폴라영역 / 버블 그래프 / 콤보(선, 막대) 그래프

 

 

P.S) 이미지 자체로 반환하거나 이미지를 base64 포맷으로 반환할 수도 있던데 하하...

// 방식1
res
      .writeHead(200, {
        'Content-Type': 'image/png',
        'Content-Length': image.length,
      })
      .end(image);
      
// 방식2

res.json({path: `data:image/png;base64,${images.toString('base64')}`})