/** * 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 = `

Error loading controls: ${error.message}

`; } } }, /** * 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 = '

No genome data available

'; 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 `
${displayName}

Current: ±${leftMb}Mb left, ±${rightMb}Mb right

Current: ${currentGenes.left} genes left, ${currentGenes.right} genes right

`; }, /** * 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 = '

No genome data available

'; return; } let html = '
'; // 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 += '
'; 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 `

Leave empty for default

`; }, /** * 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 = '

No query genes available

'; 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 = '

No query genes available

'; 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 = '
'; 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');