59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
type PriceSparklineProps = {
|
|
/** 스텝별 가격 (시간순) */
|
|
prices: number[]
|
|
/** 점선 기준선으로 표시할 목표가 */
|
|
targetPrice: number
|
|
/** 그려질 비율 0~1 — 진행도에 맞춰 계단이 드러난다 */
|
|
progress: number
|
|
}
|
|
|
|
/** 계단식 가격 하락 라인 + 목표가 점선. 진행률만큼 클립으로 드러나는 미니 차트. */
|
|
function PriceSparkline({ prices, targetPrice, progress }: PriceSparklineProps) {
|
|
const W = 100
|
|
const H = 44
|
|
// 위아래 여백을 둔 가격 범위 매핑
|
|
const hi = Math.max(...prices, targetPrice)
|
|
const lo = Math.min(...prices, targetPrice)
|
|
const pad = (hi - lo) * 0.08 || 1
|
|
const MAX = hi + pad
|
|
const MIN = lo - pad
|
|
const y = (price: number) => ((MAX - price) / (MAX - MIN)) * H
|
|
|
|
const stepX = (i: number) => (i / (prices.length - 1)) * W
|
|
|
|
// step-after 계단 경로
|
|
let d = `M 0 ${y(prices[0]).toFixed(1)}`
|
|
for (let i = 1; i < prices.length; i++) {
|
|
d += ` L ${stepX(i).toFixed(1)} ${y(prices[i - 1]).toFixed(1)} L ${stepX(i).toFixed(1)} ${y(prices[i]).toFixed(1)}`
|
|
}
|
|
|
|
return (
|
|
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" className="w-full h-16 overflow-visible" aria-hidden>
|
|
<defs>
|
|
<clipPath id="price-spark-clip">
|
|
<rect x="0" y="-4" width={Math.max(0, Math.min(progress, 1)) * W} height={H + 8} style={{ transition: "width 0.5s ease-out" }} />
|
|
</clipPath>
|
|
</defs>
|
|
{/* 목표가 기준선 */}
|
|
<g className="text-primary/40">
|
|
<line
|
|
x1="0"
|
|
y1={y(targetPrice)}
|
|
x2={W}
|
|
y2={y(targetPrice)}
|
|
stroke="currentColor"
|
|
strokeWidth="1"
|
|
strokeDasharray="4 4"
|
|
vectorEffect="non-scaling-stroke"
|
|
/>
|
|
</g>
|
|
{/* 가격 하락 계단 — 진행률만큼 클립으로 드러남 */}
|
|
<g clipPath="url(#price-spark-clip)" className="text-primary">
|
|
<path d={d} fill="none" stroke="currentColor" strokeWidth="2.5" vectorEffect="non-scaling-stroke" strokeLinejoin="round" />
|
|
</g>
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
export { PriceSparkline }
|