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/js/hgc.histogram.js src/hg/js/hgc.histogram.js new file mode 100644 index 00000000000..c992525ca70 --- /dev/null +++ src/hg/js/hgc.histogram.js @@ -0,0 +1,163 @@ +// hgc.histogram.js - ES6 module for drawing SVG histograms on hgc details pages. +// Loaded via dynamic import() from the detailsScript trackDb mechanism. +// bedDetails.scripts.histogram is an array of {field, value, title, xLabel} +// where value is logfmt: space-separated key=value pairs (e.g. "3=11 4=3809") + +export function histogram(bedDetails) { + if (!bedDetails || !bedDetails.scripts || !bedDetails.scripts.histogram) + return; + + let entries = bedDetails.scripts.histogram; + for (let entry of entries) { + if (!entry.value) + continue; + + let parsed = parseLogfmt(entry.value); + if (parsed.keys.length === 0) + continue; + + let svg = buildSvg(parsed.keys, parsed.values, entry.title, entry.xLabel); + + // Find the field row by id and draw into its value cell + let fieldRow = document.getElementById("bfld_" + entry.field); + if (fieldRow) { + let valCell = fieldRow.cells[1]; + if (valCell) + valCell.innerHTML = svg; + let labelCell = fieldRow.cells[0]; + if (labelCell) + labelCell.innerHTML = entry.title || "Distribution"; + } + } +} + +function parseLogfmt(str) { + // Parse logfmt string into parallel arrays of keys and numeric values. + // Supports: key=val, "quoted key"=val, key="quoted val" + // Examples: '3=11 4=3809', '"My point"=20 other=5' + let keys = []; + let values = []; + let i = 0; + let len = str.length; + while (i < len) { + // skip whitespace + while (i < len && str[i] === " ") + i++; + if (i >= len) + break; + // read key (possibly quoted) + let key; + if (str[i] === '"') { + i++; + let end = str.indexOf('"', i); + if (end === -1) + break; + key = str.substring(i, end); + i = end + 1; + } else { + let start = i; + while (i < len && str[i] !== "=" && str[i] !== " ") + i++; + key = str.substring(start, i); + } + // expect '=' + if (i >= len || str[i] !== "=") + break; + i++; + // read value (possibly quoted) + let val; + if (i < len && str[i] === '"') { + i++; + let vend = str.indexOf('"', i); + if (vend === -1) + break; + val = str.substring(i, vend); + i = vend + 1; + } else { + let vstart = i; + while (i < len && str[i] !== " ") + i++; + val = str.substring(vstart, i); + } + keys.push(key); + values.push(Number(val)); + } + return {keys, values}; +} + +function buildSvg(bins, values, title, xLabel) { + let maxVal = Math.max(...values); + if (maxVal === 0) + return "No data"; + + // Chart dimensions + let barWidth = Math.max(14, Math.min(36, Math.floor(600 / bins.length))); + let gap = Math.max(1, Math.floor(barWidth / 8)); + let chartHeight = 160; + let labelHeight = 30; + let topPad = 5; + let leftPad = 50; + let svgWidth = Math.max(200, leftPad + bins.length * barWidth + 10); + let svgHeight = chartHeight + labelHeight + topPad; + + let lines = []; + lines.push(``); + + // Y-axis tick marks + let ticks = calcTicks(maxVal); + for (let tickVal of ticks) { + let y = topPad + chartHeight - (tickVal / maxVal) * chartHeight; + lines.push(``); + lines.push(`${formatNumber(tickVal)}`); + lines.push(``); + } + + // Bars + let labelEvery = Math.ceil(bins.length / (svgWidth / 30)); + for (let i = 0; i < bins.length; i++) { + let barHeight = (values[i] / maxVal) * chartHeight; + let bx = leftPad + i * barWidth + gap; + let by = topPad + chartHeight - barHeight; + let bw = barWidth - 2 * gap; + + lines.push(``); + lines.push(`${bins[i]}: ${values[i]}`); + lines.push(''); + + if (i % labelEvery === 0 || bins.length <= 30) { + lines.push(`${bins[i]}`); + } + } + + // Axis lines + lines.push(``); + lines.push(``); + + if (xLabel) { + lines.push(`${xLabel}`); + } + + lines.push(''); + return lines.join("\n"); +} + +function calcTicks(maxVal) { + if (maxVal <= 5) + return [1, 2, 3, 4, 5].filter(v => v <= maxVal); + let magnitude = Math.pow(10, Math.floor(Math.log10(maxVal))); + let step = magnitude; + if (maxVal / step < 3) + step = magnitude / 2; + else if (maxVal / step > 6) + step = magnitude * 2; + let ticks = []; + for (let v = step; v <= maxVal; v += step) + ticks.push(Math.round(v)); + return ticks; +} + +function formatNumber(n) { + if (n >= 1000000) return (n / 1000000).toFixed(1) + "M"; + if (n >= 1000) return (n / 1000).toFixed(1) + "K"; + return "" + n; +}