gramene-search 2.39.1 → 2.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ var $gXNCa$gramenetreesclientsrctaxonomy = require("gramene-trees-client/src/tax
12
12
  var $gXNCa$gramenetaxonomywithgenomes = require("gramene-taxonomy-with-genomes");
13
13
  var $gXNCa$reactga4 = require("react-ga4");
14
14
  var $gXNCa$moneyclip = require("money-clip");
15
+ var $gXNCa$tbrowsecore = require("tbrowse-core");
15
16
  var $gXNCa$reactbootstrap = require("react-bootstrap");
16
17
  var $gXNCa$reactswitch = require("react-switch");
17
18
  var $gXNCa$reacticonsio5 = require("react-icons/io5");
@@ -6627,6 +6628,7 @@ var $7f865ea0feda21af$export$2e2bcd8739ae039 = $7f865ea0feda21af$var$uiViewState
6627
6628
  // `viewState` is null until the view has been interacted with (or restored);
6628
6629
  // the container falls back to its built-in default and computes initial pixel
6629
6630
  // widths in that case.
6631
+
6630
6632
  const $6048209b532f201d$var$taxTreeView = {
6631
6633
  name: 'taxTreeView',
6632
6634
  getReducer: ()=>{
@@ -6651,12 +6653,18 @@ const $6048209b532f201d$var$taxTreeView = {
6651
6653
  type: 'TAXTREE_VIEW_SET',
6652
6654
  payload: patch
6653
6655
  }),
6654
- // Re-apply persisted state from a saved-view snapshot.
6656
+ // Re-apply persisted state from a saved-view snapshot. The tbrowse ViewState
6657
+ // is run through tbrowse-core's tolerant deserialiser so a blob saved by an
6658
+ // older viewer (missing arrays / fields added since) still restores.
6655
6659
  doApplyTaxTreeSnapshot: (snap)=>({ dispatch: dispatch })=>{
6656
6660
  if (!snap || typeof snap !== 'object') return;
6661
+ const payload = snap.viewState && typeof snap.viewState === 'object' ? {
6662
+ ...snap,
6663
+ viewState: (0, $gXNCa$tbrowsecore.normalizeViewState)(snap.viewState)
6664
+ } : snap;
6657
6665
  dispatch({
6658
6666
  type: 'TAXTREE_VIEW_SET',
6659
- payload: snap
6667
+ payload: payload
6660
6668
  });
6661
6669
  },
6662
6670
  selectTaxTreeView: (state)=>state.taxTreeView
@@ -6664,6 +6672,7 @@ const $6048209b532f201d$var$taxTreeView = {
6664
6672
  var $6048209b532f201d$export$2e2bcd8739ae039 = $6048209b532f201d$var$taxTreeView;
6665
6673
 
6666
6674
 
6675
+
6667
6676
  // Serializer/deserializer for shareable gene-search views.
6668
6677
  //
6669
6678
  // `selectViewSnapshot` produces a versioned JSON-safe blob from the current
@@ -6987,10 +6996,7 @@ function $472fe3745b238881$var$buildSnapshot(state) {
6987
6996
  collapseEmpties: tv.collapseEmpties !== false,
6988
6997
  comparaOnly: tv.comparaOnly !== false,
6989
6998
  ...hasVS ? {
6990
- viewState: {
6991
- ...tv.viewState,
6992
- prunedNodeIds: []
6993
- }
6999
+ viewState: (0, $gXNCa$tbrowsecore.stripPrunesForSnapshot)(tv.viewState)
6994
7000
  } : {}
6995
7001
  };
6996
7002
  }
@@ -8593,6 +8599,7 @@ var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
8593
8599
 
8594
8600
 
8595
8601
 
8602
+
8596
8603
  const $5c2c79352d3d7b81$export$ec6b54b688be1708 = ({ fullscreen: fullscreen, onExitFullscreen: onExitFullscreen, title: title, className: className, children: children })=>{
8597
8604
  const [stableNode] = (0, $gXNCa$react.useState)(()=>typeof document !== 'undefined' ? document.createElement('div') : null);
8598
8605
  const inlineRef = (0, $gXNCa$react.useRef)(null);
@@ -8776,33 +8783,28 @@ const $5c2c79352d3d7b81$export$b2e089eb3692b073 = (props)=>/*#__PURE__*/ (0, $gX
8776
8783
 
8777
8784
 
8778
8785
 
8786
+
8779
8787
  const $cd8bc494277e92a4$var$ORGAN_CELL_W = 16;
8780
8788
  const $cd8bc494277e92a4$var$MAXTPM_W = 46;
8781
8789
  const $cd8bc494277e92a4$var$STRESS_MIN = 120;
8782
8790
  function $cd8bc494277e92a4$export$965a6b1d7408d496(docs, tree) {
8783
- const nodeOf = {};
8784
- const nodeGene = {}; // leaf nodeId -> gene id, for every leaf (hover readout)
8785
- if (tree && tree.nodes) Object.values(tree.nodes).forEach((n)=>{
8786
- if (n.isLeaf && n.geneId) {
8787
- nodeOf[n.geneId] = n.id;
8788
- nodeGene[n.id] = n.geneId;
8789
- }
8791
+ const hasTree = Boolean(tree && tree.nodes);
8792
+ const nodeGene = hasTree ? (0, $gXNCa$tbrowsecore.geneIdByLeafNodeId)(tree) : {}; // leaf nodeId -> gene id (hover readout)
8793
+ const byGeneId = {};
8794
+ (docs || []).forEach((d)=>{
8795
+ if (d && d.id) byGeneId[d.id] = (0, $4b8473139e48e77e$export$69fb5d19b5db1cfa)(d);
8790
8796
  });
8797
+ // Docs whose gene is not a leaf of this tree are dropped (joinByLeaf's `unmatched`).
8798
+ const byNode = hasTree ? (0, $gXNCa$tbrowsecore.joinByLeaf)(tree, byGeneId).byNode : {};
8791
8799
  const organSet = new Set();
8792
- const byNode = {};
8793
8800
  let tpmMin = Infinity;
8794
8801
  let tpmMax = -Infinity;
8795
- (docs || []).forEach((d)=>{
8796
- if (!d || !d.id) return;
8797
- const nodeId = nodeOf[d.id];
8798
- if (!nodeId) return;
8799
- const attrs = (0, $4b8473139e48e77e$export$69fb5d19b5db1cfa)(d);
8802
+ Object.values(byNode).forEach((attrs)=>{
8800
8803
  Object.keys(attrs.organLevels).forEach((o)=>organSet.add(o));
8801
8804
  if (attrs.maxTpm !== null) {
8802
8805
  if (attrs.maxTpm < tpmMin) tpmMin = attrs.maxTpm;
8803
8806
  if (attrs.maxTpm > tpmMax) tpmMax = attrs.maxTpm;
8804
8807
  }
8805
- byNode[nodeId] = attrs;
8806
8808
  });
8807
8809
  return {
8808
8810
  organs: (0, $4b8473139e48e77e$export$65382efdcdd96f8c)(organSet),
@@ -8814,14 +8816,6 @@ function $cd8bc494277e92a4$export$965a6b1d7408d496(docs, tree) {
8814
8816
  }
8815
8817
  };
8816
8818
  }
8817
- // Shared row-highlight background, matching the other zones so hovering any zone
8818
- // lights up the same row across all of them.
8819
- function $cd8bc494277e92a4$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
8820
- if (isSelected) return 'var(--tbrowse-row-select-bg)';
8821
- if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
8822
- if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
8823
- return 'transparent';
8824
- }
8825
8819
  const $cd8bc494277e92a4$var$gridBorder = '1px solid var(--tbrowse-grid-line, rgba(0,0,0,0.06))';
8826
8820
  const $cd8bc494277e92a4$var$stressWidth = (width, organCount)=>Math.max($cd8bc494277e92a4$var$STRESS_MIN, width - organCount * $cd8bc494277e92a4$var$ORGAN_CELL_W - $cd8bc494277e92a4$var$MAXTPM_W);
8827
8821
  const $cd8bc494277e92a4$var$ExprHeader = ({ zoneState: zoneState, setZoneState: setZoneState, width: width, data: data, hoveredNodeId: hoveredNodeId })=>{
@@ -9016,8 +9010,12 @@ const $cd8bc494277e92a4$var$ExprBody = ({ visibleRows: visibleRows, rowRange: ro
9016
9010
  max: 0
9017
9011
  };
9018
9012
  const sW = $cd8bc494277e92a4$var$stressWidth(width, organs.length);
9019
- const totalHeight = visibleRows.length ? visibleRows[visibleRows.length - 1].y + visibleRows[visibleRows.length - 1].height : 0;
9020
- const rows = visibleRows.slice(rowRange.startIndex, rowRange.endIndex);
9013
+ const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
9014
+ const rows = (0, $gXNCa$tbrowse.rowsWithFlags)(visibleRows, rowRange, {
9015
+ hoveredNodeId: hoveredNodeId,
9016
+ hoveredSubtreeIds: hoveredSubtreeIds,
9017
+ selectedNodeId: selectedNodeId
9018
+ });
9021
9019
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
9022
9020
  style: {
9023
9021
  position: 'relative',
@@ -9025,9 +9023,8 @@ const $cd8bc494277e92a4$var$ExprBody = ({ visibleRows: visibleRows, rowRange: ro
9025
9023
  height: totalHeight,
9026
9024
  overflow: 'hidden'
9027
9025
  },
9028
- children: rows.map((r)=>{
9026
+ children: rows.map(({ row: r, background: background })=>{
9029
9027
  const gene = byNode[r.nodeId];
9030
- const background = $cd8bc494277e92a4$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId)));
9031
9028
  const tpmFrac = gene ? (0, $4b8473139e48e77e$export$fc40d061e6c16645)(gene.maxTpm, tpmRange) : null;
9032
9029
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
9033
9030
  onMouseEnter: ()=>onHoverNode(r.nodeId),
@@ -9131,17 +9128,6 @@ const $047461923b1badda$export$964d88edb00bbcaa = (suggestion)=>{
9131
9128
 
9132
9129
 
9133
9130
 
9134
- const $64fad37f770d2bfe$var$genomeZone = (0, $gXNCa$tbrowse.createGenomeZone)({
9135
- id: 'genome'
9136
- });
9137
- // The genome (gene-structure) zone is gated behind the site-config flag
9138
- // `enable_tbrowse_genome_zone` (defaults to false) — see getTbrowseZones().
9139
- const $64fad37f770d2bfe$var$TBROWSE_BASE_ZONES = [
9140
- (0, $gXNCa$tbrowse.treeZone),
9141
- (0, $gXNCa$tbrowse.labelsZone),
9142
- (0, $gXNCa$tbrowse.msaZone),
9143
- (0, $gXNCa$tbrowse.neighborhoodZone)
9144
- ];
9145
9131
  // A gene's trees: the main Compara tree (homology.gene_tree) and/or trees in
9146
9132
  // clade-specific tree sets, homology.<tree_set> — e.g. a wheat gene may carry
9147
9133
  // gene_tree, homology.wheat = {id: 'EplWheatGT_…'} and homology.barley. Solr
@@ -9174,27 +9160,72 @@ const $64fad37f770d2bfe$var$treeLabel = (ref)=>{
9174
9160
  const set = ref.field === 'gene_tree' ? 'Compara' : ref.field.charAt(0).toUpperCase() + ref.field.slice(1);
9175
9161
  return ref.rootTaxonName ? `${set} \xb7 ${ref.rootTaxonName}` : set;
9176
9162
  };
9163
+ const $64fad37f770d2bfe$var$genomeZone = (0, $gXNCa$tbrowse.createGenomeZone)({
9164
+ id: 'genome'
9165
+ });
9166
+ // The genome (gene-structure) zone is gated behind the site-config flag
9167
+ // `enable_tbrowse_genome_zone` (defaults to false) — see getTbrowseZones().
9168
+ const $64fad37f770d2bfe$var$TBROWSE_BASE_ZONES = [
9169
+ (0, $gXNCa$tbrowse.treeZone),
9170
+ (0, $gXNCa$tbrowse.labelsZone),
9171
+ (0, $gXNCa$tbrowse.msaZone),
9172
+ (0, $gXNCa$tbrowse.neighborhoodZone)
9173
+ ];
9174
+ // The expression-attributes zone's data, as a slot on tbrowse-core's Gramene
9175
+ // gene-tree source: one tree-scoped /search for the expr_*__attr_* fields
9176
+ // (fetchGrameneExprAttrDocs), shaped by buildExprData and delivered through
9177
+ // hostData.exprAttrs — the same lifecycle as the built-in neighborhood and
9178
+ // gene-structure slots. Enabled per site via the ctx flag the component
9179
+ // derives from `enable_tbrowse_expression_zone`. Scoped by root.meta, which
9180
+ // carries the Solr field holding the tree id: gene_tree, or a clade tree set
9181
+ // such as wheat.
9182
+ const $64fad37f770d2bfe$var$exprAttrsSlot = (0, $gXNCa$tbrowsecore.defineSlot)({
9183
+ id: 'exprAttrs',
9184
+ enabled: (ctx)=>ctx.expression === true,
9185
+ run: async ({ ctx: ctx, fetch: fetch, signal: signal, tree: tree, root: root })=>(0, $cd8bc494277e92a4$export$965a6b1d7408d496)(await (0, $gXNCa$tbrowsecore.fetchGrameneExprAttrDocs)(ctx.base, root.meta.treeId, {
9186
+ treeField: root.meta.treeField,
9187
+ fetch: fetch,
9188
+ signal: signal
9189
+ }), tree),
9190
+ toHostData: (v)=>({
9191
+ hostData: {
9192
+ exprAttrs: v
9193
+ }
9194
+ })
9195
+ });
9196
+ const $64fad37f770d2bfe$var$homologySource = (0, $gXNCa$tbrowsecore.extendSource)((0, $gXNCa$tbrowsecore.grameneGeneTreeSource), {
9197
+ id: 'gramene-homology',
9198
+ slots: [
9199
+ $64fad37f770d2bfe$var$exprAttrsSlot
9200
+ ]
9201
+ });
9202
+ // Host zone id → the slot(s) feeding it, so the TBrowse toolbar shows the
9203
+ // per-zone loading / error affordances (TBrowseProps.zoneStatus).
9204
+ const $64fad37f770d2bfe$var$ZONE_SLOTS = {
9205
+ neighborhood: [
9206
+ 'neighborhood'
9207
+ ],
9208
+ genome: [
9209
+ 'geneStructures'
9210
+ ],
9211
+ expression: [
9212
+ 'exprAttrs'
9213
+ ]
9214
+ };
9177
9215
  class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
9178
9216
  constructor(props){
9179
9217
  super(props);
9180
- // Async data caches stay in local state (per mount, per tree). The
9218
+ // The tbrowse data — tree, MSA, domains and junctions adapted from the
9219
+ // stored genetree doc, then the neighborhood / gene-structure / expression
9220
+ // slots — is loaded by a tbrowse-core loader kept on the instance (one per
9221
+ // mount); its snapshot is mirrored into state so renders follow it. The
9181
9222
  // user-controlled view state (viewer toggle, resize height, tbrowse
9182
9223
  // ViewState) is lifted into the uiViewState bundle, keyed by geneId.
9183
- //
9184
- // `*Status` fields track per-zone load lifecycle so the TBrowse
9185
- // toolbar can render loading/error affordances via its zoneStatus
9186
- // prop. They start `undefined` (no signal); flipped to 'loading'
9187
- // when the fetch kicks off and 'ready' / 'error' on settle.
9224
+ this.loader = (0, $gXNCa$tbrowsecore.createLoader)($64fad37f770d2bfe$var$homologySource, {
9225
+ zoneSlots: $64fad37f770d2bfe$var$ZONE_SLOTS
9226
+ });
9188
9227
  this.state = {
9189
- neighborhood: null,
9190
- neighborhoodTreeId: null,
9191
- neighborhoodStatus: undefined,
9192
- geneStructures: null,
9193
- geneStructuresTreeId: null,
9194
- geneStructuresStatus: undefined,
9195
- exprAttrs: null,
9196
- exprAttrsTreeId: null,
9197
- exprAttrsStatus: undefined
9228
+ snap: this.loader.getSnapshot()
9198
9229
  };
9199
9230
  if (!props.geneDocs.hasOwnProperty(props.searchResult.id)) props.requestGene(props.searchResult.id);
9200
9231
  this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(props.grameneTaxonomy));
@@ -9264,248 +9295,95 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9264
9295
  // When the user has limited the search to a subset of genomes
9265
9296
  // (grameneGenomes.active), prune the gene tree to genes from those genomes
9266
9297
  // — mirroring the taxon_id filter api.js applies to the search itself, and
9267
- // the genomesOfInterest TreeVis already honours. Returns the ids of the
9268
- // highest nodes whose entire subtree lies outside the active set (maximal
9269
- // excluded clades), rather than individual leaves: tbrowse then sheds one
9270
- // stub per clade instead of one per leaf, and its single-marker count is
9271
- // meaningful. The gene of interest is always kept so the tree never
9272
- // collapses to nothing. Empty active set means "no limit" → full tree.
9298
+ // the genomesOfInterest TreeVis already honours. tbrowse-core's
9299
+ // maximalExcludedClades returns the ids of the highest nodes whose entire
9300
+ // subtree lies outside the active set (maximal excluded clades), rather
9301
+ // than individual leaves: tbrowse then sheds one stub per clade instead of
9302
+ // one per leaf, and its single-marker count is meaningful. The gene of
9303
+ // interest is always kept so the tree never collapses to nothing. Empty
9304
+ // active set means "no limit" → full tree.
9273
9305
  getGenomePrunedIds() {
9274
9306
  const active = this.props.grameneGenomes && this.props.grameneGenomes.active || {};
9275
9307
  const activeIds = Object.keys(active);
9276
9308
  if (activeIds.length === 0) return [];
9277
- if (!this._tbrowseData) return [];
9309
+ const snap = this.state.snap;
9310
+ if (!snap.tree) return [];
9278
9311
  const activeSet = new Set(activeIds.map(String));
9279
9312
  const keepGeneId = this.gene && this.gene._id;
9280
- const nodes = this._tbrowseData.tree.nodes;
9281
- const children = {};
9282
- Object.values(nodes).forEach((n)=>{
9283
- if (n.parentId != null) (children[n.parentId] = children[n.parentId] || []).push(n.id);
9284
- });
9285
- // hasKept(node): does the subtree contain a leaf we must keep (its taxon
9286
- // is active, or it's the gene of interest)? Memoised post-order walk.
9287
- const memo = {};
9288
- const hasKept = (id)=>{
9289
- if (id in memo) return memo[id];
9290
- const n = nodes[id];
9291
- const v = n.isLeaf ? n.taxonomyId != null && activeSet.has(String(n.taxonomyId)) || n.geneId === keepGeneId : (children[id] || []).some(hasKept);
9292
- memo[id] = v;
9293
- return v;
9294
- };
9295
- // A node is a maximal excluded clade when it keeps nothing but its parent
9296
- // does (or it's the root) — i.e. the highest fully-excluded node on its
9297
- // path. The root always keeps the gene of interest, so it is never pruned.
9298
- return Object.values(nodes).filter((n)=>!hasKept(n.id) && (n.parentId == null || hasKept(n.parentId))).map((n)=>n.id);
9313
+ return (0, $gXNCa$tbrowsecore.maximalExcludedClades)(snap.tree, (leaf)=>leaf.taxonomyId != null && activeSet.has(String(leaf.taxonomyId)) || leaf.geneId === keepGeneId);
9299
9314
  }
9300
9315
  // Seed the bundle with a pivot-computed initial tbrowse view state once the
9301
- // tree data is available and the user is actually looking at the tbrowse
9302
- // viewer. Called from lifecycle (not render) to avoid dispatching mid-render.
9316
+ // loader has the tree and the user is actually looking at the tbrowse
9317
+ // viewer, from lifecycle (not render) to avoid dispatching mid-render. The
9318
+ // dispatch lands synchronously in the per-gene slice, so the slice check is
9319
+ // the dedupe. buildInitialViewState is the same routine the chassis runs in
9320
+ // uncontrolled mode (each zone's fr-share, initial visibility, default
9321
+ // per-zone state); the pivot is computed for THIS gene — the loader is
9322
+ // shared by every gene of the tree, so its own pivot may be another gene's.
9303
9323
  maybeSeedTbrowseViewState() {
9304
9324
  if (this.getViewer() !== 'tbrowse') return;
9305
9325
  if (this.getHomologySlice().tbrowse) return;
9306
- const id = this.getGeneId();
9307
- if (!this.props.geneDocs.hasOwnProperty(id)) return;
9308
- const gene = this.props.geneDocs[id];
9309
- const ref = this.selectedTreeRef(gene);
9310
- if (!ref) return;
9311
- const treeId = ref.id;
9312
- const raw = this.props.grameneTrees[treeId];
9313
- if (!raw || !raw.taxon_id) return;
9314
- const adapted = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
9315
- raw
9316
- ]);
9317
- const pivot = (0, $gXNCa$tbrowse.computePivotState)(adapted.tree, gene._id);
9318
- this.setTbrowseViewState({
9319
- selectedNodeId: null,
9320
- collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
9321
- prunedNodeIds: [],
9322
- swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
9323
- compressedNodeIds: [],
9324
- nodeOfInterestId: pivot ? pivot.targetId : null,
9325
- // Use each zone's intended fr-share + initial visibility from its
9326
- // definition (tree:30 / labels:20 / msa:50 / neighborhood:30 /
9327
- // genome:32) instead of uniform 25 across the board. Gives the MSA
9328
- // a wider initial pane and the tree a narrower one.
9329
- //
9330
- // `defaultVisible ?? true` matches tbrowse's own buildInitialViewState
9331
- // logic — zones default to visible unless their definition opts out
9332
- // (e.g. neighborhood and genome opt out so they only appear once
9333
- // their async data lands; tbrowse's Layout auto-flips them on then).
9334
- zones: this.getTbrowseZones().map((z)=>({
9335
- id: z.id,
9336
- width: z.defaultWidth,
9337
- visible: z.defaultVisible ?? true
9338
- })),
9339
- zoneStates: {},
9340
- search: null
9341
- });
9326
+ if (!this.gene) return;
9327
+ // The live snapshot, not the mirrored one: right after a tree switch the
9328
+ // loader has already restarted but state.snap still holds the old tree.
9329
+ const snap = this.loader.getSnapshot();
9330
+ const ref = this.selectedTreeRef(this.gene);
9331
+ if (!ref || snap.status !== 'ready' || !snap.tree || !snap.root || snap.root.meta.treeId !== ref.id) return;
9332
+ this.setTbrowseViewState((0, $gXNCa$tbrowsecore.applyPivot)((0, $gXNCa$tbrowsecore.buildInitialViewState)(this.getTbrowseZones()), (0, $gXNCa$tbrowsecore.computePivotState)(snap.tree, this.gene._id)));
9342
9333
  }
9343
9334
  componentDidMount() {
9335
+ this._unsubscribe = this.loader.subscribe(()=>this.setState({
9336
+ snap: this.loader.getSnapshot()
9337
+ }));
9338
+ this.maybeStartLoader();
9344
9339
  this.maybeSeedTbrowseViewState();
9345
- this.maybeFetchTbrowseData();
9346
9340
  }
9347
9341
  componentDidUpdate() {
9342
+ this.maybeStartLoader();
9348
9343
  this.maybeSeedTbrowseViewState();
9349
- this.maybeFetchTbrowseData();
9350
9344
  }
9351
- // Kick off the neighborhood + gene-structure fetches from lifecycle,
9352
- // not render. Each fetch internally dedupes on `_*FetchedFor === treeId`
9353
- // so calling on every commit is cheap. Gated on the user actually being
9354
- // on the tbrowse viewer + the tree data being loaded — otherwise we'd
9355
- // pay network cost for genes the user never looks at.
9356
- maybeFetchTbrowseData() {
9345
+ componentWillUnmount() {
9346
+ if (this._unsubscribe) this._unsubscribe();
9347
+ this.loader.dispose();
9348
+ }
9349
+ // Start the loader (or restart it when the gene's tree changes) from
9350
+ // lifecycle, not render. Gated on the user actually being on the tbrowse
9351
+ // viewer + the tree doc being loaded — otherwise we'd pay network cost for
9352
+ // genes the user never looks at. The stored genetree doc is handed over as
9353
+ // `rawGenetree`, so the source makes no /genes or /genetrees request of its
9354
+ // own (docs.js's taxon-name rewrite stays upstream); the neighborhood,
9355
+ // gene-structure and expression slots are the only fetches, and the loader
9356
+ // retries their transient failures with backoff.
9357
+ maybeStartLoader() {
9357
9358
  if (this.getViewer() !== 'tbrowse') return;
9358
9359
  const id = this.getGeneId();
9359
9360
  if (!this.props.geneDocs.hasOwnProperty(id)) return;
9360
9361
  const gene = this.props.geneDocs[id];
9362
+ // The selected tree: gene_tree, or a clade tree set (homology.wheat,
9363
+ // homology.oat, …); none for genes with homologues but no tree. A switch
9364
+ // restarts the loader (new generation, the old slots are aborted).
9361
9365
  const ref = this.selectedTreeRef(gene);
9362
9366
  if (!ref) return;
9363
9367
  const treeId = ref.id;
9364
9368
  const raw = this.props.grameneTrees[treeId];
9365
9369
  if (!raw || !raw.taxon_id) return;
9366
- // Lazily compute the adapted tbrowse data the same way renderTBrowse
9367
- // does, so fetchGeneStructures has the leaf ids without rerunning
9368
- // fromGrameneGenetree on every commit.
9369
- if (this._tbrowseTreeId !== treeId) {
9370
- this._tbrowseTreeId = treeId;
9371
- this._tbrowseData = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
9372
- raw
9373
- ]);
9374
- }
9375
- this.fetchNeighborhood(treeId, ref.field);
9376
- // Only pay for gene-structure data when the genome zone is actually enabled.
9377
- if (this.isGenomeZoneEnabled()) this.fetchGeneStructures(treeId, this._tbrowseData.tree);
9378
- // Same for the per-gene expression attributes zone.
9379
- if (this.isExpressionZoneEnabled()) this.fetchExprAttrs(treeId, this._tbrowseData.tree, ref.field);
9380
- }
9381
- fetchNeighborhood(treeId, field = 'gene_tree') {
9382
- if (this._neighborhoodFetchedFor === treeId) return;
9383
- this._neighborhoodFetchedFor = treeId;
9384
- this.setState({
9385
- neighborhoodStatus: 'loading'
9386
- });
9387
- const api = this.props.grameneAPI;
9388
- const url = new URL(`${api}/search`);
9389
- const fl = 'id,name,gene_tree,gene_idx,region,start,end,strand,biotype,system_name,description';
9390
- url.searchParams.set('fl', field === 'gene_tree' ? fl : `${fl},${field}`);
9391
- // cache=false: as a cached fq the graph query caches its seed inside its own
9392
- // filterCache computeIfAbsent, and Solr's FastLRUCache throws "Recursive
9393
- // update" (HTTP 500) whenever the two keys collide — for some trees, until
9394
- // the seed happens to be cached by another request.
9395
- url.searchParams.set('fq', `{!graph from=compara_neighbors_10 to=compara_idx_multi maxDepth=1 cache=false}${field}:${treeId}`);
9396
- url.searchParams.set('rows', '100000');
9397
- url.searchParams.set('start', '0');
9398
- fetch(url.toString(), {
9399
- headers: {
9400
- Accept: 'application/json'
9401
- }
9402
- }).then((res)=>{
9403
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
9404
- return res.json();
9405
- }).then((json)=>{
9406
- if (this._neighborhoodFetchedFor !== treeId) return;
9407
- // The neighborhood zone colours genes by family from doc.gene_tree; in
9408
- // a clade tree set the family id lives under the set's own field.
9409
- if (field !== 'gene_tree' && json && json.response && Array.isArray(json.response.docs)) json.response.docs.forEach((d)=>{
9410
- if (d[field] && !d.gene_tree) d.gene_tree = d[field];
9411
- });
9412
- this.setState({
9413
- neighborhood: (0, $gXNCa$tbrowse.fromGrameneNeighborhood)(json),
9414
- neighborhoodTreeId: treeId,
9415
- neighborhoodStatus: 'ready'
9416
- });
9417
- }).catch((err)=>{
9418
- console.warn('tbrowse neighborhood fetch failed:', err);
9419
- // Surface the failure via the zoneStatus prop. Reset the
9420
- // dedupe key so a re-mount or tree-change can retry.
9421
- if (this._neighborhoodFetchedFor === treeId) this.setState({
9422
- neighborhoodStatus: 'error'
9423
- });
9424
- this._neighborhoodFetchedFor = null;
9425
- });
9426
- }
9427
- fetchGeneStructures(treeId, tree) {
9428
- if (this._geneStructuresFetchedFor === treeId) return;
9429
- this._geneStructuresFetchedFor = treeId;
9430
- const ids = Object.values(tree.nodes).filter((n)=>n.isLeaf && n.geneId).map((n)=>n.geneId);
9431
- if (ids.length === 0) return;
9432
- this.setState({
9433
- geneStructuresStatus: 'loading'
9434
- });
9435
- const api = this.props.grameneAPI;
9436
- const BATCH_SIZE = 50;
9437
- const batches = [];
9438
- for(let i = 0; i < ids.length; i += BATCH_SIZE)batches.push(ids.slice(i, i + BATCH_SIZE));
9439
- const fetchBatch = (batch)=>{
9440
- const url = new URL(`${api}/genes`);
9441
- url.searchParams.set('idList', batch.join(','));
9442
- url.searchParams.set('rows', '-1');
9443
- return fetch(url.toString(), {
9444
- headers: {
9445
- Accept: 'application/json'
9446
- }
9447
- }).then((res)=>{
9448
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
9449
- return res.json();
9450
- });
9451
- };
9452
- Promise.all(batches.map(fetchBatch)).then((results)=>{
9453
- if (this._geneStructuresFetchedFor !== treeId) return;
9454
- const combined = [].concat(...results.map((r)=>Array.isArray(r) ? r : []));
9455
- this.setState({
9456
- geneStructures: (0, $gXNCa$tbrowse.fromGrameneGeneStructures)(combined),
9457
- geneStructuresTreeId: treeId,
9458
- geneStructuresStatus: 'ready'
9459
- });
9460
- }).catch((err)=>{
9461
- console.warn('tbrowse gene-structures fetch failed:', err);
9462
- if (this._geneStructuresFetchedFor === treeId) this.setState({
9463
- geneStructuresStatus: 'error'
9464
- });
9465
- this._geneStructuresFetchedFor = null;
9466
- });
9467
- }
9468
- // Fetch the expression attributes (expr_*__attr_*) for this tree's genes in a
9469
- // single query: scope by the tree's field and the `expression_attributes` capability
9470
- // so only genes that actually have the data come back (rows high enough to
9471
- // cover the whole tree). Uses /search (not /genes, whose trimmed docs omit the
9472
- // attr fields) with an fl glob so the columns are discovered from the data
9473
- // rather than hard-coded. `tree` is still needed to map gene ids to leaf node
9474
- // ids (see buildExprByNode).
9475
- fetchExprAttrs(treeId, tree, field = 'gene_tree') {
9476
- if (this._exprAttrsFetchedFor === treeId) return;
9477
- this._exprAttrsFetchedFor = treeId;
9478
- this.setState({
9479
- exprAttrsStatus: 'loading'
9480
- });
9481
- const api = this.props.grameneAPI;
9482
- const url = new URL(`${api}/search`);
9483
- url.searchParams.set('q', '*:*');
9484
- url.searchParams.append('fq', `${field}:${treeId}`);
9485
- url.searchParams.append('fq', 'capabilities:expression_attributes');
9486
- url.searchParams.set('fl', 'id,expr_*__attr_*');
9487
- url.searchParams.set('rows', '10000');
9488
- fetch(url.toString(), {
9489
- headers: {
9490
- Accept: 'application/json'
9491
- }
9492
- }).then((res)=>{
9493
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
9494
- return res.json();
9495
- }).then((json)=>{
9496
- if (this._exprAttrsFetchedFor !== treeId) return;
9497
- const docs = json && json.response && Array.isArray(json.response.docs) ? json.response.docs : [];
9498
- this.setState({
9499
- exprAttrs: (0, $cd8bc494277e92a4$export$965a6b1d7408d496)(docs, tree),
9500
- exprAttrsTreeId: treeId,
9501
- exprAttrsStatus: 'ready'
9502
- });
9503
- }).catch((err)=>{
9504
- console.warn('tbrowse expression-attrs fetch failed:', err);
9505
- if (this._exprAttrsFetchedFor === treeId) this.setState({
9506
- exprAttrsStatus: 'error'
9507
- });
9508
- this._exprAttrsFetchedFor = null;
9370
+ const key = `${ref.field}:${treeId}`;
9371
+ if (this._loaderStartedFor === key) return;
9372
+ this._loaderStartedFor = key;
9373
+ this.loader.start({
9374
+ base: this.props.grameneAPI,
9375
+ treeId: treeId,
9376
+ treeField: ref.field,
9377
+ geneId: gene._id,
9378
+ rawGenetree: raw,
9379
+ // Only pay for gene-structure data when the genome zone is enabled; the
9380
+ // request shape (50 ids per /genes call, no fl) is what this tab has
9381
+ // always sent.
9382
+ geneStructures: this.isGenomeZoneEnabled(),
9383
+ geneStructureChunkSize: 50,
9384
+ geneStructureFl: null,
9385
+ // Same for the per-gene expression attributes zone.
9386
+ expression: this.isExpressionZoneEnabled()
9509
9387
  });
9510
9388
  }
9511
9389
  startResize(e) {
@@ -9558,49 +9436,30 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9558
9436
  });
9559
9437
  }
9560
9438
  renderTBrowse() {
9561
- const treeId = this.treeRef.id;
9562
- // Adapted tbrowse data is computed lazily in maybeFetchTbrowseData
9563
- // (called from lifecycle). If the user just flipped to tbrowse and
9564
- // the lifecycle hasn't fired yet, compute it once here without
9565
- // calling any fetches — those will fire from componentDidUpdate.
9566
- if (this._tbrowseTreeId !== treeId) {
9567
- this._tbrowseTreeId = treeId;
9568
- this._tbrowseData = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
9569
- this.props.grameneTrees[treeId]
9570
- ]);
9571
- }
9572
- const neighborhood = this.state.neighborhoodTreeId === treeId ? this.state.neighborhood : undefined;
9573
- const geneStructures = this.state.geneStructuresTreeId === treeId ? this.state.geneStructures : undefined;
9574
- const exprAttrs = this.state.exprAttrsTreeId === treeId ? this.state.exprAttrs : undefined;
9575
- // Per-zone status for the TBrowse toolbar — pulses while a fetch
9576
- // is in flight, turns red on failure. Tracked per-tree so a
9577
- // tree-change resets any stale 'ready'/'error' from the prior gene.
9578
- const zoneStatus = {
9579
- neighborhood: this.state.neighborhoodTreeId === treeId ? this.state.neighborhoodStatus : this.state.neighborhoodStatus === 'loading' ? 'loading' : undefined,
9580
- genome: this.state.geneStructuresTreeId === treeId ? this.state.geneStructuresStatus : this.state.geneStructuresStatus === 'loading' ? 'loading' : undefined,
9581
- expression: this.state.exprAttrsTreeId === treeId ? this.state.exprAttrsStatus : this.state.exprAttrsStatus === 'loading' ? 'loading' : undefined
9582
- };
9583
- // Bundle-driven (controlled) view state. If we're rendering tbrowse before
9584
- // componentDidMount/Update has seeded the bundle slice, skip this turn and
9585
- // let the re-render with the seeded state do the work.
9439
+ const snap = this.state.snap;
9440
+ const hostProps = (0, $gXNCa$tbrowsecore.hostPropsFrom)(snap);
9441
+ // Nothing to draw until the loader has adapted the tree (one tick after
9442
+ // start(), since the stored doc needs no fetch) and the bundle slice has
9443
+ // been seeded by maybeSeedTbrowseViewState (lifecycle, not render).
9586
9444
  const tbrowseVS = this.getHomologySlice().tbrowse;
9587
- if (!tbrowseVS) return null;
9445
+ // Also wait until the snapshot holds the SELECTED tree, so a switch never
9446
+ // draws the previous tree under the new selection.
9447
+ const isSelected = snap.root && this.treeRef && snap.root.meta.treeId === this.treeRef.id;
9448
+ if (!hostProps || !tbrowseVS || !isSelected) // Reserve the pane's height so the card doesn't jump when the tree lands.
9449
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
9450
+ className: "gene-genetree",
9451
+ style: {
9452
+ height: this.getHeight(),
9453
+ width: '100%'
9454
+ },
9455
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Spinner), {})
9456
+ });
9588
9457
  // Layer the genome-limit prune set on top of the user's own prunes for the
9589
- // controlled view state, but persist only the user's prunes (strip the
9590
- // genome-derived ones in onViewStateChange) so the bundle stays free of
9591
- // filter-derived state and re-applies cleanly when the filter changes.
9592
- const genomePrunedIds = this.getGenomePrunedIds();
9593
- const effectiveVS = genomePrunedIds.length ? {
9594
- ...tbrowseVS,
9595
- prunedNodeIds: (0, ($parcel$interopDefault($gXNCa$lodash))).union(tbrowseVS.prunedNodeIds || [], genomePrunedIds)
9596
- } : tbrowseVS;
9597
- const onViewStateChange = genomePrunedIds.length ? (next)=>{
9598
- const genomeSet = new Set(genomePrunedIds);
9599
- this.setTbrowseViewState({
9600
- ...next,
9601
- prunedNodeIds: (next.prunedNodeIds || []).filter((id)=>!genomeSet.has(id))
9602
- });
9603
- } : (next)=>this.setTbrowseViewState(next);
9458
+ // controlled view state, but persist only the user's prunes (the layer
9459
+ // strips the genome-derived ones in onViewStateChange) so the bundle stays
9460
+ // free of filter-derived state and re-applies cleanly when the filter
9461
+ // changes.
9462
+ const pruneLayer = (0, $gXNCa$tbrowsecore.createDerivedPruneLayer)(this.getGenomePrunedIds());
9604
9463
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
9605
9464
  children: [
9606
9465
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
@@ -9610,27 +9469,16 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9610
9469
  width: '100%'
9611
9470
  },
9612
9471
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$tbrowse.TBrowse), {
9613
- tree: this._tbrowseData.tree,
9614
- taxonomy: this._tbrowseData.taxonomy,
9615
- msa: this._tbrowseData.msa,
9616
- geneMetadata: this._tbrowseData.geneMetadata,
9617
- proteinDomains: this._tbrowseData.proteinDomains,
9618
- exonJunctions: this._tbrowseData.exonJunctions,
9619
- neighborhood: neighborhood,
9620
- geneStructures: geneStructures,
9621
- hostData: exprAttrs ? {
9622
- exprAttrs: exprAttrs
9623
- } : undefined,
9472
+ ...hostProps,
9624
9473
  zones: this.getTbrowseZones(),
9625
9474
  nodeOfInterest: this.gene._id,
9626
- viewState: effectiveVS,
9627
- onViewStateChange: onViewStateChange,
9475
+ viewState: pruneLayer.effective(tbrowseVS),
9476
+ onViewStateChange: (next)=>this.setTbrowseViewState(pruneLayer.onChange(next)),
9628
9477
  densityControls: true,
9629
9478
  defaultOpenSections: {
9630
9479
  zones: true,
9631
9480
  search: true
9632
- },
9633
- zoneStatus: zoneStatus
9481
+ }
9634
9482
  })
9635
9483
  }),
9636
9484
  this.renderResizeHandle()
@@ -9862,7 +9710,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9862
9710
  });
9863
9711
  }
9864
9712
  }
9865
- var $64fad37f770d2bfe$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneTaxonomy', 'selectGrameneTrees', 'selectGrameneGenomes', 'selectGrameneAPI', 'selectConfiguration', 'selectCuration', 'selectUiViewState', 'doRequestGrameneTree', 'doSetHomologyViewer', 'doSetHomologyTree', 'doSetHomologyHeight', 'doSetHomologyTbrowseViewState', 'doAcceptGrameneSuggestion', 'doReplaceGrameneFilters', $64fad37f770d2bfe$var$Homology);
9713
+ var $64fad37f770d2bfe$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneTaxonomy', 'selectGrameneTrees', 'selectGrameneGenomes', 'selectGrameneAPI', 'selectConfiguration', 'selectCuration', 'selectUiViewState', 'doRequestGrameneTree', 'doSetHomologyViewer', 'doSetHomologyHeight', 'doSetHomologyTbrowseViewState', 'doSetHomologyTree', 'doAcceptGrameneSuggestion', 'doReplaceGrameneFilters', $64fad37f770d2bfe$var$Homology);
9866
9714
 
9867
9715
 
9868
9716
 
@@ -20522,69 +20370,21 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
20522
20370
 
20523
20371
 
20524
20372
 
20373
+
20525
20374
  // Adapter: gramene taxonomy-with-genomes tree → tbrowse `Tree` + `Taxonomy`.
20526
20375
  //
20527
20376
  // `grameneTaxDist` (from the grameneTaxDist bundle's selectGrameneTaxDist) is
20528
- // the root node of a gramene-taxonomy-with-genomes tree. Each node exposes
20529
- // `node.model` ({ id, name, genome?, rank?, results? }) and `node.children`.
20530
- // Genome leaves carry `model.genome`; internal clade nodes have children.
20531
- //
20532
- // tbrowse wants a flat `Tree { rootId, nodes: { [id]: TreeNode } }` plus a
20533
- // `Taxonomy { [taxonomyId]: { scientificName, commonName, rank } }` that its
20534
- // labels zone reads to render each leaf's species name. We key tbrowse
20535
- // NodeIds off the (unique) NCBI taxon id and set `taxonomyId` to the same id.
20536
- //
20537
- // Branch lengths are uniform (taxonomy carries none); the tree zone is driven
20538
- // in 'cladogram' mode for this view, which ignores `distance` and aligns the
20539
- // genome rows at the right edge regardless.
20540
- /**
20541
- * @param {object} taxDist - root node of the gramene taxDist tree
20542
- * (the object returned by selectGrameneTaxDist).
20543
- * @returns {{ tree: {rootId: string, nodes: object}, taxonomy: object,
20544
- * leafTaxonIds: string[] } | null}
20545
- */ function $dc4f9c952681c07f$export$5fb520c9ccc6bdef(taxDist) {
20377
+ // the root node of a gramene-taxonomy-with-genomes (tree-model) tree. The
20378
+ // conversion itself — node ids = String(NCBI taxon id), uniform branch lengths
20379
+ // (the tree zone runs in 'cladogram' mode for this view), `model.name` exposed
20380
+ // as both scientific and common name, duplicate-id guard — is
20381
+ // tbrowse-core's `fromTreeModel`, which was lifted from this file.
20382
+
20383
+ function $dc4f9c952681c07f$export$5fb520c9ccc6bdef(taxDist) {
20546
20384
  if (!taxDist || !taxDist.model) return null;
20547
- const nodes = {};
20548
- const taxonomy = {};
20549
- const leafTaxonIds = [];
20550
- const rootId = String(taxDist.model.id);
20551
- const walk = (node, parentId)=>{
20552
- if (!node || !node.model) return;
20553
- const id = String(node.model.id);
20554
- // Guard against the (shouldn't-happen) cyclic / duplicate id case so a
20555
- // malformed taxonomy can't spin the recursion.
20556
- if (nodes[id]) return;
20557
- const kids = node.children || [];
20558
- const isLeaf = kids.length === 0;
20559
- nodes[id] = {
20560
- id: id,
20561
- parentId: parentId,
20562
- distance: parentId === null ? 0 : 1,
20563
- isLeaf: isLeaf,
20564
- taxonomyId: id
20565
- };
20566
- // `model.name` is the NCBI scientific name for clades and is overwritten
20567
- // with the genome's display_name for genome leaves upstream in
20568
- // selectGrameneTaxDist. Expose it as both names so the labels zone shows
20569
- // something whichever field the user has active.
20570
- const name = node.model.name;
20571
- taxonomy[id] = {
20572
- scientificName: name,
20573
- commonName: name,
20574
- rank: node.model.rank
20575
- };
20576
- if (isLeaf) leafTaxonIds.push(id);
20577
- for (const k of kids)walk(k, id);
20578
- };
20579
- walk(taxDist, null);
20580
- return {
20581
- tree: {
20582
- rootId: rootId,
20583
- nodes: nodes
20584
- },
20585
- taxonomy: taxonomy,
20586
- leafTaxonIds: leafTaxonIds
20587
- };
20385
+ return (0, $gXNCa$tbrowsecore.fromTreeModel)(taxDist, {
20386
+ taxonomyIdAs: 'string'
20387
+ });
20588
20388
  }
20589
20389
 
20590
20390
 
@@ -20611,6 +20411,7 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
20611
20411
 
20612
20412
 
20613
20413
 
20414
+
20614
20415
  // ── colour model (ported from gramene-search-vis util/colors.js) ──────────
20615
20416
  // schemeCategory10, kept as RGB triples so we need no d3 dependency here.
20616
20417
  const $bfd29d54a0f0e853$var$REGION_RGB = [
@@ -21138,12 +20939,6 @@ const $bfd29d54a0f0e853$var$BinsHeader = ({ data: data, zoneState: zoneState, se
21138
20939
  // lets the row-highlight background (rendered in the layer beneath the canvas)
21139
20940
  // show as bands above/below the strip on hover/select.
21140
20941
  const $bfd29d54a0f0e853$var$ROW_PAD_Y = 2;
21141
- function $bfd29d54a0f0e853$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
21142
- if (isSelected) return 'var(--tbrowse-row-select-bg)';
21143
- if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
21144
- if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
21145
- return 'transparent';
21146
- }
21147
20942
  const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
21148
20943
  const { visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, zoneState: zoneState, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode } = props;
21149
20944
  const canvasRef = (0, $gXNCa$react.useRef)(null);
@@ -21157,7 +20952,7 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
21157
20952
  const snap = (0, $gXNCa$react.useSyncExternalStore)(store ? store.subscribe : $bfd29d54a0f0e853$var$NOOP_SUB, store ? store.getState : $bfd29d54a0f0e853$var$NOOP_GET);
21158
20953
  const transform = snap.transform || $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM;
21159
20954
  const mode = zoneState && zoneState.mode || 'select';
21160
- const totalHeight = visibleRows.length ? visibleRows[visibleRows.length - 1].y + visibleRows[visibleRows.length - 1].height : 0;
20955
+ const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
21161
20956
  const bins = data.hostData && data.hostData.bins;
21162
20957
  const rows = visibleRows.slice(rowRange.startIndex, rowRange.endIndex);
21163
20958
  const w = Math.max(1, Math.floor(width));
@@ -21410,7 +21205,11 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
21410
21205
  left: 0,
21411
21206
  right: 0,
21412
21207
  height: r.height,
21413
- background: $bfd29d54a0f0e853$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId))),
21208
+ background: (0, $gXNCa$tbrowse.rowHighlightBackground)((0, $gXNCa$tbrowsecore.rowFlags)(r, {
21209
+ hoveredNodeId: hoveredNodeId,
21210
+ hoveredSubtreeIds: hoveredSubtreeIds,
21211
+ selectedNodeId: selectedNodeId
21212
+ })),
21414
21213
  cursor: rowCursor,
21415
21214
  opacity: r.opacity ?? 1
21416
21215
  }
@@ -21450,37 +21249,12 @@ const $bfd29d54a0f0e853$export$d85d069f2280460c = {
21450
21249
  // Host-defined tbrowse zone: a narrow, right-justified text column showing the
21451
21250
  // number of matching genes per leaf (genome) — or, for a collapsed clade, the
21452
21251
  // summed count over its subtree. Counts come from the taxon_id facet; the
21453
- // per-node sums are precomputed by computeGeneCounts and handed in via
21454
- // hostData.geneCounts = { [nodeId]: number }.
21252
+ // per-node sums are computed by tbrowse-core's `countsFromFacet` in the
21253
+ // container and handed in via hostData.geneCounts = { [nodeId]: number }.
21254
+
21455
21255
 
21456
21256
 
21457
21257
 
21458
- function $15275c4f5939e11d$export$ce87f9041549c2e9(tree, taxonIdFacet) {
21459
- if (!tree || !tree.nodes) return {};
21460
- const leafCount = {};
21461
- if (Array.isArray(taxonIdFacet)) for(let i = 0; i < taxonIdFacet.length; i += 2)leafCount[String(taxonIdFacet[i])] = +taxonIdFacet[i + 1];
21462
- const childrenOf = {};
21463
- for (const id of Object.keys(tree.nodes)){
21464
- const p = tree.nodes[id].parentId;
21465
- if (p !== null && p !== undefined) (childrenOf[p] = childrenOf[p] || []).push(id);
21466
- }
21467
- const out = {};
21468
- const visit = (id)=>{
21469
- const node = tree.nodes[id];
21470
- if (!node) return 0;
21471
- if (node.isLeaf) {
21472
- const c = leafCount[id] || 0;
21473
- out[id] = c;
21474
- return c;
21475
- }
21476
- let sum = 0;
21477
- for (const k of childrenOf[id] || [])sum += visit(k);
21478
- out[id] = sum;
21479
- return sum;
21480
- };
21481
- visit(tree.rootId);
21482
- return out;
21483
- }
21484
21258
  const $15275c4f5939e11d$var$CountsHeader = ({ zoneState: zoneState, setZoneState: setZoneState })=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21485
21259
  style: {
21486
21260
  padding: '0 10px',
@@ -21498,28 +21272,24 @@ const $15275c4f5939e11d$var$CountsHeader = ({ zoneState: zoneState, setZoneState
21498
21272
  }))
21499
21273
  })
21500
21274
  });
21501
- // Shared row-highlight background, matching the labels zone so hovering any
21502
- // zone lights up the same row across all of them (state flows through the
21503
- // tbrowse store's hoveredNodeId).
21504
- function $15275c4f5939e11d$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
21505
- if (isSelected) return 'var(--tbrowse-row-select-bg)';
21506
- if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
21507
- if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
21508
- return 'transparent';
21509
- }
21510
21275
  const $15275c4f5939e11d$var$CountsBody = ({ visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode })=>{
21511
- const totalHeight = visibleRows.length ? visibleRows[visibleRows.length - 1].y + visibleRows[visibleRows.length - 1].height : 0;
21276
+ const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
21512
21277
  const counts = data.hostData && data.hostData.geneCounts || {};
21513
- const rows = visibleRows.slice(rowRange.startIndex, rowRange.endIndex);
21278
+ // Rows in the virtualised range with the shared hover/select background, so
21279
+ // hovering any zone lights up the same row across all of them.
21280
+ const rows = (0, $gXNCa$tbrowse.rowsWithFlags)(visibleRows, rowRange, {
21281
+ hoveredNodeId: hoveredNodeId,
21282
+ hoveredSubtreeIds: hoveredSubtreeIds,
21283
+ selectedNodeId: selectedNodeId
21284
+ });
21514
21285
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21515
21286
  style: {
21516
21287
  position: 'relative',
21517
21288
  width: width,
21518
21289
  height: totalHeight
21519
21290
  },
21520
- children: rows.map((r)=>{
21291
+ children: rows.map(({ row: r, background: background })=>{
21521
21292
  const n = counts[r.nodeId];
21522
- const background = $15275c4f5939e11d$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId)));
21523
21293
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21524
21294
  onMouseEnter: ()=>onHoverNode(r.nodeId),
21525
21295
  onMouseLeave: ()=>onHoverNode(null),
@@ -21589,56 +21359,37 @@ function $15504f5eba8e73bd$var$binIdxsToSolr(idxs) {
21589
21359
  }
21590
21360
  return parts.join(' OR ');
21591
21361
  }
21362
+ // The initial controlled ViewState: tbrowse-core's buildInitialViewState over
21363
+ // this view's zone list with the view's own fr-shares, and — passed through
21364
+ // `overrides` so they replace the seeded defaults verbatim — the per-zone states
21365
+ // exactly as this view has always persisted them. The tree zone tolerates a
21366
+ // partial state (reads with `?? default`); the labels zone reads `visibleFields`
21367
+ // directly, so it must be present. Drive the tree as a cladogram (taxonomy has
21368
+ // no branch lengths) and show the genome/species name in the labels column.
21592
21369
  function $15504f5eba8e73bd$var$baseViewState() {
21593
- return {
21594
- selectedNodeId: null,
21595
- collapsedNodeIds: [],
21596
- prunedNodeIds: [],
21597
- swappedNodeIds: [],
21598
- compressedNodeIds: [],
21599
- nodeOfInterestId: null,
21600
- zones: [
21601
- {
21602
- id: 'tree',
21603
- width: 30,
21604
- visible: true
21605
- },
21606
- {
21607
- id: 'labels',
21608
- width: 22,
21609
- visible: true
21610
- },
21611
- {
21612
- id: 'counts',
21613
- width: 10,
21614
- visible: true
21615
- },
21616
- {
21617
- id: 'bins',
21618
- width: 60,
21619
- visible: true
21620
- }
21621
- ],
21622
- // Controlled mode supplies zoneStates wholesale, so each must carry the
21623
- // fields its zone reads. The tree zone tolerates a partial state (reads
21624
- // with `?? default`); the labels zone reads `visibleFields` directly, so
21625
- // it must be present. Drive the tree as a cladogram (taxonomy has no
21626
- // branch lengths) and show the genome/species name in the labels column.
21627
- zoneStates: {
21628
- tree: {
21629
- layoutMode: 'cladogram',
21630
- showLeafExtensions: false
21631
- },
21632
- labels: {
21633
- visibleFields: [
21634
- 'taxonomy.commonName'
21635
- ]
21636
- },
21637
- counts: {},
21638
- bins: {}
21370
+ return (0, $gXNCa$tbrowsecore.buildInitialViewState)($15504f5eba8e73bd$var$ZONES, {
21371
+ widths: {
21372
+ tree: 30,
21373
+ labels: 22,
21374
+ counts: 10,
21375
+ bins: 60
21639
21376
  },
21640
- search: null
21641
- };
21377
+ overrides: {
21378
+ zoneStates: {
21379
+ tree: {
21380
+ layoutMode: 'cladogram',
21381
+ showLeafExtensions: false
21382
+ },
21383
+ labels: {
21384
+ visibleFields: [
21385
+ 'taxonomy.commonName'
21386
+ ]
21387
+ },
21388
+ counts: {},
21389
+ bins: {}
21390
+ }
21391
+ }
21392
+ });
21642
21393
  }
21643
21394
  // The set of taxa to SHOW, mirroring the legacy TaxDist.js logic.
21644
21395
  function $15504f5eba8e73bd$var$computeSelectedTaxa({ grameneSearch: grameneSearch, grameneGenomes: grameneGenomes, grameneMaps: grameneMaps }, treeNodes, collapseEmpties, comparaOnly, showCompara) {
@@ -21702,7 +21453,7 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21702
21453
  const facet = props.grameneSearch && props.grameneSearch.facet_counts && props.grameneSearch.facet_counts.facet_fields && props.grameneSearch.facet_counts.facet_fields.taxon_id;
21703
21454
  return {
21704
21455
  bins: (0, $bfd29d54a0f0e853$export$390e10a52782f018)(grameneTaxDist),
21705
- geneCounts: adapter ? (0, $15275c4f5939e11d$export$ce87f9041549c2e9)(adapter.tree, facet) : null,
21456
+ geneCounts: adapter ? (0, $gXNCa$tbrowsecore.countsFromFacet)(adapter.tree, facet, 'nodeId') : null,
21706
21457
  binsUI: binsUiRef.current,
21707
21458
  onApplyBinsFilter: onApplyBinsFilter
21708
21459
  };
@@ -21728,7 +21479,7 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21728
21479
  const prunedNodeIds = (0, $gXNCa$react.useMemo)(()=>{
21729
21480
  if (!adapter) return [];
21730
21481
  const selected = $15504f5eba8e73bd$var$computeSelectedTaxa(props, adapter.tree.nodes, collapseEmpties, comparaOnly, showCompara);
21731
- return adapter.leafTaxonIds.filter((id)=>!selected[id]);
21482
+ return (0, $gXNCa$tbrowsecore.leavesFailing)(adapter.tree, (leaf)=>Boolean(selected[String(leaf.taxonomyId)]));
21732
21483
  // eslint-disable-next-line react-hooks/exhaustive-deps
21733
21484
  }, [
21734
21485
  adapter,
@@ -21740,7 +21491,9 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21740
21491
  props.grameneMaps
21741
21492
  ]);
21742
21493
  // Effective (controlled) view state = user interactions + control-driven
21743
- // pruning. Memoised so the reference is stable across unrelated renders.
21494
+ // pruning. The derived set REPLACES prunedNodeIds (this view has no user
21495
+ // prunes of its own; the snapshot strips the field for the same reason).
21496
+ // Memoised so the reference is stable across unrelated renders.
21744
21497
  const effectiveViewState = (0, $gXNCa$react.useMemo)(()=>({
21745
21498
  ...viewState,
21746
21499
  prunedNodeIds: prunedNodeIds
@@ -21761,20 +21514,18 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21761
21514
  const el = wrapperRef.current;
21762
21515
  if (!el || el.clientWidth <= 0 || window.innerWidth < 800) return;
21763
21516
  sizedRef.current = true;
21764
- const TARGET = {
21517
+ const widths = (0, $gXNCa$tbrowsecore.pixelWidthsToFr)({
21765
21518
  tree: 270,
21766
21519
  labels: 200,
21767
21520
  counts: 64
21768
- };
21769
- const used = TARGET.tree + TARGET.labels + TARGET.counts;
21770
- const binsW = Math.max(160, el.clientWidth - used);
21521
+ }, el.clientWidth, 'bins', 160);
21771
21522
  const base = $15504f5eba8e73bd$var$baseViewState();
21772
21523
  setTaxTreeView({
21773
21524
  viewState: {
21774
21525
  ...base,
21775
21526
  zones: base.zones.map((z)=>({
21776
21527
  ...z,
21777
- width: z.id === 'bins' ? binsW : TARGET[z.id] ?? z.width
21528
+ width: widths[z.id] ?? z.width
21778
21529
  }))
21779
21530
  }
21780
21531
  });