48886c882e7198f01b7c6bcaffa9ef107a291538 braney Tue Jul 21 13:38:08 2026 -0700 hgTracks: merge pixel-adjacent blocks when drawing direction barbs Chain tracks show direction chevrons only on their aligned blocks, not on the connector line between blocks. When zoomed out the blocks smash together and individually most are too narrow to hold a chevron, so a chain showed only a couple of chevrons and its orientation was nearly invisible. Accumulate blocks that touch in pixel space into a single span and draw the barbs across the whole span, so the chevrons run continuously across a run of smashed-together blocks. The gap under which blocks merge is the new hg.conf setting barbMergePixels (default 3); set it to 0 to disable merging and get the old per-block barbs. Only pixel-adjacent blocks merge, so genes with visible introns and everything else render unchanged, refs #37889 diff --git src/hg/hgTracks/simpleTracks.c src/hg/hgTracks/simpleTracks.c index 515b60ed6b0..ab5191f33b5 100644 --- src/hg/hgTracks/simpleTracks.c +++ src/hg/hgTracks/simpleTracks.c @@ -4404,30 +4404,41 @@ if (!hideArrows) { if ((intronGap == 0) && (vis == tvFull || vis == tvPack)) { if (lf->highlightColor && (lf->highlightMode == highlightOutline)) clippedBarbs(hvg, x1, midY, w, tl.barbHeight, tl.barbSpacing, lf->orientation, lf->highlightColor, FALSE); else clippedBarbs(hvg, x1, midY, w, tl.barbHeight, tl.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); +int barbRunX1 = -1; +int barbRunX2 = -1; for (sf = components; sf != NULL; sf = sf->next) { s = sf->start; e = sf->end; /* Draw UTR portion(s) of exon, if any: */ if (s < tallStart) { e2 = e; if (e2 > tallStart) e2 = tallStart; if (lf->highlightColor && (lf->highlightMode == highlightOutline)) { drawScaledBox(hvg, s, e2, scale, xOff, y+shortOff , shortHeight , lf->highlightColor); drawScaledBox(hvg, s, e2, scale, xOff + 1, y+shortOff + 1, shortHeight - 2, color); } @@ -4484,46 +4495,66 @@ } else drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, color); } /* Display barbs only if no intron is visible on the item. This occurs when the exon completely spans the window, or when it is the first or last intron in the feature and the following/preceding intron isn't visible */ if (exonArrowsAlways || ( exonArrows && (sf->start <= winStart || sf->start == lf->start) && (sf->end >= winEnd || sf->end == lf->end))) { - Color barbColor = hvGfxContrastingColor(hvg, color); // This scaling of bases to an image window occurs in several places. // It should really be broken out into a function. - if (s < winStart) - s = winStart; - if (e > winEnd) - e = winEnd; - x1 = round((double)((int)s-winStart)*scale) + xOff; - x2 = round((double)((int)e-winStart)*scale) + xOff; - w = x2-x1; - clippedBarbs(hvg, x1+1, midY, x2-x1-2, tl.barbHeight, tl.barbSpacing, - lf->orientation, barbColor, TRUE); + int bs = s, be = e; + if (bs < winStart) + bs = winStart; + if (be > winEnd) + be = winEnd; + x1 = round((double)((int)bs-winStart)*scale) + xOff; + x2 = round((double)((int)be-winStart)*scale) + xOff; + if (barbRunX1 < 0) + { // start a new run + barbRunX1 = x1; + barbRunX2 = x2; } + else if (barbMergePixels > 0 && x1 <= barbRunX2 + barbMergePixels) + { // this block touches the run: extend it + if (x2 > barbRunX2) + barbRunX2 = x2; + } + else + { // real gap: flush the run and start a new one + clippedBarbs(hvg, barbRunX1+1, midY, barbRunX2-barbRunX1-2, + tl.barbHeight, tl.barbSpacing, lf->orientation, + barbColor, TRUE); + barbRunX1 = x1; + barbRunX2 = x2; } } } + } + } + +/* Flush the final merged barb run. */ +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). */ baseColorDrawCdsArrows(tg, lf, hvg, xOff, y, scale, heightPer, winStart, drawOpt, color);