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);