diff --git a/index.html b/index.html index d7a7d10..bc6c670 100644 --- a/index.html +++ b/index.html @@ -49,27 +49,64 @@
-
-

Прохождение по дивизионам

- -
-
-

Нагрузка тренеров

- +
+

Тренинги — количество прошедших

+
+
+

Тренинги по дивизионам

+ +
+
+

Прохождение по дивизионам

+ +
+
+ +
+
+

Нагрузка тренеров

+ +

Распределение оценок

+
+ +
+
+

Средняя оценка по тренингам

+ +

Статусы обучения

+
+

📋 Кто какие тренинги прошёл

+
+ + + + + + + + + + + + +
ТренингПрошлиНе прошли% прохожденияСредняя оценкаТренер
+
+
+

🏆 Чемпионы по обучению

diff --git a/script.js b/script.js index afb53e4..1f4af33 100644 --- a/script.js +++ b/script.js @@ -65,6 +65,77 @@ function processAndRender(rawData) { document.getElementById('completionRate').textContent = rate + '%'; document.getElementById('avgScore').textContent = avgScore; + const trainingStats = {}; + data.forEach(r => { + const course = r.course || 'Без названия'; + if (!trainingStats[course]) trainingStats[course] = { completed: new Set(), all: new Set(), scores: [], trainers: new Set(), divisions: {} }; + trainingStats[course].all.add(r.name); + const s = (r.status || '').toLowerCase(); + const isCompleted = s.includes('завершен') || s.includes('прошёл') || s.includes('прошел') || s === 'completed' || s.includes('ок'); + if (isCompleted) { + trainingStats[course].completed.add(r.name); + } + const score = parseFloat(r.score); + if (!isNaN(score) && score > 0) trainingStats[course].scores.push(score); + if (r.trainer) trainingStats[course].trainers.add(r.trainer); + const div = r.division || 'Без дивизиона'; + if (!trainingStats[course].divisions[div]) trainingStats[course].divisions[div] = { total: new Set(), completed: new Set() }; + trainingStats[course].divisions[div].total.add(r.name); + if (isCompleted) trainingStats[course].divisions[div].completed.add(r.name); + }); + + const trainingLabels = Object.keys(trainingStats); + const trainingCompletedCounts = trainingLabels.map(t => trainingStats[t].completed.size); + const trainingTotalCounts = trainingLabels.map(t => trainingStats[t].all.size); + + renderChart('trainingChart', 'bar', { + labels: trainingLabels, + datasets: [ + { + label: 'Всего записей', + data: trainingTotalCounts, + backgroundColor: 'rgba(33, 150, 243, 0.4)', + borderColor: '#2196F3', + borderWidth: 1 + }, + { + label: 'Прошли', + data: trainingCompletedCounts, + backgroundColor: 'rgba(76, 175, 80, 0.7)', + borderColor: '#4caf50', + borderWidth: 1 + } + ] + }, { + indexAxis: 'y', + scales: { + x: { beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, + y: { ticks: { color: '#e0e0e0', font: { size: 11 } }, grid: { color: '#1e2d3d' } } + } + }); + + const allDivisions = [...new Set(data.map(r => r.division || 'Без дивизиона'))]; + const trainingByDivDatasets = allDivisions.map((div, i) => { + const colors = ['#2196F3', '#4caf50', '#ff9800', '#9c27b0', '#00bcd4', '#ff5722', '#8bc34a', '#e91e63']; + return { + label: div, + data: trainingLabels.map(t => trainingStats[t].divisions[div] ? trainingStats[t].divisions[div].completed.size : 0), + backgroundColor: colors[i % colors.length] + 'aa', + borderColor: colors[i % colors.length], + borderWidth: 1 + }; + }); + + renderChart('trainingByDivisionChart', 'bar', { + labels: trainingLabels, + datasets: trainingByDivDatasets + }, { + scales: { + x: { stacked: true, ticks: { color: '#90a4ae', maxRotation: 45 }, grid: { color: '#1e2d3d' } }, + y: { stacked: true, beginAtZero: true, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } } + } + }); + const divisionStats = {}; data.forEach(r => { const div = r.division || 'Без дивизиона'; @@ -169,6 +240,45 @@ function processAndRender(rawData) { }] }); + const trainingAvgScores = trainingLabels.map(t => { + const sc = trainingStats[t].scores; + return sc.length ? (sc.reduce((a, b) => a + b, 0) / sc.length).toFixed(1) : 0; + }); + + renderChart('trainingScoreChart', 'bar', { + labels: trainingLabels, + datasets: [{ + label: 'Средняя оценка', + data: trainingAvgScores, + backgroundColor: trainingAvgScores.map(v => v >= 4 ? 'rgba(76,175,80,0.7)' : v >= 3 ? 'rgba(255,152,0,0.7)' : 'rgba(244,67,54,0.7)'), + borderColor: trainingAvgScores.map(v => v >= 4 ? '#4caf50' : v >= 3 ? '#ff9800' : '#f44336'), + borderWidth: 1 + }] + }, { + scales: { + y: { beginAtZero: true, max: 5, ticks: { color: '#90a4ae' }, grid: { color: '#1e2d3d' } }, + x: { ticks: { color: '#e0e0e0', maxRotation: 45 }, grid: { color: '#1e2d3d' } } + } + }); + + const trainingDetailBody = document.querySelector('#trainingDetailTable tbody'); + trainingDetailBody.innerHTML = trainingLabels.map(t => { + const st = trainingStats[t]; + const total = st.all.size; + const done = st.completed.size; + const pct = total ? Math.round(done / total * 100) : 0; + const avg = st.scores.length ? (st.scores.reduce((a, b) => a + b, 0) / st.scores.length).toFixed(1) : '—'; + const trainers = [...st.trainers].join(', ') || '—'; + return ` + ${t} + ${done} + ${total - done} + ${pct}% + ${avg} + ${trainers} + `; + }).join(''); + const coursesPerPerson = {}; data.forEach(r => { if (!coursesPerPerson[r.name]) coursesPerPerson[r.name] = { division: r.division || '', courses: new Set() }; diff --git a/style.css b/style.css index 1a736a4..79d4b06 100644 --- a/style.css +++ b/style.css @@ -142,6 +142,10 @@ main { border: 1px solid #263545; } +.chart-card.wide { + grid-column: 1 / -1; +} + .chart-card h3 { font-size: 16px; color: #fff;