반응형

Grafana에서 Business Chart를 사용해서 누적형 그래프를 그리는 방법을 설명하겠습니다.

 

• 데이터 구조

 legend   xvalue   yvalue 
 A   2074-01    138,938
 B   2074-01      31,065
 C   2074-01       9,509
 D   2074-01      22,073
 A   2074-02    121,677
 B   2074-02      31,899
 C   2074-02      12,903
 D   2074-02      21,503

 

• 그래프

 

 

• Script 코드

const dataMap = {}; 							// 시즌별 데이터 저장용
const xLabelsSet = new Set(); 	// x축 레벨 값들
let minYValue = Infinity;      			// y축 최소값 계산용

context.panel.data.series.forEach((series) => {
  const legendField = series.fields.find(f => f.name.toLowerCase().includes("legend"));		// 범례
  const xvalueField = series.fields.find(f => f.name.toLowerCase().includes("xvalue"));			// x축에 표시할 값
  const yvalueField = series.fields.find(f => f.name.toLowerCase().includes("yvalue"));			// y축에 표시할 값
  if (!legendField || !xvalueField || !yvalueField) return;

  const legends = legendField.values;		// 범례 배열
  const xvalues = xvalueField.values;		// x축 값 배열
  const values = yvalueField.values;			// y축 값 배열

  for (let i = 0; i < legends.length; i++) {
    const legend = legends.get(i);
    const xvalue = xvalues.get(i);
    const yvalue = values.get(i);
    xLabelsSet.add(xvalue);

    if (!dataMap[legend]) {
      dataMap[legend] = {};
    }
    dataMap[legend][xvalue] = yvalue;

    // 최소값 갱신
    if (typeof yvalue === 'number' && yvalue < minYValue) {
      minYValue = yvalue;
    }
  }
});

// x축 정렬된 레벨 값들 (문자열 기준 정렬)
const xLabels = Array.from(xLabelsSet).sort((a, b) => a - b);

// 시리즈 생성
const series = Object.keys(dataMap).map(legend => {
  const xValueMap = dataMap[legend];
  const displayData = xLabels.map(xvalue => xValueMap[xvalue] ?? null);
  // 최고값 찾기
  let maxIndex = -1;
  let maxValue = -Infinity;
  displayData.forEach((v, i) => {
    if (typeof v === 'number' && v > maxValue) {
      maxValue = v;
      maxIndex = i;
    }
  });

  return {
    name: legend,
    type: "line",
    stack: "total",
    data: displayData,
    symbolSize: 4,    // 점의 크기
    areaStyle: {
      opacity: 0.3    // 투명도 조절 (0 ~ 1 사이 값)
    },
  };
});

return {
  grid: {
    bottom: "4.5%",
    containLabel: true,
    left: "3%",
    right: "4%"
  },
  tooltip: {
    trigger: "item",
    axisPointer: {
      type: 'cross',
    },
    formatter: function (params) {
      // 숫자 포맷: 천단위 구분
      const value = typeof params.data === 'number'
        ? params.data.toLocaleString('ko-KR')
        : params.data;
      return `
        ${params.marker}
        <strong style="color:${params.color}">${params.seriesName}</strong>: ${value}
      `;
    }
  },
  legend: {
    name: '시즌',
    data: Object.keys(dataMap)
  },
  xAxis: {  // x축 설정
    name: '',           // 축 이름 텍스트.
    nameLocation: "middle", // "start", "middle", "end" 가능. 이름의 위치 지정.
    nameGap: 25,            // 축 이름과 축 간의 거리(px)
    nameTextStyle: {        // 폰트 크기, 두께 등 스타일 설정
      fontSize: 14,
      fontWeight: 'bold'
    },
    boundaryGap: false,
    type: "category",
    data: xLabels, 
    axisLabel: {
      margin: 25,       // 레이블과 축 간 거리
      fontSize: 13      // ← 글씨 크기 설정 (예: 12px)
    }
  },
  yAxis: {  // y축 설정
    name: '',
    nameTextStyle: {
      fontSize: 14,
      fontWeight: 'bold'
    },
    type: "value",
    min: (minYValue * 0.85).toFixed(0)
  },
  series
};
반응형

+ Recent posts