|
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
|
|
Customized,
|
|
|
|
|
ReferenceLine,
|
|
|
|
|
ResponsiveContainer,
|
|
|
|
|
Tooltip,
|
|
|
|
|
XAxis,
|
|
|
|
|
YAxis
|
|
|
|
|
} from 'recharts'
|
|
|
|
|
@@ -97,6 +98,8 @@ interface NormalDistributionChartProps {
|
|
|
|
|
// 차트 필터 형질 선택 콜백 (외부 연동용)
|
|
|
|
|
chartFilterTrait?: string
|
|
|
|
|
onChartFilterTraitChange?: (trait: string) => void
|
|
|
|
|
// 전체 선발지수 히스토그램 (실제 분포 데이터)
|
|
|
|
|
selectionIndexHistogram?: { bin: number; count: number; farmCount: number }[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function NormalDistributionChart({
|
|
|
|
|
@@ -134,7 +137,8 @@ export function NormalDistributionChart({
|
|
|
|
|
highlightMode = null,
|
|
|
|
|
onHighlightModeChange,
|
|
|
|
|
chartFilterTrait: externalChartFilterTrait,
|
|
|
|
|
onChartFilterTraitChange
|
|
|
|
|
onChartFilterTraitChange,
|
|
|
|
|
selectionIndexHistogram = []
|
|
|
|
|
}: NormalDistributionChartProps) {
|
|
|
|
|
const { filters } = useFilterStore()
|
|
|
|
|
|
|
|
|
|
@@ -262,16 +266,87 @@ export function NormalDistributionChart({
|
|
|
|
|
}
|
|
|
|
|
}, [chartFilterTrait, overallScore, overallPercentile, allTraits, traitRankData, farmAvgZ, regionAvgZ])
|
|
|
|
|
|
|
|
|
|
// X축 범위 및 간격 계산 (내 개체 중심 방식)
|
|
|
|
|
// X축 범위 및 간격 계산 (실제 데이터에 맞게 조정, 중앙 정렬)
|
|
|
|
|
const xAxisConfig = useMemo(() => {
|
|
|
|
|
const cowScore = chartDisplayValues.originalScore
|
|
|
|
|
|
|
|
|
|
// 전체 선발지수: selectionIndexHistogram 사용
|
|
|
|
|
if (chartFilterTrait === 'overall' && selectionIndexHistogram.length > 0) {
|
|
|
|
|
const bins = selectionIndexHistogram.map(item => item.bin - cowScore)
|
|
|
|
|
// 내 개체(0)도 범위에 포함
|
|
|
|
|
const allValues = [...bins, 0]
|
|
|
|
|
const minData = Math.min(...allValues)
|
|
|
|
|
const maxData = Math.max(...allValues)
|
|
|
|
|
|
|
|
|
|
// 데이터의 중심점 계산
|
|
|
|
|
const center = (minData + maxData) / 2
|
|
|
|
|
// 데이터 범위에 20% 여유 추가
|
|
|
|
|
const dataRange = maxData - minData
|
|
|
|
|
const padding = dataRange * 0.2
|
|
|
|
|
// 중심점 기준으로 좌우 대칭 범위 설정
|
|
|
|
|
const halfRange = (dataRange / 2) + padding
|
|
|
|
|
|
|
|
|
|
const min = Math.floor(center - halfRange)
|
|
|
|
|
const max = Math.ceil(center + halfRange)
|
|
|
|
|
const range = max - min
|
|
|
|
|
|
|
|
|
|
let step: number
|
|
|
|
|
if (range <= 5) {
|
|
|
|
|
step = 0.5
|
|
|
|
|
} else if (range <= 20) {
|
|
|
|
|
step = 2
|
|
|
|
|
} else if (range <= 50) {
|
|
|
|
|
step = 5
|
|
|
|
|
} else if (range <= 100) {
|
|
|
|
|
step = 10
|
|
|
|
|
} else {
|
|
|
|
|
step = 20
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { min, max, step }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 형질별: traitRankData.histogram 사용
|
|
|
|
|
if (chartFilterTrait !== 'overall' && traitRankData?.histogram && traitRankData.histogram.length > 0) {
|
|
|
|
|
const bins = traitRankData.histogram.map(item => item.bin - cowScore)
|
|
|
|
|
// 내 개체(0)도 범위에 포함
|
|
|
|
|
const allValues = [...bins, 0]
|
|
|
|
|
const minData = Math.min(...allValues)
|
|
|
|
|
const maxData = Math.max(...allValues)
|
|
|
|
|
|
|
|
|
|
// 데이터의 중심점 계산
|
|
|
|
|
const center = (minData + maxData) / 2
|
|
|
|
|
// 데이터 범위에 20% 여유 추가
|
|
|
|
|
const dataRange = maxData - minData
|
|
|
|
|
const padding = dataRange * 0.2
|
|
|
|
|
// 중심점 기준으로 좌우 대칭 범위 설정
|
|
|
|
|
const halfRange = (dataRange / 2) + padding
|
|
|
|
|
|
|
|
|
|
const min = Math.floor(center - halfRange)
|
|
|
|
|
const max = Math.ceil(center + halfRange)
|
|
|
|
|
const range = max - min
|
|
|
|
|
|
|
|
|
|
let step: number
|
|
|
|
|
if (range <= 5) {
|
|
|
|
|
step = 0.5
|
|
|
|
|
} else if (range <= 20) {
|
|
|
|
|
step = 2
|
|
|
|
|
} else if (range <= 50) {
|
|
|
|
|
step = 5
|
|
|
|
|
} else if (range <= 100) {
|
|
|
|
|
step = 10
|
|
|
|
|
} else {
|
|
|
|
|
step = 20
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { min, max, step }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 히스토그램 데이터가 없으면 평균 대비 차이로 범위 계산 (폴백)
|
|
|
|
|
const { cowVsFarm, cowVsRegion } = chartDisplayValues
|
|
|
|
|
const maxDiff = Math.max(Math.abs(cowVsFarm), Math.abs(cowVsRegion))
|
|
|
|
|
|
|
|
|
|
// 데이터가 차트의 약 70% 영역에 들어오도록 범위 계산
|
|
|
|
|
// maxDiff가 range의 70%가 되도록: range = maxDiff / 0.7
|
|
|
|
|
const targetRange = maxDiff / 0.7
|
|
|
|
|
|
|
|
|
|
// step 계산: 범위에 따라 적절한 간격 선택
|
|
|
|
|
let step: number
|
|
|
|
|
if (targetRange <= 1) {
|
|
|
|
|
step = 0.2
|
|
|
|
|
@@ -285,12 +360,11 @@ export function NormalDistributionChart({
|
|
|
|
|
step = 10
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 범위를 step 단위로 올림 (최소값 보장)
|
|
|
|
|
const minRange = step * 3 // 최소 3개의 step
|
|
|
|
|
const minRange = step * 3
|
|
|
|
|
const range = Math.max(minRange, Math.ceil(targetRange / step) * step)
|
|
|
|
|
|
|
|
|
|
return { min: -range, max: range, step }
|
|
|
|
|
}, [chartDisplayValues])
|
|
|
|
|
}, [chartFilterTrait, selectionIndexHistogram, traitRankData, chartDisplayValues])
|
|
|
|
|
|
|
|
|
|
// X축 틱 계산 (동적 간격)
|
|
|
|
|
const xTicks = useMemo(() => {
|
|
|
|
|
@@ -302,22 +376,118 @@ export function NormalDistributionChart({
|
|
|
|
|
return ticks
|
|
|
|
|
}, [xAxisConfig])
|
|
|
|
|
|
|
|
|
|
// 히스토그램 데이터 생성 (내 개체 중심, 정규분포 곡선)
|
|
|
|
|
// 히스토그램 데이터 생성 (실제 데이터 분포 사용)
|
|
|
|
|
const histogramData = useMemo(() => {
|
|
|
|
|
// X축 범위에 맞게 표준편차 조정 (범위의 약 1/4)
|
|
|
|
|
// 전체 선발지수: selectionIndexHistogram 사용
|
|
|
|
|
if (chartFilterTrait === 'overall' && selectionIndexHistogram.length > 0) {
|
|
|
|
|
const histogram = selectionIndexHistogram
|
|
|
|
|
const totalCount = histogram.reduce((sum, item) => sum + item.count, 0)
|
|
|
|
|
|
|
|
|
|
const bins = histogram.map(item => {
|
|
|
|
|
const cowScore = chartDisplayValues.originalScore
|
|
|
|
|
const relativeBin = item.bin - cowScore
|
|
|
|
|
const percent = (item.count / totalCount) * 100
|
|
|
|
|
const farmPercent = totalCount > 0 ? (item.farmCount / totalCount) * 100 : 0
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
midPoint: relativeBin,
|
|
|
|
|
regionPercent: percent,
|
|
|
|
|
percent: percent,
|
|
|
|
|
farmPercent: farmPercent,
|
|
|
|
|
count: item.count,
|
|
|
|
|
farmCount: item.farmCount
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 🔍 실제 히스토그램 데이터 콘솔 로그
|
|
|
|
|
const dataMinMax = { min: Math.min(...bins.map(b => b.midPoint)), max: Math.max(...bins.map(b => b.midPoint)) }
|
|
|
|
|
const percentMinMax = { min: Math.min(...bins.map(b => b.percent)), max: Math.max(...bins.map(b => b.percent)) }
|
|
|
|
|
const calculatedYMax = Math.max(5, Math.ceil(percentMinMax.max * 1.2))
|
|
|
|
|
|
|
|
|
|
console.log('📊 [전체 선발지수 - 차트 범위 자동 조정]', {
|
|
|
|
|
형질명: '전체 선발지수',
|
|
|
|
|
전체개체수: totalCount,
|
|
|
|
|
'📏 X축': {
|
|
|
|
|
데이터범위: `${dataMinMax.min.toFixed(1)} ~ ${dataMinMax.max.toFixed(1)}`,
|
|
|
|
|
차트범위: `${xAxisConfig.min} ~ ${xAxisConfig.max}`,
|
|
|
|
|
범위크기: `${(xAxisConfig.max - xAxisConfig.min).toFixed(1)}`
|
|
|
|
|
},
|
|
|
|
|
'📏 Y축': {
|
|
|
|
|
데이터최대: `${percentMinMax.max.toFixed(1)}%`,
|
|
|
|
|
차트최대: `${calculatedYMax}%`,
|
|
|
|
|
여유공간: `${((calculatedYMax - percentMinMax.max) / percentMinMax.max * 100).toFixed(0)}%`
|
|
|
|
|
},
|
|
|
|
|
총데이터개수: bins.length,
|
|
|
|
|
샘플: { 첫5개: bins.slice(0, 5), 마지막5개: bins.slice(-5) }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return bins
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 형질별 데이터가 있으면 실제 히스토그램 사용
|
|
|
|
|
if (chartFilterTrait !== 'overall' && traitRankData?.histogram && traitRankData.histogram.length > 0) {
|
|
|
|
|
const histogram = traitRankData.histogram
|
|
|
|
|
const totalCount = histogram.reduce((sum, item) => sum + item.count, 0)
|
|
|
|
|
|
|
|
|
|
// 백엔드에서 받은 히스토그램을 차트 데이터로 변환
|
|
|
|
|
const bins = histogram.map(item => {
|
|
|
|
|
// bin 값은 구간의 시작값 (예: 110, 115, 120...)
|
|
|
|
|
// 개체 점수 대비 상대 위치로 변환 (내 개체 = 0 기준)
|
|
|
|
|
const cowScore = chartDisplayValues.originalScore
|
|
|
|
|
const relativeBin = item.bin - cowScore
|
|
|
|
|
|
|
|
|
|
// 백분율 계산
|
|
|
|
|
const percent = (item.count / totalCount) * 100
|
|
|
|
|
const farmPercent = totalCount > 0 ? (item.farmCount / totalCount) * 100 : 0
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
midPoint: relativeBin,
|
|
|
|
|
regionPercent: percent,
|
|
|
|
|
percent: percent,
|
|
|
|
|
farmPercent: farmPercent,
|
|
|
|
|
count: item.count,
|
|
|
|
|
farmCount: item.farmCount
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 🔍 실제 히스토그램 데이터 콘솔 로그
|
|
|
|
|
const dataMinMax = { min: Math.min(...bins.map(b => b.midPoint)), max: Math.max(...bins.map(b => b.midPoint)) }
|
|
|
|
|
const percentMinMax = { min: Math.min(...bins.map(b => b.percent)), max: Math.max(...bins.map(b => b.percent)) }
|
|
|
|
|
const calculatedYMax = Math.max(5, Math.ceil(percentMinMax.max * 1.2))
|
|
|
|
|
|
|
|
|
|
console.log(`📊 [${chartFilterTrait} - 차트 범위 자동 조정]`, {
|
|
|
|
|
형질명: chartFilterTrait,
|
|
|
|
|
전체개체수: totalCount,
|
|
|
|
|
'📏 X축': {
|
|
|
|
|
데이터범위: `${dataMinMax.min.toFixed(1)} ~ ${dataMinMax.max.toFixed(1)}`,
|
|
|
|
|
차트범위: `${xAxisConfig.min} ~ ${xAxisConfig.max}`,
|
|
|
|
|
범위크기: `${(xAxisConfig.max - xAxisConfig.min).toFixed(1)}`
|
|
|
|
|
},
|
|
|
|
|
'📏 Y축': {
|
|
|
|
|
데이터최대: `${percentMinMax.max.toFixed(1)}%`,
|
|
|
|
|
차트최대: `${calculatedYMax}%`,
|
|
|
|
|
여유공간: `${((calculatedYMax - percentMinMax.max) / percentMinMax.max * 100).toFixed(0)}%`
|
|
|
|
|
},
|
|
|
|
|
총데이터개수: bins.length,
|
|
|
|
|
샘플: { 첫5개: bins.slice(0, 5), 마지막5개: bins.slice(-5) }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return bins
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 히스토그램 데이터가 없을 때만 정규분포 곡선 사용 (폴백)
|
|
|
|
|
const range = xAxisConfig.max - xAxisConfig.min
|
|
|
|
|
const std = range / 4
|
|
|
|
|
|
|
|
|
|
// 정규분포 PDF 계산 함수 (0~1 범위로 정규화)
|
|
|
|
|
const normalPDF = (x: number, mean: number = 0) => {
|
|
|
|
|
const exponent = -Math.pow(x - mean, 2) / (2 * Math.pow(std, 2))
|
|
|
|
|
return Math.exp(exponent) // 0~1 범위
|
|
|
|
|
return Math.exp(exponent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bins = []
|
|
|
|
|
const stepSize = range / 100 // 100개의 점으로 부드러운 곡선
|
|
|
|
|
const stepSize = range / 100
|
|
|
|
|
for (let x = xAxisConfig.min; x <= xAxisConfig.max; x += stepSize) {
|
|
|
|
|
const pdfValue = normalPDF(x) * 40 // 최대 40%로 스케일링
|
|
|
|
|
const pdfValue = normalPDF(x) * 40
|
|
|
|
|
bins.push({
|
|
|
|
|
midPoint: x,
|
|
|
|
|
regionPercent: pdfValue,
|
|
|
|
|
@@ -325,11 +495,30 @@ export function NormalDistributionChart({
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return bins
|
|
|
|
|
}, [xAxisConfig])
|
|
|
|
|
// 🔍 정규분포 곡선 데이터 콘솔 로그
|
|
|
|
|
console.log('📊 [정규분포 곡선 데이터 - 폴백]', {
|
|
|
|
|
총데이터개수: bins.length,
|
|
|
|
|
X축범위: `${xAxisConfig.min} ~ ${xAxisConfig.max}`,
|
|
|
|
|
표준편차: std,
|
|
|
|
|
첫5개: bins.slice(0, 5),
|
|
|
|
|
마지막5개: bins.slice(-5)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 최대 % (Y축 범위용) - 항상 40으로 고정
|
|
|
|
|
const maxPercent = 40
|
|
|
|
|
return bins
|
|
|
|
|
}, [xAxisConfig, chartFilterTrait, traitRankData, chartDisplayValues.originalScore, selectionIndexHistogram])
|
|
|
|
|
|
|
|
|
|
// Y축 범위 (실제 데이터에 맞게 조정)
|
|
|
|
|
const maxPercent = useMemo(() => {
|
|
|
|
|
if (histogramData.length === 0) return 40
|
|
|
|
|
|
|
|
|
|
const maxValue = Math.max(...histogramData.map(d => d.percent || 0))
|
|
|
|
|
|
|
|
|
|
// 실제 최대값에 20% 여유만 추가 (너무 빡빡하지 않게)
|
|
|
|
|
const calculatedMax = Math.ceil(maxValue * 1.2)
|
|
|
|
|
|
|
|
|
|
// 최소 5% 보장 (데이터가 너무 작을 때만)
|
|
|
|
|
return Math.max(5, calculatedMax)
|
|
|
|
|
}, [histogramData])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
@@ -387,14 +576,13 @@ export function NormalDistributionChart({
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
{/* 확대 버튼 */}
|
|
|
|
|
<button
|
|
|
|
|
{/* <button
|
|
|
|
|
onClick={onOpenChartModal}
|
|
|
|
|
className="flex items-center gap-1.5 px-2 sm:px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
|
|
|
|
|
aria-label="차트 확대"
|
|
|
|
|
>
|
|
|
|
|
aria-label="차트 확대">
|
|
|
|
|
<Maximize2 className="w-4 h-4" />
|
|
|
|
|
<span className="hidden sm:inline">확대</span>
|
|
|
|
|
</button>
|
|
|
|
|
</button> */}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 순위 및 평균 대비 요약 (차트 위에 배치) */}
|
|
|
|
|
@@ -511,11 +699,11 @@ export function NormalDistributionChart({
|
|
|
|
|
{/* 데스크탑: 기존 레이아웃 */}
|
|
|
|
|
<div className="hidden sm:block">
|
|
|
|
|
{/* 현재 보고 있는 조회 기준 표시 */}
|
|
|
|
|
<div className="flex items-center justify-center mb-4">
|
|
|
|
|
{/* <div className="flex items-center justify-center mb-4">
|
|
|
|
|
<span className="px-4 py-1.5 bg-slate-200 text-slate-700 text-base font-bold rounded-full">
|
|
|
|
|
{chartFilterTrait === 'overall' ? '전체 선발지수' : chartFilterTrait} 조회 기준
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div> */}
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
|
|
|
{/* 농가 내 순위 */}
|
|
|
|
|
@@ -622,11 +810,20 @@ export function NormalDistributionChart({
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="h-[400px] sm:h-[440px] md:h-[470px] bg-gradient-to-b from-slate-50 to-slate-100 rounded-xl p-2 sm:p-4 relative" role="img" aria-label="농가 및 보은군 내 개체 위치">
|
|
|
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
|
|
|
<ComposedChart
|
|
|
|
|
data={histogramData}
|
|
|
|
|
margin={isMobileView ? { top: 110, right: 10, left: 5, bottom: 25 } : { top: 110, right: 20, left: 10, bottom: 45 }}
|
|
|
|
|
>
|
|
|
|
|
{/* 로딩 상태 */}
|
|
|
|
|
{(traitRankLoading && chartFilterTrait !== 'overall') || histogramData.length === 0 ? (
|
|
|
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-slate-50/80 rounded-xl z-10">
|
|
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent mb-4"></div>
|
|
|
|
|
<p className="text-sm text-muted-foreground font-medium">
|
|
|
|
|
{chartFilterTrait === 'overall' ? '전체 선발지수' : chartFilterTrait} 분포 데이터 로딩 중...
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
|
|
|
<ComposedChart
|
|
|
|
|
data={histogramData}
|
|
|
|
|
margin={isMobileView ? { top: 110, right: 10, left: 5, bottom: 25 } : { top: 110, right: 20, left: 10, bottom: 45 }}
|
|
|
|
|
>
|
|
|
|
|
<defs>
|
|
|
|
|
{/* 보은군 - Blue */}
|
|
|
|
|
<linearGradient id="regionGradient" x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
@@ -677,14 +874,47 @@ export function NormalDistributionChart({
|
|
|
|
|
tickFormatter={(value) => `${Math.round(value)}%`}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 정규분포 곡선 */}
|
|
|
|
|
{/* Tooltip */}
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={({ active, payload }) => {
|
|
|
|
|
if (!active || !payload || payload.length === 0) return null
|
|
|
|
|
|
|
|
|
|
const data = payload[0].payload
|
|
|
|
|
const cowScore = chartDisplayValues.originalScore
|
|
|
|
|
const binStart = Math.round((data.midPoint + cowScore) * 100) / 100
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-white p-3 border border-border rounded-lg shadow-lg">
|
|
|
|
|
<p className="text-sm font-semibold mb-2">
|
|
|
|
|
구간: {binStart >= 0 ? '+' : ''}{binStart.toFixed(2)}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
개체 수: <span className="font-bold text-foreground">{data.count || 0}마리</span>
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
비율: <span className="font-bold text-foreground">{data.percent?.toFixed(1) || 0}%</span>
|
|
|
|
|
</p>
|
|
|
|
|
{data.farmCount !== undefined && (
|
|
|
|
|
<p className="text-sm text-blue-600 mt-1">
|
|
|
|
|
내 농가: <span className="font-bold">{data.farmCount}마리</span>
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
cursor={{ fill: 'rgba(59, 130, 246, 0.1)' }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 실제 데이터 분포 (Area 그래프 + 점 표시) */}
|
|
|
|
|
<Area
|
|
|
|
|
type="natural"
|
|
|
|
|
type="linear"
|
|
|
|
|
dataKey="percent"
|
|
|
|
|
stroke="#64748b"
|
|
|
|
|
strokeWidth={2.5}
|
|
|
|
|
fill="url(#areaFillGradient)"
|
|
|
|
|
dot={false}
|
|
|
|
|
dot={{ r: 4, fill: '#64748b', strokeWidth: 2, stroke: '#fff' }}
|
|
|
|
|
activeDot={{ r: 6, fill: '#3b82f6', strokeWidth: 2, stroke: '#fff' }}
|
|
|
|
|
isAnimationActive={false}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 보은군 평균 위치 */}
|
|
|
|
|
@@ -1048,10 +1278,11 @@ export function NormalDistributionChart({
|
|
|
|
|
/>
|
|
|
|
|
</ComposedChart>
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 범례 */}
|
|
|
|
|
<div className="flex items-center justify-center gap-4 sm:gap-6 mt-4 pt-4 border-t border-border">
|
|
|
|
|
{/* <div className="flex items-center justify-center gap-4 sm:gap-6 mt-4 pt-4 border-t border-border">
|
|
|
|
|
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
|
|
|
<div className="w-3 h-3 sm:w-4 sm:h-4 rounded bg-slate-500"></div>
|
|
|
|
|
<span className="text-sm sm:text-sm text-muted-foreground">보은군 평균</span>
|
|
|
|
|
@@ -1064,7 +1295,7 @@ export function NormalDistributionChart({
|
|
|
|
|
<div className="w-3 h-3 sm:w-4 sm:h-4 rounded bg-[#1482B0]"></div>
|
|
|
|
|
<span className="text-sm sm:text-sm font-medium text-foreground">{displayCowNumber.slice(-4)} 개체</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div> */}
|
|
|
|
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|