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 */