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);