261 lines
8.2 KiB
TypeScript
261 lines
8.2 KiB
TypeScript
'use client'
|
||
|
||
import React from 'react'
|
||
|
||
interface ChartDataPoint {
|
||
critical?: number
|
||
warning?: number
|
||
label?: string
|
||
}
|
||
|
||
interface BarChartProps {
|
||
className?: string
|
||
data?: ChartDataPoint[]
|
||
}
|
||
|
||
const BarChart: React.FC<BarChartProps> = ({ className = '', data }) => {
|
||
const width = 635
|
||
const height = 280
|
||
const margin = { top: 40, 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 barData = (Array.isArray(data) && data.length > 0)
|
||
? data.map(d => ({
|
||
critical: d.critical || 0,
|
||
warning: d.warning || 0,
|
||
label: d.label || ''
|
||
}))
|
||
: Array.from({ length: 12 }, (_, i) => ({ critical: 0, warning: 0, label: `${i + 1}` }))
|
||
|
||
const maxVal = Math.max(...barData.map(b => (b.critical || 0) + (b.warning || 0)), 1)
|
||
|
||
// Генерируем 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(barData.length / 8)
|
||
const xLabels = barData
|
||
.map((d, i) => {
|
||
const barWidth = Math.max(30, plotWidth / barData.length - 8)
|
||
const barSpacing = (plotWidth - barWidth * barData.length) / (barData.length - 1 || 1)
|
||
const x = margin.left + i * (barWidth + barSpacing) + barWidth / 2
|
||
return { label: d.label || `${i + 1}`, x, index: i }
|
||
})
|
||
.filter((_, i) => i % xLabelStep === 0 || i === barData.length - 1)
|
||
|
||
return (
|
||
<div className={`w-full h-full ${className}`}>
|
||
<svg className="w-full h-full" viewBox={`0 0 ${width} ${height}`}>
|
||
{/* Сетка 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, 8) : `${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>
|
||
|
||
{/* Столбцы - сгруппированные по критическим и предупреждениям */}
|
||
{barData.map((bar, index) => {
|
||
const barWidth = Math.max(30, plotWidth / barData.length - 8)
|
||
const barSpacing = (plotWidth - barWidth * barData.length) / (barData.length - 1 || 1)
|
||
const groupX = margin.left + index * (barWidth + barSpacing)
|
||
|
||
const totalValue = (bar.critical || 0) + (bar.warning || 0)
|
||
|
||
// Ширина каждого подстолбца
|
||
const subBarWidth = barWidth / 2 - 2
|
||
|
||
// Критический столбец (красный)
|
||
const criticalHeight = (bar.critical / maxVal) * plotHeight
|
||
const criticalY = baselineY - criticalHeight
|
||
|
||
// Предупреждение столбец (оранжевый)
|
||
const warningHeight = (bar.warning / maxVal) * plotHeight
|
||
const warningY = baselineY - warningHeight
|
||
|
||
return (
|
||
<g key={`bar-group-${index}`}>
|
||
{/* Критический столбец */}
|
||
{bar.critical > 0 && (
|
||
<>
|
||
<rect
|
||
x={groupX}
|
||
y={criticalY}
|
||
width={subBarWidth}
|
||
height={criticalHeight}
|
||
fill="#ef4444"
|
||
rx="4"
|
||
ry="4"
|
||
opacity="0.9"
|
||
/>
|
||
<rect
|
||
x={groupX}
|
||
y={criticalY}
|
||
width={subBarWidth}
|
||
height={criticalHeight}
|
||
fill="none"
|
||
stroke="#ef4444"
|
||
strokeWidth="1"
|
||
rx="4"
|
||
ry="4"
|
||
opacity="0.3"
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
{/* Предупреждение столбец */}
|
||
{bar.warning > 0 && (
|
||
<>
|
||
<rect
|
||
x={groupX + subBarWidth + 4}
|
||
y={warningY}
|
||
width={subBarWidth}
|
||
height={warningHeight}
|
||
fill="#fb923c"
|
||
rx="4"
|
||
ry="4"
|
||
opacity="0.9"
|
||
/>
|
||
<rect
|
||
x={groupX + subBarWidth + 4}
|
||
y={warningY}
|
||
width={subBarWidth}
|
||
height={warningHeight}
|
||
fill="none"
|
||
stroke="#fb923c"
|
||
strokeWidth="1"
|
||
rx="4"
|
||
ry="4"
|
||
opacity="0.3"
|
||
/>
|
||
</>
|
||
)}
|
||
</g>
|
||
)
|
||
})}
|
||
|
||
{/* Легенда - горизонтально над графиком */}
|
||
<g transform={`translate(${margin.left + plotWidth / 2 - 120}, 10)`}>
|
||
<rect x="0" y="0" width="12" height="12" fill="#ef4444" rx="2" />
|
||
<text x="18" y="10" fontSize="11" fill="rgb(148, 163, 184)" fontFamily="Arial, sans-serif">
|
||
Критические
|
||
</text>
|
||
|
||
<rect x="120" y="0" width="12" height="12" fill="#fb923c" rx="2" />
|
||
<text x="138" y="10" fontSize="11" fill="rgb(148, 163, 184)" fontFamily="Arial, sans-serif">
|
||
Предупреждения
|
||
</text>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default BarChart
|