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);