ba5f4c097ecdf323f94adab36f4cf52f7f59e8cf
jcasper
  Mon Mar 3 09:47:13 2025 -0800
Initial commit of heatmap code for bigBeds, still plenty to do, refs #31812

diff --git src/hg/hgTracks/heatmap.c src/hg/hgTracks/heatmap.c
new file mode 100644
index 00000000000..d0ebb692829
--- /dev/null
+++ src/hg/hgTracks/heatmap.c
@@ -0,0 +1,782 @@
+#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;
+    }
+
+#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]);
+
+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;
+    }
+}