54a41e9d2247122e363b0ce29594d8aeddf7381e jcasper Mon Mar 24 09:50:10 2025 -0700 Make interact hgc link boxes better, add an option to suppress them in trackDb, and finish implementing arcs everywhere. refs #30380 diff --git src/hg/hgTracks/interactTrack.c src/hg/hgTracks/interactTrack.c index e52f5030e81..2581271410e 100644 --- src/hg/hgTracks/interactTrack.c +++ src/hg/hgTracks/interactTrack.c @@ -479,112 +479,191 @@ return width; } /* Draw helper routines */ #define DRAW_LINE 0 #define DRAW_CURVE 1 #define DRAW_ELLIPSE 2 static void drawFoot(struct track *tg, struct hvGfx *hvg, char *seq, int seqStart, int seqEnd, int x, int y, int width, Color color, boolean drawUp, struct hash *footHash) /* Draw interaction end, 2 pixels high. Force to black if it exactly overlaps another */ { char buf[256]; safef(buf, sizeof(buf), "%s:%d-%d", seq, seqStart, seqEnd); -char *pos = cloneString(buf); Color footColor = color; -if (hashLookup(footHash, pos)) +if (hashLookup(footHash, buf)) footColor = MG_BLACK; else - hashStore(footHash, pos); + hashStore(footHash, buf); if (drawUp) y = flipY(tg, y) - 2; hvGfxBox(hvg, x, y, width, 2, footColor); } static void drawLine(struct track *tg, struct hvGfx *hvg, int x1, int y1, int x2, int y2, Color color, boolean isDashed, boolean drawUp) /* Draw vertical or horizontal */ { if (drawUp) { y1 = flipY(tg, y1); y2 = flipY(tg, y2); } if (isDashed) hvGfxDottedLine(hvg, x1, y1, x2, y2, color, TRUE); else hvGfxLine(hvg, x1, y1, x2, y2, color); } static void drawFootMapbox(struct track *tg, struct hvGfx *hvg, int start, int end, char *item, char *status, int x, int y, int width, Color peakColor, Color highlightColor, boolean drawUp) /* Draw grab box and add map box */ { +if (trackDbSettingClosestToHomeOn(tg->tdb, INTERACT_NO_HGC)) + return; // Add var to identify endpoint ('foot'), or NULL if no name for endpoint */ char *clickArg = NULL; if (!isEmptyTextField(item)) { char buf[256]; safef(buf, sizeof(buf),"foot=%s", cgiEncode(item)); clickArg = cloneString(buf); } char *itemBuf = isEmptyTextField(item) ? status : item; if (drawUp) y = flipY(tg, y) - 3; hvGfxBox(hvg, x-1, y, 3, 2, peakColor); hvGfxBox(hvg, x, y, 1, 1, highlightColor); mapBoxHgcOrHgGene(hvg, start, end, x - width, y, width * 2, 4, tg->track, item, itemBuf, NULL, TRUE, clickArg); } void drawPeakMapbox(struct track *tg, struct hvGfx *hvg, int seqStart, int seqEnd, char *item, char *status, int x, int y, Color peakColor, Color highlightColor, boolean drawUp) /* Draw grab box and add map box */ { +if (trackDbSettingClosestToHomeOn(tg->tdb, INTERACT_NO_HGC)) + return; + if (drawUp) y = flipY(tg, y); +/* hvGfxBox(hvg, x-1, y-1, 3, 3, peakColor); hvGfxBox(hvg, x, y, 1, 1, highlightColor); mapBoxHgcOrHgGene(hvg, seqStart, seqEnd, x-1, y-1, 3, 3, tg->track, item, status, NULL, TRUE, NULL); +*/ +hvGfxBox(hvg, x-3, y-3, 7, 7, peakColor); +hvGfxBox(hvg, x-2, y-2, 5, 5, highlightColor); +mapBoxHgcOrHgGene(hvg, seqStart, seqEnd, x-3, y-3, 7, 7, + tg->track, item, status, NULL, TRUE, NULL); +} + +static void drawOtherChromItem(struct track *tg, int seqStart, int seqEnd, + struct hvGfx *hvg, int xOff, int yOff, int width, + MgFont *font, Color color, enum trackVisibility vis, + int draw, struct interact *inter, struct interactTrackInfo *tInfo, + char *statusBuf, int peakColor, int highlightColor, boolean drawUp, + boolean doDashes, char *itemBuf, struct hash *footHashOther) +/* Breaking out the draw code for interactions between chromosomes into its + * own function to give things a bit more compartmentalization. This is + * called by drawInteractItems */ +{ +char buffer[1024]; +int height = 0; +int yOffOther = yOff; + +double scale = scaleForWindow(width, seqStart, seqEnd); + +if (vis == tvDense) + { + height = tg->height; + } +else + { + height = tInfo->otherHeight/2; + yOffOther = yOff + tInfo->sameHeight; + } +unsigned r = interactRegionCenter(inter->chromStart, inter->chromEnd); +int x = getX(r, seqStart, scale, xOff); +int footWidth = regionFootWidth(inter->chromStart, inter->chromEnd, scale); +unsigned yPos = yOffOther + height; + +// draw the foot (2 pixels high) +drawFoot(tg, hvg, inter->chrom, inter->chromStart, inter->chromEnd, + x - footWidth, yOffOther, footWidth + footWidth + 1, color, drawUp, footHashOther); + +// draw the vertical +boolean isReversed = tInfo->isDirectional && + differentString(inter->chrom, inter->sourceChrom); +drawLine(tg, hvg, x, yOffOther, x, yPos, color, isReversed && doDashes, drawUp); + +if (vis == tvDense) + return; + +// add map box to foot + +char *nameBuf = (inter->chromStart == inter->sourceStart ? inter->sourceName : inter->targetName); +drawFootMapbox(tg, hvg, inter->chromStart, inter->chromEnd, nameBuf, statusBuf, + x - footWidth, yOffOther, footWidth, peakColor, highlightColor, drawUp); + +// add map box to vertical +mapBoxHgcOrHgGene(hvg, inter->chromStart, inter->chromEnd, x - 2, yOffOther, 4, + height, tg->track, itemBuf, statusBuf, NULL, TRUE, NULL); +if (tInfo->doOtherLabels) + { + // draw label + safef(buffer, sizeof buffer, "%s", sameString(inter->chrom, inter->sourceChrom) ? + inter->targetChrom : inter->sourceChrom); + yPos += 3; + if (drawUp) + yPos = flipY(tg, yPos + 6); + hvGfxTextCentered(hvg, x, yPos, 4, 4, MG_BLUE, font, buffer); + int labelWidth = vgGetFontStringWidth(hvg->vg, font, buffer); + + // add map box to label + mapBoxHgcOrHgGene(hvg, inter->chromStart, inter->chromEnd, x - labelWidth/2, + yPos, labelWidth, tInfo->fontHeight, tg->track, itemBuf, statusBuf, + NULL, TRUE, NULL); } +} + static void drawInteractItems(struct track *tg, int seqStart, int seqEnd, struct hvGfx *hvg, int xOff, int yOff, int width, MgFont *font, Color color, enum trackVisibility vis) /* Draw a list of interact items with connectors (e.g. curves) */ { // Determine drawing mode int draw = DRAW_LINE; boolean doDashes = FALSE; if (vis != tvDense) { char *drawMode = cartUsualStringClosestToHome(cart, tg->tdb, FALSE, INTERACT_DRAW, INTERACT_DRAW_DEFAULT); if (sameString(drawMode, INTERACT_DRAW_CURVE)) draw = DRAW_CURVE; else if (sameString(drawMode, INTERACT_DRAW_ELLIPSE)) draw = DRAW_ELLIPSE; doDashes = cartUsualBooleanClosestToHome(cart, tg->tdb, FALSE, INTERACT_DIRECTION_DASHES, INTERACT_DIRECTION_DASHES_DEFAULT); } double scale = scaleForWindow(width, seqStart, seqEnd); struct interact *inter = NULL; -char buffer[1024]; char itemBuf[2048]; // Gather info for layout interactGetLayoutInfo(tg, seqStart, hvg, xOff, font, scale); struct interactTrackInfo *tInfo = (struct interactTrackInfo *)tg->customPt; setYOff(tg, yOff); // TODO: better to stash this in tInfo, and save that in track struct */ int highlightColor = MG_WHITE; boolean drawUp = trackDbSettingClosestToHomeOn(tg->tdb, INTERACT_UP) && vis == tvFull; // Get spectrum range int scoreMin = atoi(trackDbSettingClosestToHomeOrDefault(tg->tdb, "scoreMin", "0")); int scoreMax = atoi(trackDbSettingClosestToHomeOrDefault(tg->tdb, "scoreMax", "1000")); // Draw items struct hash *footHash = hashNew(0); // track feet so we can override color to black @@ -597,188 +676,183 @@ // Pick colors #define MG_LIGHT_MAGENTA 0xffffbbff #define MG_LIGHT_GRAY 0xff909090 color = interactItemColor(tg, inter, hvg, scoreMin, scoreMax); if (vis == tvDense && otherChrom && color == MG_BLACK) // use highlight color for other chrom items in dense mode color = MG_LIGHT_MAGENTA; int peakColor = (color == MG_BLACK || tg->colorShades) ? MG_LIGHT_MAGENTA : MG_LIGHT_GRAY; if (otherChrom) { // different chromosomes // draw below same chrom items, if any - int height = 0; - int yOffOther = yOff; - if (vis == tvDense) - { - height = tg->height; - } - else - { - height = tInfo->otherHeight/2; - yOffOther = yOff + tInfo->sameHeight; - } - unsigned r = interactRegionCenter(inter->chromStart, inter->chromEnd); - int x = getX(r, seqStart, scale, xOff); - int footWidth = regionFootWidth(inter->chromStart, inter->chromEnd, scale); - unsigned yPos = yOffOther + height; - - // draw the foot (2 pixels high) - drawFoot(tg, hvg, inter->chrom, inter->chromStart, inter->chromEnd, - x - footWidth, yOffOther, footWidth + footWidth + 1, color, drawUp, footHashOther); - - // draw the vertical - boolean isReversed = tInfo->isDirectional && - differentString(inter->chrom, inter->sourceChrom); - drawLine(tg, hvg, x, yOffOther, x, yPos, color, isReversed && doDashes, drawUp); - - if (vis == tvDense) - continue; - - // add map box to foot - - char *nameBuf = (inter->chromStart == inter->sourceStart ? - inter->sourceName : inter->targetName); - drawFootMapbox(tg, hvg, inter->chromStart, inter->chromEnd, nameBuf, statusBuf, - x - footWidth, yOffOther, footWidth, peakColor, highlightColor, drawUp); - - // add map box to vertical - mapBoxHgcOrHgGene(hvg, inter->chromStart, inter->chromEnd, x - 2, yOffOther, 4, - height, tg->track, itemBuf, statusBuf, NULL, TRUE, NULL); - if (tInfo->doOtherLabels) - { - // draw label - safef(buffer, sizeof buffer, "%s", sameString(inter->chrom, inter->sourceChrom) ? - inter->targetChrom : inter->sourceChrom); - yPos += 3; - if (drawUp) - yPos = flipY(tg, yPos + 6); - hvGfxTextCentered(hvg, x, yPos, 4, 4, MG_BLUE, font, buffer); - int labelWidth = vgGetFontStringWidth(hvg->vg, font, buffer); - - // add map box to label - mapBoxHgcOrHgGene(hvg, inter->chromStart, inter->chromEnd, x - labelWidth/2, - yPos, labelWidth, tInfo->fontHeight, tg->track, itemBuf, statusBuf, - NULL, TRUE, NULL); - } + drawOtherChromItem(tg, seqStart, seqEnd, hvg, xOff, yOff, width, font, color, vis, draw, inter, + tInfo, statusBuf, peakColor, highlightColor, drawUp, doDashes, itemBuf, footHashOther); continue; } - // Draw same chromosome interaction + // Else, draw same chromosome interaction // source region unsigned s = interactRegionCenter(inter->sourceStart, inter->sourceEnd); int sX = getX(s, seqStart, scale, xOff); int sWidth = regionFootWidth(inter->sourceStart, inter->sourceEnd, scale); boolean sOnScreen = (s >= seqStart) && (s< seqEnd); // target region unsigned t = interactRegionCenter(inter->targetStart, inter->targetEnd); int tX = getX(t, seqStart, scale, xOff); int tWidth = regionFootWidth(inter->targetStart,inter->targetEnd, scale); boolean tOnScreen = (t >= seqStart) && (t< seqEnd); + // Reminder - isReversed just means source is downstream of target on the forward strand + // of the chromosome. It has nothing to do with the "reverse" view in the browser. boolean isReversed = (tInfo->isDirectional && t < s); int interSize = abs(t - s); int peakHeight = (tInfo->sameHeight - 15) * ((double)interSize / tInfo->maxSize) + 10; int peak = yOff + peakHeight; if (vis == tvDense) peak = yOff + tg->height; - // NOTE: until time permits, force to rectangle when in reversed strand mode. - int yTarget = yOff; int ySource = yOff; if (tInfo->offset && draw != DRAW_ELLIPSE) // ellipse code doesn't support assymetrical ends { int yOffset = tg->height/10 + 1; if (sameString(tInfo->offset, INTERACT_OFFSET_TARGET)) yTarget = yOff + yOffset; else if (sameString(tInfo->offset, INTERACT_OFFSET_SOURCE)) ySource = yOff + yOffset; } - unsigned footColor = color; + // Start with drawing the feet if they're on-screen + unsigned footColor = color; if (sOnScreen) { drawFoot(tg, hvg, inter->sourceChrom, inter->sourceStart, inter->sourceEnd, sX - sWidth, ySource, sWidth + sWidth + 1, footColor, drawUp, footHash); - if (vis == tvDense || !tOnScreen || draw == DRAW_LINE || hvg->rc) + if (vis == tvDense || (draw == DRAW_LINE)) { // draw vertical from foot to peak drawLine(tg, hvg, sX, ySource, sX, peak, color, isReversed && doDashes, drawUp); } } if (tOnScreen) { drawFoot(tg, hvg, inter->targetChrom, inter->targetStart, inter->targetEnd, tX - tWidth, yTarget, tWidth + tWidth + 1, footColor, drawUp, footHash); - if (vis == tvDense || !sOnScreen || draw == DRAW_LINE || hvg->rc) + if (vis == tvDense || (draw == DRAW_LINE)) { // draw vertical from foot to peak drawLine(tg, hvg, tX, yTarget, tX, peak, color, isReversed && doDashes, drawUp); } } if (vis == tvDense) continue; // Full mode: add map boxes and draw interaction if (sOnScreen) { - // draw grab box and map box to source region + // draw grab box and map box to source region. These are still practically invisible. drawFootMapbox(tg, hvg, inter->chromStart, inter->chromEnd, inter->sourceName, statusBuf, sX, ySource, sWidth, peakColor, highlightColor, drawUp); } if (tOnScreen) { // draw grab box and add map box to target region drawFootMapbox(tg, hvg, inter->chromStart, inter->chromEnd, inter->targetName, statusBuf, tX, yTarget, tWidth, tInfo->isDirectional ? MG_MAGENTA : peakColor, highlightColor, drawUp); } if ((s < seqStart && t < seqStart) || (s > seqEnd && t > seqEnd)) continue; // Draw interaction and map boxes int lowerX = 0, upperX = 0; - if (s < t) + if (s < t && !(hvg->rc)) { lowerX = sOnScreen ? sX : xOff; upperX = tOnScreen ? tX : xOff + width; } else { lowerX = tOnScreen ? tX : xOff; upperX = sOnScreen ? sX : xOff + width; } - if (draw == DRAW_LINE || !sOnScreen || !tOnScreen || hvg->rc) + + if (draw == DRAW_LINE || (!sOnScreen && !tOnScreen)) { // draw horizontal line between region centers at 'peak' height drawLine(tg, hvg, lowerX, peak, upperX, peak, color, isReversed && doDashes, drawUp); // draw grab box and map box on mid-point of horizontal line int xMap = lowerX + (double)(upperX-lowerX)/2; drawPeakMapbox(tg, hvg, inter->chromStart, inter->chromEnd, itemBuf, statusBuf, xMap, peak, peakColor, highlightColor, drawUp); continue; } - // Draw curves + + if (!tOnScreen || !sOnScreen) + { + // Only one endpoint is on the screen, and we're in curve or ellipse mode. + // Draw a curve that reaches to the appropriate screen edge. + int baseY, peakY = peak; + int startX, endX; // The start- and end-points for this curve + boolean offscreenLowerX = FALSE; // true if the off-screen side of the interaction has a lower X value + if (sOnScreen) + { + startX = sX; + baseY = ySource; + offscreenLowerX = (t < s); + } + else + { + startX = tX; + baseY = yTarget; + offscreenLowerX = (t > s); + } + + // Now we know whether the endpoint is on the left or right edge of the screen + if (offscreenLowerX) + endX = xOff; + else + endX = xOff+width; + + if (drawUp) + { + baseY = flipY(tg, baseY); + peakY = flipY(tg, peakY); + } + hvGfxCurve(hvg, startX, baseY, startX, peakY, endX, peakY, color, isReversed && doDashes); + + // place a mapbox at the end of the off-screen end of the curve + int mapBoxX = endX; + if (endX == xOff) + mapBoxX += 3; + else + mapBoxX -= 3; + drawPeakMapbox(tg, hvg, inter->chromStart, inter->chromEnd, inter->name, statusBuf, + mapBoxX, peakY, peakColor, highlightColor, drawUp); + continue; + } + + // Draw curves with both ends on-screen if (draw == DRAW_CURVE) { int peakX = ((upperX - lowerX + 1) / 2) + lowerX; int fudge = 30; int peakY = peak + fudge; // admittedly a hack (obscure how to define ypeak of curve) int y1 = isReversed ? yTarget : ySource; int y2 = isReversed ? ySource : yTarget; if (drawUp) { y1 = flipY(tg, y1); y2 = flipY(tg, y2); peakY = flipY(tg, peakY); } int maxY = hvGfxCurve(hvg, lowerX, y1, peakX, peakY, upperX, y2, color, isReversed && doDashes); // curve drawer does not use peakY as expected, so it returns actual max Y used