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/js/detailsHistogram.js src/hg/js/detailsHistogram.js deleted file mode 100644 index 7044d99a075..00000000000 --- src/hg/js/detailsHistogram.js +++ /dev/null @@ -1,132 +0,0 @@ -// detailsHistogram.js - Draw SVG histograms on hgc details pages from bigBed field data. -// Called automatically via the detailsJs trackDb mechanism. -// Expects bedDetails object with: track, chrom, start, end, fields, args -// args.histograms is an array of {binsField, valuesField, title} - -function detailsHistogram(bedDetails) { - if (!bedDetails || !bedDetails.args || !bedDetails.args.histograms) - return; - if (!bedDetails.fields) - return; - - var histograms = bedDetails.args.histograms; - for (var ci = 0; ci < histograms.length; ci++) { - var hist = histograms[ci]; - var binsStr = bedDetails.fields[hist.binsField]; - var valuesStr = bedDetails.fields[hist.valuesField]; - if (!binsStr || !valuesStr) - continue; - - var bins = binsStr.split(","); - var values = valuesStr.split(",").map(Number); - if (bins.length !== values.length || bins.length === 0) - continue; - - var svg = buildHistogramSvg(bins, values, hist.title); - - // Find the bins row by id attribute set by hgc and replace its value cell - var binsRow = document.getElementById("bfld_" + hist.binsField); - var valuesRow = document.getElementById("bfld_" + hist.valuesField); - if (binsRow) { - var valCell = binsRow.cells[1]; - if (valCell) - valCell.innerHTML = svg; - // Update the label to the histogram title - var labelCell = binsRow.cells[0]; - if (labelCell) - labelCell.innerHTML = hist.title || "Distribution"; - } - // Remove the values row since data is now visualized - if (valuesRow) - valuesRow.remove(); - } -} - -function buildHistogramSvg(bins, values, title) { - var maxVal = Math.max.apply(null, values); - if (maxVal === 0) - return "<em>No data</em>"; - - // Chart dimensions - var barWidth = Math.max(14, Math.min(36, Math.floor(600 / bins.length))); - var gap = Math.max(1, Math.floor(barWidth / 8)); - var chartHeight = 160; - var labelHeight = 30; - var topPad = 5; - var leftPad = 50; // room for y-axis labels - var svgWidth = leftPad + bins.length * barWidth + 10; - var svgHeight = chartHeight + labelHeight + topPad; - - var lines = []; - lines.push('<svg width="' + svgWidth + '" height="' + svgHeight + '" ' + - 'style="font-family: sans-serif; font-size: 11px;">'); - - // Y-axis: a few tick marks - var ticks = calcHistTicks(maxVal); - for (var ti = 0; ti < ticks.length; ti++) { - var tickVal = ticks[ti]; - var y = topPad + chartHeight - (tickVal / maxVal) * chartHeight; - lines.push('<line x1="' + (leftPad - 4) + '" y1="' + y + '" x2="' + (leftPad) + - '" y2="' + y + '" stroke="#666"/>'); - lines.push('<text x="' + (leftPad - 6) + '" y="' + (y + 4) + - '" text-anchor="end" fill="#333" font-size="10">' + formatHistNumber(tickVal) + '</text>'); - // Subtle grid line - lines.push('<line x1="' + leftPad + '" y1="' + y + '" x2="' + (svgWidth - 10) + - '" y2="' + y + '" stroke="#eee"/>'); - } - - // Bars - for (var i = 0; i < bins.length; i++) { - var barHeight = (values[i] / maxVal) * chartHeight; - var bx = leftPad + i * barWidth + gap; - var by = topPad + chartHeight - barHeight; - var bw = barWidth - 2 * gap; - - lines.push('<rect x="' + bx + '" y="' + by + '" width="' + bw + - '" height="' + barHeight + '" fill="#4682B4">'); - lines.push('<title>' + bins[i] + ': ' + values[i] + '</title>'); - lines.push('</rect>'); - - // X-axis label (skip some if too crowded) - var labelEvery = Math.ceil(bins.length / (svgWidth / 30)); - if (i % labelEvery === 0 || bins.length <= 30) { - lines.push('<text x="' + (bx + bw / 2) + '" y="' + (topPad + chartHeight + 14) + - '" text-anchor="middle" fill="#333" font-size="10">' + bins[i] + '</text>'); - } - } - - // Axis lines - lines.push('<line x1="' + leftPad + '" y1="' + topPad + '" x2="' + leftPad + - '" y2="' + (topPad + chartHeight) + '" stroke="#333"/>'); - lines.push('<line x1="' + leftPad + '" y1="' + (topPad + chartHeight) + '" x2="' + - (svgWidth - 10) + '" y2="' + (topPad + chartHeight) + '" stroke="#333"/>'); - - // X-axis label - lines.push('<text x="' + (leftPad + (svgWidth - leftPad) / 2) + '" y="' + - (svgHeight - 2) + '" text-anchor="middle" fill="#333" font-size="11">Allele size (repeat copies)</text>'); - - lines.push('</svg>'); - return lines.join("\n"); -} - -function calcHistTicks(maxVal) { - // Return 3-5 nice tick values for the y-axis - if (maxVal <= 5) - return [1, 2, 3, 4, 5].filter(function(v) { return v <= maxVal; }); - var magnitude = Math.pow(10, Math.floor(Math.log10(maxVal))); - var step = magnitude; - if (maxVal / step < 3) - step = magnitude / 2; - else if (maxVal / step > 6) - step = magnitude * 2; - var ticks = []; - for (var v = step; v <= maxVal; v += step) - ticks.push(Math.round(v)); - return ticks; -} - -function formatHistNumber(n) { - if (n >= 1000000) return (n / 1000000).toFixed(1) + "M"; - if (n >= 1000) return (n / 1000).toFixed(1) + "K"; - return "" + n; -}