Node.js
(Node.js) 서버에서 차트 그려서 이미지로 변환
JJeongHyun
2024. 2. 25. 13:06
반응형
● 순서
1. 요지
2. 기능 설명
3. 라이브러리 설치
4. 코드 설명
5. 마무리
1. 요지
- 특정 데이터에 대한 차트를 매번 그려서 브라우저에 과부하를 주기보다는 가능하다면 서버에서 차트를 그린다
- 그린 차트를 이미지로 만들어서 보내줄 수 있는지 없는지 있다면, 그렇게 해준다면 브라우저에 과부하를 막자
- 원하는 차트를 빠르게 출력할 수 있지 않을까 라는 ..... 작은... 생각...
몇 번의 검색과 OpenAI에게 물어본 결과로는 가능한 이야기였다
2. 기능 설명
총 2가지의 라이브러리가 도입됐다
-
- chart.js : 데이터로 차트를 그리는 라이브러리
- 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) {
// ... 생략 ...
}
};
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')}`})