|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Cancer@Home v2 - Dashboard</title>
|
|
|
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
|
|
|
<style>
|
|
|
* {
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
body {
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
color: #333;
|
|
|
min-height: 100vh;
|
|
|
}
|
|
|
|
|
|
.header {
|
|
|
background: rgba(0, 0, 0, 0.2);
|
|
|
color: white;
|
|
|
padding: 20px;
|
|
|
text-align: center;
|
|
|
backdrop-filter: blur(10px);
|
|
|
}
|
|
|
|
|
|
.header h1 {
|
|
|
font-size: 2.5em;
|
|
|
margin-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
.header p {
|
|
|
opacity: 0.9;
|
|
|
}
|
|
|
|
|
|
.container {
|
|
|
max-width: 1400px;
|
|
|
margin: 20px auto;
|
|
|
padding: 0 20px;
|
|
|
}
|
|
|
|
|
|
.tabs {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
margin-bottom: 20px;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.tab-button {
|
|
|
background: rgba(255, 255, 255, 0.9);
|
|
|
border: none;
|
|
|
padding: 15px 30px;
|
|
|
border-radius: 8px;
|
|
|
cursor: pointer;
|
|
|
font-size: 16px;
|
|
|
font-weight: 500;
|
|
|
transition: all 0.3s;
|
|
|
}
|
|
|
|
|
|
.tab-button:hover {
|
|
|
background: white;
|
|
|
transform: translateY(-2px);
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
}
|
|
|
|
|
|
.tab-button.active {
|
|
|
background: white;
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
|
}
|
|
|
|
|
|
.tab-content {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.tab-content.active {
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
.cards {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
|
gap: 20px;
|
|
|
margin-bottom: 30px;
|
|
|
}
|
|
|
|
|
|
.card {
|
|
|
background: white;
|
|
|
border-radius: 12px;
|
|
|
padding: 25px;
|
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
.card h3 {
|
|
|
color: #667eea;
|
|
|
margin-bottom: 15px;
|
|
|
font-size: 1.3em;
|
|
|
}
|
|
|
|
|
|
.stat {
|
|
|
font-size: 2.5em;
|
|
|
font-weight: bold;
|
|
|
color: #764ba2;
|
|
|
margin: 10px 0;
|
|
|
}
|
|
|
|
|
|
.graph-container {
|
|
|
background: white;
|
|
|
border-radius: 12px;
|
|
|
padding: 25px;
|
|
|
margin-bottom: 20px;
|
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
#neo4j-viz {
|
|
|
width: 100%;
|
|
|
height: 600px;
|
|
|
border: 2px solid #e0e0e0;
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
.button {
|
|
|
background: #667eea;
|
|
|
color: white;
|
|
|
border: none;
|
|
|
padding: 12px 24px;
|
|
|
border-radius: 6px;
|
|
|
cursor: pointer;
|
|
|
font-size: 16px;
|
|
|
transition: background 0.3s;
|
|
|
}
|
|
|
|
|
|
.button:hover {
|
|
|
background: #5568d3;
|
|
|
}
|
|
|
|
|
|
.task-list {
|
|
|
list-style: none;
|
|
|
}
|
|
|
|
|
|
.task-item {
|
|
|
background: #f5f5f5;
|
|
|
padding: 15px;
|
|
|
margin: 10px 0;
|
|
|
border-radius: 6px;
|
|
|
border-left: 4px solid #667eea;
|
|
|
}
|
|
|
|
|
|
.task-item.completed {
|
|
|
border-left-color: #4caf50;
|
|
|
}
|
|
|
|
|
|
.task-item.running {
|
|
|
border-left-color: #ff9800;
|
|
|
}
|
|
|
|
|
|
.status-badge {
|
|
|
display: inline-block;
|
|
|
padding: 4px 12px;
|
|
|
border-radius: 12px;
|
|
|
font-size: 12px;
|
|
|
font-weight: 600;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
.status-pending { background: #ffc107; color: #000; }
|
|
|
.status-running { background: #2196f3; color: white; }
|
|
|
.status-completed { background: #4caf50; color: white; }
|
|
|
.status-failed { background: #f44336; color: white; }
|
|
|
|
|
|
.input-group {
|
|
|
margin: 15px 0;
|
|
|
}
|
|
|
|
|
|
.input-group label {
|
|
|
display: block;
|
|
|
margin-bottom: 5px;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.input-group input, .input-group select {
|
|
|
width: 100%;
|
|
|
padding: 10px;
|
|
|
border: 1px solid #ddd;
|
|
|
border-radius: 6px;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.project-card {
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
color: white;
|
|
|
padding: 20px;
|
|
|
border-radius: 8px;
|
|
|
margin: 10px 0;
|
|
|
cursor: pointer;
|
|
|
transition: transform 0.2s;
|
|
|
}
|
|
|
|
|
|
.project-card:hover {
|
|
|
transform: translateY(-3px);
|
|
|
}
|
|
|
|
|
|
.loading {
|
|
|
text-align: center;
|
|
|
padding: 40px;
|
|
|
color: #667eea;
|
|
|
}
|
|
|
|
|
|
@keyframes spin {
|
|
|
to { transform: rotate(360deg); }
|
|
|
}
|
|
|
|
|
|
.spinner {
|
|
|
border: 4px solid #f3f3f3;
|
|
|
border-top: 4px solid #667eea;
|
|
|
border-radius: 50%;
|
|
|
width: 40px;
|
|
|
height: 40px;
|
|
|
animation: spin 1s linear infinite;
|
|
|
margin: 20px auto;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="header">
|
|
|
<h1>🧬 Cancer@Home v2</h1>
|
|
|
<p>Distributed Cancer Genomics Research Platform</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="container">
|
|
|
<div class="tabs">
|
|
|
<button class="tab-button active" onclick="showTab('dashboard')">📊 Dashboard</button>
|
|
|
<button class="tab-button" onclick="showTab('neo4j')">🔍 Neo4j Visualization</button>
|
|
|
<button class="tab-button" onclick="showTab('boinc')">⚡ BOINC Tasks</button>
|
|
|
<button class="tab-button" onclick="showTab('gdc')">📚 GDC Data</button>
|
|
|
<button class="tab-button" onclick="showTab('pipeline')">🧪 Analysis Pipeline</button>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div id="dashboard" class="tab-content active">
|
|
|
<div class="cards" id="stats-cards">
|
|
|
<div class="card">
|
|
|
<h3>Total Genes</h3>
|
|
|
<div class="stat" id="total-genes">-</div>
|
|
|
</div>
|
|
|
<div class="card">
|
|
|
<h3>Total Mutations</h3>
|
|
|
<div class="stat" id="total-mutations">-</div>
|
|
|
</div>
|
|
|
<div class="card">
|
|
|
<h3>Total Patients</h3>
|
|
|
<div class="stat" id="total-patients">-</div>
|
|
|
</div>
|
|
|
<div class="card">
|
|
|
<h3>Cancer Types</h3>
|
|
|
<div class="stat" id="total-cancer-types">-</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="graph-container">
|
|
|
<h3>Mutation Distribution by Cancer Type</h3>
|
|
|
<canvas id="mutation-chart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div id="neo4j" class="tab-content">
|
|
|
<div class="graph-container">
|
|
|
<h3>Cancer Genomics Knowledge Graph</h3>
|
|
|
<div id="neo4j-viz"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div id="boinc" class="tab-content">
|
|
|
<div class="cards">
|
|
|
<div class="card">
|
|
|
<h3>Submit New Task</h3>
|
|
|
<div class="input-group">
|
|
|
<label>Task Type</label>
|
|
|
<select id="task-type">
|
|
|
<option value="variant_calling">Variant Calling</option>
|
|
|
<option value="blast_search">BLAST Search</option>
|
|
|
<option value="alignment">Sequence Alignment</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
<div class="input-group">
|
|
|
<label>Input File</label>
|
|
|
<input type="text" id="input-file" placeholder="path/to/input.fastq">
|
|
|
</div>
|
|
|
<button class="button" onclick="submitBoincTask()">Submit Task</button>
|
|
|
</div>
|
|
|
|
|
|
<div class="card">
|
|
|
<h3>BOINC Statistics</h3>
|
|
|
<div id="boinc-stats"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card">
|
|
|
<h3>Active Tasks</h3>
|
|
|
<ul class="task-list" id="task-list"></ul>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div id="gdc" class="tab-content">
|
|
|
<div class="card">
|
|
|
<h3>Available GDC Projects</h3>
|
|
|
<div id="gdc-projects"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div id="pipeline" class="tab-content">
|
|
|
<div class="cards">
|
|
|
<div class="card">
|
|
|
<h3>FASTQ Quality Control</h3>
|
|
|
<p>Run quality control analysis on sequencing data</p>
|
|
|
<button class="button" style="margin-top: 15px;">Run QC</button>
|
|
|
</div>
|
|
|
<div class="card">
|
|
|
<h3>BLAST Search</h3>
|
|
|
<p>Perform sequence alignment and homology search</p>
|
|
|
<button class="button" style="margin-top: 15px;">Run BLAST</button>
|
|
|
</div>
|
|
|
<div class="card">
|
|
|
<h3>Variant Calling</h3>
|
|
|
<p>Identify genetic variants from sequencing data</p>
|
|
|
<button class="button" style="margin-top: 15px;">Call Variants</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
function showTab(tabName) {
|
|
|
document.querySelectorAll('.tab-content').forEach(tab => {
|
|
|
tab.classList.remove('active');
|
|
|
});
|
|
|
document.querySelectorAll('.tab-button').forEach(btn => {
|
|
|
btn.classList.remove('active');
|
|
|
});
|
|
|
|
|
|
document.getElementById(tabName).classList.add('active');
|
|
|
event.target.classList.add('active');
|
|
|
|
|
|
if (tabName === 'dashboard') loadDashboard();
|
|
|
else if (tabName === 'neo4j') loadNeo4jViz();
|
|
|
else if (tabName === 'boinc') loadBoincTasks();
|
|
|
else if (tabName === 'gdc') loadGdcProjects();
|
|
|
}
|
|
|
|
|
|
|
|
|
async function loadDashboard() {
|
|
|
try {
|
|
|
const response = await fetch('/api/neo4j/summary');
|
|
|
const data = await response.json();
|
|
|
|
|
|
document.getElementById('total-genes').textContent = data.genes || 0;
|
|
|
document.getElementById('total-mutations').textContent = data.mutations || 0;
|
|
|
document.getElementById('total-patients').textContent = data.patients || 0;
|
|
|
document.getElementById('total-cancer-types').textContent = data.cancer_types || 0;
|
|
|
|
|
|
createMutationChart();
|
|
|
} catch (error) {
|
|
|
console.error('Error loading dashboard:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function createMutationChart() {
|
|
|
const ctx = document.getElementById('mutation-chart').getContext('2d');
|
|
|
new Chart(ctx, {
|
|
|
type: 'bar',
|
|
|
data: {
|
|
|
labels: ['Breast Cancer', 'Lung Adenocarcinoma', 'Colon Adenocarcinoma', 'Glioblastoma'],
|
|
|
datasets: [{
|
|
|
label: 'Mutations',
|
|
|
data: [245, 189, 156, 203],
|
|
|
backgroundColor: [
|
|
|
'rgba(102, 126, 234, 0.8)',
|
|
|
'rgba(118, 75, 162, 0.8)',
|
|
|
'rgba(237, 100, 166, 0.8)',
|
|
|
'rgba(255, 154, 158, 0.8)'
|
|
|
]
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: true,
|
|
|
plugins: {
|
|
|
legend: { display: false }
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
function loadNeo4jViz() {
|
|
|
const viz = document.getElementById('neo4j-viz');
|
|
|
viz.innerHTML = '<div class="loading"><div class="spinner"></div><p>Loading graph visualization...</p></div>';
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
const width = viz.clientWidth;
|
|
|
const height = 600;
|
|
|
|
|
|
viz.innerHTML = '';
|
|
|
const svg = d3.select('#neo4j-viz')
|
|
|
.append('svg')
|
|
|
.attr('width', width)
|
|
|
.attr('height', height);
|
|
|
|
|
|
|
|
|
const nodes = [
|
|
|
{ id: 'TP53', type: 'gene', x: width/2, y: height/2 },
|
|
|
{ id: 'BRCA1', type: 'gene', x: width/3, y: height/3 },
|
|
|
{ id: 'KRAS', type: 'gene', x: 2*width/3, y: height/3 },
|
|
|
{ id: 'Patient 1', type: 'patient', x: width/4, y: 3*height/4 },
|
|
|
{ id: 'Patient 2', type: 'patient', x: 3*width/4, y: 3*height/4 },
|
|
|
{ id: 'Breast Cancer', type: 'cancer', x: width/2, y: height/4 }
|
|
|
];
|
|
|
|
|
|
const links = [
|
|
|
{ source: 'Patient 1', target: 'TP53' },
|
|
|
{ source: 'Patient 1', target: 'Breast Cancer' },
|
|
|
{ source: 'Patient 2', target: 'KRAS' },
|
|
|
{ source: 'TP53', target: 'Breast Cancer' }
|
|
|
];
|
|
|
|
|
|
|
|
|
svg.selectAll('line')
|
|
|
.data(links)
|
|
|
.enter()
|
|
|
.append('line')
|
|
|
.attr('x1', d => nodes.find(n => n.id === d.source).x)
|
|
|
.attr('y1', d => nodes.find(n => n.id === d.source).y)
|
|
|
.attr('x2', d => nodes.find(n => n.id === d.target).x)
|
|
|
.attr('y2', d => nodes.find(n => n.id === d.target).y)
|
|
|
.attr('stroke', '#999')
|
|
|
.attr('stroke-width', 2);
|
|
|
|
|
|
|
|
|
svg.selectAll('circle')
|
|
|
.data(nodes)
|
|
|
.enter()
|
|
|
.append('circle')
|
|
|
.attr('cx', d => d.x)
|
|
|
.attr('cy', d => d.y)
|
|
|
.attr('r', 20)
|
|
|
.attr('fill', d => {
|
|
|
if (d.type === 'gene') return '#667eea';
|
|
|
if (d.type === 'patient') return '#764ba2';
|
|
|
return '#ed64a6';
|
|
|
});
|
|
|
|
|
|
|
|
|
svg.selectAll('text')
|
|
|
.data(nodes)
|
|
|
.enter()
|
|
|
.append('text')
|
|
|
.attr('x', d => d.x)
|
|
|
.attr('y', d => d.y - 25)
|
|
|
.attr('text-anchor', 'middle')
|
|
|
.text(d => d.id)
|
|
|
.attr('font-size', '12px')
|
|
|
.attr('fill', '#333');
|
|
|
}, 500);
|
|
|
}
|
|
|
|
|
|
|
|
|
async function loadBoincTasks() {
|
|
|
try {
|
|
|
const [tasksResponse, statsResponse] = await Promise.all([
|
|
|
fetch('/api/boinc/tasks'),
|
|
|
fetch('/api/boinc/statistics')
|
|
|
]);
|
|
|
|
|
|
const tasksData = await tasksResponse.json();
|
|
|
const statsData = await statsResponse.json();
|
|
|
|
|
|
|
|
|
const taskList = document.getElementById('task-list');
|
|
|
taskList.innerHTML = tasksData.tasks.map(task => `
|
|
|
<li class="task-item ${task.status}">
|
|
|
<strong>${task.name}</strong>
|
|
|
<span class="status-badge status-${task.status}">${task.status}</span>
|
|
|
<div style="margin-top: 8px; font-size: 14px; color: #666;">
|
|
|
Type: ${task.workunit_type} | Created: ${new Date(task.created_at).toLocaleString()}
|
|
|
</div>
|
|
|
</li>
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
const statsDiv = document.getElementById('boinc-stats');
|
|
|
statsDiv.innerHTML = `
|
|
|
<p><strong>Total Tasks:</strong> ${statsData.total_tasks}</p>
|
|
|
<p><strong>Completed:</strong> ${statsData.by_status?.completed || 0}</p>
|
|
|
<p><strong>Running:</strong> ${statsData.by_status?.running || 0}</p>
|
|
|
<p><strong>Pending:</strong> ${statsData.by_status?.pending || 0}</p>
|
|
|
`;
|
|
|
} catch (error) {
|
|
|
console.error('Error loading BOINC tasks:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
async function submitBoincTask() {
|
|
|
const taskType = document.getElementById('task-type').value;
|
|
|
const inputFile = document.getElementById('input-file').value;
|
|
|
|
|
|
if (!inputFile) {
|
|
|
alert('Please provide an input file path');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/boinc/submit', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ workunit_type: taskType, input_file: inputFile })
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
alert(`Task submitted successfully! Task ID: ${data.task_id}`);
|
|
|
loadBoincTasks();
|
|
|
} catch (error) {
|
|
|
console.error('Error submitting task:', error);
|
|
|
alert('Failed to submit task');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
async function loadGdcProjects() {
|
|
|
try {
|
|
|
const response = await fetch('/api/gdc/projects');
|
|
|
const data = await response.json();
|
|
|
|
|
|
const projectsDiv = document.getElementById('gdc-projects');
|
|
|
projectsDiv.innerHTML = data.projects.map(project => `
|
|
|
<div class="project-card">
|
|
|
<h4>${project.name}</h4>
|
|
|
<p>Project ID: ${project.id}</p>
|
|
|
<p>Cases: ${project.cases}</p>
|
|
|
</div>
|
|
|
`).join('');
|
|
|
} catch (error) {
|
|
|
console.error('Error loading GDC projects:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
window.onload = () => {
|
|
|
loadDashboard();
|
|
|
};
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|
|
|
|