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/lib/jsonWrite.c src/lib/jsonWrite.c
index 1c8db7f2c23..5f271e5174e 100644
--- src/lib/jsonWrite.c
+++ src/lib/jsonWrite.c
@@ -231,30 +231,73 @@
 void jsonWriteAppend(struct jsonWrite *jwA, char *var, struct jsonWrite *jwB)
 /* Append jwB's contents to jwA's.  If jwB is non-NULL, it must be fully closed (no unclosed
  * list or object).  If var is non-NULL, write it out as a tag before appending.
  * If both var and jwB are NULL, leave jwA unchanged. */
 {
 if (jwB && jwB->stackIx)
     errAbort("jsonWriteAppend: second argument must be fully closed but its stackIx is %d not 0",
              jwB->stackIx);
 jsonWriteTag(jwA, var);
 if (jwB)
     dyStringAppendN(jwA->dy, jwB->dy->string, jwB->dy->stringSize);
 else if (var)
     dyStringAppend(jwA->dy, "null");
 }
 
+void jsonWriteJsonElement(struct jsonWrite *jw, char *var, struct jsonElement *el)
+/* Write a jsonElement (parsed JSON) into jw.  Handles all types recursively. */
+{
+switch (el->type)
+    {
+    case jsonString:
+        jsonWriteString(jw, var, el->val.jeString);
+        break;
+    case jsonNumber:
+        jsonWriteNumber(jw, var, el->val.jeNumber);
+        break;
+    case jsonDouble:
+        jsonWriteDouble(jw, var, el->val.jeDouble);
+        break;
+    case jsonBoolean:
+        jsonWriteBoolean(jw, var, el->val.jeBoolean);
+        break;
+    case jsonObject:
+        {
+        jsonWriteObjectStart(jw, var);
+        struct hashEl *hel, *helList = hashElListHash(el->val.jeHash);
+        for (hel = helList; hel != NULL; hel = hel->next)
+            jsonWriteJsonElement(jw, hel->name, hel->val);
+        hashElFreeList(&helList);
+        jsonWriteObjectEnd(jw);
+        break;
+        }
+    case jsonList:
+        {
+        jsonWriteListStart(jw, var);
+        struct slRef *ref;
+        for (ref = el->val.jeList; ref != NULL; ref = ref->next)
+            jsonWriteJsonElement(jw, NULL, ref->val);
+        jsonWriteListEnd(jw);
+        break;
+        }
+    case jsonNull:
+        jsonWriteTag(jw, var);
+        dyStringAppend(jw->dy, "null");
+        break;
+    }
+}
+
 int jsonWritePopToLevel(struct jsonWrite *jw, uint level)
 /* Close out the objects and lists that are deeper than level, so we end up at level ready to
  * add new items.  Return the level that we end up with, which may not be the same as level,
  * if level is deeper than the current stack. */
 {
 int i;
 for (i = jw->stackIx;  i > level;  i--)
     {
     if (jw->objStack[i].isObject)
         jsonWriteObjectEnd(jw);
     else
         jsonWriteListEnd(jw);
     }
 return jw->stackIx;
 }