9af188ea6147f9edb20bd531d3ec988501cf997c
chmalee
  Fri Feb 6 12:18:25 2026 -0800
Fix jsonOutputArrays columnTypes output when more than one track is requested with /getData/track. Leaves /list/schema alone. Add option to hgTracks Downloads -> Download track data in view menu to include column headers in output, refs #36858

diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index 25c93b14a81..b16c28aaf2c 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -5664,71 +5664,85 @@
 ////////
 var downloadCurrentTrackData = {
     downloadData: {}, // container for holding data while it comes in from the api
     currentRequests: {}, // pending requests
     intervalId: null, // the id of the timer that waits on the api
 
     failedTrackDataRequest: function(msg) {
         msgJson = JSON.parse(msg);
         alert("Download failed. Error message: '" + msgJson.error);
     },
 
     receiveTrackData: function(track, data) {
         downloadCurrentTrackData.downloadData[track] = data;
     },
 
-    convertJson: function(data, outType) {
+    convertJson: function(data, outType, withHeaders) {
         if (outType !== "tsv" && outType !== "csv") {
             alert("ERROR: incorrect output format option");
             return null;
         }
         outSep = outType === "tsv" ? '\t' : ',';
         // TODO: someday we will probably want to include some of these fields
         // for each track downloaded, perhaps as an option
         ignoredKeys = new Set(["chrom", "dataTime", "dataTimeStamp", "downloadTime", "downloadTimeStamp",
             "start", "end", "track", "trackType", "genome", "itemsReturned", "columnTypes",
             "bigDataUrl", "chromSize", "hubUrl"]);
         // first get rid of top level non track object keys
         _.each(data, function(val, key) {
-            if (ignoredKeys.has(key)) {delete data[key];}
+            if (ignoredKeys.has(key)) {
+                // squirrel away the columnTypes if requested
+                if (key === "columnTypes") {
+                    columnTypes = data[key];
+                }
+                delete data[key];
+            }
         });
         // now go through each track and format it correctly
         str = "";
         _.each(data, function(val, track) {
             str += "track name=\"" + track + "\"\n";
+            if (withHeaders) {
+                headers = [];
+                for (let i of columnTypes[track]) {
+                    headers.push(i.name);
+                }
+                str += headers.join(outSep) + "\n";
+            }
             for (var row in val) {
                 for (var  i = 0; i < val[row].length; i++) {
                     str += JSON.stringify(val[row][i]);
                     if (i < val[row].length) { str += outSep; }
                 }
                 str += "\n";
             }
             str += "\n"; // extra new line after each track oh well
         });
         return new Blob([str], {type: "text/plain"});
     },
 
     makeDownloadFile: function(key) {
         if (_.keys(downloadCurrentTrackData.currentRequests).length === 0) {
             // first stop the timer so we don't execute again
             clearInterval(downloadCurrentTrackData.intervalId);
-            outType = $("#outputFormat")[0].selectedOptions[0].value;
+            let outType = $("#outputFormat")[0].selectedOptions[0].value;
+            let withHeaders = document.getElementById("downloadTrackHeaders").checked;
             var blob = null;
             if (outType === 'json') {
                 blob = new Blob([JSON.stringify(downloadCurrentTrackData.downloadData[key])], {type: "text/plain"});
             } else {
-                blob = downloadCurrentTrackData.convertJson(downloadCurrentTrackData.downloadData[key], outType);
+                blob = downloadCurrentTrackData.convertJson(downloadCurrentTrackData.downloadData[key], outType, withHeaders);
             }
             if (blob) {
                 anchor = document.createElement("a");
                 anchor.href = URL.createObjectURL(blob);
                 fname = $("#downloadFileName")[0].value;
                 if (fname.length === 0) {
                     fname = "trackDownload.txt";
                 }
                 switch (outType) {
                     case "tsv":
                         if (!fname.endsWith(".tsv")) {fname += ".tsv";}
                         break;
                     case "csv":
                         if (!fname.endsWith(".csv")) {fname += ".csv";}
                         break;
@@ -5842,38 +5856,41 @@
                     htmlStr += " disabled ";
                 } else {
                     htmlStr += " checked ";
                 }
                 htmlStr +=  ">";
                 htmlStr += "<label>" + track.shortLabel + "</label>";
                 htmlStr += "</input>";
                 if (showDisabledMsg) {
                     htmlStr += "&nbsp;<span id='" + trackName + "Tooltip'><a href='#'>(?)</a></span>";
                 }
                 htmlStr += "<br>";
             }
         });
         htmlStr += "<div ><label style='padding-right: 10px' for='downloadFileName'>Enter an output file name</label>";
         htmlStr += "<input type=text size=30 class='downloadFileName' id='downloadFileName'" +
-            " value='" + getDb() + ".tracks'</input>";
+            " value='" + getDb() + ".tracks'></input>";
         htmlStr += "<br>";
         htmlStr += "<label style='padding-right: 10px' for='outputFormat'>Choose an output format</label>";
         htmlStr += "<select name='outputFormat' id='outputFormat'>";
         htmlStr += "<option selected value='json'>JSON</option>";
         htmlStr += "<option value='csv'>CSV</option>";
         htmlStr += "<option value='tsv'>TSV</option>";
         htmlStr += "</select>";
+        htmlStr += "<br>";
+        htmlStr += "<label style='padding-rught: 10px' for='downloadTrackHeaders'>Include track column headers</label>";
+        htmlStr += "<input type='checkbox' id='downloadTrackHeaders'></input>";
         htmlStr += "</div>";
         downloadDialog.innerHTML = htmlStr;
         $("#checkAllDownloadTracks").on("click", function() {
             $(".downloadTrackName").each(function(i, elem) {
                 elem.checked = true;
             });
         });
         $("#uncheckAllDownloadTracks").on("click", function() {
             $(".downloadTrackName").each(function(i, elem) {
                 elem.checked = false;
             });
         });
         $(downloadDialog).dialog('open');
         $("[id$='Tooltip'").each(function(i, elem) {
             addMouseover(elem, "This track must be downloaded with the Table Browser");