let workbook = null; let worksheet = null; let rawData = []; let fileName = ''; const uploadArea = document.getElementById('upload-area'); const fileInput = document.getElementById('file-input'); const fileInfo = document.getElementById('file-info'); const file_name = document.getElementById('file-name'); const fileMeta = document.getElementById('file-meta'); const analyzeBtn = document.getElementById('analyze-btn'); const errorMessage = document.getElementById('error-message'); const loading = document.getElementById('loading'); const results = document.getElementById('results'); uploadArea.addEventListener('click', () => fileInput.click()); uploadArea.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fileInput.click(); } }); uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); const files = e.dataTransfer.files; if (files.length > 0) { handleFile(files[0]); } }); fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleFile(e.target.files[0]); } }); function handleFile(file) { const validTypes = [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', 'text/csv', 'application/vnd.ms-excel.sheet.macroEnabled.12' ]; const ext = file.name.split('.').pop().toLowerCase(); const isValid = validTypes.includes(file.type) || ['xlsx', 'xls', 'csv'].includes(ext); if (!isValid) { showError('Неподдерживаемый формат файла. Загрузите .xlsx, .xls или .csv'); return; } fileName = file.name; file_name.textContent = '📄 ' + file.name; fileMeta.textContent = 'Размер: ' + formatFileSize(file.size); fileInfo.classList.add('visible'); uploadArea.classList.add('has-file'); analyzeBtn.disabled = false; hideError(); results.classList.remove('visible'); const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target.result); workbook = XLSX.read(data, { type: 'array' }); if (workbook.SheetNames.length > 0) { worksheet = workbook.Sheets[workbook.SheetNames[0]]; rawData = XLSX.utils.sheet_to_json(worksheet, { defval: '' }); if (rawData.length === 0) { showError('Файл пуст или не содержит данных'); analyzeBtn.disabled = true; return; } analyzeBtn.disabled = false; } } catch (err) { showError('Ошибка чтения файла: ' + err.message); analyzeBtn.disabled = true; } }; reader.onerror = () => { showError('Ошибка при чтении файла'); analyzeBtn.disabled = true; }; reader.readAsArrayBuffer(file); } analyzeBtn.addEventListener('click', () => { if (rawData.length === 0) { showError('Нет данных для анализа'); return; } loading.classList.add('visible'); results.classList.remove('visible'); setTimeout(() => { try { performAnalysis(); loading.classList.remove('visible'); results.classList.add('visible'); results.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (err) { loading.classList.remove('visible'); showError('Ошибка анализа: ' + err.message); } }, 500); }); function performAnalysis() { const columns = Object.keys(rawData[0]); const stats = calculateStats(rawData, columns); const insights = generateInsights(rawData, columns, stats); renderStats(stats); renderInsights(insights); renderCharts(rawData, columns, stats); renderTable(rawData, columns); } function calculateStats(data, columns) { const stats = { totalRows: data.length, totalColumns: columns.length, columns: columns, emptyCells: 0, numericColumns: [], textColumns: [], dateColumns: [], columnStats: {} }; columns.forEach(col => { const values = data.map(row => row[col]).filter(v => v !== '' && v !== null && v !== undefined); const emptyCount = data.length - values.length; stats.emptyCells += emptyCount; const firstValue = values[0]; const isNumeric = values.every(v => !isNaN(parseFloat(v)) && isFinite(v)); const isDate = values.some(v => { const d = new Date(v); return !isNaN(d.getTime()); }); if (isNumeric) { stats.numericColumns.push(col); const numValues = values.map(v => parseFloat(v)); stats.columnStats[col] = { type: 'numeric', min: Math.min(...numValues), max: Math.max(...numValues), avg: numValues.reduce((a, b) => a + b, 0) / numValues.length, sum: numValues.reduce((a, b) => a + b, 0), empty: emptyCount }; } else if (isDate) { stats.dateColumns.push(col); stats.columnStats[col] = { type: 'date', empty: emptyCount }; } else { stats.textColumns.push(col); const unique = [...new Set(values)]; const counts = {}; values.forEach(v => { counts[v] = (counts[v] || 0) + 1; }); const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]); stats.columnStats[col] = { type: 'text', unique: unique.length, top: sorted.slice(0, 5), empty: emptyCount }; } }); return stats; } function generateInsights(data, columns, stats) { const insights = []; insights.push({ icon: '📊', text: `Всего записей: ${stats.totalRows} в ${stats.totalColumns} колонках` }); if (stats.numericColumns.length > 0) { insights.push({ icon: '💰', text: `Числовые колонки: ${stats.numericColumns.join(', ')}` }); stats.numericColumns.forEach(col => { const s = stats.columnStats[col]; if (s && s.type === 'numeric') { insights.push({ icon: '📈', text: `${col}: среднее = ${formatNumber(s.avg)}, мин = ${formatNumber(s.min)}, макс = ${formatNumber(s.max)}` }); } }); } if (stats.textColumns.length > 0) { insights.push({ icon: '🏷️', text: `Текстовые колонки: ${stats.textColumns.join(', ')}` }); stats.textColumns.forEach(col => { const s = stats.columnStats[col]; if (s && s.type === 'text' && s.top.length > 0) { const topList = s.top.map(item => item[0]).slice(0, 3).join(', '); insights.push({ icon: '🔝', text: `${col}: чаще всего встречается "${topList}"` }); } }); } const emptyPercent = ((stats.emptyCells / (stats.totalRows * stats.totalColumns)) * 100).toFixed(1); if (emptyPercent > 10) { insights.push({ icon: '⚠️', text: `Внимание: ${emptyPercent}% пустых ячеек — проверьте полноту данных` }); } return insights; } function renderStats(stats) { const grid = document.getElementById('stats-grid'); grid.innerHTML = `
${stats.totalRows}
Всего записей
${stats.totalColumns}
Колонок
${stats.numericColumns.length}
Числовых полей
${formatNumber(stats.emptyCells)}
Пустых ячеек
`; } function renderInsights(insights) { const list = document.getElementById('insights-list'); list.innerHTML = insights.map(i => `
${i.icon}
${i.text}
`).join(''); } let charts = []; function renderCharts(data, columns, stats) { const container = document.getElementById('charts-section'); container.innerHTML = ''; charts.forEach(chart => chart.destroy()); charts = []; if (stats.numericColumns.length >= 2) { const chartCard = document.createElement('div'); chartCard.className = 'chart-card'; chartCard.innerHTML = `

Распределение числовых данных

`; container.appendChild(chartCard); const ctx = document.getElementById('barChart').getContext('2d'); const chart = new Chart(ctx, { type: 'bar', data: { labels: stats.numericColumns.slice(0, 10), datasets: [{ label: 'Среднее значение', data: stats.numericColumns.slice(0, 10).map(col => stats.columnStats[col] ? stats.columnStats[col].avg : 0 ), backgroundColor: 'rgba(99, 102, 241, 0.6)', borderColor: 'rgba(99, 102, 241, 1)', borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true } }, scales: { y: { beginAtZero: true } } } }); charts.push(chart); } if (stats.textColumns.length > 0 && stats.textColumns.some(col => stats.columnStats[col] && stats.columnStats[col].unique <= 10 )) { const textCol = stats.textColumns.find(col => stats.columnStats[col] && stats.columnStats[col].unique <= 10 ); if (textCol) { const s = stats.columnStats[textCol]; const chartCard = document.createElement('div'); chartCard.className = 'chart-card'; chartCard.innerHTML = `

Распределение: ${textCol}

`; container.appendChild(chartCard); const ctx = document.getElementById('pieChart').getContext('2d'); const chart = new Chart(ctx, { type: 'pie', data: { labels: s.top.map(item => item[0]), datasets: [{ data: s.top.map(item => item[1]), backgroundColor: [ 'rgba(99, 102, 241, 0.8)', 'rgba(16, 185, 129, 0.8)', 'rgba(245, 158, 11, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(139, 92, 246, 0.8)' ] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } } }); charts.push(chart); } } if (stats.numericColumns.length >= 1 && data.length <= 100) { const chartCard = document.createElement('div'); chartCard.className = 'chart-card'; chartCard.innerHTML = `

Динамика данных

`; container.appendChild(chartCard); const firstNumeric = stats.numericColumns[0]; const values = data.slice(0, 50).map((_, i) => data[i] && data[i][firstNumeric] ? parseFloat(data[i][firstNumeric]) : 0 ); const ctx = document.getElementById('lineChart').getContext('2d'); const chart = new Chart(ctx, { type: 'line', data: { labels: values.map((_, i) => i + 1), datasets: [{ label: firstNumeric, data: values, borderColor: 'rgba(16, 185, 129, 1)', backgroundColor: 'rgba(16, 185, 129, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true } }, scales: { y: { beginAtZero: false } } } }); charts.push(chart); } } function renderTable(data, columns) { const table = document.getElementById('data-table'); const maxRows = Math.min(100, data.length); let html = ''; columns.forEach(col => { html += '' + escapeHtml(col) + ''; }); html += ''; for (let i = 0; i < maxRows; i++) { html += ''; columns.forEach(col => { html += '' + escapeHtml(data[i][col]) + ''; }); html += ''; } if (data.length > maxRows) { html += ''; html += '... показано 100 из ' + data.length + ' записей ...'; html += ''; } html += ''; table.innerHTML = html; } function exportToCSV() { if (!rawData || rawData.length === 0) return; const ws = XLSX.utils.json_to_sheet(rawData); const csv = XLSX.utils.sheet_to_csv(ws); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = fileName.replace(/\.[^/.]+$/, '') + '_analysis.csv'; link.click(); } function exportToExcel() { if (!rawData || rawData.length === 0) return; const wb = XLSX.utils.book_new(); const ws = XLSX.utils.json_to_sheet(rawData); XLSX.utils.book_append_sheet(wb, ws, 'Data'); XLSX.writeFile(wb, fileName.replace(/\.[^/.]+$/, '') + '_analysis.xlsx'); } function showError(msg) { errorMessage.textContent = msg; errorMessage.classList.add('visible'); } function hideError() { errorMessage.classList.remove('visible'); } function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' Б'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' КБ'; return (bytes / 1048576).toFixed(1) + ' МБ'; } function formatNumber(num) { if (typeof num !== 'number') return num; if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; return Math.round(num * 100) / 100; } function escapeHtml(text) { if (text === null || text === undefined) return ''; const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; }