36b00fae56bc9de09200fd4fe5cfd07ddb4352d8 braney Wed Sep 16 12:40:10 2026 -0700 hprcPclai: back to dense, with denseClick on, refs #35415, refs #38364 This track was moved to squish a week ago and pinned there with onlyVisibility, for one reason: dense drew one merged row per subtrack and emitted no per-item map boxes at all, so there was no hgc link, no mouseOver, and no way to reach the ancestry scatterplot on the details page. denseClick removes that reason. With it, every item on a dense row gets its own map box, so it carries an hgc link and its own mouseOver while still being drawn on the single dense row. Measured on a build of this branch at chr2:60,000,000-80,000,000, the seven default-on haplotypes: dense, denseClick off 0 hgc links dense, denseClick on 1249 hgc links squish 1235 hgc links The rendered image does not change; only the image map does. So dense is now the better mode for this data as well as the clickable one. Dense is always exactly one row per haplotype, where squish spreads to three or four at whole-chromosome zoom and gets noisy. onlyVisibility moves to dense rather than going away: its real job is keeping a reader out of pack, which at 463 subtracks would be enormous. denseClick is inherited, so the single line on the composite covers every subtrack, and hgTracks ignores a setting it does not understand, so the line is inert rather than harmful on an older browser. The same change goes into the GenArk contrib collection stanza. Worth knowing about the timing there: hprc2annot is listed in betaGenArk.txt, and hgwbeta runs the release branch, so that copy will show a dense row with no clickable items until the hgTracks change reaches beta. The hg38 composite is alpha only and hgwdev CGIs are built from master, so it picks the behavior up as soon as this lands. Neither copy is in publicGenArk.txt, so no RR user sees either state. hprcPclai.ra is generated, so the change is in hprcPclaiMakeTrackDb.py and the .ra was rebuilt from the same index CSV; the regenerated file differs only in those three lines. diff --git src/hg/makeDb/scripts/hprcPclai/hprcPclaiMakeTrackDb.py src/hg/makeDb/scripts/hprcPclai/hprcPclaiMakeTrackDb.py index cdff2037ef4..076b59ca378 100755 --- src/hg/makeDb/scripts/hprcPclai/hprcPclaiMakeTrackDb.py +++ src/hg/makeDb/scripts/hprcPclai/hprcPclaiMakeTrackDb.py @@ -1,136 +1,141 @@ #!/usr/bin/env python3 """Write the trackDb stanzas for the hg38 hprcPclai composite track. One subtrack per HPRC Release 2 haplotype, read from the pcLAI GRCh38-coordinate index CSV, so the track follows whatever the index lists rather than a hardcoded sample list. Subtracks are grouped by sample and haplotype so hgTrackUi draws a sample-by-haplotype matrix instead of a flat list of several hundred checkboxes. detailsScript and metaDataUrl are repeated on every subtrack rather than set once on the composite. Both are read as local settings at the point they are used: bigBedClick.c collects detailsScript.* with trackDbLocalSettingsWildMatch(), and hgTrackUi's file fetch looks the path up on the tdb of the clicked track, so a value on the parent alone would not be seen. hprcPclaiMakeTrackDb.py defaultOnFile holds one "." per line, the subtracks that start out checked; blank lines and # comments are ignored. """ import sys import csv TRACK = "hprcPclai" GBDB = "/gbdb/$D/hprcPclai" DATA_VERSION = "HPRC Release 2, pcLAI v1.1 (GRCh38 coordinates)" # Only four coordinates ever appear in the centroid field across all haplotypes: # the four ancestry clusters pcLAI discretizes to. They would make a good # categorical filter, but each value contains a comma and comma is the separator # filterValues uses, so filtering on them needs a comma-free label field first. DETAILS_SCRIPT = ('{"dataUrl":"pclaiRefPanel.json","exportFields":["pca","centroid"],' '"title":"Position in ancestry space","xLabel":"PC1","yLabel":"PC2"}') def readIndex(fname): """Return [(sample, haplotype)] from the HPRC index CSV, sorted by sample.""" rows = [] with open(fname, encoding="utf-8", newline="") as fh: for row in csv.DictReader(fh): samp = (row["sample_id"] or "").strip() hap = (row["haplotype"] or "").strip() if samp and hap: rows.append((samp, hap)) if not rows: sys.exit("no rows in %s" % fname) return sorted(set(rows)) def readDefaultOn(fname): on = set() with open(fname, encoding="utf-8") as fh: for line in fh: line = line.split("#")[0].strip() if line: on.add(line) return on def main(): if len(sys.argv) != 4: sys.exit(__doc__) idxFname, onFname, outFname = sys.argv[1:4] haps = readIndex(idxFname) wanted = readDefaultOn(onFname) samples = sorted({s for s, _h in haps}) hapVals = sorted({h for _s, h in haps}) out = open(outFname, "w", encoding="utf-8") w = out.write w("# hg38 pcLAI local ancestry, one subtrack per HPRC Release 2 haplotype.\n") w("# Generated by hg/makeDb/scripts/hprcPclai/hprcPclaiMakeTrackDb.py -- do not\n") w("# hand-edit; see hg/makeDb/doc/hg38/hprcPclai.txt.\n\n") w("track %s\n" % TRACK) w("compositeTrack on\n") w("shortLabel pcLAI Ancestry\n") w("longLabel Point cloud local ancestry inference (pcLAI) along HPRC assembly haplotypes\n") w("group hprc\n") w("type bigBed 9 +\n") w("itemRgb on\n") - # squish, not dense, and pinned there. In tvDense hgTracks draws one merged row - # per subtrack and emits no per-item map boxes at all, so the scatterplot on the - # details page and the mouseOver below both become unreachable. squish keeps - # every map box, and since the windows tile without overlapping each haplotype - # still collapses to one or two thin rows instead of pack's ~50. - w("visibility squish\n") - w("onlyVisibility squish\n") + # dense, and pinned there. This track was on squish only because dense drew + # one merged row per subtrack with no per-item map boxes, which left the + # mouseOver below and the details-page scatterplot unreachable. denseClick + # (#38364) gives a dense row one hgc link and one mouseOver per item, so + # dense now does everything squish did and always takes exactly one row per + # haplotype, where squish spreads to three or four at whole-chromosome zoom. + # denseClick is inherited, so the one line covers all 463 subtracks. + # onlyVisibility stays, moved to dense: it is what keeps a reader out of + # pack, which at 463 subtracks would be enormous. + w("denseClick on\n") + w("visibility dense\n") + w("onlyVisibility dense\n") w("priority 30\n") # A whole chromosome holds a few thousand windows per haplotype, well over the # 1000-item default at which pack mode gives up drawing; the block structure # this track exists to show is only visible at that zoom. w("maxItems 40000\n") w("subGroup1 sample Sample %s\n" % " ".join("%s=%s" % (s, s) for s in samples)) w("subGroup2 hap Haplotype %s\n" % " ".join("h%s=%s" % (h, h) for h in hapVals)) w("sortOrder sample=+ hap=+\n") w("dimensions dimensionY=sample dimensionX=hap\n") w("dragAndDrop subTracks\n") w("dataVersion %s\n" % DATA_VERSION) w("\n") # Explicit priorities: without them the subtracks come out in an arbitrary # (in practice reversed) order in the image, and with several hundred rows a # stable sample order is what makes the display readable. nOn = 0 for prio, (samp, hap) in enumerate(haps, start=1): key = "%s.%s" % (samp, hap) sub = "%s%sH%s" % (TRACK, samp.replace("-", ""), hap) on = key in wanted nOn += on w(" track %s\n" % sub) w(" parent %s %s\n" % (TRACK, "on" if on else "off")) w(" subGroups sample=%s hap=h%s\n" % (samp, hap)) w(" priority %d\n" % prio) w(" shortLabel %s\n" % key) w(" longLabel pcLAI ancestry of HPRC assembly %s haplotype %s\n" % (samp, hap)) w(" type bigBed 9 +\n") w(" bigDataUrl %s/%s.bb\n" % (GBDB, key)) w(" itemRgb on\n") w(" mouseOver ${haplotype} ${window}
Window PC1,PC2: ${pca}" "
Ancestry centroid: ${centroid}
Confidence: ${score}\n") w(" detailsScript.scatterPlot.pca %s\n" % DETAILS_SCRIPT) w(" metaDataUrl %s/pclaiRefPanel.json\n" % GBDB) w("\n") out.close() missing = wanted - {"%s.%s" % (s, h) for s, h in haps} if missing: sys.stderr.write("WARNING: default-on not in index: %s\n" % ", ".join(sorted(missing))) sys.stderr.write("%s: %d subtracks, %d samples, %d on by default\n" % (outFname, len(haps), len(samples), nOn)) if __name__ == "__main__": main()