5dc1d6e658ab009f27314e192340275a6bb70237
max
  Tue Jun 2 16:20:48 2026 -0700
Add colorFields trackDb setting for bigBed/bigGenePred color scheme switching

Adds a new trackDb statement `colorFields` that renders a "Color by:" dropdown
in the track controls page, letting users switch among multiple pre-computed
color schemes stored as extra bigBed fields containing R,G,B strings.

The `default="label"` key renames the standard itemRgb option in the dropdown.
Other entries name extra bigBed fields whose R,G,B values override itemRgb when
selected. When a non-default scheme is active, a "(Coloring by: label)" suffix
appears in the track long label.

Changes:
- hui.c/hui.h: new colorFieldsCfgUi() rendered inside bedScoreCfgUi() for bigBed
- bigBedTrack.c: colorFieldIdx lookup + per-item filterColor override + longLabel suffix
- tagTypes.tab: register colorFields for bigBed and bigGenePred
- trackDbLibrary.shtml, trackDbDoc.html, trackDbHub.v3.html, changes.html: documentation

refs #26253

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff --git src/hg/lib/hui.c src/hg/lib/hui.c
index 61a1597fe41..7d80316bbb5 100644
--- src/hg/lib/hui.c
+++ src/hg/lib/hui.c
@@ -7423,30 +7423,107 @@
 
         boolean option = cartUsualBoolean(cart, varName, isDefault);
         cgiMakeCheckBox(varName, option);
 
         // find comment for the column listed
         struct asColumn *col = as->columnList;
         unsigned num = ptToInt(thisLabel->val);
         for(; col && num--; col = col->next)
             ;
         assert(col);
         printf(" %s&nbsp;&nbsp;&nbsp;", col->comment);
         }
     }
 }
 
+static char *colorFieldLabel(struct slPair *p)
+/* Derive a display label for one colorFields entry.
+ * Uses the explicit ="label" if provided, otherwise strips a leading "colorBy"
+ * prefix and replaces underscores with spaces. */
+{
+if (isNotEmpty((char *)p->val))
+    return (char *)p->val;
+char *lbl = p->name;
+if (startsWith("colorBy", lbl))
+    lbl += strlen("colorBy");
+char *derived = cloneString(lbl);
+subChar(derived, '_', ' ');
+return derived;
+}
+
+void colorFieldsCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *prefix)
+/* If trackDb declares colorFields, render a "Color by:" dropdown to choose among color schemes.
+ *
+ * colorFields is a space-separated list of fieldName[="Human Label"] pairs:
+ *   colorFields default="Evidence type" colorByHlaClass="HLA class" colorByKozak="Kozak strength"
+ *
+ * The special name "default" (with optional label) represents the standard itemRgb field
+ * (col 9).  When no default= entry is present, "Default" is added implicitly as the first
+ * option.  Any other name must be an extra bigBed field containing a pre-computed R,G,B string.
+ *
+ * Cart variable: <prefix>.colorField — empty string means use field 9.  */
+{
+if (tdbIsComposite(tdb))
+    return;
+char *setting = trackDbSettingClosestToHome(tdb, "colorFields");
+if (setting == NULL)
+    return;
+
+struct slPair *pairs = slPairListFromString(setting, TRUE);
+if (pairs == NULL)
+    return;
+
+/* Cart variable; empty string = use field 9. */
+char cartVar[1024];
+safef(cartVar, sizeof cartVar, "%s.colorField", prefix);
+char *current = cartUsualString(cart, cartVar, "");
+
+/* Two-pass build of label/value arrays.
+ * Pass 1: check whether the user supplied an explicit default= entry.
+ * Pass 2: populate the arrays, putting the "use field 9" option first. */
+char *defaultLabel = "Default";
+struct slPair *p;
+int nAlternatives = 0;
+for (p = pairs; p != NULL; p = p->next)
+    {
+    if (sameString(p->name, "default"))
+        defaultLabel = colorFieldLabel(p);
+    else
+        nAlternatives++;
+    }
+
+int nOptions = 1 + nAlternatives;  /* slot 0 = field-9 default + one per alternative */
+char **labels = needMem(nOptions * sizeof(char *));
+char **values = needMem(nOptions * sizeof(char *));
+labels[0] = defaultLabel;
+values[0] = "";
+
+int i = 1;
+for (p = pairs; p != NULL; p = p->next)
+    {
+    if (sameString(p->name, "default"))
+        continue;
+    values[i] = p->name;
+    labels[i] = colorFieldLabel(p);
+    i++;
+    }
+
+printf("<B>Color by:</B>&nbsp;");
+cgiMakeDropListFull(cartVar, labels, values, nOptions, current, NULL, NULL);
+printf("<BR>\n");
+}
+
 void mergeSpanCfgUi(struct cart *cart, struct trackDb *tdb, char *prefix)
 /* If this track offers a merge spanned items option, put up the cfg for it, which
  * is just a checkbox with a small explanation. Comparing tdb->track to prefix
  * ensures we don't offer this control at the composite level, as this is a
  * subtrack only config */
 {
 if (trackDbSettingOn(tdb, MERGESPAN_TDB_SETTING) && sameString(tdb->track, prefix))
     {
     boolean curOpt = trackDbSettingOn(tdb, "mergeSpannedItems");
     char mergeSetting[256];
     safef(mergeSetting, sizeof(mergeSetting), "%s.%s", tdb->track, MERGESPAN_CART_SETTING);
     if (cartVarExists(cart, mergeSetting))
         curOpt = cartBoolean(cart, mergeSetting);
     printf("<b>Merge items that span the current region</b>:");
     cgiMakeCheckBox(mergeSetting, curOpt);
@@ -7918,47 +7995,49 @@
 cgiMakeRadioButton(varName, "MANE_Select", sameString(setString, "MANE_Select"));
 printf(" %s&nbsp;&nbsp;&nbsp;", "MANE only");
 cgiMakeRadioButton(varName, "basic", sameString(setString, "basic"));
 printf(" %s&nbsp;&nbsp;&nbsp;", "BASIC only");
 cgiMakeRadioButton(varName, "all", sameString(setString, "all"));
 printf(" %s&nbsp;&nbsp;&nbsp;", "All");
 }
 
 void bedScoreCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *name, char *title, boolean boxed)
 /* Put up bed-specific score controls */
 {
 char *scoreMax = trackDbSettingClosestToHome(tdb, SCORE_FILTER _MAX);
 int maxScore = (scoreMax ? sqlUnsigned(scoreMax):1000);
 scoreCfgUi(db, cart,tdb,name,title,maxScore,boxed);
 
-if(startsWith("bigBed", tdb->type))
+if(tdbIsBigBed(tdb))
     {
     labelCfgUi(db, cart, tdb, name);
+    colorFieldsCfgUi(db, cart, tdb, name);
     mergeSpanCfgUi(cart, tdb, name);
     wigOption(cart, name, title, tdb);
     }
 }
 
 void genePredCfgUi(char *db, struct cart *cart, struct trackDb *tdb, char *name, char *title, boolean boxed)
 /* Put up genePred-specific controls */
 {
 char varName[64];
 boolean parentLevel = isNameAtParentLevel(tdb,name);
 char *geneLabel = cartUsualStringClosestToHome(cart, tdb,parentLevel, "label", "gene");
 boxed = cfgBeginBoxAndTitle(tdb, boxed, title);
 
 labelCfgUi(db, cart, tdb, name);
+colorFieldsCfgUi(db, cart, tdb, name);
 boolean isGencode3 = trackDbSettingOn(tdb, "isGencode3");
 
 if (sameString(name, "acembly"))
     {
     char *acemblyClass = cartUsualStringClosestToHome(cart,tdb,parentLevel,"type",
                                                       acemblyEnumToString(0));
     printf("<p><b>Gene Class: </b>");
     acemblyDropDown("acembly.type", acemblyClass);
     printf("  ");
     }
 else if (isGencode3)
     {
     newGencodeShowOptions(cart, tdb);
     }
 else if (startsWith("wgEncodeGencode", name))