';
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 += `
Gene box color on genomic track
`;
});
html += '
';
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 = '
No query genes available
';
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 = '
No query genes available
';
return;
}
let html = `
${workflowState.geneLabelSize || 8}
Font size for gene labels (4-12, larger = more visible but may overlap)
`;
validGenes.forEach((geneName, index) => {
// Check if this gene is already in the labels list
const isChecked = workflowState.geneLabels.includes(geneName);
html += `
`;
});
html += '
';
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');