197 lines
5.6 KiB
TypeScript
197 lines
5.6 KiB
TypeScript
'use client'
|
||
|
||
import React from 'react'
|
||
|
||
interface ChartDataPoint {
|
||
value: number
|
||
label?: string
|
||
timestamp?: string
|
||
}
|
||
|
||
interface AreaChartProps {
|
||
className?: string
|
||
data?: ChartDataPoint[]
|
||
}
|
||
|
||
const AreaChart: React.FC<AreaChartProps> = ({ className = '', data }) => {
|
||
const width = 635
|
||
const height = 280
|
||
const margin = { top: 20, right: 30, bottom: 50, left: 60 }
|
||
const plotWidth = width - margin.left - margin.right
|
||
const plotHeight = height - margin.top - margin.bottom
|
||
const baselineY = margin.top + plotHeight
|
||
|
||
const safeData = (Array.isArray(data) && data.length > 0)
|
||
? data
|
||
: Array.from({ length: 7 }, () => ({ value: 0 }))
|
||
|
||
const maxVal = Math.max(...safeData.map(d => d.value || 0), 1)
|
||
const stepX = safeData.length > 1 ? plotWidth / (safeData.length - 1) : plotWidth
|
||
|
||
const points = safeData.map((d, i) => {
|
||
const x = margin.left + i * stepX
|
||
const y = baselineY - (Math.min(d.value || 0, maxVal) / maxVal) * plotHeight
|
||
return { x, y }
|
||
})
|
||
|
||
const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')
|
||
const areaPath = `${linePath} L${width - margin.right},${baselineY} L${margin.left},${baselineY} Z`
|
||
|
||
// Генерируем Y-оси метки
|
||
const ySteps = 4
|
||
const yLabels = Array.from({ length: ySteps + 1 }, (_, i) => {
|
||
const value = (maxVal / ySteps) * (ySteps - i)
|
||
const y = margin.top + (i * plotHeight) / ySteps
|
||
return { value: value.toFixed(1), y }
|
||
})
|
||
|
||
// Генерируем X-оси метки (показываем каждую 2-ю или 3-ю точку)
|
||
const xLabelStep = Math.ceil(safeData.length / 5)
|
||
const xLabels = safeData
|
||
.map((d, i) => {
|
||
const x = margin.left + i * stepX
|
||
const label = d.label || d.timestamp || `${i + 1}`
|
||
return { label, x, index: i }
|
||
})
|
||
.filter((_, i) => i % xLabelStep === 0 || i === safeData.length - 1)
|
||
|
||
return (
|
||
<div className={`w-full h-full ${className}`}>
|
||
<svg className="w-full h-full" viewBox={`0 0 ${width} ${height}`}>
|
||
<defs>
|
||
<linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor="rgb(37, 99, 235)" stopOpacity="0.3" />
|
||
<stop offset="100%" stopColor="rgb(37, 99, 235)" stopOpacity="0" />
|
||
</linearGradient>
|
||
</defs>
|
||
|
||
{/* Сетка Y */}
|
||
{yLabels.map((label, i) => (
|
||
<line
|
||
key={`grid-y-${i}`}
|
||
x1={margin.left}
|
||
y1={label.y}
|
||
x2={width - margin.right}
|
||
y2={label.y}
|
||
stroke="rgba(148, 163, 184, 0.2)"
|
||
strokeWidth="1"
|
||
strokeDasharray="4,4"
|
||
/>
|
||
))}
|
||
|
||
{/* Ось X */}
|
||
<line
|
||
x1={margin.left}
|
||
y1={baselineY}
|
||
x2={width - margin.right}
|
||
y2={baselineY}
|
||
stroke="rgb(148, 163, 184)"
|
||
strokeWidth="2"
|
||
/>
|
||
|
||
{/* Ось Y */}
|
||
<line
|
||
x1={margin.left}
|
||
y1={margin.top}
|
||
x2={margin.left}
|
||
y2={baselineY}
|
||
stroke="rgb(148, 163, 184)"
|
||
strokeWidth="2"
|
||
/>
|
||
|
||
{/* Y-оси метки и подписи */}
|
||
{yLabels.map((label, i) => (
|
||
<g key={`y-label-${i}`}>
|
||
<line
|
||
x1={margin.left - 5}
|
||
y1={label.y}
|
||
x2={margin.left}
|
||
y2={label.y}
|
||
stroke="rgb(148, 163, 184)"
|
||
strokeWidth="1"
|
||
/>
|
||
<text
|
||
x={margin.left - 10}
|
||
y={label.y + 4}
|
||
textAnchor="end"
|
||
fontSize="12"
|
||
fill="rgb(148, 163, 184)"
|
||
fontFamily="Arial, sans-serif"
|
||
>
|
||
{label.value}
|
||
</text>
|
||
</g>
|
||
))}
|
||
|
||
{/* X-оси метки и подписи */}
|
||
{xLabels.map((label, i) => (
|
||
<g key={`x-label-${i}`}>
|
||
<line
|
||
x1={label.x}
|
||
y1={baselineY}
|
||
x2={label.x}
|
||
y2={baselineY + 5}
|
||
stroke="rgb(148, 163, 184)"
|
||
strokeWidth="1"
|
||
/>
|
||
<text
|
||
x={label.x}
|
||
y={baselineY + 20}
|
||
textAnchor="middle"
|
||
fontSize="11"
|
||
fill="rgb(148, 163, 184)"
|
||
fontFamily="Arial, sans-serif"
|
||
>
|
||
{typeof label.label === 'string' ? label.label.substring(0, 10) : `${label.index + 1}`}
|
||
</text>
|
||
</g>
|
||
))}
|
||
|
||
{/* Подпись оси Y */}
|
||
<text
|
||
x={20}
|
||
y={margin.top + plotHeight / 2}
|
||
textAnchor="middle"
|
||
fontSize="13"
|
||
fill="rgb(148, 163, 184)"
|
||
fontFamily="Arial, sans-serif"
|
||
transform={`rotate(-90, 20, ${margin.top + plotHeight / 2})`}
|
||
>
|
||
Значение
|
||
</text>
|
||
|
||
{/* Подпись оси X */}
|
||
<text
|
||
x={margin.left + plotWidth / 2}
|
||
y={height - 10}
|
||
textAnchor="middle"
|
||
fontSize="13"
|
||
fill="rgb(148, 163, 184)"
|
||
fontFamily="Arial, sans-serif"
|
||
>
|
||
Время
|
||
</text>
|
||
|
||
{/* График */}
|
||
<path d={areaPath} fill="url(#areaGradient)" />
|
||
<path d={linePath} stroke="rgb(37, 99, 235)" strokeWidth="2.5" fill="none" />
|
||
|
||
{/* Точки данных */}
|
||
{points.map((p, i) => (
|
||
<circle
|
||
key={i}
|
||
cx={p.x}
|
||
cy={p.y}
|
||
r="4"
|
||
fill="rgb(37, 99, 235)"
|
||
stroke="rgb(15, 23, 42)"
|
||
strokeWidth="2"
|
||
/>
|
||
))}
|
||
</svg>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AreaChart
|