ac21738f7d300c547f394228e274f8f161c49a88
jcasper
  Mon Mar 17 08:32:53 2025 -0700
Minor change to permit re-use of derived heatmap structures, refs #35320

diff --git src/hg/hgTracks/heatmap.c src/hg/hgTracks/heatmap.c
index d0ebb692829..90d1041a3cc 100644
--- src/hg/hgTracks/heatmap.c
+++ src/hg/hgTracks/heatmap.c
@@ -1,782 +1,786 @@
 #include "common.h"
 #include "hgTracks.h"
 #include "bigBed.h"
 #include "spaceSaver.h" // going to need this eventually to pack them?  Or not?  Can maybe remove?
 #include "mouseOver.h"  // probably want to handle custom mouseover
 #include "vGfx.h"
 #include "htmlColor.h"
 
 #include "heatmap.h"
 
 #define HEATMAP_COLOR_STEPS 20
 
 #define BG_COLOR_TDB_SETTING "heatmap.background"
 #define LOW_COLOR_TDB_SETTING "heatmap.lowColor"
 #define MID_COLOR_TDB_SETTING "heatmap.midColor"
 #define HIGH_COLOR_TDB_SETTING "heatmap.highColor"
 
 #define LOW_SCORE_TDB_SETTING "heatmap.lowScore"
 #define MID_SCORE_TDB_SETTING "heatmap.midScore"
 #define HIGH_SCORE_TDB_SETTING "heatmap.highScore"
 
 
 struct heatmap *heatmapNew(int rows, int cols)
 /* Allocate a new heatmap structure and return it */
 {
 struct heatmap *h = NULL;
 AllocVar(h);
 h->rows = rows;
 h->cols = cols;
 AllocArray(h->rowLabels, rows);
 AllocArray(h->scores, rows*cols);
 AllocArray(h->special, rows*cols);
 AllocArray(h->itemLabels, rows*cols);
 AllocArray(h->isNA, rows*cols);
 AllocArray(h->rowStarts, rows);
 AllocArray(h->rowSizes, rows);
 AllocArray(h->colStarts, cols);
 AllocArray(h->colSizes, cols);
 return h;
 }
 
 
 void heatmapSetDrawSizes(struct heatmap *h, double rowHeight, double colWidth)
 /* Update the expected cell height and width in pixels for the heatmap */
 {
 h->rowHeight = rowHeight;
 h->colWidth = colWidth;
 }
 
 int rowColToIndex(struct heatmap *h, int row, int col)
 /* Scores and labels are actually stored in 1-D arrays, so we need to convert a set
  * of row/column indices into a 1-D index for those arrays.  The storage format is
  * in row-major order.
  */
 {
 // NB: column count here is the number of exons; rows is the height (literally a number of
 // rows in the browser display).
 if (row >= h->rows || col >= h->cols)
     errAbort("Out of range indices row %d, col %d in heatmap (max %d, %d)", row, col, h->rows, h->cols);
 return row*h->cols + col;
 }
 
 void setCell(struct heatmap *h, int row, int col, double score, boolean isNA)
 /* Presently unused, might just get deleted */
 {
 int index = rowColToIndex(h, row, col);
 if (isNA)
     h->isNA[index] = TRUE;
 else
     {
     char buffer[256];
     safef(buffer, sizeof(buffer), "%lf", score);
     h->scores[index] = cloneString(buffer);
     h->isNA[index] = FALSE;
     }
 }
 
 char* getItemLabel(struct heatmap *h, int row, int col)
 /* Returns the mouseover label for a cell */
 {
 int index = rowColToIndex(h, row, col);
 return h->itemLabels[index];
 }
 
 char* getScore(struct heatmap *h, int row, int col)
 {
 int index = rowColToIndex(h, row, col);
 return h->scores[index];
 }
 
 
 boolean getNA(struct heatmap *h, int row, int col)
 /* Return whether a particular cell is NA */
 {
 int index = rowColToIndex(h, row, col);
 return h->isNA[index];
 }
 
 
 void heatmapAssignRowLabels(struct heatmap *h, char **rowLabels)
 /* Planned abstraction for assigning row labels in a heatmap */
 {
 }
 
 void heatmapAssignColumnLabels(struct heatmap *h, char **rowLabels)
 /* Planned abstraction for assigning column labels in a heatmap */
 {
 }
 
 
 struct heatmap *heatmapFromLF(struct track *tg, struct linkedFeatures *lf)
 /* This takes a linkedFeatures structure from a bigBed and builds a heatmap out
  * of it.  In the current setup, the heatmap values and labels are each stored
  * in a 1-D comma-separated matrix that we convert to a 2-D matrix using
  * row-major ordering.
  * The column count is the number of exons in the LF structure; the number of
  * rows is in an extra field.  The full type definition is in hg/lib/heatmap.as.
  */
 {
 struct bigBedInterval *bbi = lf->original;
 if (bbi == NULL)
     {
     warn("unable to make heatmap from lf - no original bbi");
     return NULL;
     }
 
+if (lf->extra != NULL)
+    return (struct heatmap*) lf->extra;
+
 #define HEATMAP_COUNT 17
 char *bedRow[HEATMAP_COUNT]; // hard-coded for now, should change this to accommodate extra .as fields
 char startBuf[16], endBuf[16];
 bigBedIntervalToRow(bbi, chromName, startBuf, endBuf, bedRow, ArraySize(bedRow));
 
 // Use this to figure out which field to pull.  For now, we're just doing 12, 13, 14, 15
 // (right after the exons)
 // 9: exonCount, 10: sizes, 11: starts, 12: rowcount, 13: rowlabels, 14: scores, 15: itemLabels
 // 16: link to maveDb
 
 struct heatmap *h = NULL;
 
 int rowCount = atoi(bedRow[12]);
 int colCount = atoi(bedRow[9]);
 
 h = heatmapNew(rowCount, colCount);
 
 // Get the row labels, which will be drawn off to the side of the heatmap
 int labelsFound = chopByCharRespectDoubleQuotesKeepEmpty(cloneString(bedRow[13]), ',', h->rowLabels, h->rows);
 if (labelsFound != rowCount)
     {
     errAbort("Field containing heatmap row labels had %d values, but %d were expected", labelsFound, rowCount);
     }
 
 // Read in the scores
 char *tmpScore[h->rows*h->cols];
 int scoresFound = chopCommas(cloneString(bedRow[14]), tmpScore);
 if (scoresFound != h->rows*h->cols)
     {
     errAbort("Field containing heatmap scores had %d values, but %d were expected", scoresFound, h->rows*h->cols);
     }
 
 int i;
 double max=atof(tmpScore[0]), min=atof(tmpScore[0]);
 for (i=0; i<scoresFound; i++)
     {
     if (isNotEmpty(tmpScore[i]))
         {
         // Check if the "score" string is actually an HTML color code.  This allows users to override
         // and set specific cell colors that aren't part of the score spectrum.
         if (tmpScore[i][0] == '#')
         {
             if (strlen(tmpScore[i]) >= 7)
             {
                 char buffer[8];
                 safencpy(buffer, sizeof(buffer), tmpScore[i], 7);
                 if (htmlColorForCode(buffer, NULL))
                     {
                     // It's a valid HTML color code - #NNNNNN
                     h->scores[i] = cloneString(buffer);
                     }
                 else
                     {
                     warn("Invalid heatmap score: %s in track %s\n", tmpScore[i], tg->track);
                     }
             }
             else
                 warn("Invalid heatmap score: %s in track %s\n", tmpScore[i], tg->track);
         }
         else
         {
             // The standard case - this score is assumed to be a floating-point number
             h->scores[i] = cloneString(tmpScore[i]);
             double value = atof(tmpScore[i]);
             h->isNA[i] = FALSE;
             if (value > max)
                 max = value;
             if (value < min)
                 min = value;
         }
         char *c = NULL;
         // Check for a | followed by a character.  That character will be displayed in the cell.
         // Usually this is to mark some weirdness, like multiple scores for that cell in the
         // data source.
         if ((c = strchr(tmpScore[i], '|')) != NULL)
             {
             c++;
             if (*c != 0 && isascii(*c))
                 h->special[i] = *c;
             }
         }
     else
         h->isNA[i] = TRUE;
     }
 
 // These are the mouseover texts for each cell of the heatmap
 // Note that we're re-using tmpScore for convenience!
 int itemLabelsFound = chopByCharRespectDoubleQuotesKeepEmpty(cloneString(bedRow[15]), ',', tmpScore, h->rows*h->cols);
 if (itemLabelsFound != h->rows*h->cols)
     {
     errAbort("Field containing heatmap mouseover labels had %d values, but %d were expected", itemLabelsFound, h->rows*h->cols);
     }
 
 for (i=0; i<itemLabelsFound; i++)
     {
     if (isNotEmpty(tmpScore[i]))
         {
         h->itemLabels[i] = cloneString(tmpScore[i]);
         }
     // else, the itemLabels[i] pointer was already zeroed by AllocArray
     }
 
 // Keep track of the min, max, and midpoint of the scores we've read
 h->highCap = max;
 h->lowCap = min;
 h->midPoint = (max+min)/2.0;
 
 // The label for this heatmap
 h->label = cloneString(bedRow[3]);
 
+lf->extra = h;
 return h;
 }
 
 
 
 int heatmapTotalHeight(struct heatmap *h)
 /*
  * Need to be able to say how high a heatmap is, including optional column labels
  * I think.  We'll see if and where we want to invoke this.  This is probably
  * dead code and can be removed.
  */
 {
 // for now, just draw the raw heatmap
 // final row starts at int(rows*rowHeight).  each row ends one pixel before the next,
 // but we'll need a sanity check for very zoomed out situations (a row might have a
 // "height" of less than 1 pixel, and we can't end before we start).
 // so max(int( (rows+1)*rowHeight)-1, int(rows*rowHeight))
 int lastStart = (int)(h->rows * h->rowHeight);
 int lastEnd = (int)( (h->rows+1) * h->rowHeight) - 1;
 if (lastEnd > lastStart)
     return lastEnd;
 else
     return lastStart;
 }
 
 int heatmapItemRowCount(struct track *tg, void *item)
 /* This returns the number of rows in a spaceSaver structure that this heatmap will
  * occupy.  This is expected to be the number of rows in the heatmap plus 1.  The
  * extra row is for the item label for the heatmap, since the space to the left is
  * already occupied by the row labels.
  */
 {
 // The problem here is that I want to access the heatmap structure for an item, but to
 // do so either the item has to include a link to the heatmap, be the heatmap, or else
 // I have to dynamically generate it just to get a row count.
 // We're doing that last right now, but because we're not freeing or storing it for later use,
 // it's quite wasteful.  Still seems to be pretty fast though.
 struct linkedFeatures *lf = (struct linkedFeatures *) item;
 struct heatmap *h = heatmapFromLF(tg, lf);
 if (h)
     return h->rows+1;  // +1 to allow for a title row.  column labels would be a further complication
 
 // something went wrong.  This should maybe be an errAbort or a warn instead.
 return 0;
 }
 
 int heatmapTotalWidth(struct heatmap *h)
 // Need to be able to say how wide a heatmap is for packing, including optional row labels
 // NB: this goes to hell if we do row labels to the left of the track image, so 
 // have fun working that out.
 //
 // This is likely dead code - we just let spaceSaver pack the item using its normal width,
 // with the caveat that we extend the left label width for packing if any of the row labels
 // are longer than the item label.
 {
 int lastCol = h->cols-1;
 int extent = h->colStarts[lastCol] + h->colSizes[lastCol] - h->colStarts[0];
 
 return extent;
 
 /*
 int i, maxLabelWidth = 0;
 for (i=0; i<h->rows; i++)
     {
     if (h->rowLabels[i] != NULL)
         {
         // this isn't quite right, but something like this below
 
         if (!tg->drawLabelInBox && !tg->drawName && withLabels && (!noLabel))
             // add item label
             {
             leftLabelSize = mgFontStringWidth(font, tg->itemName(tg, item)) + extraWidth;
             if (start - leftLabelSize + winOffset < 0)
                 leftLabelSize = start + winOffset;
             start -= leftLabelSize;
             }
         }
     }
  */
 
 /*
 int lastStart = (int)(h->cols * h->colWidth);
 int lastEnd = (int)( (h->cols+1) * h->colWidth) - 1;
 if (lastEnd > lastStart)
     return lastEnd;
 else
     return lastStart;
 */
 }
 
 
 void heatmapDrawItemAt(struct track *tg, void *item, struct hvGfx *hvg,
                          int x, int y, double scale, MgFont *font,
                          Color color, enum trackVisibility vis)
 /* Draw the cells for a single heatmap; this is intended to be a drop-in replacement
  * for linkedFeaturesDrawAt.  This doesn't pay attention to visibility because it is
  * only used as a replacement for linkedFeaturesDrawAt if we're in squish/pack/full.
  */
 {
 struct linkedFeatures *lf = (struct linkedFeatures *) item;
 
 struct heatmap *h = heatmapFromLF(tg, lf);
 
 /* Set the height and width of heatmap cells in pixels */
 heatmapSetDrawSizes(h, tg->heightPer, (lf->end - lf->start)*scale/2.0); // 2.0 for now b/c hard-coded 2 columns
 
 // Retrieve the colors that will be used in the heatmap
 char *lowColorString = trackDbSettingClosestToHomeOrDefault(tg->tdb, LOW_COLOR_TDB_SETTING, "green");
 char *midColorString = trackDbSettingClosestToHomeOrDefault(tg->tdb, MID_COLOR_TDB_SETTING, "black");
 char *highColorString = trackDbSettingClosestToHomeOrDefault(tg->tdb, HIGH_COLOR_TDB_SETTING, "red");
 struct rgbColor lowColor = bedColorToRgb(bedParseColor(lowColorString));
 struct rgbColor midColor = bedColorToRgb(bedParseColor(midColorString));
 struct rgbColor highColor = bedColorToRgb(bedParseColor(highColorString));
 
 // Get heatmap value bounds, maybe from trackDb
 double low = trackDbFloatSettingOrDefault(tg->tdb, LOW_SCORE_TDB_SETTING, h->lowCap);
 double mid = trackDbFloatSettingOrDefault(tg->tdb, MID_SCORE_TDB_SETTING, 0);
 double high = trackDbFloatSettingOrDefault(tg->tdb, HIGH_SCORE_TDB_SETTING, h->highCap);
 
 heatmapMakeSpectrum(h, hvg, low, &lowColor, mid, &midColor, high, &highColor);
 
 struct simpleFeature *exon = NULL;
 int i = 0;
 
 // Loop over the heatmap columns and set up the coordinates that each cell starts at
 for (exon = lf->components; exon != NULL; exon = exon->next)
     {
     int r = (h->cols-1) - i;  // quick hack - exons are stored in reverse order!
     h->colStarts[r] = round((double)((int)exon->start - winStart)*scale) + x;
     h->colSizes[r] = round((double)((int)exon->end - exon->start)*scale);
     i++;
     }
 
 // If a background color is specified for the heatmap, lay that down first.  Otherwise cells
 // will be on a transparent background (with the thin vertical blue lines, etc.).
 char *bgColorString = trackDbSettingClosestToHomeOrDefault(tg->tdb, BG_COLOR_TDB_SETTING, "");
 if (isNotEmpty(bgColorString))
     {
     Color bgColor = bedColorToGfxColor(bedParseColor(bgColorString));
 
     int bigBoxxOffset = (int)(h->colStarts[h->cols-1]);
     if (h->colSizes[h->cols-1] > 1)
         bigBoxxOffset += h->colSizes[h->cols-1];
     else
         bigBoxxOffset += 1;
     int bigBoxyOffset = y + (int)(h->rowHeight * (h->rows + 1));
     hvGfxBox(hvg, h->colStarts[0], y + h->rowHeight,  // + rowHeight so the enclosing box doesn't include the track label
         bigBoxxOffset- h->colStarts[0], bigBoxyOffset - (y + h->rowHeight), bgColor);
     }
 
 // Now loop over the cells and draw them
 int row_i, column_j;
 for (row_i = 0; row_i < h->rows; row_i++)
     {
     for (column_j = 0; column_j < h->cols; column_j++)
         {
         if (getNA(h, row_i, column_j))
             continue; // We draw nothing if there's no value here
 
         // At this point, we know it's not NA, so there must be something to display
         Color color = MG_BLACK;
         char *scoreStr = getScore(h, row_i, column_j);
         if (scoreStr[0] == '#')
         {
             htmlColorForCode(scoreStr, &color);
             color = bedColorToGfxColor(color);
         }
         else
         {
             double score = atof(scoreStr);
 
             if (score > h->midPoint)
                 {
                 double cappedScore = score > h->highCap ? h->highCap : score;
                 double fraction = 0;
                 if (h->highCap > h->midPoint)
                     fraction = (cappedScore - h->midPoint)/(h->highCap - h->midPoint);
                 int index = (int) (HEATMAP_COLOR_STEPS-1)*fraction;
                 color = h->highSpectrum[index];
                 }
             else
                 {
                 double cappedScore = score < h->lowCap ? h->lowCap : score;
                 double fraction = 0;
                 if (h->midPoint > h->lowCap)
                     fraction = (h->midPoint - cappedScore)/(h->midPoint - h->lowCap);
                 int index = (int) (HEATMAP_COLOR_STEPS-1)*fraction;
                 color = h->lowSpectrum[index];
                 }
             }
 
         // Now we have the color for the cell.  Time to set up the coordinates to draw it at.
 
         int yOffset = (int)((row_i+1) * h->rowHeight);  // +1 to allow for the heatmap label
         int yEnd = (int)((row_i+1+1)*h->rowHeight)-1;   // +1 to allow for the heatmap label
         if (yEnd < yOffset)
             yEnd = yOffset;
         int height = yEnd - yOffset;
         int xOffset = (int)(h->colStarts[column_j]);
 
         int width = h->colSizes[column_j];
 
         // We set width to 1 if it's less than 1.  Maybe a bad idea, but it will ensure SOMETHING
         // is visible when you're zoomed out.
         if (width < 1)
             width = 1;
         hvGfxBox(hvg, xOffset, y + yOffset, width, height, color);
 
         // Now draw the special character on top if there's space for it
         int specialIx = rowColToIndex(h, row_i, column_j);
         if (h->special[specialIx] != 0)
             {
                 char text[2];
                 safef(text, sizeof(text), "%c", h->special[specialIx]);
  
                 if (width >= vgGetFontStringWidth(hvg->vg, font, text) ) // &&
                     //height >= vgGetFontPixelHeight(hvg->vg, font))
                     // commented out because row height is always fontheight?  Unless it's not?
                     // Squish mode ... Ideally we'd resize the font for that; we'll see.
                     hvGfxTextCentered(hvg, xOffset, y + yOffset, width, height,
                                     MG_WHITE, font, text);
                 // TODO - replace MG_WHITE with that function that generates a contrasting
                 // text color based on the background at that position
             }
         }
     }
 
     // Put a black outline around the periphery of the heatmap for clarity
     int xOffset = (int)(h->colStarts[h->cols-1]);
     if (h->colSizes[h->cols-1] > 1)
         xOffset += h->colSizes[h->cols-1];
     else
         xOffset += 1;
     int yOffset = y + (int)(h->rowHeight * (h->rows + 1));
     hvGfxOutlinedBox(hvg, h->colStarts[0], y + h->rowHeight,  // + rowHeight to avoid enclosing the track label
         xOffset- h->colStarts[0], yOffset - (y + h->rowHeight), hvGfxFindAlphaColorIx(hvg, 0,0,0,0), MG_BLACK);
 }
 
 
 void heatmapSetBounds(struct heatmap *h, double lowScore, struct rgbColor *lowColor,
         double midScore, struct rgbColor *midColor, double highScore, struct rgbColor *highColor)
 /* Also dead code, and may well be removed soon */
 {
 if (lowScore > midScore || midScore > highScore)
     errAbort("attempting to make heatmap spectrum with invalid scores (low %lf, mid %lf, high %lf)",
             lowScore, midScore, highScore);
 
 }
 
 void heatmapMakeSpectrum(struct heatmap *h, struct hvGfx *hvg, double lowScore, struct rgbColor *lowColor,
         double midPoint, struct rgbColor *midColor, double highScore, struct rgbColor *highColor)
 /* Take high, low, and midpoints and associated colors, and create two spectrums (one for low scores, one for high)
  * that can be used to shade cells in the heatmap.
  */
 {
 if (h->lowSpectrum)
     freeMem(h->lowSpectrum);
 AllocArray(h->lowSpectrum, HEATMAP_COLOR_STEPS);
 if (h->highSpectrum)
     freeMem(h->highSpectrum);
 AllocArray(h->highSpectrum, HEATMAP_COLOR_STEPS);
 
 hvGfxMakeColorGradient(hvg, midColor, lowColor, HEATMAP_COLOR_STEPS, h->lowSpectrum);
 hvGfxMakeColorGradient(hvg, midColor, highColor, HEATMAP_COLOR_STEPS, h->highSpectrum);
 }
 
 
 static boolean isTooLightForTextOnWhite(struct hvGfx *hvg, Color color)
 /* Return TRUE if text in this color would probably be invisible on a white background. */
 /* Speculatively included as part of thoughts on adapting in-cell labels to be visible,
  * but this probably isn't the function we'll use */
 {
 struct rgbColor rgbColor =  hvGfxColorIxToRgb(hvg, color);
 int colorTotal = rgbColor.r + 2*rgbColor.g + rgbColor.b;
 return colorTotal >= 512;
 }
 
 void heatmapDrawItemLabel(struct track *tg, struct spaceNode *sn,
                           struct hvGfx *hvg, int xOff, int y, int width,
                           MgFont *font, Color color, Color labelColor, enum trackVisibility vis,
                           double scale, boolean withLeftLabels)
 /* Draw the labels for a heatmap.  This includes the full heatmap label as well as the labels
  * for each row (and potentially column, though we're not doing column labels right now)
  * This is cribbed and adapted from the more generic DrawItemLabel function, and could stand to
  * be cleaned up.
  */
 {
 struct slList *item = sn->val;
 int s = tg->itemStart(tg, item);
 int sClp = (s < winStart) ? winStart : s;
 int x1 = round((sClp - winStart)*scale) + xOff;
 int textX = x1;
 
 // can't draw item labels inside of a heatmap; withLeftLabels is always true
 withLeftLabels = TRUE;
 
 if (tg->itemNameColor != NULL)
     {
     color = tg->itemNameColor(tg, item, hvg);
     labelColor = color;
     if (withLeftLabels && isTooLightForTextOnWhite(hvg, color))
 	labelColor = somewhatDarkerColor(hvg, color);
     }
 
 /* pgSnpDrawAt may change withIndividualLabels between items */
 boolean withLabels = (withLeftLabels && ((vis == tvPack) || (vis == tvFull && isTypeBedLike(tg))) && (!sn->noLabel) && !tg->drawName);
 if (withLabels)
     {
     struct linkedFeatures *lf = (struct linkedFeatures *) item;
     struct heatmap *h = heatmapFromLF(tg, lf);
     heatmapSetDrawSizes(h, tg->heightPer, (lf->end - lf->start)*scale/2.0); // 2.0 for now b/c hard-coded 2 columns
 
     // heatmap title
     if (isNotEmpty(h->label))
         {
         char *name = h->label;
         textX = x1;
         int nameWidth = mgFontStringWidth(font, name);
         int dotWidth = tl.nWidth/2;
         boolean snapLeft = FALSE;
         boolean drawNameInverted = highlightItem(tg, item);
         textX -= nameWidth + dotWidth;
         snapLeft = (textX < fullInsideX);
         snapLeft |= (vis == tvFull && isTypeBedLike(tg));
 
         #ifdef IMAGEv2_NO_LEFTLABEL_ON_FULL
             if (theImgBox == NULL && snapLeft)
         #else///ifndef IMAGEv2_NO_LEFTLABEL_ON_FULL
             if (snapLeft)        /* Snap label to the left. */
         #endif ///ndef IMAGEv2_NO_LEFTLABEL_ON_FULL
                 {
                 textX = leftLabelX;
                 assert(hvgSide != NULL);
                 int prevX, prevY, prevWidth, prevHeight;
                 hvGfxGetClip(hvgSide, &prevX, &prevY, &prevWidth, &prevHeight);
                 hvGfxUnclip(hvgSide);
                 hvGfxSetClip(hvgSide, leftLabelX, y, fullInsideX - leftLabelX, tg->height);
                 if(drawNameInverted)
                     {
                     int boxStart = leftLabelX + leftLabelWidth - 2 - nameWidth;
                     hvGfxBox(hvgSide, boxStart, y, nameWidth+1, tg->heightPer - 1, color);
                     hvGfxTextRight(hvgSide, leftLabelX, y, leftLabelWidth-1, tg->heightPer,
                                 MG_WHITE, font, name);
                     }
                 else
                     hvGfxTextRight(hvgSide, leftLabelX, y, leftLabelWidth-1, tg->heightPer,
                                 labelColor, font, name);
                 hvGfxUnclip(hvgSide);
                 hvGfxSetClip(hvgSide, prevX, prevY, prevWidth, prevHeight);
                 }
             else
                 {
                 // shift clipping to allow drawing label to left of currentWindow
                 int pdfSlop=nameWidth/5;
                 int prevX, prevY, prevWidth, prevHeight;
                 hvGfxGetClip(hvgSide, &prevX, &prevY, &prevWidth, &prevHeight);
                 hvGfxUnclip(hvg);
                 hvGfxSetClip(hvg, textX-1-pdfSlop, y, nameWidth+1+pdfSlop, tg->heightPer);
                 if(drawNameInverted)
                     {
                     hvGfxBox(hvg, textX - 1, y, nameWidth+1, tg->heightPer-1, color);
                     hvGfxTextRight(hvg, textX, y, nameWidth, tg->heightPer, MG_WHITE, font, name);
                     }
                 else
                     // usual labeling
                     hvGfxTextRight(hvg, textX, y, nameWidth, tg->heightPer, labelColor, font, name);
                 hvGfxUnclip(hvg);
                 hvGfxSetClip(hvgSide, prevX, prevY, prevWidth, prevHeight);
                 }
             y += h->rowHeight;
             
         }
 
     int i = 0;
     for (i=0; i < h->rows; i++)
         {
         char *name = h->rowLabels[i];
         textX = x1;
 //        char *name = tg->itemName(tg, item);
         int nameWidth = mgFontStringWidth(font, name);
         int dotWidth = tl.nWidth/2;
         boolean snapLeft = FALSE;
         boolean drawNameInverted = highlightItem(tg, item);
         textX -= nameWidth + dotWidth;
         snapLeft = (textX < fullInsideX);
         snapLeft |= (vis == tvFull && isTypeBedLike(tg));
 
         /* Special tweak for expRatio in pack mode: force all labels
          * left to prevent only a subset from being placed right: */
         snapLeft |= (startsWith("expRatio", tg->tdb->type));
 
     #ifdef IMAGEv2_NO_LEFTLABEL_ON_FULL
         if (theImgBox == NULL && snapLeft)
     #else///ifndef IMAGEv2_NO_LEFTLABEL_ON_FULL
         if (snapLeft)        /* Snap label to the left. */
     #endif ///ndef IMAGEv2_NO_LEFTLABEL_ON_FULL
             {
             textX = leftLabelX;
             assert(hvgSide != NULL);
             int prevX, prevY, prevWidth, prevHeight;
             hvGfxGetClip(hvgSide, &prevX, &prevY, &prevWidth, &prevHeight);
             hvGfxUnclip(hvgSide);
             hvGfxSetClip(hvgSide, leftLabelX, y, fullInsideX - leftLabelX, tg->height);
             if(drawNameInverted)
                 {
                 int boxStart = leftLabelX + leftLabelWidth - 2 - nameWidth;
                 hvGfxBox(hvgSide, boxStart, y, nameWidth+1, tg->heightPer - 1, color);
                 hvGfxTextRight(hvgSide, leftLabelX, y, leftLabelWidth-1, tg->heightPer,
                             MG_WHITE, font, name);
                 }
             else
                 hvGfxTextRight(hvgSide, leftLabelX, y, leftLabelWidth-1, tg->heightPer,
                             labelColor, font, name);
             hvGfxUnclip(hvgSide);
             hvGfxSetClip(hvgSide, prevX, prevY, prevWidth, prevHeight);
             }
         else
             {
             // shift clipping to allow drawing label to left of currentWindow
             int pdfSlop=nameWidth/5;
             int prevX, prevY, prevWidth, prevHeight;
             hvGfxGetClip(hvgSide, &prevX, &prevY, &prevWidth, &prevHeight);
             hvGfxUnclip(hvg);
             hvGfxSetClip(hvg, textX-1-pdfSlop, y, nameWidth+1+pdfSlop, tg->heightPer);
             if(drawNameInverted)
                 {
                 hvGfxBox(hvg, textX - 1, y, nameWidth+1, tg->heightPer-1, color);
                 hvGfxTextRight(hvg, textX, y, nameWidth, tg->heightPer, MG_WHITE, font, name);
                 }
             else
                 // usual labeling
                 hvGfxTextRight(hvg, textX, y, nameWidth, tg->heightPer, labelColor, font, name);
             hvGfxUnclip(hvg);
             hvGfxSetClip(hvgSide, prevX, prevY, prevWidth, prevHeight);
             }
         y += h->rowHeight;
         }
     }
 }
 
 
 void heatmapMapItem(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 all the heatmap boxes for a single heatmap, one per occupied cell (i.e. where
  * isNA = FALSE).  Mouseover text is draw from the itemLabels field if populated, otherwise
  * it's just the score value.
  * Duplicates some code from other places about coordinate calculations, but for the moment this
  * is sufficient to get the job done.
  */
 {
 double scale = (double)currentWindow->insideWidth/(currentWindow->winEnd - currentWindow->winStart);
 struct linkedFeatures *lf = (struct linkedFeatures *) item;
 
 // Yet another reallocation of a heatmap structure
 struct heatmap *h = heatmapFromLF(tg, lf);
 
 heatmapSetDrawSizes(h, tg->heightPer, (lf->end - lf->start)*scale/2.0); // 2.0 for now b/c hard-coded 2 columns
 
 struct simpleFeature *exon = NULL;
 int i = 0;
 int sClp = (lf->start < winStart) ? winStart : lf->start;
 for (exon = lf->components; exon != NULL; exon = exon->next)
     {
     int r = (h->cols-1) - i;  // quick hack - exons are stored in reverse order!
     h->colStarts[r] = round((double)((int)exon->start - sClp)*scale) + x;
     h->colSizes[r] = round((double)((int)exon->end - exon->start)*scale);
     i++;
     }
 
 int row_i, column_j;
 for (row_i = 0; row_i < h->rows; row_i++)
     {
     for (column_j = 0; column_j < h->cols; column_j++)
         {
         char *label = getItemLabel(h, row_i, column_j);
         if (isNotEmpty(label))
             {
             int yOffset = (int)((row_i+1) * h->rowHeight);  // +1 to allow for the heatmap label
             int yEnd = (int)((row_i+1+1)*h->rowHeight)-1;   // +1 to allow for the heatmap label
             if (yEnd < yOffset)
                 yEnd = yOffset;
             int height = yEnd - yOffset;
             int xOffset = (int)(h->colStarts[column_j]);
 
             int width = h->colSizes[column_j];
 
             mapBoxHgcOrHgGene(hvg, lf->start, lf->end, xOffset, y+yOffset, width, height,
                        tg->track, lf->name, label, NULL, TRUE,
                        NULL);
             }
         }
     }
 }
 
 
 
 void heatmapItemMapAndArrows(struct track *tg, struct spaceNode *sn,
                                     struct hvGfx *hvg, int xOff, int y, int width,
                                     MgFont *font, Color color, Color labelColor, enum trackVisibility vis,
                                     double scale, boolean withLeftLabels)
 /* Heatmap-specific function for putting down a mapbox with a label */
 {
 struct slList *item = sn->val;
 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;
 
 int w = x2-x1;
 tg->mapItem(tg, hvg, item, tg->itemName(tg, item),
             tg->mapItemName(tg, item), s, e, x1, y, w, tg->heightPer);
 }
 
 
 void heatmapMethods(struct track *tg)
 /* This is intended to be invoked after commonBigBedMethods has already been applied to a track (really,
  * it's only expected to be invoked as part of bigBedMethods).
  * Cribbed from messageLineMethods, which also needs to check vis early on to decide what to do.
  * Here, we only want to set up to draw heatmaps if we're not in dense mode.
  */
 {
 //tg->mapsSelf = TRUE;  We're relying for now on the standard bigBed mapping, but might need to swap to mapsSelf
 char *s = cartOptionalString(cart, tg->track);
 if (cgiOptionalString("hideTracks"))
     {
     s = cgiOptionalString(tg->track);
     if (s != NULL && (hTvFromString(s) != tg->tdb->visibility))
         {
         cartSetString(cart, tg->track, s);
         }
     }
 enum trackVisibility trackVis = tg->tdb->visibility;
 if (s != NULL)
     trackVis = hTvFromString(s);
 if (trackVis != tvDense)
     {
     tg->itemHeightRowsForPack = heatmapItemRowCount;
     tg->drawItemAt = heatmapDrawItemAt;
     tg->drawItemLabel = heatmapDrawItemLabel;
     tg->mapItem = heatmapMapItem;
     tg->doItemMapAndArrows = heatmapItemMapAndArrows;
     }
 }