790667d0f7ee6965244ed31318aa9459e82fda73
braney
Mon Aug 3 16:50:48 2026 -0700
Docent: add hideKids, and make a named mouseover wait for that item's own tooltip.
hideKids is a container "visibility" meaning hide everything under it, so a child
named alongside it is left the only one drawn. A superTrack needs it: unlike a
composite, its own mode does not reach its children, so each comes up at its own
trackDb visibility and {varsInPubs: show} draws all eight of its members however
much an earlier "hide: all" hid. The expansion skips any child the step names
itself and is sent in a round of its own AFTER the rest, since a subtrack hide
travelling in the same request as its container can be dropped by the cart
(#37953). It works on a composite or view too, where it deselects (_sel=0).
The mouseover fix: tooltips are mouseenter-driven with a 500ms show delay and a
500ms hide grace (hg/js/utils.js addMouseover), so while the cursor glides in it
crosses other items and one of THEIR tooltips is often still on screen when it
arrives. Waiting for "some tooltip is visible" therefore recorded a neighbour's
text -- an Alignment Differences mismatch pinned as the adjacent aligned block's
"identical". That only bit pin:, because the dwell which follows let the right
tooltip replace the wrong one before any shot: -- so a scenario's figures and its
mp4 disagreed with each other, and FAST (a single cursor jump, never entering the
neighbour) read correctly while the full run did not. A named mouseover now waits
for the item's own text, taken from its map box and rendered the way the tooltip
renders it (innerHTML then textContent: the attribute holds markup and undecoded
entities such as ★). Comparison is whitespace-insensitive on a distinctive
prefix. A positional hover has no expected text, so it settles instead.
DOCENT_ROWS=1 now also reports mouseovers -- which map boxes matched the name,
where the cursor went, the tooltip expected from the box and the one that came
up -- and warns when the item's own text never appears. Verified over all ten
named-item mouseovers in the Current Protocols quickLift scenarios: no warnings,
every tooltip correct.
refs #37892
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git src/hg/utils/docent/README.md src/hg/utils/docent/README.md
index 7db333f1cb5..9ee143b9a7c 100644
--- src/hg/utils/docent/README.md
+++ src/hg/utils/docent/README.md
@@ -65,32 +65,33 @@
## Steps
`steps:` is an ordered list. Each item is a bare verb or a one-key map.
| Step | What it does |
|------|--------------|
| `gateway` | Open hgGateway for `db`. |
| `go` | Click **GO** with the default position (no typing). |
| `go: chr7:155799529-155812871` | Go straight to a position in hgTracks. |
| `goShow: BRCA1` | **Demonstrates the position change through the UI** (vs. `go:` which navs there directly): the cursor glides to the **position box** and the term is **typed on screen**, then the page finishes the job — **Search** on hgTracks, the arrow on hgGateway — so one verb covers either page. Takes a **gene name** or a **position** (or anything else the box accepts: HGVS, an accession). Bare string, or map form `goShow: {gene: SHH, shot: source}` / `goShow: {position: "chr7:155,799,529-155,812,871", shot: source}` — quote coords in the map form (unquoted commas split a `{..}` flow map). |
| `goShow: {gene: TP53, pick: "NR_176326"}` | A **gene name** goes through the browser's own **suggestion menu**, the way a user does it: Docent waits out the hgSuggest ajax, then clicks the matching row, so you land on the gene (not the search-results page) and the video shows the dropdown. Default match is the exact gene symbol; `pick:` selects a specific row by a substring of its text when the term is ambiguous (a `pick:` that matches nothing warns and falls back to the plain gene match). If nothing ever matches, the typed term is submitted as-is — that may land on the **search-results page**, which is legal, and a `click:` can take it from there. |
| `hide: all` | Click **Hide all**. |
| `track: {mane: pack}` | Set a track's visibility (`hide/dense/squish/pack/full`). The mouse visibly glides to that track's control dropdown, opens it, and picks the mode (then the state is applied). |
| `track: {dbSnp155Common: pack}` | A subtrack by name: trackDb's view and composite above it come along, so this is the whole "common dbSNP" config. (`dbSnp155` is a *different*, off-by-default subtrack — all variants — not an alias for the composite.) |
| `track: {clinvar: pack, clinvarCnv: hide}` | Composite: the container's mode reaches its subtracks, so name only the deviations. Docent adds the containers above and the `_sel` checkbox from trackDb — see **Track names come from trackDb**. |
+| `track: {varsInPubs: hideKids, pubtator: pack}` | **superTrack: show one member only.** `hideKids` is not a visibility — it means *hide everything under this container*, so the child named alongside it is left the only one drawn. A superTrack needs this because, unlike a composite, its own mode does **not** reach its children: each comes up at its own trackDb visibility, so `{varsInPubs: show}` draws all eight of its members and an earlier `hide: all` does not stick. The expansion skips any child the step names itself and is sent in a round of its own **after** the rest — a subtrack hide travelling in the same request as its container can be dropped by the cart (#37953), so the container goes on first and the hides follow. Works on a composite or view too, where it deselects (`_sel=0`) rather than just hiding. `hideKids` alone (no child named) simply empties the container. |
| `track: {anyTrackName: dense}` | Any track by its **trackDb** name. A name trackDb doesn't have (hub, custom track, quickLift target) is sent literally as `name=mode`. |
-| `mouseover: {track: dbSnp155Common, item: rs28406051}` | Hover a **named** item to raise its tooltip. `item:` matches the item's map box (its `&i=<name>` HREF or its TITLE) and hovers that rectangle's center — so it lands on the **correct row** even when items are stacked, which a bare coordinate can't do. `title: "SHH"` matches the TITLE text only; `value: "..."` matches a JSON `mouseOver` span (wig/dense tracks). |
+| `mouseover: {track: dbSnp155Common, item: rs28406051}` | Hover a **named** item to raise its tooltip. `item:` matches the item's map box (its `&i=<name>` HREF or its TITLE) and hovers that rectangle's center — so it lands on the **correct row** even when items are stacked, which a bare coordinate can't do. It then waits for **that item's own tooltip**, not merely for a tooltip: the browser shows one 500 ms after `mouseenter` and hides it 500 ms after `mouseleave` (`hg/js/utils.js`), so the cursor gliding in crosses other items and one of THEIR tooltips can still be on screen on arrival — which is how an Alignment Differences mismatch got pinned as its neighbour's "identical". The expected text comes from the item's own map box, rendered the way the tooltip renders it (the attribute holds markup and undecoded entities), and `DOCENT_ROWS=1` warns if it never appears. `title: "SHH"` matches the TITLE text only; `value: "..."` matches a JSON `mouseOver` span (wig/dense tracks). |
| `mouseover: {track: mane, at: chr7:155805900}` | Hover by **position** when you don't need a specific item: genomic coord (`at:`), a fraction across the view (`frac: 0.5`), or a raw pixel (`x: 400`); the y is forced to the middle of that track's row (cannot disambiguate stacked items). Optional on any `mouseover`: `hold: 2.5` (seconds to dwell) and `shot: tip_mane` (capture the image **plus** the tooltip in one still). |
| `mouseover: {track: dbSnp155Common, item: rs28406051, pin: true}` | `pin: true` **records** that tooltip (its text + position) so a later `pinShot:` can show several mouseovers open together in one figure. Nothing is added to the recorded page, so the **mp4 is unaffected** (it still shows only the transient native tooltip). Set `pinMouseovers: true` at the top of the file to record every mouseover by default (`pin: false` opts one out). Records accumulate within a view and are cleared on the next nav. |
| `pinShot: all_tips` | Write `<name>.png` with **all recorded (pinned) tooltips open at once**. Rendered on a throwaway page that shares the session (same cart/view) — never on the recorded page — so it never appears in the mp4. Consumes the recorded set (clears it). Place it after the `mouseover` steps whose tooltips you want shown together, before any nav/zoom. |
| `convert: {to: GCA_018466845.2, quicklift: true, hideDefaults: true}` | View→Convert, then **type the target into the page's own "Search for target genome" bar** and click the suggestion (`search:` overrides what is typed, `pick:` disambiguates the menu); the Assembly dropdown is checked afterwards and only opened by hand if the search didn't land there. QuickLift on, **re-checks Hide-defaults** (it reverts when the Assembly menu reloads), Submit. `to:` accepts an accession or a label fragment (`2257.pat`, matched against the dropdown text). |
| `convert: {to: ..., shot: convert_filled}` | `shot:` inside `convert:` captures the Convert page, which no other verb can reach. A bare name is the **filled-in page just before Submit**. The map form names up to three moments: `shot: {opened: a, filled: b, result: c}` — `opened` as the page comes up, `filled` ready to Submit, `result` the conversion-result page (whose coordinate link `open: lift` clicks). These are viewport stills, so they show the page from the top. |
| `hub: https://example.org/hub.txt` | **Quick, silent** attach of a track hub by URL (`hgTracks?hubUrl=...`): connects the hub so its tracks are available at their hub-declared visibility. Follow with `track:` to turn specific ones on. Map form `hub: {url: ..., db: hg38, position: chr7:...}` overrides the db/position (default: current `db` + last position). |
| `addHub: https://example.org/hub.txt` | **Demonstrates the attach through the UI** (for the figure/video): opens My Data → Track Hubs, clicks the **Connected Hubs** tab, types the URL into the box, and clicks **Add Hub** — cursor glides and the URL is typed on screen. Then, on the "Hub Connect Successful" page, it **clicks the `Open:` link for `db`** so the demo ends on the browser with the hub loaded. Map form `addHub: {url: ..., db: hg38, shot: loaded}` sets which assembly to open and captures the still on that tracks view. Use `hub:` instead when you just need the hub attached without showing the steps. |
| `addCustomTrack: <text-or-url>` | **Demonstrates loading a custom track via the UI**: opens My Data → Custom Tracks, types the track data (or a data URL) into the paste box, clicks **Submit**, then clicks through to the browser (**Go to first annotation**). Bare string is the data or URL; map form `addCustomTrack: {data: "track ...\nchr7 ...", db: hg38, goto: first, shot: loaded}` (use `url:` for a data URL, `goto: current` to land on **Return to current position** instead). Data is inserted literally (tabs/newlines preserved). In YAML, a multi-line track uses a block scalar: `addCustomTrack: |` then the indented lines. |
| `addPublicHub: GTEx` | **Demonstrates connecting a PUBLIC hub via the UI**: opens My Data → Track Hubs, the **Public Hubs** tab, types the search terms, clicks **Search Public Hubs**, then clicks **Connect** on the matching hub row, and finally **clicks the `Open:` link for `db`** to land on the browser. Bare string is the search term (also used to match the row). A search usually returns several hubs, so use the map form `addPublicHub: {search: "GTEx", match: "GTEx Analysis Hub", db: hg38, shot: loaded}` to pick the exact hub by a substring of its row text (`match:` defaults to `search:`) and the assembly to open. If no row matches it **won't connect** (warns and stops) rather than pick the wrong hub. |
| `drag: chr7:155,805,900-155,806,950` | Emulates the **Shift+drag-select** gesture: the cursor sweeps across the selection (a visible selection box is drawn) and the browser's own drag-select dialog is raised, then a button is clicked. The argument is one genomic region, `chrom:start-end`; a bare range **zooms**. For any other action, or to pass other keys, put the region under `range:` and quote it — unquoted commas split a `{..}` flow map: `drag: {range: "chr7:155,805,900-155,806,950", shot: dragselect, then: highlight}`. Endpoints that are not genomic coordinates are given as a fraction (`fromFrac:`/`toFrac:`) or raw px (`fromX:`/`toX:`) instead. Optional `track:` picks the row the box is drawn over; default is the top of the image. `shot: dragselect` captures the open dialog. `then:` = `zoom` (default → **Zoom In**) \| `highlight` (→ Single Highlight) \| `cancel` (Escape, view unchanged). (A real button-held drag would just pan, so the dialog is driven directly.) |
| `open: lift` | Click the returned coordinate link → the lifted view. |
| `zoom: out` / `zoom: in` | One zoom step (2×). |
| `shot: source` | Write `<name>.png` **and** pause the video here. On a tracks page the still is the track image (`#imgTbl`), plus any open tooltip/dialog. On any other page (an hgc detail page, an external page a link led to) it is the **viewport only — the top of the page**, never the whole scrolling document. |
Escape hatches for anything the verbs don't cover: `goto: <url>`, `click: <sel>`,
@@ -172,30 +173,37 @@
Names trackDb doesn't know — attached hubs, custom tracks, and everything on a quickLift
target (a hub genome, which is not in the listing at all) — fall back to a literal
`name=mode`, which is all Docent can honestly do for them.
Set `DOCENT_ROWS=1` to log the rows hgTracks actually drew after each step (and the
suggestion menus a `goShow:`/`convert:` search saw). That answers
"why is that subtrack still there" and "why is my `mouseover:` track not shown" directly,
instead of by guessing:
DOCENT_ROWS=1 make BP1
track: clinvar=pack
rows: ruler, mane, clinvarMain, clinvarCnv, clinvarSubLolly
track: clinvarCnv=hide clinvarCnv_sel=0
rows: ruler, mane, clinvarMain, clinvarSubLolly
+It does the same for `mouseover:` — which map boxes matched the name, where the cursor went,
+the tooltip that was expected from the box, and the one that actually came up:
+
+ item "4.3.157828209.157828210" in quickLiftChain: 1 map box(es) match (1 in row),
+ centers x=[288] -> hovering (288,216), expecting tip "mismatch A->C"
+ tip at (288,216): "mismatch A->C"
+
**After a Convert/quickLift** the target's tracks are served from a hub, so their DOM
ids gain a dynamic `hub_<n>_` prefix (e.g. `hub_191568_quickLiftChain`) that changes
every run. `mouseover:` resolves the track by **suffix**, so you still just write the
plain name — e.g. `track: quickLiftChain` (the Alignment Differences track) hits
`hub_191568_quickLiftChain` automatically.
## Where this is headed
Today a step reaches the browser as CGI variables — `hideTracks=1&clinvar=pack&...` —
which is why the verbs have to know each composite's parameter set by heart. Once the
cart can be handed a structured blob (#37838, "Store cart contents as JSON"), a Docent
step can name the state it wants instead of assembling a pile of `<track>_sel=1`
variables, and the shortcut table above mostly goes away. Docent is meant to be an early
consumer of that work.