413a24fc542e90894efde6c3d73f84d85761e1ad angie Thu Jun 4 22:40:42 2026 -0700 @FedeGueli request: also highlight mutations unique to the recombinant diff --git src/hg/js/hgPhyloPlace.js src/hg/js/hgPhyloPlace.js index 853e421b210..7493fde37b4 100644 --- src/hg/js/hgPhyloPlace.js +++ src/hg/js/hgPhyloPlace.js @@ -1,720 +1,729 @@ // hgPhyloPlace.js - Javascript for use in hgPhyloPlace CGI // Copyright (C) 2026 The Regents of the University of California // Don't complain about line break before '||' etc: /* jshint -W014 */ /* jshint esversion: 8 */ /* globals $, window */ ///////////////////////////////////////////////////////////////////////////////////////////// //// recombinantGraph: draw a RIVET-like SVG image of a recombinant and its parent nodes //// ///////////////////////////////////////////////////////////////////////////////////////////// var recombinantGraph = (function () { "use strict"; // Parameters/configuration: most will be dynamically computed from font size var cfg = { matchesAcceptor: '#40C0E0', matchesDonor: '#E86030', + matchesNeither: '#505050', uninformative: '#D0D0D0' }; const baseColorsInformative = { 'A': '#B02818', 'C': '#601C68', 'G': '#BC8040', 'T': '#488040', 'ref': '#000000' }; const baseColorsNonInformative = { 'A': '#BE7A72', 'C': '#96749A', 'G': '#C4A686', 'T': '#8AA686', 'ref': '#CCCCCC' }; function configLayout(fontSize) { // Given font size as integer, compute all layout parameters if (! fontSize || ! Number.isInteger(fontSize)) { fontSize = 8; } cfg.fontSize = fontSize; cfg.titleFontSize = cfg.fontSize + 2; cfg.titleLineHeight = cfg.fontSize * 1.25; cfg.titleAreaHeight = cfg.titleLineHeight * 7; cfg.minBaseAreaWidth = cfg.titleFontSize * 75; cfg.leftLabelAreaWidth = cfg.fontSize * 20; cfg.posLabelAreaHeight = cfg.fontSize * 5; cfg.connectorAreaHeight = cfg.fontSize * 12; cfg.genomeGraphHeight = cfg.fontSize * 2; cfg.geneGraphHeight = cfg.fontSize * 2; cfg.tickHeight = cfg.fontSize / 2; cfg.rightPad = cfg.fontSize / 2; cfg.topPad = cfg.fontSize / 2; cfg.baseTextPad = cfg.fontSize / 4; cfg.baseWidth = cfg.fontSize * 1.25; cfg.interbaseWidth = cfg.fontSize / 4; cfg.basePitch = cfg.baseWidth + cfg.interbaseWidth; cfg.baseArrayHeight = 4 * cfg.basePitch - cfg.interbaseWidth; cfg.genomeScaleHeight = cfg.topPad * 3 + cfg.baseWidth; } function makeSvg(width, height) { // Create and return an SVG of the given size. var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", width); svg.setAttribute("height", height); svg.setAttribute("viewBox", "0 0 " + width + " " + height); return svg; } function svgCreateEl(type, config) { // Helper function for creating a new SVG element and initializing its // properties and attributes. Type is something like 'rect', 'text', 'g', etc; // config is an object like { id: 'newThingie', x: 0, y: 10, title: 'blah blah' }. // Copied from hgGateway.js. var svgns = 'http://www.w3.org/2000/svg'; var xlinkns = 'http://www.w3.org/1999/xlink'; var el = document.createElementNS(svgns, type); var title, titleEl; if (el) { for (const [setting, value] of Object.entries(config)) { if (setting === 'textContent') { // Text content (the text in a text element or title element) is a property: el.textContent = value; } else if (setting === 'href') { // href comes from a different namespace so must use setAttributeNS: el.setAttributeNS(xlinkns, 'href', value); } else if (setting === 'title') { title = value; } else if (setting === 'className') { el.setAttribute('class', value); } else { // Most of the time we're just setting an attribute: el.setAttribute(setting, value); } } } // Mouseover title actually requires creating a child element. // Strangely, if I did this in the above loop, the child element was lost if // props/attributes were set afterwards!! So save title for last. if (title) { titleEl = document.createElementNS(svgns, 'title'); titleEl.textContent = title; el.appendChild(titleEl); } return el; } function addRectFill(svg, x, y, width, height, color, config={}) { // Add filled rectangle to svg config = Object.assign({ x: x, y: y, width: width, height: height, style: 'fill:' + color + '; stroke:' + color }, config); var rect = svgCreateEl('rect', config); svg.appendChild(rect); } function parseMutation(mutStr) { // Parse mutStr into reference base, position and alternate base and return object {pos, ref, alt}. var match = mutStr.match(/^([A-Za-z])(\d+)([A-Za-z])$/); if (!match) { console.error("parseMutation: invalid mutation string: " + mutStr); return null; } return { pos: parseInt(match[2], 10), ref: match[1], alt: match[3] }; } function parseMutations(mutStr) { // Return a sorted list of mutation objects return mutStr.split(",").map(parseMutation).sort(function(a, b) { return a.pos - b.pos; }); } function minPos3(mutA, mutB, mutC) { // Given 3 inputs, each of which might be undefined or null or object with pos, return the minimum pos. // Assume that at least one of the inputs is an object with pos. if (mutA && mutB && mutC) { let minAB = mutA.pos < mutB.pos ? mutA.pos : mutB.pos; return minAB < mutC.pos ? minAB : mutC.pos; } else if (mutA) { if (mutB) { return mutA.pos < mutB.pos ? mutA.pos : mutB.pos; } else if (mutC) { return mutA.pos < mutC.pos ? mutA.pos : mutC.pos; } else { return mutA.pos; } } else if (mutB) { if (mutC) { return mutB.pos < mutC.pos ? mutB.pos : mutC.pos; } else { return mutB.pos; } } else if (mutC) { return mutC.pos; } else { console.error("minPos given all empties!"); return null; } } function makeCombinedMuts(recombMuts, donorMuts, acceptorMuts) { // Return an array of objects encoding combined mutations (or alleles) from the recombinant and parents: // { pos, ref, dAl, rAl, aAl } var combinedMuts = []; var rMuts = parseMutations(recombMuts); var dMuts = parseMutations(donorMuts); var aMuts = parseMutations(acceptorMuts); var dMut = dMuts.shift(); var rMut = rMuts.shift(); var aMut = aMuts.shift(); while (dMut !== undefined || rMut !== undefined || aMut !== undefined) { let minPos = minPos3(dMut, rMut, aMut); if (minPos === null) { break; } let combo = { pos: minPos }; let ref; if (dMut && dMut.pos === minPos) { ref = dMut.ref; combo.dAl = dMut.alt; dMut = dMuts.shift(); } if (rMut && rMut.pos === minPos) { if (ref && ref != rMut.ref) { console.error("Differing ref base for", minPos, ":", ref, "vs", rMut.ref); } ref = rMut.ref; combo.rAl = rMut.alt; rMut = rMuts.shift(); } if (aMut && aMut.pos === minPos) { if (ref && ref != aMut.ref) { console.error("Differing ref base for", minPos, ":", ref, "vs", aMut.ref); } ref = aMut.ref; combo.aAl = aMut.alt; aMut = aMuts.shift(); } combo.ref = ref; if (! combo.dAl) { combo.dAl = ref; } if (! combo.rAl) { combo.rAl = ref; } if (! combo.aAl) { combo.aAl = ref; } combinedMuts.push(combo); } return combinedMuts; } function filterMuts(combinedMuts) { - // Filter combinedMuts to keep only those that are informative to whether the base comes from acceptor or donor. + // Filter combinedMuts to keep only those that are informative to whether the base comes from acceptor or donor (or neither). let filteredMuts = []; for (let column of combinedMuts) { let matchesAcceptor = column.aAl === column.rAl; let matchesDonor = column.dAl === column.rAl; - if (matchesAcceptor !== matchesDonor) { + if (! (matchesAcceptor && matchesDonor)) { filteredMuts.push(column); } } return filteredMuts; } function addText(svg, text, x, y, config) { if (! config) { config = {}; } config.x = x; config.y = y; config.textContent = text; if (! config['font-size']) { config['font-size'] = cfg.fontSize + 'px'; } var el = svgCreateEl('text', config); svg.appendChild(el); } function addTitle(svg, recombAttrs) { // Add some info about the potential recombinant above the graphic let xRight = cfg.leftLabelAreaWidth - cfg.rightPad; let x = cfg.leftLabelAreaWidth + cfg.rightPad; let y = cfg.topPad + cfg.titleFontSize; addText(svg, "Recombinant node:", xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end', 'font-size': cfg.titleFontSize }); addText(svg, recombAttrs.rNode, x, y, { 'font-size': cfg.titleFontSize }); y += cfg.titleLineHeight; addText(svg, "Acceptor node:", xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end', 'font-size': cfg.titleFontSize }); addText(svg, recombAttrs.aNode + ' (' + recombAttrs.aLin + ')', x, y, { 'font-size': cfg.titleFontSize }); y += cfg.titleLineHeight; addText(svg, "Donor node:", xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end', 'font-size': cfg.titleFontSize }); addText(svg, recombAttrs.dNode + ' (' + recombAttrs.dLin + ')', x, y, { 'font-size': cfg.titleFontSize }); y += cfg.titleLineHeight; addText(svg, "Parsimony improvement:", xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end', 'font-size': cfg.titleFontSize }); addText(svg, recombAttrs.improvement, x, y, { 'font-size': cfg.titleFontSize }); y += cfg.titleLineHeight; addText(svg, "Breakpoint range 1:", xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end', 'font-size': cfg.titleFontSize }); addText(svg, recombAttrs.bp1, x, y, { 'font-size': cfg.titleFontSize }); y += cfg.titleLineHeight; addText(svg, "Breakpoint range 2:", xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end', 'font-size': cfg.titleFontSize }); addText(svg, recombAttrs.bp2, x, y, { 'font-size': cfg.titleFontSize }); } - function addLeftLabels(svg, x, xRight, y, recombAttrs) { + function addLeftLabels(svg, x, xRight, y, recombAttrs, showInformativeOnly) { // Base value / node rows y += cfg.posLabelAreaHeight + cfg.baseWidth - cfg.interbaseWidth; addText(svg, 'Acceptor (' + recombAttrs.aLin + ')', xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end', 'fill': cfg.matchesAcceptor }); y += cfg.basePitch; addText(svg, 'Recombinant', xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end' }); y += cfg.basePitch; addText(svg, 'Donor (' + recombAttrs.dLin + ')', xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end', 'fill': cfg.matchesDonor }); y += cfg.basePitch; addText(svg, 'Reference', xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end' }); // Connector legend - y += cfg.basePitch * 3; + y += cfg.basePitch * 2.5; const legendTextX = x + cfg.basePitch; let legendTextY = y + cfg.baseWidth - cfg.baseTextPad; addRectFill(svg, x, y, cfg.baseWidth, cfg.baseWidth, cfg.matchesAcceptor); addText(svg, 'Recombinant matches acceptor', legendTextX, legendTextY, { 'fill': cfg.matchesAcceptor }); y += cfg.basePitch; legendTextY = y + cfg.baseWidth - cfg.baseTextPad; addRectFill(svg, x, y, cfg.baseWidth, cfg.baseWidth, cfg.matchesDonor); addText(svg, 'Recombinant matches donor', legendTextX, legendTextY, { 'fill': cfg.matchesDonor }); y += cfg.basePitch; legendTextY = y + cfg.baseWidth - cfg.baseTextPad; + addRectFill(svg, x, y, cfg.baseWidth, cfg.baseWidth, cfg.matchesNeither); + addText(svg, 'Recombinant matches neither', legendTextX, legendTextY, { 'fill': cfg.matchesNeither }); + y += cfg.basePitch; + legendTextY = y + cfg.baseWidth - cfg.baseTextPad; addRectFill(svg, x, y, cfg.baseWidth, cfg.baseWidth, cfg.uninformative); - addText(svg, 'Not informative', legendTextX, legendTextY, { 'fill': cfg.uninformative }); + addText(svg, 'Not informative' + (showInformativeOnly ? ' (not shown)' : ''), legendTextX, legendTextY, + { 'fill': cfg.uninformative }); // Genome & gene graph - y += cfg.connectorAreaHeight - cfg.basePitch * 4 + cfg.interbaseWidth; + y += cfg.connectorAreaHeight - cfg.basePitch * 4.5 + cfg.interbaseWidth; addText(svg, 'Genomic Coordinate', xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end' }); y += cfg.genomeGraphHeight + cfg.topPad * 5; addText(svg, 'Gene Annotations', xRight, y, { 'font-weight': 'bold', 'text-anchor': 'end' }); } function addRotation(el, angle, x, y) { // Add a rotate transform to el const rotateStr = 'rotate(' + angle + ', ' + x + ', ' + y + ')'; // Preserve any transform already set in config (e.g. a translate) const existingTransform = el.getAttribute('transform'); el.setAttribute('transform', existingTransform ? existingTransform + ' ' + rotateStr : rotateStr); } function matchColor(column) { // Select a color based on whether column alleles indicate that the recombinant matches one or the other // of acceptor and donor. If it matches both or neither, it's not informative. let color = cfg.uninformative; let matchesAcceptor = column.aAl === column.rAl; let matchesDonor = column.dAl === column.rAl; + let matchesNeither = !matchesAcceptor && !matchesDonor; if (matchesAcceptor !== matchesDonor) { color = matchesAcceptor ? cfg.matchesAcceptor : cfg.matchesDonor; + } else if (matchesNeither) { + color = cfg.matchesNeither; } return color; } function addPositionLabels(svg, x, y, combinedMuts) { // Add a row of slanted labels above the base array, with each position (1-based) // Tweak coordinates for SVG default text anchoring x += cfg.fontSize; y += cfg.posLabelAreaHeight - cfg.interbaseWidth; for (let column of combinedMuts) { const color = matchColor(column); var text = svgCreateEl('text', { x: x, y: y, textContent: column.pos, 'font-size': cfg.fontSize + 'px', fill: color }); addRotation(text, -75, x, y); svg.appendChild(text); x += cfg.basePitch; } } function baseColor(base, isRef, isInformative) { // Choose a color by base value and informativeness const baseColors = isInformative ? baseColorsInformative : baseColorsNonInformative; let color = isRef ? baseColors.ref : baseColors[base]; if (! color) { console.error("baseColor: Invalid base '" + base + "'"); color = '#FFFF00'; } return color; } function addBase(svg, base, x, y, isRef, isInformative) { // Draw a base as a rectangle colored by value and informativeness with the base value in white addRectFill(svg, x, y, cfg.baseWidth, cfg.baseWidth, baseColor(base, isRef, isInformative)); addText(svg, base, x + cfg.baseTextPad, y + cfg.baseWidth - cfg.baseTextPad, { 'fill': 'white' }); } function addBaseArray(svg, x, y, combinedMuts) { // Add base value at each position for acceptor, recombinant, donor and reference for (let column of combinedMuts) { const isInformative = (column.aAl !== column.dAl && (column.rAl === column.aAl || column.rAl === column.dAl)); let yBase = y; let isRef = (column.aAl === column.ref); addBase(svg, column.aAl, x, yBase, isRef, isInformative && !isRef); yBase += cfg.basePitch; isRef = (column.rAl === column.ref); addBase(svg, column.rAl, x, yBase, isRef, isInformative && !isRef); yBase += cfg.basePitch; isRef = (column.dAl === column.ref); addBase(svg, column.dAl, x, yBase, isRef, isInformative && !isRef); yBase += cfg.basePitch; addBase(svg, column.ref, x, yBase, true, true); x += cfg.basePitch; } } function addConnectors(svg, x, y, genomeY, genomeScale, combinedMuts) { // Add triangles connecting the bottom of each base column to the corresponding genomic coordinate, // colored by whether the recombinant matches acceptor only, donor only, or both/neither (uninformative) let cx = 0; for (let column of combinedMuts) { const color = matchColor(column); // Start at bottom left of base column let baseX = x + cx * cfg.basePitch; let path = "M " + baseX + ' ' + y + ' '; // Line to bottom right of base column path += "L " + (baseX + cfg.baseWidth) + ' ' + y + ' '; // Line to genomic coordinate point let genomeX = x + column.pos * genomeScale; path += "L " + genomeX + ' ' + genomeY + ' '; // Close the path path += 'Z'; svg.appendChild(svgCreateEl('path', { d: path, fill: color, stroke: color, title: column.ref + column.pos + column.rAl })); cx++; } } function addGenomeGraph(svg, x, y, width, height, genomeScale, combinedMuts) { // Add a gray rectangle showing the extent of the genome and a line per mutation // colored by whether the recombinant matches acceptor only, donor only, or both/neither (uninformative) addRectFill(svg, x, y, width, height, '#EEEEEE'); for (let column of combinedMuts) { let xMut = x + column.pos * genomeScale; var color = matchColor(column); var el = svgCreateEl('line', { x1: xMut, y1: y, x2: xMut, y2: y + height, stroke: color }); svg.appendChild(el); } } function addTick(svg, x, y, height) { // Add a short vertical line var el = svgCreateEl('line', { x1: x, y1: y, x2: x, y2: y + height, stroke: 'black' }); svg.appendChild(el); } function commafy(number) { // Return a string that is the number with commas delineating thousands, millions etc. const string = String(number); const digitsStart = string.length % 3; var out = digitsStart ? string.substring(0, digitsStart) : ''; for (let i = digitsStart; i < string.length; i += 3) { if (i > 0) { out += ','; } out += string.substring(i, i+3); } return out; } function estimateTextWidth(text) { // SVG text won't reveal its size until it's rendered. Return a guess based on an assumption of font size // relative to baseWidth, and font width relative to font height. return text.length * cfg.baseWidth * 0.5; } function addGenomeScale(svg, x, y, width, genomeSize, genomeScale) { // Add tick marks and centered text labels underneath the genome graph const genomeSizeString = commafy(genomeSize); const maxLabelWidth = estimateTextWidth(genomeSizeString); const maxBins = Math.floor(width / (maxLabelWidth * 1.5)); const tickTextY = y + 3 * cfg.topPad; for (let binSize of [100, 200, 500, 1000, 200, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000]) { let binCount = Math.ceil(genomeSize / binSize); if (binCount < maxBins) { for (let gx = 0; gx < genomeSize; gx += binSize) { const xOff = gx * genomeScale; const tickX = x + xOff; addTick(svg, tickX, y, cfg.tickHeight); // Don't write label if it might overlap with the label at the end of the genome if (width - xOff > maxLabelWidth) { addText(svg, commafy(gx), tickX, tickTextY, { fill: 'black', 'text-anchor': 'middle' }); } } break; } } // Add one more for the end of the genome addTick(svg, x + width, y, cfg.tickHeight); addText(svg, genomeSizeString, x + width, tickTextY, { fill: 'black', 'text-anchor': 'middle' }); } function addGeneGraph(svg, x, y, width, height, genomeScale, genes) { // Add a gray rectangle showing the extent of the genome and a colorful rectangle per gene, // with a gene name label if the rectangle is large enough addRectFill(svg, x, y, width, height, '#EEEEEE'); const geneTextY = y + 5 * height / 8; for (let gene of genes) { let geneX = x + gene.start * genomeScale; let geneWidth = (gene.end - gene.start) * genomeScale; addRectFill(svg, geneX, y, geneWidth, height, gene.color, { title: gene.name }); let geneXMid = geneX + geneWidth / 2; if (estimateTextWidth(gene.name) < geneWidth) { addText(svg, gene.name, geneXMid, geneTextY, { fill: 'white', 'text-anchor': 'middle', title: gene.name }); } } } function draw(recombAttrs, genomeSize, genes, showInformativeOnly, fontSize=10) { // Return an SVG diagram showing the mutations in acceptor, recombinant and donor to visualize the evidence for // recombination. configLayout(fontSize); let combinedMuts = makeCombinedMuts(recombAttrs.recombMuts, recombAttrs.donorMuts, recombAttrs.acceptorMuts); if (showInformativeOnly) { combinedMuts = filterMuts(combinedMuts); } let baseAreaWidth = combinedMuts.length * cfg.basePitch - cfg.interbaseWidth; if (baseAreaWidth < cfg.minBaseAreaWidth) { baseAreaWidth = cfg.minBaseAreaWidth; } let svgWidth = cfg.leftLabelAreaWidth + baseAreaWidth + 2 * cfg.baseWidth; let svgHeight = cfg.titleAreaHeight + cfg.posLabelAreaHeight + cfg.baseArrayHeight + cfg.connectorAreaHeight + cfg.genomeGraphHeight + cfg.genomeScaleHeight + cfg.geneGraphHeight; var svg = makeSvg(svgWidth, svgHeight); // Title/info area addTitle(svg, recombAttrs); // Left labels let x = cfg.rightPad; let xRight = cfg.leftLabelAreaWidth - cfg.rightPad; let y = cfg.titleAreaHeight; - addLeftLabels(svg, x, xRight, y, recombAttrs); + addLeftLabels(svg, x, xRight, y, recombAttrs, showInformativeOnly); // Base position labels x = cfg.leftLabelAreaWidth; addPositionLabels(svg, x, y, combinedMuts); // Base values for acceptor, recombinant, donor and reference y += cfg.posLabelAreaHeight; addBaseArray(svg, x, y, combinedMuts); // Connectors between base values and genomic coordinates y += cfg.baseArrayHeight; const genomeScale = baseAreaWidth / genomeSize; const genomeY = y + cfg.connectorAreaHeight; addConnectors(svg, x, y, genomeY, genomeScale, combinedMuts); // Genomic coordinate graph with mut lines y += cfg.connectorAreaHeight; addGenomeGraph(svg, x, y, baseAreaWidth, cfg.genomeGraphHeight, genomeScale, combinedMuts); // Genome coordinate scale ticks and labels y += cfg.genomeGraphHeight; addGenomeScale(svg, x, y, baseAreaWidth, genomeSize, genomeScale); // Gene graph with embedded labels y += cfg.genomeScaleHeight; addGeneGraph(svg, x, y, baseAreaWidth, cfg.geneGraphHeight, genomeScale, genes); return svg; } return { draw: draw }; }()); // recombinantGraph //////////////////////////////////////////////////////////// //// popupRecombinant: pop up a recombinantGraph dialog //// //////////////////////////////////////////////////////////// var popUpRecombinant = (function () { "use strict"; function cleanup() { // Clean out contents when done if ($('#recombinantDialog').html().length > 0) { $('#recombinantDialog').html(''); } } function saveSvg(svgElement, name="image.svg") { // Serialize the SVG to a XML string const serializer = new XMLSerializer(); let source = serializer.serializeToString(svgElement); // Add namespaces if they are missing if (!source.match(/^]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) { source = source.replace(/^]+"http:\/\/www\.w3\.org\/1999\/xlink"/)) { source = source.replace(/^"); var $div = $('#popContents'); $div.html("

  " + " Show only informative mutations  " + "  " + "Font size:   " + "

\n"); var svg = recombinantGraph.draw(recombAttrs, genomeSize, genes, showInformativeOnly, fontSize); $div.append(svg); // Clicking on the checkbox toggles showInformativeOnly behavior let $cb = $('#hgpp_informativeOnly'); $cb.prop('checked', showInformativeOnly); $cb.on('click', function() { // Redraw image, update cart var and hidden input that tracks the checkbox across popups showInformativeOnly = $cb.prop('checked'); setCartVar('hgpp_informativeOnly', showInformativeOnly ? '1' : '0', null, true); $('#hidden_showInformative').val(showInformativeOnly ? '1' : '0'); $div.children("svg").remove(); $div.append(recombinantGraph.draw(recombAttrs, genomeSize, genes, showInformativeOnly, fontSize)); }); // Disable prev/next button if at beginning/end let $prevBtn = $('#prevRecomb'); let $nextBtn = $('#nextRecomb'); $prevBtn.prop('disabled', recombinantIndex < 1); $nextBtn.prop('disabled', recombinantIndex >= recombinantData.recombinants.length - 1); $prevBtn.on('click', function() { if (recombinantIndex > 0) { recombinantIndex--; $prevBtn.prop('disabled', recombinantIndex < 1); $nextBtn.prop('disabled', recombinantIndex >= recombinantData.recombinants.length - 1); recombAttrs = recombinantData.recombinants[recombinantIndex]; $div.children("svg").remove(); $div.append(recombinantGraph.draw(recombAttrs, genomeSize, genes, showInformativeOnly, fontSize)); } }); $nextBtn.on('click', function() { if (recombinantIndex < recombinantData.recombinants.length - 1) { recombinantIndex++; $prevBtn.prop('disabled', recombinantIndex < 1); $nextBtn.prop('disabled', recombinantIndex >= recombinantData.recombinants.length - 1); recombAttrs = recombinantData.recombinants[recombinantIndex]; $div.children("svg").remove(); $div.append(recombinantGraph.draw(recombAttrs, genomeSize, genes, showInformativeOnly, fontSize)); } }); // Save image button let $saveImgBtn = $('#saveSvg'); $saveImgBtn.on('click', function() { saveSvg($div.children("svg")[0], "recombinant.svg"); }); // Font size let $select = $('#hgpp_recombFontSize'); $select.val(String(fontSize)); $select.on('change', function() { fontSize = parseInt($select.val(), 10); setCartVar('hgpp_recombFontSize', fontSize, null, true); $('#hidden_fontSize').val(fontSize); $div.children("svg").remove(); $div.append(recombinantGraph.draw(recombAttrs, genomeSize, genes, showInformativeOnly, fontSize)); }); var uiDialogButtons = { Close: function() { $(this).dialog("close"); } }; $('#recombinantDialog').dialog({ resizable: false, height: popMaxHeight, width: popMaxWidth, minHeight: 200, minWidth: 400, modal: true, closeOnEscape: true, autoOpen: false, buttons: uiDialogButtons, title: "Potential recombinant", open: function (event) { // fix popup to a location -- near the top and somewhat centered on the browser image $(event.target).parent().css('position', 'fixed'); $(event.target).parent().css('top', '10%'); }, close: function() { popUpRecombinant.cleanup(); } }); $('#recombinantDialog').dialog('open'); } // display return { cleanup: cleanup, display: display }; }()); ////////////////////// //// hgPhyloPlace //// ////////////////////// var hgPhyloPlace = (function() { "use strict"; function onClickRecombinant(recombinantData, recombinantIndex, showInformativeOnly, fontSize) { // When user clicks on "Show mutations" button for a potential recombinant, make a pop-up with a diagram showing // mutations in the recombinant and its parents, highlighting where they agree. popUpRecombinant.display(recombinantData, recombinantIndex, showInformativeOnly, fontSize); } return { onClickRecombinant: onClickRecombinant }; }()); // hgPhyloPlace