07c318cffa6769b6a4b650a88a7887ae01de608d
jcasper
  Wed Feb 4 20:07:42 2026 -0800
Impose an overall limit on active track count for featured composites,
and allow alternate labels for the data type selectors, refs #36320

diff --git src/hg/hgTrackUi/hgTrackUi.c src/hg/hgTrackUi/hgTrackUi.c
index bb587ff2872..0634c6a7df1 100644
--- src/hg/hgTrackUi/hgTrackUi.c
+++ src/hg/hgTrackUi/hgTrackUi.c
@@ -2844,46 +2844,64 @@
 char **ancestors;
 AllocArray(ancestors, count);
 count = 0;
 for(sp=speciesList; sp; sp = sp->next)
     {
     ancestors[count] = sp->name;
     count++;
     }
 char *coalescent = cartOptionalString(cart, codeVarName);
 printf("<B>Set Coalescent Ancestor to:</B>");
 cgiMakeDropListFull(codeVarName, ancestors, ancestors,
     count, coalescent, NULL, NULL);
 }
 #endif
 
-static struct slName *parseDataTypes(struct trackDb *tdb)
+static struct slPair *parseDataTypes(struct trackDb *tdb)
 {
 /* Parse the 'dataTypes' trackDb setting into an slName list.
  * 'dataTypes' is a space separated list of words, each indicating a
  * data type. Return value is NULL on error.
  */
 char *tdbDataTypes = cloneString(trackDbSetting(tdb, "dataTypes"));
 if (tdbDataTypes == NULL)
     return NULL;
 
 // A bit awkward to go through chopByWhite into slNameListFromStringArray, but
 // the slNameList family of functions doesn't have a chopByWhite equivalent.
 int n_datatypes = chopByWhiteRespectDoubleQuotes(tdbDataTypes, NULL, 0);
 char **datatypes = calloc(n_datatypes, sizeof(char *));
 chopByWhiteRespectDoubleQuotes(tdbDataTypes, datatypes, n_datatypes);
-struct slName *list = slNameListFromStringArray(datatypes, n_datatypes);
+struct slPair *list = NULL;
+
+// At this point, each row in datatypes is the string for one data type.
+// Either a (possibly quoted) label, or else that followed by | followed
+// by (possibly quoted) text to display with the data type checkox.
+
+for (int i = 0; i < n_datatypes; i++)
+    {
+    // If it's just a name, use that for the name and the display title.
+    // But if it's of the form <name|title>, then split them apart.
+    char *dtParts[2];
+    int partCount = chopByCharRespectDoubleQuotes(datatypes[i], '|',
+            dtParts, 2);
+    char *name, *title;
+    name = title = dtParts[0];
+    if (partCount > 1)
+        title = dtParts[1];
+    slPairAdd(&list, name, cloneString(title));
+    }
 freeMem(tdbDataTypes);
 return list;
 }
 
 unsigned int cartDbParseId(char *, char **);  // ADS: avoid extra include
 
 #define COMMA_IF(x) (((x)++) ? "," : "")  // ADS: pattern for JSON comma
 
 static void facetedCompositeUi(struct trackDb *tdb)
 {
 /* ADS: How facetedComposite differs from other track types
  * - compositeTrack track setting is "faceted"
  * - Required fields in the 'settings' longblob for the trackDb entry:
  * - 'metaDataUrl': a non-blocked URL (can be server-local) with
  *   metadata to generate the table.  This might change to an existing metadata
@@ -2914,31 +2932,31 @@
 const char placeholderDiv[] = "<div id='metadata-placeholder'></div>\n";
 const char openJSON[] = "<script id=\"app-data\" type=\"application/json\">{";
 const char closeJSON[] = "}</script>\n";
 const char openDataTypesJSON[] = "\"dataTypes\":{";
 const char closeDataTypesJSON[] = "}";  // closing a dict
 const char openDataElementsJSON[] = "\"dataElements\":[";
 const char closeDataElementsJSON[] = "]";  // closing an array
 const char metadataTableScriptElement[] =
     "<script type='text/javascript' src='/js/facetedComposite.js'></script>\n";
 
 // --- Get data from 'settings' field in 'trackDb' entry ---
 // required
 const char *metaDataUrl = trackDbSetting(tdb, "metaDataUrl");
 const char *primaryKey = trackDbSetting(tdb, "primaryKey");
 
-struct slName *dataTypes = parseDataTypes(tdb);
+struct slPair *dataTypes = parseDataTypes(tdb);
 boolean hasDataTypes = (dataTypes != NULL);
 
 // optional
 const char *colorSettingsUrl = (const char *)hashFindVal(tdb->settingsHash, "colorSettingsUrl");
 const char *maxCheckboxes = (const char *)hashFindVal(tdb->settingsHash, "maxCheckboxes");
 // --- done parsing values from trackDb.settings ---
 
 const char *metaDataId = tdb->track;
 const int metaDataIdLen = strlen(metaDataId);
 
 printf(pageStyle);       // css
 printf(placeholderDiv);  // placholder
 
 // start by figuring out what's on by default and hasn't been overridden
 struct hash *defaultOn = hashNew(0);
@@ -2960,59 +2978,60 @@
         char val[1024];
         safef(val, sizeof(val), "%s_sel", st->track);
         if (!cartVarExists(cart, val))
             hashAdd(defaultOn, val, NULL);
         }
     }
 
 /* --- START embedded JSON data --- */
 printf(openJSON);
 printf(openDataTypesJSON);
 // find selected data types
 int not_first = 0;
 struct slName *selectedDataTypes = NULL;  // non-null val will be used as flag
 if (hasDataTypes)
     {
-    for (struct slName *thisType = dataTypes; thisType != NULL; thisType = thisType->next)
+    for (struct slPair *thisType = dataTypes; thisType != NULL; thisType = thisType->next)
         {
         char toMatch[token_size];
         boolean selected = FALSE;
         safef(toMatch, token_size, "%s_*_%s_sel", metaDataId, thisType->name);
         // Easy case - check if there's a defaultOn track still active with this track type
         if (hashItemExistsLike(defaultOn, toMatch))
             {
             slNameAddHead(&selectedDataTypes, thisType->name);
             selected = TRUE;
             }
         else
             {
             // Now we have to check for any cart variables that turn on a track with this data type
             struct slPair *dt_vars = cartVarsLike(cart, toMatch);
             struct slPair *this_var = dt_vars;
             while (this_var != NULL)
                 {
                 if (cartBoolean(cart, this_var->name))
                     {
                     slNameAddHead(&selectedDataTypes, thisType->name);
                     selected = TRUE;
                     break;
                     }
                 this_var = this_var->next;
                 }
             slPairFreeList(&dt_vars);
             }
-        printf("%s\"%s\": %d", COMMA_IF(not_first), thisType->name, selected ? 1 : 0);
+        printf("%s\"%s\": {\"active\":%d, \"title\":\"%s\"}", COMMA_IF(not_first), thisType->name,
+                selected ? 1 : 0, stripEnclosingDoubleQuotes(thisType->val));
         }
     }
 // else: dataTypes dict is empty - JS will detect this
 printf(closeDataTypesJSON);
 printf(",");  // add separator
 
 // find selected data sets
 printf(openDataElementsJSON);
 not_first = 0;
 if (hasDataTypes)
     {
     char toMatch[token_size];
     safef(toMatch, token_size, "%s_*_*_sel", metaDataId);
     struct slPair *mdidVars = cartVarsLike(cart, toMatch);
     for (struct slPair *le = mdidVars; le != NULL; le = le->next)
@@ -3087,31 +3106,31 @@
     }
 printf(closeDataElementsJSON);
 printf(",\"mdid\": \"%s\"", metaDataId);
 printf(",\"primaryKey\": \"%s\"", primaryKey);  // must exist
 if (maxCheckboxes) // only if present in trackDb.settings entry
     printf(",\"maxCheckboxes\": \"%s\"", maxCheckboxes);
 if (colorSettingsUrl) // only if present in trackDb.settings entry
     printf(",\"colorSettingsUrl\": \"%s\"", colorSettingsUrl);
 printf(",\"metadataUrl\": \"%s\"", metaDataUrl);
 printf(closeJSON);
 /* --- END embedded JSON data --- */
 
 printf(metadataTableScriptElement);
 
 // cleanup
-slFreeList(&dataTypes);
+slPairFreeValsAndList(&dataTypes);
 hashFree(&defaultOn);
 }
 
 void specificUi(struct trackDb *tdb, struct trackDb *tdbList, struct customTrack *ct, boolean ajax)
 /* Draw track specific parts of UI. */
 {
 char *track = tdb->track;
 char *db = database;
 char *liftDb = cloneString(trackDbSetting(tdb, "quickLiftDb"));
 if (liftDb != NULL) 
     db = liftDb;
 // Ideally check cfgTypeFromTdb()/cfgByCfgType() first, but with all these special cases already in
 //    place, lets be cautious at this time.
 // NOTE: Developer, please try to use cfgTypeFromTdb()/cfgByCfgType().