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/simpleTracks.c src/hg/hgTracks/simpleTracks.c
index e207cd5ef5f..34fb8f9101f 100644
--- src/hg/hgTracks/simpleTracks.c
+++ src/hg/hgTracks/simpleTracks.c
@@ -243,30 +243,31 @@
char *chromName; /* Name of chromosome sequence . */
char *displayChromName; /* Name of chromosome sequence to display . */
int winStart; /* Start of window in sequence. */
int winEnd; /* End of window in sequence. */
char *position = NULL; /* Name of position. */
int insideX; /* Start of area to draw track in in pixels. */
int insideWidth; /* Width of area to draw tracks in in pixels. */
int leftLabelX; /* Start of area to draw left labels on. */
int leftLabelWidth; /* Width of area to draw left labels on. */
float basesPerPixel = 0; /* bases covered by a pixel; a measure of zoom */
boolean zoomedToBaseLevel; /* TRUE if zoomed so we can draw bases. */
boolean zoomedToCodonNumberLevel; /* TRUE if zoomed so we can print codons and exon number text in genePreds*/
boolean zoomedToCodonLevel; /* TRUE if zoomed so we can print codons text in genePreds*/
boolean zoomedToCdsColorLevel; /* TRUE if zoomed so we can color each codon*/
+boolean baseColorDrawCodonArrows = TRUE; /* Draw a strand chevron on each codon box? Off in squish. */
boolean withLeftLabels = TRUE; /* Display left labels? */
boolean withIndividualLabels = TRUE; /* print labels on item-by-item basis (false to skip) */
boolean withCenterLabels = TRUE; /* Display center labels? */
boolean withGuidelines = TRUE; /* Display guidelines? */
boolean withNextExonArrows = FALSE; /* Display next exon navigation buttons near center labels? */
boolean withExonNumbers = FALSE; /* Display exon and intron numbers in mouseOver instead of item name */
boolean revCmplDisp = FALSE; /* reverse-complement display */
boolean measureTiming = FALSE; /* DON'T EDIT THIS -- use CGI param "&measureTiming=." . */
struct track *trackList = NULL; /* List of all tracks. */
struct cart *cart; /* The cart where we keep persistent variables. */
int seqBaseCount; /* Number of bases in sequence. */
int winBaseCount; /* Number of bases in window. */
@@ -864,76 +865,91 @@
int lineHeight, int heightPer, boolean allowOverflow)
/* Most fixed height track groups will use this to figure out the height
* they use. */
{
int height;
if ((height = setupForWiggle(tg, vis)) != 0)
return height;
int rows;
double maxHeight = maximumTrackHeight(tg);
int itemCount = slCount(tg->items);
int maxItemsToUseOverflow = maxItemsToOverflow(tg);
tg->heightPer = heightPer;
tg->lineHeight = lineHeight;
+boolean isCompactPack = trackDbSettingOn(tg->tdb, "compactPack");
+
/* Note that the maxCount variable passed to packCountRowsOverflow()
is tied to the maximum height allowed for a track and influences
decisions about when to squish, dense, or overflow a track.
If doing overflow try to pack all the items into the maxHeight area
or put all the overflow into the last row. If not doing overflow
allow the track enough rows to go over the maxHeight (thus if the
spaceSaver fills up the total height will be more than maxHeight).
*/
switch (vis)
{
case tvFull:
if (isTypeBedLike(tg))
{
if(allowOverflow && itemCount < maxItemsToUseOverflow)
rows = packCountRowsOverflow(tg, floor(maxHeight/tg->lineHeight), FALSE, allowOverflow, vis);
else
rows = packCountRowsOverflow(tg, floor(maxHeight/tg->lineHeight)+1, FALSE, FALSE, vis);
}
else
{
rows = slCount(tg->items);
}
break;
case tvPack:
{
+ if (isCompactPack)
+ {
+ tg->heightPer = heightPer / 2;
+ tg->lineHeight = tg->heightPer;
+ }
if(allowOverflow && itemCount < maxItemsToUseOverflow)
rows = packCountRowsOverflow(tg, floor(maxHeight/tg->lineHeight), TRUE, allowOverflow, vis);
else
rows = packCountRowsOverflow(tg, floor(maxHeight/tg->lineHeight)+1, TRUE, FALSE, vis);
if (tdbIsCompositeChild(tg->tdb))
{
boolean doHideEmpties = compositeChildHideEmptySubtracks(cart, tg->tdb, NULL, NULL);
if (isCenterLabelsPackOff(tg) && !doHideEmpties)
if (rows == 0)
rows = 1; // compact pack mode, shows just side label
}
break;
}
case tvSquish:
+ {
+ if (isCompactPack)
+ {
+ tg->heightPer = 3;
+ tg->lineHeight = 3;
+ }
+ else
{
tg->heightPer = heightPer/2;
if ((tg->heightPer & 1) == 0)
tg->heightPer -= 1;
tg->lineHeight = tg->heightPer + 1;
+ }
if(allowOverflow && itemCount < maxItemsToUseOverflow)
rows = packCountRowsOverflow(tg, floor(maxHeight/tg->lineHeight), FALSE, allowOverflow, vis);
else
rows = packCountRowsOverflow(tg, floor(maxHeight/tg->lineHeight)+1, FALSE, FALSE, vis);
break;
}
case tvDense:
default:
rows = 1;
break;
}
tg->height = rows * tg->lineHeight;
return tg->height;
}
@@ -3156,31 +3172,31 @@
safecpy(aaAbbr, sizeof(aaAbbr), "Ter");
aaName = "termination";
}
else if (aaLetter == 'X') // error/partial codon: nothing to show
aaAbbr[0] = '\0';
else
{
aaToAbbr(aaLetter, aaAbbr, sizeof(aaAbbr));
aaName = aaToName(aaLetter);
}
dyStringPrintf(codonDy, "Codon: c.%d-%d (p.%d)
",
cStart, cEnd, pPos);
if (!isEmpty(aaAbbr))
{
if (aaName != NULL)
- dyStringPrintf(codonDy, "Amino acid: %s (%s)
", aaAbbr, aaName);
+ dyStringPrintf(codonDy, "Amino acid: %s - %s
", aaAbbr, aaName);
else
dyStringPrintf(codonDy, "Amino acid: %s
", aaAbbr);
}
}
else if (lf->tallStart < lf->tallEnd)
{
// UTR block of a coding transcript (codonIndex 0, so no
// c./p. above): label it with its HGVS UTR range. codonS/
// codonE span the whole UTR portion of this exon.
boolean posStrand = (lf->orientation >= 0);
int gFivePrime = posStrand ? codonS : codonE - 1;
int gThreePrime = posStrand ? codonE - 1 : codonS;
char loBuf[16], hiBuf[16];
utrHgvsCoord(lf, gFivePrime, loBuf, sizeof(loBuf));
utrHgvsCoord(lf, gThreePrime, hiBuf, sizeof(hiBuf));
@@ -4298,30 +4314,49 @@
boolean itemColorOverride(struct track *tg, void *item, Color *retColor, boolean *retWholeItem)
/* If the user set a per-item color for this item (via right-click), return TRUE and fill in the
* color and whether it recolors the whole item; otherwise return FALSE. Lets non-linkedFeatures
* draw routines (e.g. bedDrawSimpleAt) honor right-click item colors. */
{
struct itemColorSpec *spec = itemColorLookup(tg, item);
if (spec == NULL)
return FALSE;
if (retColor != NULL)
*retColor = spec->color;
if (retWholeItem != NULL)
*retWholeItem = spec->wholeItem;
return TRUE;
}
+static MgFont *squishCodonFont()
+/* Pick a small-but-readable amino-acid font for the short squish rows, where the
+ * full track font is taller than the row and gets clipped. We want ~9px. The
+ * built-in fonts jump from 8px straight to 11px, but the FreeType engine can
+ * render an in-between size, so use a 9px font there; under the GEM bitmap engine
+ * (which cannot fake a size) fall back to the size-8 font. */
+{
+boolean freetypeActive =
+#ifdef USE_FREETYPE
+ sameString(cfgOptionDefault("freeType", "on"), "on")
+#else
+ FALSE
+#endif
+ && differentString(tl.textFont, "Bitmap");
+if (freetypeActive)
+ return mgFontForCellHeight(10); // getFontCorrection(10) renders ~9px
+return mgFontForSize("8");
+}
+
void linkedFeaturesDrawAt(struct track *tg, void *item,
struct hvGfx *hvg, int xOff, int y, double scale,
MgFont *font, Color color, enum trackVisibility vis)
/* Draw a single simple bed item at position. */
{
struct linkedFeatures *lf = item;
struct simpleFeature *sf, *components;
int heightPer = tg->heightPer;
int x1,x2;
int shortOff = heightPer/4;
int shortHeight = heightPer - 2*shortOff;
int tallStart, tallEnd, s, e, e2, s2;
Color bColor;
int intronGap = 0;
boolean chainLines = ((vis != tvDense)&&(tg->subType == lfSubChain));
@@ -4380,30 +4415,42 @@
// user-chosen per-item color (right-click "Color this item"): recolor the whole glyph or
// fall back to a background highlight, unless the item is already highlighted.
struct itemColorSpec *userColorSpec = itemColorLookup(tg, lf);
if (userColorSpec != NULL)
{
if (userColorSpec->wholeItem)
color = bColor = userColorSpec->color;
else if (lf->highlightColor == 0)
{
lf->highlightColor = userColorSpec->color;
lf->highlightMode = highlightBackground;
}
}
+/* In squish the codon/CDS strand chevrons are too busy for the short rows, so
+ * turn them off and rely on the (thinned, widely spaced) intron barbs for the
+ * strand cue. Also shrink the amino-acid font so the letters fit the row
+ * instead of being clipped by it. */
+MgFont *codonFont = font;
+baseColorDrawCodonArrows = TRUE;
+if (vis == tvSquish)
+ {
+ baseColorDrawCodonArrows = FALSE;
+ codonFont = squishCodonFont();
+ }
+
struct genePred *gp = NULL;
if (startsWith("genePred", tg->tdb->type) || startsWith("bigGenePred", tg->tdb->type))
gp = (struct genePred *)(lf->original);
boolean baseColorNeedsCodons = (drawOpt == baseColorDrawItemCodons ||
drawOpt == baseColorDrawDiffCodons ||
drawOpt == baseColorDrawGenomicCodons);
if (psl && baseColorNeedsCodons)
{
boolean isXeno = ((tg->subType == lfSubXeno) || (tg->subType == lfSubChain) ||
startsWith("mrnaBla", tg->table));
int sizeMul = pslIsProtein(psl) ? 3 : 1;
lf->codons = baseColorCodonsFromPsl(lf, psl, sizeMul, isXeno, maxShade, drawOpt, tg);
}
else if (drawOpt > baseColorDrawOff)
@@ -4446,37 +4493,51 @@
// draw the item slightly smaller, and re-center the thin (UTR) boxes for the
// reduced height so they stay symmetric within the highlight
y++;
heightPer -=2;
shortOff = heightPer/4;
shortHeight = heightPer - 2*shortOff;
}
if (!hideLine)
{
innerLine(hvg, x1, midY, w, color);
}
if (!hideArrows)
{
- if ((intronGap == 0) && (vis == tvFull || vis == tvPack))
+ if ((intronGap == 0) && (vis == tvFull || vis == tvPack || vis == tvSquish))
+ {
+ int barbHeight = tl.barbHeight;
+ int barbSpacing = tl.barbSpacing;
+ if (vis == tvSquish)
{
+ /* Keep squish subtle: thin barbs sized to the short row, and spaced
+ * 4x wider so there are far fewer of them. In squish the codon/CDS
+ * chevrons are turned off, so these intron barbs are the strand cue. */
+ barbHeight = (heightPer-1)/2;
+ if (barbHeight > tl.barbHeight)
+ barbHeight = tl.barbHeight;
+ if (barbHeight < 1)
+ barbHeight = 1;
+ barbSpacing = tl.barbSpacing*4;
+ }
if (lf->highlightColor && (lf->highlightMode == highlightOutline))
- clippedBarbs(hvg, x1, midY, w, tl.barbHeight, tl.barbSpacing,
+ clippedBarbs(hvg, x1, midY, w, barbHeight, barbSpacing,
lf->orientation, lf->highlightColor, FALSE);
else
- clippedBarbs(hvg, x1, midY, w, tl.barbHeight, tl.barbSpacing,
+ clippedBarbs(hvg, x1, midY, w, barbHeight, barbSpacing,
lf->orientation, bColor, FALSE);
}
}
components = (lf->codons && zoomedToCdsColorLevel) ? lf->codons : lf->components;
/* For direction barbs, merge blocks that touch in pixel space into a single
* span (accumulated in the loop below) so the chevrons run continuously across
* them. This matters for chains, whose blocks smash together when zoomed out:
* individually most are too narrow to hold a chevron. barbRunX1 < 0 means no
* run is currently open. */
static int barbMergePixels = -1; // max pixel gap between blocks still merged for
if (barbMergePixels < 0) // barbs; hg.conf barbMergePixels, 0 disables merging
barbMergePixels = atoi(cfgOptionDefault("barbMergePixels", "3"));
Color barbColor = hvGfxContrastingColor(hvg, color);
@@ -4515,31 +4576,31 @@
}
else
{
drawScaledBox(hvg, s2, e, scale, xOff, y+shortOff, shortHeight,
color);
}
e = s2;
}
/* Draw "tall" portion of exon (or codon) */
if (e > s)
{
if (drawOpt > baseColorDrawOff
&& e + 6 >= winStart
&& s - 6 < winEnd
&& (e-s <= 3 || !baseColorNeedsCodons))
- baseColorDrawItem(tg, lf, sf->grayIx, hvg, xOff, y, scale, font, s, e, heightPer,
+ baseColorDrawItem(tg, lf, sf->grayIx, hvg, xOff, y, scale, codonFont, s, e, heightPer,
zoomedToCodonLevel, qSeq, qOffset, sf, psl, drawOpt, MAXPIXELS, winStart,
color);
else
{
if (lf->highlightColor && (lf->highlightMode == highlightOutline))
{
drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, lf->highlightColor);
drawScaledBox(hvg, s, e, scale, xOff+1, y+1, heightPer-2, color);
}
else
{
if (tg->drawLabelInBox &&
!(tg->drawLabelInBoxNotDense && vis == tvDense))
{
drawScaledBoxLabel(hvg, s, e, scale, xOff, y, heightPer,
@@ -4598,30 +4659,36 @@
if (barbRunX1 >= 0)
clippedBarbs(hvg, barbRunX1+1, midY, barbRunX2-barbRunX1-2,
tl.barbHeight, tl.barbSpacing, lf->orientation, barbColor, TRUE);
if ((intronGap > 0) || chainLines)
lfDrawSpecialGaps(lf, intronGap, chainLines, gapFactor,
tg, hvg, xOff, y, scale, color, bColor, vis);
if (vis != tvDense)
{
/* If highlighting differences between aligned sequence and genome when
* zoomed way out, this must be done in a separate pass after exons are
* drawn so that exons sharing the pixel don't overdraw differences. */
baseColorOverdrawDiff(tg, lf, hvg, xOff, y, scale, heightPer,
qSeq, qOffset, psl, winStart, drawOpt);
+ /* When codons are colored, distribute strand arrows across the exons on top
+ * of the boxes (coding exons when too small to label, plus the UTRs). Not
+ * in squish, where these are too busy for the short rows and the strand is
+ * shown by the intron barbs instead. */
+ if (vis != tvSquish)
+ baseColorDrawCdsArrows(tg, lf, hvg, xOff, y, scale, heightPer, winStart, drawOpt, color);
if (psl && (indelShowQueryInsert || indelShowPolyA))
baseColorOverdrawQInsert(tg, lf, hvg, xOff, y, scale, heightPer,
qSeq, qOffset, psl, font, winStart, drawOpt,
indelShowQueryInsert, indelShowPolyA);
}
}
static void lfSeriesDrawConnecter(struct linkedFeaturesSeries *lfs,
struct hvGfx *hvg, int start, int end, double scale, int xOff,
int midY, Color color, Color bColor, enum trackVisibility vis)
/* Draw connection between two sets of linked features. */
{
if (start != -1 && !lfs->noLine)
{
int x1 = round((double)((int)start-winStart)*scale) + xOff;