plant-msyn / static /js /plot-tweaking.js
Yoshigold's picture
Update webapp with improved logging and SQL search
9078740 verified
/**
* Plot Tweaking Module
*
* Provides advanced plot customization controls for microsynteny visualization.
* Features:
* - Asymmetric window size control (left/right padding in bp)
* - Asymmetric gene count limits (left/right gene counts)
* - Custom genome display names
* - Per-gene color customization for query genes
*/
// Genome display name mappings - Latin/scientific names for formal display
const GENOME_NAMES = {
'actinidia_chinensis': 'Actinidia chinensis',
'aegilops_tauschii': 'Aegilops tauschii',
'arabidopsis_thaliana': 'Arabidopsis thaliana',
'arachis_hypogaea': 'Arachis hypogaea',
'brachypodium_distachyon': 'Brachypodium distachyon',
'eucalyptus_grandis': 'Eucalyptus grandis',
'fragaria_vesca': 'Fragaria vesca',
'glycine_max': 'Glycine max',
'gossypium_hirsutum': 'Gossypium hirsutum',
'hordeum_vulgare': 'Hordeum vulgare',
'lactuca_sativa': 'Lactuca sativa',
'lolium_perenne': 'Lolium perenne',
'manihot_esculenta': 'Manihot esculenta',
'miscanthus_sinensis': 'Miscanthus sinensis',
'oryza_barthii': 'Oryza barthii',
'oryza_rufipogon': 'Oryza rufipogon',
'oryza_sativa': 'Oryza sativa',
'panicum_virgatum': 'Panicum virgatum',
'phaseolus_vulgaris': 'Phaseolus vulgaris',
'populus_trichocarpa': 'Populus trichocarpa',
'prunus_dulcis': 'Prunus dulcis',
'prunus_persica': 'Prunus persica',
'setaria_italica': 'Setaria italica',
'solanum_lycopersicum': 'Solanum lycopersicum',
'solanum_tuberosum': 'Solanum tuberosum',
'sorghum_bicolor': 'Sorghum bicolor',
'thinopyrum_intermedium': 'Thinopyrum intermedium',
'triticum_aestivum': 'Triticum aestivum',
'triticum_dicoccoides': 'Triticum dicoccoides',
'triticum_timopheevii': 'Triticum timopheevii',
'triticum_urartu': 'Triticum urartu',
'vigna_radiata': 'Vigna radiata',
'vigna_unguiculata': 'Vigna unguiculata',
'vitis_vinifera': 'Vitis vinifera'
};
const PlotTweaking = {
// State storage for each workflow (user, custom, csynteny)
state: {
user: {
paddingConfig: {}, // {genome: {left: bp, right: bp}}
maxGenesConfig: {}, // {genome: {left: count, right: count}}
displayNames: {}, // {genome: "Custom Name"}
geneColors: {}, // {geneId: "colorName"}
geneLabels: [], // Array of gene IDs to show labels for
geneLabelSize: 8, // Font size for gene labels (4-12 recommended)
keepLowconfColor: false // If true, color all syntenic matches (not just high-confidence)
},
custom: {
paddingConfig: {},
maxGenesConfig: {},
displayNames: {},
geneColors: {},
geneLabels: [],
geneLabelSize: 8,
keepLowconfColor: false
},
csynteny: {
paddingConfig: {},
maxGenesConfig: {},
displayNames: {},
geneColors: {},
geneLabels: [],
geneLabelSize: 8,
keepLowconfColor: false
}
},
// Default values
defaults: {
queryPadding: 500000, // 500kb
queryMaxGenes: 15, // 15 genes per side
compPadding: 1500000, // 1.5Mb
compMaxGenes: 200 // 200 genes per side
},
/**
* Initialize tweaking panel after a plot is generated
*/
initialize(workflow, queryGenome, compGenomes, genes) {
console.log(`[PlotTweaking] Initializing ${workflow} with:`, {
queryGenome,
compGenomes,
genes
});
try {
const workflowState = this.state[workflow];
// Reset state for this workflow
workflowState.paddingConfig = {};
workflowState.maxGenesConfig = {};
workflowState.displayNames = {};
workflowState.geneColors = {};
workflowState.geneLabels = [];
workflowState.geneLabelSize = 8;
// Store metadata
workflowState.queryGenome = queryGenome;
workflowState.compGenomes = compGenomes || [];
workflowState.genes = genes || [];
console.log(`[PlotTweaking] State initialized:`, workflowState);
// Populate UI
this.populateWindowControls(workflow);
this.populateGenomeLabels(workflow);
this.populateGeneHighlighting(workflow);
this.populateGeneLabelsOnPlot(workflow);
console.log(`[PlotTweaking] UI populated for ${workflow}`);
} catch (error) {
console.error(`[PlotTweaking] Error initializing ${workflow}:`, error);
// Show error in UI
const windowContainer = document.getElementById(`${workflow}-window-controls`);
if (windowContainer) {
windowContainer.innerHTML = `<p class="error-message" style="color: red;">Error loading controls: ${error.message}</p>`;
}
}
},
/**
* Toggle tweaking panel visibility
*/
togglePanel(workflow) {
const body = document.getElementById(`${workflow}-tweaking-body`);
const toggle = document.getElementById(`${workflow}-tweaking-toggle`);
console.log(`[PlotTweaking] togglePanel called for ${workflow}`, {body: !!body, toggle: !!toggle});
if (body.style.display === 'none') {
body.style.display = 'block';
toggle.textContent = '▲';
// Re-populate content when opening (in case it was empty)
const workflowState = this.state[workflow];
console.log(`[PlotTweaking] Panel opened, state:`, workflowState);
if (workflowState && workflowState.queryGenome) {
this.populateWindowControls(workflow);
this.populateGenomeLabels(workflow);
this.populateGeneHighlighting(workflow);
this.populateGeneLabelsOnPlot(workflow);
} else {
console.log(`[PlotTweaking] No state data yet for ${workflow}`);
}
} else {
body.style.display = 'none';
toggle.textContent = '▼';
}
},
/**
* Populate window size controls (padding and gene counts)
*/
populateWindowControls(workflow) {
const container = document.getElementById(`${workflow}-window-controls`);
const workflowState = this.state[workflow];
const queryGenome = workflowState.queryGenome;
const compGenomes = workflowState.compGenomes;
console.log(`[PlotTweaking] Populating window controls for ${workflow}:`, {
queryGenome,
compGenomes,
container: !!container
});
if (!container) {
console.error(`[PlotTweaking] Container not found: ${workflow}-window-controls`);
return;
}
if (!queryGenome) {
container.innerHTML = '<p class="empty-message">No genome data available</p>';
return;
}
let html = '';
// Query genome controls
html += this.renderGenomeWindowControl(workflow, queryGenome, 'Query', true);
// Comparison genome controls
compGenomes.forEach(genome => {
const displayName = GENOME_NAMES[genome] || genome;
html += this.renderGenomeWindowControl(workflow, genome, displayName, false);
});
container.innerHTML = html;
console.log(`[PlotTweaking] Window controls populated with ${1 + compGenomes.length} genomes`);
},
/**
* Render window control UI for a single genome
*/
renderGenomeWindowControl(workflow, genome, displayName, isQuery) {
const workflowState = this.state[workflow];
const defaultPadding = isQuery ? this.defaults.queryPadding : this.defaults.compPadding;
const defaultGenes = isQuery ? this.defaults.queryMaxGenes : this.defaults.compMaxGenes;
// Get current values or defaults
const currentPadding = workflowState.paddingConfig[genome] || {left: defaultPadding, right: defaultPadding};
const currentGenes = workflowState.maxGenesConfig[genome] || {left: defaultGenes, right: defaultGenes};
// Calculate Mb values for display
const leftMb = (currentPadding.left / 1000000).toFixed(2);
const rightMb = (currentPadding.right / 1000000).toFixed(2);
return `
<div class="genome-window-control" style="margin-bottom: 1.5rem; padding: 1rem; background: var(--color-bg-tertiary); border-radius: var(--radius-sm); border-left: 4px solid var(--color-accent-primary);">
<h5 style="margin-bottom: 0.75rem; color: var(--color-text); font-weight: 600;">${displayName}</h5>
<!-- Padding Controls -->
<div class="control-group" style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Window Padding (Mb)</label>
<div style="display: grid; grid-template-columns: 1fr auto auto 1fr; gap: 0.5rem; align-items: center;">
<div>
<label style="font-size: 0.85em; color: var(--color-text-muted);">← Left</label>
<input type="number"
id="${workflow}-${genome}-padding-left"
value="${leftMb}"
min="0"
max="10"
step="0.25"
style="width: 100%; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm);"
onchange="PlotTweaking.updatePadding('${workflow}', '${genome}', 'left', this.value)">
</div>
<div style="text-align: center; padding-top: 1.5rem;">
<button class="btn btn-sm btn-secondary" onclick="PlotTweaking.decreasePadding('${workflow}', '${genome}')" title="Decrease both sides by 25%">
⬇️-25%
</button>
</div>
<div style="text-align: center; padding-top: 1.5rem;">
<button class="btn btn-sm btn-secondary" onclick="PlotTweaking.increasePadding('${workflow}', '${genome}')" title="Increase both sides by 25%">
⬆️+25%
</button>
</div>
<div>
<label style="font-size: 0.85em; color: var(--color-text-muted);">Right →</label>
<input type="number"
id="${workflow}-${genome}-padding-right"
value="${rightMb}"
min="0"
max="10"
step="0.25"
style="width: 100%; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm);"
onchange="PlotTweaking.updatePadding('${workflow}', '${genome}', 'right', this.value)">
</div>
</div>
<p class="form-help" style="margin-top: 0.25rem; font-size: 0.8em;">Current: ±${leftMb}Mb left, ±${rightMb}Mb right</p>
</div>
<!-- Gene Count Controls -->
<div class="control-group">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Max Flanking Genes</label>
<div style="display: grid; grid-template-columns: 1fr auto auto 1fr; gap: 0.5rem; align-items: center;">
<div>
<label style="font-size: 0.85em; color: var(--color-text-muted);">← Left</label>
<input type="number"
id="${workflow}-${genome}-genes-left"
value="${currentGenes.left}"
min="0"
max="200"
step="5"
style="width: 100%; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm);"
onchange="PlotTweaking.updateMaxGenes('${workflow}', '${genome}', 'left', this.value)">
</div>
<div style="text-align: center; padding-top: 1.5rem;">
<button class="btn btn-sm btn-secondary" onclick="PlotTweaking.decreaseGeneCount('${workflow}', '${genome}')" title="Decrease both sides by 10 genes">
⬇️-10
</button>
</div>
<div style="text-align: center; padding-top: 1.5rem;">
<button class="btn btn-sm btn-secondary" onclick="PlotTweaking.increaseGeneCount('${workflow}', '${genome}')" title="Increase both sides by 10 genes">
⬆️+10
</button>
</div>
<div>
<label style="font-size: 0.85em; color: var(--color-text-muted);">Right →</label>
<input type="number"
id="${workflow}-${genome}-genes-right"
value="${currentGenes.right}"
min="0"
max="200"
step="5"
style="width: 100%; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm);"
onchange="PlotTweaking.updateMaxGenes('${workflow}', '${genome}', 'right', this.value)">
</div>
</div>
<p class="form-help" style="margin-top: 0.25rem; font-size: 0.8em;">Current: ${currentGenes.left} genes left, ${currentGenes.right} genes right</p>
</div>
</div>
`;
},
/**
* Populate genome label customization controls
*/
populateGenomeLabels(workflow) {
const container = document.getElementById(`${workflow}-genome-labels`);
const workflowState = this.state[workflow];
const queryGenome = workflowState.queryGenome;
const compGenomes = workflowState.compGenomes;
console.log(`[PlotTweaking] Populating genome labels for ${workflow}:`, {
queryGenome,
compGenomes,
container: !!container
});
if (!container) {
console.error(`[PlotTweaking] Container not found: ${workflow}-genome-labels`);
return;
}
if (!queryGenome) {
container.innerHTML = '<p class="empty-message">No genome data available</p>';
return;
}
let html = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">';
// Query genome label
const queryDisplayName = GENOME_NAMES[queryGenome] || queryGenome;
html += this.renderLabelControl(workflow, queryGenome, 'Query', queryDisplayName);
// Comparison genome labels
compGenomes.forEach(genome => {
const displayName = GENOME_NAMES[genome] || genome;
html += this.renderLabelControl(workflow, genome, displayName, displayName);
});
html += '</div>';
container.innerHTML = html;
console.log(`[PlotTweaking] Genome labels populated`);
},
/**
* Render label control for a single genome
*/
renderLabelControl(workflow, genome, currentLabel, defaultLabel) {
const workflowState = this.state[workflow];
const customLabel = workflowState.displayNames[genome] || '';
return `
<div class="label-control" style="padding: 0.75rem; background: var(--color-bg-tertiary); border-radius: var(--radius-sm);">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em;">${currentLabel}</label>
<input type="text"
id="${workflow}-${genome}-label"
value="${customLabel}"
placeholder="${defaultLabel}"
style="width: 100%; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm);"
onchange="PlotTweaking.updateDisplayName('${workflow}', '${genome}', this.value)">
<p class="form-help" style="margin-top: 0.25rem; font-size: 0.75em;">Leave empty for default</p>
</div>
`;
},
/**
* Populate gene highlighting controls (colors for gene boxes)
*/
populateGeneHighlighting(workflow) {
const container = document.getElementById(`${workflow}-gene-highlighting`);
const workflowState = this.state[workflow];
let genes = workflowState.genes;
console.log(`[PlotTweaking] Populating gene highlighting for ${workflow}:`, {
genes,
container: !!container
});
if (!container) {
console.error(`[PlotTweaking] Container not found: ${workflow}-gene-highlighting`);
return;
}
if (!genes || genes.length === 0) {
container.innerHTML = '<p class="empty-message">No query genes available</p>';
return;
}
// Filter to only genes with actual names (not empty entries)
// Also normalize to get the gene name strings
const validGenes = genes.map(gene => {
if (typeof gene === 'string') {
return gene.trim();
} else if (gene && typeof gene === 'object' && gene.name) {
return gene.name.trim();
}
return '';
}).filter(name => name.length > 0);
if (validGenes.length === 0) {
container.innerHTML = '<p class="empty-message">No query genes available</p>';
return;
}
// Available colors for gene boxes
const colorOptions = [
{value: 'red', label: '🔴 Red', hex: '#ff0000'},
{value: 'blue', label: '🔵 Blue', hex: '#0000ff'},
{value: 'green', label: '🟢 Green', hex: '#00ff00'},
{value: 'orange', label: '🟠 Orange', hex: '#ffa500'},
{value: 'purple', label: '🟣 Purple', hex: '#800080'},
{value: 'cyan', label: '🔵 Cyan', hex: '#00ffff'},
{value: 'magenta', label: '🟣 Magenta', hex: '#ff00ff'},
{value: 'brown', label: '🟤 Brown', hex: '#a52a2a'},
{value: 'pink', label: '🩷 Pink', hex: '#ffc0cb'},
{value: 'olive', label: '🫒 Olive', hex: '#808000'}
];
let html = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem;">';
validGenes.forEach((geneName, index) => {
// Get current color: check stored state, then check original gene object color, then use default
let currentColor = workflowState.geneColors[geneName];
if (!currentColor) {
// Check if original gene object had a color property
const originalGene = genes.find(g => (typeof g === 'object' && g.name === geneName));
if (originalGene && originalGene.color) {
// Map single-letter colors to full names
const colorMap = {'r': 'red', 'b': 'blue', 'g': 'green', 'o': 'orange', 'p': 'purple', 'c': 'cyan', 'm': 'magenta'};
currentColor = colorMap[originalGene.color] || originalGene.color;
} else {
currentColor = colorOptions[index % colorOptions.length].value;
}
}
html += `
<div class="gene-color-control" style="padding: 0.75rem; background: var(--color-bg-tertiary); border-radius: var(--radius-sm);">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9em;">${geneName}</label>
<select id="${workflow}-gene-color-${index}"
style="width: 100%; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm);"
onchange="PlotTweaking.updateGeneColor('${workflow}', '${geneName}', this.value)">
${colorOptions.map(opt => `
<option value="${opt.value}" ${currentColor === opt.value ? 'selected' : ''}>${opt.label}</option>
`).join('')}
</select>
<p class="form-help" style="margin-top: 0.25rem; font-size: 0.75em;">Gene box color on genomic track</p>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
},
/**
* Populate gene labels on plot section (checkboxes to select which genes show labels)
*/
populateGeneLabelsOnPlot(workflow) {
const container = document.getElementById(`${workflow}-gene-labels`);
const workflowState = this.state[workflow];
const genes = workflowState.genes;
console.log(`[PlotTweaking] Populating gene labels for ${workflow}:`, {genes});
if (!container) {
console.error(`[PlotTweaking] Container not found: ${workflow}-gene-labels`);
return;
}
if (!genes || genes.length === 0) {
container.innerHTML = '<p class="empty-message">No query genes available</p>';
return;
}
// Filter to only genes with actual names
const validGenes = genes.map(gene => {
if (typeof gene === 'string') {
return gene.trim();
} else if (gene && typeof gene === 'object' && gene.name) {
return gene.name.trim();
}
return '';
}).filter(name => name.length > 0);
if (validGenes.length === 0) {
container.innerHTML = '<p class="empty-message">No query genes available</p>';
return;
}
let html = `
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Label Font Size</label>
<div style="display: flex; align-items: center; gap: 1rem;">
<input type="range" id="${workflow}-gene-label-size"
min="4" max="12" value="${workflowState.geneLabelSize || 8}"
style="flex: 1; max-width: 200px;"
onchange="PlotTweaking.updateGeneLabelSize('${workflow}', this.value)">
<span id="${workflow}-gene-label-size-display" style="min-width: 2rem; text-align: center;">${workflowState.geneLabelSize || 8}</span>
</div>
<p class="form-help" style="margin-top: 0.25rem; font-size: 0.75em;">Font size for gene labels (4-12, larger = more visible but may overlap)</p>
</div>
<div style="margin-bottom: 0.5rem;">
<label class="checkbox-label" style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; margin-bottom: 0.5rem;">
<input type="checkbox" id="${workflow}-gene-label-all"
onchange="PlotTweaking.toggleAllGeneLabels('${workflow}', this.checked)">
<strong>Select All / Deselect All</strong>
</label>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 0.5rem;">
`;
validGenes.forEach((geneName, index) => {
// Check if this gene is already in the labels list
const isChecked = workflowState.geneLabels.includes(geneName);
html += `
<label class="checkbox-label" style="display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; background: var(--color-bg-tertiary); border-radius: var(--radius-sm); cursor: pointer;">
<input type="checkbox" id="${workflow}-gene-label-${index}"
data-gene="${geneName}"
${isChecked ? 'checked' : ''}
onchange="PlotTweaking.updateGeneLabel('${workflow}', '${geneName}', this.checked)">
<span style="font-size: 0.9em; word-break: break-all;">${geneName}</span>
</label>
`;
});
html += '</div>';
container.innerHTML = html;
},
/**
* Update gene label size
*/
updateGeneLabelSize(workflow, size) {
const workflowState = this.state[workflow];
workflowState.geneLabelSize = parseInt(size);
// Update display
const display = document.getElementById(`${workflow}-gene-label-size-display`);
if (display) {
display.textContent = size;
}
console.log(`[PlotTweaking] Updated gene label size for ${workflow} to ${size}`);
},
/**
* Update gene label checkbox
*/
updateGeneLabel(workflow, geneName, isChecked) {
const workflowState = this.state[workflow];
if (isChecked) {
if (!workflowState.geneLabels.includes(geneName)) {
workflowState.geneLabels.push(geneName);
}
} else {
workflowState.geneLabels = workflowState.geneLabels.filter(g => g !== geneName);
}
console.log(`[PlotTweaking] Updated gene labels for ${workflow}:`, workflowState.geneLabels);
},
/**
* Toggle all gene labels on/off
*/
toggleAllGeneLabels(workflow, selectAll) {
const workflowState = this.state[workflow];
const genes = workflowState.genes;
// Get valid gene names
const validGenes = genes.map(gene => {
if (typeof gene === 'string') {
return gene.trim();
} else if (gene && typeof gene === 'object' && gene.name) {
return gene.name.trim();
}
return '';
}).filter(name => name.length > 0);
if (selectAll) {
workflowState.geneLabels = [...validGenes];
} else {
workflowState.geneLabels = [];
}
// Update all checkboxes
validGenes.forEach((geneName, index) => {
const checkbox = document.getElementById(`${workflow}-gene-label-${index}`);
if (checkbox) {
checkbox.checked = selectAll;
}
});
console.log(`[PlotTweaking] Toggled all gene labels for ${workflow} to ${selectAll}:`, workflowState.geneLabels);
},
/**
* Get gene labels for API request
* Returns array of gene IDs that should have labels displayed
*/
getGeneLabels(workflow) {
const workflowState = this.state[workflow];
// Read current values from DOM (in case user changed them)
const genes = workflowState.genes || [];
const labels = [];
// Get valid gene names
const validGenes = genes.map(gene => {
if (typeof gene === 'string') {
return gene.trim();
} else if (gene && typeof gene === 'object' && gene.name) {
return gene.name.trim();
}
return '';
}).filter(name => name.length > 0);
// Check each checkbox
validGenes.forEach((geneName, index) => {
const checkbox = document.getElementById(`${workflow}-gene-label-${index}`);
if (checkbox && checkbox.checked) {
labels.push(geneName);
}
});
return labels;
},
/**
* Get gene label size for API request
*/
getGeneLabelSize(workflow) {
const sizeInput = document.getElementById(`${workflow}-gene-label-size`);
if (sizeInput) {
return parseInt(sizeInput.value) || 4;
}
return this.state[workflow].geneLabelSize || 4;
},
/**
* Update padding value
*/
updatePadding(workflow, genome, side, valueMb) {
const workflowState = this.state[workflow];
if (!workflowState.paddingConfig[genome]) {
const defaultPadding = genome === workflowState.queryGenome ? this.defaults.queryPadding : this.defaults.compPadding;
workflowState.paddingConfig[genome] = {left: defaultPadding, right: defaultPadding};
}
// Convert Mb to bp
workflowState.paddingConfig[genome][side] = Math.round(parseFloat(valueMb) * 1000000);
console.log(`Updated ${genome} ${side} padding to ${valueMb}Mb (${workflowState.paddingConfig[genome][side]}bp)`);
},
/**
* Increase padding by 25% on both sides
*/
increasePadding(workflow, genome) {
const workflowState = this.state[workflow];
const defaultPadding = genome === workflowState.queryGenome ? this.defaults.queryPadding : this.defaults.compPadding;
if (!workflowState.paddingConfig[genome]) {
workflowState.paddingConfig[genome] = {left: defaultPadding, right: defaultPadding};
}
const current = workflowState.paddingConfig[genome];
current.left = Math.round(current.left * 1.25);
current.right = Math.round(current.right * 1.25);
// Update UI
document.getElementById(`${workflow}-${genome}-padding-left`).value = (current.left / 1000000).toFixed(2);
document.getElementById(`${workflow}-${genome}-padding-right`).value = (current.right / 1000000).toFixed(2);
console.log(`Increased ${genome} padding by 25%: ${current.left}bp left, ${current.right}bp right`);
},
/**
* Decrease padding by 25% on both sides
*/
decreasePadding(workflow, genome) {
const workflowState = this.state[workflow];
const defaultPadding = genome === workflowState.queryGenome ? this.defaults.queryPadding : this.defaults.compPadding;
if (!workflowState.paddingConfig[genome]) {
workflowState.paddingConfig[genome] = {left: defaultPadding, right: defaultPadding};
}
const current = workflowState.paddingConfig[genome];
// Decrease by 25% (multiply by 0.75), with minimum of 100000 (0.1 Mb)
current.left = Math.max(Math.round(current.left * 0.75), 100000);
current.right = Math.max(Math.round(current.right * 0.75), 100000);
// Update UI
document.getElementById(`${workflow}-${genome}-padding-left`).value = (current.left / 1000000).toFixed(2);
document.getElementById(`${workflow}-${genome}-padding-right`).value = (current.right / 1000000).toFixed(2);
console.log(`Decreased ${genome} padding by 25%: ${current.left}bp left, ${current.right}bp right`);
},
/**
* Update max genes value
*/
updateMaxGenes(workflow, genome, side, value) {
const workflowState = this.state[workflow];
if (!workflowState.maxGenesConfig[genome]) {
const defaultGenes = genome === workflowState.queryGenome ? this.defaults.queryMaxGenes : this.defaults.compMaxGenes;
workflowState.maxGenesConfig[genome] = {left: defaultGenes, right: defaultGenes};
}
workflowState.maxGenesConfig[genome][side] = parseInt(value);
console.log(`Updated ${genome} ${side} max genes to ${value}`);
},
/**
* Increase gene count by 10 on both sides
*/
increaseGeneCount(workflow, genome) {
const workflowState = this.state[workflow];
const defaultGenes = genome === workflowState.queryGenome ? this.defaults.queryMaxGenes : this.defaults.compMaxGenes;
if (!workflowState.maxGenesConfig[genome]) {
workflowState.maxGenesConfig[genome] = {left: defaultGenes, right: defaultGenes};
}
const current = workflowState.maxGenesConfig[genome];
current.left = Math.min(current.left + 10, 200);
current.right = Math.min(current.right + 10, 200);
// Update UI
document.getElementById(`${workflow}-${genome}-genes-left`).value = current.left;
document.getElementById(`${workflow}-${genome}-genes-right`).value = current.right;
console.log(`Increased ${genome} gene count by 10: ${current.left} left, ${current.right} right`);
},
/**
* Decrease gene count by 10 on both sides
*/
decreaseGeneCount(workflow, genome) {
const workflowState = this.state[workflow];
const defaultGenes = genome === workflowState.queryGenome ? this.defaults.queryMaxGenes : this.defaults.compMaxGenes;
if (!workflowState.maxGenesConfig[genome]) {
workflowState.maxGenesConfig[genome] = {left: defaultGenes, right: defaultGenes};
}
const current = workflowState.maxGenesConfig[genome];
// Decrease by 10, with minimum of 5 genes
current.left = Math.max(current.left - 10, 5);
current.right = Math.max(current.right - 10, 5);
// Update UI
document.getElementById(`${workflow}-${genome}-genes-left`).value = current.left;
document.getElementById(`${workflow}-${genome}-genes-right`).value = current.right;
console.log(`Decreased ${genome} gene count by 10: ${current.left} left, ${current.right} right`);
},
/**
* Update display name
*/
updateDisplayName(workflow, genome, name) {
const workflowState = this.state[workflow];
const MAX_NAME_LENGTH = 100; // Max chars for names (must match backend)
if (name && name.trim()) {
const trimmedName = name.trim();
// Truncate if too long (with visual feedback)
if (trimmedName.length > MAX_NAME_LENGTH) {
workflowState.displayNames[genome] = trimmedName.substring(0, MAX_NAME_LENGTH);
console.warn(`Display name truncated to ${MAX_NAME_LENGTH} characters`);
} else {
workflowState.displayNames[genome] = trimmedName;
}
} else {
delete workflowState.displayNames[genome];
}
console.log(`Updated ${genome} display name to: ${name || '(default)'}`);
},
/**
* Update gene color
*/
updateGeneColor(workflow, geneId, color) {
const workflowState = this.state[workflow];
workflowState.geneColors[geneId] = color;
console.log(`Updated ${geneId} color to: ${color}`);
},
/**
* Reset all tweaking options to defaults
*/
resetToDefaults(workflow) {
const workflowState = this.state[workflow];
// Clear state
workflowState.paddingConfig = {};
workflowState.maxGenesConfig = {};
workflowState.displayNames = {};
workflowState.geneColors = {};
// Repopulate UI with defaults
this.populateWindowControls(workflow);
this.populateGenomeLabels(workflow);
this.populateGeneHighlighting(workflow);
console.log(`Reset ${workflow} tweaking options to defaults`);
},
/**
* Regenerate plot with current tweaking settings
*/
async regeneratePlot(workflow) {
const workflowState = this.state[workflow];
console.log('[PlotTweaking] Regenerating plot for workflow:', workflow);
console.log('[PlotTweaking] Current state:', workflowState);
// Get tweaking config to show what will be sent
const tweakingConfig = this.getTweakingConfig(workflow);
console.log('[PlotTweaking] Tweaking config to be sent:', tweakingConfig);
// Get gene colors to show what will be sent
const geneColors = this.getGeneColors(workflow);
console.log('[PlotTweaking] Gene colors to be sent:', geneColors);
try {
// Get the appropriate generate function based on workflow
if (workflow === 'user') {
// Call the Handlers function with true to indicate regeneration
console.log('[PlotTweaking] Calling Handlers.onUserGeneratePlot(true)');
await Handlers.onUserGeneratePlot(true);
} else if (workflow === 'custom') {
console.log('[PlotTweaking] Calling Handlers.onCustomGeneratePlot(true)');
await Handlers.onCustomGeneratePlot(true);
} else if (workflow === 'csynteny') {
console.log('[PlotTweaking] Calling CustomSyntenyHandlers.onGeneratePlot(true)');
await CustomSyntenyHandlers.onGeneratePlot(true);
}
} catch (error) {
console.error('[PlotTweaking] Error regenerating plot:', error);
alert('Error regenerating plot: ' + error.message);
}
},
/**
* Get tweaking config for API request
*/
getTweakingConfig(workflow) {
const workflowState = this.state[workflow];
// Read current values from DOM inputs (not just stored state)
const paddingConfig = {};
const maxGenesConfig = {};
const displayNames = {};
// Collect values for all genomes (query + comparisons)
const allGenomes = [workflowState.queryGenome, ...(workflowState.compGenomes || [])];
for (const genome of allGenomes) {
if (!genome) continue;
// Read padding values from DOM
const paddingLeftInput = document.getElementById(`${workflow}-${genome}-padding-left`);
const paddingRightInput = document.getElementById(`${workflow}-${genome}-padding-right`);
if (paddingLeftInput && paddingRightInput) {
paddingConfig[genome] = {
left: Math.round(parseFloat(paddingLeftInput.value) * 1000000),
right: Math.round(parseFloat(paddingRightInput.value) * 1000000)
};
}
// Read max genes values from DOM
const genesLeftInput = document.getElementById(`${workflow}-${genome}-genes-left`);
const genesRightInput = document.getElementById(`${workflow}-${genome}-genes-right`);
if (genesLeftInput && genesRightInput) {
maxGenesConfig[genome] = {
left: parseInt(genesLeftInput.value),
right: parseInt(genesRightInput.value)
};
}
// Read display name from DOM (input ID is -label not -display-name)
const displayNameInput = document.getElementById(`${workflow}-${genome}-label`);
if (displayNameInput && displayNameInput.value.trim()) {
displayNames[genome] = displayNameInput.value.trim();
}
}
console.log('[PlotTweaking] getTweakingConfig result:', {
padding_config: paddingConfig,
max_genes_config: maxGenesConfig,
display_names: displayNames
});
// Also get gene labels data
const geneLabels = this.getGeneLabels(workflow);
const geneLabelSize = this.getGeneLabelSize(workflow);
// Read keep low-confidence color checkbox
const keepLowconfCheckbox = document.getElementById(`${workflow}-keep-lowconf-color`);
const keepLowconfColor = keepLowconfCheckbox ? keepLowconfCheckbox.checked : false;
return {
padding_config: Object.keys(paddingConfig).length > 0 ? paddingConfig : undefined,
max_genes_config: Object.keys(maxGenesConfig).length > 0 ? maxGenesConfig : undefined,
display_names: Object.keys(displayNames).length > 0 ? displayNames : undefined,
gene_labels: geneLabels.length > 0 ? geneLabels : undefined,
gene_label_size: geneLabels.length > 0 ? geneLabelSize : undefined,
keep_lowconf_color: keepLowconfColor
};
},
/**
* Get gene colors for API request (as array matching gene order)
* Reads directly from DOM to capture all changes
* Only returns colors for genes with actual names (not empty entries)
*/
getGeneColors(workflow) {
const workflowState = this.state[workflow];
const genes = workflowState.genes;
if (!genes || genes.length === 0) return [];
// Map single-letter colors to full names (for colors from Step 2)
const colorMap = {'r': 'red', 'b': 'blue', 'g': 'green', 'o': 'orange', 'p': 'purple', 'c': 'cyan', 'm': 'magenta'};
// Available colors for gene boxes (defaults)
const colorOptions = ['red', 'blue', 'green', 'orange', 'purple', 'cyan', 'magenta', 'brown', 'pink', 'olive'];
// Filter to only genes with actual names and get their colors
const colors = [];
let validIndex = 0;
genes.forEach((gene, originalIndex) => {
// Extract gene name and original color (handle both string and object formats)
let geneName = '';
let originalColor = '';
if (typeof gene === 'string') {
geneName = gene.trim();
} else if (gene && typeof gene === 'object') {
geneName = (gene.name || '').trim();
// Get the color from the gene object (this is the Step 2 color)
if (gene.color) {
originalColor = colorMap[gene.color] || gene.color;
}
}
// Skip empty genes
if (!geneName) return;
// Try to read from DOM select element (uses validIndex, not originalIndex)
const selectEl = document.getElementById(`${workflow}-gene-color-${validIndex}`);
if (selectEl) {
colors.push(selectEl.value);
} else {
// Fallback: use original color from gene object (Step 2), then stored state, then default
colors.push(originalColor || workflowState.geneColors[geneName] || colorOptions[validIndex % colorOptions.length]);
}
validIndex++;
});
return colors;
}
};
// Log that the module loaded successfully
console.log('[PlotTweaking] Module loaded successfully');