src/hg/instinct/hgBamBam/drawingCode.c 1.6

1.6 2010/05/31 03:02:21 jsanborn
fixed up
Index: src/hg/instinct/hgBamBam/drawingCode.c
===================================================================
RCS file: /projects/compbio/cvsroot/kent/src/hg/instinct/hgBamBam/drawingCode.c,v
retrieving revision 1.5
retrieving revision 1.6
diff -b -B -U 1000000 -r1.5 -r1.6
--- src/hg/instinct/hgBamBam/drawingCode.c	31 May 2010 00:55:19 -0000	1.5
+++ src/hg/instinct/hgBamBam/drawingCode.c	31 May 2010 03:02:21 -0000	1.6
@@ -1,831 +1,849 @@
 /********************************************************************************/
 /* Copyright 2007-2009 -- The Regents of the University of California           */
 /********************************************************************************/
 
 #include <limits.h>
 #include "common.h"
 #include "bed.h"
 #include "cart.h"
 #include "customTrack.h"
 #include "errCatch.h"
 #include "genoLay.h"
 #include "trackLayout.h"
 #include "hash.h"
 #include "hdb.h"
 #include "hgColors.h"
 #include "hPrint.h"
 #include "htmshell.h"
 #include "jsHelper.h"
 #include "psGfx.h"
 #include "trashDir.h"
 #include "vGfx.h"
 #include "hvGfx.h"
 #include "web.h"
 #include "hgBamBam.h"
 #include "spaceSaver.h"
 
 #define MIN_LABEL_SIZE 100 // minimum size of labels 
 #define TEXT_BUFFER 5 // pixels to put between text and heatmap
 #define FOOT_HEIGHT 5
 #define INTER_BREAK_HEIGHT 5
 
 struct trackLayout tl;           /* Dimensions of things, fonts, etc. */
 
 /***** BEGIN HELPER *****/
 
 #define CLIP(p,limit) if (p < 0) p = 0; if (p >= (limit)) p = (limit)-1;
 void verticalTextLeft(struct vGfx *vg, int x, int y,
 		      int width, int height, int colorIx, int fontHeight, 
 		      MgFont *font, char *string)
 {
 /*      don't let this run wild */
 //CLIP(width, vg->width);
 //CLIP(height, vg->height);
 
 if ((width <= 0) || (height <= 0))
     return;
 
 struct vGfx *vgHoriz;
 int i, j;
 /*  reversed meanings of width and height here since this is going
  *  to rotate 90 degrees
  */
 
 int offset = round((double) (width - fontHeight) / 2.0);
 if (offset < 0)
     offset = 0;
 vgHoriz = vgOpenGif(height, width, "/dev/null", FALSE);
 vgText(vgHoriz, 0, offset, colorIx, font, string);
 
 /*  now, blit from the horizontal to the vertical, rotate -90 (CCW) */
 for (i = 0; i < height; i++)        /* xSrc -> yDest */
     {
     int yDest = height - (i + 1);
     for (j = 0; j < width; j++)     /* ySrc -> xDest */
 	vgDot(vg, x + (j+1), y + yDest, vgGetDot(vgHoriz, i, j));
     }
 vgClose(&vgHoriz);
 }
 
 
 void vgMakeColorGradient(struct vGfx *vg,
 			 struct rgbColor *start, struct rgbColor *end,
 			 int steps, Color *colorIxs)
 /* Make a color gradient that goes smoothly from start
  * to end colors in given number of steps.  Put indicesLis
  * in color table in colorIxs */
 {
 double scale = 0, invScale;
 double invStep;
 int i; int r,g,b;
 
 steps -= 1;     /* Easier to do the calculation in an inclusive way. */
 invStep = 1.0/steps;
 for (i=0; i<=steps; ++i)
     {
     invScale = 1.0 - scale;
     r = invScale * start->r + scale * end->r;
     g = invScale * start->g + scale * end->g;
     b = invScale * start->b + scale * end->b;
     colorIxs[i] = vgFindColorIx(vg, r, g, b);
     scale += invStep;
     }
 }
 
 char *cartSettingsString(char *prefix, char *functionName)
 {
 if (!prefix || !functionName)
     return NULL;
 
 struct hashEl *hEl = cartFindPrefix(cart, prefix);
 if (!hEl)
     return NULL;
 
 struct dyString *dy = newDyString(1000);
 while (hEl)
     {
     char *name = hEl->name;
     char *val = hEl->val;
     dyStringPrintf(dy, "%s=%s,", name, val);
 
     hEl = hEl->next;
     }
 dyStringPrintf(dy, "%s,%s", functionName, VERSION);
 
 char *str = dyStringCannibalize(&dy);
 return str;
 }
 
 void drawBackgroundLines(struct vGfx *vg, int startX, int startY, int width, int height)
 { // draw background lines like genome browser.
 int x, spacing = 12;
 
 /* First line is red */
 Color lightRed = vgFindColorIx(vg, 220, 0, 0);
 x = startX;
 vgBox(vg, x, startY, 1, height, lightRed);
 x += spacing;
 
 /* Remaining are blue */
 Color lightBlue = vgFindColorIx(vg, 220, 220, 255);
 vgSetClip(vg, startX, startY, width, height);
 for ( ; x < (startX + width); x += spacing)
     vgBox(vg, x, startY, 1, height, lightBlue);
 vgUnclip(vg);
 }
 
 void prepareImage(struct vGfx *vg, struct settings *settings)
 {
 drawBackgroundLines(vg, settings->dataOffsetX - 1, 0, settings->dataWidth + 1, settings->height);
 
 if (settings->title)
     vgTextCentered(vg, settings->dataOffsetX, 0, settings->dataWidth, TITLE_HEIGHT,
 		   MG_BLACK, settings->font, settings->title);
 }
 
 static int calcX(struct chromLay *cl, int base, int min, int max)
 {
 if (!cl)
     return -1;
 int x = round(cl->pxStart+cl->pxWidth*((float)(base - cl->baseStart)/cl->baseWidth));
 
 if (x < min)
     x = min;
 if (x > max)
     x = max;
 return x;
 }
 
 
 /***** END HELPER *****/
 
 struct chromLay *getChromLayBreaks(struct sqlConnection *conn, 
 				   struct breaks *breaks, struct settings *settings)
 {
 if (!breaks || !settings || slCount(settings->layoutList) > 1)
     return NULL;
 
 char *currentChrom = settings->layoutList->chrom;
 
 /* Find all chroms that have inter chrom breakpoints */
 struct hash *chromHash = hashNew(0);
 struct breaks *br;
 for (br = breaks; br; br = br->next)
     {
 
     struct hashEl *el = hashLookup(chromHash, br->chrom);
     if (!el)
 	hashAddInt(chromHash, br->chrom, 1);
 
     el = hashLookup(chromHash, br->chrom2);
     if (!el)
 	hashAddInt(chromHash, br->chrom2, 1);
     }
 
 struct genoLayChrom *gl, *glList = genoLayDbChroms(conn, FALSE);
 
 float totalBases = 0.0;
 for (gl = glList; gl; gl = gl->next)
     {
     if (sameString(gl->fullName, currentChrom))
 	continue;
 
     struct hashEl *el = hashLookup(chromHash, gl->fullName);
     if (el)
 	totalBases += gl->size;
     }
 
 float cbases = 0.0;
 struct chromLay *cl, *clList = NULL;
 for (gl = glList; gl; gl = gl->next)
     {
     if (sameString(gl->fullName, currentChrom))
 	continue;
 
     struct hashEl *el = hashLookup(chromHash, gl->fullName);
     if (el)
 	{
 	AllocVar(cl);
 	cl->chrom     = cloneString(gl->fullName);
 	cl->baseStart = 0.0;
 	cl->baseEnd   = (float) gl->size;
 	cl->baseWidth = cl->baseEnd - cl->baseStart;
 	cl->pxWidth   = (float) settings->dataWidth * cl->baseWidth / totalBases;
 	cl->pxStart   = (float) settings->dataWidth * cbases / totalBases + settings->dataOffsetX;
 	cbases += (float) gl->size;
 	slAddHead(&clList, cl);
 	}
     }
 slReverse(&clList);
 return clList;
 }
 
 int breaksCmp(const void *va, const void *vb)
 /* Compare to sort columns based on priority. */
 {
 const struct breaks *a = *((struct breaks **)va);
 const struct breaks *b = *((struct breaks **)vb);
 
 float dif = 0;
 if (sameString(a->chrom, b->chrom))
     dif = a->chromStart - b->chromStart;
 else if (sameString(a->chrom, b->chrom2))
     dif = a->chromStart - b->chromStart2;
 else if (sameString(a->chrom2, b->chrom2))
     dif = a->chromStart2 - b->chromStart2;
 
 if (dif < 0)
     return 1;
 else if (dif > 0)
     return -1;
 else
     return 0;
 }
 
 void sortBreaks(struct breaks **breaksPtr, char *currentChrom)
 {
 struct breaks *br, *breaks = *breaksPtr;
 
 for (br = breaks; br; br = br->next)
     {
     if (sameString(br->chrom, br->chrom2) || sameString(br->chrom, currentChrom))
 	continue;
     
     char *tmpc  = br->chrom;
     int tmpS   = br->chromStart;
     int tmpE   = br->chromEnd;
     char tmpSt[2];
     strcpy(tmpSt, br->strand);
 
     br->chrom = br->chrom2;
     br->chromStart = br->chromStart2;
     br->chromEnd = br->chromEnd2;
     strcpy(br->strand, br->strand2);
 
     br->chrom2 = tmpc;
     br->chromStart2 = tmpS;
     br->chromEnd2 = tmpE;
     strcpy(br->strand2, tmpSt);    
     }
 
 slSort(breaksPtr, breaksCmp);
 }
 
 void drawInterChromBreaks(struct hvGfx *hvg, struct breaks *breaks, 
 			  struct chromLay *clList, struct settings *settings)
 {
 if (!settings || slCount(settings->layoutList) > 1)
     return;
 
 char *currentChrom = settings->layoutList->chrom;
 
 struct vGfx *vg = hvg->vg;
 
 struct breaks *br;
 struct chromLay *cl;
 struct hash *clHash = hashNew(0);
 for (cl = clList; cl; cl = cl->next)
     {
     if (sameString(cl->chrom, currentChrom))
 	continue;
     hashAdd(clHash, cl->chrom, cl);
     }
 
 int xl1, xl2, wl, xr1, xr2, wr, ldir, rdir;
 char *lstrand, *rstrand;
 struct chromLay *clL, *clR;
 struct hashEl *el;
 
 drawColorIdeogram(hvg, settings, clList, settings->dataOffsetY + DEFAULT_LABEL_HEIGHT);
 
 int yfoot = FOOT_HEIGHT; 
 int yl = settings->height - settings->dataOffsetY - yfoot;
 int yr = yfoot + CYTO_HEIGHT + DEFAULT_LABEL_HEIGHT;
 
 vgSetClip(vg, settings->dataOffsetX, 0, settings->dataWidth, settings->height);
 
 int ibreak = 0;
 
 sortBreaks(&breaks, currentChrom);
 
 int yOff;
 for (br = breaks; br; br = br->next)
     {
     yOff = settings->dataOffsetY;
     clL = NULL;
     clR = NULL;
 
     int yBreak = INTER_BREAK_HEIGHT * ibreak + CYTO_HEIGHT + DEFAULT_LABEL_HEIGHT + yfoot + INTER_BREAK_HEIGHT/2;
 
     int csL, ceL;
     int csR, ceR;
     if (sameString(currentChrom, br->chrom))
 	{
 	el = hashLookup(settings->layoutHash, br->chrom);
 	if (el)
 	    clL = el->val;
 	csL = br->chromStart;
 	ceL = br->chromEnd;
 	lstrand = br->strand;
 
 	el = hashLookup(clHash, br->chrom2);
 	if (el)
 	    clR = el->val;
 	csR = br->chromStart2;
 	ceR = br->chromEnd2;
 	rstrand = br->strand2;
 	}
     else
 	{
 	el = hashLookup(settings->layoutHash, br->chrom2);
 	if (el)
 	    clL = el->val;
 	csL = br->chromStart2;
 	ceL = br->chromEnd2;
 	lstrand = br->strand2;
 	
 	el = hashLookup(clHash, br->chrom);
 	if (el)
 	    clR = el->val;
 	csR = br->chromStart;
 	ceR = br->chromEnd;
 	rstrand = br->strand;
 	}
     
     /* +/- FOOT_HEIGHT to push far away breakpoint outside of clipped regions, so
      * they are not displayed */
  
     xl1 = calcX(clL, csL, settings->dataOffsetX-FOOT_HEIGHT-1, settings->width+FOOT_HEIGHT);
     xl2 = calcX(clL, ceL, settings->dataOffsetX-FOOT_HEIGHT-1, settings->width+FOOT_HEIGHT);
     wl = (xl2 - xl1 > 0) ? (xl2 - xl1) : 1;
 
     xr1 = calcX(clR, csR, settings->dataOffsetX-FOOT_HEIGHT-1, settings->width+FOOT_HEIGHT);
     xr2 = calcX(clR, ceR, settings->dataOffsetX-FOOT_HEIGHT-1, settings->width+FOOT_HEIGHT);
     wr = (xr2 - xr1 > 0) ? (xr2 - xr1) : 1;
 
     ldir = 1;
     if (sameString(lstrand, "+"))
 	ldir = -1;
 
     rdir = 1;  // reversed
     if (sameString(rstrand, "+"))
 	rdir = -1;
 
     Color col = getChromColor(vg, clR->chrom);
     //vgLine(vg, xl2, yOff + yl, xr1, yOff + yr, col);
     vgLine(vg, xl2, yOff + yBreak, xr1, yOff + yBreak, col);
 
     if (xl1 >= 0 && xl2 >= 0)
 	{
 	vgBox(vg, xl1, yOff + yl, wl, 1, MG_BLACK);
 	vgLine(vg, xl1, yOff + yl, xl1 + ldir * FOOT_HEIGHT, yOff + yl + yfoot, MG_BLACK);  
 	vgLine(vg, xl1, yOff + yl, xl1, yOff + yBreak, col);
 	}
     if (xr1 >= 0 && xr2 >= 0) 
 	{
 	vgBox(vg, xr1, yOff + yr, wr, 1, MG_BLACK);
 	vgLine(vg, xr1, yOff + yr, xr1 + rdir * FOOT_HEIGHT, yOff + yr - yfoot, MG_BLACK);  
 	vgLine(vg, xr1, yOff + yr, xr1, yOff + yBreak, col);
 	}
     ibreak++;
     }
 vgUnclip(vg);
 
 }
 
 char *interBreaksGif(struct sqlConnection *conn, struct breaks *breaks, 
 		     struct settings *settings)
 /* Create genome GIF file and HT that includes it. */
 {
 if (!breaks || !settings)
     return NULL;
 
 if (settings->height <= 0 || settings->width <= 0)
     return NULL;
 
 struct chromLay *clList = getChromLayBreaks(conn, breaks, settings);
 
 if (!clList)
     return NULL;
 
 int numBreaks = slCount(breaks);    
 
 settings->dataHeight = (numBreaks + 1) * INTER_BREAK_HEIGHT + CYTO_HEIGHT + DEFAULT_LABEL_HEIGHT + FOOT_HEIGHT * 2;
 settings->height     = settings->dataHeight + TITLE_HEIGHT;
 
 struct hvGfx *vg;
 struct tempName md5Tn;
 char *strToHash = cartSettingsString(bbPrefix, "interBreaks");
 trashDirMD5File(&md5Tn, "hgg", ".gif", strToHash);
 
 off_t size = fileSize(md5Tn.forCgi);
 if (!fileExists(md5Tn.forCgi) || (size == 0) || DEBUG)
     {
     vg = hvGfxOpenGif(settings->width, settings->height, md5Tn.forCgi, FALSE);
 
     prepareImage(vg->vg, settings);
 
     drawInterChromBreaks(vg, breaks, clList, settings);
 
     hvGfxClose(&vg);
     }
 
 char *filename = replaceChars(md5Tn.forHtml, "..", "");
 return filename;
 }
 
 
 void drawIntraChromBreaks(struct vGfx *vg, struct spaceSaver *ss, 
 		     struct settings *settings)
 {
 struct breaks *br;
 
 int xl1, xl2, wl, xr1, xr2, wr, ldir, rdir;
 char *lstrand, *rstrand;
 struct chromLay *clL, *clR;
 struct hashEl *el;
 
 int yfoot = 0; 
 int hline = 1;
 int yline = FOOT_HEIGHT; 
 
 struct spaceNode *sn;
 vgSetClip(vg, settings->dataOffsetX, 0, settings->dataWidth, settings->height);
 
 int yOff;
 for (sn = ss->nodeList; sn; sn = sn->next)
     {
     yOff = sn->row * FOOT_HEIGHT * 2 + settings->dataOffsetY;
     br   = (struct breaks *)sn->val;
 
     clL = NULL;
     clR = NULL;
     el = hashLookup(settings->layoutHash, br->chrom);
     if (el)
 	clL = el->val;
 
     el = hashLookup(settings->layoutHash, br->chrom2);
     if (el)
 	clR = el->val;
     
     /* +/- FOOT_HEIGHT to push far away breakpoint outside of clipped regions, so
      * they are not displayed */
  
     xl1 = calcX(clL, br->chromStart, settings->dataOffsetX-FOOT_HEIGHT-1, settings->width+FOOT_HEIGHT);
     xl2 = calcX(clL, br->chromEnd, settings->dataOffsetX-FOOT_HEIGHT-1, settings->width+FOOT_HEIGHT);
     wl = (xl2 - xl1 > 0) ? (xl2 - xl1) : 1;
     lstrand = br->strand;
 
     xr1 = calcX(clR, br->chromStart2, settings->dataOffsetX-FOOT_HEIGHT-1, settings->width+FOOT_HEIGHT);
     xr2 = calcX(clR, br->chromEnd2, settings->dataOffsetX-FOOT_HEIGHT-1, settings->width+FOOT_HEIGHT);
     wr = (xr2 - xr1 > 0) ? (xr2 - xr1) : 1;
     rstrand = br->strand2;
 
     if (xl1 > xr1)
 	{  // always go from left -> right, flip
 	int tmp1 = xl1;
 	int tmp2 = xl2;
 	int tmpw = wl;
 	char *tmpst = lstrand;
 	xl1 = xr1;
 	xl2 = xr2;
 	wl  = wr;
 	lstrand = rstrand;
 	xr1 = tmp1;
 	xr2 = tmp2;
 	wr  = tmpw;
 	rstrand = tmpst;
 	}
 
     ldir = 1;
     if (sameString(lstrand, "+"))
 	ldir = -1;
 
     rdir = 1;  // reversed
     if (sameString(rstrand, "+"))
 	rdir = -1;
 
     if (xl2 < xr1)
 	vgBox(vg, xl2, yOff + yline, (xr1-xl2), hline, MG_RED);
 
     if (xr2 < xl1)
 	vgBox(vg, xr2, yOff + yline, (xl1-xr2), hline, MG_RED);
 
     if (xl1 >= 0 && xl2 >= 0)
 	{
 	vgBox(vg, xl1, yOff + yline, wl, 1, MG_BLACK);
 	vgLine(vg, xl1, yOff + yline, xl1 + ldir * FOOT_HEIGHT, yOff + yfoot + (yline - yfoot) * 2, MG_BLACK);  
 	}
     if (xr1 >= 0 && xr2 >= 0) 
 	{
 	vgBox(vg, xr1, yOff + yline, wr, 1, MG_BLACK);
 	vgLine(vg, xr1, yOff + yline, xr1 + rdir * FOOT_HEIGHT, yOff + yfoot + (yline - yfoot) * 2, MG_BLACK);  
 	}
     }
 vgUnclip(vg);
 
 }
 
 struct spaceSaver *intraBreaksToSS(struct breaks *breaks, struct settings *settings)
 {
 struct spaceSaver *ss = spaceSaverNew(0, settings->width, 100);
 
 struct breaks *br;
 int x1, x2;
 struct chromLay *clL, *clR;
 struct hashEl *el;
 
 for (br = breaks; br; br = br->next)
     {
     clL = NULL;
     clR = NULL;
     el = hashLookup(settings->layoutHash, br->chrom);
     if (el)
 	clL = el->val;
     
     el = hashLookup(settings->layoutHash, br->chrom2);
     if (el)
 	clR = el->val;
 
     if (!clL || !clR || !sameString(clL->chrom, clR->chrom))
 	continue;
     
     x1 = calcX(clL, br->chromStart, settings->dataOffsetX, settings->width);   
     x2 = calcX(clR, br->chromEnd2, settings->dataOffsetX, settings->width);
 
     spaceSaverAdd(ss, x1, x2, br);
     }
 
 spaceSaverFinish(ss);
 return ss;
 }
 
 
 char *intraBreaksGif(struct sqlConnection *conn, struct breaks *breaks, 
 		struct settings *settings)
 /* Create genome GIF file and HT that includes it. */
 {
 if (!breaks || !settings)
     return NULL;
 
 if (settings->height <= 0 || settings->width <= 0)
     return NULL;
 
 struct spaceSaver *ss = intraBreaksToSS(breaks, settings);
 
 int maxRow = -1;
 struct spaceNode *sn;
 for (sn = ss->nodeList; sn; sn = sn->next)
     {
     if (sn->row > maxRow)
 	maxRow = sn->row;
     }
 if (maxRow < 0)
     return NULL;
 
 settings->dataHeight = (maxRow + 1) * FOOT_HEIGHT * 2;
 settings->height = settings->dataHeight + TITLE_HEIGHT;
 
 struct hvGfx *vg;
 struct tempName md5Tn;
 char *strToHash = cartSettingsString(bbPrefix, "intraBreaks");
 trashDirMD5File(&md5Tn, "hgg", ".gif", strToHash);
 
 off_t size = fileSize(md5Tn.forCgi);
 if (!fileExists(md5Tn.forCgi) || (size == 0) || DEBUG)
     {
     vg = hvGfxOpenGif(settings->width, settings->height, md5Tn.forCgi, FALSE);
 
     prepareImage(vg->vg, settings);
 
     drawIntraChromBreaks(vg->vg, ss, settings);
 
     hvGfxClose(&vg);
     }
 
 char *filename = replaceChars(md5Tn.forHtml, "..", "");
 return filename;
 }
 
 void drawCopyNumber(struct vGfx *vg, struct bed *bedList,
 		    struct settings *settings, float medVal, Color col)
 {
 int x1, x2, y, w;
 float val;
 
 struct bed *bed;
 struct chromLay *cl = NULL;
 
 char *prevChrom = NULL;
 
 struct hash *pixelHash = hashNew(0);
 struct hmPixel *hm, *hmList = NULL;
 
 char pixelStr[256];
 struct hashEl *el;
 for (bed = bedList; bed; bed = bed->next)
     {
     if (!cl || !sameString(bed->chrom, prevChrom))
 	{
 	el = hashLookup(settings->layoutHash, bed->chrom);
 	if (!el)
 	    errAbort("couldn't find chrom %s in layout", bed->chrom);
 	cl = el->val;
 	prevChrom = bed->chrom;
 	}
 
     if (bed->chromStart > cl->baseEnd || bed->chromEnd < cl->baseStart)
 	continue;
 
     x1 = calcX(cl, bed->chromStart, settings->dataOffsetX, settings->width);
     x2 = calcX(cl, bed->chromEnd, settings->dataOffsetX, settings->width);
     w  = (x2 - x1 > 0) ? x2 - x1 : 1;
     val = sqlFloat(bed->name);
     
     safef(pixelStr,sizeof (pixelStr), "%d", x1);
     el = hashLookup(pixelHash, pixelStr);
     if (!el)
 	{
 	hm = AllocA(struct hmPixel);
 	hm->x = x1;
 	hm->y = 0;
 	hm->w = w;
 	hm->h = 0;
 	hm->val = 0.0;
 	hm->count = 0;
 	slAddHead(&hmList, hm);
 	hashAdd(pixelHash, pixelStr, hm);
 	}
     else
 	hm = el->val;
     
     if (x2 - hm->x > hm->w)
 	hm->w = x2 - hm->x;
 
     hm->val += val;
     hm->count += 1;
     }
 
 struct rgbColor rgb = vgColorIxToRgb(vg, col);
 int r = rgb.r + floor((float) (255 - rgb.r) * 0.8);
 int g = rgb.g + floor((float) (255 - rgb.g) * 0.8);
 int b = rgb.b + floor((float) (255 - rgb.b) * 0.8);
 Color fillCol = vgFindColorIx(vg, r, g, b);
 
 medVal = medVal / (settings->maxVal - settings->minVal);
 if (medVal > 1.0)
     medVal = 1.0;
 int medY = round((float) settings->dataHeight * (1.0 - medVal) ) + settings->dataOffsetY;
 
 vgSetClip(vg, settings->dataOffsetX, 0, settings->dataWidth, settings->height);
 
 for (hm = hmList; hm ; hm = hm->next)
     {
     val = hm->val / hm->count;
     val = val / (settings->maxVal - settings->minVal);
     if (val > 1.0)
 	val = 1.0;
     
     y = round((float) settings->dataHeight * (1.0 - val) ) + settings->dataOffsetY;
 
     if (y > medY)
 	vgBox(vg, hm->x, medY, hm->w, y - medY, fillCol);
        else
 	vgBox(vg, hm->x, y, hm->w, medY - y, fillCol);
 
     vgBox(vg, hm->x, y-1, hm->w, 1, col);
     }
 vgUnclip(vg);
+
+vgSetClip(vg, 0, 0, settings->dataOffsetX, settings->height);
+int minY = round((float) settings->dataHeight * 1.0) + settings->dataOffsetY - 1.0;
+int maxY = round((float) settings->dataHeight * 0.0) + settings->dataOffsetY;
+
+char minValStr[256];
+safef(minValStr, sizeof(minValStr), "%3.2f", settings->minVal);
+char maxValStr[256];
+safef(maxValStr, sizeof(maxValStr), "%3.2f", settings->maxVal);
+
+int x = settings->dataOffsetX;
+vgBox(vg, x - 10, minY, 10, 1, MG_BLACK);
+vgTextRight(vg, 0, minY - settings->fontHeight, x-10, settings->fontHeight,
+	    MG_BLACK, settings->font, minValStr);
+
+vgBox(vg, x - 10, maxY, 10, 1, MG_BLACK);
+vgTextRight(vg, 0, maxY + settings->fontHeight/2, x-10, settings->fontHeight,
+	    MG_BLACK, settings->font, maxValStr);
 }
 
 char *copyNumberGif(struct sqlConnection *conn, struct bed *bed, 
 		    struct settings *settings, char *suffix, float medVal, Color col)
 /* Create genome GIF file and HT that includes it. */
 {
 if (!bed || !settings)
     return NULL;
 
 if (settings->height <= 0 || settings->width <= 0)
     return NULL;
 
 struct hvGfx *vg;
 struct tempName md5Tn;
 char *strToHash = cartSettingsString(bbPrefix, suffix);
 trashDirMD5File(&md5Tn, "hgg", ".gif", strToHash);
 
 off_t size = fileSize(md5Tn.forCgi);
 if (!fileExists(md5Tn.forCgi) || (size == 0) || DEBUG)
     {
     vg = hvGfxOpenGif(settings->width, settings->height, md5Tn.forCgi, FALSE);
 
     prepareImage(vg->vg, settings);
 
     drawCopyNumber(vg->vg, bed, settings, medVal, col);
 
     hvGfxClose(&vg);
     }
 
 char *filename = replaceChars(md5Tn.forHtml, "..", "");
 return filename;
 }
 
 
 struct settings *initSettings(struct sqlConnection *conn, 
 			      char *pos, char *name, int width, int height, 
 			      float minVal, float maxVal)
 {
 struct settings *settings = AllocA(struct settings);
 
 trackLayoutInit(&tl, cart);
 
 settings->title = NULL;
 if (name)
     settings->title = cloneString(name);
 
 settings->fontHeight = tl.fontHeight;
 settings->font = tl.font;
 
 settings->width  = width;
 settings->height = height;
 
 settings->dataOffsetX = GUTTER_WIDTH;
 settings->dataWidth   = settings->width - GUTTER_WIDTH;
 settings->dataOffsetY = TITLE_HEIGHT;
 settings->dataHeight  = settings->height - TITLE_HEIGHT;
 
 settings->minVal = minVal;
 settings->maxVal = maxVal;
 
 char *chrom = NULL;
 int start = 0;
 int stop = 0;
 
 settings->layoutHash = hashNew(0);
 settings->layoutList = NULL;
 settings->totalBases = 0.0;
 
 
 if (pos)
     {
     if (!hgParseChromRange(NULL, pos, &chrom, &start, &stop))
 	return NULL;
     }
 
 struct genoLayChrom *gl, *glList = genoLayDbChroms(conn, FALSE);
 
 slSort(&glList, genoLayChromCmpName);
 
 for (gl = glList; gl; gl = gl->next)
     {
     if (!chrom)
 	settings->totalBases += gl->size;
     else if (chrom && sameString(chrom, gl->fullName))
 	{
 	settings->totalBases += (float) (stop - start);
 	break;
 	}
     }
 
 if (settings->totalBases == 0.0)
     settings->totalBases = 1.0;
 
 float cbases = 0.0;
 struct chromLay *cl;
 for (gl = glList; gl; gl = gl->next)
     {
     if (!chrom)
 	{
 	AllocVar(cl);
 	cl->chrom     = cloneString(gl->fullName);
 	cl->baseStart = 0.0;
 	cl->baseEnd   = (float) gl->size;
 	cl->baseWidth = cl->baseEnd - cl->baseStart;
 	cl->pxWidth   = (float) settings->dataWidth * cl->baseWidth / settings->totalBases;
 	cl->pxStart   = (float) settings->dataWidth * cbases / settings->totalBases + settings->dataOffsetX;
 
 	hashAdd(settings->layoutHash, cl->chrom, cl);
 	slAddHead(&settings->layoutList, cl);
 	cbases += (float) gl->size;
 	}
     else if (chrom && sameString(chrom, gl->fullName))
 	{
 	AllocVar(cl);
 	cl->chrom = cloneString(gl->fullName);
 	cl->baseStart = (float) start;
 	cl->baseEnd   = (float) stop;
 	cl->baseWidth = cl->baseEnd - cl->baseStart;
 	cl->pxWidth   = (float) settings->dataWidth;
 	cl->pxStart   = settings->dataOffsetX;
 
 	hashAdd(settings->layoutHash, cl->chrom, cl);
 	slAddHead(&settings->layoutList, cl);
 	break;
 	}
     }
 slReverse(&settings->layoutList);
 
 return settings;
 }