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
@@ -6,30 +6,31 @@
 /* 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) {
@@ -204,36 +205,36 @@
             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';
@@ -260,79 +261,87 @@
         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',
@@ -494,31 +503,31 @@
         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);