| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Large CSV Analyzer</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/papaparse@5.3.0/papaparse.min.js"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .dropzone { |
| border: 2px dashed #ccc; |
| transition: all 0.3s ease; |
| } |
| .dropzone.active { |
| border-color: #4f46e5; |
| background-color: #f0f7ff; |
| } |
| .progress-bar { |
| transition: width 0.3s ease; |
| } |
| .column-selector:hover { |
| background-color: #f3f4f6; |
| } |
| .column-type-selector { |
| max-width: 80px; |
| background-color: white; |
| } |
| .column-type-selector:focus { |
| outline: none; |
| border-color: #4f46e5; |
| } |
| .column-selected { |
| background-color: #e0e7ff; |
| border-left: 3px solid #4f46e5; |
| } |
| .data-table-container { |
| max-height: 500px; |
| overflow-y: auto; |
| } |
| .chart-container { |
| height: 400px; |
| } |
| |
| |
| ::-webkit-scrollbar { |
| width: 8px; |
| height: 8px; |
| } |
| ::-webkit-scrollbar-track { |
| background: #f1f1f1; |
| } |
| ::-webkit-scrollbar-thumb { |
| background: #c7d2fe; |
| border-radius: 4px; |
| } |
| ::-webkit-scrollbar-thumb:hover { |
| background: #a5b4fc; |
| } |
| |
| |
| .table-wrapper { |
| overflow-x: auto; |
| width: 100%; |
| } |
| .data-table { |
| min-width: 100%; |
| width: auto; |
| } |
| .data-table th, .data-table td { |
| white-space: nowrap; |
| padding: 0.75rem 1rem; |
| text-align: left; |
| border-bottom: 1px solid #e5e7eb; |
| } |
| .data-table th { |
| position: sticky; |
| top: 0; |
| background-color: #f9fafb; |
| z-index: 10; |
| } |
| .data-table tr:hover { |
| background-color: #f3f4f6; |
| } |
| .row-number { |
| color: #6b7280; |
| font-weight: 500; |
| } |
| .pagination-btn { |
| min-width: 2.5rem; |
| } |
| .pagination-btn.active { |
| background-color: #4f46e5; |
| color: white; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <div class="text-center mb-8"> |
| <h1 class="text-3xl font-bold text-indigo-700 mb-2">Large CSV Analyzer</h1> |
| <p class="text-gray-600">Upload CSV files up to 5GB, preview data, and generate insightful visualizations</p> |
| </div> |
|
|
| |
| <div class="bg-white rounded-lg shadow-md p-6 mb-8"> |
| <div id="upload-container" class="mb-6"> |
| <div id="dropzone" class="dropzone rounded-lg p-12 text-center cursor-pointer"> |
| <div class="flex flex-col items-center justify-center"> |
| <i class="fas fa-cloud-upload-alt text-4xl text-indigo-500 mb-4"></i> |
| <h3 class="text-xl font-semibold text-gray-700 mb-2">Drag & Drop your CSV file here</h3> |
| <p class="text-gray-500 mb-4">or</p> |
| <label for="file-input" class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-6 rounded-md cursor-pointer transition duration-200"> |
| Browse Files |
| </label> |
| <input id="file-input" type="file" accept=".csv" class="hidden"> |
| </div> |
| </div> |
| <div class="mt-4 text-sm text-gray-500"> |
| <p>Supported file types: .csv (max 5GB)</p> |
| </div> |
| </div> |
|
|
| |
| <div id="upload-progress" class="hidden"> |
| <div class="flex justify-between mb-1"> |
| <span class="text-sm font-medium text-indigo-700" id="filename-display"></span> |
| <span class="text-sm font-medium text-indigo-700" id="progress-percentage">0%</span> |
| </div> |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> |
| <div id="progress-bar" class="progress-bar bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div> |
| </div> |
| <div class="flex justify-between mt-2 text-sm text-gray-500"> |
| <span id="uploaded-size">0 MB</span> |
| <span id="total-size">0 MB</span> |
| </div> |
| <div class="mt-4 flex justify-center"> |
| <button id="cancel-upload" class="text-red-600 hover:text-red-800 font-medium flex items-center"> |
| <i class="fas fa-times-circle mr-2"></i> Cancel Upload |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="data-preview" class="hidden bg-white rounded-lg shadow-md p-6 mb-8"> |
| <div class="flex justify-between items-center mb-6"> |
| <h2 class="text-xl font-semibold text-gray-800">Data Preview</h2> |
| <div class="flex gap-2"> |
| <button id="reset-analysis" class="text-indigo-600 hover:text-indigo-800 font-medium flex items-center"> |
| <i class="fas fa-redo-alt mr-2"></i> Reset Analysis |
| </button> |
| <button id="load-full-data" class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md"> |
| <i class="fas fa-database mr-2"></i> Load Full Dataset |
| </button> |
| </div> |
| </div> |
| |
| <div class="flex flex-col md:flex-row gap-6"> |
| |
| <div class="w-full md:w-1/4"> |
| <div class="bg-gray-50 rounded-lg p-4 border border-gray-200"> |
| <h3 class="font-medium text-gray-700 mb-3">Select Columns</h3> |
| <div class="space-y-2 max-h-96 overflow-y-auto" id="column-list"> |
| |
| </div> |
| <div class="mt-4"> |
| <button id="analyze-btn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
| <i class="fas fa-chart-bar mr-2"></i> Analyze Selected Columns |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="w-full md:w-3/4"> |
| <div class="table-wrapper"> |
| <div class="data-table-container border border-gray-200 rounded-lg"> |
| <table class="data-table"> |
| <thead> |
| <tr id="table-header"> |
| <th class="row-number">#</th> |
| |
| </tr> |
| </thead> |
| <tbody id="table-body"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
| |
| |
| <div class="flex flex-col sm:flex-row items-center justify-between mt-4"> |
| <div class="text-sm text-gray-500 mb-2 sm:mb-0"> |
| <span id="row-count-display">Showing rows 1-20 of 20</span> |
| </div> |
| <div class="flex items-center space-x-1" id="pagination-controls"> |
| <button class="pagination-btn bg-white border border-gray-300 rounded-md px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" id="first-page" disabled> |
| <i class="fas fa-angle-double-left"></i> |
| </button> |
| <button class="pagination-btn bg-white border border-gray-300 rounded-md px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" id="prev-page" disabled> |
| <i class="fas fa-angle-left"></i> |
| </button> |
| <div class="flex space-x-1" id="page-numbers"> |
| |
| </div> |
| <button class="pagination-btn bg-white border border-gray-300 rounded-md px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" id="next-page" disabled> |
| <i class="fas fa-angle-right"></i> |
| </button> |
| <button class="pagination-btn bg-white border border-gray-300 rounded-md px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" id="last-page" disabled> |
| <i class="fas fa-angle-double-right"></i> |
| </button> |
| </div> |
| <div class="flex items-center mt-2 sm:mt-0"> |
| <span class="text-sm text-gray-500 mr-2">Rows per page:</span> |
| <select id="rows-per-page" class="border border-gray-300 rounded-md px-2 py-1 text-sm"> |
| <option value="20">20</option> |
| <option value="50">50</option> |
| <option value="100">100</option> |
| <option value="200">200</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="analysis-results" class="hidden bg-white rounded-lg shadow-md p-6 mb-8"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-6">Analysis Results</h2> |
| |
| |
| <div class="mb-8"> |
| <h3 class="font-medium text-gray-700 mb-3">Summary Statistics</h3> |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4" id="stats-summary"> |
| |
| </div> |
| </div> |
| |
| |
| <div class="mb-6"> |
| <h3 class="font-medium text-gray-700 mb-3">Visualization</h3> |
| <div class="flex flex-wrap gap-4 mb-4"> |
| <select id="chart-type" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
| <option value="bar">Bar Chart</option> |
| <option value="line">Line Chart</option> |
| <option value="pie">Pie Chart</option> |
| <option value="scatter">Scatter Plot</option> |
| <option value="histogram">Histogram</option> |
| </select> |
| <button id="update-chart" class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md"> |
| Update Chart |
| </button> |
| <button id="export-chart" class="border border-indigo-600 text-indigo-600 hover:bg-indigo-50 font-medium py-2 px-4 rounded-md"> |
| <i class="fas fa-download mr-2"></i> Export Image |
| </button> |
| </div> |
| <div class="chart-container bg-gray-50 rounded-lg p-4 border border-gray-200"> |
| <canvas id="analysis-chart"></canvas> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let csvData = []; |
| let fullCsvData = []; |
| let headers = []; |
| let selectedColumns = []; |
| let columnTypes = {}; |
| let chart = null; |
| let fileReader = null; |
| let uploadAbortController = null; |
| const CHUNK_SIZE = 5 * 1024 * 1024; |
| let isFullDataLoaded = false; |
| |
| |
| let currentPage = 1; |
| let rowsPerPage = 20; |
| let totalPages = 1; |
| let visiblePages = 5; |
| |
| |
| const dropzone = document.getElementById('dropzone'); |
| const fileInput = document.getElementById('file-input'); |
| const uploadContainer = document.getElementById('upload-container'); |
| const uploadProgress = document.getElementById('upload-progress'); |
| const progressBar = document.getElementById('progress-bar'); |
| const progressPercentage = document.getElementById('progress-percentage'); |
| const uploadedSize = document.getElementById('uploaded-size'); |
| const totalSize = document.getElementById('total-size'); |
| const filenameDisplay = document.getElementById('filename-display'); |
| const cancelUpload = document.getElementById('cancel-upload'); |
| const dataPreview = document.getElementById('data-preview'); |
| const columnList = document.getElementById('column-list'); |
| const tableHeader = document.getElementById('table-header'); |
| const tableBody = document.getElementById('table-body'); |
| const analyzeBtn = document.getElementById('analyze-btn'); |
| const analysisResults = document.getElementById('analysis-results'); |
| const statsSummary = document.getElementById('stats-summary'); |
| const chartCanvas = document.getElementById('analysis-chart'); |
| const chartTypeSelect = document.getElementById('chart-type'); |
| const updateChartBtn = document.getElementById('update-chart'); |
| const exportChartBtn = document.getElementById('export-chart'); |
| const resetAnalysisBtn = document.getElementById('reset-analysis'); |
| const loadFullDataBtn = document.getElementById('load-full-data'); |
| const rowCountDisplay = document.getElementById('row-count-display'); |
| const firstPageBtn = document.getElementById('first-page'); |
| const prevPageBtn = document.getElementById('prev-page'); |
| const nextPageBtn = document.getElementById('next-page'); |
| const lastPageBtn = document.getElementById('last-page'); |
| const pageNumbersContainer = document.getElementById('page-numbers'); |
| const rowsPerPageSelect = document.getElementById('rows-per-page'); |
| |
| |
| dropzone.addEventListener('dragover', (e) => { |
| e.preventDefault(); |
| dropzone.classList.add('active'); |
| }); |
| |
| dropzone.addEventListener('dragleave', () => { |
| dropzone.classList.remove('active'); |
| }); |
| |
| dropzone.addEventListener('drop', (e) => { |
| e.preventDefault(); |
| dropzone.classList.remove('active'); |
| if (e.dataTransfer.files.length) { |
| fileInput.files = e.dataTransfer.files; |
| handleFileUpload(e.dataTransfer.files[0]); |
| } |
| }); |
| |
| fileInput.addEventListener('change', () => { |
| if (fileInput.files.length) { |
| handleFileUpload(fileInput.files[0]); |
| } |
| }); |
| |
| cancelUpload.addEventListener('click', () => { |
| if (uploadAbortController) { |
| uploadAbortController.abort(); |
| } |
| resetUploadUI(); |
| }); |
| |
| analyzeBtn.addEventListener('click', analyzeSelectedColumns); |
| updateChartBtn.addEventListener('click', updateChart); |
| exportChartBtn.addEventListener('click', exportChart); |
| resetAnalysisBtn.addEventListener('click', resetAnalysis); |
| loadFullDataBtn.addEventListener('click', loadFullDataset); |
| |
| |
| firstPageBtn.addEventListener('click', () => goToPage(1)); |
| prevPageBtn.addEventListener('click', () => goToPage(currentPage - 1)); |
| nextPageBtn.addEventListener('click', () => goToPage(currentPage + 1)); |
| lastPageBtn.addEventListener('click', () => goToPage(totalPages)); |
| rowsPerPageSelect.addEventListener('change', (e) => { |
| rowsPerPage = parseInt(e.target.value); |
| currentPage = 1; |
| updatePagination(); |
| renderTablePage(); |
| }); |
| |
| |
| function handleFileUpload(file) { |
| if (!file.name.endsWith('.csv')) { |
| showError('Please upload a CSV file'); |
| return; |
| } |
| |
| |
| isFullDataLoaded = false; |
| csvData = []; |
| fullCsvData = []; |
| headers = []; |
| selectedColumns = []; |
| currentPage = 1; |
| rowsPerPage = 20; |
| |
| |
| uploadContainer.classList.add('hidden'); |
| uploadProgress.classList.remove('hidden'); |
| filenameDisplay.textContent = file.name; |
| totalSize.textContent = formatFileSize(file.size); |
| |
| |
| if (file.size > 50 * 1024 * 1024) { |
| uploadLargeFile(file); |
| } else { |
| uploadSmallFile(file); |
| } |
| } |
| |
| function uploadSmallFile(file) { |
| uploadAbortController = new AbortController(); |
| |
| |
| let uploaded = 0; |
| const total = file.size; |
| const interval = setInterval(() => { |
| uploaded += Math.min(CHUNK_SIZE / 10, total - uploaded); |
| updateProgress(uploaded, total); |
| |
| if (uploaded >= total) { |
| clearInterval(interval); |
| parseCSVFile(file, true); |
| } |
| }, 100); |
| } |
| |
| function uploadLargeFile(file) { |
| uploadAbortController = new AbortController(); |
| |
| |
| |
| let uploaded = 0; |
| const total = file.size; |
| const chunks = Math.ceil(total / CHUNK_SIZE); |
| |
| const uploadNextChunk = (chunkIndex) => { |
| if (uploadAbortController.signal.aborted) return; |
| |
| const start = chunkIndex * CHUNK_SIZE; |
| const end = Math.min(start + CHUNK_SIZE, total); |
| const chunk = file.slice(start, end); |
| |
| |
| setTimeout(() => { |
| uploaded = end; |
| updateProgress(uploaded, total); |
| |
| if (chunkIndex < chunks - 1) { |
| uploadNextChunk(chunkIndex + 1); |
| } else { |
| parseCSVFile(file, true); |
| } |
| }, 300); |
| }; |
| |
| uploadNextChunk(0); |
| } |
| |
| function convertValue(value, type) { |
| if (!value) return value; |
| |
| switch(type) { |
| case 'number': |
| return parseFloat(value) || 0; |
| case 'date': |
| return new Date(value); |
| case 'boolean': |
| return value.toLowerCase() === 'true' || value === '1'; |
| default: |
| return value.toString(); |
| } |
| } |
| |
| function parseCSVFile(file, isPreview = false) { |
| fileReader = new FileReader(); |
| |
| fileReader.onload = (e) => { |
| try { |
| const results = Papa.parse(e.target.result, { |
| header: true, |
| skipEmptyLines: true, |
| preview: isPreview ? 20 : null |
| }); |
| |
| if (results.errors.length > 0) { |
| showError('Error parsing CSV: ' + results.errors[0].message); |
| resetUploadUI(); |
| return; |
| } |
| |
| |
| headers.forEach(header => { |
| columnTypes[header] = 'string'; |
| }); |
| |
| if (isPreview) { |
| csvData = results.data; |
| fullCsvData = []; |
| } else { |
| fullCsvData = results.data; |
| csvData = fullCsvData.slice(0, 20); |
| isFullDataLoaded = true; |
| loadFullDataBtn.classList.add('hidden'); |
| } |
| |
| |
| setTimeout(() => { |
| document.querySelectorAll('.column-type-selector').forEach(select => { |
| select.addEventListener('change', (e) => { |
| const column = e.target.dataset.column; |
| columnTypes[column] = e.target.value; |
| renderTablePage(); |
| }); |
| }); |
| }, 0); |
| |
| headers = results.meta.fields; |
| |
| if (csvData.length === 0) { |
| showError('CSV file is empty or could not be parsed'); |
| resetUploadUI(); |
| return; |
| } |
| |
| displayDataPreview(); |
| } catch (error) { |
| showError('Error parsing CSV: ' + error.message); |
| resetUploadUI(); |
| } |
| }; |
| |
| fileReader.onerror = () => { |
| showError('Error reading file'); |
| resetUploadUI(); |
| }; |
| |
| fileReader.readAsText(file); |
| } |
| |
| function loadFullDataset() { |
| if (fileInput.files.length === 0) return; |
| |
| |
| loadFullDataBtn.disabled = true; |
| loadFullDataBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Loading...'; |
| |
| |
| parseCSVFile(fileInput.files[0], false); |
| } |
| |
| function displayDataPreview() { |
| |
| uploadContainer.classList.remove('hidden'); |
| uploadProgress.classList.add('hidden'); |
| dataPreview.classList.remove('hidden'); |
| |
| |
| const totalRows = isFullDataLoaded ? fullCsvData.length : csvData.length; |
| totalPages = Math.ceil(totalRows / rowsPerPage); |
| |
| |
| updateRowCountDisplay(); |
| |
| |
| if (isFullDataLoaded) { |
| loadFullDataBtn.classList.add('hidden'); |
| } else { |
| loadFullDataBtn.classList.remove('hidden'); |
| loadFullDataBtn.disabled = false; |
| loadFullDataBtn.innerHTML = '<i class="fas fa-database mr-2"></i> Load Full Dataset'; |
| } |
| |
| |
| columnList.innerHTML = ''; |
| headers.forEach(header => { |
| const columnItem = document.createElement('div'); |
| columnItem.className = 'column-selector p-2 rounded-md cursor-pointer flex items-center'; |
| columnItem.innerHTML = ` |
| <input type="checkbox" id="col-${header}" class="hidden column-checkbox"> |
| <label for="col-${header}" class="flex items-center w-full cursor-pointer"> |
| <span class="w-5 h-5 inline-block border border-gray-300 rounded mr-2 flex-shrink-0"></span> |
| <span class="truncate flex-grow">${header}</span> |
| <select class="column-type-selector text-xs border rounded px-1 py-0.5 ml-2" data-column="${header}"> |
| <option value="string">Text</option> |
| <option value="number">Number</option> |
| <option value="date">Date</option> |
| <option value="boolean">Boolean</option> |
| </select> |
| </label> |
| `; |
| |
| |
| const checkboxLabel = columnItem.querySelector('label'); |
| checkboxLabel.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| toggleColumnSelection(header, columnItem); |
| }); |
| |
| columnList.appendChild(columnItem); |
| }); |
| |
| |
| tableHeader.innerHTML = '<th class="row-number">#</th>'; |
| headers.forEach(header => { |
| const th = document.createElement('th'); |
| th.className = 'px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'; |
| th.textContent = header; |
| th.dataset.column = header; |
| tableHeader.appendChild(th); |
| }); |
| |
| |
| updatePagination(); |
| |
| |
| renderTablePage(); |
| } |
| |
| function renderTablePage() { |
| |
| tableBody.innerHTML = ''; |
| |
| |
| const dataToShow = isFullDataLoaded ? fullCsvData : csvData; |
| const totalRows = dataToShow.length; |
| |
| |
| const startIndex = (currentPage - 1) * rowsPerPage; |
| const endIndex = Math.min(startIndex + rowsPerPage, totalRows); |
| |
| |
| for (let i = startIndex; i < endIndex; i++) { |
| const row = dataToShow[i]; |
| const tr = document.createElement('tr'); |
| tr.className = i % 2 === 0 ? 'bg-white' : 'bg-gray-50'; |
| |
| |
| const rowNumberCell = document.createElement('td'); |
| rowNumberCell.className = 'row-number px-4 py-2 whitespace-nowrap text-sm'; |
| rowNumberCell.textContent = i + 1; |
| tr.appendChild(rowNumberCell); |
| |
| |
| headers.forEach(header => { |
| const td = document.createElement('td'); |
| td.className = 'px-4 py-2 whitespace-nowrap text-sm text-gray-500'; |
| const value = row[header]; |
| const type = columnTypes[header]; |
| const convertedValue = convertValue(value, type); |
| |
| if (type === 'date' && convertedValue instanceof Date) { |
| td.textContent = convertedValue.toLocaleDateString(); |
| } else if (type === 'boolean') { |
| td.textContent = convertedValue ? '✓' : '✗'; |
| td.style.textAlign = 'center'; |
| } else { |
| td.textContent = convertedValue || ''; |
| } |
| tr.appendChild(td); |
| }); |
| |
| tableBody.appendChild(tr); |
| } |
| |
| |
| updateRowCountDisplay(); |
| } |
| |
| function updateRowCountDisplay() { |
| const totalRows = isFullDataLoaded ? fullCsvData.length : csvData.length; |
| const startRow = (currentPage - 1) * rowsPerPage + 1; |
| const endRow = Math.min(currentPage * rowsPerPage, totalRows); |
| |
| rowCountDisplay.textContent = isFullDataLoaded ? |
| `Showing rows ${startRow}-${endRow} of ${totalRows}` : |
| `Showing first ${csvData.length} rows of data (${startRow}-${endRow}). Click "Load Full Dataset" to analyze all data.`; |
| } |
| |
| function updatePagination() { |
| const totalRows = isFullDataLoaded ? fullCsvData.length : csvData.length; |
| totalPages = Math.ceil(totalRows / rowsPerPage); |
| |
| |
| firstPageBtn.disabled = currentPage === 1; |
| prevPageBtn.disabled = currentPage === 1; |
| nextPageBtn.disabled = currentPage === totalPages; |
| lastPageBtn.disabled = currentPage === totalPages; |
| |
| |
| pageNumbersContainer.innerHTML = ''; |
| |
| |
| let startPage, endPage; |
| if (totalPages <= visiblePages) { |
| |
| startPage = 1; |
| endPage = totalPages; |
| } else { |
| |
| const maxPagesBeforeCurrent = Math.floor(visiblePages / 2); |
| const maxPagesAfterCurrent = Math.ceil(visiblePages / 2) - 1; |
| |
| if (currentPage <= maxPagesBeforeCurrent) { |
| |
| startPage = 1; |
| endPage = visiblePages; |
| } else if (currentPage + maxPagesAfterCurrent >= totalPages) { |
| |
| startPage = totalPages - visiblePages + 1; |
| endPage = totalPages; |
| } else { |
| |
| startPage = currentPage - maxPagesBeforeCurrent; |
| endPage = currentPage + maxPagesAfterCurrent; |
| } |
| } |
| |
| |
| for (let i = startPage; i <= endPage; i++) { |
| const pageBtn = document.createElement('button'); |
| pageBtn.className = `pagination-btn bg-white border border-gray-300 rounded-md px-3 py-1 text-sm font-medium ${i === currentPage ? 'active bg-indigo-600 text-white' : 'text-gray-700 hover:bg-gray-50'}`; |
| pageBtn.textContent = i; |
| pageBtn.addEventListener('click', () => goToPage(i)); |
| pageNumbersContainer.appendChild(pageBtn); |
| } |
| } |
| |
| function goToPage(page) { |
| if (page < 1 || page > totalPages) return; |
| |
| currentPage = page; |
| renderTablePage(); |
| updatePagination(); |
| |
| |
| document.querySelector('.data-table-container').scrollTop = 0; |
| } |
| |
| function toggleColumnSelection(columnName, columnElement) { |
| const index = selectedColumns.indexOf(columnName); |
| |
| if (index === -1) { |
| selectedColumns.push(columnName); |
| columnElement.classList.add('column-selected'); |
| columnElement.querySelector('span:first-child').innerHTML = '<i class="fas fa-check text-indigo-600"></i>'; |
| } else { |
| selectedColumns.splice(index, 1); |
| columnElement.classList.remove('column-selected'); |
| columnElement.querySelector('span:first-child').innerHTML = ''; |
| } |
| |
| |
| document.querySelectorAll('th[data-column]').forEach(th => { |
| if (selectedColumns.includes(th.dataset.column)) { |
| th.classList.add('bg-indigo-50', 'text-indigo-700'); |
| } else { |
| th.classList.remove('bg-indigo-50', 'text-indigo-700'); |
| } |
| }); |
| |
| |
| analyzeBtn.disabled = selectedColumns.length === 0; |
| } |
| |
| function analyzeSelectedColumns() { |
| if (selectedColumns.length === 0) return; |
| |
| |
| analysisResults.classList.remove('hidden'); |
| |
| |
| generateStatistics(); |
| |
| |
| createChart(); |
| } |
| |
| function generateStatistics() { |
| statsSummary.innerHTML = ''; |
| |
| |
| const dataToAnalyze = isFullDataLoaded ? fullCsvData : csvData; |
| |
| selectedColumns.forEach(column => { |
| const values = dataToAnalyze.map(row => parseFloat(row[column])).filter(val => !isNaN(val)); |
| |
| if (values.length === 0) { |
| |
| const uniqueCount = new Set(dataToAnalyze.map(row => row[column])).size; |
| |
| const card = document.createElement('div'); |
| card.className = 'bg-gray-50 rounded-lg p-4 border border-gray-200'; |
| card.innerHTML = ` |
| <h4 class="font-medium text-gray-700 truncate">${column}</h4> |
| <div class="mt-2"> |
| <div class="flex justify-between text-sm text-gray-600"> |
| <span>Type:</span> |
| <span>Categorical</span> |
| </div> |
| <div class="flex justify-between text-sm text-gray-600 mt-1"> |
| <span>Unique Values:</span> |
| <span>${uniqueCount}</span> |
| </div> |
| <div class="flex justify-between text-sm text-gray-600 mt-1"> |
| <span>Missing Values:</span> |
| <span>${dataToAnalyze.filter(row => !row[column]).length}</span> |
| </div> |
| <div class="flex justify-between text-sm text-gray-600 mt-1"> |
| <span>Total Rows:</span> |
| <span>${dataToAnalyze.length}</span> |
| </div> |
| </div> |
| `; |
| statsSummary.appendChild(card); |
| } else { |
| |
| const sum = values.reduce((a, b) => a + b, 0); |
| const mean = sum / values.length; |
| const sorted = [...values].sort((a, b) => a - b); |
| const median = sorted[Math.floor(sorted.length / 2)]; |
| const min = sorted[0]; |
| const max = sorted[sorted.length - 1]; |
| |
| |
| const squaredDiffs = values.map(val => Math.pow(val - mean, 2)); |
| const variance = squaredDiffs.reduce((a, b) => a + b, 0) / values.length; |
| const stdDev = Math.sqrt(variance); |
| |
| const card = document.createElement('div'); |
| card.className = 'bg-gray-50 rounded-lg p-4 border border-gray-200'; |
| card.innerHTML = ` |
| <h4 class="font-medium text-gray-700 truncate">${column}</h4> |
| <div class="mt-2"> |
| <div class="flex justify-between text-sm text-gray-600"> |
| <span>Mean:</span> |
| <span>${mean.toFixed(2)}</span> |
| </div> |
| <div class="flex justify-between text-sm text-gray-600 mt-1"> |
| <span>Median:</span> |
| <span>${median.toFixed(2)}</span> |
| </div> |
| <div class="flex justify-between text-sm text-gray-600 mt-1"> |
| <span>Min/Max:</span> |
| <span>${min.toFixed(2)} / ${max.toFixed(2)}</span> |
| </div> |
| <div class="flex justify-between text-sm text-gray-600 mt-1"> |
| <span>Std Dev:</span> |
| <span>${stdDev.toFixed(2)}</span> |
| </div> |
| <div class="flex justify-between text-sm text-gray-600 mt-1"> |
| <span>Missing Values:</span> |
| <span>${dataToAnalyze.filter(row => !row[column]).length}</span> |
| </div> |
| <div class="flex justify-between text-sm text-gray-600 mt-1"> |
| <span>Total Rows:</span> |
| <span>${dataToAnalyze.length}</span> |
| </div> |
| </div> |
| `; |
| statsSummary.appendChild(card); |
| } |
| }); |
| } |
| |
| function createChart() { |
| const ctx = chartCanvas.getContext('2d'); |
| const chartType = chartTypeSelect.value; |
| |
| if (chart) { |
| chart.destroy(); |
| } |
| |
| |
| const dataToAnalyze = isFullDataLoaded ? fullCsvData : csvData; |
| |
| |
| const datasets = []; |
| const labels = Array.from({ length: dataToAnalyze.length }, (_, i) => `Row ${i + 1}`); |
| |
| selectedColumns.forEach((column, i) => { |
| const values = dataToAnalyze.map(row => parseFloat(row[column])); |
| const isNumeric = values.every(v => !isNaN(v)); |
| |
| if (isNumeric) { |
| datasets.push({ |
| label: column, |
| data: values, |
| backgroundColor: getColor(i, 0.7), |
| borderColor: getColor(i, 1), |
| borderWidth: 1 |
| }); |
| } else { |
| |
| const valueCounts = {}; |
| dataToAnalyze.forEach(row => { |
| const val = row[column] || 'Missing'; |
| valueCounts[val] = (valueCounts[val] || 0) + 1; |
| }); |
| |
| |
| if (chartType === 'pie' || chartType === 'doughnut') { |
| datasets.push({ |
| label: column, |
| data: Object.values(valueCounts), |
| backgroundColor: Object.keys(valueCounts).map((_, i) => getColor(i, 0.7)), |
| borderColor: Object.keys(valueCounts).map((_, i) => getColor(i, 1)), |
| borderWidth: 1 |
| }); |
| } else { |
| |
| datasets.push({ |
| label: column, |
| data: Object.values(valueCounts), |
| backgroundColor: getColor(i, 0.7), |
| borderColor: getColor(i, 1), |
| borderWidth: 1 |
| }); |
| } |
| } |
| }); |
| |
| |
| if (chartType === 'pie' || chartType === 'doughnut') { |
| |
| const firstColumn = selectedColumns[0]; |
| const valueCounts = {}; |
| dataToAnalyze.forEach(row => { |
| const val = row[firstColumn] || 'Missing'; |
| valueCounts[val] = (valueCounts[val] || 0) + 1; |
| }); |
| |
| chart = new Chart(ctx, { |
| type: chartType, |
| data: { |
| labels: Object.keys(valueCounts), |
| datasets: [{ |
| data: Object.values(valueCounts), |
| backgroundColor: Object.keys(valueCounts).map((_, i) => getColor(i, 0.7)), |
| borderColor: '#fff', |
| borderWidth: 1 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| title: { |
| display: true, |
| text: `Distribution of ${firstColumn}` |
| } |
| } |
| } |
| }); |
| } else if (chartType === 'scatter') { |
| |
| const numericColumns = selectedColumns.filter(col => { |
| const values = dataToAnalyze.map(row => parseFloat(row[col])); |
| return values.every(v => !isNaN(v)); |
| }); |
| |
| if (numericColumns.length >= 2) { |
| const xValues = dataToAnalyze.map(row => parseFloat(row[numericColumns[0]])); |
| const yValues = dataToAnalyze.map(row => parseFloat(row[numericColumns[1]])); |
| |
| chart = new Chart(ctx, { |
| type: 'scatter', |
| data: { |
| datasets: [{ |
| label: `${numericColumns[0]} vs ${numericColumns[1]}`, |
| data: xValues.map((x, i) => ({x, y: yValues[i]})), |
| backgroundColor: getColor(0, 0.7), |
| borderColor: getColor(0, 1), |
| borderWidth: 1, |
| pointRadius: 5 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| scales: { |
| x: { |
| title: { |
| display: true, |
| text: numericColumns[0] |
| } |
| }, |
| y: { |
| title: { |
| display: true, |
| text: numericColumns[1] |
| } |
| } |
| }, |
| plugins: { |
| title: { |
| display: true, |
| text: `Scatter Plot: ${numericColumns[0]} vs ${numericColumns[1]}` |
| } |
| } |
| } |
| }); |
| } else { |
| showError('Scatter plot requires at least 2 numeric columns'); |
| } |
| } else if (chartType === 'histogram') { |
| |
| const numericColumns = selectedColumns.filter(col => { |
| const values = dataToAnalyze.map(row => parseFloat(row[col])); |
| return values.every(v => !isNaN(v)); |
| }); |
| |
| if (numericColumns.length > 0) { |
| const column = numericColumns[0]; |
| const values = dataToAnalyze.map(row => parseFloat(row[column])); |
| |
| |
| const binCount = Math.ceil(Math.log2(values.length) + 1); |
| |
| |
| const min = Math.min(...values); |
| const max = Math.max(...values); |
| const range = max - min; |
| const binSize = range / binCount; |
| |
| |
| const bins = Array(binCount).fill(0); |
| values.forEach(val => { |
| const binIndex = Math.min(Math.floor((val - min) / binSize), binCount - 1); |
| bins[binIndex]++; |
| }); |
| |
| |
| const binLabels = Array(binCount).fill().map((_, i) => { |
| const start = min + i * binSize; |
| const end = min + (i + 1) * binSize; |
| return `${start.toFixed(2)} - ${end.toFixed(2)}`; |
| }); |
| |
| chart = new Chart(ctx, { |
| type: 'bar', |
| data: { |
| labels: binLabels, |
| datasets: [{ |
| label: `Frequency of ${column}`, |
| data: bins, |
| backgroundColor: getColor(0, 0.7), |
| borderColor: getColor(0, 1), |
| borderWidth: 1 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| scales: { |
| y: { |
| beginAtZero: true, |
| title: { |
| display: true, |
| text: 'Frequency' |
| } |
| }, |
| x: { |
| title: { |
| display: true, |
| text: column |
| } |
| } |
| }, |
| plugins: { |
| title: { |
| display: true, |
| text: `Histogram of ${column}` |
| } |
| } |
| } |
| }); |
| } else { |
| showError('Histogram requires at least 1 numeric column'); |
| } |
| } else { |
| |
| chart = new Chart(ctx, { |
| type: chartType, |
| data: { |
| labels: labels.slice(0, 100), |
| datasets: datasets.map((dataset, i) => ({ |
| ...dataset, |
| data: dataset.data.slice(0, 100) |
| })) |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| scales: { |
| y: { |
| beginAtZero: true |
| } |
| }, |
| plugins: { |
| title: { |
| display: true, |
| text: `Analysis of ${selectedColumns.join(', ')}` |
| }, |
| tooltip: { |
| mode: 'index', |
| intersect: false |
| } |
| } |
| } |
| }); |
| } |
| } |
| |
| function updateChart() { |
| createChart(); |
| } |
| |
| function exportChart() { |
| if (!chart) return; |
| |
| const link = document.createElement('a'); |
| link.download = 'chart.png'; |
| link.href = chartCanvas.toDataURL('image/png'); |
| link.click(); |
| } |
| |
| function resetAnalysis() { |
| selectedColumns = []; |
| analysisResults.classList.add('hidden'); |
| |
| |
| document.querySelectorAll('.column-selector').forEach(el => { |
| el.classList.remove('column-selected'); |
| el.querySelector('span:first-child').innerHTML = ''; |
| }); |
| |
| |
| document.querySelectorAll('th[data-column]').forEach(th => { |
| th.classList.remove('bg-indigo-50', 'text-indigo-700'); |
| }); |
| |
| |
| analyzeBtn.disabled = true; |
| } |
| |
| function resetUploadUI() { |
| uploadContainer.classList.remove('hidden'); |
| uploadProgress.classList.add('hidden'); |
| progressBar.style.width = '0%'; |
| progressPercentage.textContent = '0%'; |
| uploadedSize.textContent = '0 MB'; |
| fileInput.value = ''; |
| |
| if (uploadAbortController) { |
| uploadAbortController.abort(); |
| uploadAbortController = null; |
| } |
| } |
| |
| function updateProgress(uploaded, total) { |
| const percentage = Math.round((uploaded / total) * 100); |
| progressBar.style.width = `${percentage}%`; |
| progressPercentage.textContent = `${percentage}%`; |
| uploadedSize.textContent = formatFileSize(uploaded); |
| } |
| |
| function formatFileSize(bytes) { |
| if (bytes < 1024) return bytes + ' B'; |
| else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; |
| else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB'; |
| else return (bytes / 1073741824).toFixed(1) + ' GB'; |
| } |
| |
| function getColor(index, opacity = 1) { |
| const colors = [ |
| `rgba(79, 70, 229, ${opacity})`, |
| `rgba(220, 38, 38, ${opacity})`, |
| `rgba(5, 150, 105, ${opacity})`, |
| `rgba(234, 88, 12, ${opacity})`, |
| `rgba(124, 58, 237, ${opacity})`, |
| `rgba(8, 145, 178, ${opacity})`, |
| `rgba(202, 138, 4, ${opacity})`, |
| `rgba(22, 163, 74, ${opacity})`, |
| `rgba(217, 70, 239, ${opacity})`, |
| `rgba(239, 68, 68, ${opacity})` |
| ]; |
| return colors[index % colors.length]; |
| } |
| |
| function showError(message) { |
| alert(message); |
| } |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=anzuo/deepsite" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |