gramene-search 2.39.0 → 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
  }
@@ -8356,6 +8362,18 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
8356
8362
  studies,
8357
8363
  geneStudyIds
8358
8364
  ]);
8365
+ // Without a baseline study the EBI Studies heatmap has nothing to draw (its
8366
+ // request fails rather than coming back empty), so the tab says so and
8367
+ // points at the differential EBI studies the gene does appear in -- e.g.
8368
+ // Medicago truncatula, whose studies are all differential.
8369
+ const ebiDifferential = (0, $gXNCa$react.useMemo)(()=>{
8370
+ if (!geneStudyIds || !studies) return [];
8371
+ const mine = new Set(geneStudyIds);
8372
+ 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);
8373
+ }, [
8374
+ studies,
8375
+ geneStudyIds
8376
+ ]);
8359
8377
  const allJgiKey = JSON.stringify(allJgiStudies.map((e)=>[
8360
8378
  e._id,
8361
8379
  e.name
@@ -8454,7 +8472,43 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
8454
8472
  role: "status",
8455
8473
  children: "Loading expression studies\u2026"
8456
8474
  }),
8457
- activeTab === "gene" && studiesSettled && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$grameneatlasheatmap.ExpressionAtlasHeatmap), {
8475
+ activeTab === "gene" && studiesSettled && !hasEbiBaseline && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
8476
+ className: "text-muted small",
8477
+ role: "status",
8478
+ children: [
8479
+ gene.name || gene._id,
8480
+ " is not in any baseline expression study, so there is no heatmap here.",
8481
+ ebiDifferential.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
8482
+ children: [
8483
+ ' ',
8484
+ "It has results in ",
8485
+ ebiDifferential.length === 1 ? 'a differential study' : `${ebiDifferential.length} differential studies`,
8486
+ " at Expression Atlas:",
8487
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("ul", {
8488
+ className: "mt-1 mb-0",
8489
+ children: ebiDifferential.map((e)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
8490
+ children: [
8491
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("a", {
8492
+ href: `${$9e29a4f60318db7a$var$EBI_GXA}experiments/${encodeURIComponent(e._id)}/Results?geneQuery=${encodeURIComponent(JSON.stringify([
8493
+ {
8494
+ value: geneQuery
8495
+ }
8496
+ ]))}`,
8497
+ target: "_blank",
8498
+ rel: "noopener noreferrer",
8499
+ children: e.description || e._id
8500
+ }),
8501
+ " (",
8502
+ e._id,
8503
+ ")"
8504
+ ]
8505
+ }, e._id))
8506
+ })
8507
+ ]
8508
+ })
8509
+ ]
8510
+ }),
8511
+ activeTab === "gene" && studiesSettled && hasEbiBaseline && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$grameneatlasheatmap.ExpressionAtlasHeatmap), {
8458
8512
  ...$9e29a4f60318db7a$var$HEATMAP_OPTIONS,
8459
8513
  resolveUrl: resolveUrl,
8460
8514
  atlasUrl: atlasUrl,
@@ -8545,6 +8599,7 @@ var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
8545
8599
 
8546
8600
 
8547
8601
 
8602
+
8548
8603
  const $5c2c79352d3d7b81$export$ec6b54b688be1708 = ({ fullscreen: fullscreen, onExitFullscreen: onExitFullscreen, title: title, className: className, children: children })=>{
8549
8604
  const [stableNode] = (0, $gXNCa$react.useState)(()=>typeof document !== 'undefined' ? document.createElement('div') : null);
8550
8605
  const inlineRef = (0, $gXNCa$react.useRef)(null);
@@ -8728,33 +8783,28 @@ const $5c2c79352d3d7b81$export$b2e089eb3692b073 = (props)=>/*#__PURE__*/ (0, $gX
8728
8783
 
8729
8784
 
8730
8785
 
8786
+
8731
8787
  const $cd8bc494277e92a4$var$ORGAN_CELL_W = 16;
8732
8788
  const $cd8bc494277e92a4$var$MAXTPM_W = 46;
8733
8789
  const $cd8bc494277e92a4$var$STRESS_MIN = 120;
8734
8790
  function $cd8bc494277e92a4$export$965a6b1d7408d496(docs, tree) {
8735
- const nodeOf = {};
8736
- const nodeGene = {}; // leaf nodeId -> gene id, for every leaf (hover readout)
8737
- if (tree && tree.nodes) Object.values(tree.nodes).forEach((n)=>{
8738
- if (n.isLeaf && n.geneId) {
8739
- nodeOf[n.geneId] = n.id;
8740
- nodeGene[n.id] = n.geneId;
8741
- }
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);
8742
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 : {};
8743
8799
  const organSet = new Set();
8744
- const byNode = {};
8745
8800
  let tpmMin = Infinity;
8746
8801
  let tpmMax = -Infinity;
8747
- (docs || []).forEach((d)=>{
8748
- if (!d || !d.id) return;
8749
- const nodeId = nodeOf[d.id];
8750
- if (!nodeId) return;
8751
- const attrs = (0, $4b8473139e48e77e$export$69fb5d19b5db1cfa)(d);
8802
+ Object.values(byNode).forEach((attrs)=>{
8752
8803
  Object.keys(attrs.organLevels).forEach((o)=>organSet.add(o));
8753
8804
  if (attrs.maxTpm !== null) {
8754
8805
  if (attrs.maxTpm < tpmMin) tpmMin = attrs.maxTpm;
8755
8806
  if (attrs.maxTpm > tpmMax) tpmMax = attrs.maxTpm;
8756
8807
  }
8757
- byNode[nodeId] = attrs;
8758
8808
  });
8759
8809
  return {
8760
8810
  organs: (0, $4b8473139e48e77e$export$65382efdcdd96f8c)(organSet),
@@ -8766,14 +8816,6 @@ function $cd8bc494277e92a4$export$965a6b1d7408d496(docs, tree) {
8766
8816
  }
8767
8817
  };
8768
8818
  }
8769
- // Shared row-highlight background, matching the other zones so hovering any zone
8770
- // lights up the same row across all of them.
8771
- function $cd8bc494277e92a4$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
8772
- if (isSelected) return 'var(--tbrowse-row-select-bg)';
8773
- if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
8774
- if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
8775
- return 'transparent';
8776
- }
8777
8819
  const $cd8bc494277e92a4$var$gridBorder = '1px solid var(--tbrowse-grid-line, rgba(0,0,0,0.06))';
8778
8820
  const $cd8bc494277e92a4$var$stressWidth = (width, organCount)=>Math.max($cd8bc494277e92a4$var$STRESS_MIN, width - organCount * $cd8bc494277e92a4$var$ORGAN_CELL_W - $cd8bc494277e92a4$var$MAXTPM_W);
8779
8821
  const $cd8bc494277e92a4$var$ExprHeader = ({ zoneState: zoneState, setZoneState: setZoneState, width: width, data: data, hoveredNodeId: hoveredNodeId })=>{
@@ -8968,8 +9010,12 @@ const $cd8bc494277e92a4$var$ExprBody = ({ visibleRows: visibleRows, rowRange: ro
8968
9010
  max: 0
8969
9011
  };
8970
9012
  const sW = $cd8bc494277e92a4$var$stressWidth(width, organs.length);
8971
- const totalHeight = visibleRows.length ? visibleRows[visibleRows.length - 1].y + visibleRows[visibleRows.length - 1].height : 0;
8972
- 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
+ });
8973
9019
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
8974
9020
  style: {
8975
9021
  position: 'relative',
@@ -8977,9 +9023,8 @@ const $cd8bc494277e92a4$var$ExprBody = ({ visibleRows: visibleRows, rowRange: ro
8977
9023
  height: totalHeight,
8978
9024
  overflow: 'hidden'
8979
9025
  },
8980
- children: rows.map((r)=>{
9026
+ children: rows.map(({ row: r, background: background })=>{
8981
9027
  const gene = byNode[r.nodeId];
8982
- const background = $cd8bc494277e92a4$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId)));
8983
9028
  const tpmFrac = gene ? (0, $4b8473139e48e77e$export$fc40d061e6c16645)(gene.maxTpm, tpmRange) : null;
8984
9029
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
8985
9030
  onMouseEnter: ()=>onHoverNode(r.nodeId),
@@ -9083,17 +9128,6 @@ const $047461923b1badda$export$964d88edb00bbcaa = (suggestion)=>{
9083
9128
 
9084
9129
 
9085
9130
 
9086
- const $64fad37f770d2bfe$var$genomeZone = (0, $gXNCa$tbrowse.createGenomeZone)({
9087
- id: 'genome'
9088
- });
9089
- // The genome (gene-structure) zone is gated behind the site-config flag
9090
- // `enable_tbrowse_genome_zone` (defaults to false) — see getTbrowseZones().
9091
- const $64fad37f770d2bfe$var$TBROWSE_BASE_ZONES = [
9092
- (0, $gXNCa$tbrowse.treeZone),
9093
- (0, $gXNCa$tbrowse.labelsZone),
9094
- (0, $gXNCa$tbrowse.msaZone),
9095
- (0, $gXNCa$tbrowse.neighborhoodZone)
9096
- ];
9097
9131
  // A gene's trees: the main Compara tree (homology.gene_tree) and/or trees in
9098
9132
  // clade-specific tree sets, homology.<tree_set> — e.g. a wheat gene may carry
9099
9133
  // gene_tree, homology.wheat = {id: 'EplWheatGT_…'} and homology.barley. Solr
@@ -9126,27 +9160,72 @@ const $64fad37f770d2bfe$var$treeLabel = (ref)=>{
9126
9160
  const set = ref.field === 'gene_tree' ? 'Compara' : ref.field.charAt(0).toUpperCase() + ref.field.slice(1);
9127
9161
  return ref.rootTaxonName ? `${set} \xb7 ${ref.rootTaxonName}` : set;
9128
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
+ };
9129
9215
  class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
9130
9216
  constructor(props){
9131
9217
  super(props);
9132
- // 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
9133
9222
  // user-controlled view state (viewer toggle, resize height, tbrowse
9134
9223
  // ViewState) is lifted into the uiViewState bundle, keyed by geneId.
9135
- //
9136
- // `*Status` fields track per-zone load lifecycle so the TBrowse
9137
- // toolbar can render loading/error affordances via its zoneStatus
9138
- // prop. They start `undefined` (no signal); flipped to 'loading'
9139
- // 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
+ });
9140
9227
  this.state = {
9141
- neighborhood: null,
9142
- neighborhoodTreeId: null,
9143
- neighborhoodStatus: undefined,
9144
- geneStructures: null,
9145
- geneStructuresTreeId: null,
9146
- geneStructuresStatus: undefined,
9147
- exprAttrs: null,
9148
- exprAttrsTreeId: null,
9149
- exprAttrsStatus: undefined
9228
+ snap: this.loader.getSnapshot()
9150
9229
  };
9151
9230
  if (!props.geneDocs.hasOwnProperty(props.searchResult.id)) props.requestGene(props.searchResult.id);
9152
9231
  this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(props.grameneTaxonomy));
@@ -9216,248 +9295,95 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9216
9295
  // When the user has limited the search to a subset of genomes
9217
9296
  // (grameneGenomes.active), prune the gene tree to genes from those genomes
9218
9297
  // — mirroring the taxon_id filter api.js applies to the search itself, and
9219
- // the genomesOfInterest TreeVis already honours. Returns the ids of the
9220
- // highest nodes whose entire subtree lies outside the active set (maximal
9221
- // excluded clades), rather than individual leaves: tbrowse then sheds one
9222
- // stub per clade instead of one per leaf, and its single-marker count is
9223
- // meaningful. The gene of interest is always kept so the tree never
9224
- // 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.
9225
9305
  getGenomePrunedIds() {
9226
9306
  const active = this.props.grameneGenomes && this.props.grameneGenomes.active || {};
9227
9307
  const activeIds = Object.keys(active);
9228
9308
  if (activeIds.length === 0) return [];
9229
- if (!this._tbrowseData) return [];
9309
+ const snap = this.state.snap;
9310
+ if (!snap.tree) return [];
9230
9311
  const activeSet = new Set(activeIds.map(String));
9231
9312
  const keepGeneId = this.gene && this.gene._id;
9232
- const nodes = this._tbrowseData.tree.nodes;
9233
- const children = {};
9234
- Object.values(nodes).forEach((n)=>{
9235
- if (n.parentId != null) (children[n.parentId] = children[n.parentId] || []).push(n.id);
9236
- });
9237
- // hasKept(node): does the subtree contain a leaf we must keep (its taxon
9238
- // is active, or it's the gene of interest)? Memoised post-order walk.
9239
- const memo = {};
9240
- const hasKept = (id)=>{
9241
- if (id in memo) return memo[id];
9242
- const n = nodes[id];
9243
- const v = n.isLeaf ? n.taxonomyId != null && activeSet.has(String(n.taxonomyId)) || n.geneId === keepGeneId : (children[id] || []).some(hasKept);
9244
- memo[id] = v;
9245
- return v;
9246
- };
9247
- // A node is a maximal excluded clade when it keeps nothing but its parent
9248
- // does (or it's the root) — i.e. the highest fully-excluded node on its
9249
- // path. The root always keeps the gene of interest, so it is never pruned.
9250
- 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);
9251
9314
  }
9252
9315
  // Seed the bundle with a pivot-computed initial tbrowse view state once the
9253
- // tree data is available and the user is actually looking at the tbrowse
9254
- // 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.
9255
9323
  maybeSeedTbrowseViewState() {
9256
9324
  if (this.getViewer() !== 'tbrowse') return;
9257
9325
  if (this.getHomologySlice().tbrowse) return;
9258
- const id = this.getGeneId();
9259
- if (!this.props.geneDocs.hasOwnProperty(id)) return;
9260
- const gene = this.props.geneDocs[id];
9261
- const ref = this.selectedTreeRef(gene);
9262
- if (!ref) return;
9263
- const treeId = ref.id;
9264
- const raw = this.props.grameneTrees[treeId];
9265
- if (!raw || !raw.taxon_id) return;
9266
- const adapted = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
9267
- raw
9268
- ]);
9269
- const pivot = (0, $gXNCa$tbrowse.computePivotState)(adapted.tree, gene._id);
9270
- this.setTbrowseViewState({
9271
- selectedNodeId: null,
9272
- collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
9273
- prunedNodeIds: [],
9274
- swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
9275
- compressedNodeIds: [],
9276
- nodeOfInterestId: pivot ? pivot.targetId : null,
9277
- // Use each zone's intended fr-share + initial visibility from its
9278
- // definition (tree:30 / labels:20 / msa:50 / neighborhood:30 /
9279
- // genome:32) instead of uniform 25 across the board. Gives the MSA
9280
- // a wider initial pane and the tree a narrower one.
9281
- //
9282
- // `defaultVisible ?? true` matches tbrowse's own buildInitialViewState
9283
- // logic — zones default to visible unless their definition opts out
9284
- // (e.g. neighborhood and genome opt out so they only appear once
9285
- // their async data lands; tbrowse's Layout auto-flips them on then).
9286
- zones: this.getTbrowseZones().map((z)=>({
9287
- id: z.id,
9288
- width: z.defaultWidth,
9289
- visible: z.defaultVisible ?? true
9290
- })),
9291
- zoneStates: {},
9292
- search: null
9293
- });
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)));
9294
9333
  }
9295
9334
  componentDidMount() {
9335
+ this._unsubscribe = this.loader.subscribe(()=>this.setState({
9336
+ snap: this.loader.getSnapshot()
9337
+ }));
9338
+ this.maybeStartLoader();
9296
9339
  this.maybeSeedTbrowseViewState();
9297
- this.maybeFetchTbrowseData();
9298
9340
  }
9299
9341
  componentDidUpdate() {
9342
+ this.maybeStartLoader();
9300
9343
  this.maybeSeedTbrowseViewState();
9301
- this.maybeFetchTbrowseData();
9302
9344
  }
9303
- // Kick off the neighborhood + gene-structure fetches from lifecycle,
9304
- // not render. Each fetch internally dedupes on `_*FetchedFor === treeId`
9305
- // so calling on every commit is cheap. Gated on the user actually being
9306
- // on the tbrowse viewer + the tree data being loaded — otherwise we'd
9307
- // pay network cost for genes the user never looks at.
9308
- 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() {
9309
9358
  if (this.getViewer() !== 'tbrowse') return;
9310
9359
  const id = this.getGeneId();
9311
9360
  if (!this.props.geneDocs.hasOwnProperty(id)) return;
9312
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).
9313
9365
  const ref = this.selectedTreeRef(gene);
9314
9366
  if (!ref) return;
9315
9367
  const treeId = ref.id;
9316
9368
  const raw = this.props.grameneTrees[treeId];
9317
9369
  if (!raw || !raw.taxon_id) return;
9318
- // Lazily compute the adapted tbrowse data the same way renderTBrowse
9319
- // does, so fetchGeneStructures has the leaf ids without rerunning
9320
- // fromGrameneGenetree on every commit.
9321
- if (this._tbrowseTreeId !== treeId) {
9322
- this._tbrowseTreeId = treeId;
9323
- this._tbrowseData = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
9324
- raw
9325
- ]);
9326
- }
9327
- this.fetchNeighborhood(treeId, ref.field);
9328
- // Only pay for gene-structure data when the genome zone is actually enabled.
9329
- if (this.isGenomeZoneEnabled()) this.fetchGeneStructures(treeId, this._tbrowseData.tree);
9330
- // Same for the per-gene expression attributes zone.
9331
- if (this.isExpressionZoneEnabled()) this.fetchExprAttrs(treeId, this._tbrowseData.tree, ref.field);
9332
- }
9333
- fetchNeighborhood(treeId, field = 'gene_tree') {
9334
- if (this._neighborhoodFetchedFor === treeId) return;
9335
- this._neighborhoodFetchedFor = treeId;
9336
- this.setState({
9337
- neighborhoodStatus: 'loading'
9338
- });
9339
- const api = this.props.grameneAPI;
9340
- const url = new URL(`${api}/search`);
9341
- const fl = 'id,name,gene_tree,gene_idx,region,start,end,strand,biotype,system_name,description';
9342
- url.searchParams.set('fl', field === 'gene_tree' ? fl : `${fl},${field}`);
9343
- // cache=false: as a cached fq the graph query caches its seed inside its own
9344
- // filterCache computeIfAbsent, and Solr's FastLRUCache throws "Recursive
9345
- // update" (HTTP 500) whenever the two keys collide — for some trees, until
9346
- // the seed happens to be cached by another request.
9347
- url.searchParams.set('fq', `{!graph from=compara_neighbors_10 to=compara_idx_multi maxDepth=1 cache=false}${field}:${treeId}`);
9348
- url.searchParams.set('rows', '100000');
9349
- url.searchParams.set('start', '0');
9350
- fetch(url.toString(), {
9351
- headers: {
9352
- Accept: 'application/json'
9353
- }
9354
- }).then((res)=>{
9355
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
9356
- return res.json();
9357
- }).then((json)=>{
9358
- if (this._neighborhoodFetchedFor !== treeId) return;
9359
- // The neighborhood zone colours genes by family from doc.gene_tree; in
9360
- // a clade tree set the family id lives under the set's own field.
9361
- if (field !== 'gene_tree' && json && json.response && Array.isArray(json.response.docs)) json.response.docs.forEach((d)=>{
9362
- if (d[field] && !d.gene_tree) d.gene_tree = d[field];
9363
- });
9364
- this.setState({
9365
- neighborhood: (0, $gXNCa$tbrowse.fromGrameneNeighborhood)(json),
9366
- neighborhoodTreeId: treeId,
9367
- neighborhoodStatus: 'ready'
9368
- });
9369
- }).catch((err)=>{
9370
- console.warn('tbrowse neighborhood fetch failed:', err);
9371
- // Surface the failure via the zoneStatus prop. Reset the
9372
- // dedupe key so a re-mount or tree-change can retry.
9373
- if (this._neighborhoodFetchedFor === treeId) this.setState({
9374
- neighborhoodStatus: 'error'
9375
- });
9376
- this._neighborhoodFetchedFor = null;
9377
- });
9378
- }
9379
- fetchGeneStructures(treeId, tree) {
9380
- if (this._geneStructuresFetchedFor === treeId) return;
9381
- this._geneStructuresFetchedFor = treeId;
9382
- const ids = Object.values(tree.nodes).filter((n)=>n.isLeaf && n.geneId).map((n)=>n.geneId);
9383
- if (ids.length === 0) return;
9384
- this.setState({
9385
- geneStructuresStatus: 'loading'
9386
- });
9387
- const api = this.props.grameneAPI;
9388
- const BATCH_SIZE = 50;
9389
- const batches = [];
9390
- for(let i = 0; i < ids.length; i += BATCH_SIZE)batches.push(ids.slice(i, i + BATCH_SIZE));
9391
- const fetchBatch = (batch)=>{
9392
- const url = new URL(`${api}/genes`);
9393
- url.searchParams.set('idList', batch.join(','));
9394
- url.searchParams.set('rows', '-1');
9395
- return fetch(url.toString(), {
9396
- headers: {
9397
- Accept: 'application/json'
9398
- }
9399
- }).then((res)=>{
9400
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
9401
- return res.json();
9402
- });
9403
- };
9404
- Promise.all(batches.map(fetchBatch)).then((results)=>{
9405
- if (this._geneStructuresFetchedFor !== treeId) return;
9406
- const combined = [].concat(...results.map((r)=>Array.isArray(r) ? r : []));
9407
- this.setState({
9408
- geneStructures: (0, $gXNCa$tbrowse.fromGrameneGeneStructures)(combined),
9409
- geneStructuresTreeId: treeId,
9410
- geneStructuresStatus: 'ready'
9411
- });
9412
- }).catch((err)=>{
9413
- console.warn('tbrowse gene-structures fetch failed:', err);
9414
- if (this._geneStructuresFetchedFor === treeId) this.setState({
9415
- geneStructuresStatus: 'error'
9416
- });
9417
- this._geneStructuresFetchedFor = null;
9418
- });
9419
- }
9420
- // Fetch the expression attributes (expr_*__attr_*) for this tree's genes in a
9421
- // single query: scope by the tree's field and the `expression_attributes` capability
9422
- // so only genes that actually have the data come back (rows high enough to
9423
- // cover the whole tree). Uses /search (not /genes, whose trimmed docs omit the
9424
- // attr fields) with an fl glob so the columns are discovered from the data
9425
- // rather than hard-coded. `tree` is still needed to map gene ids to leaf node
9426
- // ids (see buildExprByNode).
9427
- fetchExprAttrs(treeId, tree, field = 'gene_tree') {
9428
- if (this._exprAttrsFetchedFor === treeId) return;
9429
- this._exprAttrsFetchedFor = treeId;
9430
- this.setState({
9431
- exprAttrsStatus: 'loading'
9432
- });
9433
- const api = this.props.grameneAPI;
9434
- const url = new URL(`${api}/search`);
9435
- url.searchParams.set('q', '*:*');
9436
- url.searchParams.append('fq', `${field}:${treeId}`);
9437
- url.searchParams.append('fq', 'capabilities:expression_attributes');
9438
- url.searchParams.set('fl', 'id,expr_*__attr_*');
9439
- url.searchParams.set('rows', '10000');
9440
- fetch(url.toString(), {
9441
- headers: {
9442
- Accept: 'application/json'
9443
- }
9444
- }).then((res)=>{
9445
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
9446
- return res.json();
9447
- }).then((json)=>{
9448
- if (this._exprAttrsFetchedFor !== treeId) return;
9449
- const docs = json && json.response && Array.isArray(json.response.docs) ? json.response.docs : [];
9450
- this.setState({
9451
- exprAttrs: (0, $cd8bc494277e92a4$export$965a6b1d7408d496)(docs, tree),
9452
- exprAttrsTreeId: treeId,
9453
- exprAttrsStatus: 'ready'
9454
- });
9455
- }).catch((err)=>{
9456
- console.warn('tbrowse expression-attrs fetch failed:', err);
9457
- if (this._exprAttrsFetchedFor === treeId) this.setState({
9458
- exprAttrsStatus: 'error'
9459
- });
9460
- 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()
9461
9387
  });
9462
9388
  }
9463
9389
  startResize(e) {
@@ -9510,49 +9436,30 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9510
9436
  });
9511
9437
  }
9512
9438
  renderTBrowse() {
9513
- const treeId = this.treeRef.id;
9514
- // Adapted tbrowse data is computed lazily in maybeFetchTbrowseData
9515
- // (called from lifecycle). If the user just flipped to tbrowse and
9516
- // the lifecycle hasn't fired yet, compute it once here without
9517
- // calling any fetches — those will fire from componentDidUpdate.
9518
- if (this._tbrowseTreeId !== treeId) {
9519
- this._tbrowseTreeId = treeId;
9520
- this._tbrowseData = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
9521
- this.props.grameneTrees[treeId]
9522
- ]);
9523
- }
9524
- const neighborhood = this.state.neighborhoodTreeId === treeId ? this.state.neighborhood : undefined;
9525
- const geneStructures = this.state.geneStructuresTreeId === treeId ? this.state.geneStructures : undefined;
9526
- const exprAttrs = this.state.exprAttrsTreeId === treeId ? this.state.exprAttrs : undefined;
9527
- // Per-zone status for the TBrowse toolbar — pulses while a fetch
9528
- // is in flight, turns red on failure. Tracked per-tree so a
9529
- // tree-change resets any stale 'ready'/'error' from the prior gene.
9530
- const zoneStatus = {
9531
- neighborhood: this.state.neighborhoodTreeId === treeId ? this.state.neighborhoodStatus : this.state.neighborhoodStatus === 'loading' ? 'loading' : undefined,
9532
- genome: this.state.geneStructuresTreeId === treeId ? this.state.geneStructuresStatus : this.state.geneStructuresStatus === 'loading' ? 'loading' : undefined,
9533
- expression: this.state.exprAttrsTreeId === treeId ? this.state.exprAttrsStatus : this.state.exprAttrsStatus === 'loading' ? 'loading' : undefined
9534
- };
9535
- // Bundle-driven (controlled) view state. If we're rendering tbrowse before
9536
- // componentDidMount/Update has seeded the bundle slice, skip this turn and
9537
- // 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).
9538
9444
  const tbrowseVS = this.getHomologySlice().tbrowse;
9539
- 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
+ });
9540
9457
  // Layer the genome-limit prune set on top of the user's own prunes for the
9541
- // controlled view state, but persist only the user's prunes (strip the
9542
- // genome-derived ones in onViewStateChange) so the bundle stays free of
9543
- // filter-derived state and re-applies cleanly when the filter changes.
9544
- const genomePrunedIds = this.getGenomePrunedIds();
9545
- const effectiveVS = genomePrunedIds.length ? {
9546
- ...tbrowseVS,
9547
- prunedNodeIds: (0, ($parcel$interopDefault($gXNCa$lodash))).union(tbrowseVS.prunedNodeIds || [], genomePrunedIds)
9548
- } : tbrowseVS;
9549
- const onViewStateChange = genomePrunedIds.length ? (next)=>{
9550
- const genomeSet = new Set(genomePrunedIds);
9551
- this.setTbrowseViewState({
9552
- ...next,
9553
- prunedNodeIds: (next.prunedNodeIds || []).filter((id)=>!genomeSet.has(id))
9554
- });
9555
- } : (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());
9556
9463
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
9557
9464
  children: [
9558
9465
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
@@ -9562,27 +9469,16 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9562
9469
  width: '100%'
9563
9470
  },
9564
9471
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$tbrowse.TBrowse), {
9565
- tree: this._tbrowseData.tree,
9566
- taxonomy: this._tbrowseData.taxonomy,
9567
- msa: this._tbrowseData.msa,
9568
- geneMetadata: this._tbrowseData.geneMetadata,
9569
- proteinDomains: this._tbrowseData.proteinDomains,
9570
- exonJunctions: this._tbrowseData.exonJunctions,
9571
- neighborhood: neighborhood,
9572
- geneStructures: geneStructures,
9573
- hostData: exprAttrs ? {
9574
- exprAttrs: exprAttrs
9575
- } : undefined,
9472
+ ...hostProps,
9576
9473
  zones: this.getTbrowseZones(),
9577
9474
  nodeOfInterest: this.gene._id,
9578
- viewState: effectiveVS,
9579
- onViewStateChange: onViewStateChange,
9475
+ viewState: pruneLayer.effective(tbrowseVS),
9476
+ onViewStateChange: (next)=>this.setTbrowseViewState(pruneLayer.onChange(next)),
9580
9477
  densityControls: true,
9581
9478
  defaultOpenSections: {
9582
9479
  zones: true,
9583
9480
  search: true
9584
- },
9585
- zoneStatus: zoneStatus
9481
+ }
9586
9482
  })
9587
9483
  }),
9588
9484
  this.renderResizeHandle()
@@ -9814,7 +9710,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
9814
9710
  });
9815
9711
  }
9816
9712
  }
9817
- 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);
9818
9714
 
9819
9715
 
9820
9716
 
@@ -20474,69 +20370,21 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
20474
20370
 
20475
20371
 
20476
20372
 
20373
+
20477
20374
  // Adapter: gramene taxonomy-with-genomes tree → tbrowse `Tree` + `Taxonomy`.
20478
20375
  //
20479
20376
  // `grameneTaxDist` (from the grameneTaxDist bundle's selectGrameneTaxDist) is
20480
- // the root node of a gramene-taxonomy-with-genomes tree. Each node exposes
20481
- // `node.model` ({ id, name, genome?, rank?, results? }) and `node.children`.
20482
- // Genome leaves carry `model.genome`; internal clade nodes have children.
20483
- //
20484
- // tbrowse wants a flat `Tree { rootId, nodes: { [id]: TreeNode } }` plus a
20485
- // `Taxonomy { [taxonomyId]: { scientificName, commonName, rank } }` that its
20486
- // labels zone reads to render each leaf's species name. We key tbrowse
20487
- // NodeIds off the (unique) NCBI taxon id and set `taxonomyId` to the same id.
20488
- //
20489
- // Branch lengths are uniform (taxonomy carries none); the tree zone is driven
20490
- // in 'cladogram' mode for this view, which ignores `distance` and aligns the
20491
- // genome rows at the right edge regardless.
20492
- /**
20493
- * @param {object} taxDist - root node of the gramene taxDist tree
20494
- * (the object returned by selectGrameneTaxDist).
20495
- * @returns {{ tree: {rootId: string, nodes: object}, taxonomy: object,
20496
- * leafTaxonIds: string[] } | null}
20497
- */ 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) {
20498
20384
  if (!taxDist || !taxDist.model) return null;
20499
- const nodes = {};
20500
- const taxonomy = {};
20501
- const leafTaxonIds = [];
20502
- const rootId = String(taxDist.model.id);
20503
- const walk = (node, parentId)=>{
20504
- if (!node || !node.model) return;
20505
- const id = String(node.model.id);
20506
- // Guard against the (shouldn't-happen) cyclic / duplicate id case so a
20507
- // malformed taxonomy can't spin the recursion.
20508
- if (nodes[id]) return;
20509
- const kids = node.children || [];
20510
- const isLeaf = kids.length === 0;
20511
- nodes[id] = {
20512
- id: id,
20513
- parentId: parentId,
20514
- distance: parentId === null ? 0 : 1,
20515
- isLeaf: isLeaf,
20516
- taxonomyId: id
20517
- };
20518
- // `model.name` is the NCBI scientific name for clades and is overwritten
20519
- // with the genome's display_name for genome leaves upstream in
20520
- // selectGrameneTaxDist. Expose it as both names so the labels zone shows
20521
- // something whichever field the user has active.
20522
- const name = node.model.name;
20523
- taxonomy[id] = {
20524
- scientificName: name,
20525
- commonName: name,
20526
- rank: node.model.rank
20527
- };
20528
- if (isLeaf) leafTaxonIds.push(id);
20529
- for (const k of kids)walk(k, id);
20530
- };
20531
- walk(taxDist, null);
20532
- return {
20533
- tree: {
20534
- rootId: rootId,
20535
- nodes: nodes
20536
- },
20537
- taxonomy: taxonomy,
20538
- leafTaxonIds: leafTaxonIds
20539
- };
20385
+ return (0, $gXNCa$tbrowsecore.fromTreeModel)(taxDist, {
20386
+ taxonomyIdAs: 'string'
20387
+ });
20540
20388
  }
20541
20389
 
20542
20390
 
@@ -20563,6 +20411,7 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
20563
20411
 
20564
20412
 
20565
20413
 
20414
+
20566
20415
  // ── colour model (ported from gramene-search-vis util/colors.js) ──────────
20567
20416
  // schemeCategory10, kept as RGB triples so we need no d3 dependency here.
20568
20417
  const $bfd29d54a0f0e853$var$REGION_RGB = [
@@ -21090,12 +20939,6 @@ const $bfd29d54a0f0e853$var$BinsHeader = ({ data: data, zoneState: zoneState, se
21090
20939
  // lets the row-highlight background (rendered in the layer beneath the canvas)
21091
20940
  // show as bands above/below the strip on hover/select.
21092
20941
  const $bfd29d54a0f0e853$var$ROW_PAD_Y = 2;
21093
- function $bfd29d54a0f0e853$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
21094
- if (isSelected) return 'var(--tbrowse-row-select-bg)';
21095
- if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
21096
- if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
21097
- return 'transparent';
21098
- }
21099
20942
  const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
21100
20943
  const { visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, zoneState: zoneState, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode } = props;
21101
20944
  const canvasRef = (0, $gXNCa$react.useRef)(null);
@@ -21109,7 +20952,7 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
21109
20952
  const snap = (0, $gXNCa$react.useSyncExternalStore)(store ? store.subscribe : $bfd29d54a0f0e853$var$NOOP_SUB, store ? store.getState : $bfd29d54a0f0e853$var$NOOP_GET);
21110
20953
  const transform = snap.transform || $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM;
21111
20954
  const mode = zoneState && zoneState.mode || 'select';
21112
- const totalHeight = visibleRows.length ? visibleRows[visibleRows.length - 1].y + visibleRows[visibleRows.length - 1].height : 0;
20955
+ const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
21113
20956
  const bins = data.hostData && data.hostData.bins;
21114
20957
  const rows = visibleRows.slice(rowRange.startIndex, rowRange.endIndex);
21115
20958
  const w = Math.max(1, Math.floor(width));
@@ -21362,7 +21205,11 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
21362
21205
  left: 0,
21363
21206
  right: 0,
21364
21207
  height: r.height,
21365
- 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
+ })),
21366
21213
  cursor: rowCursor,
21367
21214
  opacity: r.opacity ?? 1
21368
21215
  }
@@ -21402,37 +21249,12 @@ const $bfd29d54a0f0e853$export$d85d069f2280460c = {
21402
21249
  // Host-defined tbrowse zone: a narrow, right-justified text column showing the
21403
21250
  // number of matching genes per leaf (genome) — or, for a collapsed clade, the
21404
21251
  // summed count over its subtree. Counts come from the taxon_id facet; the
21405
- // per-node sums are precomputed by computeGeneCounts and handed in via
21406
- // 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
+
21407
21255
 
21408
21256
 
21409
21257
 
21410
- function $15275c4f5939e11d$export$ce87f9041549c2e9(tree, taxonIdFacet) {
21411
- if (!tree || !tree.nodes) return {};
21412
- const leafCount = {};
21413
- if (Array.isArray(taxonIdFacet)) for(let i = 0; i < taxonIdFacet.length; i += 2)leafCount[String(taxonIdFacet[i])] = +taxonIdFacet[i + 1];
21414
- const childrenOf = {};
21415
- for (const id of Object.keys(tree.nodes)){
21416
- const p = tree.nodes[id].parentId;
21417
- if (p !== null && p !== undefined) (childrenOf[p] = childrenOf[p] || []).push(id);
21418
- }
21419
- const out = {};
21420
- const visit = (id)=>{
21421
- const node = tree.nodes[id];
21422
- if (!node) return 0;
21423
- if (node.isLeaf) {
21424
- const c = leafCount[id] || 0;
21425
- out[id] = c;
21426
- return c;
21427
- }
21428
- let sum = 0;
21429
- for (const k of childrenOf[id] || [])sum += visit(k);
21430
- out[id] = sum;
21431
- return sum;
21432
- };
21433
- visit(tree.rootId);
21434
- return out;
21435
- }
21436
21258
  const $15275c4f5939e11d$var$CountsHeader = ({ zoneState: zoneState, setZoneState: setZoneState })=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21437
21259
  style: {
21438
21260
  padding: '0 10px',
@@ -21450,28 +21272,24 @@ const $15275c4f5939e11d$var$CountsHeader = ({ zoneState: zoneState, setZoneState
21450
21272
  }))
21451
21273
  })
21452
21274
  });
21453
- // Shared row-highlight background, matching the labels zone so hovering any
21454
- // zone lights up the same row across all of them (state flows through the
21455
- // tbrowse store's hoveredNodeId).
21456
- function $15275c4f5939e11d$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
21457
- if (isSelected) return 'var(--tbrowse-row-select-bg)';
21458
- if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
21459
- if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
21460
- return 'transparent';
21461
- }
21462
21275
  const $15275c4f5939e11d$var$CountsBody = ({ visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode })=>{
21463
- const totalHeight = visibleRows.length ? visibleRows[visibleRows.length - 1].y + visibleRows[visibleRows.length - 1].height : 0;
21276
+ const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
21464
21277
  const counts = data.hostData && data.hostData.geneCounts || {};
21465
- 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
+ });
21466
21285
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21467
21286
  style: {
21468
21287
  position: 'relative',
21469
21288
  width: width,
21470
21289
  height: totalHeight
21471
21290
  },
21472
- children: rows.map((r)=>{
21291
+ children: rows.map(({ row: r, background: background })=>{
21473
21292
  const n = counts[r.nodeId];
21474
- const background = $15275c4f5939e11d$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId)));
21475
21293
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21476
21294
  onMouseEnter: ()=>onHoverNode(r.nodeId),
21477
21295
  onMouseLeave: ()=>onHoverNode(null),
@@ -21541,56 +21359,37 @@ function $15504f5eba8e73bd$var$binIdxsToSolr(idxs) {
21541
21359
  }
21542
21360
  return parts.join(' OR ');
21543
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.
21544
21369
  function $15504f5eba8e73bd$var$baseViewState() {
21545
- return {
21546
- selectedNodeId: null,
21547
- collapsedNodeIds: [],
21548
- prunedNodeIds: [],
21549
- swappedNodeIds: [],
21550
- compressedNodeIds: [],
21551
- nodeOfInterestId: null,
21552
- zones: [
21553
- {
21554
- id: 'tree',
21555
- width: 30,
21556
- visible: true
21557
- },
21558
- {
21559
- id: 'labels',
21560
- width: 22,
21561
- visible: true
21562
- },
21563
- {
21564
- id: 'counts',
21565
- width: 10,
21566
- visible: true
21567
- },
21568
- {
21569
- id: 'bins',
21570
- width: 60,
21571
- visible: true
21572
- }
21573
- ],
21574
- // Controlled mode supplies zoneStates wholesale, so each must carry the
21575
- // fields its zone reads. The tree zone tolerates a partial state (reads
21576
- // with `?? default`); the labels zone reads `visibleFields` directly, so
21577
- // it must be present. Drive the tree as a cladogram (taxonomy has no
21578
- // branch lengths) and show the genome/species name in the labels column.
21579
- zoneStates: {
21580
- tree: {
21581
- layoutMode: 'cladogram',
21582
- showLeafExtensions: false
21583
- },
21584
- labels: {
21585
- visibleFields: [
21586
- 'taxonomy.commonName'
21587
- ]
21588
- },
21589
- counts: {},
21590
- bins: {}
21370
+ return (0, $gXNCa$tbrowsecore.buildInitialViewState)($15504f5eba8e73bd$var$ZONES, {
21371
+ widths: {
21372
+ tree: 30,
21373
+ labels: 22,
21374
+ counts: 10,
21375
+ bins: 60
21591
21376
  },
21592
- search: null
21593
- };
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
+ });
21594
21393
  }
21595
21394
  // The set of taxa to SHOW, mirroring the legacy TaxDist.js logic.
21596
21395
  function $15504f5eba8e73bd$var$computeSelectedTaxa({ grameneSearch: grameneSearch, grameneGenomes: grameneGenomes, grameneMaps: grameneMaps }, treeNodes, collapseEmpties, comparaOnly, showCompara) {
@@ -21654,7 +21453,7 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21654
21453
  const facet = props.grameneSearch && props.grameneSearch.facet_counts && props.grameneSearch.facet_counts.facet_fields && props.grameneSearch.facet_counts.facet_fields.taxon_id;
21655
21454
  return {
21656
21455
  bins: (0, $bfd29d54a0f0e853$export$390e10a52782f018)(grameneTaxDist),
21657
- geneCounts: adapter ? (0, $15275c4f5939e11d$export$ce87f9041549c2e9)(adapter.tree, facet) : null,
21456
+ geneCounts: adapter ? (0, $gXNCa$tbrowsecore.countsFromFacet)(adapter.tree, facet, 'nodeId') : null,
21658
21457
  binsUI: binsUiRef.current,
21659
21458
  onApplyBinsFilter: onApplyBinsFilter
21660
21459
  };
@@ -21680,7 +21479,7 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21680
21479
  const prunedNodeIds = (0, $gXNCa$react.useMemo)(()=>{
21681
21480
  if (!adapter) return [];
21682
21481
  const selected = $15504f5eba8e73bd$var$computeSelectedTaxa(props, adapter.tree.nodes, collapseEmpties, comparaOnly, showCompara);
21683
- return adapter.leafTaxonIds.filter((id)=>!selected[id]);
21482
+ return (0, $gXNCa$tbrowsecore.leavesFailing)(adapter.tree, (leaf)=>Boolean(selected[String(leaf.taxonomyId)]));
21684
21483
  // eslint-disable-next-line react-hooks/exhaustive-deps
21685
21484
  }, [
21686
21485
  adapter,
@@ -21692,7 +21491,9 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21692
21491
  props.grameneMaps
21693
21492
  ]);
21694
21493
  // Effective (controlled) view state = user interactions + control-driven
21695
- // 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.
21696
21497
  const effectiveViewState = (0, $gXNCa$react.useMemo)(()=>({
21697
21498
  ...viewState,
21698
21499
  prunedNodeIds: prunedNodeIds
@@ -21713,20 +21514,18 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
21713
21514
  const el = wrapperRef.current;
21714
21515
  if (!el || el.clientWidth <= 0 || window.innerWidth < 800) return;
21715
21516
  sizedRef.current = true;
21716
- const TARGET = {
21517
+ const widths = (0, $gXNCa$tbrowsecore.pixelWidthsToFr)({
21717
21518
  tree: 270,
21718
21519
  labels: 200,
21719
21520
  counts: 64
21720
- };
21721
- const used = TARGET.tree + TARGET.labels + TARGET.counts;
21722
- const binsW = Math.max(160, el.clientWidth - used);
21521
+ }, el.clientWidth, 'bins', 160);
21723
21522
  const base = $15504f5eba8e73bd$var$baseViewState();
21724
21523
  setTaxTreeView({
21725
21524
  viewState: {
21726
21525
  ...base,
21727
21526
  zones: base.zones.map((z)=>({
21728
21527
  ...z,
21729
- width: z.id === 'bins' ? binsW : TARGET[z.id] ?? z.width
21528
+ width: widths[z.id] ?? z.width
21730
21529
  }))
21731
21530
  }
21732
21531
  });