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/bigBedClick.c src/hg/hgc/bigBedClick.c
index 6df3282ae3c..d3f24d7a78f 100644
--- src/hg/hgc/bigBedClick.c
+++ src/hg/hgc/bigBedClick.c
@@ -6,30 +6,31 @@
 #include "common.h"
 #include "wiggle.h"
 #include "cart.h"
 #include "hgc.h"
 #include "hCommon.h"
 #include "hgColors.h"
 #include "bigBed.h"
 #include "hui.h"
 #include "subText.h"
 #include "web.h"
 #include "chromAlias.h"
 #include "quickLift.h"
 #include "hgConfig.h"
 #include "jsHelper.h"
 #include "jsonParse.h"
+#include "jsonWrite.h"
 
 static void bigGenePredLinks(char *track, char *item)
 /* output links to genePred driven sequence dumps */
 {
 printf("<H3>Links to sequence:</H3>\n");
 printf("<UL>\n");
 puts("<LI>\n");
 hgcAnchorSomewhere("htcTranslatedPredMRna", item, "translate", seqName);
 printf("Translated Protein</A> from genomic DNA\n");
 puts("</LI>\n");
 
 puts("<LI>\n");
 hgcAnchorSomewhere("htcGeneMrna", item, track, seqName);
 printf("Predicted mRNA</A> \n");
 puts("</LI>\n");
@@ -354,30 +355,71 @@
 	{
 	/* need to show the empty off-targets for crispr tracks */
 	if (startsWith("crispr", tdb->track))
 	    extFieldCrisprOfftargets(NULL, NULL);
         // empty or "0" value in bigBed means that the lookup should not be performed
         continue;
 	}
     off_t offset = atoll(offsetStr);
 
     printCount += seekAndPrintTable(tdb, detailsUrl, offset, extraFields);
     }
 slPairFreeValsAndList(&detailsUrls);
 return printCount;
 }
 
+static struct hash *detailsScriptGroupByPlotType(struct trackDb *tdb)
+/* Parse detailsScript.<plotType>.<fieldName> trackDb settings and return a hash
+ * of plotType -> slPair list (fieldName -> jsonConfig).  Returns NULL if no settings found.
+ * See also hgc.c detailsScriptFieldNames() which parses the same settings for field skipping. */
+{
+struct slName *settings = trackDbLocalSettingsWildMatch(tdb, DETAILS_SCRIPT_PREFIX);
+if (settings == NULL)
+    return NULL;
+struct hash *plotTypeHash = hashNew(0);
+struct slName *setting;
+for (setting = settings; setting != NULL; setting = setting->next)
+    {
+    // Parse "detailsScript.<plotType>.<fieldName>"
+    char *key = cloneString(setting->name);
+    char *dot1 = strchr(key, '.');
+    if (dot1 == NULL)
+        continue;
+    dot1++;
+    char *dot2 = strchr(dot1, '.');
+    if (dot2 == NULL)
+        continue;
+    *dot2 = '\0';
+    char *plotType = dot1;
+    char *fieldName = dot2 + 1;
+    char *jsonConfig = trackDbSetting(tdb, setting->name);
+
+    struct slPair *entry;
+    AllocVar(entry);
+    entry->name = cloneString(fieldName);
+    entry->val = cloneString(jsonConfig);
+    struct slPair *existing = hashFindVal(plotTypeHash, plotType);
+    slAddTail(&existing, entry);
+    if (hashLookup(plotTypeHash, plotType) == NULL)
+        hashAdd(plotTypeHash, plotType, entry);
+    else
+        hashReplace(plotTypeHash, plotType, existing);
+    }
+slFreeList(&settings);
+return plotTypeHash;
+}
+
 static void bigBedClick(char *fileName, struct trackDb *tdb,
                      char *item, int start, int end, int bedSize)
 /* Handle click in generic bigBed track. */
 {
 char *chrom = cartString(cart, "c");
 
 /* Open BigWig file and get interval list. */
 struct bbiFile *bbi =  bigBedFileOpenAlias(fileName, chromAliasFindAliases);
 struct lm *lm = lmInit(0);
 int ivStart = start, ivEnd = end;
 char *itemForUrl = item;
 if (start == end)
     {
     // item is an insertion; expand the search range from 0 bases to 2 so we catch it:
     ivStart = max(0, start-1);
@@ -530,102 +572,96 @@
     if (isCustomTrack(tdb->track))
 	{
 	time_t timep = bbiUpdateTime(bbi);
 	printBbiUpdateTime(&timep);
 	}
     char *motifPwmTable = trackDbSetting(tdb, "motifPwmTable");
     if (motifPwmTable)
         {
         struct dnaSeq *seq = hDnaFromSeq(database, bed->chrom, bed->chromStart, bed->chromEnd, dnaLower);
         if (bed->strand[0] == '-')
             reverseComplement(seq->dna, seq->size);
         struct dnaMotif *motif = loadDnaMotif(bed->name, motifPwmTable);
         motifHitSection(seq, motif);
         }
 
-    // detailsJs: load JavaScript files and export selected field data as JSON
-    char *detailsJs = trackDbSetting(tdb, "detailsJs");
-    if (detailsJs)
+    // detailsScript.*: load JS visualization scripts and export field data as JSON
+    // see also hgc.c detailsScriptFieldNames() which parses the same settings to skip fields
+    struct hash *plotTypeHash = detailsScriptGroupByPlotType(tdb);
+    if (plotTypeHash)
+        {
+        // Build the bedDetails JSON object using jsonWrite
+        struct jsonWrite *jw = jsonWriteNew();
+        jsonWriteObjectStart(jw, NULL);
+        jsonWriteString(jw, "track", tdb->track);
+        jsonWriteString(jw, "chrom", chrom);
+        jsonWriteNumber(jw, "start", bed->chromStart);
+        jsonWriteNumber(jw, "end", bed->chromEnd);
+        jsonWriteObjectStart(jw, "scripts");
+
+        struct hashEl *hel, *helList = hashElListHash(plotTypeHash);
+        for (hel = helList; hel != NULL; hel = hel->next)
             {
-        // Include each comma-separated JS file
-        char *jsFiles = cloneString(detailsJs);
-        char *words[64];
-        int jsFileCount = chopCommas(jsFiles, words);
-        int ji;
-        for (ji = 0; ji < jsFileCount; ji++)
+            struct slPair *fieldList = hel->val;
+            jsonWriteListStart(jw, hel->name);
+            struct slPair *fp;
+            for (fp = fieldList; fp != NULL; fp = fp->next)
                 {
-            char *jsFile = trimSpaces(words[ji]);
-            if (isNotEmpty(jsFile))
-                jsIncludeFile(jsFile, NULL);
+                jsonWriteObjectStart(jw, NULL);
+                jsonWriteString(jw, "field", fp->name);
+                // Look up field value from bigBed extra fields
+                char *fv = "";
+                if (extraFieldPairs)
+                    {
+                    char *found = slPairFindVal(extraFieldPairs, fp->name);
+                    if (found)
+                        fv = found;
+                    }
+                jsonWriteString(jw, "value", fv);
+                // Parse trackDb JSON config and merge its keys into this object
+                char *jsonConfig = fp->val;
+                if (isNotEmpty(jsonConfig))
+                    {
+                    struct jsonElement *configEl = jsonParse(jsonConfig);
+                    struct hash *configHash = jsonObjectVal(configEl, "detailsScript config");
+                    struct hashEl *cel, *celList = hashElListHash(configHash);
+                    for (cel = celList; cel != NULL; cel = cel->next)
+                        jsonWriteJsonElement(jw, cel->name, cel->val);
+                    hashElFreeList(&celList);
+                    }
+                jsonWriteObjectEnd(jw);
+                }
+            jsonWriteListEnd(jw);
             }
 
-        // Build the bedDetails JSON object
+        jsonWriteObjectEnd(jw);  // scripts
+        jsonWriteObjectEnd(jw);  // root
+
+        // Emit as inline JavaScript
         struct dyString *ds = dyStringNew(1024);
-        dyStringPrintf(ds, "var bedDetails = {\"track\":\"%s\",\"chrom\":\"%s\","
-            "\"start\":%d,\"end\":%d",
-            tdb->track, chrom, bed->chromStart, bed->chromEnd);
-
-        // Export requested fields
-        char *detailsJsFieldsStr = trackDbSetting(tdb, "detailsJsFields");
-        if (detailsJsFieldsStr && extraFieldPairs)
-            {
-            dyStringAppend(ds, ",\"fields\":{");
-            char *fieldsCopy = cloneString(detailsJsFieldsStr);
-            char *fieldNames[256];
-            int nFields = chopCommas(fieldsCopy, fieldNames);
-            boolean first = TRUE;
-            int fi;
-            for (fi = 0; fi < nFields; fi++)
-                {
-                char *fn = trimSpaces(fieldNames[fi]);
-                char *fv = slPairFindVal(extraFieldPairs, fn);
-                if (fv == NULL)
-                    fv = "";
-                if (!first)
-                    dyStringAppendC(ds, ',');
-                char *escaped = jsonStringEscape(fv);
-                dyStringPrintf(ds, "\"%s\":\"%s\"", fn, escaped);
-                freeMem(escaped);
-                first = FALSE;
-                }
-            dyStringAppendC(ds, '}');
-            }
-
-        // Include detailsJsArgs if present
-        char *detailsJsArgs = trackDbSetting(tdb, "detailsJsArgs");
-        if (detailsJsArgs)
-            dyStringPrintf(ds, ",\"args\":%s", detailsJsArgs);
-
-        dyStringAppend(ds, "};\n");
-
-        // Call the default function derived from each JS filename (strip .js)
-        // e.g. barChart.js -> barChart(bedDetails)
-        for (ji = 0; ji < jsFileCount; ji++)
-            {
-            char *jsFile = trimSpaces(words[ji]);
-            if (isEmpty(jsFile))
-                continue;
-            char funcName[256];
-            safecpy(funcName, sizeof(funcName), jsFile);
-            // strip .js extension
-            char *dot = strrchr(funcName, '.');
-            if (dot)
-                *dot = '\0';
-            dyStringPrintf(ds, "$(document).ready(function() { %s(bedDetails); });\n", funcName);
-            }
+        dyStringPrintf(ds, "var bedDetails = %s;\n", jw->dy->string);
+
+        // Dynamically import and call each plot type's module
+        for (hel = helList; hel != NULL; hel = hel->next)
+            dyStringPrintf(ds, "$(document).ready(function() {\n"
+                "  import('../js/hgc.%s.js').then(function(mod) { mod.%s(bedDetails); });\n"
+                "});\n", hel->name, hel->name);
 
         jsInline(dyStringCannibalize(&ds));
+        jsonWriteFree(&jw);
+        hashElFreeList(&helList);
+        hashFree(&plotTypeHash);
         }
     }
 if (!found)
     {
     printf("No item %s starting at %d\n", emptyForNull(item), start);
     }
 lmCleanup(&lm);
 bbiFileClose(&bbi);
 }
 
 void genericBigBedClick(struct sqlConnection *conn, struct trackDb *tdb,
                      char *item, int start, int end, int bedSize)
 /* Handle click in generic bigBed track. */
 {
 char *fileName = bbiNameFromSettingOrTable(tdb, conn, tdb->table);