OCx25 / xrd_plot_blueprint.py
mshuaibi's picture
add xrd data
5746040
Raw
History Blame Contribute Delete
58.7 kB
import pandas as pd
import matplotlib.pyplot as plt
from flask import Blueprint, render_template_string, request, jsonify, Response, send_from_directory
import os
import json
# Create Blueprint
xrd_plot_bp = Blueprint('xrd_plot', __name__, url_prefix='/xrd')
XRD_DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Data', 'XRD')
@xrd_plot_bp.route('/data/metadata.json')
def xrd_metadata():
"""Serve the locally bundled XRD metadata."""
response = send_from_directory(
XRD_DATA_DIR,
'metadata.json.gz',
mimetype='application/json',
conditional=True,
)
response.headers['Content-Encoding'] = 'gzip'
response.headers['Vary'] = 'Accept-Encoding'
return response
@xrd_plot_bp.route('/data/<dataset>.json')
def xrd_dataset(dataset):
"""Stream a locally bundled XRD dataset on demand."""
response = send_from_directory(
os.path.join(XRD_DATA_DIR, 'datasets'),
f'{dataset}.json.gz',
mimetype='application/json',
conditional=True,
)
response.headers['Content-Encoding'] = 'gzip'
response.headers['Vary'] = 'Accept-Encoding'
return response
@xrd_plot_bp.route('/')
def xrd_plot_main():
"""Main XRD plot page"""
# Create the HTML template exactly matching the original XRD dashboard
html_template = r'''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OCx25 XRD Analysis Dashboard</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.back-link {
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
background: #1a73e8;
color: white;
padding: 12px 20px;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
font-size: 14px;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(26, 115, 232, 0.3);
}
.back-link:hover {
background: #1557b0;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(26, 115, 232, 0.4);
}
.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 20px;
}
.header h1 {
color: #333;
margin: 0;
font-size: 2.5em;
}
.header p {
color: #666;
margin: 10px 0 0 0;
font-size: 1.1em;
}
.controls {
display: flex;
gap: 20px;
margin-bottom: 30px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.control-group label {
font-weight: 600;
color: #555;
font-size: 0.9em;
}
.control-group select,
.control-group input {
padding: 10px 15px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 1em;
background-color: white;
min-width: 200px;
transition: border-color 0.3s;
}
.control-group select:focus,
.control-group input:focus {
outline: none;
border-color: #4CAF50;
}
.control-group input.invalid {
border-color: #f44336;
background-color: #ffebee;
}
.control-group input.valid {
border-color: #4CAF50;
background-color: #e8f5e8;
}
.plot-container {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.metadata-panel {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.metadata-panel h3 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #4CAF50;
padding-bottom: 10px;
}
.metadata-section {
margin-bottom: 20px;
}
.metadata-section h4 {
color: #555;
margin-bottom: 10px;
font-size: 1.1em;
}
.metadata-item {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid #eee;
}
.metadata-item:last-child {
border-bottom: none;
}
.metadata-label {
font-weight: 600;
color: #666;
}
.metadata-value {
color: #333;
}
.iteration-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
}
.iteration-item {
padding: 8px;
margin: 5px 0;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.iteration-item:hover {
background-color: #f5f5f5;
}
.iteration-item.selected {
background-color: #e8f5e8;
border: 1px solid #4CAF50;
}
.rwp-score {
font-weight: bold;
}
.rwp-good { color: #4CAF50; }
.rwp-medium { color: #FF9800; }
.rwp-poor { color: #f44336; }
.iterations-table-panel {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.iterations-table-panel h3 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #4CAF50;
padding-bottom: 10px;
}
.table-controls {
margin-bottom: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.rwp-filter {
display: flex;
align-items: center;
gap: 5px;
background-color: #f8f9fa;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ddd;
}
.rwp-filter label {
font-size: 0.9em;
font-weight: 600;
color: #555;
margin: 0;
}
.rwp-filter input {
width: 80px;
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 0.9em;
}
.rwp-filter input:focus {
outline: none;
border-color: #4CAF50;
}
.weight-filter {
display: flex;
align-items: center;
gap: 5px;
background-color: #f8f9fa;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ddd;
}
.weight-filter label {
font-size: 0.9em;
font-weight: 600;
color: #555;
margin: 0;
}
.weight-filter input {
width: 80px;
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 0.9em;
}
.weight-filter input:focus {
outline: none;
border-color: #4CAF50;
}
.sort-btn, .filter-btn {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f8f9fa;
cursor: pointer;
font-size: 0.9em;
transition: all 0.2s;
}
.sort-btn:hover, .filter-btn:hover {
background-color: #e9ecef;
border-color: #4CAF50;
}
.sort-btn.active {
background-color: #4CAF50;
color: white;
border-color: #4CAF50;
}
.filter-btn.active {
background-color: #2196F3;
color: white;
border-color: #2196F3;
}
.table-container {
overflow-x: auto;
border: 1px solid #ddd;
border-radius: 4px;
}
#iterationsTable {
width: 100%;
border-collapse: collapse;
font-size: 0.9em;
}
#iterationsTable th {
background-color: #f8f9fa;
padding: 12px 8px;
text-align: left;
border-bottom: 2px solid #ddd;
font-weight: 600;
color: #555;
white-space: normal;
word-wrap: break-word;
max-width: 150px;
}
#iterationsTable th.sortable {
cursor: pointer;
user-select: none;
position: relative;
}
#iterationsTable th.sortable:hover {
background-color: #e9ecef;
}
#iterationsTable th.sortable::after {
content: ' ↕';
opacity: 0.5;
}
#iterationsTable th.sort-asc::after {
content: ' ↑';
opacity: 1;
color: #4CAF50;
}
#iterationsTable th.sort-desc::after {
content: ' ↓';
opacity: 1;
color: #4CAF50;
}
#iterationsTable td {
padding: 10px 8px;
border-bottom: 1px solid #eee;
vertical-align: top;
}
#iterationsTable td:has(.phase-visual) {
padding-top: 10px;
padding-bottom: 10px;
}
#iterationsTable tr:hover {
background-color: #f8f9fa;
}
#iterationsTable tr.selected {
background-color: #e8f5e8;
}
.phase-names {
max-width: 200px;
font-size: 0.8em;
line-height: 1.3;
}
.weights {
font-family: monospace;
font-size: 0.8em;
}
.action-btn {
padding: 4px 8px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 0.8em;
}
.action-btn:hover {
background-color: #45a049;
}
.status-pass { color: #4CAF50; font-weight: bold; }
.status-fail { color: #f44336; font-weight: bold; }
.status-yes { color: #4CAF50; font-weight: bold; }
.status-no { color: #666; }
.phase-visual {
width: 120px;
height: 40px;
position: relative;
border: 1px solid #ddd;
border-radius: 4px;
overflow: visible;
cursor: pointer;
transition: transform 0.2s;
}
.phase-visual:hover {
transform: scale(1.05);
border-color: #4CAF50;
}
.phase-bar {
height: 100%;
float: left;
position: relative;
transition: all 0.2s;
cursor: pointer;
}
.phase-bar:hover {
opacity: 0.8;
transform: scaleY(1.1);
}
.phase-label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.7em;
font-weight: bold;
color: white;
text-shadow: 1px 1px 1px rgba(0,0,0,0.5);
pointer-events: none;
}
.phase-tooltip {
position: fixed;
background-color: rgba(0,0,0,0.95);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.8em;
z-index: 9999;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
white-space: nowrap;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
border: 1px solid rgba(255,255,255,0.1);
}
.pie-chart-container {
width: 40px;
height: 40px;
position: relative;
cursor: pointer;
}
.pie-slice {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
clip-path: polygon(50% 50%, 50% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%);
transform-origin: 50% 50%;
}
#plotly-chart {
width: 100%;
height: 600px;
}
.info-panel {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
border-left: 4px solid #4CAF50;
}
.info-panel h3 {
margin: 0 0 10px 0;
color: #333;
}
.info-panel p {
margin: 5px 0;
color: #666;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
font-style: italic;
}
/* Animated loading spinner */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #1976d2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-animated {
text-align: center;
padding: 40px;
color: #1976d2;
background-color: #e3f2fd;
border: 1px solid #bbdefb;
border-radius: 8px;
margin: 20px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.error {
text-align: center;
padding: 40px;
color: #f44336;
background-color: #ffebee;
border: 1px solid #f5c6cb;
border-radius: 8px;
margin: 20px;
}
.loading {
text-align: center;
padding: 40px;
color: #1976d2;
background-color: #e3f2fd;
border: 1px solid #bbdefb;
border-radius: 8px;
margin: 20px;
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.control-group {
width: 100%;
}
.control-group select,
.control-group input {
min-width: auto;
width: 100%;
}
.table-controls {
flex-direction: column;
align-items: stretch;
}
}
</style>
</head>
<body>
<a href="/" class="back-link">← Back to Dashboard</a>
<div class="container">
<div class="header">
<h1>OCx25 XRD Analysis Dashboard</h1>
</div>
<div class="controls">
<div class="control-group">
<label for="datasetSelect">Dataset:</label>
<select id="datasetSelect" onchange="updateDataset()">
<option value="">Select dataset...</option>
</select>
</div>
<div class="control-group">
<label for="sampleInput">Sample ID:</label>
<input type="text" id="sampleInput" list="sampleList" placeholder="Type or select sample ID..." onchange="updateSample()" oninput="updateSample()" onclick="showAllSamples()">
<datalist id="sampleList">
<option value="">Loading samples...</option>
</datalist>
</div>
<div class="control-group">
<label for="fittingSelect">Data:</label>
<select id="fittingSelect" onchange="updatePlot()">
<option value="original">Original Data</option>
</select>
</div>
</div>
<div class="plot-container">
<div id="plotly-chart">
</div>
</div>
<div class="metadata-panel">
<h3>Sample Metadata</h3>
<div id="metadataContent">
<div class="metadata-section">
<div id="basicInfo">
<div class="loading">Select a sample to view metadata</div>
</div>
</div>
</div>
</div>
<div class="iterations-table-panel">
<h3>Table overview of all XRD fitting solutions</h3>
<div class="table-controls">
<div class="rwp-filter">
<label>RWP Max:</label>
<input type="number" id="rwpMaxInput" placeholder="e.g., 50" onkeypress="if(event.key==='Enter') applyRwpFilter()">
<button class="filter-btn" onclick="applyRwpFilter()">Apply</button>
</div>
<div class="weight-filter">
<label>Major Phase Weight Min:</label>
<input type="number" id="weightMinInput" placeholder="e.g., 0.5" step="0.1" min="0" max="1" onkeypress="if(event.key==='Enter') applyWeightFilter()">
<button class="filter-btn" onclick="applyWeightFilter()">Apply</button>
</div>
<button class="filter-btn" onclick="checkPhaseMassBalance()" title="Filter iterations where phase weights match XRF composition within tolerance">Check Phase Mass Balance with XRF</button>
<button class="filter-btn" onclick="clearFilters()">Clear Filters</button>
</div>
<div class="table-container">
<table id="iterationsTable">
<thead>
<tr>
<th onclick="sortTable('xrd_fit_number')" class="sortable">#</th>
<th onclick="sortTable('rwp_score')" class="sortable">RWP Score</th>
<th onclick="sortTable('number_of_phases_matched')" class="sortable"># Phases</th>
<th>Phase Names</th>
<th>Weights</th>
<th>Visual</th>
<th>Action</th>
</tr>
</thead>
<tbody id="iterationsTableBody">
<tr><td colspan="7" style="text-align: center; padding: 20px;">Select a dataset and sample to view iterations</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
// Global variables
let allDatasets = {};
let currentDataset = '';
let currentSample = '';
let currentFitting = 'original';
let currentTableData = [];
let currentFilters = {};
let currentSortColumn = 'rwp_score';
let currentSortDirection = 'asc';
// Available datasets - these are the actual filenames in the extracted data folder
const datasets = [
'uoft4_240405', 'uoft5_240507', 'uoft6_240624', 'uoft7_240716', 'uoft8_241025', 'uoft9_241025',
'vsp1_240503', 'vsp10_240726', 'vsp11_240802', 'vsp12_240816', 'vsp13_240823',
'vsp14_240906', 'vsp15_240809', 'vsp2_240718', 'vsp20_241011', 'vsp23_241025', 'vsp26_241004',
'vsp27_241030', 'vsp28_240927', 'vsp29_241004', 'vsp3_240718', 'vsp4_240726', 'vsp5_241011',
'vsp6_240718', 'vsp7_241025', 'vsp8_240802', 'vsp9_240726'
];
// Load metadata for all locally bundled datasets
async function loadAllDatasets() {
console.log('🔄 Loading locally bundled OCx24 XRD metadata...');
try {
console.log('📋 Loading local metadata...');
const metadataResponse = await fetch('/xrd/data/metadata.json', {
headers: {
'Accept': 'application/json',
}
});
if (!metadataResponse.ok) {
throw new Error(`Failed to load local metadata: ${metadataResponse.status}`);
}
// Use streaming JSON parsing for metadata as well
const reader = metadataResponse.body.getReader();
const decoder = new TextDecoder();
let jsonString = '';
console.log('🔄 Streaming metadata JSON...');
while (true) {
const { done, value } = await reader.read();
if (done) break;
jsonString += decoder.decode(value, { stream: true });
}
const metadataData = JSON.parse(jsonString);
console.log('✅ Local metadata loaded successfully using streaming');
// Initialize datasets with metadata
allDatasets = {};
Object.keys(metadataData).forEach(dataset => {
allDatasets[dataset] = {
plotly: null, // Will load on demand
metadata: metadataData[dataset]
};
});
console.log(`🎉 Initialized ${Object.keys(allDatasets).length} datasets with metadata`);
populateDatasetDropdown();
// Show success message
document.getElementById('plotly-chart').innerHTML =
'<div class="loading">' +
Object.keys(allDatasets).length + ' datasets available<br><br>' +
'Select a dataset from the dropdown to load XRD data...</div>';
} catch (error) {
console.error('❌ Error loading local XRD data:', error);
document.getElementById('plotly-chart').innerHTML =
'<div class="error">Error loading local XRD data<br><br>' +
'Please ensure:<br>' +
'1. Git LFS files were downloaded<br>' +
'2. Data/XRD/metadata.json.gz is available<br>' +
'3. Data/XRD/datasets contains the selected dataset<br><br>' +
'Error details: ' + error.message + '</div>';
}
}
// Load plotly data for specific dataset on demand using JSON streaming
async function loadDatasetData(dataset) {
if (allDatasets[dataset] && allDatasets[dataset].plotly) {
return allDatasets[dataset].plotly; // Already loaded
}
console.log(`📊 Loading plotly data for ${dataset} using JSON streaming...`);
try {
const dataResponse = await fetch(`/xrd/data/${encodeURIComponent(dataset)}.json`, {
headers: {
'Accept': 'application/json',
}
});
if (!dataResponse.ok) {
throw new Error(`Failed to load data for ${dataset}: ${dataResponse.status}`);
}
// Use streaming JSON parsing for all datasets regardless of size
const reader = dataResponse.body.getReader();
const decoder = new TextDecoder();
let jsonString = '';
console.log(`🔄 Streaming JSON data for ${dataset}...`);
while (true) {
const { done, value } = await reader.read();
if (done) break;
jsonString += decoder.decode(value, { stream: true });
}
// Parse the complete JSON string
const plotlyData = JSON.parse(jsonString);
console.log(`✅ Streamed JSON data loaded for ${dataset}`);
// Cache the data
if (allDatasets[dataset]) {
allDatasets[dataset].plotly = plotlyData;
}
return plotlyData;
} catch (error) {
console.error(`❌ Error loading streamed data for ${dataset}:`, error);
throw error;
}
}
// Populate dataset dropdown
function populateDatasetDropdown() {
const datasetSelect = document.getElementById('datasetSelect');
datasetSelect.innerHTML = '<option value="">Select dataset...</option>';
// Sort datasets properly (numerical order for numbers)
const sortedDatasets = Object.keys(allDatasets).sort((a, b) => {
// Extract the numeric part for proper sorting
const aMatch = a.match(/^(uoft|vsp)(\d+)/);
const bMatch = b.match(/^(uoft|vsp)(\d+)/);
if (aMatch && bMatch) {
const aPrefix = aMatch[1];
const bPrefix = bMatch[1];
const aNum = parseInt(aMatch[2]);
const bNum = parseInt(bMatch[2]);
// First sort by prefix (uoft before vsp)
if (aPrefix !== bPrefix) {
return aPrefix.localeCompare(bPrefix);
}
// Then sort by number
return aNum - bNum;
}
// Fallback to regular string sorting
return a.localeCompare(b);
});
sortedDatasets.forEach(dataset => {
const option = document.createElement('option');
option.value = dataset;
option.textContent = dataset;
datasetSelect.appendChild(option);
});
}
// Update dataset selection
async function updateDataset() {
const datasetSelect = document.getElementById('datasetSelect');
currentDataset = datasetSelect.value;
if (currentDataset && allDatasets[currentDataset]) {
// Populate sample dropdown and auto-load first sample's Original Data
populateSampleDropdown();
// populateSampleDropdown() will automatically call updateSample() for the first sample
// which will trigger updatePlot() and updateMetadata()
} else {
// Clear everything
document.getElementById('sampleList').innerHTML = '<option value="">No dataset selected</option>';
document.getElementById('sampleInput').value = '';
document.getElementById('fittingSelect').innerHTML = '<option value="original">Original Data</option>';
document.getElementById('plotly-chart').innerHTML = '<div class="loading">Select a dataset to view data</div>';
document.getElementById('basicInfo').innerHTML = '<div class="loading">Select a dataset and sample to view metadata</div>';
document.getElementById('iterationsTableBody').innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 20px;">Select a dataset and sample to view iterations</td></tr>';
}
}
// Populate the sample dropdown using metadata
function populateSampleDropdown() {
const sampleList = document.getElementById('sampleList');
sampleList.innerHTML = '';
if (!currentDataset || !allDatasets[currentDataset] || !allDatasets[currentDataset].metadata) {
return;
}
// Get sample IDs from metadata instead of plotly data
const samples = Object.keys(allDatasets[currentDataset].metadata).sort();
samples.forEach(sampleId => {
const option = document.createElement('option');
option.value = sampleId;
sampleList.appendChild(option);
});
if (samples.length > 0) {
document.getElementById('sampleInput').value = samples[0];
updateSample();
}
}
// Show all samples when dropdown arrow is clicked
function showAllSamples() {
const sampleInput = document.getElementById('sampleInput');
const currentValue = sampleInput.value;
// Temporarily clear the input to show all options
sampleInput.value = '';
// Restore the value after a short delay to allow the dropdown to appear
setTimeout(() => {
sampleInput.value = currentValue;
}, 10);
}
// Update sample selection
function updateSample() {
const sampleInput = document.getElementById('sampleInput');
const fittingSelect = document.getElementById('fittingSelect');
currentSample = sampleInput.value.trim();
// Validate sample ID
if (!currentSample) {
return;
}
// Check if sample exists in current dataset metadata
if (!currentDataset || !allDatasets[currentDataset] || !allDatasets[currentDataset].metadata || !allDatasets[currentDataset].metadata[currentSample]) {
sampleInput.classList.add('invalid');
sampleInput.classList.remove('valid');
return;
}
sampleInput.classList.add('valid');
sampleInput.classList.remove('invalid');
// Update fitting options - start with Original Data, load others on demand
fittingSelect.innerHTML = '<option value="original">Original Data</option>';
currentFitting = 'original';
// Auto-load and plot the Original Data immediately
updatePlot();
updateMetadata();
}
// Update the plot
async function updatePlot() {
const fittingSelect = document.getElementById('fittingSelect');
currentFitting = fittingSelect.value;
if (!currentDataset || !allDatasets[currentDataset] || !currentSample) {
document.getElementById('plotly-chart').innerHTML = '<div class="loading">Select a dataset and sample to view plot</div>';
return;
}
// Load plotly data only when needed for plotting
if (!allDatasets[currentDataset].plotly) {
console.log(`🔄 Loading plotly data for ${currentDataset} (needed for plotting)...`);
try {
document.getElementById('plotly-chart').innerHTML = '<div class="loading-animated"><div class="loading-spinner"></div>Loading XRD data for plotting...</div>';
const plotlyData = await loadDatasetData(currentDataset);
allDatasets[currentDataset].plotly = plotlyData;
console.log(`✅ Plotly data loaded for ${currentDataset}`);
// Populate fitting options now that data is loaded
const plotlyDataForSample = plotlyData[currentSample];
if (plotlyDataForSample && plotlyDataForSample.files && plotlyDataForSample.files.iterations) {
Object.keys(plotlyDataForSample.files.iterations).sort((a, b) => parseInt(a) - parseInt(b)).forEach(iterNum => {
const option = document.createElement('option');
option.value = iterNum;
option.textContent = `XRD Fit ${iterNum}`;
fittingSelect.appendChild(option);
});
}
} catch (error) {
console.error(`❌ Error loading plotly data for ${currentDataset}:`, error);
document.getElementById('plotly-chart').innerHTML =
'<div class="error">Error loading XRD data for plotting<br><br>' +
'Error details: ' + error.message + '</div>';
return;
}
}
const plotlyData = allDatasets[currentDataset].plotly[currentSample];
let traces = [];
let layout = {
title: `${currentSample} - ${currentFitting === 'original' ? 'Original Data' : `XRD Fit ${currentFitting}`}`,
xaxis: { title: '2θ (degrees)' },
yaxis: { title: 'Intensity' },
showlegend: true,
legend: { x: 1.02, y: 1 },
margin: { r: 150 }
};
if (currentFitting === 'original') {
// Show original XRD data
const xrdData = plotlyData.files.xrd;
traces = xrdData.traces.map(trace => {
const traceName = trace.name ? trace.name.toLowerCase() : '';
const isNormalized = traceName.includes('normalized');
const isExtractedPeaks = traceName.includes('peak') || traceName.includes('extracted');
const plotlyTrace = {
x: trace.x || [],
y: trace.y || [],
type: isExtractedPeaks ? 'bar' : 'scatter',
mode: isExtractedPeaks ? undefined : 'lines',
name: trace.name || 'unnamed',
visible: isNormalized || isExtractedPeaks ? true : 'legendonly'
};
// Add styling based on trace type
if (isExtractedPeaks) {
plotlyTrace.marker = { size: 4 };
} else {
plotlyTrace.line = { width: 2 };
}
return plotlyTrace;
});
} else {
// Show normalized original + iteration data
const xrdData = plotlyData.files.xrd;
const iterData = plotlyData.files.iterations[currentFitting];
// Find normalized trace from original data by name
const normalizedTrace = xrdData.traces.find(trace =>
trace.name && trace.name.toLowerCase().includes('normalized')
);
if (normalizedTrace) {
traces.push({
x: normalizedTrace.x,
y: normalizedTrace.y,
type: 'scatter',
mode: 'lines',
name: 'normalized xrd',
line: { width: 2, color: '#1f77b4' },
visible: true
});
}
// Add iteration traces
iterData.traces.forEach(trace => {
const traceName = trace.name ? trace.name.toLowerCase() : '';
const isCalc = traceName === 'calc';
const isBackground = traceName.includes('background');
const isNormalized = traceName.includes('normalized');
const hasZeroWeight = traceName.includes('wt=0.0');
// Determine trace type and visibility
let traceType = 'scatter';
let traceMode = 'lines';
let isVisible = true;
if (isBackground) {
isVisible = 'legendonly'; // Background invisible by default
} else if (hasZeroWeight) {
isVisible = 'legendonly'; // Zero weight traces invisible by default
} else if (!isCalc && !isNormalized) {
traceType = 'bar'; // Everything else as bars
traceMode = undefined;
}
const plotlyTrace = {
x: trace.x || [],
y: trace.y || [],
type: traceType,
mode: traceMode,
name: trace.name || 'unnamed',
visible: isVisible
};
// Add appropriate styling
if (traceType === 'bar') {
plotlyTrace.marker = { size: 4 };
} else {
plotlyTrace.line = { width: 2 };
}
traces.push(plotlyTrace);
});
}
// Clear any loading animation before rendering the plot
document.getElementById('plotly-chart').innerHTML = '';
Plotly.newPlot('plotly-chart', traces, layout, {
responsive: true,
toImageButtonOptions: {
format: 'png',
filename: 'xrd_analysis_plot',
height: 800,
width: 1200,
scale: 3
}
});
}
// Update metadata display
function updateMetadata() {
if (!currentDataset || !allDatasets[currentDataset] || !allDatasets[currentDataset].metadata || !currentSample) {
document.getElementById('basicInfo').innerHTML = '<div class="loading">Select a dataset and sample to view metadata</div>';
return;
}
const sampleMetadata = allDatasets[currentDataset].metadata[currentSample];
if (!sampleMetadata) {
document.getElementById('basicInfo').innerHTML = '<div class="loading">No metadata available for this sample</div>';
return;
}
updateBasicInfo(sampleMetadata);
updateIterationsTable(sampleMetadata);
}
// Update basic info section
function updateBasicInfo(sampleMetadata) {
const basicInfo = document.getElementById('basicInfo');
basicInfo.innerHTML = `
<div class="metadata-item">
<span class="metadata-label">Target Composition:</span>
<span class="metadata-value">${sampleMetadata.target_composition || 'N/A'}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">XRF Composition:</span>
<span class="metadata-value">${sampleMetadata.xrf_composition || 'N/A'}</span>
</div>
`;
}
// Update iterations table
function updateIterationsTable(sampleMetadata) {
if (!sampleMetadata.iterations || sampleMetadata.iterations.length === 0) {
document.getElementById('iterationsTableBody').innerHTML =
'<tr><td colspan="7" style="text-align: center; padding: 20px;">No data available</td></tr>';
return;
}
currentTableData = [...sampleMetadata.iterations];
// Set default sort by RWP score (ascending - best scores first)
currentSortColumn = 'rwp_score';
currentSortDirection = 'asc';
renderTable();
}
// Render the iterations table
function renderTable() {
const tbody = document.getElementById('iterationsTableBody');
let filteredData = [...currentTableData];
// Apply filters
filteredData = filteredData.filter(iteration => {
for (const [key, value] of Object.entries(currentFilters)) {
if (key === 'rwp_max') {
// Special handling for RWP maximum filter
if (iteration.rwp_score !== undefined && iteration.rwp_score > value) {
return false;
}
} else if (key === 'weight_min') {
// Special handling for weight minimum filter (major phase detection)
if (iteration.weights && iteration.weights.length > 0) {
const hasMajorPhase = iteration.weights.some(weight => weight >= value);
if (!hasMajorPhase) {
return false;
}
} else {
// No weights data, filter out
return false;
}
} else {
// Standard filter handling
if (iteration[key] !== value) {
return false;
}
}
}
return true;
});
// Apply sorting
if (currentSortColumn) {
filteredData.sort((a, b) => {
const aVal = a[currentSortColumn];
const bVal = b[currentSortColumn];
if (aVal === undefined && bVal === undefined) return 0;
if (aVal === undefined) return 1;
if (bVal === undefined) return -1;
if (typeof aVal === 'number' && typeof bVal === 'number') {
return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
}
const aStr = String(aVal).toLowerCase();
const bStr = String(bVal).toLowerCase();
return currentSortDirection === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
});
}
// Generate HTML
let html = '';
if (filteredData.length === 0) {
html = '<tr><td colspan="7" style="text-align: center; padding: 20px;">No iterations match the current filters</td></tr>';
} else {
filteredData.forEach(iteration => {
const isSelected = iteration.xrd_fit_number == currentFitting;
html += `
<tr class="${isSelected ? 'selected' : ''}" data-iteration="${iteration.xrd_fit_number}">
<td>${iteration.xrd_fit_number || 'N/A'}</td>
<td class="rwp-score ${getRwpClass(iteration.rwp_score)}">${iteration.rwp_score || 'N/A'}</td>
<td>${iteration.number_of_phases_matched || 'N/A'}</td>
<td class="phase-names">${iteration.phases_matched ? iteration.phases_matched.join(', ') : 'N/A'}</td>
<td class="weights">${iteration.weights ? '[' + iteration.weights.join(', ') + ']' : 'N/A'}</td>
<td>${generatePhaseVisual(iteration)}</td>
<td><button class="action-btn" onclick="selectIteration(${iteration.xrd_fit_number})">Plot</button></td>
</tr>
`;
});
}
tbody.innerHTML = html;
}
// Get RWP class for styling
function getRwpClass(rwpScore) {
if (rwpScore === 'N/A' || rwpScore === undefined) return '';
if (rwpScore <= 50) return 'rwp-good';
if (rwpScore <= 100) return 'rwp-medium';
return 'rwp-poor';
}
// Generate phase visual
function generatePhaseVisual(iteration) {
if (!iteration.phases_matched || !iteration.weights ||
iteration.phases_matched.length === 0 || iteration.weights.length === 0) {
return '<div class="phase-visual" style="background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 0.8em; color: #666;">No Data</div>';
}
// Generate colors for phases
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'];
let visualHtml = '<div class="phase-visual">';
// Calculate total weight for normalization
const totalWeight = iteration.weights.reduce((sum, weight) => sum + Math.abs(weight), 0);
if (totalWeight === 0) {
visualHtml += '<div style="width: 100%; height: 100%; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 0.8em; color: #666;">Zero Weights</div>';
} else {
iteration.phases_matched.forEach((phase, index) => {
const weight = Math.abs(iteration.weights[index] || 0);
const percentage = (weight / totalWeight) * 100;
const color = colors[index % colors.length];
if (percentage > 0) {
visualHtml += `
<div class="phase-bar"
style="width: ${percentage}%; background-color: ${color};"
onmouseenter="showTooltip(event, '${phase}', '${percentage.toFixed(1)}%')"
onmouseleave="hideTooltip()">
${percentage > 15 ? `<div class="phase-label">${(percentage).toFixed(0)}%</div>` : ''}
</div>
`;
}
});
}
visualHtml += '</div>';
return visualHtml;
}
// Sort table
function sortTable(column) {
if (currentSortColumn === column) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
currentSortColumn = column;
currentSortDirection = 'asc';
}
renderTable();
}
// Apply RWP filter
function applyRwpFilter() {
const rwpMaxInput = document.getElementById('rwpMaxInput');
const value = parseFloat(rwpMaxInput.value);
if (!isNaN(value) && value > 0) {
currentFilters.rwp_max = value;
} else {
delete currentFilters.rwp_max;
}
renderTable();
}
// Apply weight filter
function applyWeightFilter() {
const weightMinInput = document.getElementById('weightMinInput');
const value = parseFloat(weightMinInput.value);
if (!isNaN(value) && value >= 0 && value <= 1) {
currentFilters.weight_min = value;
} else {
delete currentFilters.weight_min;
}
renderTable();
}
// Check phase mass balance
function checkPhaseMassBalance() {
currentFilters.xrf_mass_balance_check = true;
renderTable();
}
// Clear all filters
function clearFilters() {
currentFilters = {};
document.getElementById('rwpMaxInput').value = '';
document.getElementById('weightMinInput').value = '';
renderTable();
}
// Select iteration for plotting
function selectIteration(iterationNumber) {
const fittingSelect = document.getElementById('fittingSelect');
fittingSelect.value = iterationNumber;
currentFitting = iterationNumber;
updatePlot();
}
// Tooltip functions for phase visuals
function showTooltip(event, phase, percentage) {
// Remove any existing tooltip
hideTooltip();
// Extract short form of phase name (first part before underscore)
const shortPhase = phase.split('_')[0];
// Create tooltip element
const tooltip = document.createElement('div');
tooltip.className = 'phase-tooltip';
tooltip.innerHTML = `${shortPhase}<br>${percentage}`;
document.body.appendChild(tooltip);
// Position tooltip relative to mouse position
const rect = event.target.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
let left = rect.right + 8;
let top = rect.top + (rect.height / 2) - (tooltipRect.height / 2);
// Adjust if tooltip would go off screen
if (left + tooltipRect.width > window.innerWidth) {
left = rect.left - tooltipRect.width - 8;
}
if (top < 0) {
top = 8;
}
if (top + tooltipRect.height > window.innerHeight) {
top = window.innerHeight - tooltipRect.height - 8;
}
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
tooltip.style.opacity = '1';
}
function hideTooltip() {
const existingTooltip = document.querySelector('.phase-tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
}
// Initialize the dashboard
document.addEventListener('DOMContentLoaded', async function() {
try {
await loadAllDatasets();
// Check for URL parameters
const urlParams = new URLSearchParams(window.location.search);
const datasetParam = urlParams.get('dataset');
const sampleParam = urlParams.get('sample');
if (datasetParam && sampleParam) {
// Set dataset selection
const datasetSelect = document.getElementById('datasetSelect');
datasetSelect.value = datasetParam;
// Update dataset and load samples
await updateDataset();
// Set sample input
const sampleInput = document.getElementById('sampleInput');
sampleInput.value = sampleParam;
// Load the specific sample
await updateSample();
}
} catch (error) {
console.error('Error initializing dashboard:', error);
alert('Error loading datasets: ' + error.message);
}
});
</script>
</body>
</html>
'''
return html_template