abd7278ac7167ede325d8c144a35ea59a0798766
chmalee
  Wed Jun 17 03:55:47 2026 -0700
Add a right click option to change color or background highlight color of individual track items. Only works for bed like items, refs #37778

diff --git src/hg/hgTracks/simpleTracks.c src/hg/hgTracks/simpleTracks.c
index 3203fa63434..6cfd1da99a2 100644
--- src/hg/hgTracks/simpleTracks.c
+++ src/hg/hgTracks/simpleTracks.c
@@ -3,30 +3,31 @@
 
 /* Copyright (C) 2014 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 /* NOTE: This code was imported from hgTracks.c 1.1469, May 19 2008,
  * so a lot of revision history has been obscured.  To see code history
  * from before this file was created, run this:
  * cvs ann -r 1.1469 hgTracks.c | less +Gp
  */
 
 #include "common.h"
 #include "spaceSaver.h"
 #include "portable.h"
 #include "bed.h"
 #include "basicBed.h"
+#include "htmlColor.h"
 #include "psl.h"
 #include "web.h"
 #include "hdb.h"
 #include "hgFind.h"
 #include "hCommon.h"
 #include "hgColors.h"
 #include "trackDb.h"
 #include "bedCart.h"
 #include "wiggle.h"
 #include "lfs.h"
 #include "grp.h"
 #include "chromColors.h"
 #include "hgTracks.h"
 #include "subText.h"
 #include "cds.h"
@@ -275,30 +276,32 @@
                                  * Red is put at end to alert overflow. */
 Color shadesOfBrown[10+1];	/* 10 shades of brown from tan to tar. */
 struct rgbColor brownColor = {100, 50, 0, 255};
 struct rgbColor tanColor = {255, 240, 200, 255};
 struct rgbColor guidelineColor = {220, 220, 255, 255};
 struct rgbColor multiRegionAltColor = {235, 235, 255, 255};
 struct rgbColor undefinedYellowColor = {240,240,180, 255};
 
 Color shadesOfSea[10+1];       /* Ten sea shades. */
 struct rgbColor darkSeaColor = {0, 60, 120, 255};
 struct rgbColor lightSeaColor = {200, 220, 255, 255};
 
 struct hash *hgFindMatches; /* The matches found by hgFind that should be highlighted. */
 boolean hgFindMatchesShowHighlight; /* For use with pdf mode which suppresses label highlight */
 
+struct hash *itemColorHash; /* Per-item background highlight colors keyed by "track\titemName". */
+
 struct trackLayout tl;
 
 void initTl()
 /* Initialize layout around small font and a picture about 600 pixels
  * wide. */
 {
 trackLayoutInit(&tl, cart);
 
 }
 
 static boolean isTooLightForTextOnWhite(struct hvGfx *hvg, Color color)
 /* Return TRUE if text in this color would probably be invisible on a white background. */
 {
 struct rgbColor rgbColor =  hvGfxColorIxToRgb(hvg, color);
 int colorTotal = rgbColor.r + 2*rgbColor.g + rgbColor.b;
@@ -4131,30 +4134,80 @@
 		}
 	    }
 	if ((vis == tvFull || vis == tvPack) && (intronGap && (qGap == 0) && (tGap >= intronGap)))
 	    {
             clippedBarbs(hvg, x1, midY, w, tl.barbHeight, tl.barbSpacing,
 			 lf->orientation, bColor, FALSE);
 	    }
         }
     }
 }
 
 /* Rule of thumb for displaying chain gaps: consider a valid double-sided
  * gap if target side is at most 5 times greater than query side. */
 #define CHAIN_GAP_FACTOR 5
 
+struct itemColorSpec
+/* A user-chosen color for a single item, set via the right-click "Color this item" menu. */
+    {
+    Color color;          /* The chosen color. */
+    boolean wholeItem;    /* TRUE to recolor the item glyph, FALSE for a background highlight. */
+    };
+
+static struct itemColorSpec *itemColorLookup(struct track *tg, void *item)
+/* Return the user-chosen color spec for this item, or NULL. Matches on mapItemName, itemName, or
+ * genomic position ("pos:chrom:start-end"), the same identities the JS uses to build the record.
+ * Nameless items (e.g. bed3) have no usable name, so the position key identifies them. */
+{
+if (itemColorHash == NULL)
+    return NULL;
+char key[2048];
+struct itemColorSpec *spec = NULL;
+if (tg->mapItemName != NULL)
+    {
+    safef(key, sizeof key, "%s\t%s", tg->track, tg->mapItemName(tg, item));
+    spec = hashFindVal(itemColorHash, key);
+    }
+if (spec == NULL && tg->itemName != NULL)
+    {
+    safef(key, sizeof key, "%s\t%s", tg->track, tg->itemName(tg, item));
+    spec = hashFindVal(itemColorHash, key);
+    }
+if (spec == NULL && tg->itemStart != NULL && tg->itemEnd != NULL)
+    {
+    safef(key, sizeof key, "%s\tpos:%s:%d-%d", tg->track, chromName,
+          tg->itemStart(tg, item), tg->itemEnd(tg, item));
+    spec = hashFindVal(itemColorHash, key);
+    }
+return spec;
+}
+
+boolean itemColorOverride(struct track *tg, void *item, Color *retColor, boolean *retWholeItem)
+/* If the user set a per-item color for this item (via right-click), return TRUE and fill in the
+ * color and whether it recolors the whole item; otherwise return FALSE. Lets non-linkedFeatures
+ * draw routines (e.g. bedDrawSimpleAt) honor right-click item colors. */
+{
+struct itemColorSpec *spec = itemColorLookup(tg, item);
+if (spec == NULL)
+    return FALSE;
+if (retColor != NULL)
+    *retColor = spec->color;
+if (retWholeItem != NULL)
+    *retWholeItem = spec->wholeItem;
+return TRUE;
+}
+
 void linkedFeaturesDrawAt(struct track *tg, void *item,
                           struct hvGfx *hvg, int xOff, int y, double scale,
                           MgFont *font, Color color, enum trackVisibility vis)
 /* Draw a single simple bed item at position. */
 {
 struct linkedFeatures *lf = item;
 struct simpleFeature *sf, *components;
 int heightPer = tg->heightPer;
 int x1,x2;
 int shortOff = heightPer/4;
 int shortHeight = heightPer - 2*shortOff;
 int tallStart, tallEnd, s, e, e2, s2;
 Color bColor;
 int intronGap = 0;
 boolean chainLines = ((vis != tvDense)&&(tg->subType == lfSubChain));
@@ -4199,30 +4252,44 @@
     {
     drawOpt = baseColorDrawSetup(hvg, tg, lf, &qSeq, &qOffset, &psl);
     if (drawOpt > baseColorDrawOff)
 	exonArrows = FALSE;
     }
 if ((tg->tdb != NULL) && (vis != tvDense))
     intronGap = atoi(trackDbSettingOrDefault(tg->tdb, "intronGap", "0"));
 
 lfColors(tg, lf, hvg, &color, &bColor);
 if (vis == tvDense && trackDbSetting(tg->tdb, EXP_COLOR_DENSE))
     color = saveColor;
 
 color = colorFromCart(tg, color);
 bColor = colorFromCart(tg, bColor);
 
+// user-chosen per-item color (right-click "Color this item"): recolor the whole glyph or
+// fall back to a background highlight, unless the item is already highlighted.
+struct itemColorSpec *userColorSpec = itemColorLookup(tg, lf);
+if (userColorSpec != NULL)
+    {
+    if (userColorSpec->wholeItem)
+        color = bColor = userColorSpec->color;
+    else if (lf->highlightColor == 0)
+        {
+        lf->highlightColor = userColorSpec->color;
+        lf->highlightMode = highlightBackground;
+        }
+    }
+
 struct genePred *gp = NULL;
 if (startsWith("genePred", tg->tdb->type) || startsWith("bigGenePred", tg->tdb->type))
     gp = (struct genePred *)(lf->original);
 
 boolean baseColorNeedsCodons = (drawOpt == baseColorDrawItemCodons ||
 				drawOpt == baseColorDrawDiffCodons ||
 				drawOpt == baseColorDrawGenomicCodons);
 if (psl && baseColorNeedsCodons)
     {
     boolean isXeno = ((tg->subType == lfSubXeno) || (tg->subType == lfSubChain) ||
 		      startsWith("mrnaBla", tg->table));
     int sizeMul = pslIsProtein(psl) ? 3 : 1;
     lf->codons = baseColorCodonsFromPsl(lf, psl, sizeMul, isXeno, maxShade, drawOpt, tg);
     }
 else if (drawOpt > baseColorDrawOff)
@@ -4762,30 +4829,39 @@
 int sClp = (s < winStart) ? winStart : s;
 int x1 = round((sClp - winStart)*scale) + xOff;
 int textX = x1;
 
 if (tg->drawLabelInBox)
     withLeftLabels = FALSE;
 
 if (tg->itemNameColor != NULL)
     {
     color = tg->itemNameColor(tg, item, hvg);
     labelColor = color;
     if (withLeftLabels && isTooLightForTextOnWhite(hvg, color))
 	labelColor = somewhatDarkerColor(hvg, color);
     }
 
+// user-chosen per-item color (right-click "Color this item"): color the label to match the glyph
+struct itemColorSpec *userColorSpec = itemColorLookup(tg, item);
+if (userColorSpec != NULL && userColorSpec->wholeItem)
+    {
+    color = labelColor = userColorSpec->color;
+    if (withLeftLabels && isTooLightForTextOnWhite(hvg, color))
+	labelColor = somewhatDarkerColor(hvg, color);
+    }
+
 /* pgSnpDrawAt may change withIndividualLabels between items */
 boolean withLabels = (withLeftLabels && withIndividualLabels && ((vis == tvPack) || (vis == tvFull && isTypeBedLike(tg))) && (!sn->noLabel) && !tg->drawName);
 if (withLabels)
     {
     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: */
@@ -16023,15 +16099,63 @@
 char *matchLine = NULL;
 struct slName *nameList = NULL, *name = NULL;
 matchLine = cartOptionalString(cart, "hgFind.matches");
 if(matchLine == NULL)
     return;
 nameList = slNameListFromString(matchLine,',');
 hgFindMatches = newHash(5);
 for(name = nameList; name != NULL; name = name->next)
     {
     hashAddInt(hgFindMatches, name->name, 1);
     }
 slFreeList(&nameList);
 hgFindMatchesShowHighlight = TRUE;  // default to showing the highlight searched item label.
 }
 
+void createItemColorHash()
+/* Read the itemColors cart variable into a hash of per-item colors keyed by "track\titemName",
+ * keeping only records for the current database. The cart format is db#track#mode#itemName#hexColor
+ * records joined by '|', where mode is "item" (recolor the glyph) or "bg" (background highlight).
+ * The color is the last '#' field so that item names containing '#' are tolerated; item names
+ * containing '|' are not supported. The cart value is user-editable, so malformed records (bad
+ * color, missing fields) are skipped rather than aborting the image. */
+{
+char *itemColors = cartOptionalString(cart, "itemColors");
+if (isEmpty(itemColors))
+    return;
+struct slName *recordList = slNameListFromString(itemColors, '|'), *record;
+for (record = recordList; record != NULL; record = record->next)
+    {
+    char *p = record->name;
+    char *db = cloneNextWordByDelimiter(&p, '#');
+    char *track = cloneNextWordByDelimiter(&p, '#');
+    char *mode = cloneNextWordByDelimiter(&p, '#');
+    char *lastHash = (p != NULL) ? strrchr(p, '#') : NULL;
+    if (!isEmpty(db) && !isEmpty(track) && !isEmpty(mode) && lastHash != NULL
+            && sameString(db, database))
+        {
+        *lastHash = '\0';
+        char *itemName = p;
+        char *hex = lastHash + 1;
+        char colorSpec[16];
+        safef(colorSpec, sizeof colorSpec, "#%s", hex);
+        unsigned rgb;
+        if (!isEmpty(itemName) && htmlColorForCode(colorSpec, &rgb))
+            {
+            struct itemColorSpec *spec;
+            AllocVar(spec);
+            spec->color = bedColorToGfxColor(rgb);
+            spec->wholeItem = sameString(mode, "item");
+            char key[2048];
+            safef(key, sizeof key, "%s\t%s", track, itemName);
+            if (itemColorHash == NULL)
+                itemColorHash = newHash(0);
+            hashAdd(itemColorHash, key, spec);
+            }
+        }
+    freeMem(db);
+    freeMem(track);
+    freeMem(mode);
+    }
+slFreeList(&recordList);
+}
+