Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Анализ оценок студентов - ОП</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #4285F4; | |
| --google-blue: #1A73E8; | |
| --google-red: #D93025; | |
| --google-yellow: #F4B400; | |
| --google-green: #0F9D58; | |
| --light-gray: #f5f5f5; | |
| --border-gray: #dadce0; | |
| --text-dark: #202124; | |
| --text-medium: #5f6368; | |
| --text-light: #80868b; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Product Sans', 'Roboto', Arial, sans-serif; | |
| } | |
| body { | |
| background-color: var(--light-gray); | |
| color: var(--text-dark); | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| header { | |
| display: flex; | |
| align-items: center; | |
| padding: 16px 0; | |
| margin-bottom: 24px; | |
| border-bottom: 1px solid var(--border-gray); | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| margin-right: 24px; | |
| } | |
| .logo-icon { | |
| font-size: 24px; | |
| color: var(--google-blue); | |
| margin-right: 8px; | |
| } | |
| .logo-text { | |
| font-size: 22px; | |
| font-weight: 500; | |
| color: var(--text-dark); | |
| } | |
| .card { | |
| background-color: white; | |
| border-radius: 8px; | |
| box-shadow: 0 1px 2px 0 rgba(60,64,67,0.3), 0 2px 6px 2px rgba(60,64,67,0.15); | |
| padding: 24px; | |
| margin-bottom: 24px; | |
| } | |
| .section-title { | |
| font-size: 20px; | |
| font-weight: 500; | |
| margin-bottom: 20px; | |
| color: var(--google-blue); | |
| display: flex; | |
| align-items: center; | |
| } | |
| .section-title i { | |
| margin-right: 10px; | |
| } | |
| .grid-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .chart-container { | |
| position: relative; | |
| height: 400px; | |
| background: white; | |
| padding: 20px; | |
| border-radius: 8px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| } | |
| .stats-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-bottom: 30px; | |
| } | |
| .stat-card { | |
| background: white; | |
| padding: 15px; | |
| border-radius: 8px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 28px; | |
| font-weight: bold; | |
| color: var(--google-blue); | |
| margin: 10px 0; | |
| } | |
| .stat-label { | |
| font-size: 14px; | |
| color: var(--text-medium); | |
| } | |
| .table-container { | |
| overflow-x: auto; | |
| margin-bottom: 20px; | |
| } | |
| .data-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| .data-table th { | |
| background-color: var(--light-gray); | |
| padding: 12px 15px; | |
| text-align: left; | |
| font-weight: 500; | |
| } | |
| .data-table td { | |
| padding: 10px 15px; | |
| border-bottom: 1px solid var(--border-gray); | |
| } | |
| .data-table tr:hover { | |
| background-color: rgba(66, 133, 244, 0.05); | |
| } | |
| .loading { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 200px; | |
| } | |
| .loading-spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 3px solid rgba(66, 133, 244, 0.2); | |
| border-radius: 50%; | |
| border-top-color: var(--google-blue); | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .excellent { background-color: rgba(15, 157, 88, 0.1); } | |
| .good { background-color: rgba(244, 180, 0, 0.1); } | |
| .average { background-color: rgba(217, 48, 37, 0.1); } | |
| .poor { background-color: rgba(217, 48, 37, 0.2); } | |
| @media (max-width: 768px) { | |
| .grid-container { | |
| grid-template-columns: 1fr; | |
| } | |
| .stats-container { | |
| grid-template-columns: 1fr 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div class="logo"> | |
| <i class="fas fa-chart-line logo-icon"></i> | |
| <span class="logo-text">Анализ оценок студентов</span> | |
| </div> | |
| <span>Данные из таблицы "ОП" - Оценки за домашние задания</span> | |
| </header> | |
| <div class="card"> | |
| <div class="section-title"> | |
| <i class="fas fa-chart-pie"></i> | |
| Статистика оценок | |
| </div> | |
| <div class="stats-container" id="statsContainer"> | |
| <div class="loading" id="loadingIndicator"> | |
| <div class="loading-spinner"></div> | |
| </div> | |
| </div> | |
| <div class="section-title"> | |
| <i class="fas fa-chart-bar"></i> | |
| Визуализация данных | |
| </div> | |
| <div class="grid-container"> | |
| <div class="chart-container"> | |
| <canvas id="distributionChart"></canvas> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="histogramChart"></canvas> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="pieChart"></canvas> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="boxPlotChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="section-title"> | |
| <i class="fas fa-table"></i> | |
| Данные студентов | |
| </div> | |
| <div class="table-container"> | |
| <table class="data-table" id="studentTable"> | |
| <thead> | |
| <tr> | |
| <th>№</th> | |
| <th>ФИО</th> | |
| <th>Группа</th> | |
| <th>ДЗ (из 2800)</th> | |
| <th>Процент</th> | |
| <th>Категория</th> | |
| </tr> | |
| </thead> | |
| <tbody id="studentTableBody"> | |
| <!-- Filled by JavaScript --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Mock data based on the structure of the actual Google Sheet | |
| // In a real implementation, you would fetch this from the Google Sheets API | |
| const mockStudentData = [ | |
| { id: 1, name: "Иванов Иван Иванович", group: "Группа 1", dz: 2450 }, | |
| { id: 2, name: "Петров Петр Петрович", group: "Группа 1", dz: 2100 }, | |
| { id: 3, name: "Сидорова Мария Сергеевна", group: "Группа 2", dz: 1960 }, | |
| { id: 4, name: "Кузнецов Алексей Дмитриевич", group: "Группа 2", dz: 1780 }, | |
| { id: 5, name: "Смирнова Екатерина Викторовна", group: "Группа 3", dz: 2650 }, | |
| { id: 6, name: "Васильев Денис Олегович", group: "Группа 3", dz: 1550 }, | |
| { id: 7, name: "Николаева Анна Павловна", group: "Группа 1", dz: 2250 }, | |
| { id: 8, name: "Михайлов Артем Игоревич", group: "Группа 4", dz: 1980 }, | |
| { id: 9, name: "Федорова Ольга Николаевна", group: "Группа 4", dz: 2400 }, | |
| { id: 10, name: "Алексеев Сергей Владимирович", group: "Группа 5", dz: 1750 }, | |
| { id: 11, name: "Дмитриева Татьяна Александровна", group: "Группа 5", dz: 2100 }, | |
| { id: 12, name: "Андреев Максим Юрьевич", group: "Группа 5", dz: 1900 }, | |
| { id: 13, name: "Егорова Виктория Алексеевна", group: "Группа 1", dz: 2300 }, | |
| { id: 14, name: "Григорьева Наталья Сергеевна", group: "Группа 3", dz: 1680 }, | |
| { id: 15, name: "Семенов Дмитрий Петрович", group: "Группа 4", dz: 1950 }, | |
| { id: 16, name: "Павлов Антон Викторович", group: "Группа 2", dz: 2500 }, | |
| { id: 17, name: "Романова Елена Дмитриевна", group: "Группа 3", dz: 2350 }, | |
| { id: 18, name: "Козлов Илья Сергеевич", group: "Группа 4", dz: 2050 }, | |
| { id: 19, name: "Орлова Анастасия Андреевна", group: "Группа 5", dz: 1850 }, | |
| { id: 20, name: "Лебедев Владислав Игоревич", group: "Группа 2", dz: 1720 } | |
| ]; | |
| // Constants | |
| const maxScore = 2800; | |
| const gradeCategories = [ | |
| { name: "Отлично", minPercent: 85, color: "#0F9D58", colorLight: "rgba(15, 157, 88, 0.1)" }, | |
| { name: "Хорошо", minPercent: 70, color: "#F4B400", colorLight: "rgba(244, 180, 0, 0.1)" }, | |
| { name: "Удовлетворительно", minPercent: 50, color: "#FF9800", colorLight: "rgba(255, 152, 0, 0.1)" }, | |
| { name: "Неудовлетворительно", minPercent: 0, color: "#D93025", colorLight: "rgba(217, 48, 37, 0.1)" } | |
| ]; | |
| // DOM elements | |
| const loadingIndicator = document.getElementById('loadingIndicator'); | |
| const statsContainer = document.getElementById('statsContainer'); | |
| const studentTableBody = document.getElementById('studentTableBody'); | |
| const distributionCtx = document.getElementById('distributionChart').getContext('2d'); | |
| const histogramCtx = document.getElementById('histogramChart').getContext('2d'); | |
| const pieCtx = document.getElementById('pieChart').getContext('2d'); | |
| const boxPlotCtx = document.getElementById('boxPlotChart').getContext('2d'); | |
| // Initialize the application | |
| function init() { | |
| // Simulate loading delay | |
| setTimeout(() => { | |
| // Process student data | |
| const processedStudents = processStudentData(mockStudentData); | |
| // Calculate statistics | |
| const stats = calculateStatistics(processedStudents); | |
| // Display statistics | |
| displayStatistics(stats); | |
| // Display student table | |
| displayStudentTable(processedStudents); | |
| // Create charts | |
| createCharts(processedStudents, stats); | |
| // Hide loading indicator | |
| loadingIndicator.style.display = 'none'; | |
| }, 1000); | |
| } | |
| // Process student data to add percentage and category | |
| function processStudentData(students) { | |
| return students.map(student => { | |
| const percentage = Math.round((student.dz / maxScore) * 100); | |
| let category = ""; | |
| // Determine grade category | |
| for (const cat of gradeCategories) { | |
| if (percentage >= cat.minPercent) { | |
| category = cat.name; | |
| break; | |
| } | |
| } | |
| return { | |
| ...student, | |
| percentage, | |
| category | |
| }; | |
| }); | |
| } | |
| // Calculate statistical measures | |
| function calculateStatistics(students) { | |
| const dzScores = students.map(s => s.dz); | |
| const percentages = students.map(s => s.percentage); | |
| // Basic statistics | |
| const count = dzScores.length; | |
| const min = Math.min(...dzScores); | |
| const max = Math.max(...dzScores); | |
| const sum = dzScores.reduce((a, b) => a + b, 0); | |
| const mean = Math.round(sum / count); | |
| const meanPerc = Math.round(mean / maxScore * 100); | |
| // Median | |
| const sorted = [...dzScores].sort((a, b) => a - b); | |
| const median = sorted.length % 2 === 0 | |
| ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2 | |
| : sorted[Math.floor(sorted.length/2)]; | |
| const medianPerc = Math.round(median / maxScore * 100); | |
| // Standard deviation | |
| const squaredDiffs = dzScores.map(score => Math.pow(score - mean, 2)); | |
| const variance = squaredDiffs.reduce((a, b) => a + b, 0) / count; | |
| const stdDev = Math.round(Math.sqrt(variance)); | |
| // Quartiles | |
| const q1 = percentile(sorted, 25); | |
| const q3 = percentile(sorted, 75); | |
| const iqr = q3 - q1; | |
| // Grade category counts | |
| const gradeCounts = {}; | |
| gradeCategories.forEach(cat => { | |
| gradeCounts[cat.name] = students.filter(s => s.category === cat.name).length; | |
| }); | |
| return { | |
| count, min, max, mean, meanPerc, median, medianPerc, | |
| stdDev, q1, q3, iqr, gradeCounts | |
| }; | |
| } | |
| // Helper function to calculate percentiles | |
| function percentile(arr, p) { | |
| const index = p * (arr.length - 1); | |
| const lower = Math.floor(index); | |
| const upper = lower + 1; | |
| const weight = index % 1; | |
| if (upper >= arr.length) return arr[lower]; | |
| return arr[lower] * (1 - weight) + arr[upper] * weight; | |
| } | |
| // Display statistics cards | |
| function displayStatistics(stats) { | |
| statsContainer.innerHTML = ` | |
| <div class="stat-card"> | |
| <div class="stat-label">Количество студентов</div> | |
| <div class="stat-value">${stats.count}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Средний балл</div> | |
| <div class="stat-value">${stats.mean} (${stats.meanPerc}%)</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Медианный балл</div> | |
| <div class="stat-value">${stats.median} (${stats.medianPerc}%)</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Стандартное отклонение</div> | |
| <div class="stat-value">${stats.stdDev}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Минимальный балл</div> | |
| <div class="stat-value">${stats.min}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">Максимальный балл</div> | |
| <div class="stat-value">${stats.max}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">1-й квартиль (Q1)</div> | |
| <div class="stat-value">${stats.q1}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">3-й квартиль (Q3)</div> | |
| <div class="stat-value">${stats.q3}</div> | |
| </div> | |
| `; | |
| } | |
| // Display student table | |
| function displayStudentTable(students) { | |
| studentTableBody.innerHTML = ''; | |
| students.forEach(student => { | |
| const row = document.createElement('tr'); | |
| // Apply CSS class based on grade category | |
| for (const cat of gradeCategories) { | |
| if (student.category === cat.name) { | |
| if (cat.name === "Отлично") row.classList.add("excellent"); | |
| else if (cat.name === "Хорошо") row.classList.add("good"); | |
| else if (cat.name === "Удовлетворительно") row.classList.add("average"); | |
| else if (cat.name === "Неудовлетворительно") row.classList.add("poor"); | |
| break; | |
| } | |
| } | |
| row.innerHTML = ` | |
| <td>${student.id}</td> | |
| <td>${student.name}</td> | |
| <td>${student.group}</td> | |
| <td>${student.dz}</td> | |
| <td>${student.percentage}%</td> | |
| <td>${student.category}</td> | |
| `; | |
| studentTableBody.appendChild(row); | |
| }); | |
| } | |
| // Create all charts | |
| function createCharts(students, stats) { | |
| createDistributionChart(students); | |
| createHistogramChart(students); | |
| createPieChart(students, stats); | |
| createBoxPlotChart(stats); | |
| } | |
| // Create distribution chart | |
| function createDistributionChart(students) { | |
| const scores = students.map(s => s.dz); | |
| const percentages = students.map(s => s.percentage); | |
| new Chart(distributionCtx, { | |
| type: 'scatter', | |
| data: { | |
| datasets: [{ | |
| label: 'Баллы студентов', | |
| data: students.map(s => ({ x: s.id, y: s.dz })), | |
| backgroundColor: students.map(s => { | |
| for (const cat of gradeCategories) { | |
| if (s.category === cat.name) return cat.color; | |
| } | |
| return '#4285F4'; | |
| }), | |
| pointRadius: 6, | |
| pointHoverRadius: 8 | |
| }, { | |
| label: 'Средний балл', | |
| data: [{ x: 1, y: stats.mean }, { x: students.length, y: stats.mean }], | |
| type: 'line', | |
| borderColor: '#EA4335', | |
| borderWidth: 2, | |
| borderDash: [5, 5], | |
| pointRadius: 0, | |
| fill: false, | |
| showLine: true | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| x: { | |
| title: { | |
| display: true, | |
| text: '№ студента' | |
| } | |
| }, | |
| y: { | |
| title: { | |
| display: true, | |
| text: 'Баллы (из 2800)' | |
| }, | |
| min: 0, | |
| max: 2800 | |
| } | |
| }, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: 'Распределение оценок студентов' | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| const student = students[context.parsed.x - 1]; | |
| return `${student.name}: ${student.dz} (${student.percentage}%) - ${student.category}`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Create histogram chart | |
| function createHistogramChart(students) { | |
| const step = 200; | |
| const bins = {}; | |
| // Create bins from 0 to 2800 with step=200 | |
| for (let i = 0; i <= maxScore; i += step) { | |
| bins[`${i}-${i+step-1}`] = 0; | |
| } | |
| // Count students in each bin | |
| students.forEach(student => { | |
| const binKey = Object.keys(bins).find(key => { | |
| const [min, max] = key.split('-').map(Number); | |
| return student.dz >= min && student.dz <= max; | |
| }); | |
| if (binKey) bins[binKey]++; | |
| }); | |
| new Chart(histogramCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: Object.keys(bins), | |
| datasets: [{ | |
| label: 'Количество студентов', | |
| data: Object.values(bins), | |
| backgroundColor: '#4285F4', | |
| borderColor: '#1A73E8', | |
| borderWidth: 1 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| x: { | |
| title: { | |
| display: true, | |
| text: 'Диапазон баллов' | |
| } | |
| }, | |
| y: { | |
| title: { | |
| display: true, | |
| text: 'Количество студентов' | |
| }, | |
| beginAtZero: true | |
| } | |
| }, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: 'Гистограмма распределения оценок' | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Create pie chart of grade categories | |
| function createPieChart(students, stats) { | |
| const data = { | |
| labels: Object.keys(stats.gradeCounts), | |
| datasets: [{ | |
| data: Object.values(stats.gradeCounts), | |
| backgroundColor: gradeCategories.map(cat => cat.color), | |
| borderWidth: 1 | |
| }] | |
| }; | |
| new Chart(pieCtx, { | |
| type: 'pie', | |
| data: data, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: 'Распределение по категориям оценок' | |
| }, | |
| legend: { | |
| position: 'right' | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| label: function(context) { | |
| const label = context.label; | |
| const value = context.raw; | |
| const percentage = Math.round((value / stats.count) * 100); | |
| return `${label}: ${value} студентов (${percentage}%)`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Create box plot chart | |
| function createBoxPlotChart(stats) { | |
| new Chart(boxPlotCtx, { | |
| type: 'boxplot', | |
| data: { | |
| labels: ['Оценки за ДЗ'], | |
| datasets: [{ | |
| label: 'Статистика оценок', | |
| data: [{ | |
| min: stats.min, | |
| q1: stats.q1, | |
| median: stats.median, | |
| q3: stats.q3, | |
| max: stats.max | |
| }], | |
| backgroundColor: 'rgba(66, 133, 244, 0.2)', | |
| borderColor: '#4285F4', | |
| borderWidth: 2, | |
| outlierColor: '#EA4335', | |
| padding: 10, | |
| itemRadius: 0 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| title: { | |
| display: true, | |
| text: 'Баллы (из 2800)' | |
| }, | |
| min: 0, | |
| max: 2800 | |
| } | |
| }, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: 'Box plot оценок студентов' | |
| }, | |
| tooltip: { | |
| callbacks: { | |
| beforeLabel: function(context) { | |
| return 'Статистика оценок:'; | |
| }, | |
| label: function(context) { | |
| const data = context.raw; | |
| return [ | |
| `Минимум: ${data.min}`, | |
| `Q1: ${data.q1}`, | |
| `Медиана: ${data.median}`, | |
| `Q3: ${data.q3}`, | |
| `Максимум: ${data.max}`, | |
| `Диапазон: ${data.max - data.min}`, | |
| `IQR: ${data.q3 - data.q1}` | |
| ]; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Initialize the application when the page loads | |
| document.addEventListener('DOMContentLoaded', init); | |
| </script> | |
| </body> | |
| </html> |