c385c1661b72541e1e8fb28187323f3b31c431e7
angie
  Fri May 29 18:24:49 2026 -0700
If new columns are present in ripples server output then use them to draw RIVET-like recombination diagram in pop-up.

diff --git src/hg/hgPhyloPlace/phyloPlace.c src/hg/hgPhyloPlace/phyloPlace.c
index 37ac82598cf..e1379f5219c 100644
--- src/hg/hgPhyloPlace/phyloPlace.c
+++ src/hg/hgPhyloPlace/phyloPlace.c
@@ -2254,33 +2254,119 @@
         {
         if (gotClades)
             printf("<td>n/a</td>");
         if (gotLineages)
             printf("<td>n/a</td>");
         }
     // pangolin-assigned lineage
     char *lineage = lineageForSample(sms, si->name);
     printLineageTd(lineage, "n/a", db);
     // Maybe also #mutations with mouseover to show mutation path?
     printSubtreeTd(results->subtreeInfoList, jsonTns, si->name, subtreeSize);
     }
 puts("</tbody></table><p></p>");
 }
 
-static void summarizeRecombinants(struct recombinantInfo *recombinants, struct hash *descendants, struct slName *sampleIds)
+static void makeNiceNodeName(struct dyString *dyRNodeId, struct recombinantInfo *ri, struct hash *descendants, struct slName *sampleIds)
+/* A "node_NNNN" name is not very informative, but for the recombinant node, we know that at least one of its descendants should be
+ * one of the user's uploaded samples.  Add uploaded sample name(s) and number of other descendants, to give an idea of how large of
+ * a branch is this potential recombinant. */
+{
+dyStringClear(dyRNodeId);
+dyStringPrintf(dyRNodeId, "%s", ri->recombNodeId);
+if (startsWith("node_", ri->recombNodeId))
+    {
+    dyStringPrintf(dyRNodeId, " (");
+    struct slName *descendantList = hashFindVal(descendants, ri->recombNodeId), *desc;
+    int sampleCount = 0;
+    for (desc = descendantList;  desc != NULL;  desc = desc->next)
+        {
+        if (slNameInList(sampleIds, desc->name))
+            {
+            if (sampleCount > 0)
+                dyStringPrintf(dyRNodeId, ", ");
+            dyStringPrintf(dyRNodeId, "%s", desc->name);
+            sampleCount++;
+            }
+        }
+    int others = ri->recombNumDesc - sampleCount;
+    if (others)
+        dyStringPrintf(dyRNodeId, " + %d", others);
+    dyStringPrintf(dyRNodeId, ")");
+    }
+}
+
+static const char *nextstrainColors10[10] = {
+    // Lifted from the colors nextstrain uses for SARS-CoV-2 genes.  Not an exact match because they use the same color
+    // for S and M, and they have ORF9b and we don't, so I shuffled those a bit.
+    "rgb(230, 112, 48)",
+    "rgb(217, 173, 61)",
+    "rgb(229, 150, 55)",
+    "rgb(96, 170, 158)",
+    "rgb(223, 67, 39)",
+    "rgb(170, 189, 82)",
+    "rgb(96, 170, 158)",
+    "rgb(196, 185, 69)",
+    "rgb(117, 182, 129)",
+    "rgb(80, 151, 186)",
+};
+
+static void inlineRecombinantData(struct recombinantInfo *recombinants, struct hash *descendants, struct slName *sampleIds,
+                                  int genomeSize, struct geneInfo *geneInfoList)
+/* Write out inline javascript with data used to draw recombinant graph in popup when button is clicked. */
+{
+struct dyString *dy = dyStringNew(4096 * slCount(recombinants));
+dyStringPrintf(dy, "var recombinantData = { genomeSize: %d,\n  genes: [\n    ", genomeSize);
+int gIx;
+struct geneInfo *gi;
+for (gi = geneInfoList, gIx = 0;  gi != NULL;  gi = gi->next, gIx++)
+    {
+    if (gi != geneInfoList)
+        dyStringAppend(dy, ",\n    ");
+    dyStringPrintf(dy, "{ name: '%s', start: %d, end: %d, color: '%s' }",
+                   gi->psl->qName, gi->psl->tStart, gi->psl->tEnd,
+                   nextstrainColors10[gIx % ArraySize(nextstrainColors10)]);
+    }
+dyStringAppend(dy, "  ],\n  recombinants: [    \n");
+struct dyString *dyRNodeId = dyStringNew(0);
+struct recombinantInfo *ri;
+for (ri = recombinants;  ri != NULL;  ri = ri->next)
+    {
+    if (ri != recombinants)
+        dyStringAppend(dy, ",\n    ");
+    makeNiceNodeName(dyRNodeId, ri, descendants, sampleIds);
+    dyStringPrintf(dy, "{ recombMuts: '%s', donorMuts: '%s', acceptorMuts: '%s', "
+                   "bp1: '(%d,%d)', bp2: '(%d,%d)', "
+                   "rNode: '%s', dNode: '%s', aNode: '%s', "
+                   "rLin: '%s', dLin: '%s', aLin: '%s', "
+                   "improvement: %d }",
+                   ri->recombMutations, ri->donorMutations, ri->acceptorMutations,
+                   ri->bp1Min, ri->bp1Max, ri->bp2Min, ri->bp2Max,
+                   dyRNodeId->string, ri->donorNodeId, ri->acceptorNodeId,
+                   ri->recombLineage, ri->donorLineage, ri->acceptorLineage,
+                   ri->parsimonyImprovement);
+    }
+dyStringAppend(dy, "\n  ] };");
+jsInline(dy->string);
+}
+
+static void summarizeRecombinants(struct recombinantInfo *recombinants, struct hash *descendants, struct slName *sampleIds,
+                                  int genomeSize, struct geneInfo *geneInfoList, struct cart *cart)
 /* Show a table describing potential recombinants found by ripples search. */
 {
+inlineRecombinantData(recombinants, descendants, sampleIds, genomeSize, geneInfoList);
+puts("<div id='recombinantDialog' style='display: none'></div>");
 puts("<p><b>Potential recombinants found: </b><br>");
 puts("<table class='seqSummary'>");
 puts("<thead><tr><th>Potential recombinant node"
      TOOLTIP("Sequence or internal node for which potential recombination was found. "
              "If internal node, then the uploaded sequence(s) descended from the node are listed, "
              "and if there are additional sequences already at that node then their number is "
              "given")
      "</th>");
 puts("<th>Placement clade"
      TOOLTIP("Nextstrain clade in which the potential recombinant was placed")
      "</th>");
 puts("<th>Placement lineage"
      TOOLTIP("Pango lineage in which the potential recombinant was placed")
      "</th>");
 puts("<th>Parsimony score improvement"
@@ -2311,63 +2397,73 @@
              "with the acceptor node")
      "</th>");
 puts("<th>Acceptor clade"
      TOOLTIP("Nextstrain clade in which the acceptor node was placed")
      "</th>");
 puts("<th>Acceptor lineage"
      TOOLTIP("Pango lineage in which the acceptor node was placed")
      "</th>");
 puts("<th>Breakpoint 1 range"
      TOOLTIP("Coordinate range, bounded by mutations explained by the donor node at lower "
              "coordinates and by the acceptor node at higher coordinates")
      "</th>");
 puts("<th>Breakpoint 2 range"
      TOOLTIP("Coordinate range, bounded by mutations explained by the acceptor node at lower "
              "coordinates and by the donor node at higher coordinates")
-     "</th></tr></thead><tbody>");
-struct recombinantInfo *ri;
-for (ri = recombinants;  ri != NULL;  ri = ri->next)
-    {
-    printf("<tr><td>%s", ri->recombNodeId);
-    if (startsWith("node_", ri->recombNodeId))
-        {
-        printf(" (");
-        struct slName *descendantList = hashFindVal(descendants, ri->recombNodeId), *desc;
-        int sampleCount = 0;
-        for (desc = descendantList;  desc != NULL;  desc = desc->next)
-            {
-            if (slNameInList(sampleIds, desc->name))
+     "</th>");
+if (isNotEmpty(recombinants->recombMutations))
     {
-                if (sampleCount > 0)
-                    printf(", ");
-                printf("%s", desc->name);
-                sampleCount++;
-                }
+    puts("<th>Show mutations"
+         TOOLTIP("Click the button to show a diagram of mutations matching the donor and/or acceptor")
+         "</th>");
     }
-        int others = ri->recombNumDesc - sampleCount;
-        if (others)
-            printf(" + %d", others);
-        printf(")");
+puts("</tr></thead><tbody>");
+if (isNotEmpty(recombinants->recombMutations))
+    {
+    // The user may change this using a checkbox in a dialog; keep a hidden input on the main page to be updated
+    // by the dialog JS when the user clicks on the dialog checkbox, to keep the state in case they close and reopen
+    // the dialog.
+    boolean informativeOnly = cartUsualBoolean(cart, "hgpp_informativeOnly", FALSE);
+    cgiMakeHiddenVarWithIdExtra("hidden_showInformative", "hidden_showInformative", informativeOnly ? "1" : "0", NULL);
     }
-    printf("</td><td>%s</td><td>%s</td>", ri->recombClade, ri->recombLineage);
+struct dyString *dyRNodeId = dyStringNew(0);
+struct dyString *dyJs = dyStringNew(0);
+int rNum;
+struct recombinantInfo *ri;
+for (ri = recombinants, rNum = 0;  ri != NULL;  ri = ri->next, rNum++)
+    {
+    makeNiceNodeName(dyRNodeId, ri, descendants, sampleIds);
+    printf("<tr><td>%s</td><td>%s</td><td>%s</td>", dyRNodeId->string, ri->recombClade, ri->recombLineage);
     printf("<td>%d (from %d)</td>", ri->parsimonyImprovement, ri->originalParsimony);
     printf("<td>%s</td><td>%d</td><td>%s</td><td>%s</td>",
            ri->donorNodeId, ri->donorNumDesc, ri->donorClade, ri->donorLineage);
     printf("<td>%s</td><td>%d</td><td>%s</td><td>%s</td>",
            ri->acceptorNodeId, ri->acceptorNumDesc, ri->acceptorClade, ri->acceptorLineage);
-    printf("<td>(%d, %d)</td><td>(%d, %d)</td></tr>\n",
+    printf("<td>(%d, %d)</td><td>(%d, %d)</td>",
            ri->bp1Min, ri->bp1Max, ri->bp2Min, ri->bp2Max);
+    if (isNotEmpty(ri->recombMutations))
+        {
+        puts("<td>");
+        dyStringClear(dyJs);
+        dyStringPrintf(dyJs, "hgPhyloPlace.onClickRecombinant(recombinantData, %d, "
+                       "$('#hidden_showInformative').val() == 1); return 0;", rNum);
+        char name[256];
+        safef(name, sizeof name, "showMutations_%d", rNum);
+        cgiMakeOnClickButton(name, dyJs->string, "Show mutations");
+        puts("</td>");
+        }
+    puts("</tr>\n");
     }
 puts("</tbody></table>");
 puts("</p>");
 }
 
 static struct singleNucChange *sncListFromSampleMutsAndImputed(struct slName *sampleMuts,
                                                                struct baseVal *imputedBases,
                                                                struct seqWindow *gSeqWin)
 /* Convert a list of "<ref><pos><alt>" names to struct singleNucChange list.
  * However, if <alt> is ambiguous, skip it because variantProjector doesn't like it.
  * Add imputed base predictions. */
 {
 struct singleNucChange *sncList = NULL;
 struct slName *mut;
 for (mut = sampleMuts;  mut != NULL;  mut = mut->next)
@@ -3654,31 +3750,32 @@
                                      tl->fontHeight, sampleTree, &startTime);
             }
 
         // Make a sample summary TSV file and accumulate S gene changes
         struct hash *seqInfoHash = hashFromSeqInfoListAndIds(seqInfoList, sampleIds);
         addSampleMutsFromSeqInfo(results->samplePlacements, seqInfoHash);
         struct hash *spikeChanges = hashNew(0);
         tsvTn = writeTsvSummary(results, sampleTree, sampleIds, seqInfoHash,
                                 geneInfoList, gSeqWin, spikeChanges, &startTime);
         sTsvTn = writeSpikeChangeSummary(spikeChanges, slCount(sampleIds));
         downloadsRow(results->bigTreePlusTn->forHtml, tsvTn->forHtml, sTsvTn->forHtml,
                      zipTn->forHtml);
 
         if (results->recombinants)
             {
-            summarizeRecombinants(results->recombinants, results->recombinantDescendants, sampleIds);
+            summarizeRecombinants(results->recombinants, results->recombinantDescendants, sampleIds,
+                                  refGenome->size, geneInfoList, cart);
             }
         else if (ripplesEnabled)
             {
             if (doRipples)
                 puts("<p><b>No potential recombinants found.</b></p>");
             else
                 printf("<p><b>Did not search for potential recombinants, too many input sequences "
                        "(maximum %d for recombinant search).</b></p>\n", MAX_RIPPLES_SEARCH);
             }
 
         int seqCount = slCount(seqInfoList);
         if (seqCount <= MAX_SEQ_DETAILS)
             {
             char *refAcc = cloneString(refGenome->name);
             if (regexMatch(refAcc, "v[0-9]+$"))