Act99 기술블로그

[React] 직접 주가 캔들 차트 & 거래량 바 차트 만들기2 (라이브러리 x) (SVG 연습용) 본문

개발팁저장소/react

[React] 직접 주가 캔들 차트 & 거래량 바 차트 만들기2 (라이브러리 x) (SVG 연습용)

Act99 2021. 11. 29. 17:00

이번에는 캔들 차트를 만들 차례이다.

기본틀은 거래량 차트 만들기와 같으며,

캔들차트 구현이라는 점에서 다르다.

 

https://bugerstory.tistory.com/32

 

[React] 직접 주가 캔들 차트 & 거래량 바 차트 만들기 (라이브러리 x) (SVG 연습용)

React-Google-Chart 를 사용하던 중, 직접 차트를 만들고 싶은 충동이 생겼다. 먼저 handmade-chart.tsx 를 만들어주고 svg 를 이용해 차트 틀부터 만들어 주었다. (CSS 경우 tailwind를 사용하였다.) - handmade-..

bugerstory.tistory.com

 

해야 할 일

1. 데이터 불러오기

2. 금액 최소값과 최대값을 구한 후, 7개의 구분선으로 나누기.

3. 시가와 종가 데이터로 바 차트 구현하기

4. 고가와 저가 데이터로 얇은 바 차트 구현하기

5. 시가&종가 바와 고가&저가 바를 합쳐서 캔들차트 구현하기

 

이다.

 

먼저 해야할 일 1, 2, 3 을 작업했다.

저번 시간에 했던 거래량 바 차트 만들기와 유사하다.

다른 점은 바의 최저점이 0이 아니라 시가 또는 종가라는 점이다.

 

차트 바 구현 간략 계획
차트 바 위치 구현 계획

const CandleChart: React.FC<CandleStickProps> = ({
  date,
  open,
  close,
  high,
  low,
  name,
}) => {
  const x0 = 150;
  const xAxisLength = SVG_CHART_WIDTH - x0 * 2;
  const y0 = 50;
  const yAxisLength = SVG_CHART_HEIGHT - y0 * 2;
  const xAxisY = y0 + yAxisLength;

  const dataArray: [string, number, number, number, number][] = [];
  for (let i = 0; i < date.length; i++) {
    dataArray.push([date[i], open[i], close[i], high[i], low[i]]);
  }
  console.log(dataArray);
  const dataYMax = dataArray.reduce(
    (max, [_, open, close, high, low]) => Math.max(max, high),
    -Infinity
  );
  console.log(dataYMax);
  const dataYMin = dataArray.reduce(
    (min, [_, open, close, high, low]) => Math.min(min, low),
    +Infinity
  );
  console.log(dataYMin);
  const dataYRange = dataYMax - dataYMin;
  const numYTicks = 7;
  const barPlothWidth = xAxisLength / dataArray.length;
  return (
    <div>
      <svg width={SVG_CHART_WIDTH} height={SVG_CHART_HEIGHT}>
        <line x1={x0} y1={xAxisY} x2={x0 + xAxisLength} y2={xAxisY} />
        <text x={SVG_CHART_WIDTH - x0} y={xAxisY + 10}>
          {name[name.length - 1]}
        </text>
        {/* 가로선 작성(css name => lineLight) */}
        {Array.from({ length: numYTicks }).map((_, index) => {
          const y = y0 + index * (yAxisLength / numYTicks);
          const yValue = Math.round(
            dataYMax - index * (dataYRange / numYTicks)
          );
          return (
            <g key={index}>
              <line
                className="lineLight"
                x1={SVG_CHART_WIDTH - x0}
                x2={x0}
                y1={y}
                y2={y}
              ></line>
              <text x={x0 + xAxisLength + 80} y={y + 5} textAnchor="end">
                {yValue.toLocaleString()} ₩
              </text>
            </g>
          );
        })}
        {dataArray.map(([day, open, close, high, low], index) => {
          const openAndClose = [];
          openAndClose.push([open, close]);
          // console.log(open > close ? 1 : 2);
          console.log(open);
          console.log(close);
          console.log(openAndClose);
          const x = x0 + index * barPlothWidth;
          const sidePadding = 5;
          let yRatio = 0;
          const yRatioGenerator = () => {};
          return (
            <g key={index}>
              <rect
                fill={open > close ? "green" : "red"}
                x={x + sidePadding / 2}
                y={
                  open > close
                    ? (SVG_CHART_HEIGHT * ((open + close) / 2)) / dataYMax
                    : (SVG_CHART_HEIGHT * ((close + open) / 2)) / dataYMax
                }
                width={barPlothWidth - sidePadding}
                height={
                  open > close
                    ? (SVG_CHART_HEIGHT * (open - close)) / dataYMax
                    : (SVG_CHART_HEIGHT * (close - open)) / dataYMax
                }
              ></rect>
            </g>
          );
        })}
        <line
          x1={SVG_VOLUME_WIDTH - x0}
          y1={y0}
          x2={x0 + xAxisLength}
          y2={y0 + yAxisLength}
        />
      </svg>
    </div>
  );
};

 

그리고 실행해 본 결과..

 

 

차트가 뒤집혀 있는 것을 볼 수 있다. (차트 구현도 제대로 되지 않고 있는 것을 알 수 있다.)

그 이유는.....

 

svg의 y축은 아래로 그 값이 형성되는데, 이를 계산하지 않았기 때문이다.

(말로 설명이 어렵지만 쉽게 말해서 뒤집어주어야 한다.)

간략하게 말해서 애초부터 계획을 잘못 짠 것이었다... ㅎ.ㅎ

 

 

수정한 결과

 

          let yRatio = 0;
          const yRatioGenerator = (open: number, close: number) => {
            if (open > close) {
              yRatio = (open - dataYMin) / dataYRange;
              if (yRatio > 0) {
                return yRatio;
              } else return (yRatio = open / dataYRange / 2);
            } else {
              yRatio = (close - dataYMin) / dataYRange;
              if (yRatio > 0) {
                return yRatio;
              } else return (yRatio = open / dataYRange / 2);
            }
          };
          let yHighRatio = 0;
          const yHighRatioGenerator = (high: number) => {
            yHighRatio = (high - dataYMin) / dataYRange;
            if (yHighRatio > 0) {
              return yHighRatio;
            } else return (yRatio = high / dataYRange / 2);
          };
          const yOpenClose =
            y0 + (1 - yRatioGenerator(open, close)) * yAxisLength;
          const yHighLow = y0 + (1 - yHighRatioGenerator(high)) * yAxisLength;

 

 

 

 

얼핏보면 차트가 잘 구현된 것으로 보일 수 있으나

아직 제대로 구현이 되고 있지 않았다.

그 이유는 "시가 종가의 y 축의 scale 값 대비 y 축 시작점" 과 "고가 저가의 y축 scale 값 대비 y 축 시작점" 에서 미스가 났기 때문이다.

 

따라서 이를 구현하기 위해서 모든 데이터를 종합한 캔들스틱 컴퍼넌트를 직접 구현한 후, 차트상에 올리는 작업을 실시해야하며, 아예 다 갈아엎어야한다.

 

지금 바로 갈아엎기 작업을 실행하러 갈 예정이다.