Skip to content

Commit

Permalink
Merge pull request #88 from boostcampwm-2024/feature/layout/stock/det…
Browse files Browse the repository at this point in the history
…ail/chart-#60

[FE] 주식 차트 그리기 (라인 차트, bar 차트, 캔들 차트)
  • Loading branch information
dannysir authored Nov 12, 2024
2 parents 10b7b8a + 3462d8b commit 01db4dc
Show file tree
Hide file tree
Showing 5 changed files with 843 additions and 9 deletions.
10 changes: 6 additions & 4 deletions FE/src/components/Login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export default function Login() {
}[errorCode]
}
</p>
<form className='mb-2 flex flex-col' onSubmit={handleSubmit}>
<div className='my-10 flex flex-col gap-2'>
<form className='flex flex-col mb-2' onSubmit={handleSubmit}>
<div className='flex flex-col gap-2 my-10'>
<Input
type='text'
placeholder='아이디'
Expand All @@ -63,13 +63,15 @@ export default function Login() {
autoComplete='current-password'
/>
</div>
<button className='rounded-3xl bg-juga-blue-40 py-2 text-white transition hover:bg-juga-blue-50'>
<button className='py-2 text-white transition rounded-3xl bg-juga-blue-40 hover:bg-juga-blue-50'>
로그인
</button>
</form>
<button className='flex items-center justify-center gap-2 rounded-3xl bg-yellow-300 px-3.5 py-2 transition hover:bg-yellow-400'>
<ChatBubbleOvalLeftIcon className='size-5' />
<p>카카오 계정으로 로그인</p>
<a href='http://223.130.151.42:3000/auth/kakao'>
카카오 계정으로 로그인
</a>
</button>
</section>
</>
Expand Down
192 changes: 191 additions & 1 deletion FE/src/components/StocksDetail/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,193 @@
import { useEffect, useRef } from 'react';
import { dummy, DummyStock } from './dummy';

type Padding = {
top: number;
left: number;
right: number;
bottom: number;
};

export default function Chart() {
return <div className='flex-1 bg-orange-200'>차트</div>;
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);

useEffect(() => {
const parent = containerRef.current;
const canvas = canvasRef.current;

if (!canvas || !parent) return;

const displayWidth = parent.clientWidth;
const displayHeight = parent.clientHeight;

// 해상도 높이기
canvas.width = displayWidth * 4;
canvas.height = displayHeight * 4;

canvas.style.width = `${displayWidth}px`;
canvas.style.height = `${displayHeight}px`;

const ctx = canvas.getContext('2d');
if (!ctx) return;

ctx.fillStyle = 'white';

ctx.fillRect(0, 0, canvas.width, canvas.height);

const padding = {
top: 10,
right: 10,
bottom: 10,
left: 10,
};

const chartWidth = canvas.width - padding.left - padding.right;
const chartHeight = canvas.height - padding.top - padding.bottom;
const volumeBoundary = chartHeight * 0.2; // chartHeight의 20%
const mainHeight = chartHeight - volumeBoundary;

drawLineChart(ctx, dummy, chartWidth, mainHeight, padding);

drawBarChart(
ctx,
dummy,
chartWidth,
chartHeight,
padding,
0,
chartHeight * 0.8,
);

drawCandleChart(ctx, dummy, chartWidth, mainHeight, padding, 0, 0);
}, []);

return (
<div className='flex-1' ref={containerRef}>
<canvas ref={canvasRef} />
</div>
);
}

function drawLineChart(
ctx: CanvasRenderingContext2D,
data: DummyStock[],
width: number,
height: number,
padding: Padding,
x: number = 0,
y: number = 0,
) {
ctx.beginPath();

const n = data.length;

const yMax = Math.round(Math.max(...data.map((d) => d.low)) * 1.006 * 100);
const yMin = Math.round(Math.min(...data.map((d) => d.low)) * 0.994 * 100);

data.forEach((v, i) => {
const value = Math.round(v.low * 100);
const cx = x + padding.left + (width * i) / (n - 1);
const cy =
y + padding.top + height - (height * (value - yMin)) / (yMax - yMin);

if (i === 0) {
ctx.moveTo(cx, cy);
} else {
ctx.lineTo(cx, cy);
}
});

ctx.lineWidth = 1;
ctx.stroke();
}

function drawBarChart(
ctx: CanvasRenderingContext2D,
data: DummyStock[],
width: number,
height: number,
padding: Padding,
x: number,
y: number,
) {
ctx.beginPath();

const yMax = Math.round(Math.max(...data.map((d) => d.volume)) * 1.006 * 100);
const yMin = Math.round(Math.min(...data.map((d) => d.volume)) * 0.994 * 100);

const gap = Math.floor((width / dummy.length) * 0.8);

data.forEach((e, i) => {
const value = Math.round(e.volume * 100);
const cx = x + padding.left + (width * i) / (dummy.length - 1);
const cy = padding.top + ((height - y) * (value - yMin)) / (yMax - yMin);

ctx.fillStyle = e.open < e.close ? 'red' : 'blue';
ctx.fillRect(cx, height, gap, -cy);
});

ctx.lineWidth = 2;
ctx.stroke();
}

function drawCandleChart(
ctx: CanvasRenderingContext2D,
data: DummyStock[],
width: number,
height: number,
padding: Padding,
x: number,
y: number,
) {
ctx.beginPath();

const yMax = Math.round(
Math.max(...data.map((d) => Math.max(d.close, d.open, d.high, d.low))) *
1.006 *
100,
);
const yMin = Math.round(
Math.min(...data.map((d) => Math.max(d.close, d.open, d.high, d.low))) *
0.994 *
100,
);

data.forEach((e, i) => {
ctx.beginPath();

const { open, close, high, low } = e;
const gap = Math.floor((width / dummy.length) * 0.8);
const cx = x + padding.left + (width * i) / (dummy.length - 1);

const openValue = Math.round(open * 100);
const closeValue = Math.round(close * 100);
const highValue = Math.round(high * 100);
const lowValue = Math.round(low * 100);

const openY =
y + padding.top + height - (height * (openValue - yMin)) / (yMax - yMin);
const closeY =
y + padding.top + height - (height * (closeValue - yMin)) / (yMax - yMin);
const highY =
y + padding.top + height - (height * (highValue - yMin)) / (yMax - yMin);
const lowY =
y + padding.top + height - (height * (lowValue - yMin)) / (yMax - yMin);

if (open > close) {
ctx.fillStyle = 'blue';
ctx.strokeStyle = 'blue';
ctx.fillRect(cx, closeY, gap, openY - closeY);
} else {
ctx.fillStyle = 'red';
ctx.strokeStyle = 'red';
ctx.fillRect(cx, openY, gap, closeY - openY);
}

const middle = cx + Math.floor(gap / 2);

ctx.moveTo(middle, highY);
ctx.lineTo(middle, lowY);
ctx.stroke();
});
}
37 changes: 34 additions & 3 deletions FE/src/components/StocksDetail/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
export default function Header() {
return (
<div className='flex justify-between w-full h-16'>
<div>삼성전자</div>
<div>당기순이익</div>
<div className='flex items-center justify-between w-full h-16 px-2'>
<div className='flex flex-col font-semibold'>
<div className='flex gap-2 text-sm'>
<h2>삼성전자</h2>
<p className='text-juga-grayscale-200'>005930</p>
</div>
<div className='flex items-center gap-2'>
<p className='text-lg'>60,900원</p>
<p>어제보다</p>
<p className='text-juga-red-60'>+1800원 (3.0%)</p>
</div>
</div>
<div className='flex gap-4 text-xs font-semibold'>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>당기순이익</p>
<p>9조 8,143억</p>
</div>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>영업이익</p>
<p>10조 4,439억</p>
</div>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>매출액</p>
<p>74조 683억</p>
</div>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>시총</p>
<p>361조 1,718억</p>
</div>
<div className='flex gap-2'>
<p className='text-juga-grayscale-200'>PER</p>
<p>14.79배</p>
</div>
</div>
</div>
);
}
Loading

0 comments on commit 01db4dc

Please sign in to comment.