ddb85ced5e8b6127a233b5cda5fcb1fbe2260578
max
  Wed Mar 25 04:22:06 2026 -0700
Add detailsScript trackDb mechanism for JS visualizations on bigBed details pages

Changing based on feedback from Jonathan, Chris and Brian after group
discussion. Refactored existing Claude-generated code, moving functions
into libraries.

This is the first use of ES6 modules in the kent js code. In 2026, this
should be acceptable?

New trackDb syntax: detailsScript.<plotType>.<fieldName> <jsonConfig>
The C code (bigBedClick.c) collects these settings, exports field values as JSON
(bedDetails object), and dynamically imports hgc.<plotType>.js as an ES6 module.
Fields used by detailsScript are shown in the HTML table with empty values,
filled by JavaScript.

Includes hgc.histogram.js module for drawing SVG bar chart histograms from
logfmt-encoded data (space-separated key=value pairs). Applied to both the
trexplorer and webstr tracks in the strVar supertrack.

Also adds jsonWriteJsonElement() helper to jsonWrite.c for writing parsed
jsonElement trees into a jsonWrite stream.

max, refs #36652
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git src/hg/hgc/hgc.c src/hg/hgc/hgc.c
index ba853ae81d5..017d99936ed 100644
--- src/hg/hgc/hgc.c
+++ src/hg/hgc/hgc.c
@@ -1784,51 +1784,75 @@
 void printFieldLabel(char *entry)
 /* print the field label, the first column in the table, as a <td>. Allow a
  * longer description after a |-char, as some fields are not easy to
  * understand. */
 {
 printFieldLabelInner(entry, NULL);
 }
 
 void printFieldLabelWithId(char *entry, char *fieldName)
 /* Like printFieldLabel but adds id="bfld_<fieldName>" to the <tr> element,
  * so JavaScript can find the row by field name. */
 {
 printFieldLabelInner(entry, fieldName);
 }
 
+static struct slName *detailsScriptFieldNames(struct trackDb *tdb)
+/* Return list of bigBed field names used by detailsScript.* trackDb settings.
+ * These fields are rendered by JavaScript, so their values should not be printed in the HTML table.
+ * See also bigBedClick.c which parses the same settings to build JSON and load JS modules. */
+{
+struct slName *dsSettings = trackDbLocalSettingsWildMatch(tdb, DETAILS_SCRIPT_PREFIX);
+struct slName *fieldNames = NULL;
+struct slName *setting;
+for (setting = dsSettings; setting != NULL; setting = setting->next)
+    {
+    char *dot1 = strchr(setting->name, '.');
+    if (dot1)
+        {
+        char *dot2 = strchr(dot1 + 1, '.');
+        if (dot2)
+            slNameAddHead(&fieldNames, dot2 + 1);
+        }
+    }
+slFreeList(&dsSettings);
+return fieldNames;
+}
+
 #define TDB_STATICTABLE_SETTING "extraDetailsTable"
 #define TDB_STATICTABLE_SETTING_2 "detailsStaticTable"
 int extraFieldsPrintAs(struct trackDb *tdb,struct sqlResult *sr,char **fields,int fieldCount, struct asObject *as)
 // Any extra bed or bigBed fields (defined in as and occurring after N in bed N + types.
 // sr may be null for bigBeds.
 // Returns number of extra fields actually printed.
 {
 // We are trying to print extra fields so we need to figure out how many fields to skip
 int start = extraFieldsStart(tdb, fieldCount, as);
 
 struct asColumn *col = as->columnList;
 char *urlsStr = trackDbSettingClosestToHomeOrDefault(tdb, "urls", NULL);
 struct hash* fieldToUrl = hashFromString(urlsStr);
 boolean skipEmptyFields = trackDbSettingOn(tdb, "skipEmptyFields");
 
 // make list of fields to skip
 char *skipFieldsStr = trackDbSetting(tdb, "skipFields");
 struct slName *skipIds = NULL;
 if (skipFieldsStr)
     skipIds = slNameListFromComma(skipFieldsStr);
 
+struct slName *dsScriptFields = detailsScriptFieldNames(tdb);
+
 // make list of fields that are separated from other fields
 char *sepFieldsStr = trackDbSetting(tdb, "sepFields");
 struct slName *sepFields = NULL;
 if (sepFieldsStr)
     sepFields = slNameListFromComma(sepFieldsStr);
 
 // make list of fields that we want to substitute
 // this setting has format description|URLorFilePath, with the stuff before the pipe optional
 char *extraDetailsTableName = NULL, *extraDetails = cloneString(trackDbSetting(tdb, TDB_STATICTABLE_SETTING));
 if (extraDetails && strchr(extraDetails,'|'))
     {
     extraDetailsTableName = extraDetails;
     extraDetails = strchr(extraDetails,'|');
     *extraDetails++ = 0;
     }
@@ -1934,31 +1958,34 @@
     // split this table to separate current row from the previous one, if the trackDb option is set
     if (sepFields && slNameInList(sepFields, fieldName))
         printf("</tr></table>\n<p>\n<table class='bedExtraTbl'>");
 
     // field description
     char *entry;
     if (sameString(fieldName, "cdsStartStat") && sameString("enum('none','unk','incmpl','cmpl')", col->comment))
         entry = "Status of CDS start annotation (none, unknown, incomplete, or complete)";
     else if (sameString(fieldName, "cdsEndStat") && sameString("enum('none','unk','incmpl','cmpl')", col->comment))
         entry = "Status of CDS end annotation (none, unknown, incomplete, or complete)";
     else
         entry = col->comment;
 
     printFieldLabelWithId(entry, fieldName);
 
-    if (col->isList || col->isArray || col->lowType->stringy || asTypesIsInt(col->lowType->type))
+    // detailsScript fields: print empty cell, JavaScript will fill it
+    if (dsScriptFields && slNameInList(dsScriptFields, fieldName))
+        printf("<td></td></tr>\n");
+    else if (col->isList || col->isArray || col->lowType->stringy || asTypesIsInt(col->lowType->type))
         printIdOrLinks(col, fieldToUrl, tdb, fields[ix]);
     else if (asTypesIsFloating(col->lowType->type))
         {
         double valDouble = strtod(fields[ix],NULL);
         if (errno == 0 && valDouble != 0)
             printf("<td>%g</td></tr>\n", valDouble);
         else
             printf("<td>%s</td></tr>\n", fields[ix]); // decided not to print error
         }
     else
         printf("<td class='bedExtraTblVal'>%s</td></tr>\n", fields[ix]);
     printCount++;
     }
 if (skipIds)
     slFreeList(skipIds);