sozday-sovremennyy-dostupnyy/script.js

511 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: `<strong>Всего записей:</strong> ${stats.totalRows} в ${stats.totalColumns} колонках`
});
if (stats.numericColumns.length > 0) {
insights.push({
icon: '💰',
text: `<strong>Числовые колонки:</strong> ${stats.numericColumns.join(', ')}`
});
stats.numericColumns.forEach(col => {
const s = stats.columnStats[col];
if (s && s.type === 'numeric') {
insights.push({
icon: '📈',
text: `<strong>${col}:</strong> среднее = ${formatNumber(s.avg)}, мин = ${formatNumber(s.min)}, макс = ${formatNumber(s.max)}`
});
}
});
}
if (stats.textColumns.length > 0) {
insights.push({
icon: '🏷️',
text: `<strong>Текстовые колонки:</strong> ${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: `<strong>${col}:</strong> чаще всего встречается "${topList}"`
});
}
});
}
const emptyPercent = ((stats.emptyCells / (stats.totalRows * stats.totalColumns)) * 100).toFixed(1);
if (emptyPercent > 10) {
insights.push({
icon: '⚠️',
text: `<strong>Внимание:</strong> ${emptyPercent}% пустых ячеек — проверьте полноту данных`
});
}
return insights;
}
function renderStats(stats) {
const grid = document.getElementById('stats-grid');
grid.innerHTML = `
<div class="stat-card">
<div class="stat-value">${stats.totalRows}</div>
<div class="stat-label">Всего записей</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.totalColumns}</div>
<div class="stat-label">Колонок</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.numericColumns.length}</div>
<div class="stat-label">Числовых полей</div>
</div>
<div class="stat-card">
<div class="stat-value">${formatNumber(stats.emptyCells)}</div>
<div class="stat-label">Пустых ячеек</div>
</div>
`;
}
function renderInsights(insights) {
const list = document.getElementById('insights-list');
list.innerHTML = insights.map(i => `
<div class="insight-item">
<div class="insight-icon">${i.icon}</div>
<div class="insight-text">${i.text}</div>
</div>
`).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 = `
<h3>Распределение числовых данных</h3>
<div class="chart-container">
<canvas id="barChart"></canvas>
</div>
`;
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 = `
<h3>Распределение: ${textCol}</h3>
<div class="chart-container">
<canvas id="pieChart"></canvas>
</div>
`;
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 = `
<h3>Динамика данных</h3>
<div class="chart-container">
<canvas id="lineChart"></canvas>
</div>
`;
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 = '<thead><tr>';
columns.forEach(col => {
html += '<th>' + escapeHtml(col) + '</th>';
});
html += '</tr></thead><tbody>';
for (let i = 0; i < maxRows; i++) {
html += '<tr>';
columns.forEach(col => {
html += '<td>' + escapeHtml(data[i][col]) + '</td>';
});
html += '</tr>';
}
if (data.length > maxRows) {
html += '<tr><td colspan="' + columns.length + '" style="text-align:center;color:#718096;">';
html += '... показано 100 из ' + data.length + ' записей ...';
html += '</td></tr>';
}
html += '</tbody>';
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;
}