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) 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);