triflix's picture
Create templates/index.html
0fc3f1d verified
<!DOCTYPE html>
<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 !important; }
</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>