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]+$"))