d0632693bee08bf61b7990e0c6a1de8c050e337f mspeir Sat Aug 1 20:53:09 2026 -0700 singleCellSignalsPeaks: color by cell class, harmonize labels and facets (hg38) Overhaul of the hg38 track and the shared build scripts it and the mm10 track are generated from: - Color every subtrack by broad cell class from one colorblind-conscious palette (shared with the mm10 track, so a class is the same color on both assemblies); add a color legend to the description page. - Add a "Cell class" facet; the fine cell type becomes a searchable table column. Group subtracks by class via priority; every subtrack is off by default. - Paper-curated cell-type names, redundant-synonym merges, QC-cluster drop, and per-collection tissue/life-stage/condition (including the SEA-AD region and ADNC neuropathology level, from Gabitto 2024 and Hawrylycz 2024). - Rebuild the longLabels from the harmonized cell type + facets, so the cryptic source short labels decode. - Reclassify 10 mislabeled interaction bigBeds out of the signal/peaks composite, retype a narrowPeak-format bigBed, and drop deprecated *.old data (936 -> 925 subtracks). - Archive the curation with the scripts: build_celltype_crosswalks.py and celltype-crosswalks/ (per-collection crosswalks, palette, class map, and the paper-decode source tables). refs #37914 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> diff --git src/hg/makeDb/scripts/singleCellSignalsPeaks/makeSingleCellSignalsPeaksRa.py src/hg/makeDb/scripts/singleCellSignalsPeaks/makeSingleCellSignalsPeaksRa.py index 36f9d32493b..d7f352eb93e 100644 --- src/hg/makeDb/scripts/singleCellSignalsPeaks/makeSingleCellSignalsPeaksRa.py +++ src/hg/makeDb/scripts/singleCellSignalsPeaks/makeSingleCellSignalsPeaksRa.py @@ -1,83 +1,123 @@ #!/usr/bin/env python3 """ -Regenerate human/hg38/singleCellSignalsPeaks.ra from the Cell Browser -all-tracks-hub build (Redmine #37820). +Regenerate the native singleCellSignalsPeaks trackDb .ra for a genome assembly +from the Cell Browser all-tracks-hub build (Redmine #37820 for hg38, #37914 for +mm10). -The native hg38 "singleCellSignalsPeaks" faceted composite is the browser -version of the hub's main hg38 signal-&-peaks composite (cellBrowserHg38). This -script takes that composite's stanzas from the hub build and rewrites them into -a native track: - - the composite is renamed cellBrowserHg38 -> singleCellSignalsPeaks +The native "singleCellSignalsPeaks" faceted composite is the Genome Browser +version of the hub's main signal-&-peaks composite for that assembly +(cellBrowserHg38 for hg38, cellBrowserMm10 for mm10). This script takes that +composite's stanzas from the hub build and rewrites them into a native track: + - the composite is renamed cellBrowser<Asm> -> singleCellSignalsPeaks - each subtrack bigDataUrl is repointed to the local /gbdb copy - subtrack colors / labels / types are carried through unchanged (so the - SEA-AD subclass colors and harmonized cell-type labels come along for free) + harmonized cell-type labels and any per-track colors come along for free) The data files themselves are copied into -/hive/data/genomes/hg38/bed/singleCellSignalsPeaks/<served-relpath> and served -via the /gbdb/hg38/bbi/singleCellSignalsPeaks symlink (see the makeDoc); this -script only (re)writes the trackDb .ra. +/hive/data/genomes/<asm>/bed/singleCellSignalsPeaks/<served-relpath> (see +copySingleCellSignalsPeaksFiles.py) and served via the +/gbdb/<asm>/bbi/singleCellSignalsPeaks symlink; this script only (re)writes the +trackDb .ra. Usage: - makeSingleCellSignalsPeaksRa.py [--stanzas STANZAS] [--out OUT] + makeSingleCellSignalsPeaksRa.py [--assembly hg38|mm10] [--stanzas STANZAS] [--out OUT] """ import re, os, argparse from urllib.parse import urlparse HUB_BUILD = "/hive/users/mspeir/claude/cell-browser/all-tracks-hub-build" -GBDB = "/gbdb/hg38/bbi/singleCellSignalsPeaks" -HUB_COMPOSITE = "cellBrowserHg38" # main hg38 signal-&-peaks faceted composite TRACK = "singleCellSignalsPeaks" GROUP = "regulation" # ATAC-seq signal/peaks live with the ENCODE # regulatory tracks, not under singleCell +ORG = {"hg38": "human", "mm10": "mouse"} # trackDb organism subdir per assembly def main(): ap = argparse.ArgumentParser() - ap.add_argument("--stanzas", default=os.path.join(HUB_BUILD, "stanzas/hg38.trackDb.txt")) - ap.add_argument("--out", default=os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "../../trackDb/human/hg38/singleCellSignalsPeaks.ra")) + ap.add_argument("--assembly", default="hg38", choices=sorted(ORG)) + ap.add_argument("--stanzas") + ap.add_argument("--out") args = ap.parse_args() + asm = args.assembly + hub_composite = "cellBrowser" + asm.capitalize() # cellBrowserHg38 / cellBrowserMm10 + gbdb = "/gbdb/%s/bbi/%s" % (asm, TRACK) + stanzas = args.stanzas or os.path.join(HUB_BUILD, "stanzas/%s.trackDb.txt" % asm) + out = args.out or os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../../trackDb/%s/%s/%s.ra" % (ORG[asm], asm, TRACK)) + header = "\n".join([ "track " + TRACK, "compositeTrack faceted", "group " + GROUP, "visibility hide", "type bigBed 3", - "shortLabel Cell Browser signal & peaks", - "longLabel Signal and peak tracks from UCSC Cell Browser datasets", - "metaDataUrl %s/%s_metadata.tsv" % (GBDB, TRACK), + "shortLabel Single-cell ATAC-seq", + "longLabel Single-cell ATAC-seq Peaks and Signals for UCSC Cell Browser datasets", + "metaDataUrl %s/%s_metadata.tsv" % (gbdb, TRACK), "primaryKey Track", "subtrackUrls Dataset=https://cells.ucsc.edu/?ds=$$", - "defaultSortField Dataset", + "defaultSortField Cell_class", "maxCheckboxes 200", ]) - out = [header] - n = 0 - for s in re.split(r"\n\s*\n", open(args.stanzas).read().strip()): + # class ordering for subtrack priority: palette line order (neurons, glia, + # vascular, immune, ...) so same-class tracks group together in the display, + # with the source (hub/dataset) order preserved within a class. The subtrack's + # broad class is recovered from its color (palette is 1:1 class<->color). + color_rank = {} + _palf = os.path.join(HUB_BUILD, "celltype-crosswalks", "celltype-palette.tsv") + for _i, _l in enumerate(open(_palf)): + _pp = _l.rstrip("\n").split("\t") + if len(_pp) >= 2: + color_rank[_pp[1]] = _i + class_seq = {} # rank -> running counter within that class + + # a source path segment of "old" / "*.old" / "*_old" marks deprecated data + # (e.g. cortex-atac/hub/interact.old/); the hub may keep it, but the native + # track must not carry it. + OLD_SEG = re.compile(r"(^|/)[^/]*(\.old|_old|\bold)($|/)", re.I) + + out_stanzas = [header] + n = skipped_old = 0 + for s in re.split(r"\n\s*\n", open(stanzas).read().strip()): lines = s.splitlines() parent = next((l for l in lines if l.startswith("parent ")), "") - if parent.strip() != "parent " + HUB_COMPOSITE: + if parent.strip() != "parent " + hub_composite: + continue + bdu = next((l for l in lines if l.strip().startswith("bigDataUrl ")), "") + rel_check = urlparse(bdu.split(None, 1)[1].strip()).path if bdu else "" + if OLD_SEG.search(rel_check): + skipped_old += 1 continue n += 1 + # priority groups tracks by broad class (via color), source order within + color = next((l.split(None, 1)[1].strip() for l in lines + if l.strip().startswith("color ")), "") + rank = color_rank.get(color, len(color_rank)) # unknown/uncolored last + seq = class_seq.get(rank, 0); class_seq[rank] = seq + 1 + priority = rank * 100000 + seq newl = [] for l in lines: if l.startswith("track "): - suffix = l.split(None, 1)[1][len(HUB_COMPOSITE) + 1:] + suffix = l.split(None, 1)[1][len(hub_composite) + 1:] newl.append("track %s_%s" % (TRACK, suffix)) - elif l.strip() == "parent " + HUB_COMPOSITE: - newl.append("parent " + TRACK) + elif l.strip() == "parent " + hub_composite: + # "off" so every subtrack is unchecked by default; the user turns + # on individual tracks via the faceted selector + newl.append("parent " + TRACK + " off") + newl.append("priority " + str(priority)) elif l.strip().startswith("bigDataUrl "): rel = urlparse(l.split(None, 1)[1].strip()).path.lstrip("/") - newl.append("bigDataUrl %s/%s" % (GBDB, rel)) + newl.append("bigDataUrl %s/%s" % (gbdb, rel)) else: newl.append(l) - out.append("\n".join(newl)) + out_stanzas.append("\n".join(newl)) - with open(os.path.abspath(args.out), "w") as fh: - fh.write("\n\n".join(out) + "\n") - print("wrote %s: %d subtracks (group=%s)" % (args.out, n, GROUP)) + with open(os.path.abspath(out), "w") as fh: + fh.write("\n\n".join(out_stanzas) + "\n") + print("wrote %s: %d subtracks (assembly=%s, composite=%s, group=%s; skipped %d old-dir)" % ( + out, n, asm, hub_composite, GROUP, skipped_old)) if __name__ == "__main__": main()