4d36c5c0a9c01a918b9f54ee7e46d6373bfe89f3
braney
  Wed Sep 16 11:39:00 2026 -0700
hgTracks: a dense row can have one clickable map box per item, refs #38364

In dense the whole row is covered by a single map box that switches the
track to pack, so nothing inside a dense track points at a details page.
To look at an item you have to expand the track, which reloads the page
and changes the layout you just set up.

With denseClick on, genericDrawItemsFullDense puts down one map box per
item as it draws the row.  It does this in the draw pass, where the scale
and x offset for the window are already in hand, so multi-region windows
come out right.  The whole draw loop runs before the loop that calls
doTrackMap, and map items print in creation order, so the per-item boxes
come first in the image map and the existing whole-row box still catches
the gaps between items.  Clicking empty space in the row, the center
label, or the right-click menu all still change the visibility.  Nothing
had to be removed and the drawn image does not change.

A dense row can hold tens of thousands of items, so denseMapItem keeps
one flag per pixel of the row and skips an item whose pixels are all
claimed by an earlier one.  Such a box sits under the earlier one and
could never be clicked.  That bounds the row at one box per pixel: hg38
simpleRepeat in dense across chr1 at pix=1200 goes from 74,515 map boxes
and an 18.5 MB page to 969 boxes and 525 KB.  It also decides what
happens when several items share a pixel, which is that the first one in
the list wins.

Three copies of the same early return, all commented "Don't bother if we
are imageV2 and a dense child", kept composite subtracks out of this.
They now test denseClickEnabled too, which is what lets the hg38
hprcPclai haplotypes work.  The fourth copy in wigTrack.c puts down a
whole-track box rather than a per-item one and is left alone.

Off by default.  Set denseClick in hg.conf for every dense track, or a
denseClick trackDb setting for one track, which inherits to a container's
subtracks.  The trackDb setting is not documented yet.

diff --git src/hg/hgTracks/simpleTracks.c src/hg/hgTracks/simpleTracks.c
index 3d0fda03bc2..942d37cf089 100644
--- src/hg/hgTracks/simpleTracks.c
+++ src/hg/hgTracks/simpleTracks.c
@@ -5034,32 +5034,34 @@
 char *type = tg->tdb->type;
 if (startsWith("bigGenePred", type) || startsWith("genePred", type))
     defVal = "on";
 
 boolean exonNumbers = sameString(trackDbSettingOrDefault(tg->tdb, "exonNumbers", defVal), "on");
 return (withExonNumbers && exonNumbers && (vis==tvSquish || vis==tvFull || vis==tvPack) && (winEnd - winStart < 400000)
  && (tg->nextPrevExon==linkedFeaturesNextPrevItem));
 }
 
 void genericMapItem(struct track *tg, struct hvGfx *hvg, void *item,
 		    char *itemName, char *mapItemName, int start, int end,
 		    int x, int y, int width, int height)
 /* This is meant to be used by genericDrawItems to set to tg->mapItem in */
 /* case tg->mapItem isn't set to anything already. */
 {
-// Don't bother if we are imageV2 and a dense child.
-if (!theImgBox || tg->limitedVis != tvDense || !tdbIsCompositeChild(tg->tdb))
+// Don't bother if we are imageV2 and a dense child, unless denseClick is on for
+// this track, in which case the dense row is meant to be clickable per item.
+if (!theImgBox || tg->limitedVis != tvDense || !tdbIsCompositeChild(tg->tdb)
+||  denseClickEnabled(tg))
     {
     char *directUrl = trackDbSetting(tg->tdb, "directUrl");
     boolean withHgsid = (trackDbSetting(tg->tdb, "hgsid") != NULL);
     char *trackName = tg->track;
     if (tg->originalTrack != NULL)
         trackName = tg->originalTrack;
     mapBoxHgcOrHgGene(hvg, start, end, x, y, width, height, trackName,
                       mapItemName, itemName, directUrl, withHgsid, NULL);
     }
 }
 
 void genericDrawNextItemStuff(struct track *tg, struct hvGfx *hvg, enum trackVisibility vis,
                               struct slList *item, double scale, int x2, int x1, int textX, int y, int heightPer,
                               Color color, boolean doButtons)
 /* After the item is drawn in genericDrawItems, draw next/prev item related */
@@ -5910,43 +5912,101 @@
 if (!isNextItemCompatible && !isExonNumberMapsCompatible)
     return;
 boolean doButtons = (isExonNumberMapsCompatible ? FALSE: TRUE);
 
 // Convert start/end coordinates to pix
 int s = tg->itemStart(tg, item);
 int e = tg->itemEnd(tg, item);
 int sClp = (s < winStart) ? winStart : s;
 int eClp = (e > winEnd)   ? winEnd   : e;
 int x1 = round((sClp - winStart)*scale) + xOff;
 int x2 = round((eClp - winStart)*scale) + xOff;
 genericDrawNextItemStuff(tg, hvg, vis, item, scale, x2, x1, -1, y, tg->heightPer, color,
                             doButtons);
 }
 
+boolean denseClickEnabled(struct track *tg)
+/* Should a dense row of this track get one clickable map box per item, instead
+ * of a single box that expands the track?  Off unless denseClick is set in
+ * hg.conf, and a track can override that with a denseClick trackDb setting. */
+{
+char *setting = trackDbSetting(tg->tdb, "denseClick");
+if (setting != NULL)
+    return trackDbSettingOn(tg->tdb, "denseClick");
+return cfgOptionBooleanDefault("denseClick", FALSE);
+}
+
+static void denseMapItem(struct track *tg, struct hvGfx *hvg, struct slList *item,
+                         int xOff, int y, int width, double scale, char *pixelUsed)
+/* Put down a map box for one item of a dense row, so that a click on it reaches
+ * the item's details page instead of expanding the track.  A dense row can hold
+ * tens of thousands of items, so skip an item whose every pixel already belongs
+ * to an earlier item: that box would sit under the earlier one and could never
+ * be clicked.  This holds the row to at most one box per pixel. */
+{
+int s = tg->itemStart(tg, item);
+int e = tg->itemEnd(tg, item);
+int sClp = (s < winStart) ? winStart : s;
+int eClp = (e > winEnd)   ? winEnd   : e;
+int x1 = round((sClp - winStart)*scale) + xOff;
+int x2 = round((eClp - winStart)*scale) + xOff;
+if (x2 <= x1)
+    x2 = x1 + 1;
+int p1 = x1 - xOff;
+int p2 = x2 - xOff;
+if (p1 < 0)
+    p1 = 0;
+if (p2 > width)
+    p2 = width;
+if (p1 >= p2)
+    return;
+boolean anyFree = FALSE;
+int p;
+for (p = p1;  p < p2;  ++p)
+    {
+    if (!pixelUsed[p])
+        {
+        anyFree = TRUE;
+        pixelUsed[p] = TRUE;
+        }
+    }
+if (!anyFree)
+    return;
+tg->mapItem(tg, hvg, item, tg->itemName(tg, item), tg->mapItemName(tg, item),
+            s, e, x1, y, x2 - x1, tg->heightPer);
+}
+
 static void genericDrawItemsFullDense(struct track *tg, int seqStart, int seqEnd,
                                       struct hvGfx *hvg, int xOff, int yOff, int width,
                                       MgFont *font, Color color, enum trackVisibility vis)
 /* genericDrawItems logic for full and dense modes */
 {
 double scale = scaleForWindow(width, seqStart, seqEnd);
 struct slList *item;
 int y = yOff;
+/* In dense the whole row is normally one box that expands the track.  With
+ * denseClick on, each item gets its own box instead; the whole-row box that
+ * doTrackMap puts down afterwards still covers the gaps between items. */
+boolean denseMaps = (vis == tvDense && width > 0 && !tg->mapsSelf && denseClickEnabled(tg));
+char *pixelUsed = (denseMaps ? needMem(width) : NULL);
 for (item = tg->items; item != NULL; item = item->next)
     {
     if (tg->itemColor != NULL)
         color = tg->itemColor(tg, item, hvg);
     tg->drawItemAt(tg, item, hvg, xOff, y, scale, font, color, vis);
+    if (denseMaps)
+        denseMapItem(tg, hvg, item, xOff, y, width, scale, pixelUsed);
     if (vis == tvFull)
         {
         /* The doMapItems will make the mapboxes normally but make */
         /* them here if we're drawing nextItem buttons. */
         if (nextItemCompatible(tg))
 #ifdef IMAGEv2_SHORT_MAPITEMS
             {
             // Convert start/end coordinates to pix
             int s = tg->itemStart(tg, item);
             int e = tg->itemEnd(tg, item);
             int sClp = (s < winStart) ? winStart : s;
             int eClp = (e > winEnd)   ? winEnd   : e;
             int x1 = round((sClp - winStart)*scale) + xOff;
             int x2 = round((eClp - winStart)*scale) + xOff;
         #ifdef IMAGEv2_NO_LEFTLABEL_ON_FULL
@@ -5980,30 +6040,31 @@
         else if (exonNumberMapsCompatible(tg, vis))
             {
             // Convert start/end coordinates to pix
             int s = tg->itemStart(tg, item);
             int e = tg->itemEnd(tg, item);
             int sClp = (s < winStart) ? winStart : s;
             int eClp = (e > winEnd)   ? winEnd   : e;
             int x1 = round((sClp - winStart)*scale) + xOff;
             int x2 = round((eClp - winStart)*scale) + xOff;
             genericDrawNextItemStuff(tg, hvg, vis, item, scale, x2, x1, -1, y, tg->heightPer, color, FALSE); // was -1, -1, -1
 	    }
 #endif//ndef IMAGEv2_SHORT_MAPITEMS
         y += tg->lineHeight;
         }
     }
+freez(&pixelUsed);
 }
 
 void genericDrawItems(struct track *tg, int seqStart, int seqEnd,
                       struct hvGfx *hvg, int xOff, int yOff, int width,
                       MgFont *font, Color color, enum trackVisibility vis)
 /* Draw generic item list.  Features must be fixed height
  * and tg->drawItemAt has to be filled in. */
 {
 withIndividualLabels = TRUE;  // set this back to default just in case someone left it false (I'm looking at you pgSnp)
 
 color = colorFromCart(tg, color);
 
 if (tg->mapItem == NULL)
     tg->mapItem = genericMapItem;
 if (vis != tvDense && baseColorCanDraw(tg))
@@ -6269,32 +6330,34 @@
 int linkedFeaturesItemEnd(struct track *tg, void *item)
 /* Return end chromosome coordinate of item. */
 {
 struct linkedFeatures *lf = item;
 return lf->end;
 }
 
 
 void linkedFeaturesMapItem(struct track *tg, struct hvGfx *hvg, void *item,
 				char *itemName, char *mapItemName, int start, int end,
 				int x, int y, int width, int height)
 /* Draw the mouseOver (aka statusLine) text from the mouseOver field of lf
  * Fallback to itemName if there is no mouseOver field.
  * (derived from genericMapItem) */
 {
-// Don't bother if we are imageV2 and a dense child.
-if (theImgBox && tg->limitedVis == tvDense && tdbIsCompositeChild(tg->tdb))
+// Don't bother if we are imageV2 and a dense child, unless denseClick is on for
+// this track, in which case the dense row is meant to be clickable per item.
+if (theImgBox && tg->limitedVis == tvDense && tdbIsCompositeChild(tg->tdb)
+&&  !denseClickEnabled(tg))
     return;
 
 struct linkedFeatures *lf = item;
 
 char *newItemName   = (isEmpty(lf->mouseOver)) ? itemName: lf->mouseOver;
 
 // copied from genericMapItem
 char *directUrl = trackDbSetting(tg->tdb, "directUrl");
 boolean withHgsid = (trackDbSetting(tg->tdb, "hgsid") != NULL);
 char *trackName = tg->track;
 if (tg->originalTrack != NULL)
     trackName = tg->originalTrack;
 mapBoxHgcOrHgGene(hvg, start, end, x, y, width, height, trackName,
                   mapItemName, newItemName, directUrl, withHgsid, NULL);
 }
@@ -6817,32 +6880,34 @@
 hvGfxBox(hvg, x1, y, w, heightPer, color);
 
 // In full mode, draw bpl->label to the left of item:
 if (vis == tvFull)
     {
     int textWidth = mgFontStringWidth(font, bpl->label);
     hvGfxTextRight(hvg, x1-textWidth-2, y, textWidth, heightPer, color, font, bpl->label);
     }
 }
 
 static void bedPlusLabelMapItem(struct track *tg, struct hvGfx *hvg, void *item,
 				char *itemName, char *mapItemName, int start, int end,
 				int x, int y, int width, int height)
 /* Special mouseover text from item->label. (derived from genericMapItem) */
 {
-// Don't bother if we are imageV2 and a dense child.
-if(!theImgBox || tg->limitedVis != tvDense || !tdbIsCompositeChild(tg->tdb))
+// Don't bother if we are imageV2 and a dense child, unless denseClick is on for
+// this track, in which case the dense row is meant to be clickable per item.
+if(!theImgBox || tg->limitedVis != tvDense || !tdbIsCompositeChild(tg->tdb)
+|| denseClickEnabled(tg))
     {
     struct bedPlusLabel *bpl = item;;
     char *mouseOverText = isEmpty(bpl->label) ? bpl->bed.name : bpl->label;
     mapBoxHc(hvg, start, end, x, y, width, height, tg->track, mapItemName, mouseOverText);
     }
 }
 
 static char *collapseRowsFromQuery(char *db, char *query, char *sep, int limit)
 /* Return a string that is the concatenation of (up to limit) row[0]'s returned from query,
  * separated by sep.  Don't free the return value! */
 {
 static struct dyString *dy = NULL;
 if (dy == NULL)
     dy = dyStringNew(0);
 dyStringClear(dy);