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.. The C code (bigBedClick.c) collects these settings, exports field values as JSON (bedDetails object), and dynamically imports hgc..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) 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("

Links to sequence:

\n"); printf("
    \n"); puts("
  • \n"); hgcAnchorSomewhere("htcTranslatedPredMRna", item, "translate", seqName); printf("Translated Protein from genomic DNA\n"); puts("
  • \n"); puts("
  • \n"); hgcAnchorSomewhere("htcGeneMrna", item, track, seqName); printf("Predicted mRNA \n"); puts("
  • \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.. 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.." + 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);