- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 코인차트
- typeorm
- 3주차
- Firebase
- 항해99
- apollo
- javascript
- nestjs
- 주식
- 차트구현
- typescript
- 채팅
- graphql
- react
- chart
- 비전공자
- 차트만들기
- nextjs
- websocket
- 에러
- Coin
- 주식차트
- Flutter
- rtk
- API
- Redux
- 코인
- error
- 리액트
- 차트
Act99 기술블로그
[React] 직접 주가 캔들 차트 & 거래량 바 차트 만들기3 (라이브러리 x) (SVG 연습용) 본문
먼저 오늘 한 결과물은 이렇다.
저번 시간에는 차트구현까지는 했으나, 데이터 값 대비 차트 구현이 정확하지 않았으며,
화면 리사이즈를 할 때 오류가 생겼다. (몇개의 데이터만 변화)
따라서 유지보수에도 편하고, 화면 리사이즈 시 자연스럽게 변하며, 차트를 좀 더 정밀하게 구현할 필요가 있었다.
먼저, 화면 리사이즈부터 다루었다.
ReactHook 을 이용해 사이즈 변화상태마다 width, height 값을 구할 예정이다.
(tailwindCSS 를 사용하면 편하지만, 정확한 width 값과 height 값이 필요했으며, Hook 연습도 필요했기 때문에 직접 구현했다.)
Hook Docs를 보면 useWindowSize 를 이용하는 코드가 나와있다.
(공부를 목적으로 하면 직접 타이핑하면서 작성할 것을 추천드립니다.)
interface Size {
width: number;
height: number;
}
function useWindowSize(): Size {
const [windowSize, setWindowSize] = useState<Size>({
width: 0,
height: 0,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
다음 페이지에 useWindowSize 함수를 넣고 size 값을 차트에 보내주었다.
export const Stock = () => {
const size: Size = useWindowSize();
...
...
...
...
<div>
<HandmadeChart
width={size.width}
height={size.height}
그다음 먼저 작업한 것은 기존 차트의 size를 반응형으로 바꿔줄 차례이다.
console.log(width, height)을 한 결과
undefined 가 종종 발생했기 때문에 type이 number 가 아니면 0으로 변환시켜주었다.
또한 세로선을 12개 만들어주어 date 를 구현시켰다.
세로선 당 date값을 구해주는 함수는 다음과 같다.
const xValue: string[] = [];
const generateDate = () => {
console.log(date[Math.round(9.121345)]);
for (let i = 0; i < 12; i++) {
xValue.push(date[Math.round(date.length / 12) * i]);
}
// xValue.reverse();
// console.log(xValue);
return xValue;
};
generateDate();
후에, 마우스 scroll 을 할 때 데이터 값을 array에 추가하고 빼서 차트의 확대 축소 기능을 구현할 예정이며,
날짜는 총 데이터 중 12개의 데이터를 특정 간격으로 추출했는데,
최대 축소는 12개의 데이터 값으로 할 예정이기 때문에 12번 반복하는 반복문을 사용했다.
그 다음, 해외 비트코인 거래소 차트를 보니 날짜의 폰트사이즈는 화면이 아주 작아지면 글자가 같이 작아지지만,
금액의 폰트사이즈는 일정했다.
따라서 이 역시 구현시켰다.
그 코드는 다음과 같다.
type Props = {
width: number;
height: number;
date: string[];
open: number[];
close: number[];
high: number[];
low: number[];
volume: number[];
name: string[];
};
export const HandmadeChart: React.FC<Props> = ({
width,
height,
date,
open,
close,
high,
low,
volume,
name,
}) => {
// const [windowSize, setWindowSize] = useState({
// width: window.innerWidth,
// height: window.innerHeight,
// });
// const handleResize = () => {
// setWindowSize({ width: window.innerWidth, height: window.innerHeight });
// };
// useEffect(() => {
// window.addEventListener("resize", handleResize);
// return () => {
// window.removeEventListener("resize", handleResize);
// };
// }, []);
return (
<div className=" bg-chartGray-default flex-col flex">
<h3 className="text-white"> {width}</h3>
<h3 className="text-white"> {height}</h3>
<CandleChart
width={width}
height={height}
date={date}
open={open}
close={close}
high={high}
low={low}
name={name}
/>
{/* <VolumeChart date={date} volume={volume} /> */}
</div>
);
};
type CandleStickProps = {
width: number;
height: number;
date: string[];
open: number[];
close: number[];
high: number[];
low: number[];
name: string[];
};
const CandleChart: React.FC<CandleStickProps> = ({
width,
height,
date,
open,
close,
high,
low,
name,
}) => {
let SVG_CHART_WIDTH = typeof width === "number" ? width * 1 : 0;
let SVG_CHART_HEIGHT = typeof height === "number" ? height * 0.5 : 0;
const x0 = SVG_CHART_WIDTH * 0.015;
const y0 = SVG_CHART_HEIGHT * 0.015;
const xForPrice = 75;
const xAxisLength = SVG_CHART_WIDTH - xForPrice;
const yAxisLength = SVG_CHART_HEIGHT * 0.94;
// 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
);
const dateMax = dataArray.reduce(
(max, [date, open, close, high, low]) => Math.max(max, parseInt(date)),
-Infinity
);
const dateMin = dataArray.reduce(
(min, [date, open, close, high, low]) => Math.min(min, parseInt(date)),
+Infinity
);
const dateRange = dateMax - dateMin;
console.log(dataYMin);
const dataYRange = dataYMax - dataYMin;
const numYTicks = 7;
const numXTicks = 12;
const barPlothWidth = xAxisLength / dataArray.length;
const xValue: string[] = [];
const generateDate = () => {
console.log(date[Math.round(9.121345)]);
for (let i = 0; i < 12; i++) {
xValue.push(date[Math.round(date.length / 12) * i]);
}
// xValue.reverse();
// console.log(xValue);
return xValue;
};
generateDate();
return (
<div>
<svg width={SVG_CHART_WIDTH} height={SVG_CHART_HEIGHT}>
<line
x1={x0}
y1={yAxisLength - 10}
x2={xAxisLength}
y2={yAxisLength - 10}
stroke="gray"
/>
<line
x1={xAxisLength}
y1={y0}
x2={xAxisLength}
y2={yAxisLength - 10}
stroke="gray"
/>
<text
x={x0 + 15}
y={y0 + yAxisLength * 0.06}
fontSize={
SVG_CHART_WIDTH > 700
? SVG_CHART_WIDTH * 0.01
: SVG_CHART_WIDTH * 0.02
}
>
{name[name.length - 1]}
</text>
{/* 세로선 작성 */}
{Array.from({ length: numXTicks }).map((_, index) => {
const x = x0 + index * (xAxisLength / numXTicks) + 10;
return (
<g key={index}>
<line
className="lineLight"
x1={x}
x2={x}
y1={yAxisLength - 10}
y2={y0}
></line>
<text
x={x}
y={SVG_CHART_HEIGHT - 10}
textAnchor="middle"
fontSize={SVG_CHART_WIDTH < 800 ? 6 : 10}
>
{xValue[index]}
</text>
</g>
);
})}
{/* 가로선 작성(css name => lineLight) */}
{Array.from({ length: numYTicks }).map((_, index) => {
const y = y0 + index * (yAxisLength / numYTicks) + 10;
const yValue = Math.round(
dataYMax - index * (dataYRange / numYTicks)
);
return (
<g key={index}>
<line
className="lineLight"
x1={xAxisLength}
x2={x0}
y1={y}
y2={y}
></line>
<text x={SVG_CHART_WIDTH - 60} y={y} fontSize="12">
{yValue.toLocaleString()} ₩
</text>
</g>
);
})}
{dataArray.map(([day, open, close, high, low], index) => {
const x = x0 + index * barPlothWidth;
const sidePadding = 5;
const max = Math.max(open, close);
const min = Math.min(open, close);
const scaleY = scaleLinear()
.domain([dataYMin, dataYMax])
.range([y0, yAxisLength]);
const fill = close > open ? "#4AFA9A" : "#E33F64";
return (
<g key={index}>
<line
x1={x + sidePadding / 2 + (barPlothWidth - sidePadding) / 2}
y1={SVG_CHART_HEIGHT - scaleY(high) - y0}
x2={x + sidePadding / 2 + (barPlothWidth - sidePadding) / 2}
y2={SVG_CHART_HEIGHT - scaleY(low) - y0}
stroke={open > close ? "red" : "green"}
strokeWidth={fill}
/>
<rect
{...{ fill }}
x={x + sidePadding / 2}
y={SVG_CHART_HEIGHT - scaleY((max + min) / 2)}
width={barPlothWidth - sidePadding}
height={scaleY(max - min)}
></rect>
{/* <rect
fill={open > close ? "red" : "green"}
x={x + sidePadding / 2}
y={0}
width={1}
height={(SVG_CHART_HEIGHT * (high - low)) / dataYMax}
></rect> */}
</g>
);
})}
<line
x1={SVG_CHART_WIDTH - x0}
y1={y0}
x2={x0 + SVG_CHART_WIDTH}
y2={y0 + SVG_CHART_HEIGHT}
/>
</svg>
</div>
);
};
다음은 정확한 차트 바를 구현하는 것이었다.
여기서 사용한 것이 d3-scale 이다.
`npm i d3-scale`
`npm i --save-dev @types/d3-scale`
https://www.npmjs.com/package/d3-scale
이 패키지는 (가져온 데이터) -> (내가 사용할 사이즈 범위) 로 치환시킬 때 편리히다.
기본 개념은
scaleLinear().domain([내가 가져올 데이터의 최소, 내가 가져올 데이터의 최대]).range([내가 사용할(압축시킬) 범위의 최소, 내가 사용할 범위의 최대]) 이다. 그리고 이것으로 캔들차트를 잘 구현시켰다.
- 전체코드 (아래에 깃허브 링크를 남기겠습니다.)
import React, { useEffect, useState } from "react";
import { scaleLinear, ScaleLinear } from "d3-scale";
// date, open, close, high, low, volume
type Props = {
width: number;
height: number;
date: string[];
open: number[];
close: number[];
high: number[];
low: number[];
volume: number[];
name: string[];
};
export const HandmadeChart: React.FC<Props> = ({
width,
height,
date,
open,
close,
high,
low,
volume,
name,
}) => {
// const [windowSize, setWindowSize] = useState({
// width: window.innerWidth,
// height: window.innerHeight,
// });
// const handleResize = () => {
// setWindowSize({ width: window.innerWidth, height: window.innerHeight });
// };
// useEffect(() => {
// window.addEventListener("resize", handleResize);
// return () => {
// window.removeEventListener("resize", handleResize);
// };
// }, []);
return (
<div className=" bg-chartGray-default flex-col flex">
<h3 className="text-white"> {width}</h3>
<h3 className="text-white"> {height}</h3>
<CandleChart
width={width}
height={height}
date={date}
open={open}
close={close}
high={high}
low={low}
name={name}
/>
{/* <VolumeChart date={date} volume={volume} /> */}
</div>
);
};
type CandleStickProps = {
width: number;
height: number;
date: string[];
open: number[];
close: number[];
high: number[];
low: number[];
name: string[];
};
const CandleChart: React.FC<CandleStickProps> = ({
width,
height,
date,
open,
close,
high,
low,
name,
}) => {
let SVG_CHART_WIDTH = typeof width === "number" ? width * 1 : 0;
let SVG_CHART_HEIGHT = typeof height === "number" ? height * 0.5 : 0;
const xForPrice = 75;
const xAxisLength = SVG_CHART_WIDTH - xForPrice;
const yAxisLength = SVG_CHART_HEIGHT * 0.94;
const x0 = 0;
const y0 = 0;
// 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
);
const dateMax = dataArray.reduce(
(max, [date, open, close, high, low]) => Math.max(max, parseInt(date)),
-Infinity
);
const dateMin = dataArray.reduce(
(min, [date, open, close, high, low]) => Math.min(min, parseInt(date)),
+Infinity
);
console.log(dataYMin);
const dataYRange = dataYMax - dataYMin;
const numYTicks = 7;
const barPlothWidth = xAxisLength / dataArray.length;
const numXTicks = 12;
const xValue: string[] = [];
const generateDate = () => {
console.log(date[Math.round(9.121345)]);
for (let i = 0; i < 12; i++) {
xValue.push(date[Math.round(date.length / 12) * i]);
}
// xValue.reverse();
// console.log(xValue);
return xValue;
};
generateDate();
return (
<div>
<svg width={SVG_CHART_WIDTH} height={SVG_CHART_HEIGHT}>
<line
x1={x0}
y1={yAxisLength}
x2={xAxisLength}
y2={yAxisLength}
stroke="gray"
/>
<line
x1={xAxisLength}
y1={y0}
x2={xAxisLength}
y2={yAxisLength}
stroke="gray"
/>
<text
x={x0 + 15}
y={y0 + yAxisLength * 0.06}
fontSize={
SVG_CHART_WIDTH > 700
? SVG_CHART_WIDTH * 0.01
: SVG_CHART_WIDTH * 0.02
}
>
{name[name.length - 1]}
</text>
{/* 세로선 작성 */}
{Array.from({ length: numXTicks }).map((_, index) => {
const x = x0 + index * (xAxisLength / numXTicks) + 10;
return (
<g key={index}>
<line
className="lineLight"
x1={x}
x2={x}
y1={yAxisLength}
y2={y0}
></line>
<text
x={x}
y={SVG_CHART_HEIGHT}
textAnchor="middle"
fontSize={SVG_CHART_WIDTH < 800 ? 6 : 10}
>
{xValue[index]}
</text>
</g>
);
})}
{/* 가로선 작성(css name => lineLight) */}
{Array.from({ length: numYTicks }).map((_, index) => {
const y = y0 + index * (yAxisLength / numYTicks) + 10;
const yValue = Math.round(
dataYMax - index * (dataYRange / numYTicks)
);
return (
<g key={index}>
<line
className="lineLight"
x1={xAxisLength}
x2={x0}
y1={y}
y2={y}
></line>
<text x={SVG_CHART_WIDTH - 60} y={y} fontSize="12">
{yValue.toLocaleString()} ₩
</text>
</g>
);
})}
{dataArray.map(([day, open, close, high, low], index) => {
const x = x0 + index * barPlothWidth;
const sidePadding = xAxisLength * 0.0015;
const max = Math.max(open, close);
const min = Math.min(open, close);
const scaleY = scaleLinear()
.domain([dataYMin, dataYMax])
.range([y0, yAxisLength]);
const fill = close > open ? "#4AFA9A" : "#E33F64";
console.log(scaleY(max));
console.log(scaleY(min));
return (
<g key={index}>
<line
x1={x + xAxisLength * 0.002}
y1={yAxisLength - scaleY(low)}
x2={x + xAxisLength * 0.002}
y2={yAxisLength - scaleY(high)}
stroke={open > close ? "red" : "green"}
/>
<rect
{...{ fill }}
x={x}
y={yAxisLength - scaleY(max)}
width={barPlothWidth - sidePadding}
// 시가 종가 최대 최소값의 차
height={scaleY(max) - scaleY(min)}
></rect>
{/* <rect
fill={open > close ? "red" : "green"}
x={x + sidePadding / 2}
y={0}
width={1}
height={(SVG_CHART_HEIGHT * (high - low)) / dataYMax}
></rect> */}
</g>
);
})}
</svg>
</div>
);
};
이젠 스크롤로 데이터를 축소, 증가 시키는 작업을 실행할 것이다.
github: https://github.com/act99/stock-frontend
'개발팁저장소 > react' 카테고리의 다른 글
[React] 직접 주가 캔들 차트 만들기5 (이동평균선 추가) (SVG 연습용) (0) | 2021.12.01 |
---|---|
[React] 직접 주가 캔들 차트 & 거래량 바 차트 만들기4 - onWheel 을 통해 데이터값을 추가시키거나 감소시키기 (라이브러리 x) (SVG 연습용) (0) | 2021.12.01 |
[React] 직접 주가 캔들 차트 & 거래량 바 차트 만들기2 (라이브러리 x) (SVG 연습용) (1) | 2021.11.29 |
[React] 직접 주가 캔들 차트 & 거래량 바 차트 만들기 (라이브러리 x) (SVG 연습용) (0) | 2021.11.27 |
[React] 주식사이트 만들기 / React Google Chart 사용 (1) | 2021.11.26 |