| | <!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> |