7d149b1935f8a1e5f11f31f3cb8da99d31b3a8e6
jcasper
  Fri Apr 10 16:34:48 2026 -0700
Replaced handrolled JSON structure in faceted composite output from hgTrackUi with
jsonWrite.h library-generated version, and added a defaultSortField trackDb setting to faceted
composites to improve the subtrack presentation order, refs #36320

diff --git src/hg/hgTrackUi/hgTrackUi.c src/hg/hgTrackUi/hgTrackUi.c
index 46eb5b6070e..ab13cb88f3e 100644
--- src/hg/hgTrackUi/hgTrackUi.c
+++ src/hg/hgTrackUi/hgTrackUi.c
@@ -45,30 +45,31 @@
 #include "vcfUi.h" 
 #include "bbiFile.h"
 #include "ensFace.h"
 #include "microarray.h"
 #include "trackVersion.h"
 #include "gtexUi.h"
 #include "genbank.h"
 #include "botDelay.h"
 #include "customComposite.h"
 #include "hicUi.h"
 #include "decoratorUi.h"
 #include "genark.h"
 #include "cart.h"
 #include "filePath.h"
 #include "md5.h"
+#include "jsonWrite.h"
     
 #ifdef USE_HAL 
 #include "halBlockViz.h"
 #endif 
 
 #define MAIN_FORM "mainForm"
 #define WIGGLE_HELP_PAGE  "../goldenPath/help/hgWiggleTrackHelp.html"
 
 /* for earlyBotCheck() function at the beginning of main() */
 #define delayFraction   0.25    /* standard penalty is 1.0 for most CGIs */
                                 /* this one is 0.25 */
 static boolean issueBotWarning = FALSE;
 
 struct cart *cart = NULL;	/* Cookie cart with UI settings */
 char *database = NULL;		/* Current database. */
@@ -3002,31 +3003,30 @@
     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
  *   setting in the future.
  * - 'dataTypes': the names of the data types, ordered and space separated.
  *
  * This function will embed sessionDb.settings/cart data in the
  * generated HTML. Instead of embedding all relevant tracks, it
  * parses the tracks named like:
  *
@@ -3036,36 +3036,30 @@
  * much smaller. The faceted composite assumes that selection of
  * dataTypes applies to all dataElements, but within the cart, these
  * are separate tracks, and each must be present to be drawn. But
  * the JS doesn't need the product {dataType} x {dataElement}, just
  * the union {datType} U {dataElement}. These are put in two arrays
  * in a JSON section of the HTML.
  */
 
 const int token_size = 1024;
 
 // html elements for the controls page (from singleCellMerged)
 const char pageStyle[] =
     "<style>body.cgi { background: #F0F0F0; }"
     "table.hgInside { background: #FFFFFF; }</style>";
 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
 
 // --- Get data from 'settings' field in 'trackDb' entry ---
 // required
 const char *metaDataUrl = trackDbSetting(tdb, "metaDataUrl");
 const char *primaryKey = trackDbSetting(tdb, "primaryKey");
 
 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;
@@ -3087,34 +3081,35 @@
         if (chopLine(clone = cloneString(setting), words) >= 2)
             if (sameString(words[1], "off"))
                 enabled = FALSE;
         freeMem(clone);
         }
     if (enabled)
         {
         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);
+struct jsonWrite *jw = jsonWriteNew();
+jsonWriteObjectStart(jw, NULL);
+
 // find selected data types
-int not_first = 0;
+jsonWriteObjectStart(jw, "dataTypes");
 struct slName *selectedDataTypes = NULL;  // non-null val will be used as flag
 if (hasDataTypes)
     {
     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
@@ -3123,129 +3118,140 @@
             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);
             }
         char *label = htmlEncode(stripEnclosingDoubleQuotes(thisType->val));
-        printf("%s\"%s\": {\"active\":%d, \"title\":\"%s\"}", COMMA_IF(not_first), thisType->name,
-                selected ? 1 : 0, label);
+        jsonWriteObjectStart(jw, thisType->name);
+        jsonWriteNumber(jw, "active", selected ? 1 : 0);
+        jsonWriteString(jw, "title", label);
+        jsonWriteObjectEnd(jw);
         freeMem(label);
         }
     }
 // else: dataTypes dict is empty - JS will detect this
-printf(closeDataTypesJSON);
-printf(",");  // add separator
+jsonWriteObjectEnd(jw);
 
 // find selected data sets
-printf(openDataElementsJSON);
-not_first = 0;
+jsonWriteListStart(jw, "dataElements");
 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)
         {
         if (cartBoolean(cart, le->name))
             {
             const char *nameStart = le->name + metaDataIdLen + 1;
             const char *nameEnd = strchr(nameStart, '_');
             if (nameEnd && nameEnd > nameStart)
                 {
-                const int nameLen = nameEnd - nameStart;
-                printf("%s\"%.*s\"", COMMA_IF(not_first), nameLen, nameStart);
+                char *name = cloneStringZ(nameStart, nameEnd - nameStart);
+                jsonWriteString(jw, NULL, name);
+                freeMem(name);
                 }
             }
         }
     slPairFreeList(&mdidVars);
     // Now add data elements on by default that haven't been modified
     struct hashEl *el, *elList = hashElListHash(defaultOn);
     for (el = elList; el != NULL; el = el->next)
         {
         if (!cartVarExistsLike(cart, el->name))
             {
             const char *nameStart = el->name + metaDataIdLen + 1;
             const char *nameEnd = strchr(nameStart, '_');
             if (nameEnd && nameEnd > nameStart)
                 {
-                const int nameLen = nameEnd - nameStart;
-                printf("%s\"%.*s\"", COMMA_IF(not_first), nameLen, nameStart);
+                char *name = cloneStringZ(nameStart, nameEnd - nameStart);
+                jsonWriteString(jw, NULL, name);
+                freeMem(name);
                 }
             }
         }
     hashElFreeList(&elList);
     }
 else
     {
     // No data types - look for {mdid}_{de}_sel pattern (no dataType component)
     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)
         {
         if (cartBoolean(cart, le->name))
             {
             // Extract data element name: between mdid_ and _sel
             const char *nameStart = le->name + metaDataIdLen + 1;
             const char *nameEnd = strstr(nameStart, "_sel");
             if (nameEnd && nameEnd > nameStart)
                 {
-                const int nameLen = nameEnd - nameStart;
-                printf("%s\"%.*s\"", COMMA_IF(not_first), nameLen, nameStart);
+                char *name = cloneStringZ(nameStart, nameEnd - nameStart);
+                jsonWriteString(jw, NULL, name);
+                freeMem(name);
                 }
             }
         }
     slPairFreeList(&mdidVars);
 
     // Now add data elements on by default that haven't been modified
     struct hashEl *el, *elList = hashElListHash(defaultOn);
     for (el = elList; el != NULL; el = el->next)
         {
         if (!cartVarExistsLike(cart, el->name))
             {
             const char *nameStart = el->name + metaDataIdLen + 1;
             const char *nameEnd = strstr(nameStart, "_sel");
             if (nameEnd && nameEnd > nameStart)
                 {
-                const int nameLen = nameEnd - nameStart;
-                printf("%s\"%.*s\"", COMMA_IF(not_first), nameLen, nameStart);
+                char *name = cloneStringZ(nameStart, nameEnd - nameStart);
+                jsonWriteString(jw, NULL, name);
+                freeMem(name);
                 }
             }
         }
     hashElFreeList(&elList);
     }
-printf(closeDataElementsJSON);
-printf(",\"mdid\": \"%s\"", metaDataId);
-printf(",\"primaryKey\": \"%s\"", primaryKey);  // must exist
+jsonWriteListEnd(jw);
+
+jsonWriteString(jw, "mdid", (char *)metaDataId);
+jsonWriteString(jw, "primaryKey", (char *)primaryKey);  // must exist
 if (maxCheckboxes) // only if present in trackDb.settings entry
-    printf(",\"maxCheckboxes\": \"%s\"", maxCheckboxes);
+    jsonWriteString(jw, "maxCheckboxes", (char *)maxCheckboxes);
 if (colorSettingsUrl) // only if present in trackDb.settings entry
-    printf(",\"colorSettingsUrl\": \"%s\"", cgiEncode((char*) colorSettingsUrl));
-printf(",\"metadataUrl\": \"%s\"", cgiEncode((char*) metaDataUrl));
-printf(",\"track\": \"%s\"", tdb->track);
+    jsonWriteString(jw, "colorSettingsUrl", cgiEncode((char *)colorSettingsUrl));
+jsonWriteString(jw, "metadataUrl", cgiEncode((char *)metaDataUrl));
+jsonWriteString(jw, "track", tdb->track);
+char *defaultSortField = trackDbSetting(tdb, "defaultSortField");
+if (isNotEmpty(defaultSortField))
+    jsonWriteString(jw, "defaultSortField", defaultSortField);
 if (isNotEmpty(cartOptionalString(cart, "udcTimeout")))
-    printf(",\"udcTimeout\": true");
-printf(closeJSON);
+    jsonWriteBoolean(jw, "udcTimeout", TRUE);
+
+jsonWriteObjectEnd(jw);
+printf("<script id=\"app-data\" type=\"application/json\">%s</script>\n", jw->dy->string);
+jsonWriteFree(&jw);
 /* --- END embedded JSON data --- */
 
 jsIncludeFile("dataTables-2.2.2.min.js", NULL);
 jsIncludeFile("dataTables.select-3.0.0.min.js", NULL);
 jsIncludeFile("facetedComposite.js", NULL);
 
 webIncludeResourceFile("dataTables-2.2.2.min.css");
 webIncludeResourceFile("dataTables.select-3.0.0.min.css");
 webIncludeResourceFile("facetedComposite.css");
 
 
 // cleanup
 slPairFreeValsAndList(&dataTypes);
 hashFree(&defaultOn);
 }