Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Data Analyzer</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script> | |
| <style> | |
| body { font-family: sans-serif; line-height: 1.6; margin: 20px; background-color: #f4f4f4; } | |
| .container { max-width: 1200px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); } | |
| h1, h2 { color: #333; } | |
| form { margin-bottom: 20px; } | |
| #status { padding: 10px; margin-bottom: 20px; border-radius: 5px; font-weight: bold; } | |
| .status-info { background-color: #e7f3fe; border-left: 6px solid #2196F3; } | |
| .status-success { background-color: #dff0d8; border-left: 6px solid #3c763d; } | |
| .status-error { background-color: #f2dede; border-left: 6px solid #a94442; } | |
| #chartsContainer { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; } | |
| .chart-box { border: 1px solid #ddd; padding: 10px; border-radius: 5px; } | |
| #dataContainer { margin-top: 30px; } | |
| #dataContainer table { width: 100%; border-collapse: collapse; } | |
| #dataContainer th, #dataContainer td { border: 1px solid #ddd; padding: 8px; text-align: left; } | |
| #dataContainer th { background-color: #f2f2f2; cursor: pointer; position: relative; } | |
| #dataContainer th:hover .tooltip { visibility: visible; opacity: 1; } | |
| .tooltip { visibility: hidden; width: 200px; background-color: #555; color: #fff; text-align: left; border-radius: 6px; padding: 5px; position: absolute; z-index: 1; bottom: 125%; left: 50%; margin-left: -100px; opacity: 0; transition: opacity 0.3s; font-size: 12px; font-weight: normal; } | |
| .tooltip::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #555 transparent transparent transparent; } | |
| .highlight { background-color: #fff3cd ; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Upload and Analyze Data File</h1> | |
| <form id="uploadForm"> | |
| <input type="file" id="fileInput" name="file" required accept=".csv,.xlsx,.xls"> | |
| <label><input type="checkbox" id="forceCheckbox" name="force"> Force Re-analysis</label> | |
| <button type="submit">Analyze</button> | |
| </form> | |
| <div id="status"></div> | |
| <h2>Analysis Charts</h2> | |
| <div id="chartsContainer"></div> | |
| <div id="dataContainer"></div> | |
| </div> | |
| <script> | |
| // chartAdapter.js logic (included directly for simplicity) | |
| function generateChartConfigs(apiResponse) { | |
| if (!apiResponse || !apiResponse.chart_data) { return []; } | |
| const configs = []; | |
| const chartData = apiResponse.chart_data; | |
| const chartTypes = ['bar', 'pie', 'scatter', 'timeseries']; | |
| chartTypes.forEach(type => { | |
| if (chartData[type]) { | |
| chartData[type].forEach((chart, idx) => { | |
| let config; | |
| const labels = chart.data.map(d => d[chart.columns[0]]); | |
| const values = chart.data.map(d => d[chart.columns[1]]); | |
| switch(type) { | |
| case 'bar': | |
| config = { type: "bar", data: { labels: labels, datasets: [{ label: chart.title || `Bar Chart ${idx + 1}`, data: values, backgroundColor: "rgba(54, 162, 235, 0.6)" }] }, options: { responsive: true, plugins: { title: { display: true, text: chart.title } } } }; | |
| break; | |
| case 'pie': | |
| config = { type: "pie", data: { labels: labels, datasets: [{ label: chart.title || `Pie Chart ${idx + 1}`, data: values, backgroundColor: ["rgba(255, 99, 132, 0.6)","rgba(54, 162, 235, 0.6)","rgba(255, 206, 86, 0.6)","rgba(75, 192, 192, 0.6)","rgba(153, 102, 255, 0.6)"] }] }, options: { responsive: true, plugins: { title: { display: true, text: chart.title } } } }; | |
| break; | |
| case 'scatter': | |
| const points = chart.data.map(d => ({ x: d[chart.columns[0]], y: d[chart.columns[1]] })); | |
| config = { type: "scatter", data: { datasets: [{ label: chart.title || `Scatter Plot ${idx + 1}`, data: points, backgroundColor: "rgba(255, 99, 132, 0.6)" }] }, options: { responsive: true, plugins: { title: { display: true, text: chart.title } }, scales: { x: { type: "linear", position: "bottom" } } } }; | |
| break; | |
| case 'timeseries': | |
| config = { type: "line", data: { labels: labels, datasets: [{ label: chart.title || `Timeseries ${idx + 1}`, data: values, borderColor: "rgba(75, 192, 192, 1)", backgroundColor: "rgba(75, 192, 192, 0.2)", fill: true, tension: 0.3 }] }, options: { responsive: true, plugins: { title: { display: true, text: chart.title } }, scales: { x: { type: "time", time: { unit: "day" } } } } }; | |
| break; | |
| } | |
| if (config) configs.push(config); | |
| }); | |
| } | |
| }); | |
| return configs; | |
| } | |
| // Main application logic | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const form = document.getElementById('uploadForm'); | |
| const statusDiv = document.getElementById('status'); | |
| const chartsContainer = document.getElementById('chartsContainer'); | |
| const dataContainer = document.getElementById('dataContainer'); | |
| let activeCharts = []; | |
| form.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const fileInput = document.getElementById('fileInput'); | |
| const forceCheckbox = document.getElementById('forceCheckbox'); | |
| if (!fileInput.files.length) { | |
| updateStatus('Please select a file.', 'error'); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file', fileInput.files[0]); | |
| formData.append('force', forceCheckbox.checked); | |
| updateStatus('Uploading and analyzing... This may take a moment.', 'info'); | |
| clearResults(); | |
| try { | |
| const response = await fetch('/upload-and-analyze/', { | |
| method: 'POST', | |
| body: formData, | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.detail || `HTTP error! Status: ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| let statusMsg = `Analysis complete. Status: ${result.status}. Filename: ${result.source_filename}. Snapshot ID: ${result.snapshot_id}`; | |
| updateStatus(statusMsg, 'success'); | |
| // Render charts | |
| const chartConfigs = generateChartConfigs(result.api_response); | |
| renderCharts(chartConfigs); | |
| // Add button to show preprocessed data | |
| renderDataButton(result.snapshot_id); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| updateStatus(`An error occurred: ${error.message}`, 'error'); | |
| } | |
| }); | |
| function updateStatus(message, type) { | |
| statusDiv.textContent = message; | |
| statusDiv.className = `status-${type}`; | |
| } | |
| function clearResults() { | |
| // Destroy old charts | |
| activeCharts.forEach(chart => chart.destroy()); | |
| activeCharts = []; | |
| chartsContainer.innerHTML = ''; | |
| dataContainer.innerHTML = ''; | |
| } | |
| function renderCharts(configs) { | |
| configs.forEach((config, i) => { | |
| const chartBox = document.createElement('div'); | |
| chartBox.className = 'chart-box'; | |
| const canvas = document.createElement('canvas'); | |
| chartBox.appendChild(canvas); | |
| chartsContainer.appendChild(chartBox); | |
| activeCharts.push(new Chart(canvas, config)); | |
| }); | |
| } | |
| function renderDataButton(snapshotId) { | |
| dataContainer.innerHTML = ''; // Clear previous button/table | |
| const button = document.createElement('button'); | |
| button.textContent = 'Show Preprocessed Data'; | |
| button.onclick = () => { | |
| button.disabled = true; | |
| button.textContent = 'Loading Data...'; | |
| loadAndRenderData(snapshotId); | |
| }; | |
| dataContainer.appendChild(button); | |
| } | |
| async function loadAndRenderData(snapshotId) { | |
| try { | |
| const [dataResponse, statsResponse] = await Promise.all([ | |
| fetch(`/snapshots/${snapshot_id}/preprocessed`), | |
| fetch(`/snapshots/${snapshot_id}/column-stats`) | |
| ]); | |
| if (!dataResponse.ok || !statsResponse.ok) { | |
| throw new Error('Failed to load snapshot data.'); | |
| } | |
| const csvText = await dataResponse.text(); | |
| const colStats = await statsResponse.json(); | |
| renderDataTable(csvText, colStats); | |
| } catch (error) { | |
| console.error('Data loading error:', error); | |
| dataContainer.innerHTML = `<p style="color: red;">Error loading data: ${error.message}</p>`; | |
| } | |
| } | |
| function renderDataTable(csvText, colStats) { | |
| dataContainer.innerHTML = ''; // Clear button | |
| const rows = csvText.trim().split('\n'); | |
| const headers = rows[0].split(','); | |
| const table = document.createElement('table'); | |
| const thead = document.createElement('thead'); | |
| const tbody = document.createElement('tbody'); | |
| // Header row with tooltips | |
| const headerRow = document.createElement('tr'); | |
| headers.forEach(header => { | |
| const th = document.createElement('th'); | |
| th.textContent = header; | |
| th.onclick = () => highlightColumn(header); | |
| const stats = colStats[header]; | |
| if (stats) { | |
| const tooltip = document.createElement('span'); | |
| tooltip.className = 'tooltip'; | |
| tooltip.innerHTML = ` | |
| <strong>Type:</strong> ${stats.dtype}<br> | |
| <strong>Unique:</strong> ${stats.n_unique}<br> | |
| <strong>Missing:</strong> ${stats.n_missing}<br> | |
| <strong>Sample:</strong> ${stats.sample_values.join(', ')} | |
| `; | |
| th.appendChild(tooltip); | |
| } | |
| headerRow.appendChild(th); | |
| }); | |
| thead.appendChild(headerRow); | |
| // Body rows (limit to 100 for performance) | |
| for (let i = 1; i < Math.min(rows.length, 101); i++) { | |
| const bodyRow = document.createElement('tr'); | |
| const values = rows[i].split(','); | |
| values.forEach(val => { | |
| const td = document.createElement('td'); | |
| td.textContent = val; | |
| bodyRow.appendChild(td); | |
| }); | |
| tbody.appendChild(bodyRow); | |
| } | |
| table.appendChild(thead); | |
| table.appendChild(tbody); | |
| const title = document.createElement('h2'); | |
| title.textContent = 'Preprocessed Data (First 100 rows)'; | |
| dataContainer.appendChild(title); | |
| dataContainer.appendChild(table); | |
| } | |
| function highlightColumn(headerText) { | |
| const table = dataContainer.querySelector('table'); | |
| if (!table) return; | |
| // Clear previous highlights | |
| table.querySelectorAll('.highlight').forEach(el => el.classList.remove('highlight')); | |
| // Find header index | |
| const headers = Array.from(table.querySelectorAll('th')); | |
| const colIndex = headers.findIndex(th => th.textContent === headerText); | |
| if (colIndex !== -1) { | |
| // Highlight header | |
| headers[colIndex].classList.add('highlight'); | |
| // Highlight all cells in that column | |
| table.querySelectorAll(`tr > td:nth-child(${colIndex + 1})`).forEach(td => { | |
| td.classList.add('highlight'); | |
| }); | |
| } | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |