v2 added training breakdown charts and tables
This commit is contained in:
parent
c22404737a
commit
4466c3ddb0
51
index.html
51
index.html
@ -49,27 +49,64 @@
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<div class="chart-card">
|
||||
<h3>Прохождение по дивизионам</h3>
|
||||
<canvas id="divisionChart"></canvas>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Нагрузка тренеров</h3>
|
||||
<canvas id="trainerChart"></canvas>
|
||||
<div class="chart-card wide">
|
||||
<h3>Тренинги — количество прошедших</h3>
|
||||
<canvas id="trainingChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<div class="chart-card">
|
||||
<h3>Тренинги по дивизионам</h3>
|
||||
<canvas id="trainingByDivisionChart"></canvas>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Прохождение по дивизионам</h3>
|
||||
<canvas id="divisionChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<div class="chart-card">
|
||||
<h3>Нагрузка тренеров</h3>
|
||||
<canvas id="trainerChart"></canvas>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Распределение оценок</h3>
|
||||
<canvas id="scoreChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<div class="chart-card">
|
||||
<h3>Средняя оценка по тренингам</h3>
|
||||
<canvas id="trainingScoreChart"></canvas>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<h3>Статусы обучения</h3>
|
||||
<canvas id="statusChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card full-width">
|
||||
<h3>📋 Кто какие тренинги прошёл</h3>
|
||||
<div class="table-wrapper">
|
||||
<table id="trainingDetailTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Тренинг</th>
|
||||
<th>Прошли</th>
|
||||
<th>Не прошли</th>
|
||||
<th>% прохождения</th>
|
||||
<th>Средняя оценка</th>
|
||||
<th>Тренер</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tables-row">
|
||||
<div class="table-card">
|
||||
<h3>🏆 Чемпионы по обучению</h3>
|
||||
|
||||
110
script.js
110
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 `<tr>
|
||||
<td><strong>${t}</strong></td>
|
||||
<td>${done}</td>
|
||||
<td>${total - done}</td>
|
||||
<td>${pct}%</td>
|
||||
<td>${avg}</td>
|
||||
<td>${trainers}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
const coursesPerPerson = {};
|
||||
data.forEach(r => {
|
||||
if (!coursesPerPerson[r.name]) coursesPerPerson[r.name] = { division: r.division || '', courses: new Set() };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user