4efa2a1c3d370f0c36bfae6a4e96cb6c36fb83ff braney Mon Dec 7 11:46:58 2020 -0800 scale fonts used on the y axis labels in lollipop graphs diff --git src/hg/hgTracks/lollyTrack.c src/hg/hgTracks/lollyTrack.c index f0471da..53a558a 100644 --- src/hg/hgTracks/lollyTrack.c +++ src/hg/hgTracks/lollyTrack.c @@ -1,434 +1,518 @@ /* lollyTrack -- load and draw lollys */ /* Copyright (C) 2019 The Regents of the University of California * See README in this or parent directory for licensing information. */ #include "common.h" #include "obscure.h" #include "hgTracks.h" #include "bedCart.h" #include "bigWarn.h" #include "lolly.h" #include "limits.h" #include "float.h" #include "bigBedFilter.h" #include "asParse.h" #define LOLLY_DIAMETER 2 * (lollyCart->radius + 2) struct lolly { struct lolly *next; char *name; /* the mouseover name */ double val; /* value in the data file */ unsigned start; /* genomic start address */ unsigned end; /* genomic end address */ unsigned radius; /* radius of the top of the lolly */ unsigned height; /* height of the lolly */ Color color; /* color of the lolly */ char *mouseOver; }; static unsigned getLollyColor( struct hvGfx *hvg, unsigned color) /* Get the device color from our internal definition. */ { struct rgbColor itemRgb; itemRgb.r = (color & 0xff0000) >> 16; itemRgb.g = (color & 0xff00) >> 8; itemRgb.b = color & 0xff; return hvGfxFindColorIx(hvg, itemRgb.r, itemRgb.g, itemRgb.b); } +// structure to capture yLabels +struct yLabel +{ +struct yLabel *next; +unsigned y; +char *label; +unsigned long color; +boolean on; +}; + +int cmpY(const void *va, const void *vb) +// sort the lines by y value +{ +const struct yLabel *a = *((struct yLabel **)va); +const struct yLabel *b = *((struct yLabel **)vb); +return a->y - b->y; +} + +// font heights available to the user +unsigned fontHeights[] = { 6,8,10,12,14,18,24 ,34 }; + +unsigned findBiggest(unsigned num) +/* find biggest font not bigger than num */ +{ +int ii; +unsigned prev = 0; + +for (ii=0; ii < ArraySize(fontHeights); prev = fontHeights[ii], ii++) + if (fontHeights[ii] > num) + return prev; + +return 34; +} + void doYLabels(struct track *tg, struct hvGfx *hvg, int width, int height, struct lollyCartOptions *lollyCart, int xOff, int yOff, Color color, MgFont *font, boolean doLabels ) -/* Draw labels or lines for labels. */ +/* parse lines like yAxisLabel <y offset> <draw line ?> <R,G,B> <label> + * Draw labels or lines for labels. */ { +struct hashEl *hel = trackDbSettingsLike(tg->tdb, "yAxisLabel*"); + +if (hel == NULL) + return; + double range = tg->graphUpperLimit - tg->graphLowerLimit; int fontHeight = tl.fontHeight+1; // we need a margin on top and bottom for half a lolly, and some space at the bottom // for the lolly stems double minimumStemHeight = fontHeight; double topAndBottomMargins = LOLLY_DIAMETER / 2 + LOLLY_DIAMETER / 2; double usableHeight = height - topAndBottomMargins - minimumStemHeight; yOff += LOLLY_DIAMETER / 2; -struct hashEl *hel = trackDbSettingsLike(tg->tdb, "yAxisLabel*"); -// parse lines like yAxisLabel <y offset> <draw line ?> <R,G,B> <label> +struct yLabel *lineList = NULL; +struct yLabel *line; for(; hel; hel = hel->next) { + AllocVar(line); + slAddHead(&lineList, line); + char *setting = cloneString((char *)hel->val); double number = atof(nextWord(&setting)); - boolean drawLine = sameString("on", nextWord(&setting)); + line->on = sameString("on", nextWord(&setting)); unsigned char red, green, blue; char *colorStr = nextWord(&setting); parseColor(colorStr, &red, &green, &blue); - unsigned long lineColor = MAKECOLOR_32(red, green, blue); - char *label = setting; + line->color = MAKECOLOR_32(red, green, blue); + line->label = cloneString(setting); + int offset = usableHeight * ((double)number - tg->graphLowerLimit) / range; + line->y = yOff + (usableHeight - offset); + } + +slSort(&lineList, cmpY); +unsigned minDiff = BIGNUM; +struct yLabel *prev = NULL; +MgFont *labelfont = font; + +// figure out what font height we should use +if (doLabels) + { + for(line = lineList; line; prev = line, line = line->next) + { + if (prev) + { + unsigned min = line->y - prev->y; + + if (min < minDiff) + minDiff = min; + } + } + + if (minDiff <= tl.fontHeight) + { + unsigned choice = findBiggest(minDiff); + if (doLabels && (choice == 0)) + return; + if (choice < tl.fontHeight) + { + char size[1024]; + + safef(size, sizeof size, "%d", choice); + labelfont = mgFontForSizeAndStyle(size, "medium"); + } + } + fontHeight = mgFontPixelHeight(labelfont) + 1; + } + +for(line = lineList; line; line = line->next) + { if (doLabels) - hvGfxTextRight(hvg, xOff, yOff + (usableHeight - offset) - fontHeight / 2 , width - 1, fontHeight, color, font, label); - else if (drawLine) - hvGfxLine(hvg, xOff, yOff + (usableHeight - offset), xOff + width , yOff + (usableHeight - offset), lineColor); + hvGfxTextRight(hvg, xOff, line->y - fontHeight / 2 , width - 1, fontHeight, line->color, labelfont, line->label); + else if (line->on) + hvGfxLine(hvg, xOff, line->y, xOff + width , line->y, line->color); } } static void lollyDrawItems(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 lolly structures. */ { double scale = scaleForWindow(width, seqStart, seqEnd); struct lolly *popList = tg->items, *pop; struct lollyCartOptions *lollyCart = tg->lollyCart; int trackHeight = tg->lollyCart->height ; if ( tg->visibility == tvDense) // in dense mode we just draw lines { for (pop = popList; pop; pop = pop->next) { int sx = ((pop->start - seqStart) + .5) * scale + xOff; // x coord of center (lower region) unsigned color = getLollyColor(hvg, pop->color); hvGfxLine(hvg, sx, yOff, sx , yOff+ tl.fontHeight, color); } return; } doYLabels(tg, hvg, width, trackHeight, tg->lollyCart, xOff, yOff, color, font, FALSE); if (popList == NULL) // nothing to draw return; boolean noMapBoxes = FALSE; int numItems = slCount(popList); if ( numItems > 5000) { char buffer[4096]; safef(buffer, sizeof buffer, "Too many (%d) items in window. Zoom in or set viewing range to be more restrictive.", numItems); noMapBoxes = TRUE; mapBoxHgcOrHgGene(hvg, winStart, winEnd, xOff, yOff, width, trackHeight, tg->track, "", buffer, "hgTracks", FALSE, NULL); } // calculate the lollipop heights double range = tg->graphUpperLimit - tg->graphLowerLimit; int fontHeight = tl.fontHeight+1; double usableHeight = trackHeight - LOLLY_DIAMETER - fontHeight; for(pop = popList; pop; pop = pop->next) { if (pop->radius == -1) pop->radius = lollyCart->radius; if (range == 0.0) pop->height = usableHeight ; else pop->height = usableHeight * (pop->val - tg->graphLowerLimit) / range; } // first draw the lines so they won't overlap any lollies yOff += LOLLY_DIAMETER / 2; if (!lollyCart->noStems) for (pop = popList; pop; pop = pop->next) { int sx = ((pop->start - seqStart) + .5) * scale + xOff; // x coord of center (lower region) hvGfxLine(hvg, sx, yOff + trackHeight, sx , yOff+(usableHeight - pop->height), MG_GRAY); } // now draw the sucker part! for (pop = popList; pop; pop = pop->next) { int sx = ((pop->start - seqStart) + .5) * scale + xOff; // x coord of center (lower region) unsigned color = getLollyColor(hvg, pop->color); hvGfxCircle(hvg, sx, yOff + (usableHeight - (pop->height )), pop->radius, color, TRUE); if ( tg->visibility != tvSquish) hvGfxCircle(hvg, sx, yOff + (usableHeight - (pop->height )), pop->radius, MG_BLACK, FALSE); if (!noMapBoxes) mapBoxHgcOrHgGene(hvg, pop->start, pop->end, sx - pop->radius, yOff + usableHeight - pop->radius - pop->height, 2 * pop->radius,2 * pop->radius, tg->track, pop->name, pop->mouseOver, NULL, TRUE, NULL); } } void lollyLeftLabels(struct track *tg, int seqStart, int seqEnd, struct hvGfx *hvg, int xOff, int yOff, int width, int height, boolean withCenterLabels, MgFont *font, Color color, enum trackVisibility vis) // draw the labels on the left margin { struct lollyCartOptions *lollyCart = tg->lollyCart; int fontHeight = tl.fontHeight+1; int centerLabel = (height/2)-(fontHeight/2); if ( tg->visibility == tvDense) { hvGfxTextRight(hvg, xOff, yOff+fontHeight, width - 1, fontHeight, color, font, tg->shortLabel); return; } //doYLabels(tg, hvg, width, height-fontHeight, lollyCart, xOff, yOff+fontHeight, color, font, TRUE); doYLabels(tg, hvg, width, height-fontHeight, lollyCart, xOff, yOff+fontHeight, color, font, TRUE); hvGfxText(hvg, xOff, yOff+centerLabel, color, font, tg->shortLabel); char *setting = trackDbSetting(tg->tdb, "yAxisNumLabels"); if (setting && sameString("off", setting)) return; if (isnan(tg->graphUpperLimit)) { hvGfxTextRight(hvg, xOff, yOff + LOLLY_DIAMETER, width - 1, fontHeight, color, font, "NO DATA"); return; } char upper[1024]; safef(upper, sizeof(upper), "%g -", tg->graphUpperLimit); hvGfxTextRight(hvg, xOff, yOff + fontHeight / 2 + LOLLY_DIAMETER / 2 , width - 1, fontHeight, color, font, upper); char lower[1024]; if (tg->graphLowerLimit < tg->graphUpperLimit) { safef(lower, sizeof(lower), "%g -", tg->graphLowerLimit); hvGfxTextRight(hvg, xOff, yOff+height - LOLLY_DIAMETER / 2 - 2 *fontHeight + fontHeight/2 , width - 1, fontHeight, color, font, lower); } } static int lollyHeight(struct track *tg, enum trackVisibility vis) /* calculate height of all the lollys being displayed */ { if ( tg->visibility == tvDense) return tl.fontHeight; // return the height we calculated at load time return tg->lollyCart->height; } int cmpHeight(const void *va, const void *vb) // sort the lollies by height { const struct lolly *a = *((struct lolly **)va); const struct lolly *b = *((struct lolly **)vb); return a->height - b->height; } static int getField(struct trackDb *tdb, char *name, struct asObject *as, int defaultValue) /* Get the as field (if any) that a trackDb variable references. This can either be * a field name, or a number. */ { int ret = defaultValue; char *setting = trackDbSetting(tdb, name); if (setting != NULL) { if (isNumericString(setting)) ret = atoi(setting); else { struct asColumn *columns = as->columnList; ret = asColumnMustFindIx(columns, setting) + 1; } } return ret; } void lollyLoadItems(struct track *tg) // load lollies from the data file { struct lollyCartOptions *lollyCart = tg->lollyCart; if (tg->visibility == tvSquish) { //lollyCart->radius = 2; lollyCart->height = lollyCart->origHeight / 2; } else if (tg->visibility == tvPack) { //lollyCart->radius = 4; lollyCart->height = lollyCart->origHeight / 1.5; } struct lm *lm = lmInit(0); struct bbiFile *bbi = fetchBbiForTrack(tg); struct asObject *as = bigBedAsOrDefault(bbi); struct bigBedInterval *bb, *bbList = bigBedIntervalQuery(bbi, chromName, winStart, winEnd, 0, lm); char *bedRow[bbi->fieldCount]; char startBuf[16], endBuf[16]; struct lolly *popList = NULL, *pop; unsigned lollyField = getField(tg->tdb, "lollyField", as, 5); // we use the score field by default int lollySizeField = getField(tg->tdb, "lollySizeField", as, -1); // no size field by default double minVal = DBL_MAX, maxVal = -DBL_MAX; //double sumData = 0.0, sumSquares = 0.0; unsigned count = 0; int trackHeight = tg->lollyCart->height; struct bigBedFilter *filters = bigBedBuildFilters(cart, bbi, tg->tdb); char *mouseOverField = cartOrTdbString(cart, tg->tdb, "mouseOverField", NULL); int mouseOverIdx = bbExtraFieldIndex(bbi, mouseOverField) ; char *mouseOverPattern = NULL; char **fieldNames = NULL; if (!mouseOverIdx) { mouseOverPattern = cartOrTdbString(cart, tg->tdb, "mouseOver", NULL); if (mouseOverPattern) { AllocArray(fieldNames, bbi->fieldCount); struct slName *field = NULL, *fields = bbFieldNames(bbi); int i = 0; for (field = fields; field != NULL; field = field->next) fieldNames[i++] = field->name; } } int filtered = 0; for (bb = bbList; bb != NULL; bb = bb->next) { bigBedIntervalToRow(bb, chromName, startBuf, endBuf, bedRow, ArraySize(bedRow)); // throw away items that don't pass the filters if (!bigBedFilterInterval(bedRow, filters)) { filtered++; continue; } // clip out lollies that aren't in our display range double val = atof(bedRow[lollyField - 1]); if (!((lollyCart->autoScale == wiggleScaleAuto) || ((val >= lollyCart->minY) && (val <= lollyCart->maxY) ))) continue; // don't draw lollies off the screen if (atoi(bedRow[1]) < winStart) continue; AllocVar(pop); slAddHead(&popList, pop); pop->val = val; pop->start = atoi(bedRow[1]); pop->end = atoi(bedRow[2]); pop->name = cloneString(bedRow[3]); pop->radius = -1; if (lollySizeField > 0) { double radius = atoi(bedRow[lollySizeField - 1]) * trackHeight / 100.0; pop->radius = radius; } pop->color = 0; pop->mouseOver = pop->name; extern char* restField(struct bigBedInterval *bb, int fieldIdx) ; if (mouseOverIdx > 0) pop->mouseOver = restField(bb, mouseOverIdx); else if (mouseOverPattern) pop->mouseOver = replaceFieldInPattern(mouseOverPattern, bbi->fieldCount, fieldNames, bedRow); if (bbi->fieldCount > 8) pop->color = itemRgbColumn(bedRow[8]); count++; if (val > maxVal) maxVal = val; if (val < minVal) minVal = val; } if (filtered) labelTrackAsFilteredNumber(tg, filtered); if (lollyCart->autoScale == wiggleScaleAuto) { if (maxVal == -DBL_MAX) { tg->graphUpperLimit = NAN; tg->graphLowerLimit = NAN; } else { tg->graphUpperLimit = maxVal; tg->graphLowerLimit = minVal; } } slSort(&popList, cmpHeight); tg->items = popList; } static struct lollyCartOptions *lollyCartOptionsNew(struct cart *cart, struct track *track, int wordCount, char *words[]) // the structure that is attached to the track structure { struct trackDb *tdb = track->tdb; struct lollyCartOptions *lollyCart; AllocVar(lollyCart); int maxHeightPixels; int minHeightPixels; int defaultHeight; /* pixels per item */ int settingsDefault; cartTdbFetchMinMaxPixels(cart, tdb, MIN_HEIGHT_PER, atoi(DEFAULT_HEIGHT_PER), atoi(DEFAULT_HEIGHT_PER), &minHeightPixels, &maxHeightPixels, &settingsDefault, &defaultHeight); lollyCart->origHeight = lollyCart->height = defaultHeight; lollyCart->autoScale = wigFetchAutoScaleWithCart(cart,tdb, tdb->track, NULL); double tDbMinY; /* data range limits from trackDb type line */ double tDbMaxY; /* data range limits from trackDb type line */ char *trackWords[8]; /* to parse the trackDb type line */ int trackWordCount = 0; /* to parse the trackDb type line */ wigFetchMinMaxYWithCart(cart, tdb, tdb->track, &lollyCart->minY, &lollyCart->maxY, &tDbMinY, &tDbMaxY, trackWordCount, trackWords); track->graphUpperLimit = lollyCart->maxY; track->graphLowerLimit = lollyCart->minY; lollyCart->radius = 5; lollyCart->noStems = trackDbSettingOn(tdb, "lollyNoStems"); char *setting = trackDbSetting(tdb, "lollyMaxSize"); if (setting) lollyCart->radius = atoi(setting); return lollyCart; } void lollyRegionGraphLimits(struct track *tg) /* Set common graphLimits across all windows */ { double graphUpperLimit = -BIGDOUBLE; double graphLowerLimit = BIGDOUBLE; struct track *tgSave = tg; struct window *w; // find graphLimits across all windows. for(w=windows,tg=tgSave; w; w=w->next,tg=tg->nextWindow) { if (isnan(tg->graphUpperLimit)) continue; if (tg->graphUpperLimit > graphUpperLimit) graphUpperLimit = tg->graphUpperLimit; if (tg->graphLowerLimit < graphLowerLimit) graphLowerLimit = tg->graphLowerLimit; } if (graphUpperLimit == -BIGDOUBLE) graphUpperLimit = graphLowerLimit = NAN; // set same common graphLimits in all windows. for(w=windows,tg=tgSave; w; w=w->next,tg=tg->nextWindow) { tg->graphUpperLimit = graphUpperLimit; tg->graphLowerLimit = graphLowerLimit; } } void lollyMethods(struct track *track, struct trackDb *tdb, int wordCount, char *words[]) /* bigLolly track type methods */ { bigBedMethods(track, tdb, wordCount, words); struct lollyCartOptions *lollyCart = lollyCartOptionsNew(cart, track, wordCount, words); lollyCart->typeWordCount = wordCount; AllocArray(lollyCart->typeWords, wordCount); int ii; for(ii=0; ii < wordCount; ii++) lollyCart->typeWords[ii] = cloneString(words[ii]); track->loadItems = lollyLoadItems; track->drawItems = lollyDrawItems; track->totalHeight = lollyHeight; track->preDrawMultiRegion = lollyRegionGraphLimits; track->drawLeftLabels = lollyLeftLabels; track->lollyCart = lollyCart; }