Spaces:
Sleeping
Sleeping
| /** | |
| * 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'); | |