ba3e92274483227d4969e7f9d89e6cd507e2bfb3
braney
Fri Jul 31 14:46:55 2026 -0700
Docent: drag takes a genomic range instead of from/to endpoints, refs #37892
The usual form is now one region and it zooms:
drag: chr7:155,806,100-155,806,557
For any other action, or to pass shot:/track: as well, the region goes
under range: in the map form:
drag: {range: "chr7:155,806,100-155,806,557", shot: dragselect, then: highlight}
A malformed range is reported at the drag step rather than failing later
in the sweep. from:/to: still parse, so existing scripts keep working,
and the fraction (fromFrac:/toFrac:) and pixel (fromX:/toX:) endpoints are
untouched since neither has a genomic range to express.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git src/hg/utils/docent/docent.js src/hg/utils/docent/docent.js
index 5c27819655b..a2fd5ba8914 100755
--- src/hg/utils/docent/docent.js
+++ src/hg/utils/docent/docent.js
@@ -676,38 +676,50 @@
const p = path.join(STILLDIR, name + '.png');
const clip = await pg2.evaluate(() => {
const im = document.getElementById('imgTbl'); if (!im) return null;
const els = [im, ...document.querySelectorAll('.__pinnedTip')];
let x = Infinity, y = Infinity, x2 = -Infinity, y2 = -Infinity;
for (const o of els) { const r = o.getBoundingClientRect(); x = Math.min(x, r.left); y = Math.min(y, r.top); x2 = Math.max(x2, r.right); y2 = Math.max(y2, r.bottom); }
return { x: Math.max(0, x - 4), y: Math.max(0, y - 4), width: (x2 - x) + 8, height: (y2 - y) + 8 };
});
if (clip) await pg2.screenshot({ path: p, clip });
else await pg2.locator('#imgTbl').screenshot({ path: p });
await ctx2.close();
console.log('SHOT', p, `(pinned: ${pinnedTips.length})`);
pinnedTips.length = 0; // consume the set
}
// Shift+drag across the track image to open the browser's own drag-select dialog
- // ("Zoom In / Single Highlight / ..."), then act on it. Endpoints are given as
- // genomic coords (from:/to:), a fraction across the view (fromFrac:/toFrac:) or a
- // raw pixel (fromX:/toX:). Optional `track:` picks the row the drag runs over (y);
- // default is the middle of the image. `shot:` captures the open dialog (e.g. the
- // Figure 1A drag-select box). `then:` = zoom (default, clicks Zoom In) | highlight
- // (Single Highlight) | cancel (Escape, leaves the view unchanged).
+ // ("Zoom In / Single Highlight / ..."), then act on it. The usual form gives one
+ // genomic region and zooms: drag: chr7:155,806,100-155,806,557
+ // Any other action needs the map form, which is also how you pass shot:/track:
+ // drag: {range: "chr7:155,806,100-155,806,557", then: highlight}
+ // Endpoints that are not genomic coords use a fraction
+ // across the view (fromFrac:/toFrac:) or a raw pixel (fromX:/toX:) instead.
+ // Optional `track:` picks the row the drag runs over (y); default is the middle of
+ // the image. `shot:` captures the open dialog (e.g. the Figure 1A drag-select box).
+ // `then:` = zoom (default, clicks Zoom In) | highlight (Single Highlight) | cancel
+ // (Escape, leaves the view unchanged).
async function drag(o) {
+ // A bare string is the region; `range:` is the same thing with room for other
+ // keys. Both expand to the from:/to: endpoints the rest of this function uses.
+ if (typeof o === 'string') o = { range: o };
o = o || {};
+ if (o.range != null) {
+ const m = String(o.range).match(/^\s*(.+):([\d,]+)\s*-\s*([\d,]+)\s*$/);
+ if (!m) throw new Error(`drag: range "${o.range}" is not chrom:start-end`);
+ o = Object.assign({}, o, { from: `${m[1]}:${m[2]}`, to: `${m[1]}:${m[3]}` });
+ }
const img = await page.locator('img[id^="img_data_"]').first().boundingBox({ timeout: 8000 }).catch(() => null);
const tbl = await page.locator('#imgTbl').first().boundingBox({ timeout: 8000 }).catch(() => null);
if (!img || !tbl) throw new Error('drag: track image not shown (need #imgTbl)');
const coordFrac = at => page.evaluate(a => {
try { const s = hgTracks.winStart, e = hgTracks.winEnd;
const c = +String(a).replace(/.*:/, '').replace(/,/g, '');
return Math.max(0, Math.min(1, (c - s) / (e - s))); } catch (_) { return null; }
}, at);
// The grey side-label strip is baked into the LEFT of every full-width track
// image, so the genomic data area starts insideX px in — fractions/coords map
// across [img.x+insideX, img.x+img.width], not the whole image width.
const insideX = await page.evaluate(() => { try { return hgTracks.insideX || 0; } catch (_) { return 0; } });
const dataLeft = img.x + insideX, dataW = Math.max(1, img.width - insideX);
const endX = async (px, fr, coord) => {
if (px != null) return img.x + Number(px);