Files
genome2025/frontend/src/app/demo/chart-options/page.tsx
2025-12-09 17:02:27 +09:00

486 lines
25 KiB
TypeScript

'use client'
import { useState } from 'react'
import { Card, CardContent } from "@/components/ui/card"
import {
Area,
ComposedChart,
ResponsiveContainer,
XAxis,
YAxis,
ReferenceLine,
Customized,
} from 'recharts'
// 샘플 데이터
const SAMPLE_DATA = {
cow: { name: '7805', score: 0.85 },
farm: { name: '농가', score: 0.53 },
region: { name: '보은군', score: 0.21 },
}
// 정규분포 히스토그램 데이터
const histogramData = [
{ midPoint: -2.5, percent: 2.3 },
{ midPoint: -2.0, percent: 4.4 },
{ midPoint: -1.5, percent: 9.2 },
{ midPoint: -1.0, percent: 15.0 },
{ midPoint: -0.5, percent: 19.1 },
{ midPoint: 0.0, percent: 19.1 },
{ midPoint: 0.5, percent: 15.0 },
{ midPoint: 1.0, percent: 9.2 },
{ midPoint: 1.5, percent: 4.4 },
{ midPoint: 2.0, percent: 2.3 },
]
export default function ChartOptionsDemo() {
const [selectedOption, setSelectedOption] = useState<string>('A')
const cowScore = SAMPLE_DATA.cow.score
const farmScore = SAMPLE_DATA.farm.score
const regionScore = SAMPLE_DATA.region.score
const farmDiff = cowScore - farmScore
const regionDiff = cowScore - regionScore
return (
<div className="min-h-screen bg-slate-100 p-4 sm:p-8">
<div className="max-w-4xl mx-auto space-y-6">
<h1 className="text-2xl font-bold text-foreground"> </h1>
<p className="text-muted-foreground">
: +{cowScore.toFixed(2)} | : +{farmScore.toFixed(2)} | : +{regionScore.toFixed(2)}
</p>
{/* 옵션 선택 탭 */}
<div className="flex flex-wrap gap-2">
{['A', 'B', 'C', 'D', 'E'].map((opt) => (
<button
key={opt}
onClick={() => setSelectedOption(opt)}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
selectedOption === opt
? 'bg-primary text-white'
: 'bg-white text-foreground hover:bg-slate-200'
}`}
>
{opt}
</button>
))}
</div>
{/* 옵션 A: 차트 내에 대비값 항상 표시 */}
{selectedOption === 'A' && (
<Card>
<CardContent className="p-4">
<h2 className="text-lg font-bold mb-2"> A: 차트 </h2>
<p className="text-sm text-muted-foreground mb-4"> / </p>
<div className="h-[400px] bg-slate-50 rounded-xl p-4">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={histogramData} margin={{ top: 80, right: 30, left: 10, bottom: 30 }}>
<defs>
<linearGradient id="areaGradientA" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.4} />
<stop offset="100%" stopColor="#93c5fd" stopOpacity={0.1} />
</linearGradient>
</defs>
<XAxis dataKey="midPoint" type="number" domain={[-2.5, 2.5]} tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 10 }} width={30} />
<Area type="natural" dataKey="percent" stroke="#3b82f6" fill="url(#areaGradientA)" />
<ReferenceLine x={regionScore} stroke="#2563eb" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={farmScore} stroke="#f59e0b" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={cowScore} stroke="#1482B0" strokeWidth={3} />
<Customized
component={(props: any) => {
const { xAxisMap, yAxisMap } = props
if (!xAxisMap || !yAxisMap) return null
const xAxis = Object.values(xAxisMap)[0] as any
const yAxis = Object.values(yAxisMap)[0] as any
if (!xAxis || !yAxis) return null
const chartX = xAxis.x
const chartWidth = xAxis.width
const chartTop = yAxis.y
const [domainMin, domainMax] = xAxis.domain || [-2.5, 2.5]
const domainRange = domainMax - domainMin
const sigmaToX = (sigma: number) => chartX + ((sigma - domainMin) / domainRange) * chartWidth
const cowX = sigmaToX(cowScore)
return (
<g>
{/* 개체 라벨 + 대비값 */}
<rect x={cowX + 10} y={chartTop + 20} width={120} height={50} rx={6} fill="#1482B0" />
<text x={cowX + 70} y={chartTop + 38} textAnchor="middle" fill="white" fontSize={12} fontWeight={600}>
+{cowScore.toFixed(2)}
</text>
<text x={cowX + 70} y={chartTop + 55} textAnchor="middle" fill="white" fontSize={10}>
+{farmDiff.toFixed(2)} | +{regionDiff.toFixed(2)}
</text>
</g>
)
}}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)}
{/* 옵션 B: 선 사이 영역 색으로 채우기 */}
{selectedOption === 'B' && (
<Card>
<CardContent className="p-4">
<h2 className="text-lg font-bold mb-2"> B: </h2>
<p className="text-sm text-muted-foreground mb-4">~, ~ </p>
<div className="h-[400px] bg-slate-50 rounded-xl p-4">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={histogramData} margin={{ top: 80, right: 30, left: 10, bottom: 30 }}>
<defs>
<linearGradient id="areaGradientB" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.4} />
<stop offset="100%" stopColor="#93c5fd" stopOpacity={0.1} />
</linearGradient>
</defs>
<XAxis dataKey="midPoint" type="number" domain={[-2.5, 2.5]} tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 10 }} width={30} />
<Area type="natural" dataKey="percent" stroke="#3b82f6" fill="url(#areaGradientB)" />
<ReferenceLine x={regionScore} stroke="#2563eb" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={farmScore} stroke="#f59e0b" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={cowScore} stroke="#1482B0" strokeWidth={3} />
<Customized
component={(props: any) => {
const { xAxisMap, yAxisMap } = props
if (!xAxisMap || !yAxisMap) return null
const xAxis = Object.values(xAxisMap)[0] as any
const yAxis = Object.values(yAxisMap)[0] as any
if (!xAxis || !yAxis) return null
const chartX = xAxis.x
const chartWidth = xAxis.width
const chartTop = yAxis.y
const chartHeight = yAxis.height
const [domainMin, domainMax] = xAxis.domain || [-2.5, 2.5]
const domainRange = domainMax - domainMin
const sigmaToX = (sigma: number) => chartX + ((sigma - domainMin) / domainRange) * chartWidth
const cowX = sigmaToX(cowScore)
const farmX = sigmaToX(farmScore)
const regionX = sigmaToX(regionScore)
return (
<g>
{/* 개체~농가 영역 (주황색) */}
<rect
x={farmX}
y={chartTop}
width={cowX - farmX}
height={chartHeight}
fill="rgba(245, 158, 11, 0.25)"
/>
{/* 농가~보은군 영역 (파란색) */}
<rect
x={regionX}
y={chartTop}
width={farmX - regionX}
height={chartHeight}
fill="rgba(37, 99, 235, 0.15)"
/>
{/* 대비값 라벨 */}
<rect x={(cowX + farmX) / 2 - 35} y={chartTop + 30} width={70} height={24} rx={4} fill="#f59e0b" />
<text x={(cowX + farmX) / 2} y={chartTop + 46} textAnchor="middle" fill="white" fontSize={11} fontWeight={600}>
+{farmDiff.toFixed(2)}
</text>
<rect x={(farmX + regionX) / 2 - 35} y={chartTop + 60} width={70} height={24} rx={4} fill="#2563eb" />
<text x={(farmX + regionX) / 2} y={chartTop + 76} textAnchor="middle" fill="white" fontSize={11} fontWeight={600}>
+{(farmScore - regionScore).toFixed(2)}
</text>
</g>
)
}}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)}
{/* 옵션 C: 개체 배지에 대비값 추가 */}
{selectedOption === 'C' && (
<Card>
<CardContent className="p-4">
<h2 className="text-lg font-bold mb-2"> C: 개체 </h2>
<p className="text-sm text-muted-foreground mb-4"> </p>
<div className="h-[400px] bg-slate-50 rounded-xl p-4">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={histogramData} margin={{ top: 100, right: 30, left: 10, bottom: 30 }}>
<defs>
<linearGradient id="areaGradientC" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.4} />
<stop offset="100%" stopColor="#93c5fd" stopOpacity={0.1} />
</linearGradient>
</defs>
<XAxis dataKey="midPoint" type="number" domain={[-2.5, 2.5]} tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 10 }} width={30} />
<Area type="natural" dataKey="percent" stroke="#3b82f6" fill="url(#areaGradientC)" />
<ReferenceLine x={regionScore} stroke="#2563eb" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={farmScore} stroke="#f59e0b" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={cowScore} stroke="#1482B0" strokeWidth={3} />
<Customized
component={(props: any) => {
const { xAxisMap, yAxisMap } = props
if (!xAxisMap || !yAxisMap) return null
const xAxis = Object.values(xAxisMap)[0] as any
const yAxis = Object.values(yAxisMap)[0] as any
if (!xAxis || !yAxis) return null
const chartX = xAxis.x
const chartWidth = xAxis.width
const chartTop = yAxis.y
const [domainMin, domainMax] = xAxis.domain || [-2.5, 2.5]
const domainRange = domainMax - domainMin
const sigmaToX = (sigma: number) => chartX + ((sigma - domainMin) / domainRange) * chartWidth
const cowX = sigmaToX(cowScore)
const farmX = sigmaToX(farmScore)
const regionX = sigmaToX(regionScore)
return (
<g>
{/* 보은군 배지 */}
<rect x={regionX - 50} y={chartTop - 85} width={100} height={26} rx={6} fill="#dbeafe" stroke="#93c5fd" strokeWidth={2} />
<text x={regionX} y={chartTop - 68} textAnchor="middle" fill="#2563eb" fontSize={12} fontWeight={600}>
+{regionScore.toFixed(2)}
</text>
{/* 농가 배지 */}
<rect x={farmX - 50} y={chartTop - 55} width={100} height={26} rx={6} fill="#fef3c7" stroke="#fcd34d" strokeWidth={2} />
<text x={farmX} y={chartTop - 38} textAnchor="middle" fill="#d97706" fontSize={12} fontWeight={600}>
+{farmScore.toFixed(2)}
</text>
{/* 개체 배지 (확장) */}
<rect x={cowX - 80} y={chartTop - 25} width={160} height={40} rx={6} fill="#1482B0" />
<text x={cowX} y={chartTop - 8} textAnchor="middle" fill="white" fontSize={13} fontWeight={700}>
+{cowScore.toFixed(2)}
</text>
<text x={cowX} y={chartTop + 10} textAnchor="middle" fill="rgba(255,255,255,0.9)" fontSize={10}>
+{farmDiff.toFixed(2)} | +{regionDiff.toFixed(2)}
</text>
</g>
)
}}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)}
{/* 옵션 D: 차트 모서리에 오버레이 박스 */}
{selectedOption === 'D' && (
<Card>
<CardContent className="p-4">
<h2 className="text-lg font-bold mb-2"> D: 차트 </h2>
<p className="text-sm text-muted-foreground mb-4"> </p>
<div className="h-[400px] bg-slate-50 rounded-xl p-4 relative">
{/* 오버레이 박스 */}
<div className="absolute top-6 right-6 bg-white rounded-lg shadow-lg border border-slate-200 p-3 z-10">
<div className="text-xs text-muted-foreground mb-2"> </div>
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded bg-amber-500"></span>
<span className="text-sm"></span>
</span>
<span className="text-sm font-bold text-green-600">+{farmDiff.toFixed(2)}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded bg-blue-500"></span>
<span className="text-sm"></span>
</span>
<span className="text-sm font-bold text-green-600">+{regionDiff.toFixed(2)}</span>
</div>
</div>
</div>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={histogramData} margin={{ top: 80, right: 30, left: 10, bottom: 30 }}>
<defs>
<linearGradient id="areaGradientD" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.4} />
<stop offset="100%" stopColor="#93c5fd" stopOpacity={0.1} />
</linearGradient>
</defs>
<XAxis dataKey="midPoint" type="number" domain={[-2.5, 2.5]} tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 10 }} width={30} />
<Area type="natural" dataKey="percent" stroke="#3b82f6" fill="url(#areaGradientD)" />
<ReferenceLine x={regionScore} stroke="#2563eb" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={farmScore} stroke="#f59e0b" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={cowScore} stroke="#1482B0" strokeWidth={3} />
<Customized
component={(props: any) => {
const { xAxisMap, yAxisMap } = props
if (!xAxisMap || !yAxisMap) return null
const xAxis = Object.values(xAxisMap)[0] as any
const yAxis = Object.values(yAxisMap)[0] as any
if (!xAxis || !yAxis) return null
const chartX = xAxis.x
const chartWidth = xAxis.width
const chartTop = yAxis.y
const [domainMin, domainMax] = xAxis.domain || [-2.5, 2.5]
const domainRange = domainMax - domainMin
const sigmaToX = (sigma: number) => chartX + ((sigma - domainMin) / domainRange) * chartWidth
const cowX = sigmaToX(cowScore)
const farmX = sigmaToX(farmScore)
const regionX = sigmaToX(regionScore)
return (
<g>
{/* 심플 배지들 */}
<rect x={regionX - 40} y={chartTop - 60} width={80} height={22} rx={4} fill="#dbeafe" stroke="#93c5fd" />
<text x={regionX} y={chartTop - 45} textAnchor="middle" fill="#2563eb" fontSize={11} fontWeight={600}></text>
<rect x={farmX - 30} y={chartTop - 35} width={60} height={22} rx={4} fill="#fef3c7" stroke="#fcd34d" />
<text x={farmX} y={chartTop - 20} textAnchor="middle" fill="#d97706" fontSize={11} fontWeight={600}></text>
<rect x={cowX - 30} y={chartTop - 10} width={60} height={22} rx={4} fill="#1482B0" />
<text x={cowX} y={chartTop + 5} textAnchor="middle" fill="white" fontSize={11} fontWeight={600}></text>
</g>
)
}}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)}
{/* 옵션 E: 화살표로 차이 표시 */}
{selectedOption === 'E' && (
<Card>
<CardContent className="p-4">
<h2 className="text-lg font-bold mb-2"> E: 화살표로 </h2>
<p className="text-sm text-muted-foreground mb-4"> / + </p>
<div className="h-[400px] bg-slate-50 rounded-xl p-4">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={histogramData} margin={{ top: 80, right: 30, left: 10, bottom: 30 }}>
<defs>
<linearGradient id="areaGradientE" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.4} />
<stop offset="100%" stopColor="#93c5fd" stopOpacity={0.1} />
</linearGradient>
<marker id="arrowFarm" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#f59e0b" />
</marker>
<marker id="arrowRegion" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#2563eb" />
</marker>
</defs>
<XAxis dataKey="midPoint" type="number" domain={[-2.5, 2.5]} tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 10 }} width={30} />
<Area type="natural" dataKey="percent" stroke="#3b82f6" fill="url(#areaGradientE)" />
<ReferenceLine x={regionScore} stroke="#2563eb" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={farmScore} stroke="#f59e0b" strokeWidth={2} strokeDasharray="4 2" />
<ReferenceLine x={cowScore} stroke="#1482B0" strokeWidth={3} />
<Customized
component={(props: any) => {
const { xAxisMap, yAxisMap } = props
if (!xAxisMap || !yAxisMap) return null
const xAxis = Object.values(xAxisMap)[0] as any
const yAxis = Object.values(yAxisMap)[0] as any
if (!xAxis || !yAxis) return null
const chartX = xAxis.x
const chartWidth = xAxis.width
const chartTop = yAxis.y
const chartHeight = yAxis.height
const [domainMin, domainMax] = xAxis.domain || [-2.5, 2.5]
const domainRange = domainMax - domainMin
const sigmaToX = (sigma: number) => chartX + ((sigma - domainMin) / domainRange) * chartWidth
const cowX = sigmaToX(cowScore)
const farmX = sigmaToX(farmScore)
const regionX = sigmaToX(regionScore)
const arrowY1 = chartTop + chartHeight * 0.3
const arrowY2 = chartTop + chartHeight * 0.5
return (
<g>
{/* 개체 → 농가 화살표 */}
<line
x1={cowX} y1={arrowY1}
x2={farmX + 10} y2={arrowY1}
stroke="#f59e0b"
strokeWidth={3}
markerEnd="url(#arrowFarm)"
/>
<rect x={(cowX + farmX) / 2 - 30} y={arrowY1 - 22} width={60} height={20} rx={4} fill="#f59e0b" />
<text x={(cowX + farmX) / 2} y={arrowY1 - 8} textAnchor="middle" fill="white" fontSize={11} fontWeight={700}>
+{farmDiff.toFixed(2)}
</text>
{/* 개체 → 보은군 화살표 */}
<line
x1={cowX} y1={arrowY2}
x2={regionX + 10} y2={arrowY2}
stroke="#2563eb"
strokeWidth={3}
markerEnd="url(#arrowRegion)"
/>
<rect x={(cowX + regionX) / 2 - 30} y={arrowY2 - 22} width={60} height={20} rx={4} fill="#2563eb" />
<text x={(cowX + regionX) / 2} y={arrowY2 - 8} textAnchor="middle" fill="white" fontSize={11} fontWeight={700}>
+{regionDiff.toFixed(2)}
</text>
{/* 배지 */}
<rect x={cowX - 30} y={chartTop - 25} width={60} height={22} rx={4} fill="#1482B0" />
<text x={cowX} y={chartTop - 10} textAnchor="middle" fill="white" fontSize={11} fontWeight={600}></text>
</g>
)
}}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
)}
{/* 옵션 설명 */}
<Card>
<CardContent className="p-4">
<h2 className="text-lg font-bold mb-3"> </h2>
<div className="space-y-2 text-sm">
<div><strong>A:</strong> - </div>
<div><strong>B:</strong> - </div>
<div><strong>C:</strong> - </div>
<div><strong>D:</strong> - </div>
<div><strong>E:</strong> - </div>
</div>
</CardContent>
</Card>
</div>
</div>
)
}