백테스팅 적용
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: string;
|
||||
pct: number;
|
||||
max?: number;
|
||||
tone?: 'cyan' | 'green' | 'amber' | 'red';
|
||||
}
|
||||
|
||||
const TONE: Record<string, { arc: string; glow: string }> = {
|
||||
cyan: { arc: '#22d3ee', glow: 'rgba(34, 211, 238, 0.45)' },
|
||||
green: { arc: '#34d399', glow: 'rgba(52, 211, 153, 0.45)' },
|
||||
amber: { arc: '#fbbf24', glow: 'rgba(251, 191, 36, 0.45)' },
|
||||
red: { arc: '#f87171', glow: 'rgba(248, 113, 113, 0.45)' },
|
||||
};
|
||||
|
||||
function polar(cx: number, cy: number, r: number, deg: number) {
|
||||
const rad = (deg * Math.PI) / 180;
|
||||
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
|
||||
}
|
||||
|
||||
/** 반원형 게이지 — Master Plan [01] MDD/Sharpe 스타일 */
|
||||
export default function DashboardGauge({ label, value, pct, max = 100, tone = 'cyan' }: Props) {
|
||||
const clamped = Math.max(0, Math.min(max, pct));
|
||||
const ratio = clamped / max;
|
||||
const start = 180;
|
||||
const sweep = 180 * ratio;
|
||||
const c = TONE[tone] ?? TONE.cyan;
|
||||
const r = 42;
|
||||
const cx = 50;
|
||||
const cy = 52;
|
||||
|
||||
const p0 = polar(cx, cy, r, start);
|
||||
const pEnd = polar(cx, cy, r, start + sweep);
|
||||
const p180 = polar(cx, cy, r, start + 180);
|
||||
const large = sweep > 90 ? 1 : 0;
|
||||
const arcPath = sweep > 0
|
||||
? `M ${p0.x} ${p0.y} A ${r} ${r} 0 ${large} 1 ${pEnd.x} ${pEnd.y}`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div className="gc-gauge">
|
||||
<svg viewBox="0 0 100 62" className="gc-gauge-svg" aria-hidden>
|
||||
<path
|
||||
d={`M ${p0.x} ${p0.y} A ${r} ${r} 0 1 1 ${p180.x} ${p180.y}`}
|
||||
fill="none"
|
||||
stroke="color-mix(in srgb, var(--se-border) 80%, transparent)"
|
||||
strokeWidth="6"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{arcPath && (
|
||||
<path
|
||||
d={arcPath}
|
||||
fill="none"
|
||||
stroke={c.arc}
|
||||
strokeWidth="6"
|
||||
strokeLinecap="round"
|
||||
style={{ filter: `drop-shadow(0 0 4px ${c.glow})` }}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
<div className="gc-gauge-body">
|
||||
<span className="gc-gauge-value">{value}</span>
|
||||
<span className="gc-gauge-label">{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
pct: number;
|
||||
}
|
||||
|
||||
/** Master Plan [01] — 세로 인디케이터 바 */
|
||||
export default function DashboardIndicatorBar({ label, pct }: Props) {
|
||||
const blocks = 10;
|
||||
const filled = Math.round(Math.max(0, Math.min(100, pct)) / 100 * blocks);
|
||||
return (
|
||||
<div className="gc-ind-bar">
|
||||
<span className="gc-ind-bar-label">{label}</span>
|
||||
<div className="gc-ind-bar-stack" aria-hidden>
|
||||
{Array.from({ length: blocks }, (_, i) => {
|
||||
const idx = blocks - 1 - i;
|
||||
const on = idx < filled;
|
||||
let tone = 'gc-ind-block--mid';
|
||||
if (idx >= 7) tone = 'gc-ind-block--hi';
|
||||
else if (idx <= 2) tone = 'gc-ind-block--lo';
|
||||
return <span key={i} className={`gc-ind-block${on ? ` ${tone} gc-ind-block--on` : ''}`} />;
|
||||
})}
|
||||
</div>
|
||||
<span className="gc-ind-bar-pct">{Math.round(pct)}%</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
valueLabel: string;
|
||||
pct: number;
|
||||
segments?: number;
|
||||
tone?: 'cyan' | 'green' | 'amber' | 'red';
|
||||
}
|
||||
|
||||
const FILL: Record<string, string> = {
|
||||
cyan: '#22d3ee',
|
||||
green: '#34d399',
|
||||
amber: '#fbbf24',
|
||||
red: '#f87171',
|
||||
};
|
||||
|
||||
export default function DashboardSegmentBar({ label, valueLabel, pct, segments = 12, tone = 'cyan' }: Props) {
|
||||
const filled = Math.round(Math.max(0, Math.min(100, pct)) / 100 * segments);
|
||||
const color = FILL[tone] ?? FILL.cyan;
|
||||
return (
|
||||
<div className="gc-seg-bar">
|
||||
<div className="gc-seg-bar-head">
|
||||
<span className="gc-seg-bar-label">{label}</span>
|
||||
<span className="gc-seg-bar-val">{valueLabel}</span>
|
||||
</div>
|
||||
<div className="gc-seg-bar-track" role="meter" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100}>
|
||||
{Array.from({ length: segments }, (_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="gc-seg-bar-block"
|
||||
style={{ background: i < filled ? color : undefined, boxShadow: i < filled ? `0 0 6px ${color}55` : undefined }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
interface Props {
|
||||
series: number[];
|
||||
label?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
function sparkPath(values: number[], w: number, h: number): string {
|
||||
if (values.length < 2) return '';
|
||||
const lo = Math.min(...values);
|
||||
const hi = Math.max(...values);
|
||||
const range = hi - lo || 1;
|
||||
return values.map((v, i) => {
|
||||
const x = (i / (values.length - 1)) * w;
|
||||
const y = h - 8 - ((v - lo) / range) * (h - 16);
|
||||
return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
export default function DashboardSparkChart({ series, label, height = 140 }: Props) {
|
||||
const w = 480;
|
||||
const path = useMemo(() => sparkPath(series, w, height), [series, height]);
|
||||
const area = path ? `${path} L ${w},${height} L 0,${height} Z` : '';
|
||||
const last = series.length ? series[series.length - 1] : 0;
|
||||
|
||||
return (
|
||||
<div className="gc-spark-chart">
|
||||
{label && (
|
||||
<div className="gc-spark-chart-head">
|
||||
<span>{label}</span>
|
||||
<strong>{last.toFixed(1)}/s</strong>
|
||||
</div>
|
||||
)}
|
||||
<svg viewBox={`0 0 ${w} ${height}`} preserveAspectRatio="none" className="gc-spark-chart-svg">
|
||||
<defs>
|
||||
<linearGradient id="gcSparkGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#22d3ee" stopOpacity="0.55" />
|
||||
<stop offset="100%" stopColor="#6366f1" stopOpacity="0.05" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{area && <path d={area} fill="url(#gcSparkGrad)" />}
|
||||
{path && (
|
||||
<path d={path} fill="none" stroke="#22d3ee" strokeWidth="2" style={{ filter: 'drop-shadow(0 0 4px rgba(34,211,238,0.5))' }} />
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user