gramene-search 2.39.1 → 2.41.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
  }
@@ -8356,18 +8362,6 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
8356
8362
  studies,
8357
8363
  geneStudyIds
8358
8364
  ]);
8359
- // Without a baseline study the EBI Studies heatmap has nothing to draw (its
8360
- // request fails rather than coming back empty), so the tab says so and
8361
- // points at the differential EBI studies the gene does appear in -- e.g.
8362
- // Medicago truncatula, whose studies are all differential.
8363
- const ebiDifferential = (0, $gXNCa$react.useMemo)(()=>{
8364
- if (!geneStudyIds || !studies) return [];
8365
- const mine = new Set(geneStudyIds);
8366
- return studies.filter((e)=>mine.has(e._id) && $9e29a4f60318db7a$var$isDifferential(e) && !$9e29a4f60318db7a$var$isJgiStudy(e)).sort((a, b)=>$9e29a4f60318db7a$var$studyLabel(a) < $9e29a4f60318db7a$var$studyLabel(b) ? -1 : 1);
8367
- }, [
8368
- studies,
8369
- geneStudyIds
8370
- ]);
8371
8365
  const allJgiKey = JSON.stringify(allJgiStudies.map((e)=>[
8372
8366
  e._id,
8373
8367
  e.name
@@ -8472,32 +8466,21 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
8472
8466
  children: [
8473
8467
  gene.name || gene._id,
8474
8468
  " is not in any baseline expression study, so there is no heatmap here.",
8475
- ebiDifferential.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
8469
+ paralogQuery && atlasExperiment && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
8476
8470
  children: [
8477
8471
  ' ',
8478
- "It has results in ",
8479
- ebiDifferential.length === 1 ? 'a differential study' : `${ebiDifferential.length} differential studies`,
8480
- " at Expression Atlas:",
8481
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("ul", {
8482
- className: "mt-1 mb-0",
8483
- children: ebiDifferential.map((e)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
8484
- children: [
8485
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("a", {
8486
- href: `${$9e29a4f60318db7a$var$EBI_GXA}experiments/${encodeURIComponent(e._id)}/Results?geneQuery=${encodeURIComponent(JSON.stringify([
8487
- {
8488
- value: geneQuery
8489
- }
8490
- ]))}`,
8491
- target: "_blank",
8492
- rel: "noopener noreferrer",
8493
- children: e.description || e._id
8494
- }),
8495
- " (",
8496
- e._id,
8497
- ")"
8498
- ]
8499
- }, e._id))
8500
- })
8472
+ "To explore the differential expression of this gene and its paralogs, use the",
8473
+ ' ',
8474
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
8475
+ variant: "link",
8476
+ size: "sm",
8477
+ className: "p-0 align-baseline",
8478
+ onClick: ()=>setExpression({
8479
+ activeTab: 'paralogs'
8480
+ }),
8481
+ children: "Paralogs tab"
8482
+ }),
8483
+ "."
8501
8484
  ]
8502
8485
  })
8503
8486
  ]
@@ -8593,6 +8576,7 @@ var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
8593
8576
 
8594
8577
 
8595
8578
 
8579
+
8596
8580
  const $5c2c79352d3d7b81$export$ec6b54b688be1708 = ({ fullscreen: fullscreen, onExitFullscreen: onExitFullscreen, title: title, className: className, children: children })=>{
8597
8581
  const [stableNode] = (0, $gXNCa$react.useState)(()=>typeof document !== 'undefined' ? document.createElement('div') : null);
8598
8582
  const inlineRef = (0, $gXNCa$react.useRef)(null);
@@ -8776,33 +8760,28 @@ const $5c2c79352d3d7b81$export$b2e089eb3692b073 = (props)=>/*#__PURE__*/ (0, $gX
8776
8760
 
8777
8761
 
8778
8762
 
8763
+
8779
8764
  const $cd8bc494277e92a4$var$ORGAN_CELL_W = 16;
8780
8765
  const $cd8bc494277e92a4$var$MAXTPM_W = 46;
8781
8766
  const $cd8bc494277e92a4$var$STRESS_MIN = 120;
8782
8767
  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
- }
8768
+ const hasTree = Boolean(tree && tree.nodes);
8769
+ const nodeGene = hasTree ? (0, $gXNCa$tbrowsecore.geneIdByLeafNodeId)(tree) : {}; // leaf nodeId -> gene id (hover readout)
8770
+ const byGeneId = {};
8771
+ (docs || []).forEach((d)=>{
8772
+ if (d && d.id) byGeneId[d.id] = (0, $4b8473139e48e77e$export$69fb5d19b5db1cfa)(d);
8790
8773
  });
8774
+ // Docs whose gene is not a leaf of this tree are dropped (joinByLeaf's `unmatched`).
8775
+ const byNode = hasTree ? (0, $gXNCa$tbrowsecore.joinByLeaf)(tree, byGeneId).byNode : {};
8791
8776
  const organSet = new Set();
8792
- const byNode = {};
8793
8777
  let tpmMin = Infinity;
8794
8778
  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);
8779
+ Object.values(byNode).forEach((attrs)=>{
8800
8780
  Object.keys(attrs.organLevels).forEach((o)=>organSet.add(o));
8801
8781
  if (attrs.maxTpm !== null) {
8802
8782
  if (attrs.maxTpm < tpmMin) tpmMin = attrs.maxTpm;
8803
8783
  if (attrs.maxTpm > tpmMax) tpmMax = attrs.maxTpm;
8804
8784
  }
8805
- byNode[nodeId] = attrs;
8806
8785
  });
8807
8786
  return {
8808
8787
  organs: (0, $4b8473139e48e77e$export$65382efdcdd96f8c)(organSet),
@@ -8814,16 +8793,34 @@ function $cd8bc494277e92a4$export$965a6b1d7408d496(docs, tree) {
8814
8793
  }
8815
8794
  };
8816
8795
  }
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
8796
  const $cd8bc494277e92a4$var$gridBorder = '1px solid var(--tbrowse-grid-line, rgba(0,0,0,0.06))';
8826
8797
  const $cd8bc494277e92a4$var$stressWidth = (width, organCount)=>Math.max($cd8bc494277e92a4$var$STRESS_MIN, width - organCount * $cd8bc494277e92a4$var$ORGAN_CELL_W - $cd8bc494277e92a4$var$MAXTPM_W);
8798
+ // "Expressed only" toggle. Homology applies it: while on (and the zone is
8799
+ // visible) it prunes every branch with no gene that has expression
8800
+ // attributes, and turning it on expands the branches that remain.
8801
+ const $cd8bc494277e92a4$var$ExpressedOnlyToggle = ({ on: on, onChange: onChange })=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("label", {
8802
+ title: "Prune branches without expression data and expand the rest",
8803
+ style: {
8804
+ display: 'inline-flex',
8805
+ alignItems: 'center',
8806
+ gap: 3,
8807
+ fontSize: 10,
8808
+ cursor: 'pointer',
8809
+ whiteSpace: 'nowrap',
8810
+ margin: 0
8811
+ },
8812
+ children: [
8813
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("input", {
8814
+ type: "checkbox",
8815
+ checked: on,
8816
+ onChange: (e)=>onChange(e.target.checked),
8817
+ style: {
8818
+ margin: 0
8819
+ }
8820
+ }),
8821
+ "expressed only"
8822
+ ]
8823
+ });
8827
8824
  const $cd8bc494277e92a4$var$ExprHeader = ({ zoneState: zoneState, setZoneState: setZoneState, width: width, data: data, hoveredNodeId: hoveredNodeId })=>{
8828
8825
  const ea = data.hostData && data.hostData.exprAttrs || {};
8829
8826
  const organs = ea.organs || [];
@@ -8856,6 +8853,13 @@ const $cd8bc494277e92a4$var$ExprHeader = ({ zoneState: zoneState, setZoneState:
8856
8853
  name: next
8857
8854
  }))
8858
8855
  }),
8856
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($cd8bc494277e92a4$var$ExpressedOnlyToggle, {
8857
+ on: !!zoneState?.expressedOnly,
8858
+ onChange: (on)=>setZoneState((s)=>({
8859
+ ...s ?? {},
8860
+ expressedOnly: on
8861
+ }))
8862
+ }),
8859
8863
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
8860
8864
  style: {
8861
8865
  display: 'flex',
@@ -9016,8 +9020,12 @@ const $cd8bc494277e92a4$var$ExprBody = ({ visibleRows: visibleRows, rowRange: ro
9016
9020
  max: 0
9017
9021
  };
9018
9022
  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);
9023
+ const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
9024
+ const rows = (0, $gXNCa$tbrowse.rowsWithFlags)(visibleRows, rowRange, {
9025
+ hoveredNodeId: hoveredNodeId,
9026
+ hoveredSubtreeIds: hoveredSubtreeIds,
9027
+ selectedNodeId: selectedNodeId
9028
+ });
9021
9029
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
9022
9030
  style: {
9023
9031
  position: 'relative',
@@ -9025,9 +9033,8 @@ const $cd8bc494277e92a4$var$ExprBody = ({ visibleRows: visibleRows, rowRange: ro
9025
9033
  height: totalHeight,
9026
9034
  overflow: 'hidden'
9027
9035
  },
9028
- children: rows.map((r)=>{
9036
+ children: rows.map(({ row: r, background: background })=>{
9029
9037
  const gene = byNode[r.nodeId];
9030
- const background = $cd8bc494277e92a4$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId)));
9031
9038
  const tpmFrac = gene ? (0, $4b8473139e48e77e$export$fc40d061e6c16645)(gene.maxTpm, tpmRange) : null;
9032
9039
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
9033
9040
  onMouseEnter: ()=>onHoverNode(r.nodeId),
@@ -9131,17 +9138,6 @@ const $047461923b1badda$export$964d88edb00bbcaa = (suggestion)=>{
9131
9138
 
9132
9139
 
9133
9140
 
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
9141
  // A gene's trees: the main Compara tree (homology.gene_tree) and/or trees in
9146
9142
  // clade-specific tree sets, homology.<tree_set> — e.g. a wheat gene may carry
9147
9143
  // gene_tree, homology.wheat = {id: 'EplWheatGT_…'} and homology.barley. Solr
@@ -9174,27 +9170,72 @@ const $64fad37f770d2bfe$var$treeLabel = (ref)=>{
9174
9170
  const set = ref.field === 'gene_tree' ? 'Compara' : ref.field.charAt(0).toUpperCase() + ref.field.slice(1);
9175
9171
  return ref.rootTaxonName ? `${set} \xb7 ${ref.rootTaxonName}` : set;
9176
9172
  };
9173
+ const $64fad37f770d2bfe$var$genomeZone = (0, $gXNCa$tbrowse.createGenomeZone)({
9174
+ id: 'genome'
9175
+ });
9176
+ // The genome (gene-structure) zone is gated behind the site-config flag
9177
+ // `enable_tbrowse_genome_zone` (defaults to false) — see getTbrowseZones().
9178
+ const $64fad37f770d2bfe$var$TBROWSE_BASE_ZONES = [
9179
+ (0, $gXNCa$tbrowse.treeZone),
9180
+ (0, $gXNCa$tbrowse.labelsZone),
9181
+ (0, $gXNCa$tbrowse.msaZone),
9182
+ (0, $gXNCa$tbrowse.neighborhoodZone)
9183
+ ];
9184
+ // The expression-attributes zone's data, as a slot on tbrowse-core's Gramene
9185
+ // gene-tree source: one tree-scoped /search for the expr_*__attr_* fields
9186
+ // (fetchGrameneExprAttrDocs), shaped by buildExprData and delivered through
9187
+ // hostData.exprAttrs — the same lifecycle as the built-in neighborhood and
9188
+ // gene-structure slots. Enabled per site via the ctx flag the component
9189
+ // derives from `enable_tbrowse_expression_zone`. Scoped by root.meta, which
9190
+ // carries the Solr field holding the tree id: gene_tree, or a clade tree set
9191
+ // such as wheat.
9192
+ const $64fad37f770d2bfe$var$exprAttrsSlot = (0, $gXNCa$tbrowsecore.defineSlot)({
9193
+ id: 'exprAttrs',
9194
+ enabled: (ctx)=>ctx.expression === true,
9195
+ 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, {
9196
+ treeField: root.meta.treeField,
9197
+ fetch: fetch,
9198
+ signal: signal
9199
+ }), tree),
9200
+ toHostData: (v)=>({
9201
+ hostData: {
9202
+ exprAttrs: v
9203
+ }
9204
+ })
9205
+ });
9206
+ const $64fad37f770d2bfe$var$homologySource = (0, $gXNCa$tbrowsecore.extendSource)((0, $gXNCa$tbrowsecore.grameneGeneTreeSource), {
9207
+ id: 'gramene-homology',
9208
+ slots: [
9209
+ $64fad37f770d2bfe$var$exprAttrsSlot
9210
+ ]
9211
+ });
9212
+ // Host zone id → the slot(s) feeding it, so the TBrowse toolbar shows the
9213
+ // per-zone loading / error affordances (TBrowseProps.zoneStatus).
9214
+ const $64fad37f770d2bfe$var$ZONE_SLOTS = {
9215
+ neighborhood: [
9216
+ 'neighborhood'
9217
+ ],
9218
+ genome: [
9219
+ 'geneStructures'
9220
+ ],
9221
+ expression: [
9222
+ 'exprAttrs'
9223
+ ]
9224
+ };
9177
9225
  class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
9178
9226
  constructor(props){
9179
9227
  super(props);
9180
- // Async data caches stay in local state (per mount, per tree). The
9228
+ // The tbrowse data — tree, MSA, domains and junctions adapted from the
9229
+ // stored genetree doc, then the neighborhood / gene-structure / expression
9230
+ // slots — is loaded by a tbrowse-core loader kept on the instance (one per
9231
+ // mount); its snapshot is mirrored into state so renders follow it. The
9181
9232
  // user-controlled view state (viewer toggle, resize height, tbrowse
9182
9233
  // 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.
9234
+ this.loader = (0, $gXNCa$tbrowsecore.createLoader)($64fad37f770d2bfe$var$homologySource, {
9235
+ zoneSlots: $64fad37f770d2bfe$var$ZONE_SLOTS
9236
+ });
9188
9237
  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
9238
+ snap: this.loader.getSnapshot()
9198
9239
  };
9199
9240
  if (!props.geneDocs.hasOwnProperty(props.searchResult.id)) props.requestGene(props.searchResult.id);
9200
9241
  this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(props.grameneTaxonomy));
@@ -9264,248 +9305,119 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9264
9305
  // When the user has limited the search to a subset of genomes
9265
9306
  // (grameneGenomes.active), prune the gene tree to genes from those genomes
9266
9307
  // — 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.
9308
+ // the genomesOfInterest TreeVis already honours. tbrowse-core's
9309
+ // maximalExcludedClades returns the ids of the highest nodes whose entire
9310
+ // subtree lies outside the active set (maximal excluded clades), rather
9311
+ // than individual leaves: tbrowse then sheds one stub per clade instead of
9312
+ // one per leaf, and its single-marker count is meaningful. The gene of
9313
+ // interest is always kept so the tree never collapses to nothing. Empty
9314
+ // active set means "no limit" → full tree.
9273
9315
  getGenomePrunedIds() {
9274
9316
  const active = this.props.grameneGenomes && this.props.grameneGenomes.active || {};
9275
9317
  const activeIds = Object.keys(active);
9276
9318
  if (activeIds.length === 0) return [];
9277
- if (!this._tbrowseData) return [];
9319
+ const snap = this.state.snap;
9320
+ if (!snap.tree) return [];
9278
9321
  const activeSet = new Set(activeIds.map(String));
9279
9322
  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);
9323
+ return (0, $gXNCa$tbrowsecore.maximalExcludedClades)(snap.tree, (leaf)=>leaf.taxonomyId != null && activeSet.has(String(leaf.taxonomyId)) || leaf.geneId === keepGeneId);
9324
+ }
9325
+ // The Expression zone's "expressed only" toggle (zoneStates.expression
9326
+ // .expressedOnly): while it is on and the zone is visible, prune the largest
9327
+ // branches with no leaf carrying expression attributes. The gene of interest
9328
+ // stays, as with the genome prunes, so the root is never pruned.
9329
+ getExpressionPrunedIds(vs, hostProps) {
9330
+ const zs = vs && vs.zoneStates && vs.zoneStates.expression;
9331
+ if (!zs || !zs.expressedOnly) return [];
9332
+ if (!(vs.zones || []).some((z)=>z.id === 'expression' && z.visible)) return [];
9333
+ const ea = hostProps && hostProps.hostData && hostProps.hostData.exprAttrs;
9334
+ if (!ea || !ea.byNode || !hostProps.tree) return [];
9335
+ const keepGeneId = this.gene && this.gene._id;
9336
+ return (0, $gXNCa$tbrowsecore.maximalExcludedClades)(hostProps.tree, (leaf)=>ea.byNode.hasOwnProperty(leaf.id) || leaf.geneId === keepGeneId);
9337
+ }
9338
+ // Turning "expressed only" on also expands every branch, so the expressed
9339
+ // genes left after pruning are all in view. Done here, in the same update
9340
+ // as the toggle, rather than by a second viewer action that would race it.
9341
+ expandOnExpressedOnly(prev, next) {
9342
+ const was = !!(prev && prev.zoneStates && prev.zoneStates.expression && prev.zoneStates.expression.expressedOnly);
9343
+ const is = !!(next && next.zoneStates && next.zoneStates.expression && next.zoneStates.expression.expressedOnly);
9344
+ return !was && is ? {
9345
+ ...next,
9346
+ collapsedNodeIds: []
9347
+ } : next;
9299
9348
  }
9300
9349
  // 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.
9350
+ // loader has the tree and the user is actually looking at the tbrowse
9351
+ // viewer, from lifecycle (not render) to avoid dispatching mid-render. The
9352
+ // dispatch lands synchronously in the per-gene slice, so the slice check is
9353
+ // the dedupe. buildInitialViewState is the same routine the chassis runs in
9354
+ // uncontrolled mode (each zone's fr-share, initial visibility, default
9355
+ // per-zone state); the pivot is computed for THIS gene — the loader is
9356
+ // shared by every gene of the tree, so its own pivot may be another gene's.
9303
9357
  maybeSeedTbrowseViewState() {
9304
9358
  if (this.getViewer() !== 'tbrowse') return;
9305
9359
  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
- });
9360
+ if (!this.gene) return;
9361
+ // The live snapshot, not the mirrored one: right after a tree switch the
9362
+ // loader has already restarted but state.snap still holds the old tree.
9363
+ const snap = this.loader.getSnapshot();
9364
+ const ref = this.selectedTreeRef(this.gene);
9365
+ if (!ref || snap.status !== 'ready' || !snap.tree || !snap.root || snap.root.meta.treeId !== ref.id) return;
9366
+ this.setTbrowseViewState((0, $gXNCa$tbrowsecore.applyPivot)((0, $gXNCa$tbrowsecore.buildInitialViewState)(this.getTbrowseZones()), (0, $gXNCa$tbrowsecore.computePivotState)(snap.tree, this.gene._id)));
9342
9367
  }
9343
9368
  componentDidMount() {
9369
+ this._unsubscribe = this.loader.subscribe(()=>this.setState({
9370
+ snap: this.loader.getSnapshot()
9371
+ }));
9372
+ this.maybeStartLoader();
9344
9373
  this.maybeSeedTbrowseViewState();
9345
- this.maybeFetchTbrowseData();
9346
9374
  }
9347
9375
  componentDidUpdate() {
9376
+ this.maybeStartLoader();
9348
9377
  this.maybeSeedTbrowseViewState();
9349
- this.maybeFetchTbrowseData();
9350
- }
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() {
9378
+ }
9379
+ componentWillUnmount() {
9380
+ if (this._unsubscribe) this._unsubscribe();
9381
+ this.loader.dispose();
9382
+ }
9383
+ // Start the loader (or restart it when the gene's tree changes) from
9384
+ // lifecycle, not render. Gated on the user actually being on the tbrowse
9385
+ // viewer + the tree doc being loaded — otherwise we'd pay network cost for
9386
+ // genes the user never looks at. The stored genetree doc is handed over as
9387
+ // `rawGenetree`, so the source makes no /genes or /genetrees request of its
9388
+ // own (docs.js's taxon-name rewrite stays upstream); the neighborhood,
9389
+ // gene-structure and expression slots are the only fetches, and the loader
9390
+ // retries their transient failures with backoff.
9391
+ maybeStartLoader() {
9357
9392
  if (this.getViewer() !== 'tbrowse') return;
9358
9393
  const id = this.getGeneId();
9359
9394
  if (!this.props.geneDocs.hasOwnProperty(id)) return;
9360
9395
  const gene = this.props.geneDocs[id];
9396
+ // The selected tree: gene_tree, or a clade tree set (homology.wheat,
9397
+ // homology.oat, …); none for genes with homologues but no tree. A switch
9398
+ // restarts the loader (new generation, the old slots are aborted).
9361
9399
  const ref = this.selectedTreeRef(gene);
9362
9400
  if (!ref) return;
9363
9401
  const treeId = ref.id;
9364
9402
  const raw = this.props.grameneTrees[treeId];
9365
9403
  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;
9404
+ const key = `${ref.field}:${treeId}`;
9405
+ if (this._loaderStartedFor === key) return;
9406
+ this._loaderStartedFor = key;
9407
+ this.loader.start({
9408
+ base: this.props.grameneAPI,
9409
+ treeId: treeId,
9410
+ treeField: ref.field,
9411
+ geneId: gene._id,
9412
+ rawGenetree: raw,
9413
+ // Only pay for gene-structure data when the genome zone is enabled; the
9414
+ // request shape (50 ids per /genes call, no fl) is what this tab has
9415
+ // always sent.
9416
+ geneStructures: this.isGenomeZoneEnabled(),
9417
+ geneStructureChunkSize: 50,
9418
+ geneStructureFl: null,
9419
+ // Same for the per-gene expression attributes zone.
9420
+ expression: this.isExpressionZoneEnabled()
9509
9421
  });
9510
9422
  }
9511
9423
  startResize(e) {
@@ -9558,49 +9470,35 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9558
9470
  });
9559
9471
  }
9560
9472
  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.
9473
+ const snap = this.state.snap;
9474
+ const hostProps = (0, $gXNCa$tbrowsecore.hostPropsFrom)(snap);
9475
+ // Nothing to draw until the loader has adapted the tree (one tick after
9476
+ // start(), since the stored doc needs no fetch) and the bundle slice has
9477
+ // been seeded by maybeSeedTbrowseViewState (lifecycle, not render).
9586
9478
  const tbrowseVS = this.getHomologySlice().tbrowse;
9587
- if (!tbrowseVS) return null;
9588
- // 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);
9479
+ // Also wait until the snapshot holds the SELECTED tree, so a switch never
9480
+ // draws the previous tree under the new selection.
9481
+ const isSelected = snap.root && this.treeRef && snap.root.meta.treeId === this.treeRef.id;
9482
+ if (!hostProps || !tbrowseVS || !isSelected) // Reserve the pane's height so the card doesn't jump when the tree lands.
9483
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
9484
+ className: "gene-genetree",
9485
+ style: {
9486
+ height: this.getHeight(),
9487
+ width: '100%'
9488
+ },
9489
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Spinner), {})
9490
+ });
9491
+ // Layer the genome-limit and "expressed only" prune sets on top of the
9492
+ // user's own prunes for the controlled view state, but persist only the
9493
+ // user's prunes (the layer strips the derived ones in onViewStateChange) so
9494
+ // the bundle stays free of derived state and re-applies cleanly when the
9495
+ // filter or toggle changes.
9496
+ const pruneLayer = (0, $gXNCa$tbrowsecore.createDerivedPruneLayer)([
9497
+ ...new Set([
9498
+ ...this.getGenomePrunedIds(),
9499
+ ...this.getExpressionPrunedIds(tbrowseVS, hostProps)
9500
+ ])
9501
+ ]);
9604
9502
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
9605
9503
  children: [
9606
9504
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
@@ -9610,27 +9508,16 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9610
9508
  width: '100%'
9611
9509
  },
9612
9510
  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,
9511
+ ...hostProps,
9624
9512
  zones: this.getTbrowseZones(),
9625
9513
  nodeOfInterest: this.gene._id,
9626
- viewState: effectiveVS,
9627
- onViewStateChange: onViewStateChange,
9514
+ viewState: pruneLayer.effective(tbrowseVS),
9515
+ onViewStateChange: (next)=>this.setTbrowseViewState(this.expandOnExpressedOnly(tbrowseVS, pruneLayer.onChange(next))),
9628
9516
  densityControls: true,
9629
9517
  defaultOpenSections: {
9630
9518
  zones: true,
9631
9519
  search: true
9632
- },
9633
- zoneStatus: zoneStatus
9520
+ }
9634
9521
  })
9635
9522
  }),
9636
9523
  this.renderResizeHandle()
@@ -9862,7 +9749,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9862
9749
  });
9863
9750
  }
9864
9751
  }
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);
9752
+ 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
9753
 
9867
9754
 
9868
9755
 
@@ -20522,69 +20409,21 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
20522
20409
 
20523
20410
 
20524
20411
 
20412
+
20525
20413
  // Adapter: gramene taxonomy-with-genomes tree → tbrowse `Tree` + `Taxonomy`.
20526
20414
  //
20527
20415
  // `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) {
20416
+ // the root node of a gramene-taxonomy-with-genomes (tree-model) tree. The
20417
+ // conversion itself — node ids = String(NCBI taxon id), uniform branch lengths
20418
+ // (the tree zone runs in 'cladogram' mode for this view), `model.name` exposed
20419
+ // as both scientific and common name, duplicate-id guard — is
20420
+ // tbrowse-core's `fromTreeModel`, which was lifted from this file.
20421
+
20422
+ function $dc4f9c952681c07f$export$5fb520c9ccc6bdef(taxDist) {
20546
20423
  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
- };
20424
+ return (0, $gXNCa$tbrowsecore.fromTreeModel)(taxDist, {
20425
+ taxonomyIdAs: 'string'
20426
+ });
20588
20427
  }
20589
20428
 
20590
20429
 
@@ -20611,6 +20450,7 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
20611
20450
 
20612
20451
 
20613
20452
 
20453
+
20614
20454
  // ── colour model (ported from gramene-search-vis util/colors.js) ──────────
20615
20455
  // schemeCategory10, kept as RGB triples so we need no d3 dependency here.
20616
20456
  const $bfd29d54a0f0e853$var$REGION_RGB = [
@@ -21138,12 +20978,6 @@ const $bfd29d54a0f0e853$var$BinsHeader = ({ data: data, zoneState: zoneState, se
21138
20978
  // lets the row-highlight background (rendered in the layer beneath the canvas)
21139
20979
  // show as bands above/below the strip on hover/select.
21140
20980
  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
20981
  const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
21148
20982
  const { visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, zoneState: zoneState, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode } = props;
21149
20983
  const canvasRef = (0, $gXNCa$react.useRef)(null);
@@ -21157,7 +20991,7 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
21157
20991
  const snap = (0, $gXNCa$react.useSyncExternalStore)(store ? store.subscribe : $bfd29d54a0f0e853$var$NOOP_SUB, store ? store.getState : $bfd29d54a0f0e853$var$NOOP_GET);
21158
20992
  const transform = snap.transform || $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM;
21159
20993
  const mode = zoneState && zoneState.mode || 'select';
21160
- const totalHeight = visibleRows.length ? visibleRows[visibleRows.length - 1].y + visibleRows[visibleRows.length - 1].height : 0;
20994
+ const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
21161
20995
  const bins = data.hostData && data.hostData.bins;
21162
20996
  const rows = visibleRows.slice(rowRange.startIndex, rowRange.endIndex);
21163
20997
  const w = Math.max(1, Math.floor(width));
@@ -21410,7 +21244,11 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
21410
21244
  left: 0,
21411
21245
  right: 0,
21412
21246
  height: r.height,
21413
- background: $bfd29d54a0f0e853$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId))),
21247
+ background: (0, $gXNCa$tbrowse.rowHighlightBackground)((0, $gXNCa$tbrowsecore.rowFlags)(r, {
21248
+ hoveredNodeId: hoveredNodeId,
21249
+ hoveredSubtreeIds: hoveredSubtreeIds,
21250
+ selectedNodeId: selectedNodeId
21251
+ })),
21414
21252
  cursor: rowCursor,
21415
21253
  opacity: r.opacity ?? 1
21416
21254
  }
@@ -21450,37 +21288,12 @@ const $bfd29d54a0f0e853$export$d85d069f2280460c = {
21450
21288
  // Host-defined tbrowse zone: a narrow, right-justified text column showing the
21451
21289
  // number of matching genes per leaf (genome) — or, for a collapsed clade, the
21452
21290
  // 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 }.
21291
+ // per-node sums are computed by tbrowse-core's `countsFromFacet` in the
21292
+ // container and handed in via hostData.geneCounts = { [nodeId]: number }.
21293
+
21455
21294
 
21456
21295
 
21457
21296
 
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
21297
  const $15275c4f5939e11d$var$CountsHeader = ({ zoneState: zoneState, setZoneState: setZoneState })=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21485
21298
  style: {
21486
21299
  padding: '0 10px',
@@ -21498,28 +21311,24 @@ const $15275c4f5939e11d$var$CountsHeader = ({ zoneState: zoneState, setZoneState
21498
21311
  }))
21499
21312
  })
21500
21313
  });
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
21314
  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;
21315
+ const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
21512
21316
  const counts = data.hostData && data.hostData.geneCounts || {};
21513
- const rows = visibleRows.slice(rowRange.startIndex, rowRange.endIndex);
21317
+ // Rows in the virtualised range with the shared hover/select background, so
21318
+ // hovering any zone lights up the same row across all of them.
21319
+ const rows = (0, $gXNCa$tbrowse.rowsWithFlags)(visibleRows, rowRange, {
21320
+ hoveredNodeId: hoveredNodeId,
21321
+ hoveredSubtreeIds: hoveredSubtreeIds,
21322
+ selectedNodeId: selectedNodeId
21323
+ });
21514
21324
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21515
21325
  style: {
21516
21326
  position: 'relative',
21517
21327
  width: width,
21518
21328
  height: totalHeight
21519
21329
  },
21520
- children: rows.map((r)=>{
21330
+ children: rows.map(({ row: r, background: background })=>{
21521
21331
  const n = counts[r.nodeId];
21522
- const background = $15275c4f5939e11d$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId)));
21523
21332
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21524
21333
  onMouseEnter: ()=>onHoverNode(r.nodeId),
21525
21334
  onMouseLeave: ()=>onHoverNode(null),
@@ -21589,56 +21398,37 @@ function $15504f5eba8e73bd$var$binIdxsToSolr(idxs) {
21589
21398
  }
21590
21399
  return parts.join(' OR ');
21591
21400
  }
21401
+ // The initial controlled ViewState: tbrowse-core's buildInitialViewState over
21402
+ // this view's zone list with the view's own fr-shares, and — passed through
21403
+ // `overrides` so they replace the seeded defaults verbatim — the per-zone states
21404
+ // exactly as this view has always persisted them. The tree zone tolerates a
21405
+ // partial state (reads with `?? default`); the labels zone reads `visibleFields`
21406
+ // directly, so it must be present. Drive the tree as a cladogram (taxonomy has
21407
+ // no branch lengths) and show the genome/species name in the labels column.
21592
21408
  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: {}
21409
+ return (0, $gXNCa$tbrowsecore.buildInitialViewState)($15504f5eba8e73bd$var$ZONES, {
21410
+ widths: {
21411
+ tree: 30,
21412
+ labels: 22,
21413
+ counts: 10,
21414
+ bins: 60
21639
21415
  },
21640
- search: null
21641
- };
21416
+ overrides: {
21417
+ zoneStates: {
21418
+ tree: {
21419
+ layoutMode: 'cladogram',
21420
+ showLeafExtensions: false
21421
+ },
21422
+ labels: {
21423
+ visibleFields: [
21424
+ 'taxonomy.commonName'
21425
+ ]
21426
+ },
21427
+ counts: {},
21428
+ bins: {}
21429
+ }
21430
+ }
21431
+ });
21642
21432
  }
21643
21433
  // The set of taxa to SHOW, mirroring the legacy TaxDist.js logic.
21644
21434
  function $15504f5eba8e73bd$var$computeSelectedTaxa({ grameneSearch: grameneSearch, grameneGenomes: grameneGenomes, grameneMaps: grameneMaps }, treeNodes, collapseEmpties, comparaOnly, showCompara) {
@@ -21702,7 +21492,7 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21702
21492
  const facet = props.grameneSearch && props.grameneSearch.facet_counts && props.grameneSearch.facet_counts.facet_fields && props.grameneSearch.facet_counts.facet_fields.taxon_id;
21703
21493
  return {
21704
21494
  bins: (0, $bfd29d54a0f0e853$export$390e10a52782f018)(grameneTaxDist),
21705
- geneCounts: adapter ? (0, $15275c4f5939e11d$export$ce87f9041549c2e9)(adapter.tree, facet) : null,
21495
+ geneCounts: adapter ? (0, $gXNCa$tbrowsecore.countsFromFacet)(adapter.tree, facet, 'nodeId') : null,
21706
21496
  binsUI: binsUiRef.current,
21707
21497
  onApplyBinsFilter: onApplyBinsFilter
21708
21498
  };
@@ -21728,7 +21518,7 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21728
21518
  const prunedNodeIds = (0, $gXNCa$react.useMemo)(()=>{
21729
21519
  if (!adapter) return [];
21730
21520
  const selected = $15504f5eba8e73bd$var$computeSelectedTaxa(props, adapter.tree.nodes, collapseEmpties, comparaOnly, showCompara);
21731
- return adapter.leafTaxonIds.filter((id)=>!selected[id]);
21521
+ return (0, $gXNCa$tbrowsecore.leavesFailing)(adapter.tree, (leaf)=>Boolean(selected[String(leaf.taxonomyId)]));
21732
21522
  // eslint-disable-next-line react-hooks/exhaustive-deps
21733
21523
  }, [
21734
21524
  adapter,
@@ -21740,7 +21530,9 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21740
21530
  props.grameneMaps
21741
21531
  ]);
21742
21532
  // Effective (controlled) view state = user interactions + control-driven
21743
- // pruning. Memoised so the reference is stable across unrelated renders.
21533
+ // pruning. The derived set REPLACES prunedNodeIds (this view has no user
21534
+ // prunes of its own; the snapshot strips the field for the same reason).
21535
+ // Memoised so the reference is stable across unrelated renders.
21744
21536
  const effectiveViewState = (0, $gXNCa$react.useMemo)(()=>({
21745
21537
  ...viewState,
21746
21538
  prunedNodeIds: prunedNodeIds
@@ -21761,20 +21553,18 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21761
21553
  const el = wrapperRef.current;
21762
21554
  if (!el || el.clientWidth <= 0 || window.innerWidth < 800) return;
21763
21555
  sizedRef.current = true;
21764
- const TARGET = {
21556
+ const widths = (0, $gXNCa$tbrowsecore.pixelWidthsToFr)({
21765
21557
  tree: 270,
21766
21558
  labels: 200,
21767
21559
  counts: 64
21768
- };
21769
- const used = TARGET.tree + TARGET.labels + TARGET.counts;
21770
- const binsW = Math.max(160, el.clientWidth - used);
21560
+ }, el.clientWidth, 'bins', 160);
21771
21561
  const base = $15504f5eba8e73bd$var$baseViewState();
21772
21562
  setTaxTreeView({
21773
21563
  viewState: {
21774
21564
  ...base,
21775
21565
  zones: base.zones.map((z)=>({
21776
21566
  ...z,
21777
- width: z.id === 'bins' ? binsW : TARGET[z.id] ?? z.width
21567
+ width: widths[z.id] ?? z.width
21778
21568
  }))
21779
21569
  }
21780
21570
  });