d87ef2fb8472135b899fd1a89434033d5895217a
mspeir
  Thu Oct 1 15:31:26 2026 -0700
Add barChartReorder, a utility for changing the order of the bars in a barChart track, refs #37619

The bars of a barChart are drawn in the order their values appear in the
expScores field, and nothing re-sorts them when the track is drawn, so the
order is a property of the data file. Putting the bars in a different order
means rewriting that field in every row, which is awkward enough by hand that
we have twice done it for a data author instead of explaining it. This has
come up on #36998 and #37001.

barChartReorder takes the current bar order and the wanted order as two lists
of names, rewrites expScores in every row, optionally puts a .categories file
into the same order, and prints the barChartBars line for the track. It works
on any barChart BED whatever produced it, so unlike the --groupOrderFile
option of expMatrixToBarchartBed it does not assume the file was built here
from an expression matrix. A bigBarChart is handled by converting it with
bigBedToBed and rebuilding it afterwards, which the usage message spells out.

It refuses to write a half-correct file: the two orders must name the same
bars, every row must hold as many values as there are names, and a .categories
file must have a row for each bar.

Listed in USER_APP_SCRIPTS so it reaches hgdownload, and in the no-argument
branch of mkREADME.sh so the README picks up its usage message rather than the
argparse error that -verbose=2 would produce.

Its tests are also wired into the test target directly. testAll iterates
ALL_APPS, which does not include USER_APP_SCRIPTS, so none of these programs'
tests have ever been reachable from "make test" - that is why the suite sitting
next to expMatrixToBarchartBed never reported it was broken. The makefile
already keeps a short list of directories testAll cannot reach, and this adds
two more to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/utils/barChartReorder/barChartReorder src/utils/barChartReorder/barChartReorder
new file mode 100755
index 00000000000..81e0a095724
--- /dev/null
+++ src/utils/barChartReorder/barChartReorder
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+"""Reorder the bars of a barChart or bigBarChart BED file."""
+import argparse
+import sys
+
+def parseArgs():
+    """Parse the command line arguments."""
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+The bars of a barChart track are drawn in the order their values appear in the
+expScores field, so changing the order means rewriting that field in every row.
+The two order files list the bar names, one per line: the order the BED file is
+in now, and the order you want it in.  Both must name the same bars.
+
+The barChartBars line for the track is printed on stdout.  Remember to put the
+barChartColors setting and the barChartStatsUrl file into the same order.
+
+To reorder a bigBarChart file, convert it to BED first and rebuild it after:
+
+   bigBedToBed input.bb input.bed
+   barChartReorder oldOrder.txt newOrder.txt input.bed reordered.bed
+   bedToBigBed -sort -as=barChartBed.as -type=bed6+5 reordered.bed chrom.sizes output.bb
+
+example:
+   barChartReorder oldOrder.txt newOrder.txt input.bed reordered.bed \\
+       --categories old.categories --outCategories new.categories
+""")
+    parser.add_argument("oldOrder",
+        help="file of bar names, one per line, in the order they appear in expScores now")
+    parser.add_argument("newOrder",
+        help="file of the same bar names, in the order you want them drawn")
+    parser.add_argument("inBed",
+        help="barChart BED file to read, bed6+3 or bed6+5")
+    parser.add_argument("outBed",
+        help="reordered BED file to write")
+    parser.add_argument("--categories",
+        help="a .categories file (barChartCategoryUrl) to put into the new order")
+    parser.add_argument("--outCategories",
+        help="where to write the reordered .categories file, default is to overwrite")
+    if len(sys.argv) == 1:
+        parser.print_help()
+        exit(1)
+    return parser.parse_args()
+
+def readNames(fileName):
+    """Read a file of names, one per line, ignoring blank lines."""
+    return [line.strip() for line in open(fileName) if line.strip()]
+
+def reorderBed(inBed, outBed, old, new, order):
+    """Write outBed with the expScores field of every row put into the new order."""
+    with open(inBed) as inf, open(outBed, "w") as outf:
+        for lineNumber, line in enumerate(inf, 1):
+            if line.startswith("#"):
+                outf.write(line.replace(" ".join(old), " ".join(new)))
+                continue
+            fields = line.rstrip("\n").split("\t")
+            if len(fields) < 9:
+                sys.exit("error: line %d has %d fields, expected at least 9"
+                         % (lineNumber, len(fields)))
+            scores = fields[8].rstrip(",").split(",")
+            if len(scores) != len(old):
+                sys.exit("error: line %d has %d values but %d bar names were given"
+                         % (lineNumber, len(scores), len(old)))
+            fields[8] = ",".join(scores[i] for i in order)
+            outf.write("\t".join(fields) + "\n")
+
+def reorderCategories(inFile, outFile, new):
+    """Rewrite a .categories file so its rows follow the new bar order."""
+    rows = dict()
+    for line in open(inFile):
+        if line.strip():
+            rows[line.split("\t")[0]] = line
+    missing = [name for name in new if name not in rows]
+    if missing:
+        sys.exit("error: no row in %s for: %s" % (inFile, ", ".join(missing)))
+    with open(outFile, "w") as outf:
+        for name in new:
+            outf.write(rows[name])
+
+def main():
+    args = parseArgs()
+    old = readNames(args.oldOrder)
+    new = readNames(args.newOrder)
+    if sorted(old) != sorted(new):
+        sys.exit("error: %s and %s must list the same bar names"
+                 % (args.oldOrder, args.newOrder))
+    order = [old.index(name) for name in new]
+    reorderBed(args.inBed, args.outBed, old, new, order)
+    if args.categories:
+        reorderCategories(args.categories, args.outCategories or args.categories, new)
+    print("barChartBars " + " ".join(new))
+
+main()