43f042236ce180b8e0712d43c8baabc4ad8df1cd max Sat Jul 25 18:21:54 2026 -0700 hgTracks: add compactPack mode and codon strand-direction arrows When zoomed to codon-coloring level, draw strand-direction chevrons across the exons (white over coding codons when too small to label, contrasting color over UTRs), and a per-codon arrow between amino-acid letters via drawScaledBoxWithText. Add a compactPack trackDb setting for shorter pack/squish rows and a small squish-mode codon font. Adds mgFontForCellHeight() to memgfx and enables compactPack on the JASPAR track. diff --git src/hg/hgTracks/cds.c src/hg/hgTracks/cds.c index cefe211aea1..a307ebf0e8c 100644 --- src/hg/hgTracks/cds.c +++ src/hg/hgTracks/cds.c @@ -100,59 +100,121 @@ else if (btStart >= tEnd) { qEnd = lastBqEnd; foundEnd = TRUE; break; } } lastBqEnd = bqEnd; } *retQStart = qStart; *retQEnd = qEnd; return TRUE; } +static void drawCodonStrandArrow(struct hvGfx *hvg, int boundaryX, int midY, + int height, int maxHalfWidth, Color boxColor, int strand) +/* Draw a strand-direction chevron centered on the left boundary of a codon box + * (i.e. in the whitespace between the centered amino-acid letters of two + * adjacent codons). The chevron points in the strand direction (1 = right, + * -1 = left; reverse-complement display is handled by hvGfxLine). It is drawn + * in the box's contrasting color (white on the dark-blue codon shades, the same + * color as the codon letter) so it reads clearly, and the caller draws it before + * the amino-acid text so the letter stays crisp. maxHalfWidth is how far the + * chevron may extend to each side of the boundary before it would touch a + * letter: if there is no room (< 1px) nothing is drawn, otherwise the chevron + * is narrowed to fit that whitespace. */ +{ +if (strand == 0) + return; +if (maxHalfWidth < 1) + return; // no room between the letters - don't draw +int bh = (height - 2) / 2; // chevron half-height +if (bh > 3) + bh = 3; // keep it small +if (bh < 2) + return; // not enough vertical room +int hw = (bh*2 + 1)/3; // preferred half-width: ~1/3 narrower than the + // half-height, so the arrow stays slim +if (hw < 1) + hw = 1; +if (hw > maxHalfWidth) // but never wider than the whitespace allows + hw = maxHalfWidth; + +// contrasting color: white on the dark-blue codon shades, matching the letter +Color aColor = hvGfxContrastingColor(hvg, boxColor); +if (strand > 0) + { + // ">" apex on the right, wings opening to the left + hvGfxLine(hvg, boundaryX + hw, midY, boundaryX - hw, midY - bh, aColor); + hvGfxLine(hvg, boundaryX + hw, midY, boundaryX - hw, midY + bh, aColor); + } +else + { + // "<" apex on the left, wings opening to the right + hvGfxLine(hvg, boundaryX - hw, midY, boundaryX + hw, midY - bh, aColor); + hvGfxLine(hvg, boundaryX - hw, midY, boundaryX + hw, midY + bh, aColor); + } +} + static void drawScaledBoxWithText(struct hvGfx *hvg, int chromStart, int chromEnd, double scale, int xOff, int y, int height, Color color, int score, MgFont *font, char *text, bool zoomed, - int winStart, int maxPixels, boolean isCoding, boolean justifyString) + int winStart, int maxPixels, boolean isCoding, boolean justifyString, + int strand) /* Draw a box scaled from chromosome to window coordinates with - a codon or set of 3 or less bases drawn in the box. */ + a codon or set of 3 or less bases drawn in the box. If strand is non-zero + and a whole codon is drawn, a faint strand-direction chevron is drawn in the + box before the amino-acid letter. */ { /*first draw the box itself*/ drawScaledBox(hvg, chromStart, chromEnd, scale, xOff, y, height, color); /*draw text in box if space, and align properly for codons or DNA*/ if (zoomed) { int i; Color textColor = hvGfxContrastingColor(hvg, color); int x1, x2, w; x1 = round((double)(chromStart-winStart)*scale) + xOff; x2 = round((double)(chromEnd-winStart)*scale) + xOff; if (x2 >= maxPixels) x2 = maxPixels - 1; w = x2-x1; if (w < 1) w = 1; if (chromEnd - chromStart == 3 && isCoding) { + /* faint strand arrow between this amino acid and the previous one, + * drawn before the letter so the letter stays legible. Suppressed in + * squish mode, where the strand is shown by the intron barbs instead. + * Only draw the arrow when there is genuine whitespace between the + * centered letters: the letters sit letterWidth wide in a box w wide, so + * there is (w-letterWidth)/2 of space on each side of the boundary. Keep + * a 1px gap so the arrow never touches a letter; if that leaves no room, + * skip the arrow rather than forcing it in. */ + if (baseColorDrawCodonArrows) + { + int letterWidth = mgFontStringWidth(font, text); + int maxHalfWidth = (w - letterWidth)/2 - 1; + drawCodonStrandArrow(hvg, x1, y + height/2, height, maxHalfWidth, color, strand); + } if (justifyString) spreadBasesString(hvg, x1, y, w, height, textColor, font, text, strlen(text), TRUE); else hvGfxTextCentered(hvg, x1, y, w, height, textColor, font, text); } else if (chromEnd - chromStart < 3 && isCoding) { if (justifyString) spreadBasesString(hvg, x1, y, w, height, cdsColor[CDS_PARTIAL_CODON], font, text, strlen(text), TRUE); else hvGfxTextCentered(hvg, x1, y, w, height, cdsColor[CDS_PARTIAL_CODON], font, text); } else { int thisX,thisX2; @@ -1654,89 +1716,89 @@ mrnaBases[0] = '\0'; if (psl && isCoding) getMrnaBases(psl, mrnaSeq, mrnaOffset, mrnaS, s, e, (lf->orientation == -1), mrnaBases, &queryInsertion); if (queryInsertion && isCoding) color = cdsColor[CDS_QUERY_INSERTION]; dyStringAppendN(dyMrnaSeq, (char*)&mrnaSeq->dna[mrnaS - mrnaOffset], e-s); if (drawOpt == baseColorDrawItemBases) { if (cartUsualBooleanDb(cart, database, COMPLEMENT_BASES_VAR, FALSE)) complement(dyMrnaSeq->string, dyMrnaSeq->stringSize); drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, dyMrnaSeq->string, - zoomedToBaseLevel, winStart, maxPixels, isCoding, TRUE); + zoomedToBaseLevel, winStart, maxPixels, isCoding, TRUE, lf->orientation); } else if (drawOpt == baseColorDrawItemCodons) { if (e <= lf->tallEnd) { boolean startColor = FALSE; /* re-set color of this block based on mrna codons rather than * genomic, but keep the odd/even cycle of dark/light shades. */ int mrnaGrayIx = codonToGrayIx(mrnaBases, (grayIx > 26), NULL, FALSE, TRUE, NULL); if (color == cdsColor[CDS_START]) startColor = TRUE; color = colorAndCodonFromGrayIx(hvg, mrnaCodon, mrnaGrayIx, ixColor); if (startColor && sameString(mrnaCodon,"M")) color = cdsColor[CDS_START]; drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, mrnaCodon, zoomedToCodonLevel, winStart, - maxPixels, isCoding, TRUE); + maxPixels, isCoding, TRUE, lf->orientation); } else drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, color); } else if (drawOpt == baseColorDrawDiffBases) { char *diffStr = NULL; char *genoDna = getCachedDna(s, e); diffStr = needMem(sizeof(char) * (e - s + 1)); maskDiffString(diffStr, dyMrnaSeq->string, genoDna, ' ', dyMrnaSeq->stringSize); // fprintf(stderr, "drawOpt =- diffBases. %d %d %d %d\n", (int)strlen(genoDna), (int)strlen(dyMrnaSeq->string), (int)dyMrnaSeq->stringSize, e-s); if (cartUsualBooleanDb(cart, database, COMPLEMENT_BASES_VAR, FALSE)) complement(diffStr, strlen(diffStr)); drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, diffStr, - zoomedToBaseLevel, winStart, maxPixels, isCoding, TRUE); + zoomedToBaseLevel, winStart, maxPixels, isCoding, TRUE, lf->orientation); freeMem(diffStr); } else if (drawOpt == baseColorDrawDiffCodons) { if (e <= lf->tallEnd) { /* Color codons red wherever mrna differs from genomic; * keep the odd/even cycle of dark/light shades. */ colorAndCodonFromGrayIx(hvg, genomicCodon, grayIx, ixColor); int mrnaGrayIx = mrnaCodonToGrayIx(mrnaBases, genomicCodon[0], (grayIx > 26)); color = colorAndCodonFromGrayIx(hvg, mrnaCodon, mrnaGrayIx, ixColor); // Look up mrnaCodon again because if mrnaGrayIx is GRAYIX_SYN_PROT, // codon value is lost: safef(mrnaCodon, sizeof(mrnaCodon), "%c", baseColorLookupCodon(mrnaBases)); if (mrnaCodon[0] != genomicCodon[0]) { drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, mrnaCodon, zoomedToCodonLevel, - winStart, maxPixels, isCoding, TRUE); + winStart, maxPixels, isCoding, TRUE, lf->orientation); } else drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, color); } else drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, color); } else if (drawOpt != baseColorDrawCds) errAbort("Unknown drawOpt: %d<br>\n", drawOpt); dyStringFree(&dyMrnaSeq); } else { if (s < e) @@ -1766,37 +1828,37 @@ /* When we are zoomed out far enough so that multiple bases/codons share the * same pixel, we have to draw differences in a separate pass (baseColorOverdrawDiff) * so don't waste time drawing the differences here: */ boolean zoomedOutToPostProcessing = ((drawOpt == baseColorDrawDiffBases && !zoomedToBaseLevel) || (drawOpt == baseColorDrawDiffCodons && !zoomedToCdsColorLevel)); if (drawOpt == baseColorDrawGenomicCodons && (e-s <= 3)) { if (lf->highlightColor) { drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, lf->highlightColor); drawScaledBoxWithText(hvg, s, e, scale, xOff, y+1, heightPer-2, color, lf->score, font, codon, - zoomedToCodonLevel, winStart, maxPixels, TRUE, !sf->codonIndex); + zoomedToCodonLevel, winStart, maxPixels, TRUE, !sf->codonIndex, lf->orientation); } else { drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, codon, - zoomedToCodonLevel, winStart, maxPixels, TRUE, !sf->codonIndex); + zoomedToCodonLevel, winStart, maxPixels, TRUE, !sf->codonIndex, lf->orientation); } } else if (qSeq != NULL && (psl != NULL || sf != NULL) && !zoomedOutToPostProcessing && drawOpt != baseColorDrawGenomicCodons && drawOpt != baseColorDrawOff) { if (lf->highlightColor) { drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, lf->highlightColor); drawDiffTextBox(hvg, xOff+1, y+1, scale, heightPer-2, font, color, chromName, s, e, sf, psl, qSeq, qOffset, lf, grayIx, drawOpt, maxPixels, tg->colorShades, originalColor); } else @@ -1907,30 +1969,130 @@ enabled = FALSE; if (drawOpt == baseColorDrawDiffCodons && !zoomedToCdsColorLevel && lf->codons && enabled) { drawCdsDiffCodonsOnly(tg, lf, hvg, xOff, y, scale, heightPer, qSeq, qOffset, psl, winStart); } if (drawOpt == baseColorDrawDiffBases && !zoomedToBaseLevel && enabled) { drawCdsDiffBaseTickmarksOnly(tg, lf, hvg, xOff, y, scale, heightPer, qSeq, qOffset, psl, winStart); } } +static void drawStrandBarbsInRange(struct hvGfx *hvg, int s, int e, double scale, + int xOff, int winStart, int midY, int barbHeight, int barbSpacing, + int orientation, Color color) +/* Clip base range [s,e] to the window and draw evenly-spaced strand chevrons + * (barbs only, no connecting line) across it. */ +{ +if (s < winStart) s = winStart; +if (e > winEnd) e = winEnd; +if (e <= s) + return; +int x1 = round((double)(s-winStart)*scale) + xOff; +int x2 = round((double)(e-winStart)*scale) + xOff; +int w = x2 - x1; +if (w < barbHeight*2) + return; // too narrow to show an arrow cleanly +clippedBarbs(hvg, x1, midY, w, barbHeight, barbSpacing, orientation, color, FALSE); +} + +static boolean anyIntronOnScreen(struct linkedFeatures *lf) +/* TRUE if a gap between two consecutive exons (an intron) overlaps the window, + * i.e. the transcript's intron fishbones are visible and already show strand. */ +{ +struct simpleFeature *sf; +for (sf = lf->components; sf != NULL && sf->next != NULL; sf = sf->next) + { + int gapStart = sf->end, gapEnd = sf->next->start; + if (gapEnd > gapStart && rangeIntersection(gapStart, gapEnd, winStart, winEnd) > 0) + return TRUE; + } +return FALSE; +} + +void baseColorDrawCdsArrows(struct track *tg, struct linkedFeatures *lf, + struct hvGfx *hvg, int xOff, int y, double scale, + int heightPer, int winStart, enum baseColorDrawOpt drawOpt, + Color color) +/* When zoomed in far enough to color the codons, distribute strand-direction + * chevrons across each exon on top of the boxes (the per-codon letter arrows in + * drawScaledBoxWithText only appear once the codons are big enough to label). + * The coding part gets white chevrons at the standard barb spacing, but only + * when the codons are too small to label (otherwise the letter arrows cover it). + * The UTR parts get the feature's contrasting color at a wider spacing, as a + * visual hint that they are non-coding. No-op below the codon-color zoom level, + * when coding coloring is off, or when the strand is unknown. */ +{ +if (drawOpt <= baseColorDrawOff) + return; +if (!zoomedToCdsColorLevel) + return; +if (lf->orientation == 0) + return; + +if (!cdsColorsMade) + { + makeCdsShades(hvg, cdsColor); + cdsColorsMade = TRUE; + } + +int midY = y + (heightPer>>1); +int orientation = lf->orientation; + +/* white reads clearly on top of the dark-blue codon shades (the same high + * contrast the codon-letter text uses); only draw it when the codons are too + * small to show their letters (which carry their own arrows), and not when an + * intron of this transcript is visible on screen - then the intron fishbones + * already show the strand, so arrows on the coding boxes are redundant clutter */ +boolean drawCds = !zoomedToCodonLevel && !anyIntronOnScreen(lf); +Color cdsColor1 = hvGfxFindColorIx(hvg, 0xff, 0xff, 0xff); +int cdsBh = tl.barbHeight; +int cdsSpacing = tl.barbSpacing*2; + +/* UTR boxes are drawn shorter and in the feature color, so size the chevron to + * the short box (but at least 1px, so it still shows in squish mode where the + * box is only a few pixels tall) and use the feature's contrasting color; space + * them more widely than the coding chevrons as a cue that UTRs are non-coding */ +int shortOff = heightPer/4; +int shortHeight = heightPer - 2*shortOff; +int utrBh = shortHeight/2; +if (utrBh > tl.barbHeight) + utrBh = tl.barbHeight; +if (utrBh < 1) + utrBh = 1; +Color utrColor = hvGfxContrastingColor(hvg, color); +int utrSpacing = tl.barbSpacing*3; + +struct simpleFeature *sf; +for (sf = lf->components; sf != NULL; sf = sf->next) + { + if (drawCds) + drawStrandBarbsInRange(hvg, max(sf->start, lf->tallStart), min(sf->end, lf->tallEnd), + scale, xOff, winStart, midY, cdsBh, cdsSpacing, orientation, cdsColor1); + /* 5' and 3' UTR portions of this exon (the parts outside [tallStart,tallEnd]) */ + drawStrandBarbsInRange(hvg, sf->start, min(sf->end, lf->tallStart), + scale, xOff, winStart, midY, utrBh, utrSpacing, orientation, utrColor); + drawStrandBarbsInRange(hvg, max(sf->start, lf->tallEnd), sf->end, + scale, xOff, winStart, midY, utrBh, utrSpacing, orientation, utrColor); + } +} + + void baseColorOverdrawQInsert(struct track *tg, struct linkedFeatures *lf, struct hvGfx *hvg, int xOff, int y, double scale, int heightPer, struct dnaSeq *qSeq, int qOffset, struct psl *psl, MgFont *font, int winStart, enum baseColorDrawOpt drawOpt, boolean indelShowQInsert, boolean indelShowPolyA) /* If applicable, draw 1-pixel wide orange lines for query insertions in the * middle of the query, 1-pixel wide blue lines for query insertions at the * end of the query, and 1-pixel wide green (instead of blue) when a query * insertion at the end is a valid poly-A tail. */ { assert(psl); int i; int s; int lastBlk = psl->blockCount - 1; @@ -2300,31 +2462,31 @@ struct simpleFeature *sf; if (!cdsColorsMade) { makeCdsShades(hvg, cdsColor); cdsColorsMade = TRUE; } for (sf = sfList; sf != NULL; sf = sf->next) { char codon[4]; Color color = colorAndCodonFromGrayIx(hvg, codon, sf->grayIx, MG_GRAY); if (zoomedToText) drawScaledBoxWithText(hvg, sf->start, sf->end, scale, insideX, y, height, color, 1.0, font, codon, TRUE, - winStart, maxPixels, TRUE, TRUE); + winStart, maxPixels, TRUE, TRUE, 0); else /* zoomed in just enough to see colored boxes */ drawScaledBox(hvg, sf->start, sf->end, scale, xOff, y, height, color); /* mouse-over the codon box with the amino acid's three-letter abbreviation * and full name (sf->codonAa was set when the codon was translated) */ baseColorAddRulerCodonMapItem(hvg, sf, scale, xOff, y, height); } } void baseColorSetCdsBounds(struct linkedFeatures *lf, struct psl *psl, struct track *tg) /* set CDS bounds in linked features for a PSL. Used when zoomed out too far * for codon or base coloring, but still want to render CDS bounds */