Files
play-life/play-life-web/src/components/FullStatistics.jsx
poignatov 8ba6a9a78f
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 26s
Убрана логика создания нулевой точки в графике полной статистики
2025-12-30 21:00:41 +03:00

242 lines
7.2 KiB
JavaScript

import React from 'react'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
} from 'chart.js'
import { Line } from 'react-chartjs-2'
import WeekProgressChart from './WeekProgressChart'
import { getAllProjectsSorted, getProjectColor, sortProjectsLikeCurrentWeek } from '../utils/projectUtils'
// Экспортируем для обратной совместимости (если используется в других местах)
export { getProjectColorByIndex } from '../utils/projectUtils'
const parseWeekKey = (weekKey) => {
const [yearStr, weekStr] = weekKey.split('-W')
return { year: Number(yearStr), week: Number(weekStr) }
}
const compareWeekKeys = (a, b) => {
const [yearA, weekA] = a.split('-W').map(Number)
const [yearB, weekB] = b.split('-W').map(Number)
if (yearA !== yearB) {
return yearA - yearB
}
return weekA - weekB
}
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
)
function FullStatistics({ selectedProject, onClearSelection, data, loading, error, onRetry, currentWeekData, onNavigate }) {
const processData = () => {
if (!data || data.length === 0) return null
// Группируем данные по проектам
const projectsMap = {}
data.forEach(item => {
const projectName = item.project_name
const weekKey = `${item.report_year}-W${item.report_week.toString().padStart(2, '0')}`
if (!projectsMap[projectName]) {
projectsMap[projectName] = {}
}
projectsMap[projectName][weekKey] = parseFloat(item.total_score)
})
// Собираем все уникальные недели и сортируем их по году и неделе
const allWeeks = new Set()
Object.values(projectsMap).forEach(weeks => {
Object.keys(weeks).forEach(week => allWeeks.add(week))
})
const sortedWeeks = Array.from(allWeeks).sort(compareWeekKeys)
// Получаем отсортированный список всех проектов для синхронизации цветов
const allProjectNames = getAllProjectsSorted(data)
// Фильтруем по выбранному проекту, если он указан
let projectNames = allProjectNames.filter(name => projectsMap[name])
// Сортируем проекты так же, как на экране списка проектов (по priority и min_goal_score)
if (currentWeekData) {
projectNames = sortProjectsLikeCurrentWeek(projectNames, currentWeekData)
}
if (selectedProject) {
projectNames = projectNames.filter(name => name === selectedProject)
}
const datasets = projectNames.map((projectName) => {
let cumulativeSum = 0
const values = sortedWeeks.map(week => {
const score = projectsMap[projectName]?.[week] || 0
cumulativeSum += score
return cumulativeSum
})
// Генерируем цвет для линии на основе индекса в полном списке проектов
const color = getProjectColor(projectName, allProjectNames)
return {
label: projectName,
data: values,
borderColor: color,
backgroundColor: color.replace('50%)', '20%)'),
fill: false,
tension: 0.4,
pointRadius: 4,
pointHoverRadius: 6,
}
})
return {
labels: sortedWeeks,
datasets: datasets,
}
}
const chartData = processData()
// Показываем loading только если данных нет и идет загрузка
if (loading && !chartData) {
return (
<div className="flex justify-center items-center py-16">
<div className="flex flex-col items-center">
<div className="w-12 h-12 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mb-4"></div>
<div className="text-gray-600 font-medium">Загрузка данных...</div>
</div>
</div>
)
}
if (error && !chartData) {
return (
<div className="flex flex-col items-center justify-center py-16">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 mb-4 max-w-md">
<div className="text-red-700 font-semibold mb-2">Ошибка загрузки</div>
<div className="text-red-600 text-sm">{error}</div>
</div>
<button
onClick={onRetry}
className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl font-semibold"
>
Попробовать снова
</button>
</div>
)
}
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
animation: false,
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
padding: 15,
font: {
size: 12,
},
},
padding: {
top: 20,
},
},
title: {
display: false,
},
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}`
}
}
},
},
scales: {
x: {
display: true,
title: {
display: false,
},
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.05)',
},
},
y: {
display: true,
title: {
display: false,
},
beginAtZero: true,
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.05)',
},
},
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false,
},
}
if (!chartData) {
return (
<div className="flex justify-center items-center py-16">
<div className="text-gray-500 text-lg">Нет данных для отображения</div>
</div>
)
}
return (
<div>
{onNavigate && (
<div className="flex justify-end mb-4">
<button
onClick={() => onNavigate('current')}
className="flex items-center justify-center w-10 h-10 rounded-full bg-white hover:bg-gray-100 text-gray-600 hover:text-gray-800 border border-gray-200 hover:border-gray-300 transition-all duration-200 shadow-sm hover:shadow-md"
title="Закрыть"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
)}
<div style={{ height: '550px' }}>
<Line data={chartData} options={chartOptions} />
</div>
<WeekProgressChart data={data} allProjectsSorted={getAllProjectsSorted(data)} currentWeekData={currentWeekData} selectedProject={selectedProject} />
</div>
)
}
export default FullStatistics