gramene-search 2.0.6 → 2.0.7

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
@@ -11,6 +11,7 @@ var $gXNCa$gramenebinsclient = require("gramene-bins-client");
11
11
  var $gXNCa$gramenetreesclient = require("gramene-trees-client");
12
12
  var $gXNCa$gramenetaxonomywithgenomes = require("gramene-taxonomy-with-genomes");
13
13
  var $gXNCa$reactga4 = require("react-ga4");
14
+ var $gXNCa$moneyclip = require("money-clip");
14
15
  var $gXNCa$reactbootstrap = require("react-bootstrap");
15
16
  var $gXNCa$reactswitch = require("react-switch");
16
17
  var $gXNCa$reacticonsio5 = require("react-icons/io5");
@@ -1456,6 +1457,12 @@ const $24971af0a229e0e3$var$grameneViews = {
1456
1457
  show: 'off',
1457
1458
  shouldScroll: false
1458
1459
  },
1460
+ {
1461
+ id: 'gsea',
1462
+ name: 'Gene set enrichment',
1463
+ show: 'off',
1464
+ shouldScroll: false
1465
+ },
1459
1466
  {
1460
1467
  id: 'userLists',
1461
1468
  name: 'User Gene Lists',
@@ -1551,7 +1558,9 @@ const $24971af0a229e0e3$var$grameneViews = {
1551
1558
  const resultDependentIds = new Set([
1552
1559
  'taxonomy',
1553
1560
  'list',
1554
- 'export'
1561
+ 'export',
1562
+ 'exprViz',
1563
+ 'gsea'
1555
1564
  ]);
1556
1565
  const autoDisable = numFound === 0 || !hasFilters;
1557
1566
  const hasFirebase = !!(config && config.firebaseConfig);
@@ -1599,6 +1608,16 @@ const $24971af0a229e0e3$var$grameneViews = {
1599
1608
  var $24971af0a229e0e3$export$2e2bcd8739ae039 = $24971af0a229e0e3$var$grameneViews;
1600
1609
 
1601
1610
 
1611
+
1612
+ // Dedicated IndexedDB store for the full pathway corpus, persisted
1613
+ // indefinitely (default maxAge of `Infinity`). The pathway set is small
1614
+ // and stable enough to keep around across sessions, so we bulk-load it
1615
+ // once with `?rows=-1` and reuse it instead of issuing per-id requests.
1616
+ const $671312b287158a8a$var$pathwayCache = (0, $gXNCa$moneyclip.getConfiguredCache)({
1617
+ version: 1,
1618
+ name: 'gramene_pathways'
1619
+ });
1620
+ let $671312b287158a8a$var$pathwaysBulkPromise = null;
1602
1621
  const $671312b287158a8a$var$grameneDocs = {
1603
1622
  name: 'grameneDocs',
1604
1623
  getReducer: ()=>{
@@ -1803,26 +1822,37 @@ const $671312b287158a8a$var$grameneDocs = {
1803
1822
  });
1804
1823
  }
1805
1824
  },
1806
- doRequestGramenePathways: (ids)=>({ dispatch: dispatch, store: store })=>{
1807
- const pathways = store.selectGramenePathways();
1808
- let newIds = ids.filter((id)=>!pathways.hasOwnProperty(id));
1809
- if (newIds) {
1810
- dispatch({
1811
- type: 'GRAMENE_PATHWAYS_REQUESTED',
1812
- payload: newIds
1813
- });
1814
- if (newIds.length === 1) newIds.push(0);
1815
- fetch(`${store.selectGrameneAPI()}/pathways?idList=${newIds.join(',')}`).then((res)=>res.json()).then((res)=>{
1816
- let pathways = {};
1817
- res.forEach((p)=>{
1818
- pathways[p._id] = p;
1825
+ // Signature kept for backward compatibility; the `ids` argument is
1826
+ // ignored. On first call we bulk-load every pathway record from the
1827
+ // dedicated IndexedDB cache (or `${api}/pathways?rows=-1` on miss),
1828
+ // dispatch a single GRAMENE_PATHWAYS_RECEIVED, and short-circuit all
1829
+ // subsequent calls.
1830
+ doRequestGramenePathways: (_ids)=>({ dispatch: dispatch, store: store })=>{
1831
+ if ($671312b287158a8a$var$pathwaysBulkPromise) return $671312b287158a8a$var$pathwaysBulkPromise;
1832
+ $671312b287158a8a$var$pathwaysBulkPromise = $671312b287158a8a$var$pathwayCache.get('all').then((cached)=>{
1833
+ if (cached) {
1834
+ dispatch({
1835
+ type: 'GRAMENE_PATHWAYS_RECEIVED',
1836
+ payload: cached
1837
+ });
1838
+ return;
1839
+ }
1840
+ return fetch(`${store.selectGrameneAPI()}/pathways?rows=-1`).then((res)=>res.json()).then((res)=>{
1841
+ const pathways = {};
1842
+ (res || []).forEach((p)=>{
1843
+ if (p && p._id != null) pathways[p._id] = p;
1819
1844
  });
1820
1845
  dispatch({
1821
1846
  type: 'GRAMENE_PATHWAYS_RECEIVED',
1822
1847
  payload: pathways
1823
1848
  });
1849
+ $671312b287158a8a$var$pathwayCache.set('all', pathways).catch((e)=>console.warn('Failed to cache pathways', e));
1824
1850
  });
1825
- }
1851
+ }).catch((err)=>{
1852
+ console.error('Failed to load pathways', err);
1853
+ $671312b287158a8a$var$pathwaysBulkPromise = null; // allow retry on next call
1854
+ });
1855
+ return $671312b287158a8a$var$pathwaysBulkPromise;
1826
1856
  },
1827
1857
  doRequestExpressionStudies: (id)=>({ dispatch: dispatch, store: store })=>{
1828
1858
  fetch(`${store.selectGrameneAPI()}/experiments?rows=-1`).then((res)=>res.json()).then((res)=>{
@@ -3765,13 +3795,21 @@ const $1508f5a42be6e7b5$var$exporter = {
3765
3795
  var $1508f5a42be6e7b5$export$2e2bcd8739ae039 = $1508f5a42be6e7b5$var$exporter;
3766
3796
 
3767
3797
 
3798
+
3768
3799
  const $c921a0d2b34aadb6$var$ONT_KEYS = [
3769
3800
  'GO',
3770
3801
  'PO',
3771
3802
  'TO',
3772
3803
  'domains'
3773
3804
  ];
3774
- const $c921a0d2b34aadb6$var$BATCH_SIZE = 200;
3805
+ // Dedicated IndexedDB store for ontology records. The full term set per
3806
+ // ontology is stable enough to keep around indefinitely (default maxAge of
3807
+ // `Infinity`), and we don't want it to share the short TTL of the app-wide
3808
+ // cache configured in demo.js.
3809
+ const $c921a0d2b34aadb6$var$cache = (0, $gXNCa$moneyclip.getConfiguredCache)({
3810
+ version: 1,
3811
+ name: 'gramene_ontologies'
3812
+ });
3775
3813
  const $c921a0d2b34aadb6$var$inflight = {};
3776
3814
  const $c921a0d2b34aadb6$var$ontologies = {
3777
3815
  name: 'ontologies',
@@ -3780,95 +3818,64 @@ const $c921a0d2b34aadb6$var$ontologies = {
3780
3818
  GO: {},
3781
3819
  PO: {},
3782
3820
  TO: {},
3783
- domains: {}
3821
+ domains: {},
3822
+ loaded: {}
3784
3823
  };
3785
3824
  return (state = initialState, { type: type, payload: payload })=>{
3786
3825
  switch(type){
3787
- case 'ONTOLOGY_RECORDS_REQUESTED':
3788
- {
3789
- const { key: key, ids: ids } = payload;
3790
- const bucket = {
3791
- ...state[key]
3792
- };
3793
- for (const id of ids)if (!bucket.hasOwnProperty(id)) bucket[id] = null;
3794
- return {
3795
- ...state,
3796
- [key]: bucket
3797
- };
3798
- }
3799
- case 'ONTOLOGY_RECORDS_RECEIVED':
3800
- {
3801
- const { key: key, records: records } = payload;
3802
- return {
3803
- ...state,
3804
- [key]: {
3805
- ...state[key],
3806
- ...records
3807
- }
3808
- };
3809
- }
3826
+ case 'ONTOLOGY_BULK_LOADED':
3827
+ return {
3828
+ ...state,
3829
+ [payload.key]: payload.records,
3830
+ loaded: {
3831
+ ...state.loaded,
3832
+ [payload.key]: true
3833
+ }
3834
+ };
3810
3835
  default:
3811
3836
  return state;
3812
3837
  }
3813
3838
  };
3814
3839
  },
3815
- doEnsureOntologyRecords: (key, ids)=>({ dispatch: dispatch, store: store })=>{
3840
+ // Signature kept for backward compatibility the `ids` argument is
3841
+ // ignored. On first call per ontology key we load the full term set
3842
+ // (cache hit if available, otherwise `${api}/${key}?rows=-1`) and
3843
+ // dispatch a single bulk load. Subsequent calls are no-ops.
3844
+ doEnsureOntologyRecords: (key, _ids)=>({ dispatch: dispatch, store: store })=>{
3816
3845
  if (!$c921a0d2b34aadb6$var$ONT_KEYS.includes(key)) return Promise.resolve();
3817
- if (!Array.isArray(ids) || ids.length === 0) return Promise.resolve();
3818
- const existing = store.selectOntologies()[key] || {};
3819
- const missing = [];
3820
- for (const id of ids){
3821
- if (id == null) continue;
3822
- const idNum = +id;
3823
- if (!existing.hasOwnProperty(idNum) && !($c921a0d2b34aadb6$var$inflight[key] && $c921a0d2b34aadb6$var$inflight[key].has(idNum))) missing.push(idNum);
3824
- }
3825
- if (missing.length === 0) return Promise.resolve();
3826
- if (!$c921a0d2b34aadb6$var$inflight[key]) $c921a0d2b34aadb6$var$inflight[key] = new Set();
3827
- for (const id of missing)$c921a0d2b34aadb6$var$inflight[key].add(id);
3828
- dispatch({
3829
- type: 'ONTOLOGY_RECORDS_REQUESTED',
3830
- payload: {
3831
- key: key,
3832
- ids: missing
3833
- }
3834
- });
3835
- const api = store.selectGrameneAPI();
3836
- const batches = [];
3837
- for(let i = 0; i < missing.length; i += $c921a0d2b34aadb6$var$BATCH_SIZE)batches.push(missing.slice(i, i + $c921a0d2b34aadb6$var$BATCH_SIZE));
3838
- const fetchBatch = (batch)=>{
3839
- const idList = batch.length === 1 ? `${batch[0]},0` : batch.join(',');
3840
- return fetch(`${api}/${key}?idList=${idList}&rows=${batch.length + 1}`).then((r)=>r.json()).then((docs)=>{
3841
- const records = {};
3842
- for (const d of docs || [])if (d && d._id != null) records[d._id] = d;
3843
- for (const id of batch)if (!records.hasOwnProperty(id)) records[id] = {
3844
- _id: id,
3845
- missing: true
3846
- };
3846
+ const state = store.selectOntologies();
3847
+ if (state.loaded && state.loaded[key]) return Promise.resolve();
3848
+ if ($c921a0d2b34aadb6$var$inflight[key]) return $c921a0d2b34aadb6$var$inflight[key];
3849
+ $c921a0d2b34aadb6$var$inflight[key] = $c921a0d2b34aadb6$var$cache.get(key).then((cached)=>{
3850
+ if (cached) {
3847
3851
  dispatch({
3848
- type: 'ONTOLOGY_RECORDS_RECEIVED',
3852
+ type: 'ONTOLOGY_BULK_LOADED',
3849
3853
  payload: {
3850
3854
  key: key,
3851
- records: records
3855
+ records: cached
3852
3856
  }
3853
3857
  });
3854
- }).catch(()=>{
3858
+ return;
3859
+ }
3860
+ const api = store.selectGrameneAPI();
3861
+ return fetch(`${api}/${key}?rows=-1`).then((r)=>r.json()).then((docs)=>{
3855
3862
  const records = {};
3856
- for (const id of batch)records[id] = {
3857
- _id: id,
3858
- missing: true
3859
- };
3863
+ for (const d of docs || [])if (d && d._id != null) records[d._id] = d;
3860
3864
  dispatch({
3861
- type: 'ONTOLOGY_RECORDS_RECEIVED',
3865
+ type: 'ONTOLOGY_BULK_LOADED',
3862
3866
  payload: {
3863
3867
  key: key,
3864
3868
  records: records
3865
3869
  }
3866
3870
  });
3867
- }).finally(()=>{
3868
- for (const id of batch)$c921a0d2b34aadb6$var$inflight[key].delete(id);
3871
+ $c921a0d2b34aadb6$var$cache.set(key, records).catch((e)=>console.warn('Failed to cache ontology', key, e));
3869
3872
  });
3870
- };
3871
- return Promise.all(batches.map(fetchBatch));
3873
+ }).catch((err)=>{
3874
+ console.error(`Failed to load ontology ${key}`, err);
3875
+ }).finally(()=>{
3876
+ delete $c921a0d2b34aadb6$var$inflight[key];
3877
+ });
3878
+ return $c921a0d2b34aadb6$var$inflight[key];
3872
3879
  },
3873
3880
  selectOntologies: (state)=>state.ontologies
3874
3881
  };
@@ -4443,185 +4450,863 @@ const $4f15cd8a7d970b18$var$exprViz = {
4443
4450
  var $4f15cd8a7d970b18$export$2e2bcd8739ae039 = $4f15cd8a7d970b18$var$exprViz;
4444
4451
 
4445
4452
 
4446
- var $5df6c55c1bef3469$export$2e2bcd8739ae039 = [
4447
- ...(0, $9d9aeaf9299e61a1$export$2e2bcd8739ae039),
4448
- (0, $671312b287158a8a$export$2e2bcd8739ae039),
4449
- (0, $af4441dd29af05df$export$2e2bcd8739ae039),
4450
- (0, $24971af0a229e0e3$export$2e2bcd8739ae039),
4451
- (0, $0d54502f6cafe273$export$2e2bcd8739ae039),
4452
- (0, $0f839422d0d8c772$export$2e2bcd8739ae039),
4453
- (0, $1508f5a42be6e7b5$export$2e2bcd8739ae039),
4454
- (0, $c921a0d2b34aadb6$export$2e2bcd8739ae039),
4455
- (0, $4f15cd8a7d970b18$export$2e2bcd8739ae039)
4456
- ];
4457
-
4458
-
4459
4453
 
4460
-
4461
-
4462
- const $2fec4872fbf7ebd2$var$Gene = ({ gene: gene })=>{
4463
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
4464
- className: "row",
4465
- children: gene.id
4466
- });
4467
- };
4468
- const $2fec4872fbf7ebd2$var$Genes = (results, rows, doChangeQuantity)=>{
4469
- if (results && results.numFound > 0) {
4470
- const moreButton = results.numFound > rows ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
4471
- onClick: (e)=>doChangeQuantity('Genes', 20),
4472
- children: "more"
4473
- }) : '';
4474
- const fewerButton = rows > 20 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
4475
- onClick: (e)=>doChangeQuantity('Genes', -20),
4476
- children: "fewer"
4477
- }) : '';
4478
- const docsToShow = results.response.docs.slice(0, rows);
4479
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
4480
- id: "Genes",
4481
- className: "container mb40 anchor",
4482
- children: [
4483
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
4484
- className: "fancy-title mb40",
4485
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h4", {
4486
- children: "Genes"
4487
- })
4488
- }),
4489
- docsToShow.map((doc, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($2fec4872fbf7ebd2$var$Gene, {
4490
- gene: doc
4491
- }, idx)),
4492
- fewerButton,
4493
- moreButton
4494
- ]
4495
- });
4454
+ // Gene Set Enrichment Analysis bundle.
4455
+ //
4456
+ // For each species tab we run:
4457
+ // foreground: q=<filters>&fq=taxon_id:<tid>&rows=0 + facet on six __ancestors fields
4458
+ // background: q=taxon_id:<tid>&rows=0 + same facets (cached forever per tid)
4459
+ //
4460
+ // Per-ontology denominators (n_ont, N_ont) come from the root term's facet
4461
+ // count, since every annotated gene carries the root in __ancestors. Roots
4462
+ // are identified once ontology records are loaded by picking the ancestor
4463
+ // (or self) with the highest background facet count — this works for
4464
+ // multi-rooted GO (BP/MF/CC) and for the rice-rooted pathway tree.
4465
+ //
4466
+ // Enrichment uses the upper-tail Fisher exact (hypergeometric); p-values
4467
+ // are corrected per ontology with Benjamini–Hochberg. The "most-specific"
4468
+ // collapse is applied AFTER BH so a parent term that's significant on its
4469
+ // own is preserved even when none of its children clear the cutoff.
4470
+ const $0736cb5a41609896$var$ONTOLOGIES = [
4471
+ {
4472
+ key: 'GO',
4473
+ field: 'GO__ancestors',
4474
+ label: 'Gene Ontology',
4475
+ bucket: 'GO'
4476
+ },
4477
+ {
4478
+ key: 'PO',
4479
+ field: 'PO__ancestors',
4480
+ label: 'Plant Ontology',
4481
+ bucket: 'PO'
4482
+ },
4483
+ {
4484
+ key: 'TO',
4485
+ field: 'TO__ancestors',
4486
+ label: 'Trait Ontology',
4487
+ bucket: 'TO'
4488
+ },
4489
+ {
4490
+ key: 'QTL_TO',
4491
+ field: 'QTL_TO__ancestors',
4492
+ label: 'QTL Traits (TO)',
4493
+ bucket: 'TO'
4494
+ },
4495
+ {
4496
+ key: 'domains',
4497
+ field: 'domains__ancestors',
4498
+ label: 'InterPro Domains',
4499
+ bucket: 'domains'
4500
+ },
4501
+ {
4502
+ key: 'pathways',
4503
+ field: 'pathways__ancestors',
4504
+ label: 'Pathways',
4505
+ bucket: null
4496
4506
  }
4497
- };
4498
- const $2fec4872fbf7ebd2$var$Pathway = ({ pathway: pathway })=>{
4499
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
4500
- className: "row",
4501
- children: pathway.name
4502
- });
4503
- };
4504
- const $2fec4872fbf7ebd2$var$Pathways = (results)=>{
4505
- if (results && results.numFound > 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
4506
- id: "Pathways",
4507
- className: "container mb40 anchor",
4508
- children: [
4509
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
4510
- className: "fancy-title",
4511
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h4", {
4512
- children: "Pathways"
4513
- })
4514
- }),
4515
- results.pathways.map((doc, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($2fec4872fbf7ebd2$var$Pathway, {
4516
- pathway: doc
4517
- }, idx))
4518
- ]
4519
- });
4520
- };
4521
- const $2fec4872fbf7ebd2$var$Domain = ({ domain: domain })=>{
4522
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
4523
- className: "row",
4524
- children: domain.id
4525
- });
4526
- };
4527
- const $2fec4872fbf7ebd2$var$Domains = (results)=>{
4528
- if (results && results.numFound > 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
4529
- id: "Domains",
4530
- className: "container mb40 anchor",
4531
- children: [
4532
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
4533
- className: "fancy-title mb40",
4534
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h4", {
4535
- children: "Domains"
4536
- })
4537
- }),
4538
- results.domains.map((doc, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($2fec4872fbf7ebd2$var$Domain, {
4539
- domain: doc
4540
- }, idx))
4541
- ]
4542
- });
4543
- };
4544
- const $2fec4872fbf7ebd2$var$Taxon = ({ taxon: taxon })=>{
4545
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
4546
- className: "row",
4547
- children: taxon.id
4548
- });
4549
- };
4550
- const $2fec4872fbf7ebd2$var$Species = (results)=>{
4551
- if (results && results.numFound > 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
4552
- id: "Species",
4553
- className: "container mb40 anchor",
4554
- children: [
4555
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
4556
- className: "fancy-title mb40",
4557
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h4", {
4558
- children: "Species"
4559
- })
4560
- }),
4561
- results.taxonomy.map((doc, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($2fec4872fbf7ebd2$var$Taxon, {
4562
- taxon: doc
4563
- }, idx))
4564
- ]
4565
- });
4566
- };
4567
- const $2fec4872fbf7ebd2$var$ResultList = ({ grameneGenes: grameneGenes, grameneDomains: grameneDomains, gramenePathways: gramenePathways, grameneTaxonomy: grameneTaxonomy, searchUI: searchUI, searchUpdated: searchUpdated, doChangeQuantity: doChangeQuantity })=>{
4568
- if (searchUI.Gramene) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
4569
- id: "gramene",
4570
- className: "row",
4571
- children: [
4572
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
4573
- className: "fancy-title pt50",
4574
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h3", {
4575
- children: "Gramene search results"
4576
- })
4577
- }),
4578
- searchUI.Genes && $2fec4872fbf7ebd2$var$Genes(grameneGenes, searchUI.rows.Genes, doChangeQuantity),
4579
- searchUI.Domains && $2fec4872fbf7ebd2$var$Domains(grameneDomains),
4580
- searchUI.Pathways && $2fec4872fbf7ebd2$var$Pathways(gramenePathways),
4581
- searchUI.Species && $2fec4872fbf7ebd2$var$Species(grameneTaxonomy)
4582
- ]
4583
- });
4584
- else return null;
4585
- };
4586
- var $2fec4872fbf7ebd2$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneGenes', 'selectGrameneDomains', 'selectGramenePathways', 'selectGrameneTaxonomy', 'selectSearchUI', 'selectSearchUpdated', 'doChangeQuantity', $2fec4872fbf7ebd2$var$ResultList);
4587
-
4588
-
4589
-
4590
-
4591
-
4592
- const $27617dbc24e7faf0$var$getStatus = (cat, results, isChecked, toggle)=>{
4593
- const tally = results ? results.numFound : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
4594
- src: "/static/images/dna_spinner.svg"
4595
- });
4596
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
4597
- className: "category-leaf",
4598
- children: [
4599
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("input", {
4600
- type: "checkbox",
4601
- checked: isChecked,
4602
- onChange: (e)=>toggle(cat)
4603
- }),
4604
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("a", {
4605
- "data-scroll": true,
4606
- href: `#${cat}`,
4607
- className: "nav-link active",
4608
- children: [
4609
- cat,
4610
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
4611
- style: {
4612
- float: "right"
4507
+ ];
4508
+ const $0736cb5a41609896$var$FACET_PARAMS = $0736cb5a41609896$var$ONTOLOGIES.map((o)=>`facet.field=${encodeURIComponent(`{!facet.limit=10000 facet.mincount=1 key=${o.key}}${o.field}`)}`).join('&');
4509
+ const $0736cb5a41609896$var$fgPending = {};
4510
+ const $0736cb5a41609896$var$bgPending = {};
4511
+ function $0736cb5a41609896$var$fgSig(q, taxon) {
4512
+ return `${q}|${taxon}`;
4513
+ }
4514
+ function $0736cb5a41609896$var$bgSig(taxon) {
4515
+ return `bg|${taxon}`;
4516
+ }
4517
+ function $0736cb5a41609896$var$parseFacets(json) {
4518
+ const out = {};
4519
+ const ff = json && json.facet_counts && json.facet_counts.facet_fields || {};
4520
+ for (const o of $0736cb5a41609896$var$ONTOLOGIES){
4521
+ const arr = ff[o.key] || [];
4522
+ const map = {};
4523
+ for(let i = 0; i < arr.length; i += 2)map[+arr[i]] = +arr[i + 1];
4524
+ out[o.key] = map;
4525
+ }
4526
+ return out;
4527
+ }
4528
+ const $0736cb5a41609896$var$gsea = {
4529
+ name: 'gsea',
4530
+ // Background facet counts depend only on the species — they're invariant
4531
+ // across filter changes and across sessions, so we persist whenever a bg
4532
+ // fetch completes. Foreground state piggybacks on the same write but is
4533
+ // self-invalidated by the signature check in the reactor.
4534
+ persistActions: [
4535
+ 'GSEA_BG_SUCCEEDED'
4536
+ ],
4537
+ getReducer: ()=>{
4538
+ const initialState = {
4539
+ activeTaxon: null,
4540
+ byTaxon: {},
4541
+ ui: {
4542
+ pAdjCutoff: 0.05,
4543
+ minK: 2,
4544
+ mostSpecific: true,
4545
+ ontology: 'all',
4546
+ search: ''
4547
+ }
4548
+ };
4549
+ function ensureTaxon(state, tid) {
4550
+ if (state.byTaxon[tid]) return state;
4551
+ return {
4552
+ ...state,
4553
+ byTaxon: {
4554
+ ...state.byTaxon,
4555
+ [tid]: {
4556
+ fg: {
4557
+ status: 'idle',
4558
+ signature: null,
4559
+ requestId: 0,
4560
+ terms: null,
4561
+ numFound: 0,
4562
+ error: null
4613
4563
  },
4614
- children: tally
4615
- })
4616
- ]
4617
- })
4618
- ]
4619
- });
4620
- };
4621
- const $27617dbc24e7faf0$var$ResultSummary = ({ grameneGenes: grameneGenes, gramenePathways: gramenePathways, grameneDomains: grameneDomains, grameneTaxonomy: grameneTaxonomy, searchUI: searchUI, searchUpdated: searchUpdated, doToggleCategory: doToggleCategory })=>{
4622
- const status = grameneGenes ? grameneGenes.numFound : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
4623
- src: "/static/images/dna_spinner.svg"
4624
- });
4564
+ bg: {
4565
+ status: 'idle',
4566
+ signature: null,
4567
+ requestId: 0,
4568
+ terms: null,
4569
+ numFound: 0,
4570
+ error: null
4571
+ }
4572
+ }
4573
+ }
4574
+ };
4575
+ }
4576
+ return (state = initialState, { type: type, payload: payload })=>{
4577
+ switch(type){
4578
+ case 'GSEA_ACTIVE_TAXON_SET':
4579
+ return {
4580
+ ...ensureTaxon(state, payload),
4581
+ activeTaxon: payload
4582
+ };
4583
+ case 'GSEA_UI_SET':
4584
+ return {
4585
+ ...state,
4586
+ ui: {
4587
+ ...state.ui,
4588
+ ...payload || {}
4589
+ }
4590
+ };
4591
+ case 'GSEA_FG_STARTED':
4592
+ {
4593
+ const next = ensureTaxon(state, payload.taxon);
4594
+ const t = next.byTaxon[payload.taxon];
4595
+ return {
4596
+ ...next,
4597
+ byTaxon: {
4598
+ ...next.byTaxon,
4599
+ [payload.taxon]: {
4600
+ ...t,
4601
+ fg: {
4602
+ status: 'loading',
4603
+ signature: payload.signature,
4604
+ requestId: payload.requestId,
4605
+ terms: t.fg.terms,
4606
+ numFound: t.fg.numFound,
4607
+ error: null
4608
+ }
4609
+ }
4610
+ }
4611
+ };
4612
+ }
4613
+ case 'GSEA_FG_SUCCEEDED':
4614
+ {
4615
+ const t = state.byTaxon[payload.taxon];
4616
+ if (!t || payload.requestId !== t.fg.requestId) return state;
4617
+ return {
4618
+ ...state,
4619
+ byTaxon: {
4620
+ ...state.byTaxon,
4621
+ [payload.taxon]: {
4622
+ ...t,
4623
+ fg: {
4624
+ ...t.fg,
4625
+ status: 'ready',
4626
+ terms: payload.terms,
4627
+ numFound: payload.numFound
4628
+ }
4629
+ }
4630
+ }
4631
+ };
4632
+ }
4633
+ case 'GSEA_FG_FAILED':
4634
+ {
4635
+ const t = state.byTaxon[payload.taxon];
4636
+ if (!t || payload.requestId !== t.fg.requestId) return state;
4637
+ return {
4638
+ ...state,
4639
+ byTaxon: {
4640
+ ...state.byTaxon,
4641
+ [payload.taxon]: {
4642
+ ...t,
4643
+ fg: {
4644
+ ...t.fg,
4645
+ status: 'error',
4646
+ error: payload.error
4647
+ }
4648
+ }
4649
+ }
4650
+ };
4651
+ }
4652
+ case 'GSEA_BG_STARTED':
4653
+ {
4654
+ const next = ensureTaxon(state, payload.taxon);
4655
+ const t = next.byTaxon[payload.taxon];
4656
+ return {
4657
+ ...next,
4658
+ byTaxon: {
4659
+ ...next.byTaxon,
4660
+ [payload.taxon]: {
4661
+ ...t,
4662
+ bg: {
4663
+ status: 'loading',
4664
+ signature: payload.signature,
4665
+ requestId: payload.requestId,
4666
+ terms: t.bg.terms,
4667
+ numFound: t.bg.numFound,
4668
+ error: null
4669
+ }
4670
+ }
4671
+ }
4672
+ };
4673
+ }
4674
+ case 'GSEA_BG_SUCCEEDED':
4675
+ {
4676
+ const t = state.byTaxon[payload.taxon];
4677
+ if (!t || payload.requestId !== t.bg.requestId) return state;
4678
+ return {
4679
+ ...state,
4680
+ byTaxon: {
4681
+ ...state.byTaxon,
4682
+ [payload.taxon]: {
4683
+ ...t,
4684
+ bg: {
4685
+ ...t.bg,
4686
+ status: 'ready',
4687
+ terms: payload.terms,
4688
+ numFound: payload.numFound
4689
+ }
4690
+ }
4691
+ }
4692
+ };
4693
+ }
4694
+ case 'GSEA_BG_FAILED':
4695
+ {
4696
+ const t = state.byTaxon[payload.taxon];
4697
+ if (!t || payload.requestId !== t.bg.requestId) return state;
4698
+ return {
4699
+ ...state,
4700
+ byTaxon: {
4701
+ ...state.byTaxon,
4702
+ [payload.taxon]: {
4703
+ ...t,
4704
+ bg: {
4705
+ ...t.bg,
4706
+ status: 'error',
4707
+ error: payload.error
4708
+ }
4709
+ }
4710
+ }
4711
+ };
4712
+ }
4713
+ case 'GRAMENE_SEARCH_CLEARED':
4714
+ {
4715
+ // Filters changed — invalidate foreground but keep background
4716
+ // (it depends only on taxon).
4717
+ const newByTaxon = {};
4718
+ for (const tid of Object.keys(state.byTaxon)){
4719
+ const t = state.byTaxon[tid];
4720
+ newByTaxon[tid] = {
4721
+ ...t,
4722
+ fg: {
4723
+ status: 'idle',
4724
+ signature: null,
4725
+ requestId: 0,
4726
+ terms: null,
4727
+ numFound: 0,
4728
+ error: null
4729
+ }
4730
+ };
4731
+ }
4732
+ return {
4733
+ ...state,
4734
+ byTaxon: newByTaxon
4735
+ };
4736
+ }
4737
+ default:
4738
+ return state;
4739
+ }
4740
+ };
4741
+ },
4742
+ doSetGseaActiveTaxon: (tid)=>({ dispatch: dispatch })=>dispatch({
4743
+ type: 'GSEA_ACTIVE_TAXON_SET',
4744
+ payload: tid
4745
+ }),
4746
+ doSetGseaUI: (patch)=>({ dispatch: dispatch })=>dispatch({
4747
+ type: 'GSEA_UI_SET',
4748
+ payload: patch
4749
+ }),
4750
+ doFetchGseaForeground: (taxon)=>({ dispatch: dispatch, store: store })=>{
4751
+ const q = store.selectGrameneFiltersQueryString();
4752
+ const signature = $0736cb5a41609896$var$fgSig(q, taxon);
4753
+ const state = store.selectGsea();
4754
+ const t = state.byTaxon[taxon];
4755
+ if (t && t.fg.signature === signature && (t.fg.status === 'loading' || t.fg.status === 'ready')) return;
4756
+ const requestId = $0736cb5a41609896$var$fgPending[taxon] = ($0736cb5a41609896$var$fgPending[taxon] || 0) + 1;
4757
+ dispatch({
4758
+ type: 'GSEA_FG_STARTED',
4759
+ payload: {
4760
+ taxon: taxon,
4761
+ signature: signature,
4762
+ requestId: requestId
4763
+ }
4764
+ });
4765
+ const api = store.selectGrameneAPI();
4766
+ const url = `${api}/search?q=${q}&fq=taxon_id:${taxon}&rows=0&facet=true&${$0736cb5a41609896$var$FACET_PARAMS}`;
4767
+ fetch(url).then((r)=>r.json()).then((json)=>{
4768
+ if (requestId !== $0736cb5a41609896$var$fgPending[taxon]) return;
4769
+ const terms = $0736cb5a41609896$var$parseFacets(json);
4770
+ const numFound = json && json.response && json.response.numFound || 0;
4771
+ dispatch({
4772
+ type: 'GSEA_FG_SUCCEEDED',
4773
+ payload: {
4774
+ taxon: taxon,
4775
+ requestId: requestId,
4776
+ terms: terms,
4777
+ numFound: numFound
4778
+ }
4779
+ });
4780
+ store.doEnsureGseaTermRecords(taxon);
4781
+ }).catch((err)=>{
4782
+ if (requestId !== $0736cb5a41609896$var$fgPending[taxon]) return;
4783
+ dispatch({
4784
+ type: 'GSEA_FG_FAILED',
4785
+ payload: {
4786
+ taxon: taxon,
4787
+ requestId: requestId,
4788
+ error: String(err)
4789
+ }
4790
+ });
4791
+ });
4792
+ },
4793
+ doFetchGseaBackground: (taxon)=>({ dispatch: dispatch, store: store })=>{
4794
+ const signature = $0736cb5a41609896$var$bgSig(taxon);
4795
+ const state = store.selectGsea();
4796
+ const t = state.byTaxon[taxon];
4797
+ if (t && t.bg.signature === signature && (t.bg.status === 'loading' || t.bg.status === 'ready')) return;
4798
+ const requestId = $0736cb5a41609896$var$bgPending[taxon] = ($0736cb5a41609896$var$bgPending[taxon] || 0) + 1;
4799
+ dispatch({
4800
+ type: 'GSEA_BG_STARTED',
4801
+ payload: {
4802
+ taxon: taxon,
4803
+ signature: signature,
4804
+ requestId: requestId
4805
+ }
4806
+ });
4807
+ const api = store.selectGrameneAPI();
4808
+ const url = `${api}/search?q=taxon_id:${taxon}&rows=0&facet=true&${$0736cb5a41609896$var$FACET_PARAMS}`;
4809
+ fetch(url).then((r)=>r.json()).then((json)=>{
4810
+ if (requestId !== $0736cb5a41609896$var$bgPending[taxon]) return;
4811
+ const terms = $0736cb5a41609896$var$parseFacets(json);
4812
+ const numFound = json && json.response && json.response.numFound || 0;
4813
+ dispatch({
4814
+ type: 'GSEA_BG_SUCCEEDED',
4815
+ payload: {
4816
+ taxon: taxon,
4817
+ requestId: requestId,
4818
+ terms: terms,
4819
+ numFound: numFound
4820
+ }
4821
+ });
4822
+ store.doEnsureGseaTermRecords(taxon);
4823
+ }).catch((err)=>{
4824
+ if (requestId !== $0736cb5a41609896$var$bgPending[taxon]) return;
4825
+ dispatch({
4826
+ type: 'GSEA_BG_FAILED',
4827
+ payload: {
4828
+ taxon: taxon,
4829
+ requestId: requestId,
4830
+ error: String(err)
4831
+ }
4832
+ });
4833
+ });
4834
+ },
4835
+ doEnsureGseaTermRecords: (taxon)=>({ store: store })=>{
4836
+ const state = store.selectGsea();
4837
+ if (!state.byTaxon[taxon]) return;
4838
+ // The ontologies and pathways bundles bulk-load + persist on first
4839
+ // request; we just need to nudge each one once.
4840
+ const buckets = new Set();
4841
+ for (const o of $0736cb5a41609896$var$ONTOLOGIES)if (o.bucket) buckets.add(o.bucket);
4842
+ for (const k of buckets)store.doEnsureOntologyRecords(k);
4843
+ if (store.doRequestGramenePathways) store.doRequestGramenePathways();
4844
+ },
4845
+ reactGseaFetch: (0, $gXNCa$reduxbundler.createSelector)('selectGsea', 'selectGrameneFiltersStatus', 'selectGrameneFiltersQueryString', 'selectGrameneViewsOn', (gs, fStatus, q, viewsOn)=>{
4846
+ if (!viewsOn || !viewsOn.has('gsea')) return;
4847
+ if (fStatus === 'init') return;
4848
+ const tid = gs.activeTaxon;
4849
+ if (!tid) return;
4850
+ const t = gs.byTaxon[tid];
4851
+ if (!t) return;
4852
+ const sig = $0736cb5a41609896$var$fgSig(q, tid);
4853
+ // A 'loading' status from a rehydrated state with no live request is
4854
+ // treated as idle — otherwise we'd deadlock waiting on a fetch that
4855
+ // ended when the previous tab closed.
4856
+ const fgInFlight = t.fg.status === 'loading' && ($0736cb5a41609896$var$fgPending[tid] || 0) === t.fg.requestId && t.fg.requestId > 0;
4857
+ if (t.fg.signature !== sig && !fgInFlight) return {
4858
+ actionCreator: 'doFetchGseaForeground',
4859
+ args: [
4860
+ tid
4861
+ ]
4862
+ };
4863
+ const bgInFlight = t.bg.status === 'loading' && ($0736cb5a41609896$var$bgPending[tid] || 0) === t.bg.requestId && t.bg.requestId > 0;
4864
+ if (t.bg.status !== 'ready' && !bgInFlight) return {
4865
+ actionCreator: 'doFetchGseaBackground',
4866
+ args: [
4867
+ tid
4868
+ ]
4869
+ };
4870
+ }),
4871
+ selectGsea: (state)=>state.gsea,
4872
+ selectGseaUI: (state)=>state.gsea.ui,
4873
+ selectGseaOntologyDefs: ()=>$0736cb5a41609896$var$ONTOLOGIES,
4874
+ selectGseaResults: (0, $gXNCa$reduxbundler.createSelector)('selectGsea', 'selectOntologies', 'selectGramenePathways', (gs, ontoBuckets, pathwayDocs)=>{
4875
+ const tid = gs.activeTaxon;
4876
+ if (!tid) return null;
4877
+ const t = gs.byTaxon[tid];
4878
+ if (!t || !t.fg.terms || !t.bg.terms) return null;
4879
+ const ui = gs.ui;
4880
+ const out = {};
4881
+ for (const o of $0736cb5a41609896$var$ONTOLOGIES){
4882
+ const fg = t.fg.terms[o.key] || {};
4883
+ const bg = t.bg.terms[o.key] || {};
4884
+ const recs = o.key === 'pathways' ? pathwayDocs || {} : ontoBuckets && ontoBuckets[o.bucket] || {};
4885
+ // Forest-fallback denominators: when a term is itself a forest root
4886
+ // (no parents in `recs` — common for InterPro), root finding returns
4887
+ // the term and we'd get fold=1 by construction. Use the maximum
4888
+ // counts across the ontology as a proxy for "annotated in this
4889
+ // ontology" instead. For ontologies with a true synthetic root,
4890
+ // these maxima equal the root counts and the answer is unchanged.
4891
+ let maxFg = 0, maxBg = 0;
4892
+ for (const idStr of Object.keys(bg)){
4893
+ const v = bg[idStr];
4894
+ if (v > maxBg) maxBg = v;
4895
+ }
4896
+ for (const idStr of Object.keys(fg)){
4897
+ const v = fg[idStr];
4898
+ if (v > maxFg) maxFg = v;
4899
+ }
4900
+ const rootCache = {};
4901
+ const rootOf = (id)=>{
4902
+ if (rootCache.hasOwnProperty(id)) return rootCache[id];
4903
+ const r = $0736cb5a41609896$var$findRoot(o.key, id, recs, bg);
4904
+ rootCache[id] = r;
4905
+ return r;
4906
+ };
4907
+ const rows = [];
4908
+ for (const idStr of Object.keys(fg)){
4909
+ const id = +idStr;
4910
+ const k = fg[id];
4911
+ const K = bg[id] || 0;
4912
+ if (K < k) continue; // bg should always be >= fg
4913
+ if (k < ui.minK) continue;
4914
+ const termRec = recs && recs[id];
4915
+ if (termRec && (termRec.is_obsolete || termRec.obsolete)) continue;
4916
+ const root = rootOf(id);
4917
+ // If the "root" is the term itself, the term is a forest root in
4918
+ // this ontology — fall back to ontology-wide maxima.
4919
+ const fellBack = +root === id;
4920
+ const n = fellBack ? maxFg : root != null && fg[root] ? fg[root] : k;
4921
+ const N = fellBack ? maxBg : root != null && bg[root] ? bg[root] : K;
4922
+ if (n <= 0 || N <= 0) continue;
4923
+ if (k > n || K > N) continue;
4924
+ const fold = k / n / (K / N);
4925
+ const p = $0736cb5a41609896$var$fisherUpperTail(k, n, K, N);
4926
+ rows.push({
4927
+ ontology: o.key,
4928
+ ontology_label: o.label,
4929
+ term_id: id,
4930
+ field: o.field,
4931
+ k: k,
4932
+ n: n,
4933
+ K: K,
4934
+ N: N,
4935
+ fold: fold,
4936
+ p: p,
4937
+ pAdj: 1,
4938
+ root: root,
4939
+ denomFallback: fellBack
4940
+ });
4941
+ }
4942
+ // GO is split into its three top-level namespaces (BP / MF / CC)
4943
+ // and each is tested as its own ontology — both BH correction and
4944
+ // most-specific collapse run within a namespace. We only split once
4945
+ // ontology records have arrived and root finding has produced
4946
+ // canonical roots; otherwise we'd see a swarm of singleton groups
4947
+ // during the brief loading window between bg landing and records
4948
+ // being fetched.
4949
+ if (o.key === 'GO' && Object.keys(recs).length > 0) {
4950
+ const byRoot = {};
4951
+ for (const r of rows){
4952
+ const k = String(r.root);
4953
+ if (!byRoot[k]) byRoot[k] = [];
4954
+ byRoot[k].push(r);
4955
+ }
4956
+ const rootKeys = Object.keys(byRoot).sort((a, b)=>{
4957
+ const na = $0736cb5a41609896$var$goRootName(recs[+a]) || a;
4958
+ const nb = $0736cb5a41609896$var$goRootName(recs[+b]) || b;
4959
+ return na.localeCompare(nb);
4960
+ });
4961
+ for (const rootKey of rootKeys){
4962
+ const rootRec = recs[+rootKey];
4963
+ const rootName = $0736cb5a41609896$var$goRootName(rootRec);
4964
+ const sectionKey = `GO:${rootKey}`;
4965
+ const sectionLabel = rootName ? `GO: ${$0736cb5a41609896$var$titleCase(rootName)}` : `GO: ${rootKey}`;
4966
+ out[sectionKey] = $0736cb5a41609896$var$finalizeBlock(byRoot[rootKey], o.key, o.field, recs, ui, sectionKey, sectionLabel);
4967
+ }
4968
+ } else out[o.key] = $0736cb5a41609896$var$finalizeBlock(rows, o.key, o.field, recs, ui, o.key, o.label);
4969
+ }
4970
+ return out;
4971
+ })
4972
+ };
4973
+ function $0736cb5a41609896$var$goRootName(rec) {
4974
+ if (!rec) return '';
4975
+ return rec.name || rec.display_name || rec.namespace || '';
4976
+ }
4977
+ function $0736cb5a41609896$var$titleCase(s) {
4978
+ return String(s).split('_').map((w)=>w ? w[0].toUpperCase() + w.slice(1) : '').join(' ');
4979
+ }
4980
+ // BH correction + most-specific collapse + metadata + final sort, returning
4981
+ // the block descriptor consumed by the view layer.
4982
+ function $0736cb5a41609896$var$finalizeBlock(rows, ontKey, ontField, recs, ui, sectionKey, sectionLabel) {
4983
+ rows.sort((a, b)=>a.p - b.p);
4984
+ const m = rows.length;
4985
+ let prev = 1;
4986
+ for(let i = m - 1; i >= 0; i--){
4987
+ const adj = Math.min(prev, rows[i].p * m / (i + 1));
4988
+ rows[i].pAdj = adj;
4989
+ prev = adj;
4990
+ }
4991
+ const passing = rows.filter((r)=>r.pAdj <= ui.pAdjCutoff);
4992
+ let display = passing;
4993
+ if (ui.mostSpecific) display = $0736cb5a41609896$var$collapseToMostSpecific(ontKey, passing, recs);
4994
+ for (const r of display){
4995
+ const rec = recs && recs[r.term_id];
4996
+ if (rec) {
4997
+ r.term_name = rec.name || rec.display_name || '';
4998
+ r.term_namespace = rec.namespace || rec.type || '';
4999
+ r.term_display_id = rec.id != null ? String(rec.id) : String(r.term_id);
5000
+ } else {
5001
+ r.term_name = '';
5002
+ r.term_namespace = '';
5003
+ r.term_display_id = String(r.term_id);
5004
+ }
5005
+ }
5006
+ display.sort((a, b)=>a.pAdj - b.pAdj || b.fold - a.fold);
5007
+ return {
5008
+ ontology: sectionKey,
5009
+ label: sectionLabel,
5010
+ field: ontField,
5011
+ tested: rows.length,
5012
+ passing: passing.length,
5013
+ rows: display
5014
+ };
5015
+ }
5016
+ // ---------- math helpers ----------
5017
+ const $0736cb5a41609896$var$LF_CACHE = [
5018
+ 0,
5019
+ 0
5020
+ ];
5021
+ function $0736cb5a41609896$var$logFactorial(n) {
5022
+ if (n < $0736cb5a41609896$var$LF_CACHE.length) return $0736cb5a41609896$var$LF_CACHE[n];
5023
+ let lf = $0736cb5a41609896$var$LF_CACHE[$0736cb5a41609896$var$LF_CACHE.length - 1];
5024
+ for(let i = $0736cb5a41609896$var$LF_CACHE.length; i <= n; i++){
5025
+ lf += Math.log(i);
5026
+ $0736cb5a41609896$var$LF_CACHE.push(lf);
5027
+ }
5028
+ return $0736cb5a41609896$var$LF_CACHE[n];
5029
+ }
5030
+ function $0736cb5a41609896$var$logChoose(n, k) {
5031
+ if (k < 0 || k > n) return -Infinity;
5032
+ return $0736cb5a41609896$var$logFactorial(n) - $0736cb5a41609896$var$logFactorial(k) - $0736cb5a41609896$var$logFactorial(n - k);
5033
+ }
5034
+ function $0736cb5a41609896$var$logHypergeom(x, n, K, N) {
5035
+ return $0736cb5a41609896$var$logChoose(K, x) + $0736cb5a41609896$var$logChoose(N - K, n - x) - $0736cb5a41609896$var$logChoose(N, n);
5036
+ }
5037
+ function $0736cb5a41609896$var$logSumExp(a, b) {
5038
+ if (a === -Infinity) return b;
5039
+ if (b === -Infinity) return a;
5040
+ const m = Math.max(a, b);
5041
+ return m + Math.log(Math.exp(a - m) + Math.exp(b - m));
5042
+ }
5043
+ function $0736cb5a41609896$var$fisherUpperTail(k, n, K, N) {
5044
+ const upper = Math.min(n, K);
5045
+ let logP = -Infinity;
5046
+ for(let x = k; x <= upper; x++)logP = $0736cb5a41609896$var$logSumExp(logP, $0736cb5a41609896$var$logHypergeom(x, n, K, N));
5047
+ const p = Math.exp(logP);
5048
+ if (!isFinite(p)) return 1;
5049
+ return Math.min(1, Math.max(0, p));
5050
+ }
5051
+ // ---------- ontology graph helpers ----------
5052
+ // Pick the "root" for a term as the ancestor (or self) with the highest
5053
+ // background facet count. The true root has every annotated gene under it,
5054
+ // so this selects BP/MF/CC for GO terms automatically and the rice root
5055
+ // for pathways without needing per-ontology hardcoded ids.
5056
+ function $0736cb5a41609896$var$findRoot(ontKey, id, recs, bgCounts) {
5057
+ const rec = recs && recs[id];
5058
+ const candidates = new Set([
5059
+ +id
5060
+ ]);
5061
+ if (rec) {
5062
+ if (ontKey === 'pathways') for (const k of Object.keys(rec)){
5063
+ if (k.startsWith('ancestors_') && Array.isArray(rec[k])) for (const a of rec[k])candidates.add(+a);
5064
+ }
5065
+ else if (Array.isArray(rec.ancestors)) for (const a of rec.ancestors)candidates.add(+a);
5066
+ else if (Array.isArray(rec.is_a)) {
5067
+ const stack = rec.is_a.slice();
5068
+ while(stack.length){
5069
+ const cur = +stack.pop();
5070
+ if (candidates.has(cur)) continue;
5071
+ candidates.add(cur);
5072
+ const r = recs[cur];
5073
+ if (r && Array.isArray(r.is_a)) for (const p of r.is_a)stack.push(p);
5074
+ }
5075
+ }
5076
+ }
5077
+ let best = +id;
5078
+ let bestCount = bgCounts[+id] || 0;
5079
+ for (const c of candidates){
5080
+ const cnt = bgCounts[c] || 0;
5081
+ if (cnt > bestCount) {
5082
+ bestCount = cnt;
5083
+ best = c;
5084
+ }
5085
+ }
5086
+ return best;
5087
+ }
5088
+ function $0736cb5a41609896$var$ancestorsOf(ontKey, id, recs) {
5089
+ const out = new Set();
5090
+ const rec = recs && recs[id];
5091
+ if (!rec) return out;
5092
+ if (ontKey === 'pathways') {
5093
+ for (const k of Object.keys(rec))if (k.startsWith('ancestors_') && Array.isArray(rec[k])) {
5094
+ for (const a of rec[k])if (+a !== +id) out.add(+a);
5095
+ }
5096
+ return out;
5097
+ }
5098
+ if (Array.isArray(rec.ancestors)) {
5099
+ for (const a of rec.ancestors)if (+a !== +id) out.add(+a);
5100
+ return out;
5101
+ }
5102
+ if (Array.isArray(rec.is_a)) {
5103
+ const stack = rec.is_a.slice();
5104
+ while(stack.length){
5105
+ const cur = +stack.pop();
5106
+ if (out.has(cur)) continue;
5107
+ out.add(cur);
5108
+ const r = recs[cur];
5109
+ if (r && Array.isArray(r.is_a)) for (const p of r.is_a)stack.push(p);
5110
+ }
5111
+ }
5112
+ return out;
5113
+ }
5114
+ // Drop any term that is an ancestor of another term in the same passing set.
5115
+ // Applied AFTER BH so a parent term can survive when none of its children
5116
+ // pass the p_adj cutoff.
5117
+ function $0736cb5a41609896$var$collapseToMostSpecific(ontKey, rows, recs) {
5118
+ if (!rows || rows.length <= 1) return rows;
5119
+ const inSet = new Set(rows.map((r)=>+r.term_id));
5120
+ const covered = new Set();
5121
+ for (const r of rows){
5122
+ const ancs = $0736cb5a41609896$var$ancestorsOf(ontKey, +r.term_id, recs);
5123
+ for (const a of ancs)if (inSet.has(a)) covered.add(a);
5124
+ }
5125
+ return rows.filter((r)=>!covered.has(+r.term_id));
5126
+ }
5127
+ var $0736cb5a41609896$export$2e2bcd8739ae039 = $0736cb5a41609896$var$gsea;
5128
+
5129
+
5130
+ var $5df6c55c1bef3469$export$2e2bcd8739ae039 = [
5131
+ ...(0, $9d9aeaf9299e61a1$export$2e2bcd8739ae039),
5132
+ (0, $671312b287158a8a$export$2e2bcd8739ae039),
5133
+ (0, $af4441dd29af05df$export$2e2bcd8739ae039),
5134
+ (0, $24971af0a229e0e3$export$2e2bcd8739ae039),
5135
+ (0, $0d54502f6cafe273$export$2e2bcd8739ae039),
5136
+ (0, $0f839422d0d8c772$export$2e2bcd8739ae039),
5137
+ (0, $1508f5a42be6e7b5$export$2e2bcd8739ae039),
5138
+ (0, $c921a0d2b34aadb6$export$2e2bcd8739ae039),
5139
+ (0, $4f15cd8a7d970b18$export$2e2bcd8739ae039),
5140
+ (0, $0736cb5a41609896$export$2e2bcd8739ae039)
5141
+ ];
5142
+
5143
+
5144
+
5145
+
5146
+
5147
+ const $2fec4872fbf7ebd2$var$Gene = ({ gene: gene })=>{
5148
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
5149
+ className: "row",
5150
+ children: gene.id
5151
+ });
5152
+ };
5153
+ const $2fec4872fbf7ebd2$var$Genes = (results, rows, doChangeQuantity)=>{
5154
+ if (results && results.numFound > 0) {
5155
+ const moreButton = results.numFound > rows ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
5156
+ onClick: (e)=>doChangeQuantity('Genes', 20),
5157
+ children: "more"
5158
+ }) : '';
5159
+ const fewerButton = rows > 20 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
5160
+ onClick: (e)=>doChangeQuantity('Genes', -20),
5161
+ children: "fewer"
5162
+ }) : '';
5163
+ const docsToShow = results.response.docs.slice(0, rows);
5164
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
5165
+ id: "Genes",
5166
+ className: "container mb40 anchor",
5167
+ children: [
5168
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
5169
+ className: "fancy-title mb40",
5170
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h4", {
5171
+ children: "Genes"
5172
+ })
5173
+ }),
5174
+ docsToShow.map((doc, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($2fec4872fbf7ebd2$var$Gene, {
5175
+ gene: doc
5176
+ }, idx)),
5177
+ fewerButton,
5178
+ moreButton
5179
+ ]
5180
+ });
5181
+ }
5182
+ };
5183
+ const $2fec4872fbf7ebd2$var$Pathway = ({ pathway: pathway })=>{
5184
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
5185
+ className: "row",
5186
+ children: pathway.name
5187
+ });
5188
+ };
5189
+ const $2fec4872fbf7ebd2$var$Pathways = (results)=>{
5190
+ if (results && results.numFound > 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
5191
+ id: "Pathways",
5192
+ className: "container mb40 anchor",
5193
+ children: [
5194
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
5195
+ className: "fancy-title",
5196
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h4", {
5197
+ children: "Pathways"
5198
+ })
5199
+ }),
5200
+ results.pathways.map((doc, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($2fec4872fbf7ebd2$var$Pathway, {
5201
+ pathway: doc
5202
+ }, idx))
5203
+ ]
5204
+ });
5205
+ };
5206
+ const $2fec4872fbf7ebd2$var$Domain = ({ domain: domain })=>{
5207
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
5208
+ className: "row",
5209
+ children: domain.id
5210
+ });
5211
+ };
5212
+ const $2fec4872fbf7ebd2$var$Domains = (results)=>{
5213
+ if (results && results.numFound > 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
5214
+ id: "Domains",
5215
+ className: "container mb40 anchor",
5216
+ children: [
5217
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
5218
+ className: "fancy-title mb40",
5219
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h4", {
5220
+ children: "Domains"
5221
+ })
5222
+ }),
5223
+ results.domains.map((doc, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($2fec4872fbf7ebd2$var$Domain, {
5224
+ domain: doc
5225
+ }, idx))
5226
+ ]
5227
+ });
5228
+ };
5229
+ const $2fec4872fbf7ebd2$var$Taxon = ({ taxon: taxon })=>{
5230
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
5231
+ className: "row",
5232
+ children: taxon.id
5233
+ });
5234
+ };
5235
+ const $2fec4872fbf7ebd2$var$Species = (results)=>{
5236
+ if (results && results.numFound > 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
5237
+ id: "Species",
5238
+ className: "container mb40 anchor",
5239
+ children: [
5240
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
5241
+ className: "fancy-title mb40",
5242
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h4", {
5243
+ children: "Species"
5244
+ })
5245
+ }),
5246
+ results.taxonomy.map((doc, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($2fec4872fbf7ebd2$var$Taxon, {
5247
+ taxon: doc
5248
+ }, idx))
5249
+ ]
5250
+ });
5251
+ };
5252
+ const $2fec4872fbf7ebd2$var$ResultList = ({ grameneGenes: grameneGenes, grameneDomains: grameneDomains, gramenePathways: gramenePathways, grameneTaxonomy: grameneTaxonomy, searchUI: searchUI, searchUpdated: searchUpdated, doChangeQuantity: doChangeQuantity })=>{
5253
+ if (searchUI.Gramene) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
5254
+ id: "gramene",
5255
+ className: "row",
5256
+ children: [
5257
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
5258
+ className: "fancy-title pt50",
5259
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("h3", {
5260
+ children: "Gramene search results"
5261
+ })
5262
+ }),
5263
+ searchUI.Genes && $2fec4872fbf7ebd2$var$Genes(grameneGenes, searchUI.rows.Genes, doChangeQuantity),
5264
+ searchUI.Domains && $2fec4872fbf7ebd2$var$Domains(grameneDomains),
5265
+ searchUI.Pathways && $2fec4872fbf7ebd2$var$Pathways(gramenePathways),
5266
+ searchUI.Species && $2fec4872fbf7ebd2$var$Species(grameneTaxonomy)
5267
+ ]
5268
+ });
5269
+ else return null;
5270
+ };
5271
+ var $2fec4872fbf7ebd2$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneGenes', 'selectGrameneDomains', 'selectGramenePathways', 'selectGrameneTaxonomy', 'selectSearchUI', 'selectSearchUpdated', 'doChangeQuantity', $2fec4872fbf7ebd2$var$ResultList);
5272
+
5273
+
5274
+
5275
+
5276
+
5277
+ const $27617dbc24e7faf0$var$getStatus = (cat, results, isChecked, toggle)=>{
5278
+ const tally = results ? results.numFound : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
5279
+ src: "/static/images/dna_spinner.svg"
5280
+ });
5281
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
5282
+ className: "category-leaf",
5283
+ children: [
5284
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("input", {
5285
+ type: "checkbox",
5286
+ checked: isChecked,
5287
+ onChange: (e)=>toggle(cat)
5288
+ }),
5289
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("a", {
5290
+ "data-scroll": true,
5291
+ href: `#${cat}`,
5292
+ className: "nav-link active",
5293
+ children: [
5294
+ cat,
5295
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
5296
+ style: {
5297
+ float: "right"
5298
+ },
5299
+ children: tally
5300
+ })
5301
+ ]
5302
+ })
5303
+ ]
5304
+ });
5305
+ };
5306
+ const $27617dbc24e7faf0$var$ResultSummary = ({ grameneGenes: grameneGenes, gramenePathways: gramenePathways, grameneDomains: grameneDomains, grameneTaxonomy: grameneTaxonomy, searchUI: searchUI, searchUpdated: searchUpdated, doToggleCategory: doToggleCategory })=>{
5307
+ const status = grameneGenes ? grameneGenes.numFound : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
5308
+ src: "/static/images/dna_spinner.svg"
5309
+ });
4625
5310
  if (searchUI.Gramene) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
4626
5311
  className: "active category-expanded",
4627
5312
  children: [
@@ -7916,6 +8601,36 @@ const $b36244140732570a$var$examples = [
7916
8601
  ]
7917
8602
  }
7918
8603
  },
8604
+ {
8605
+ subsite: {
8606
+ maize: 1,
8607
+ sorghum: 1,
8608
+ grapevine: 1,
8609
+ rice: 1,
8610
+ main: 1
8611
+ },
8612
+ text: "Which genes are curated in the scientific literature?",
8613
+ filters: {
8614
+ status: 'init',
8615
+ rows: 20,
8616
+ operation: 'AND',
8617
+ negate: false,
8618
+ leftIdx: 0,
8619
+ rightIdx: 3,
8620
+ children: [
8621
+ {
8622
+ fq_field: 'capabilities',
8623
+ fq_value: 'pubs',
8624
+ name: 'publication',
8625
+ category: 'Curated',
8626
+ leftIdx: 1,
8627
+ rightIdx: 2,
8628
+ negate: false,
8629
+ marked: false
8630
+ }
8631
+ ]
8632
+ }
8633
+ },
7919
8634
  {
7920
8635
  subsite: {
7921
8636
  grapevine: 1
@@ -11609,340 +12324,729 @@ function $4ab64e76c1caef59$var$buildColumnDefs(fields, fieldInfo, expanded, togg
11609
12324
  {
11610
12325
  headerName: 'Study'
11611
12326
  }
11612
- ];
11613
- const titleLabels = [
11614
- {
11615
- headerName: 'Title'
12327
+ ];
12328
+ const titleLabels = [
12329
+ {
12330
+ headerName: 'Title'
12331
+ }
12332
+ ];
12333
+ if (expanded.factors) factorTypes.forEach((t, i)=>{
12334
+ studyLabels.push({
12335
+ headerName: 'Factor',
12336
+ headerClass: 'exprviz-hg-factors',
12337
+ ...i === 0 ? {
12338
+ headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
12339
+ headerGroupComponentParams: {
12340
+ onToggle: toggles.toggleFactors,
12341
+ expanded: true
12342
+ }
12343
+ } : {}
12344
+ });
12345
+ titleLabels.push({
12346
+ headerName: t,
12347
+ headerClass: 'exprviz-hg-factors'
12348
+ });
12349
+ });
12350
+ else if (factorTypes.length > 0) {
12351
+ studyLabels.push({
12352
+ headerName: 'Factors',
12353
+ headerClass: 'exprviz-hg-factors exprviz-hg-collapsed',
12354
+ headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
12355
+ headerGroupComponentParams: {
12356
+ onToggle: toggles.toggleFactors,
12357
+ expanded: false,
12358
+ suffix: `(${factorTypes.length})`
12359
+ }
12360
+ });
12361
+ titleLabels.push({
12362
+ headerName: '',
12363
+ headerClass: 'exprviz-hg-factors exprviz-hg-collapsed'
12364
+ });
12365
+ }
12366
+ if (expanded.chars) charTypes.forEach((t, i)=>{
12367
+ studyLabels.push({
12368
+ headerName: 'Characteristic',
12369
+ headerClass: 'exprviz-hg-chars',
12370
+ ...i === 0 ? {
12371
+ headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
12372
+ headerGroupComponentParams: {
12373
+ onToggle: toggles.toggleChars,
12374
+ expanded: true
12375
+ }
12376
+ } : {}
12377
+ });
12378
+ titleLabels.push({
12379
+ headerName: t,
12380
+ headerClass: 'exprviz-hg-chars'
12381
+ });
12382
+ });
12383
+ else if (charTypes.length > 0) {
12384
+ studyLabels.push({
12385
+ headerName: 'Characteristics',
12386
+ headerClass: 'exprviz-hg-chars exprviz-hg-collapsed',
12387
+ headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
12388
+ headerGroupComponentParams: {
12389
+ onToggle: toggles.toggleChars,
12390
+ expanded: false,
12391
+ suffix: `(${charTypes.length})`
12392
+ }
12393
+ });
12394
+ titleLabels.push({
12395
+ headerName: '',
12396
+ headerClass: 'exprviz-hg-chars exprviz-hg-collapsed'
12397
+ });
12398
+ }
12399
+ const idCol = $4ab64e76c1caef59$var$wrapLeafWithLabels($4ab64e76c1caef59$var$baseColDefs[0], studyLabels);
12400
+ const nameCol = $4ab64e76c1caef59$var$wrapLeafWithLabels($4ab64e76c1caef59$var$baseColDefs[1], titleLabels);
12401
+ return [
12402
+ idCol,
12403
+ nameCol,
12404
+ ...exprTopGroups
12405
+ ];
12406
+ }
12407
+ const $4ab64e76c1caef59$var$ExprTable = ({ rows: rows, fields: fields, onReorder: onReorder, studies: studies, expressionSamples: expressionSamples, onHoverRow: onHoverRow })=>{
12408
+ const fieldInfo = (0, $gXNCa$react.useMemo)(()=>$4ab64e76c1caef59$export$7b242440eb2c300d(fields, studies, expressionSamples), [
12409
+ fields,
12410
+ studies,
12411
+ expressionSamples
12412
+ ]);
12413
+ // Default: factor rows expanded, characteristic rows collapsed (per spec).
12414
+ const [expanded, setExpanded] = (0, $gXNCa$react.useState)({
12415
+ factors: true,
12416
+ chars: false
12417
+ });
12418
+ const toggleFactors = (0, $gXNCa$react.useCallback)(()=>setExpanded((e)=>({
12419
+ ...e,
12420
+ factors: !e.factors
12421
+ })), []);
12422
+ const toggleChars = (0, $gXNCa$react.useCallback)(()=>setExpanded((e)=>({
12423
+ ...e,
12424
+ chars: !e.chars
12425
+ })), []);
12426
+ const columnDefs = (0, $gXNCa$react.useMemo)(()=>$4ab64e76c1caef59$var$buildColumnDefs(fields, fieldInfo, expanded, {
12427
+ toggleFactors: toggleFactors,
12428
+ toggleChars: toggleChars
12429
+ }), [
12430
+ fields,
12431
+ fieldInfo,
12432
+ expanded,
12433
+ toggleFactors,
12434
+ toggleChars
12435
+ ]);
12436
+ const onColumnMoved = (e)=>{
12437
+ if (!onReorder || !e.finished) return;
12438
+ const allCols = e.api.getAllGridColumns ? e.api.getAllGridColumns() : e.columnApi && e.columnApi.getAllGridColumns && e.columnApi.getAllGridColumns();
12439
+ if (!allCols) return;
12440
+ const next = allCols.map((c)=>c.getColId()).filter((id)=>fields.includes(id));
12441
+ if (!$4ab64e76c1caef59$var$arraysEqual(next, fields)) onReorder(next);
12442
+ };
12443
+ if (!rows || rows.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12444
+ className: "exprviz-table-empty",
12445
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
12446
+ children: "No data loaded."
12447
+ })
12448
+ });
12449
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12450
+ className: "ag-theme-quartz exprviz-aggrid",
12451
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$aggridreact.AgGridReact), {
12452
+ rowData: rows,
12453
+ columnDefs: columnDefs,
12454
+ defaultColDef: $4ab64e76c1caef59$var$DEFAULT_COL_DEF,
12455
+ animateRows: false,
12456
+ suppressFieldDotNotation: true,
12457
+ suppressDragLeaveHidesColumns: true,
12458
+ suppressColumnVirtualisation: true,
12459
+ groupHeaderHeight: 24,
12460
+ onColumnMoved: onColumnMoved,
12461
+ onCellMouseOver: (e)=>onHoverRow && onHoverRow(e.data && e.data.id),
12462
+ onCellMouseOut: ()=>onHoverRow && onHoverRow(null)
12463
+ })
12464
+ });
12465
+ };
12466
+ var $4ab64e76c1caef59$export$2e2bcd8739ae039 = $4ab64e76c1caef59$var$ExprTable;
12467
+
12468
+
12469
+
12470
+
12471
+
12472
+ // Parallel-coordinates plot with per-axis brushing and drag-to-reorder axes.
12473
+ // - Axis labels are draggable horizontally; on drop, onReorder(newOrder) fires.
12474
+ // - Brushes on axes intersect (AND): a row is "in" only when every active brush contains it.
12475
+ // - Numeric fields use a linear or symlog scale; non-numeric values are skipped.
12476
+ const $caf32827df861c4e$var$MARGIN = {
12477
+ top: 100,
12478
+ right: 24,
12479
+ bottom: 24,
12480
+ left: 32
12481
+ };
12482
+ const $caf32827df861c4e$var$LABEL_ROTATION = -40;
12483
+ const $caf32827df861c4e$var$BRUSH_WIDTH = 16;
12484
+ function $caf32827df861c4e$var$isNumeric(v) {
12485
+ if (v == null) return false;
12486
+ if (Array.isArray(v)) return false;
12487
+ const n = +v;
12488
+ return Number.isFinite(n);
12489
+ }
12490
+ function $caf32827df861c4e$var$arraysEqual(a, b) {
12491
+ if (a.length !== b.length) return false;
12492
+ for(let i = 0; i < a.length; i++)if (a[i] !== b[i]) return false;
12493
+ return true;
12494
+ }
12495
+ // Powers-of-10 tick values spanning [lo, hi]; includes 0 if the range crosses zero.
12496
+ // Clamped to |v| >= 0.1 to keep low-magnitude tick labels from overlapping near 0.
12497
+ const $caf32827df861c4e$var$MIN_LOG_TICK = 0.1;
12498
+ function $caf32827df861c4e$var$logTickValues([lo, hi]) {
12499
+ const ticks = new Set();
12500
+ if (lo <= 0 && hi >= 0) ticks.add(0);
12501
+ if (hi >= $caf32827df861c4e$var$MIN_LOG_TICK) {
12502
+ const start = Math.max(-1, Math.floor(Math.log10(lo > 0 ? lo : $caf32827df861c4e$var$MIN_LOG_TICK)));
12503
+ const end = Math.ceil(Math.log10(hi));
12504
+ for(let p = start; p <= end; p++){
12505
+ const v = Math.pow(10, p);
12506
+ if (v >= $caf32827df861c4e$var$MIN_LOG_TICK && v <= hi) ticks.add(v);
12507
+ }
12508
+ }
12509
+ if (lo <= -$caf32827df861c4e$var$MIN_LOG_TICK) {
12510
+ const start = Math.max(-1, Math.floor(Math.log10(hi < 0 ? -hi : $caf32827df861c4e$var$MIN_LOG_TICK)));
12511
+ const end = Math.ceil(Math.log10(-lo));
12512
+ for(let p = start; p <= end; p++){
12513
+ const v = -Math.pow(10, p);
12514
+ if (-v >= $caf32827df861c4e$var$MIN_LOG_TICK && v >= lo) ticks.add(v);
11616
12515
  }
11617
- ];
11618
- if (expanded.factors) factorTypes.forEach((t, i)=>{
11619
- studyLabels.push({
11620
- headerName: 'Factor',
11621
- headerClass: 'exprviz-hg-factors',
11622
- ...i === 0 ? {
11623
- headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
11624
- headerGroupComponentParams: {
11625
- onToggle: toggles.toggleFactors,
11626
- expanded: true
11627
- }
11628
- } : {}
11629
- });
11630
- titleLabels.push({
11631
- headerName: t,
11632
- headerClass: 'exprviz-hg-factors'
11633
- });
12516
+ }
12517
+ return Array.from(ticks).sort((a, b)=>a - b);
12518
+ }
12519
+ function $caf32827df861c4e$var$logTickFormat(v) {
12520
+ if (v === 0) return '0';
12521
+ const a = Math.abs(v);
12522
+ if (a >= 0.01 && a < 10000) return $gXNCa$d3.format('~g')(v);
12523
+ return $gXNCa$d3.format('.0e')(v);
12524
+ }
12525
+ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null, axisLabels: axisLabels = null })=>{
12526
+ const svgRef = (0, $gXNCa$react.useRef)(null);
12527
+ const containerRef = (0, $gXNCa$react.useRef)(null);
12528
+ // selections in data domain: { [field]: [lo, hi] }
12529
+ const selectionsRef = (0, $gXNCa$react.useRef)({});
12530
+ const lastClearRef = (0, $gXNCa$react.useRef)(0);
12531
+ // Track container size so the d3 render reruns when the user drags the
12532
+ // pane resizer (or when the window is resized). The values themselves
12533
+ // aren't read inside the effect — the effect always reads clientWidth/
12534
+ // clientHeight — but listing them in the deps array is what triggers it.
12535
+ const [size, setSize] = (0, $gXNCa$react.useState)({
12536
+ w: 0,
12537
+ h: 0
11634
12538
  });
11635
- else if (factorTypes.length > 0) {
11636
- studyLabels.push({
11637
- headerName: 'Factors',
11638
- headerClass: 'exprviz-hg-factors exprviz-hg-collapsed',
11639
- headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
11640
- headerGroupComponentParams: {
11641
- onToggle: toggles.toggleFactors,
11642
- expanded: false,
11643
- suffix: `(${factorTypes.length})`
12539
+ // Custom HTML tooltip for axis labels — gives us bold labels and structured
12540
+ // sections, which the native SVG <title> can't do.
12541
+ const [tooltip, setTooltip] = (0, $gXNCa$react.useState)(null);
12542
+ (0, $gXNCa$react.useEffect)(()=>{
12543
+ const el = containerRef.current;
12544
+ if (!el || typeof ResizeObserver === 'undefined') return;
12545
+ const ro = new ResizeObserver((entries)=>{
12546
+ for (const entry of entries){
12547
+ const { width: width, height: height } = entry.contentRect;
12548
+ setSize((prev)=>{
12549
+ if (Math.abs(prev.w - width) < 1 && Math.abs(prev.h - height) < 1) return prev;
12550
+ return {
12551
+ w: width,
12552
+ h: height
12553
+ };
12554
+ });
11644
12555
  }
11645
12556
  });
11646
- titleLabels.push({
11647
- headerName: '',
11648
- headerClass: 'exprviz-hg-factors exprviz-hg-collapsed'
11649
- });
11650
- }
11651
- if (expanded.chars) charTypes.forEach((t, i)=>{
11652
- studyLabels.push({
11653
- headerName: 'Characteristic',
11654
- headerClass: 'exprviz-hg-chars',
11655
- ...i === 0 ? {
11656
- headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
11657
- headerGroupComponentParams: {
11658
- onToggle: toggles.toggleChars,
11659
- expanded: true
11660
- }
11661
- } : {}
12557
+ ro.observe(el);
12558
+ return ()=>ro.disconnect();
12559
+ }, []);
12560
+ (0, $gXNCa$react.useEffect)(()=>{
12561
+ if (clearVersion !== lastClearRef.current) {
12562
+ selectionsRef.current = {};
12563
+ lastClearRef.current = clearVersion;
12564
+ if (onBrushChange) onBrushChange({});
12565
+ }
12566
+ Object.keys(selectionsRef.current).forEach((f)=>{
12567
+ if (!fields || !fields.includes(f)) delete selectionsRef.current[f];
11662
12568
  });
11663
- titleLabels.push({
11664
- headerName: t,
11665
- headerClass: 'exprviz-hg-chars'
12569
+ const svg = $gXNCa$d3.select(svgRef.current);
12570
+ svg.selectAll('*').remove();
12571
+ if (!fields || fields.length === 0 || !rows || rows.length === 0) return;
12572
+ const el = containerRef.current;
12573
+ const width = el && el.clientWidth || 600;
12574
+ const height = el && el.clientHeight || 300;
12575
+ const innerW = width - $caf32827df861c4e$var$MARGIN.left - $caf32827df861c4e$var$MARGIN.right;
12576
+ const innerH = height - $caf32827df861c4e$var$MARGIN.top - $caf32827df861c4e$var$MARGIN.bottom;
12577
+ svg.attr('viewBox', `0 0 ${width} ${height}`);
12578
+ const g = svg.append('g').attr('transform', `translate(${$caf32827df861c4e$var$MARGIN.left},${$caf32827df861c4e$var$MARGIN.top})`);
12579
+ // Mutable order during drag — starts as a copy of fields.
12580
+ let order = fields.slice();
12581
+ const x = $gXNCa$d3.scalePoint().range([
12582
+ 0,
12583
+ innerW
12584
+ ]).padding(0.5).domain(order);
12585
+ const yByField = {};
12586
+ let globalExt = null;
12587
+ if (scale === 'log') {
12588
+ const all = [];
12589
+ fields.forEach((f)=>{
12590
+ rows.forEach((r)=>{
12591
+ const v = r[f];
12592
+ if ($caf32827df861c4e$var$isNumeric(v)) all.push(+v);
12593
+ });
12594
+ });
12595
+ globalExt = all.length ? $gXNCa$d3.extent(all) : [
12596
+ 0,
12597
+ 1
12598
+ ];
12599
+ }
12600
+ fields.forEach((f)=>{
12601
+ if (scale === 'log') yByField[f] = $gXNCa$d3.scaleSymlog().domain(globalExt).range([
12602
+ innerH,
12603
+ 0
12604
+ ]).nice();
12605
+ else {
12606
+ const vals = rows.map((r)=>r[f]).filter($caf32827df861c4e$var$isNumeric).map(Number);
12607
+ const ext = vals.length ? $gXNCa$d3.extent(vals) : [
12608
+ 0,
12609
+ 1
12610
+ ];
12611
+ yByField[f] = $gXNCa$d3.scaleLinear().domain(ext).range([
12612
+ innerH,
12613
+ 0
12614
+ ]).nice();
12615
+ }
11666
12616
  });
11667
- });
11668
- else if (charTypes.length > 0) {
11669
- studyLabels.push({
11670
- headerName: 'Characteristics',
11671
- headerClass: 'exprviz-hg-chars exprviz-hg-collapsed',
11672
- headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
11673
- headerGroupComponentParams: {
11674
- onToggle: toggles.toggleChars,
11675
- expanded: false,
11676
- suffix: `(${charTypes.length})`
12617
+ function pathForRow(row, posOf) {
12618
+ const pts = order.map((f)=>{
12619
+ const v = row[f];
12620
+ if (!$caf32827df861c4e$var$isNumeric(v)) return null;
12621
+ return [
12622
+ posOf(f),
12623
+ yByField[f](Number(v))
12624
+ ];
12625
+ });
12626
+ return line(pts);
12627
+ }
12628
+ const line = $gXNCa$d3.line().defined((d)=>d != null && Number.isFinite(d[1])).x((d)=>d[0]).y((d)=>d[1]);
12629
+ const linesG = g.append('g').attr('class', 'exprviz-pc-lines');
12630
+ const paths = linesG.selectAll('path').data(rows).enter().append('path').attr('fill', 'none').attr('stroke', 'steelblue').attr('stroke-width', 1).attr('data-id', (d)=>d && d.id != null ? String(d.id) : null).attr('d', (row)=>pathForRow(row, (f)=>x(f)));
12631
+ function isBrushedIn(row) {
12632
+ for (const f of order){
12633
+ const sel = selectionsRef.current[f];
12634
+ if (!sel) continue;
12635
+ const v = row[f];
12636
+ if (!$caf32827df861c4e$var$isNumeric(v)) return false;
12637
+ const n = Number(v);
12638
+ const [lo, hi] = sel;
12639
+ if (n < lo || n > hi) return false;
12640
+ }
12641
+ return true;
12642
+ }
12643
+ function applyBrushStyles() {
12644
+ const anyActive = Object.keys(selectionsRef.current).length > 0;
12645
+ paths.classed('exprviz-pc-line-in', (d)=>!anyActive || isBrushedIn(d)).classed('exprviz-pc-line-out', (d)=>anyActive && !isBrushedIn(d));
12646
+ }
12647
+ applyBrushStyles();
12648
+ // axis groups, keyed by field name so D3 can match them across reorders
12649
+ const axisG = g.selectAll('.exprviz-pc-axis').data(order, (d)=>d).enter().append('g').attr('class', 'exprviz-pc-axis').attr('transform', (d)=>`translate(${x(d)},0)`);
12650
+ axisG.each(function(f) {
12651
+ const ax = $gXNCa$d3.select(this);
12652
+ const axisGen = $gXNCa$d3.axisLeft(yByField[f]);
12653
+ if (scale === 'log') axisGen.tickValues($caf32827df861c4e$var$logTickValues(yByField[f].domain())).tickFormat($caf32827df861c4e$var$logTickFormat);
12654
+ else axisGen.ticks(5);
12655
+ ax.call(axisGen);
12656
+ // Compact axis label. Hovering the label or its drag-handle rect shows
12657
+ // a custom HTML tooltip (rendered outside the SVG by React) that can
12658
+ // include bold labels and section headings.
12659
+ const labelInfo = axisLabels && axisLabels[f] || {
12660
+ short: f.replace(/__expr$/, ''),
12661
+ structured: {
12662
+ studyTitle: f,
12663
+ group: '',
12664
+ factors: [],
12665
+ characteristics: []
12666
+ }
12667
+ };
12668
+ const showTip = (event)=>setTooltip({
12669
+ x: event.clientX,
12670
+ y: event.clientY,
12671
+ info: labelInfo.structured
12672
+ });
12673
+ const moveTip = (event)=>setTooltip((t)=>t ? {
12674
+ ...t,
12675
+ x: event.clientX,
12676
+ y: event.clientY
12677
+ } : null);
12678
+ const hideTip = ()=>setTooltip(null);
12679
+ ax.append('text').attr('class', 'exprviz-pc-axis-label').attr('x', 4).attr('y', -4).attr('text-anchor', 'start').attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', '#333').style('font-size', '10px').style('cursor', 'grab').text(labelInfo.short).on('mouseenter', showTip).on('mousemove', moveTip).on('mouseleave', hideTip);
12680
+ // hit area for grabbing — sits along the rotated label
12681
+ ax.append('rect').attr('class', 'exprviz-pc-axis-handle').attr('x', 0).attr('y', -11).attr('width', 140).attr('height', 14).attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', 'transparent').style('cursor', 'grab').on('mouseenter', showTip).on('mousemove', moveTip).on('mouseleave', hideTip);
12682
+ const brush = $gXNCa$d3.brushY().extent([
12683
+ [
12684
+ -$caf32827df861c4e$var$BRUSH_WIDTH / 2,
12685
+ 0
12686
+ ],
12687
+ [
12688
+ $caf32827df861c4e$var$BRUSH_WIDTH / 2,
12689
+ innerH
12690
+ ]
12691
+ ]).on('brush end', (event)=>{
12692
+ const s = event.selection;
12693
+ if (!s) delete selectionsRef.current[f];
12694
+ else {
12695
+ const y = yByField[f];
12696
+ const a = y.invert(s[0]);
12697
+ const b = y.invert(s[1]);
12698
+ selectionsRef.current[f] = [
12699
+ Math.min(a, b),
12700
+ Math.max(a, b)
12701
+ ];
12702
+ }
12703
+ applyBrushStyles();
12704
+ // event.sourceEvent is null when brush.move is called programmatically
12705
+ // (e.g. when this effect re-runs and we restore prior selections).
12706
+ // Skipping that case avoids a re-render loop with the parent.
12707
+ if (event.type === 'end' && event.sourceEvent && onBrushChange) onBrushChange({
12708
+ ...selectionsRef.current
12709
+ });
12710
+ });
12711
+ const brushG = ax.append('g').attr('class', 'exprviz-pc-brush').call(brush);
12712
+ const prior = selectionsRef.current[f];
12713
+ if (prior) {
12714
+ const y = yByField[f];
12715
+ const py0 = y(prior[1]);
12716
+ const py1 = y(prior[0]);
12717
+ if (Number.isFinite(py0) && Number.isFinite(py1)) brushG.call(brush.move, [
12718
+ py0,
12719
+ py1
12720
+ ]);
11677
12721
  }
11678
12722
  });
11679
- titleLabels.push({
11680
- headerName: '',
11681
- headerClass: 'exprviz-hg-chars exprviz-hg-collapsed'
12723
+ // Drag-to-reorder: while dragging, only the dragged axis moves and the
12724
+ // line segments connecting to it are recomputed. Other axes stay put.
12725
+ // The new order is computed once at drag end and emitted via onReorder.
12726
+ const drag = $gXNCa$d3.drag().container(function() {
12727
+ return g.node();
12728
+ }).subject(function(event, d) {
12729
+ return {
12730
+ x: x(d),
12731
+ y: 0
12732
+ };
12733
+ }).on('start', function(event, d) {
12734
+ const axNode = this.parentNode;
12735
+ $gXNCa$d3.select(axNode).raise().classed('exprviz-pc-axis-dragging', true);
12736
+ $gXNCa$d3.select(axNode).select('.exprviz-pc-axis-label').style('cursor', 'grabbing');
12737
+ linesG.classed('exprviz-pc-lines-dragging', true);
12738
+ }).on('drag', function(event, d) {
12739
+ const axNode = this.parentNode;
12740
+ const newX = Math.max(0, Math.min(innerW, event.x));
12741
+ $gXNCa$d3.select(axNode).attr('transform', `translate(${newX},0)`);
12742
+ paths.attr('d', (row)=>pathForRow(row, (f)=>f === d ? newX : x(f)));
12743
+ }).on('end', function(event, d) {
12744
+ const axNode = this.parentNode;
12745
+ const newX = Math.max(0, Math.min(innerW, event.x));
12746
+ $gXNCa$d3.select(axNode).classed('exprviz-pc-axis-dragging', false);
12747
+ $gXNCa$d3.select(axNode).select('.exprviz-pc-axis-label').style('cursor', 'grab');
12748
+ linesG.classed('exprviz-pc-lines-dragging', false);
12749
+ const newOrder = order.slice().sort((a, b)=>{
12750
+ const xa = a === d ? newX : x(a);
12751
+ const xb = b === d ? newX : x(b);
12752
+ return xa - xb;
12753
+ });
12754
+ if (onReorder && !$caf32827df861c4e$var$arraysEqual(newOrder, fields)) {
12755
+ // Snap the dragged axis to its target slot for the brief moment
12756
+ // before the parent re-renders with the new order.
12757
+ x.domain(newOrder);
12758
+ $gXNCa$d3.select(axNode).attr('transform', `translate(${x(d)},0)`);
12759
+ paths.attr('d', (row)=>pathForRow(row, (f)=>x(f)));
12760
+ onReorder(newOrder);
12761
+ } else {
12762
+ // No order change — restore the dragged axis to its original slot.
12763
+ $gXNCa$d3.select(axNode).attr('transform', `translate(${x(d)},0)`);
12764
+ paths.attr('d', (row)=>pathForRow(row, (f)=>x(f)));
12765
+ }
11682
12766
  });
11683
- }
11684
- const idCol = $4ab64e76c1caef59$var$wrapLeafWithLabels($4ab64e76c1caef59$var$baseColDefs[0], studyLabels);
11685
- const nameCol = $4ab64e76c1caef59$var$wrapLeafWithLabels($4ab64e76c1caef59$var$baseColDefs[1], titleLabels);
11686
- return [
11687
- idCol,
11688
- nameCol,
11689
- ...exprTopGroups
11690
- ];
11691
- }
11692
- const $4ab64e76c1caef59$var$ExprTable = ({ rows: rows, fields: fields, onReorder: onReorder, studies: studies, expressionSamples: expressionSamples, onHoverRow: onHoverRow })=>{
11693
- const fieldInfo = (0, $gXNCa$react.useMemo)(()=>$4ab64e76c1caef59$export$7b242440eb2c300d(fields, studies, expressionSamples), [
12767
+ axisG.selectAll('.exprviz-pc-axis-label, .exprviz-pc-axis-handle').call(drag);
12768
+ }, [
12769
+ rows,
11694
12770
  fields,
11695
- studies,
11696
- expressionSamples
12771
+ scale,
12772
+ onBrushChange,
12773
+ onReorder,
12774
+ clearVersion,
12775
+ axisLabels,
12776
+ size.w,
12777
+ size.h
11697
12778
  ]);
11698
- // Default: factor rows expanded, characteristic rows collapsed (per spec).
11699
- const [expanded, setExpanded] = (0, $gXNCa$react.useState)({
11700
- factors: true,
11701
- chars: false
11702
- });
11703
- const toggleFactors = (0, $gXNCa$react.useCallback)(()=>setExpanded((e)=>({
11704
- ...e,
11705
- factors: !e.factors
11706
- })), []);
11707
- const toggleChars = (0, $gXNCa$react.useCallback)(()=>setExpanded((e)=>({
11708
- ...e,
11709
- chars: !e.chars
11710
- })), []);
11711
- const columnDefs = (0, $gXNCa$react.useMemo)(()=>$4ab64e76c1caef59$var$buildColumnDefs(fields, fieldInfo, expanded, {
11712
- toggleFactors: toggleFactors,
11713
- toggleChars: toggleChars
11714
- }), [
12779
+ // Highlight the polyline matching the hovered row id without rebuilding the
12780
+ // SVG. Raises the highlighted path so it draws above its neighbors.
12781
+ (0, $gXNCa$react.useEffect)(()=>{
12782
+ const svg = $gXNCa$d3.select(svgRef.current);
12783
+ if (svg.empty()) return;
12784
+ const paths = svg.selectAll('.exprviz-pc-lines path');
12785
+ paths.classed('exprviz-pc-line-hover', false);
12786
+ if (hoveredId == null) return;
12787
+ const target = paths.filter(function() {
12788
+ return this.getAttribute('data-id') === String(hoveredId);
12789
+ });
12790
+ target.classed('exprviz-pc-line-hover', true).raise();
12791
+ }, [
12792
+ hoveredId,
12793
+ rows,
11715
12794
  fields,
11716
- fieldInfo,
11717
- expanded,
11718
- toggleFactors,
11719
- toggleChars
12795
+ scale
11720
12796
  ]);
11721
- const onColumnMoved = (e)=>{
11722
- if (!onReorder || !e.finished) return;
11723
- const allCols = e.api.getAllGridColumns ? e.api.getAllGridColumns() : e.columnApi && e.columnApi.getAllGridColumns && e.columnApi.getAllGridColumns();
11724
- if (!allCols) return;
11725
- const next = allCols.map((c)=>c.getColId()).filter((id)=>fields.includes(id));
11726
- if (!$4ab64e76c1caef59$var$arraysEqual(next, fields)) onReorder(next);
11727
- };
11728
- if (!rows || rows.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11729
- className: "exprviz-table-empty",
12797
+ if (!fields || fields.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12798
+ className: "exprviz-plot-empty",
11730
12799
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11731
- children: "No data loaded."
12800
+ children: "Select fields to plot."
11732
12801
  })
11733
12802
  });
11734
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11735
- className: "ag-theme-quartz exprviz-aggrid",
11736
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$aggridreact.AgGridReact), {
11737
- rowData: rows,
11738
- columnDefs: columnDefs,
11739
- defaultColDef: $4ab64e76c1caef59$var$DEFAULT_COL_DEF,
11740
- animateRows: false,
11741
- suppressFieldDotNotation: true,
11742
- suppressDragLeaveHidesColumns: true,
11743
- suppressColumnVirtualisation: true,
11744
- groupHeaderHeight: 24,
11745
- onColumnMoved: onColumnMoved,
11746
- onCellMouseOver: (e)=>onHoverRow && onHoverRow(e.data && e.data.id),
11747
- onCellMouseOut: ()=>onHoverRow && onHoverRow(null)
11748
- })
12803
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12804
+ ref: containerRef,
12805
+ className: "exprviz-pc-container",
12806
+ children: [
12807
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("svg", {
12808
+ ref: svgRef,
12809
+ width: "100%",
12810
+ height: "100%",
12811
+ preserveAspectRatio: "none"
12812
+ }),
12813
+ tooltip && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($caf32827df861c4e$var$AxisTooltip, {
12814
+ x: tooltip.x,
12815
+ y: tooltip.y,
12816
+ info: tooltip.info
12817
+ })
12818
+ ]
11749
12819
  });
11750
12820
  };
11751
- var $4ab64e76c1caef59$export$2e2bcd8739ae039 = $4ab64e76c1caef59$var$ExprTable;
11752
-
11753
-
12821
+ // Position-fixed so it can escape the plot pane's clipping. Offset slightly
12822
+ // from the cursor and clamped to the viewport so it never spills off-screen.
12823
+ const $caf32827df861c4e$var$AxisTooltip = ({ x: x, y: y, info: info })=>{
12824
+ const ref = (0, $gXNCa$react.useRef)(null);
12825
+ const [pos, setPos] = (0, $gXNCa$react.useState)({
12826
+ left: x + 12,
12827
+ top: y + 12
12828
+ });
12829
+ (0, $gXNCa$react.useEffect)(()=>{
12830
+ const el = ref.current;
12831
+ if (!el) return;
12832
+ const w = el.offsetWidth;
12833
+ const h = el.offsetHeight;
12834
+ const vw = window.innerWidth;
12835
+ const vh = window.innerHeight;
12836
+ let left = x + 12;
12837
+ let top = y + 12;
12838
+ if (left + w > vw - 4) left = Math.max(4, x - 12 - w);
12839
+ if (top + h > vh - 4) top = Math.max(4, y - 12 - h);
12840
+ setPos({
12841
+ left: left,
12842
+ top: top
12843
+ });
12844
+ }, [
12845
+ x,
12846
+ y,
12847
+ info
12848
+ ]);
12849
+ const { studyTitle: studyTitle, group: group, factors: factors, characteristics: characteristics } = info;
12850
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12851
+ ref: ref,
12852
+ className: "exprviz-pc-tooltip",
12853
+ style: pos,
12854
+ children: [
12855
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12856
+ children: [
12857
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
12858
+ className: "exprviz-pc-tip-key",
12859
+ children: "Study:"
12860
+ }),
12861
+ " ",
12862
+ studyTitle,
12863
+ group ? ` (${group})` : ''
12864
+ ]
12865
+ }),
12866
+ factors.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
12867
+ children: [
12868
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12869
+ className: "exprviz-pc-tip-section",
12870
+ children: "Factors"
12871
+ }),
12872
+ factors.map((p, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12873
+ className: "exprviz-pc-tip-row",
12874
+ children: [
12875
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
12876
+ className: "exprviz-pc-tip-key",
12877
+ children: [
12878
+ p.name,
12879
+ ":"
12880
+ ]
12881
+ }),
12882
+ " ",
12883
+ p.value
12884
+ ]
12885
+ }, `f-${i}`))
12886
+ ]
12887
+ }),
12888
+ characteristics.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
12889
+ children: [
12890
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12891
+ className: "exprviz-pc-tip-section",
12892
+ children: "Characteristics"
12893
+ }),
12894
+ characteristics.map((p, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12895
+ className: "exprviz-pc-tip-row",
12896
+ children: [
12897
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
12898
+ className: "exprviz-pc-tip-key",
12899
+ children: [
12900
+ p.name,
12901
+ ":"
12902
+ ]
12903
+ }),
12904
+ " ",
12905
+ p.value
12906
+ ]
12907
+ }, `c-${i}`))
12908
+ ]
12909
+ })
12910
+ ]
12911
+ });
12912
+ };
12913
+ var $caf32827df861c4e$export$2e2bcd8739ae039 = $caf32827df861c4e$var$ParallelCoordsPlot;
11754
12914
 
11755
12915
 
11756
12916
 
11757
- // Parallel-coordinates plot with per-axis brushing and drag-to-reorder axes.
11758
- // - Axis labels are draggable horizontally; on drop, onReorder(newOrder) fires.
11759
- // - Brushes on axes intersect (AND): a row is "in" only when every active brush contains it.
11760
- // - Numeric fields use a linear or symlog scale; non-numeric values are skipped.
11761
- const $caf32827df861c4e$var$MARGIN = {
11762
- top: 100,
11763
- right: 24,
11764
- bottom: 24,
11765
- left: 32
11766
- };
11767
- const $caf32827df861c4e$var$LABEL_ROTATION = -40;
11768
- const $caf32827df861c4e$var$BRUSH_WIDTH = 16;
11769
- function $caf32827df861c4e$var$isNumeric(v) {
11770
- if (v == null) return false;
11771
- if (Array.isArray(v)) return false;
11772
- const n = +v;
11773
- return Number.isFinite(n);
11774
- }
11775
- function $caf32827df861c4e$var$arraysEqual(a, b) {
11776
- if (a.length !== b.length) return false;
11777
- for(let i = 0; i < a.length; i++)if (a[i] !== b[i]) return false;
11778
- return true;
11779
- }
11780
- // Powers-of-10 tick values spanning [lo, hi]; includes 0 if the range crosses zero.
11781
- // Clamped to |v| >= 0.1 to keep low-magnitude tick labels from overlapping near 0.
11782
- const $caf32827df861c4e$var$MIN_LOG_TICK = 0.1;
11783
- function $caf32827df861c4e$var$logTickValues([lo, hi]) {
11784
- const ticks = new Set();
11785
- if (lo <= 0 && hi >= 0) ticks.add(0);
11786
- if (hi >= $caf32827df861c4e$var$MIN_LOG_TICK) {
11787
- const start = Math.max(-1, Math.floor(Math.log10(lo > 0 ? lo : $caf32827df861c4e$var$MIN_LOG_TICK)));
11788
- const end = Math.ceil(Math.log10(hi));
11789
- for(let p = start; p <= end; p++){
11790
- const v = Math.pow(10, p);
11791
- if (v >= $caf32827df861c4e$var$MIN_LOG_TICK && v <= hi) ticks.add(v);
11792
- }
11793
- }
11794
- if (lo <= -$caf32827df861c4e$var$MIN_LOG_TICK) {
11795
- const start = Math.max(-1, Math.floor(Math.log10(hi < 0 ? -hi : $caf32827df861c4e$var$MIN_LOG_TICK)));
11796
- const end = Math.ceil(Math.log10(-lo));
11797
- for(let p = start; p <= end; p++){
11798
- const v = -Math.pow(10, p);
11799
- if (-v >= $caf32827df861c4e$var$MIN_LOG_TICK && v >= lo) ticks.add(v);
11800
- }
11801
- }
11802
- return Array.from(ticks).sort((a, b)=>a - b);
12917
+ function $1fd2507769d5bd00$var$speciesTaxonId(tid) {
12918
+ const n = +tid;
12919
+ return n > 1000000 ? Math.floor(n / 1000) : n;
11803
12920
  }
11804
- function $caf32827df861c4e$var$logTickFormat(v) {
11805
- if (v === 0) return '0';
11806
- const a = Math.abs(v);
11807
- if (a >= 0.01 && a < 10000) return $gXNCa$d3.format('~g')(v);
11808
- return $gXNCa$d3.format('.0e')(v);
12921
+ function $1fd2507769d5bd00$var$genomeName(grameneMaps, tid) {
12922
+ if (!grameneMaps) return tid;
12923
+ const direct = grameneMaps[tid];
12924
+ if (direct && direct.display_name) return direct.display_name;
12925
+ const sp = grameneMaps[$1fd2507769d5bd00$var$speciesTaxonId(tid)];
12926
+ if (sp && sp.display_name) return sp.display_name;
12927
+ return tid;
11809
12928
  }
11810
- const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null, axisLabels: axisLabels = null })=>{
11811
- const svgRef = (0, $gXNCa$react.useRef)(null);
11812
- const containerRef = (0, $gXNCa$react.useRef)(null);
11813
- // selections in data domain: { [field]: [lo, hi] }
11814
- const selectionsRef = (0, $gXNCa$react.useRef)({});
11815
- const lastClearRef = (0, $gXNCa$react.useRef)(0);
11816
- // Track container size so the d3 render reruns when the user drags the
11817
- // pane resizer (or when the window is resized). The values themselves
11818
- // aren't read inside the effect — the effect always reads clientWidth/
11819
- // clientHeight — but listing them in the deps array is what triggers it.
11820
- const [size, setSize] = (0, $gXNCa$react.useState)({
11821
- w: 0,
11822
- h: 0
11823
- });
11824
- // Custom HTML tooltip for axis labels — gives us bold labels and structured
11825
- // sections, which the native SVG <title> can't do.
11826
- const [tooltip, setTooltip] = (0, $gXNCa$react.useState)(null);
11827
- (0, $gXNCa$react.useEffect)(()=>{
11828
- const el = containerRef.current;
11829
- if (!el || typeof ResizeObserver === 'undefined') return;
11830
- const ro = new ResizeObserver((entries)=>{
11831
- for (const entry of entries){
11832
- const { width: width, height: height } = entry.contentRect;
11833
- setSize((prev)=>{
11834
- if (Math.abs(prev.w - width) < 1 && Math.abs(prev.h - height) < 1) return prev;
11835
- return {
11836
- w: width,
11837
- h: height
11838
- };
11839
- });
11840
- }
12929
+ const $1fd2507769d5bd00$var$ExprVizViewCmp = (props)=>{
12930
+ const { exprVizPivot: pivot, exprViz: exprViz, exprVizActiveTaxon: activeTaxon, grameneMaps: grameneMaps, expressionStudies: expressionStudies, expressionSamples: expressionSamples, doSetExprVizActiveTaxon: doSetExprVizActiveTaxon, doToggleExprVizFieldsModal: doToggleExprVizFieldsModal, doFetchExprVizData: doFetchExprVizData } = props;
12931
+ const studiesFor = (tid)=>{
12932
+ if (!expressionStudies) return [];
12933
+ return expressionStudies[tid] || expressionStudies[$1fd2507769d5bd00$var$speciesTaxonId(tid)] || [];
12934
+ };
12935
+ const taxa = (0, $gXNCa$react.useMemo)(()=>{
12936
+ const ids = Object.keys(pivot.data || {});
12937
+ if (!grameneMaps) return ids;
12938
+ return ids.sort((a, b)=>{
12939
+ const ma = grameneMaps[a] || grameneMaps[$1fd2507769d5bd00$var$speciesTaxonId(a)];
12940
+ const mb = grameneMaps[b] || grameneMaps[$1fd2507769d5bd00$var$speciesTaxonId(b)];
12941
+ return (ma && ma.left_index || 0) - (mb && mb.left_index || 0);
11841
12942
  });
11842
- ro.observe(el);
11843
- return ()=>ro.disconnect();
11844
- }, []);
12943
+ }, [
12944
+ pivot.data,
12945
+ grameneMaps
12946
+ ]);
11845
12947
  (0, $gXNCa$react.useEffect)(()=>{
11846
- if (clearVersion !== lastClearRef.current) {
11847
- selectionsRef.current = {};
11848
- lastClearRef.current = clearVersion;
11849
- if (onBrushChange) onBrushChange({});
11850
- }
11851
- Object.keys(selectionsRef.current).forEach((f)=>{
11852
- if (!fields || !fields.includes(f)) delete selectionsRef.current[f];
11853
- });
11854
- const svg = $gXNCa$d3.select(svgRef.current);
11855
- svg.selectAll('*').remove();
11856
- if (!fields || fields.length === 0 || !rows || rows.length === 0) return;
11857
- const el = containerRef.current;
11858
- const width = el && el.clientWidth || 600;
11859
- const height = el && el.clientHeight || 300;
11860
- const innerW = width - $caf32827df861c4e$var$MARGIN.left - $caf32827df861c4e$var$MARGIN.right;
11861
- const innerH = height - $caf32827df861c4e$var$MARGIN.top - $caf32827df861c4e$var$MARGIN.bottom;
11862
- svg.attr('viewBox', `0 0 ${width} ${height}`);
11863
- const g = svg.append('g').attr('transform', `translate(${$caf32827df861c4e$var$MARGIN.left},${$caf32827df861c4e$var$MARGIN.top})`);
11864
- // Mutable order during drag — starts as a copy of fields.
11865
- let order = fields.slice();
11866
- const x = $gXNCa$d3.scalePoint().range([
11867
- 0,
11868
- innerW
11869
- ]).padding(0.5).domain(order);
11870
- const yByField = {};
11871
- let globalExt = null;
11872
- if (scale === 'log') {
11873
- const all = [];
11874
- fields.forEach((f)=>{
11875
- rows.forEach((r)=>{
11876
- const v = r[f];
11877
- if ($caf32827df861c4e$var$isNumeric(v)) all.push(+v);
11878
- });
11879
- });
11880
- globalExt = all.length ? $gXNCa$d3.extent(all) : [
11881
- 0,
11882
- 1
11883
- ];
11884
- }
11885
- fields.forEach((f)=>{
11886
- if (scale === 'log') yByField[f] = $gXNCa$d3.scaleSymlog().domain(globalExt).range([
11887
- innerH,
11888
- 0
11889
- ]).nice();
11890
- else {
11891
- const vals = rows.map((r)=>r[f]).filter($caf32827df861c4e$var$isNumeric).map(Number);
11892
- const ext = vals.length ? $gXNCa$d3.extent(vals) : [
11893
- 0,
11894
- 1
11895
- ];
11896
- yByField[f] = $gXNCa$d3.scaleLinear().domain(ext).range([
11897
- innerH,
11898
- 0
11899
- ]).nice();
11900
- }
11901
- });
11902
- function pathForRow(row, posOf) {
11903
- const pts = order.map((f)=>{
11904
- const v = row[f];
11905
- if (!$caf32827df861c4e$var$isNumeric(v)) return null;
11906
- return [
11907
- posOf(f),
11908
- yByField[f](Number(v))
11909
- ];
11910
- });
11911
- return line(pts);
11912
- }
11913
- const line = $gXNCa$d3.line().defined((d)=>d != null && Number.isFinite(d[1])).x((d)=>d[0]).y((d)=>d[1]);
11914
- const linesG = g.append('g').attr('class', 'exprviz-pc-lines');
11915
- const paths = linesG.selectAll('path').data(rows).enter().append('path').attr('fill', 'none').attr('stroke', 'steelblue').attr('stroke-width', 1).attr('data-id', (d)=>d && d.id != null ? String(d.id) : null).attr('d', (row)=>pathForRow(row, (f)=>x(f)));
11916
- function isBrushedIn(row) {
11917
- for (const f of order){
11918
- const sel = selectionsRef.current[f];
11919
- if (!sel) continue;
11920
- const v = row[f];
11921
- if (!$caf32827df861c4e$var$isNumeric(v)) return false;
11922
- const n = Number(v);
11923
- const [lo, hi] = sel;
11924
- if (n < lo || n > hi) return false;
11925
- }
11926
- return true;
11927
- }
11928
- function applyBrushStyles() {
11929
- const anyActive = Object.keys(selectionsRef.current).length > 0;
11930
- paths.classed('exprviz-pc-line-in', (d)=>!anyActive || isBrushedIn(d)).classed('exprviz-pc-line-out', (d)=>anyActive && !isBrushedIn(d));
11931
- }
11932
- applyBrushStyles();
11933
- // axis groups, keyed by field name so D3 can match them across reorders
11934
- const axisG = g.selectAll('.exprviz-pc-axis').data(order, (d)=>d).enter().append('g').attr('class', 'exprviz-pc-axis').attr('transform', (d)=>`translate(${x(d)},0)`);
11935
- axisG.each(function(f) {
11936
- const ax = $gXNCa$d3.select(this);
11937
- const axisGen = $gXNCa$d3.axisLeft(yByField[f]);
11938
- if (scale === 'log') axisGen.tickValues($caf32827df861c4e$var$logTickValues(yByField[f].domain())).tickFormat($caf32827df861c4e$var$logTickFormat);
11939
- else axisGen.ticks(5);
11940
- ax.call(axisGen);
11941
- // Compact axis label. Hovering the label or its drag-handle rect shows
11942
- // a custom HTML tooltip (rendered outside the SVG by React) that can
11943
- // include bold labels and section headings.
11944
- const labelInfo = axisLabels && axisLabels[f] || {
11945
- short: f.replace(/__expr$/, ''),
12948
+ if (taxa.length === 0) return;
12949
+ if (!activeTaxon || !taxa.includes(String(activeTaxon))) doSetExprVizActiveTaxon(taxa[0]);
12950
+ }, [
12951
+ taxa,
12952
+ activeTaxon,
12953
+ doSetExprVizActiveTaxon
12954
+ ]);
12955
+ if (pivot.status === 'loading') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12956
+ className: "exprviz-view",
12957
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
12958
+ children: "Loading studies\u2026"
12959
+ })
12960
+ });
12961
+ if (pivot.status === 'error') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12962
+ className: "exprviz-view",
12963
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
12964
+ children: [
12965
+ "Error: ",
12966
+ pivot.error
12967
+ ]
12968
+ })
12969
+ });
12970
+ if (taxa.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12971
+ className: "exprviz-view",
12972
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
12973
+ children: "No expression studies for current results."
12974
+ })
12975
+ });
12976
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12977
+ className: "exprviz-view",
12978
+ children: [
12979
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tabs), {
12980
+ activeKey: activeTaxon || taxa[0],
12981
+ onSelect: (k)=>doSetExprVizActiveTaxon(k),
12982
+ className: "exprviz-tabs",
12983
+ children: taxa.map((tid)=>{
12984
+ const studies = studiesFor(tid);
12985
+ const taxName = $1fd2507769d5bd00$var$genomeName(grameneMaps, tid);
12986
+ const geneCount = pivot.data[tid] || 0;
12987
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tab), {
12988
+ eventKey: tid,
12989
+ title: `${taxName} (${studies.length} studies \xb7 ${geneCount} genes)`,
12990
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($1fd2507769d5bd00$var$TaxonPanel, {
12991
+ taxon: tid,
12992
+ studies: studies,
12993
+ expressionSamples: expressionSamples,
12994
+ tabState: exprViz.byTaxon[tid],
12995
+ onOpenFields: ()=>doToggleExprVizFieldsModal(tid, true),
12996
+ onLoad: ()=>doFetchExprVizData(tid),
12997
+ onReorder: (next)=>props.doReorderExprVizFields(tid, next),
12998
+ onAddRangeQuery: props.doAddGrameneRangeQuery
12999
+ })
13000
+ }, tid);
13001
+ })
13002
+ }),
13003
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $4d0c2e01f58b53b1$export$2e2bcd8739ae039), {})
13004
+ ]
13005
+ });
13006
+ };
13007
+ // Compact axis labels for the parallel-coords plot. The raw Solr field name
13008
+ // (e.g. "E_CURD_148_g5__expr") is uninformative; we prefer the assay's
13009
+ // factor labels, falling back to "organism part" then to the group id.
13010
+ // `full` is exposed via an SVG <title> so the user can hover to see study,
13011
+ // group, and every factor/characteristic.
13012
+ const $1fd2507769d5bd00$var$AXIS_LABEL_MAX = 22;
13013
+ function $1fd2507769d5bd00$var$truncateLabel(s, n) {
13014
+ if (!s) return '';
13015
+ return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
13016
+ }
13017
+ function $1fd2507769d5bd00$var$compactAssayLabel(assay, group) {
13018
+ if (!assay) return group || '';
13019
+ const factorVals = (assay.factor || []).map((f)=>f && f.label).filter(Boolean);
13020
+ if (factorVals.length) return factorVals.join('; ');
13021
+ const chars = assay.characteristic || [];
13022
+ const organ = chars.find((c)=>c && c.type === 'organism part');
13023
+ if (organ && organ.label) return organ.label;
13024
+ const firstChar = chars.find((c)=>c && c.label);
13025
+ if (firstChar) return firstChar.label;
13026
+ return group || '';
13027
+ }
13028
+ function $1fd2507769d5bd00$var$assayPairs(list) {
13029
+ return (list || []).filter((x)=>x && x.label).map((x)=>({
13030
+ name: x.type || '',
13031
+ value: x.label
13032
+ }));
13033
+ }
13034
+ function $1fd2507769d5bd00$var$buildAxisLabels(fields, studies, expressionSamples) {
13035
+ const labels = {};
13036
+ if (!fields) return labels;
13037
+ const studyById = {};
13038
+ (studies || []).forEach((s)=>{
13039
+ if (s && s._id) studyById[s._id] = s;
13040
+ });
13041
+ const findAssay = (studyId, group)=>{
13042
+ const arr = expressionSamples && expressionSamples[studyId];
13043
+ return arr ? arr.find((a)=>a.group === group) : null;
13044
+ };
13045
+ for (const f of fields){
13046
+ const m = f.match(/^(.+?)_g(\d+)__expr$/);
13047
+ if (!m) {
13048
+ labels[f] = {
13049
+ short: $1fd2507769d5bd00$var$truncateLabel(f.replace(/__expr$/, ''), $1fd2507769d5bd00$var$AXIS_LABEL_MAX),
11946
13050
  structured: {
11947
13051
  studyTitle: f,
11948
13052
  group: '',
@@ -11950,716 +13054,1117 @@ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields,
11950
13054
  characteristics: []
11951
13055
  }
11952
13056
  };
11953
- const showTip = (event)=>setTooltip({
11954
- x: event.clientX,
11955
- y: event.clientY,
11956
- info: labelInfo.structured
11957
- });
11958
- const moveTip = (event)=>setTooltip((t)=>t ? {
11959
- ...t,
11960
- x: event.clientX,
11961
- y: event.clientY
11962
- } : null);
11963
- const hideTip = ()=>setTooltip(null);
11964
- ax.append('text').attr('class', 'exprviz-pc-axis-label').attr('x', 4).attr('y', -4).attr('text-anchor', 'start').attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', '#333').style('font-size', '10px').style('cursor', 'grab').text(labelInfo.short).on('mouseenter', showTip).on('mousemove', moveTip).on('mouseleave', hideTip);
11965
- // hit area for grabbing — sits along the rotated label
11966
- ax.append('rect').attr('class', 'exprviz-pc-axis-handle').attr('x', 0).attr('y', -11).attr('width', 140).attr('height', 14).attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', 'transparent').style('cursor', 'grab').on('mouseenter', showTip).on('mousemove', moveTip).on('mouseleave', hideTip);
11967
- const brush = $gXNCa$d3.brushY().extent([
11968
- [
11969
- -$caf32827df861c4e$var$BRUSH_WIDTH / 2,
11970
- 0
11971
- ],
11972
- [
11973
- $caf32827df861c4e$var$BRUSH_WIDTH / 2,
11974
- innerH
11975
- ]
11976
- ]).on('brush end', (event)=>{
11977
- const s = event.selection;
11978
- if (!s) delete selectionsRef.current[f];
11979
- else {
11980
- const y = yByField[f];
11981
- const a = y.invert(s[0]);
11982
- const b = y.invert(s[1]);
11983
- selectionsRef.current[f] = [
11984
- Math.min(a, b),
11985
- Math.max(a, b)
11986
- ];
11987
- }
11988
- applyBrushStyles();
11989
- // event.sourceEvent is null when brush.move is called programmatically
11990
- // (e.g. when this effect re-runs and we restore prior selections).
11991
- // Skipping that case avoids a re-render loop with the parent.
11992
- if (event.type === 'end' && event.sourceEvent && onBrushChange) onBrushChange({
11993
- ...selectionsRef.current
11994
- });
11995
- });
11996
- const brushG = ax.append('g').attr('class', 'exprviz-pc-brush').call(brush);
11997
- const prior = selectionsRef.current[f];
11998
- if (prior) {
11999
- const y = yByField[f];
12000
- const py0 = y(prior[1]);
12001
- const py1 = y(prior[0]);
12002
- if (Number.isFinite(py0) && Number.isFinite(py1)) brushG.call(brush.move, [
12003
- py0,
12004
- py1
12005
- ]);
12006
- }
12007
- });
12008
- // Drag-to-reorder: while dragging, only the dragged axis moves and the
12009
- // line segments connecting to it are recomputed. Other axes stay put.
12010
- // The new order is computed once at drag end and emitted via onReorder.
12011
- const drag = $gXNCa$d3.drag().container(function() {
12012
- return g.node();
12013
- }).subject(function(event, d) {
12014
- return {
12015
- x: x(d),
12016
- y: 0
12017
- };
12018
- }).on('start', function(event, d) {
12019
- const axNode = this.parentNode;
12020
- $gXNCa$d3.select(axNode).raise().classed('exprviz-pc-axis-dragging', true);
12021
- $gXNCa$d3.select(axNode).select('.exprviz-pc-axis-label').style('cursor', 'grabbing');
12022
- linesG.classed('exprviz-pc-lines-dragging', true);
12023
- }).on('drag', function(event, d) {
12024
- const axNode = this.parentNode;
12025
- const newX = Math.max(0, Math.min(innerW, event.x));
12026
- $gXNCa$d3.select(axNode).attr('transform', `translate(${newX},0)`);
12027
- paths.attr('d', (row)=>pathForRow(row, (f)=>f === d ? newX : x(f)));
12028
- }).on('end', function(event, d) {
12029
- const axNode = this.parentNode;
12030
- const newX = Math.max(0, Math.min(innerW, event.x));
12031
- $gXNCa$d3.select(axNode).classed('exprviz-pc-axis-dragging', false);
12032
- $gXNCa$d3.select(axNode).select('.exprviz-pc-axis-label').style('cursor', 'grab');
12033
- linesG.classed('exprviz-pc-lines-dragging', false);
12034
- const newOrder = order.slice().sort((a, b)=>{
12035
- const xa = a === d ? newX : x(a);
12036
- const xb = b === d ? newX : x(b);
12037
- return xa - xb;
12038
- });
12039
- if (onReorder && !$caf32827df861c4e$var$arraysEqual(newOrder, fields)) {
12040
- // Snap the dragged axis to its target slot for the brief moment
12041
- // before the parent re-renders with the new order.
12042
- x.domain(newOrder);
12043
- $gXNCa$d3.select(axNode).attr('transform', `translate(${x(d)},0)`);
12044
- paths.attr('d', (row)=>pathForRow(row, (f)=>x(f)));
12045
- onReorder(newOrder);
12046
- } else {
12047
- // No order change — restore the dragged axis to its original slot.
12048
- $gXNCa$d3.select(axNode).attr('transform', `translate(${x(d)},0)`);
12049
- paths.attr('d', (row)=>pathForRow(row, (f)=>x(f)));
13057
+ continue;
13058
+ }
13059
+ const expId = m[1].replace(/_/g, '-');
13060
+ const group = 'g' + m[2];
13061
+ const assay = findAssay(expId, group);
13062
+ const study = studyById[expId];
13063
+ const studyName = study && study.description || expId;
13064
+ labels[f] = {
13065
+ short: $1fd2507769d5bd00$var$truncateLabel($1fd2507769d5bd00$var$compactAssayLabel(assay, group), $1fd2507769d5bd00$var$AXIS_LABEL_MAX),
13066
+ structured: {
13067
+ studyTitle: studyName,
13068
+ group: group,
13069
+ factors: $1fd2507769d5bd00$var$assayPairs(assay && assay.factor),
13070
+ characteristics: $1fd2507769d5bd00$var$assayPairs(assay && assay.characteristic)
12050
13071
  }
12051
- });
12052
- axisG.selectAll('.exprviz-pc-axis-label, .exprviz-pc-axis-handle').call(drag);
13072
+ };
13073
+ }
13074
+ return labels;
13075
+ }
13076
+ function $1fd2507769d5bd00$var$rowMatchesSelections(row, selections) {
13077
+ for (const f of Object.keys(selections)){
13078
+ const v = row[f];
13079
+ if (v == null || Array.isArray(v)) return false;
13080
+ const n = +v;
13081
+ if (!Number.isFinite(n)) return false;
13082
+ const [lo, hi] = selections[f];
13083
+ if (n < lo || n > hi) return false;
13084
+ }
13085
+ return true;
13086
+ }
13087
+ function $1fd2507769d5bd00$var$fmt(n) {
13088
+ if (!Number.isFinite(n)) return String(n);
13089
+ const a = Math.abs(n);
13090
+ if (a !== 0 && (a < 0.001 || a >= 1e6)) return n.toExponential(3);
13091
+ return Number(n.toFixed(4)).toString();
13092
+ }
13093
+ function $1fd2507769d5bd00$var$tsvCell(v) {
13094
+ if (v == null) return '';
13095
+ if (Array.isArray(v)) return v.join(',');
13096
+ const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
13097
+ return s.replace(/[\t\r\n]+/g, ' ');
13098
+ }
13099
+ // Mirror the on-screen table header in the TSV: one row per metadata level
13100
+ // the table is showing (Study, then one row per distinct factor type, then
13101
+ // one row per distinct characteristic type), followed by the leaf header
13102
+ // (Gene ID / Name / per-sample group). The first two columns are repurposed
13103
+ // to carry the row category and the row's specific name, matching the
13104
+ // pinned-column labels in ExprTable.
13105
+ function $1fd2507769d5bd00$var$downloadTsv(filename, rows, fields, studies, expressionSamples) {
13106
+ const cols = [
13107
+ 'id',
13108
+ 'name',
13109
+ ...fields
13110
+ ];
13111
+ const fieldInfo = (0, $4ab64e76c1caef59$export$7b242440eb2c300d)(fields, studies, expressionSamples);
13112
+ const factorTypes = new Set();
13113
+ const charTypes = new Set();
13114
+ for (const f of fields){
13115
+ const info = fieldInfo[f];
13116
+ if (!info) continue;
13117
+ Object.keys(info.factors || {}).forEach((t)=>factorTypes.add(t));
13118
+ Object.keys(info.characteristics || {}).forEach((t)=>charTypes.add(t));
13119
+ }
13120
+ const factorTypeList = Array.from(factorTypes).sort();
13121
+ const charTypeList = Array.from(charTypes).sort();
13122
+ const metaRow = (cat, label, getValue)=>{
13123
+ const cells = [
13124
+ cat,
13125
+ label
13126
+ ];
13127
+ for (const f of fields)cells.push($1fd2507769d5bd00$var$tsvCell(getValue(fieldInfo[f] || {})));
13128
+ return cells.join('\t');
13129
+ };
13130
+ const lines = [];
13131
+ lines.push(metaRow('Study', 'Title', (info)=>info.studyDescription || ''));
13132
+ for (const t of factorTypeList)lines.push(metaRow('Factor', t, (info)=>info.factors && info.factors[t] || ''));
13133
+ for (const t of charTypeList)lines.push(metaRow('Characteristic', t, (info)=>info.characteristics && info.characteristics[t] || ''));
13134
+ // Leaf header — column ids for the data rows.
13135
+ lines.push([
13136
+ 'Gene ID',
13137
+ 'Name',
13138
+ ...fields.map((f)=>{
13139
+ const info = fieldInfo[f];
13140
+ return info && info.group || f.replace(/__expr$/, '');
13141
+ })
13142
+ ].join('\t'));
13143
+ for (const r of rows)lines.push(cols.map((c)=>$1fd2507769d5bd00$var$tsvCell(r[c])).join('\t'));
13144
+ const blob = new Blob([
13145
+ lines.join('\n') + '\n'
13146
+ ], {
13147
+ type: 'text/tab-separated-values'
13148
+ });
13149
+ const url = URL.createObjectURL(blob);
13150
+ const a = document.createElement('a');
13151
+ a.href = url;
13152
+ a.download = filename;
13153
+ document.body.appendChild(a);
13154
+ a.click();
13155
+ document.body.removeChild(a);
13156
+ URL.revokeObjectURL(url);
13157
+ }
13158
+ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expressionSamples: expressionSamples, tabState: tabState, onOpenFields: onOpenFields, onLoad: onLoad, onReorder: onReorder, onAddRangeQuery: onAddRangeQuery })=>{
13159
+ const selected = tabState && tabState.selectedFields || [];
13160
+ const rows = tabState && tabState.rows || [];
13161
+ const fetchInfo = tabState && tabState.fetch || {
13162
+ status: 'idle',
13163
+ total: 0
13164
+ };
13165
+ const [scale, setScale] = (0, $gXNCa$react.useState)('linear');
13166
+ const [selections, setSelections] = (0, $gXNCa$react.useState)({});
13167
+ const [clearVersion, setClearVersion] = (0, $gXNCa$react.useState)(0);
13168
+ const [hoveredId, setHoveredId] = (0, $gXNCa$react.useState)(null);
13169
+ const [plotHeight, setPlotHeight] = (0, $gXNCa$react.useState)(320);
13170
+ const resizeStateRef = (0, $gXNCa$react.useRef)(null);
13171
+ // Drag the horizontal separator between the plot and the table to retune
13172
+ // their relative sizes. Bounded so neither pane disappears entirely.
13173
+ const startResize = (e)=>{
13174
+ e.preventDefault();
13175
+ resizeStateRef.current = {
13176
+ startY: e.clientY,
13177
+ startHeight: plotHeight
13178
+ };
13179
+ const onMove = (ev)=>{
13180
+ const s = resizeStateRef.current;
13181
+ if (!s) return;
13182
+ const next = Math.max(120, Math.min(1200, s.startHeight + (ev.clientY - s.startY)));
13183
+ setPlotHeight(next);
13184
+ };
13185
+ const onUp = ()=>{
13186
+ resizeStateRef.current = null;
13187
+ document.removeEventListener('mousemove', onMove);
13188
+ document.removeEventListener('mouseup', onUp);
13189
+ document.body.style.cursor = '';
13190
+ document.body.style.userSelect = '';
13191
+ };
13192
+ document.body.style.cursor = 'row-resize';
13193
+ document.body.style.userSelect = 'none';
13194
+ document.addEventListener('mousemove', onMove);
13195
+ document.addEventListener('mouseup', onUp);
13196
+ };
13197
+ const hasBrush = Object.keys(selections).length > 0;
13198
+ const filteredRows = (0, $gXNCa$react.useMemo)(()=>{
13199
+ if (!hasBrush) return rows;
13200
+ return rows.filter((r)=>$1fd2507769d5bd00$var$rowMatchesSelections(r, selections));
12053
13201
  }, [
12054
13202
  rows,
12055
- fields,
12056
- scale,
12057
- onBrushChange,
12058
- onReorder,
12059
- clearVersion,
12060
- axisLabels,
12061
- size.w,
12062
- size.h
13203
+ selections,
13204
+ hasBrush
12063
13205
  ]);
12064
- // Highlight the polyline matching the hovered row id without rebuilding the
12065
- // SVG. Raises the highlighted path so it draws above its neighbors.
12066
- (0, $gXNCa$react.useEffect)(()=>{
12067
- const svg = $gXNCa$d3.select(svgRef.current);
12068
- if (svg.empty()) return;
12069
- const paths = svg.selectAll('.exprviz-pc-lines path');
12070
- paths.classed('exprviz-pc-line-hover', false);
12071
- if (hoveredId == null) return;
12072
- const target = paths.filter(function() {
12073
- return this.getAttribute('data-id') === String(hoveredId);
12074
- });
12075
- target.classed('exprviz-pc-line-hover', true).raise();
13206
+ // Drop fields with no numeric data in the loaded rows so empty axes/columns
13207
+ // don't clutter the visualization. Selected-but-empty fields stay in the
13208
+ // underlying selection so a future load can repopulate them.
13209
+ const visibleFields = (0, $gXNCa$react.useMemo)(()=>{
13210
+ if (rows.length === 0 || selected.length === 0) return selected;
13211
+ return selected.filter((f)=>rows.some((r)=>{
13212
+ const v = r[f];
13213
+ return v != null && !Array.isArray(v) && Number.isFinite(+v);
13214
+ }));
12076
13215
  }, [
12077
- hoveredId,
12078
13216
  rows,
12079
- fields,
12080
- scale
13217
+ selected
12081
13218
  ]);
12082
- if (!fields || fields.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12083
- className: "exprviz-plot-empty",
12084
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
12085
- children: "Select fields to plot."
12086
- })
13219
+ const handleReorder = onReorder ? (newVisibleOrder)=>{
13220
+ const visibleSet = new Set(newVisibleOrder);
13221
+ const hidden = selected.filter((f)=>!visibleSet.has(f));
13222
+ onReorder([
13223
+ ...newVisibleOrder,
13224
+ ...hidden
13225
+ ]);
13226
+ } : undefined;
13227
+ const axisLabels = (0, $gXNCa$react.useMemo)(()=>$1fd2507769d5bd00$var$buildAxisLabels(visibleFields, studies, expressionSamples), [
13228
+ visibleFields,
13229
+ studies,
13230
+ expressionSamples
13231
+ ]);
13232
+ (0, $gXNCa$react.useEffect)(()=>{
13233
+ if (rows.length === 0 && hasBrush) {
13234
+ setSelections({});
13235
+ setClearVersion((v)=>v + 1);
13236
+ }
13237
+ }, [
13238
+ rows.length,
13239
+ hasBrush
13240
+ ]);
13241
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13242
+ className: "exprviz-tab-panel",
13243
+ children: [
13244
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13245
+ className: "exprviz-toolbar",
13246
+ children: [
13247
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Button), {
13248
+ size: "sm",
13249
+ onClick: onOpenFields,
13250
+ children: [
13251
+ "Select fields (",
13252
+ selected.length,
13253
+ " selected, ",
13254
+ studies.length,
13255
+ " studies)"
13256
+ ]
13257
+ }),
13258
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13259
+ size: "sm",
13260
+ variant: "primary",
13261
+ disabled: selected.length === 0 || fetchInfo.status === 'loading',
13262
+ onClick: onLoad,
13263
+ children: fetchInfo.status === 'loading' ? "Loading\u2026" : 'Load data'
13264
+ }),
13265
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.ToggleButtonGroup), {
13266
+ type: "radio",
13267
+ name: `exprviz-scale-${taxon}`,
13268
+ size: "sm",
13269
+ value: scale,
13270
+ onChange: setScale,
13271
+ children: [
13272
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.ToggleButton), {
13273
+ id: `exprviz-scale-${taxon}-lin`,
13274
+ value: "linear",
13275
+ variant: "outline-secondary",
13276
+ children: "Linear"
13277
+ }),
13278
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.ToggleButton), {
13279
+ id: `exprviz-scale-${taxon}-log`,
13280
+ value: "log",
13281
+ variant: "outline-secondary",
13282
+ children: "Log"
13283
+ })
13284
+ ]
13285
+ }),
13286
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13287
+ size: "sm",
13288
+ variant: "outline-secondary",
13289
+ disabled: !hasBrush,
13290
+ onClick: ()=>{
13291
+ setClearVersion((v)=>v + 1);
13292
+ setSelections({});
13293
+ },
13294
+ children: "Clear brushes"
13295
+ }),
13296
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13297
+ size: "sm",
13298
+ variant: "outline-secondary",
13299
+ disabled: filteredRows.length === 0 || visibleFields.length === 0,
13300
+ onClick: ()=>$1fd2507769d5bd00$var$downloadTsv(`expression_${taxon}.tsv`, filteredRows, visibleFields, studies, expressionSamples),
13301
+ title: "Download the visible rows and columns as tab-delimited text",
13302
+ children: "Download TSV"
13303
+ }),
13304
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13305
+ size: "sm",
13306
+ variant: "success",
13307
+ disabled: !hasBrush || !onAddRangeQuery,
13308
+ onClick: ()=>{
13309
+ const terms = Object.keys(selections).map((field)=>{
13310
+ const [lo, hi] = selections[field];
13311
+ return {
13312
+ category: 'Expression',
13313
+ name: `${field}: ${$1fd2507769d5bd00$var$fmt(lo)}\u{2013}${$1fd2507769d5bd00$var$fmt(hi)}`,
13314
+ fq_field: field,
13315
+ fq_value: `[${lo} TO ${hi}]`
13316
+ };
13317
+ });
13318
+ onAddRangeQuery(terms);
13319
+ },
13320
+ title: "Add brush ranges as an AND-conjunction filter on the search",
13321
+ children: "Apply as filter"
13322
+ }),
13323
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
13324
+ className: "exprviz-status",
13325
+ children: [
13326
+ hasBrush ? `${filteredRows.length} of ${rows.length}` : rows.length,
13327
+ fetchInfo.total ? ` / ${fetchInfo.total}` : '',
13328
+ " genes",
13329
+ hasBrush ? ' (brushed)' : ' loaded'
13330
+ ]
13331
+ })
13332
+ ]
13333
+ }),
13334
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13335
+ className: "exprviz-body",
13336
+ children: [
13337
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13338
+ className: "exprviz-plot",
13339
+ style: {
13340
+ height: plotHeight
13341
+ },
13342
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $caf32827df861c4e$export$2e2bcd8739ae039), {
13343
+ rows: rows,
13344
+ fields: visibleFields,
13345
+ scale: scale,
13346
+ onBrushChange: setSelections,
13347
+ onReorder: handleReorder,
13348
+ clearVersion: clearVersion,
13349
+ hoveredId: hoveredId,
13350
+ axisLabels: axisLabels
13351
+ })
13352
+ }),
13353
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13354
+ className: "exprviz-resizer",
13355
+ role: "separator",
13356
+ "aria-orientation": "horizontal",
13357
+ "aria-label": "Resize plot",
13358
+ onMouseDown: startResize,
13359
+ title: "Drag to resize"
13360
+ }),
13361
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13362
+ className: "exprviz-table",
13363
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $4ab64e76c1caef59$export$2e2bcd8739ae039), {
13364
+ rows: filteredRows,
13365
+ fields: visibleFields,
13366
+ onReorder: handleReorder,
13367
+ studies: studies,
13368
+ expressionSamples: expressionSamples,
13369
+ onHoverRow: setHoveredId
13370
+ })
13371
+ })
13372
+ ]
13373
+ })
13374
+ ]
13375
+ });
13376
+ };
13377
+ var $1fd2507769d5bd00$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectExprViz', 'selectExprVizPivot', 'selectExprVizActiveTaxon', 'selectGrameneMaps', 'selectExpressionStudies', 'selectExpressionSamples', 'doSetExprVizActiveTaxon', 'doToggleExprVizFieldsModal', 'doFetchExprVizData', 'doReorderExprVizFields', 'doAddGrameneRangeQuery', $1fd2507769d5bd00$var$ExprVizViewCmp);
13378
+
13379
+
13380
+
13381
+
13382
+
13383
+
13384
+
13385
+
13386
+ function $f511198465122338$var$speciesTaxonId(tid) {
13387
+ const n = +tid;
13388
+ return n > 1000000 ? Math.floor(n / 1000) : n;
13389
+ }
13390
+ function $f511198465122338$var$genomeName(grameneMaps, tid) {
13391
+ if (!grameneMaps) return String(tid);
13392
+ const direct = grameneMaps[tid];
13393
+ if (direct && direct.display_name) return direct.display_name;
13394
+ const sp = grameneMaps[$f511198465122338$var$speciesTaxonId(tid)];
13395
+ if (sp && sp.display_name) return sp.display_name;
13396
+ return String(tid);
13397
+ }
13398
+ function $f511198465122338$var$fmtP(p) {
13399
+ if (p == null) return '';
13400
+ if (p === 0) return '0';
13401
+ if (p < 1e-4) return p.toExponential(2);
13402
+ return p.toPrecision(3);
13403
+ }
13404
+ function $f511198465122338$var$fmtFold(f) {
13405
+ if (!isFinite(f)) return '';
13406
+ if (f >= 100) return f.toFixed(0);
13407
+ return f.toFixed(2);
13408
+ }
13409
+ // ---------- species tree helpers ----------
13410
+ function $f511198465122338$var$findRoots(tax) {
13411
+ return Object.values(tax).filter((n)=>!n.is_a || n.is_a.length === 0 || !n.is_a.some((p)=>tax[p]));
13412
+ }
13413
+ // Place each facet tid onto a taxonomy node id (fall back to species level
13414
+ // when the exact subspecies tid isn't in grameneTaxonomy).
13415
+ function $f511198465122338$var$placeTaxa(taxonomy, taxa) {
13416
+ const placement = {};
13417
+ const byPlace = {};
13418
+ for (const { tid: tid, count: count } of taxa){
13419
+ const place = taxonomy[tid] ? +tid : $f511198465122338$var$speciesTaxonId(tid);
13420
+ placement[tid] = place;
13421
+ if (!byPlace[place]) byPlace[place] = [];
13422
+ byPlace[place].push({
13423
+ tid: tid,
13424
+ count: count
13425
+ });
13426
+ }
13427
+ return {
13428
+ placement: placement,
13429
+ byPlace: byPlace
13430
+ };
13431
+ }
13432
+ // Set of taxonomy node ids on the path from any placed leaf back to its root.
13433
+ function $f511198465122338$var$relevantNodeIds(taxonomy, byPlace) {
13434
+ const out = new Set();
13435
+ for (const placeId of Object.keys(byPlace)){
13436
+ let cur = taxonomy[placeId];
13437
+ const seen = new Set();
13438
+ while(cur && !seen.has(+cur._id)){
13439
+ seen.add(+cur._id);
13440
+ out.add(+cur._id);
13441
+ const pid = cur.is_a && cur.is_a[0];
13442
+ cur = pid ? taxonomy[pid] : null;
13443
+ }
13444
+ }
13445
+ return out;
13446
+ }
13447
+ function $f511198465122338$var$relevantChildren(node, taxonomy, relevant) {
13448
+ return (node.children || []).map((cid)=>taxonomy[cid]).filter((c)=>c && relevant.has(+c._id));
13449
+ }
13450
+ // Walk down through single-relevant-child internal nodes to a branch / leaf.
13451
+ function $f511198465122338$var$compressChain(node, taxonomy, relevant, byPlace) {
13452
+ const chain = [
13453
+ node
13454
+ ];
13455
+ let cur = node;
13456
+ while(true){
13457
+ if (byPlace[cur._id]) break; // node itself is a placed leaf
13458
+ const kids = $f511198465122338$var$relevantChildren(cur, taxonomy, relevant);
13459
+ if (kids.length !== 1) break;
13460
+ cur = kids[0];
13461
+ chain.push(cur);
13462
+ }
13463
+ return {
13464
+ chain: chain,
13465
+ terminal: cur
13466
+ };
13467
+ }
13468
+ const $f511198465122338$var$CHAIN_DISPLAY_MAX = 3;
13469
+ function $f511198465122338$var$renderChain(chain) {
13470
+ const nameOf = (n)=>n.short_name || n.name || String(n._id);
13471
+ if (chain.length <= $f511198465122338$var$CHAIN_DISPLAY_MAX) return chain.map((n, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, ($parcel$interopDefault($gXNCa$react))).Fragment, {
13472
+ children: [
13473
+ i > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13474
+ className: "tax-sep",
13475
+ children: " \u203A "
13476
+ }),
13477
+ nameOf(n)
13478
+ ]
13479
+ }, n._id));
13480
+ const first = chain[0];
13481
+ const last = chain[chain.length - 1];
13482
+ const middle = chain.slice(1, -1);
13483
+ const fullTitle = chain.map(nameOf).join(" \u203A ");
13484
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
13485
+ children: [
13486
+ nameOf(first),
13487
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13488
+ className: "tax-sep",
13489
+ children: " \u203A "
13490
+ }),
13491
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
13492
+ className: "tax-ellipsis",
13493
+ title: fullTitle,
13494
+ children: [
13495
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13496
+ className: "tax-ellipsis-short",
13497
+ children: "\u2026"
13498
+ }),
13499
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13500
+ className: "tax-ellipsis-full",
13501
+ children: middle.map((n)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, ($parcel$interopDefault($gXNCa$react))).Fragment, {
13502
+ children: [
13503
+ nameOf(n),
13504
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13505
+ className: "tax-sep",
13506
+ children: " \u203A "
13507
+ })
13508
+ ]
13509
+ }, n._id))
13510
+ })
13511
+ ]
13512
+ }),
13513
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13514
+ className: "tax-sep",
13515
+ children: " \u203A "
13516
+ }),
13517
+ nameOf(last)
13518
+ ]
12087
13519
  });
13520
+ }
13521
+ function $f511198465122338$var$dedupAdjacent(chain) {
13522
+ const out = [];
13523
+ for (const n of chain)if (out.length && out[out.length - 1].name === n.name) out[out.length - 1] = n;
13524
+ else out.push(n);
13525
+ return out;
13526
+ }
13527
+ const $f511198465122338$var$SpeciesTreeNode = ({ node: node, depth: depth, taxonomy: taxonomy, relevant: relevant, byPlace: byPlace, grameneMaps: grameneMaps, expanded: expanded, onToggleExpand: onToggleExpand, activeTaxon: activeTaxon, onSelect: onSelect })=>{
13528
+ const { chain: rawChain, terminal: terminal } = $f511198465122338$var$compressChain(node, taxonomy, relevant, byPlace);
13529
+ const chain = $f511198465122338$var$dedupAdjacent(rawChain);
13530
+ const placed = byPlace[terminal._id] || [];
13531
+ const kids = $f511198465122338$var$relevantChildren(terminal, taxonomy, relevant);
13532
+ const hasKids = kids.length > 0;
13533
+ const isOpen = expanded.has(+terminal._id);
12088
13534
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12089
- ref: containerRef,
12090
- className: "exprviz-pc-container",
13535
+ className: "tax-node gsea-tree-node",
12091
13536
  children: [
12092
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("svg", {
12093
- ref: svgRef,
12094
- width: "100%",
12095
- height: "100%",
12096
- preserveAspectRatio: "none"
13537
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13538
+ className: "tax-row",
13539
+ style: {
13540
+ paddingLeft: depth * 7
13541
+ },
13542
+ children: [
13543
+ hasKids ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13544
+ className: "tax-chevron",
13545
+ onClick: ()=>onToggleExpand(+terminal._id),
13546
+ children: isOpen ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronDown), {}) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronRight), {})
13547
+ }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13548
+ className: "tax-chevron-spacer"
13549
+ }),
13550
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13551
+ className: hasKids ? 'tax-label tax-label-internal' : 'tax-label',
13552
+ children: $f511198465122338$var$renderChain(chain)
13553
+ })
13554
+ ]
12097
13555
  }),
12098
- tooltip && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($caf32827df861c4e$var$AxisTooltip, {
12099
- x: tooltip.x,
12100
- y: tooltip.y,
12101
- info: tooltip.info
13556
+ placed.map(({ tid: tid, count: count })=>{
13557
+ const isActive = String(activeTaxon) === String(tid);
13558
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13559
+ className: 'gsea-tree-leaf' + (isActive ? ' gsea-tree-leaf-active' : ''),
13560
+ style: {
13561
+ paddingLeft: (depth + 1) * 7 + 18
13562
+ },
13563
+ onClick: ()=>onSelect(tid),
13564
+ children: [
13565
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("input", {
13566
+ type: "radio",
13567
+ readOnly: true,
13568
+ checked: isActive,
13569
+ onChange: ()=>onSelect(tid)
13570
+ }),
13571
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13572
+ className: "gsea-tree-leaf-name",
13573
+ children: $f511198465122338$var$genomeName(grameneMaps, tid)
13574
+ }),
13575
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13576
+ className: "gsea-tree-leaf-count",
13577
+ children: count.toLocaleString()
13578
+ })
13579
+ ]
13580
+ }, tid);
13581
+ }),
13582
+ hasKids && isOpen && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13583
+ className: "tax-children",
13584
+ children: kids.map((c)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SpeciesTreeNode, {
13585
+ node: c,
13586
+ depth: depth + 1,
13587
+ taxonomy: taxonomy,
13588
+ relevant: relevant,
13589
+ byPlace: byPlace,
13590
+ grameneMaps: grameneMaps,
13591
+ expanded: expanded,
13592
+ onToggleExpand: onToggleExpand,
13593
+ activeTaxon: activeTaxon,
13594
+ onSelect: onSelect
13595
+ }, c._id))
12102
13596
  })
12103
13597
  ]
12104
13598
  });
12105
13599
  };
12106
- // Position-fixed so it can escape the plot pane's clipping. Offset slightly
12107
- // from the cursor and clamped to the viewport so it never spills off-screen.
12108
- const $caf32827df861c4e$var$AxisTooltip = ({ x: x, y: y, info: info })=>{
12109
- const ref = (0, $gXNCa$react.useRef)(null);
12110
- const [pos, setPos] = (0, $gXNCa$react.useState)({
12111
- left: x + 12,
12112
- top: y + 12
12113
- });
13600
+ const $f511198465122338$var$SpeciesTree = ({ taxonomy: taxonomy, grameneMaps: grameneMaps, taxa: taxa, activeTaxon: activeTaxon, onSelect: onSelect })=>{
13601
+ const { placement: placement, byPlace: byPlace } = (0, $gXNCa$react.useMemo)(()=>$f511198465122338$var$placeTaxa(taxonomy, taxa), [
13602
+ taxonomy,
13603
+ taxa
13604
+ ]);
13605
+ const relevant = (0, $gXNCa$react.useMemo)(()=>$f511198465122338$var$relevantNodeIds(taxonomy, byPlace), [
13606
+ taxonomy,
13607
+ byPlace
13608
+ ]);
13609
+ const roots = (0, $gXNCa$react.useMemo)(()=>$f511198465122338$var$findRoots(taxonomy).filter((r)=>relevant.has(+r._id)), [
13610
+ taxonomy,
13611
+ relevant
13612
+ ]);
13613
+ // Default: every relevant internal node expanded so the user sees the
13614
+ // full pruned tree on first paint.
13615
+ const [expanded, setExpanded] = (0, $gXNCa$react.useState)(()=>new Set(relevant));
12114
13616
  (0, $gXNCa$react.useEffect)(()=>{
12115
- const el = ref.current;
12116
- if (!el) return;
12117
- const w = el.offsetWidth;
12118
- const h = el.offsetHeight;
12119
- const vw = window.innerWidth;
12120
- const vh = window.innerHeight;
12121
- let left = x + 12;
12122
- let top = y + 12;
12123
- if (left + w > vw - 4) left = Math.max(4, x - 12 - w);
12124
- if (top + h > vh - 4) top = Math.max(4, y - 12 - h);
12125
- setPos({
12126
- left: left,
12127
- top: top
12128
- });
13617
+ setExpanded(new Set(relevant));
12129
13618
  }, [
12130
- x,
12131
- y,
12132
- info
13619
+ relevant
12133
13620
  ]);
12134
- const { studyTitle: studyTitle, group: group, factors: factors, characteristics: characteristics } = info;
12135
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12136
- ref: ref,
12137
- className: "exprviz-pc-tooltip",
12138
- style: pos,
13621
+ const handleToggle = (id)=>{
13622
+ setExpanded((prev)=>{
13623
+ const next = new Set(prev);
13624
+ next.has(id) ? next.delete(id) : next.add(id);
13625
+ return next;
13626
+ });
13627
+ };
13628
+ // Skip the root spine: if a root has only one relevant child and isn't
13629
+ // itself a placed leaf, render from its child instead.
13630
+ const topLevel = [];
13631
+ for (const r of roots){
13632
+ const { terminal: terminal } = $f511198465122338$var$compressChain(r, taxonomy, relevant, byPlace);
13633
+ if (byPlace[terminal._id]) topLevel.push(r);
13634
+ else {
13635
+ const kids = $f511198465122338$var$relevantChildren(terminal, taxonomy, relevant);
13636
+ kids.forEach((k)=>topLevel.push(k));
13637
+ }
13638
+ }
13639
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13640
+ className: "gsea-tree",
13641
+ children: topLevel.map((n)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SpeciesTreeNode, {
13642
+ node: n,
13643
+ depth: 0,
13644
+ taxonomy: taxonomy,
13645
+ relevant: relevant,
13646
+ byPlace: byPlace,
13647
+ grameneMaps: grameneMaps,
13648
+ expanded: expanded,
13649
+ onToggleExpand: handleToggle,
13650
+ activeTaxon: activeTaxon,
13651
+ onSelect: onSelect
13652
+ }, n._id))
13653
+ });
13654
+ };
13655
+ // ---------- enrichment panel ----------
13656
+ const $f511198465122338$var$ControlsBar = ({ ui: ui, onChange: onChange })=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13657
+ className: "gsea-controls",
12139
13658
  children: [
12140
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13659
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Group, {
13660
+ className: "gsea-control",
12141
13661
  children: [
12142
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
12143
- className: "exprviz-pc-tip-key",
12144
- children: "Study:"
13662
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Label, {
13663
+ children: "p_adj \u2264"
12145
13664
  }),
12146
- " ",
12147
- studyTitle,
12148
- group ? ` (${group})` : ''
13665
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
13666
+ type: "number",
13667
+ step: "0.01",
13668
+ min: "0",
13669
+ max: "1",
13670
+ value: ui.pAdjCutoff,
13671
+ onChange: (e)=>onChange({
13672
+ pAdjCutoff: +e.target.value
13673
+ })
13674
+ })
12149
13675
  ]
12150
13676
  }),
12151
- factors.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
13677
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Group, {
13678
+ className: "gsea-control",
12152
13679
  children: [
12153
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12154
- className: "exprviz-pc-tip-section",
12155
- children: "Factors"
13680
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Label, {
13681
+ children: "min k"
12156
13682
  }),
12157
- factors.map((p, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12158
- className: "exprviz-pc-tip-row",
12159
- children: [
12160
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
12161
- className: "exprviz-pc-tip-key",
12162
- children: [
12163
- p.name,
12164
- ":"
12165
- ]
12166
- }),
12167
- " ",
12168
- p.value
12169
- ]
12170
- }, `f-${i}`))
13683
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
13684
+ type: "number",
13685
+ step: "1",
13686
+ min: "1",
13687
+ value: ui.minK,
13688
+ onChange: (e)=>onChange({
13689
+ minK: Math.max(1, +e.target.value)
13690
+ })
13691
+ })
12171
13692
  ]
12172
13693
  }),
12173
- characteristics.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
13694
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Check, {
13695
+ className: "gsea-control",
13696
+ type: "switch",
13697
+ id: "gsea-most-specific",
13698
+ label: "Most-specific only",
13699
+ checked: !!ui.mostSpecific,
13700
+ onChange: (e)=>onChange({
13701
+ mostSpecific: e.target.checked
13702
+ })
13703
+ }),
13704
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Group, {
13705
+ className: "gsea-control gsea-control-grow",
12174
13706
  children: [
12175
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12176
- className: "exprviz-pc-tip-section",
12177
- children: "Characteristics"
13707
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Label, {
13708
+ children: "Filter terms"
12178
13709
  }),
12179
- characteristics.map((p, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12180
- className: "exprviz-pc-tip-row",
12181
- children: [
12182
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
12183
- className: "exprviz-pc-tip-key",
12184
- children: [
12185
- p.name,
12186
- ":"
12187
- ]
12188
- }),
12189
- " ",
12190
- p.value
12191
- ]
12192
- }, `c-${i}`))
13710
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
13711
+ type: "text",
13712
+ placeholder: "term id or name\u2026",
13713
+ value: ui.search || '',
13714
+ onChange: (e)=>onChange({
13715
+ search: e.target.value
13716
+ })
13717
+ })
13718
+ ]
13719
+ })
13720
+ ]
13721
+ });
13722
+ const $f511198465122338$var$TermRow = ({ row: row, showType: showType, onAddFilter: onAddFilter })=>{
13723
+ const handleClick = ()=>onAddFilter && onAddFilter(row);
13724
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tr", {
13725
+ onClick: handleClick,
13726
+ title: "Click to add as a filter",
13727
+ children: [
13728
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
13729
+ className: "gsea-term-id",
13730
+ children: row.term_display_id
13731
+ }),
13732
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
13733
+ className: "gsea-term-name",
13734
+ children: row.term_name || /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
13735
+ children: "(loading\u2026)"
13736
+ })
13737
+ }),
13738
+ showType && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
13739
+ className: "gsea-term-type",
13740
+ children: row.term_namespace || ''
13741
+ }),
13742
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("td", {
13743
+ className: "gsea-num",
13744
+ children: [
13745
+ row.k,
13746
+ " / ",
13747
+ row.n
13748
+ ]
13749
+ }),
13750
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("td", {
13751
+ className: "gsea-num",
13752
+ children: [
13753
+ row.K,
13754
+ " / ",
13755
+ row.N
13756
+ ]
13757
+ }),
13758
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
13759
+ className: "gsea-num",
13760
+ children: $f511198465122338$var$fmtFold(row.fold)
13761
+ }),
13762
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
13763
+ className: "gsea-num",
13764
+ children: $f511198465122338$var$fmtP(row.p)
13765
+ }),
13766
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
13767
+ className: "gsea-num",
13768
+ children: $f511198465122338$var$fmtP(row.pAdj)
13769
+ })
13770
+ ]
13771
+ });
13772
+ };
13773
+ // InterPro entry types and pathway types are useful per-row context
13774
+ // (e.g. Domain vs Family vs Repeat for InterPro), and aren't reflected in
13775
+ // the section title the way GO namespaces are. Show a Type column for
13776
+ // those two ontologies only.
13777
+ const $f511198465122338$var$ONTS_WITH_TYPE_COLUMN = new Set([
13778
+ 'domains',
13779
+ 'pathways'
13780
+ ]);
13781
+ const $f511198465122338$var$SORT_ACCESSORS = {
13782
+ term: (r)=>r.term_display_id || '',
13783
+ name: (r)=>(r.term_name || '').toLowerCase(),
13784
+ type: (r)=>(r.term_namespace || '').toLowerCase(),
13785
+ k: (r)=>r.k,
13786
+ K: (r)=>r.K,
13787
+ fold: (r)=>r.fold,
13788
+ p: (r)=>r.p,
13789
+ pAdj: (r)=>r.pAdj
13790
+ };
13791
+ // Defaults chosen so a single click does what the user usually wants:
13792
+ // numeric "more interesting" columns (k, K, fold) descend; p-values and
13793
+ // text columns ascend.
13794
+ const $f511198465122338$var$SORT_DEFAULT_DIR = {
13795
+ term: 'asc',
13796
+ name: 'asc',
13797
+ type: 'asc',
13798
+ k: 'desc',
13799
+ K: 'desc',
13800
+ fold: 'desc',
13801
+ p: 'asc',
13802
+ pAdj: 'asc'
13803
+ };
13804
+ const $f511198465122338$var$SortableTh = ({ label: label, sortKey: sortKey, activeKey: activeKey, activeDir: activeDir, onSort: onSort, numeric: numeric })=>{
13805
+ const active = activeKey === sortKey;
13806
+ const arrow = active ? activeDir === 'asc' ? " \u25B2" : " \u25BC" : '';
13807
+ const cls = 'gsea-sort-th' + (active ? ' gsea-sort-th-active' : '') + (numeric ? ' gsea-num' : '');
13808
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("th", {
13809
+ className: cls,
13810
+ onClick: ()=>onSort(sortKey),
13811
+ children: [
13812
+ label,
13813
+ arrow
13814
+ ]
13815
+ });
13816
+ };
13817
+ const $f511198465122338$var$OntologySection = ({ block: block, search: search, onAddFilter: onAddFilter })=>{
13818
+ const filtered = (0, $gXNCa$react.useMemo)(()=>{
13819
+ if (!search) return block.rows;
13820
+ const needle = search.toLowerCase();
13821
+ return block.rows.filter((r)=>r.term_display_id && r.term_display_id.toLowerCase().includes(needle) || r.term_name && r.term_name.toLowerCase().includes(needle));
13822
+ }, [
13823
+ block.rows,
13824
+ search
13825
+ ]);
13826
+ const showType = $f511198465122338$var$ONTS_WITH_TYPE_COLUMN.has(block.ontology);
13827
+ const [sortKey, setSortKey] = (0, $gXNCa$react.useState)('pAdj');
13828
+ const [sortDir, setSortDir] = (0, $gXNCa$react.useState)('asc');
13829
+ const handleSort = (key)=>{
13830
+ if (key === sortKey) setSortDir((d)=>d === 'asc' ? 'desc' : 'asc');
13831
+ else {
13832
+ setSortKey(key);
13833
+ setSortDir($f511198465122338$var$SORT_DEFAULT_DIR[key] || 'asc');
13834
+ }
13835
+ };
13836
+ const sorted = (0, $gXNCa$react.useMemo)(()=>{
13837
+ const accessor = $f511198465122338$var$SORT_ACCESSORS[sortKey];
13838
+ if (!accessor) return filtered;
13839
+ const sign = sortDir === 'desc' ? -1 : 1;
13840
+ const arr = filtered.slice();
13841
+ arr.sort((a, b)=>{
13842
+ const va = accessor(a);
13843
+ const vb = accessor(b);
13844
+ if (typeof va === 'number' && typeof vb === 'number') {
13845
+ if (va === vb) return 0;
13846
+ return (va - vb) * sign;
13847
+ }
13848
+ return String(va).localeCompare(String(vb)) * sign;
13849
+ });
13850
+ return arr;
13851
+ }, [
13852
+ filtered,
13853
+ sortKey,
13854
+ sortDir
13855
+ ]);
13856
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Accordion).Item, {
13857
+ eventKey: block.ontology,
13858
+ children: [
13859
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Accordion).Header, {
13860
+ children: [
13861
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13862
+ className: "gsea-ont-title",
13863
+ children: block.label
13864
+ }),
13865
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Badge), {
13866
+ bg: "secondary",
13867
+ className: "gsea-ont-badge",
13868
+ children: [
13869
+ block.passing,
13870
+ " significant / ",
13871
+ block.tested,
13872
+ " tested"
13873
+ ]
13874
+ })
12193
13875
  ]
13876
+ }),
13877
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Accordion).Body, {
13878
+ children: sorted.length === 0 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
13879
+ children: "No terms pass the current cutoffs."
13880
+ }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("table", {
13881
+ className: "gsea-table",
13882
+ children: [
13883
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("thead", {
13884
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tr", {
13885
+ children: [
13886
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SortableTh, {
13887
+ label: "Term",
13888
+ sortKey: "term",
13889
+ activeKey: sortKey,
13890
+ activeDir: sortDir,
13891
+ onSort: handleSort
13892
+ }),
13893
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SortableTh, {
13894
+ label: "Name",
13895
+ sortKey: "name",
13896
+ activeKey: sortKey,
13897
+ activeDir: sortDir,
13898
+ onSort: handleSort
13899
+ }),
13900
+ showType && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SortableTh, {
13901
+ label: "Type",
13902
+ sortKey: "type",
13903
+ activeKey: sortKey,
13904
+ activeDir: sortDir,
13905
+ onSort: handleSort
13906
+ }),
13907
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SortableTh, {
13908
+ label: "k / n",
13909
+ sortKey: "k",
13910
+ activeKey: sortKey,
13911
+ activeDir: sortDir,
13912
+ onSort: handleSort,
13913
+ numeric: true
13914
+ }),
13915
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SortableTh, {
13916
+ label: "K / N",
13917
+ sortKey: "K",
13918
+ activeKey: sortKey,
13919
+ activeDir: sortDir,
13920
+ onSort: handleSort,
13921
+ numeric: true
13922
+ }),
13923
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SortableTh, {
13924
+ label: "Fold",
13925
+ sortKey: "fold",
13926
+ activeKey: sortKey,
13927
+ activeDir: sortDir,
13928
+ onSort: handleSort,
13929
+ numeric: true
13930
+ }),
13931
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SortableTh, {
13932
+ label: "p",
13933
+ sortKey: "p",
13934
+ activeKey: sortKey,
13935
+ activeDir: sortDir,
13936
+ onSort: handleSort,
13937
+ numeric: true
13938
+ }),
13939
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SortableTh, {
13940
+ label: "p_adj",
13941
+ sortKey: "pAdj",
13942
+ activeKey: sortKey,
13943
+ activeDir: sortDir,
13944
+ onSort: handleSort,
13945
+ numeric: true
13946
+ })
13947
+ ]
13948
+ })
13949
+ }),
13950
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("tbody", {
13951
+ children: sorted.map((r)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$TermRow, {
13952
+ row: r,
13953
+ showType: showType,
13954
+ onAddFilter: onAddFilter
13955
+ }, `${r.ontology}:${r.term_id}`))
13956
+ })
13957
+ ]
13958
+ })
12194
13959
  })
12195
13960
  ]
12196
13961
  });
12197
13962
  };
12198
- var $caf32827df861c4e$export$2e2bcd8739ae039 = $caf32827df861c4e$var$ParallelCoordsPlot;
12199
-
12200
-
12201
-
12202
- function $1fd2507769d5bd00$var$speciesTaxonId(tid) {
12203
- const n = +tid;
12204
- return n > 1000000 ? Math.floor(n / 1000) : n;
12205
- }
12206
- function $1fd2507769d5bd00$var$genomeName(grameneMaps, tid) {
12207
- if (!grameneMaps) return tid;
12208
- const direct = grameneMaps[tid];
12209
- if (direct && direct.display_name) return direct.display_name;
12210
- const sp = grameneMaps[$1fd2507769d5bd00$var$speciesTaxonId(tid)];
12211
- if (sp && sp.display_name) return sp.display_name;
12212
- return tid;
12213
- }
12214
- const $1fd2507769d5bd00$var$ExprVizViewCmp = (props)=>{
12215
- const { exprVizPivot: pivot, exprViz: exprViz, exprVizActiveTaxon: activeTaxon, grameneMaps: grameneMaps, expressionStudies: expressionStudies, expressionSamples: expressionSamples, doSetExprVizActiveTaxon: doSetExprVizActiveTaxon, doToggleExprVizFieldsModal: doToggleExprVizFieldsModal, doFetchExprVizData: doFetchExprVizData } = props;
12216
- const studiesFor = (tid)=>{
12217
- if (!expressionStudies) return [];
12218
- return expressionStudies[tid] || expressionStudies[$1fd2507769d5bd00$var$speciesTaxonId(tid)] || [];
12219
- };
12220
- const taxa = (0, $gXNCa$react.useMemo)(()=>{
12221
- const ids = Object.keys(pivot.data || {});
12222
- if (!grameneMaps) return ids;
12223
- return ids.sort((a, b)=>{
12224
- const ma = grameneMaps[a] || grameneMaps[$1fd2507769d5bd00$var$speciesTaxonId(a)];
12225
- const mb = grameneMaps[b] || grameneMaps[$1fd2507769d5bd00$var$speciesTaxonId(b)];
12226
- return (ma && ma.left_index || 0) - (mb && mb.left_index || 0);
12227
- });
12228
- }, [
12229
- pivot.data,
12230
- grameneMaps
12231
- ]);
12232
- (0, $gXNCa$react.useEffect)(()=>{
12233
- if (taxa.length === 0) return;
12234
- if (!activeTaxon || !taxa.includes(String(activeTaxon))) doSetExprVizActiveTaxon(taxa[0]);
12235
- }, [
12236
- taxa,
12237
- activeTaxon,
12238
- doSetExprVizActiveTaxon
12239
- ]);
12240
- if (pivot.status === 'loading') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12241
- className: "exprviz-view",
13963
+ const $f511198465122338$var$TaxonPanel = ({ taxon: taxon, gsea: gsea, results: results, ui: ui, onUiChange: onUiChange, onAddFilter: onAddFilter })=>{
13964
+ if (!taxon) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13965
+ className: "gsea-panel",
12242
13966
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
12243
- children: "Loading studies\u2026"
13967
+ children: "Select a species on the left."
12244
13968
  })
12245
13969
  });
12246
- if (pivot.status === 'error') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12247
- className: "exprviz-view",
12248
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
12249
- children: [
12250
- "Error: ",
12251
- pivot.error
12252
- ]
13970
+ const t = gsea.byTaxon[taxon];
13971
+ if (!t) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13972
+ className: "gsea-panel",
13973
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
13974
+ children: "Initializing\u2026"
12253
13975
  })
12254
13976
  });
12255
- if (taxa.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12256
- className: "exprviz-view",
13977
+ if (t.fg.status === 'loading' || t.bg.status === 'loading' || t.fg.status === 'idle' || t.bg.status === 'idle') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13978
+ className: "gsea-panel gsea-loading",
12257
13979
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
12258
- children: "No expression studies for current results."
13980
+ children: "Loading enrichment\u2026"
12259
13981
  })
12260
13982
  });
12261
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12262
- className: "exprviz-view",
12263
- children: [
12264
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tabs), {
12265
- activeKey: activeTaxon || taxa[0],
12266
- onSelect: (k)=>doSetExprVizActiveTaxon(k),
12267
- className: "exprviz-tabs",
12268
- children: taxa.map((tid)=>{
12269
- const studies = studiesFor(tid);
12270
- const taxName = $1fd2507769d5bd00$var$genomeName(grameneMaps, tid);
12271
- const geneCount = pivot.data[tid] || 0;
12272
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tab), {
12273
- eventKey: tid,
12274
- title: `${taxName} (${studies.length} studies \xb7 ${geneCount} genes)`,
12275
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($1fd2507769d5bd00$var$TaxonPanel, {
12276
- taxon: tid,
12277
- studies: studies,
12278
- expressionSamples: expressionSamples,
12279
- tabState: exprViz.byTaxon[tid],
12280
- onOpenFields: ()=>doToggleExprVizFieldsModal(tid, true),
12281
- onLoad: ()=>doFetchExprVizData(tid),
12282
- onReorder: (next)=>props.doReorderExprVizFields(tid, next),
12283
- onAddRangeQuery: props.doAddGrameneRangeQuery
12284
- })
12285
- }, tid);
12286
- })
12287
- }),
12288
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $4d0c2e01f58b53b1$export$2e2bcd8739ae039), {})
12289
- ]
12290
- });
12291
- };
12292
- // Compact axis labels for the parallel-coords plot. The raw Solr field name
12293
- // (e.g. "E_CURD_148_g5__expr") is uninformative; we prefer the assay's
12294
- // factor labels, falling back to "organism part" then to the group id.
12295
- // `full` is exposed via an SVG <title> so the user can hover to see study,
12296
- // group, and every factor/characteristic.
12297
- const $1fd2507769d5bd00$var$AXIS_LABEL_MAX = 22;
12298
- function $1fd2507769d5bd00$var$truncateLabel(s, n) {
12299
- if (!s) return '';
12300
- return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
12301
- }
12302
- function $1fd2507769d5bd00$var$compactAssayLabel(assay, group) {
12303
- if (!assay) return group || '';
12304
- const factorVals = (assay.factor || []).map((f)=>f && f.label).filter(Boolean);
12305
- if (factorVals.length) return factorVals.join('; ');
12306
- const chars = assay.characteristic || [];
12307
- const organ = chars.find((c)=>c && c.type === 'organism part');
12308
- if (organ && organ.label) return organ.label;
12309
- const firstChar = chars.find((c)=>c && c.label);
12310
- if (firstChar) return firstChar.label;
12311
- return group || '';
12312
- }
12313
- function $1fd2507769d5bd00$var$assayPairs(list) {
12314
- return (list || []).filter((x)=>x && x.label).map((x)=>({
12315
- name: x.type || '',
12316
- value: x.label
12317
- }));
12318
- }
12319
- function $1fd2507769d5bd00$var$buildAxisLabels(fields, studies, expressionSamples) {
12320
- const labels = {};
12321
- if (!fields) return labels;
12322
- const studyById = {};
12323
- (studies || []).forEach((s)=>{
12324
- if (s && s._id) studyById[s._id] = s;
13983
+ if (t.fg.status === 'error') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13984
+ className: "gsea-panel",
13985
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
13986
+ children: [
13987
+ "Foreground error: ",
13988
+ t.fg.error
13989
+ ]
13990
+ })
12325
13991
  });
12326
- const findAssay = (studyId, group)=>{
12327
- const arr = expressionSamples && expressionSamples[studyId];
12328
- return arr ? arr.find((a)=>a.group === group) : null;
12329
- };
12330
- for (const f of fields){
12331
- const m = f.match(/^(.+?)_g(\d+)__expr$/);
12332
- if (!m) {
12333
- labels[f] = {
12334
- short: $1fd2507769d5bd00$var$truncateLabel(f.replace(/__expr$/, ''), $1fd2507769d5bd00$var$AXIS_LABEL_MAX),
12335
- structured: {
12336
- studyTitle: f,
12337
- group: '',
12338
- factors: [],
12339
- characteristics: []
12340
- }
12341
- };
12342
- continue;
12343
- }
12344
- const expId = m[1].replace(/_/g, '-');
12345
- const group = 'g' + m[2];
12346
- const assay = findAssay(expId, group);
12347
- const study = studyById[expId];
12348
- const studyName = study && study.description || expId;
12349
- labels[f] = {
12350
- short: $1fd2507769d5bd00$var$truncateLabel($1fd2507769d5bd00$var$compactAssayLabel(assay, group), $1fd2507769d5bd00$var$AXIS_LABEL_MAX),
12351
- structured: {
12352
- studyTitle: studyName,
12353
- group: group,
12354
- factors: $1fd2507769d5bd00$var$assayPairs(assay && assay.factor),
12355
- characteristics: $1fd2507769d5bd00$var$assayPairs(assay && assay.characteristic)
12356
- }
12357
- };
12358
- }
12359
- return labels;
12360
- }
12361
- function $1fd2507769d5bd00$var$rowMatchesSelections(row, selections) {
12362
- for (const f of Object.keys(selections)){
12363
- const v = row[f];
12364
- if (v == null || Array.isArray(v)) return false;
12365
- const n = +v;
12366
- if (!Number.isFinite(n)) return false;
12367
- const [lo, hi] = selections[f];
12368
- if (n < lo || n > hi) return false;
12369
- }
12370
- return true;
12371
- }
12372
- function $1fd2507769d5bd00$var$fmt(n) {
12373
- if (!Number.isFinite(n)) return String(n);
12374
- const a = Math.abs(n);
12375
- if (a !== 0 && (a < 0.001 || a >= 1e6)) return n.toExponential(3);
12376
- return Number(n.toFixed(4)).toString();
12377
- }
12378
- function $1fd2507769d5bd00$var$tsvCell(v) {
12379
- if (v == null) return '';
12380
- if (Array.isArray(v)) return v.join(',');
12381
- const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
12382
- return s.replace(/[\t\r\n]+/g, ' ');
12383
- }
12384
- // Mirror the on-screen table header in the TSV: one row per metadata level
12385
- // the table is showing (Study, then one row per distinct factor type, then
12386
- // one row per distinct characteristic type), followed by the leaf header
12387
- // (Gene ID / Name / per-sample group). The first two columns are repurposed
12388
- // to carry the row category and the row's specific name, matching the
12389
- // pinned-column labels in ExprTable.
12390
- function $1fd2507769d5bd00$var$downloadTsv(filename, rows, fields, studies, expressionSamples) {
12391
- const cols = [
12392
- 'id',
12393
- 'name',
12394
- ...fields
12395
- ];
12396
- const fieldInfo = (0, $4ab64e76c1caef59$export$7b242440eb2c300d)(fields, studies, expressionSamples);
12397
- const factorTypes = new Set();
12398
- const charTypes = new Set();
12399
- for (const f of fields){
12400
- const info = fieldInfo[f];
12401
- if (!info) continue;
12402
- Object.keys(info.factors || {}).forEach((t)=>factorTypes.add(t));
12403
- Object.keys(info.characteristics || {}).forEach((t)=>charTypes.add(t));
12404
- }
12405
- const factorTypeList = Array.from(factorTypes).sort();
12406
- const charTypeList = Array.from(charTypes).sort();
12407
- const metaRow = (cat, label, getValue)=>{
12408
- const cells = [
12409
- cat,
12410
- label
12411
- ];
12412
- for (const f of fields)cells.push($1fd2507769d5bd00$var$tsvCell(getValue(fieldInfo[f] || {})));
12413
- return cells.join('\t');
12414
- };
12415
- const lines = [];
12416
- lines.push(metaRow('Study', 'Title', (info)=>info.studyDescription || ''));
12417
- for (const t of factorTypeList)lines.push(metaRow('Factor', t, (info)=>info.factors && info.factors[t] || ''));
12418
- for (const t of charTypeList)lines.push(metaRow('Characteristic', t, (info)=>info.characteristics && info.characteristics[t] || ''));
12419
- // Leaf header — column ids for the data rows.
12420
- lines.push([
12421
- 'Gene ID',
12422
- 'Name',
12423
- ...fields.map((f)=>{
12424
- const info = fieldInfo[f];
12425
- return info && info.group || f.replace(/__expr$/, '');
13992
+ if (t.bg.status === 'error') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13993
+ className: "gsea-panel",
13994
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
13995
+ children: [
13996
+ "Background error: ",
13997
+ t.bg.error
13998
+ ]
13999
+ })
14000
+ });
14001
+ if (!results) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
14002
+ className: "gsea-panel",
14003
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
14004
+ children: "No data."
12426
14005
  })
12427
- ].join('\t'));
12428
- for (const r of rows)lines.push(cols.map((c)=>$1fd2507769d5bd00$var$tsvCell(r[c])).join('\t'));
12429
- const blob = new Blob([
12430
- lines.join('\n') + '\n'
12431
- ], {
12432
- type: 'text/tab-separated-values'
12433
14006
  });
12434
- const url = URL.createObjectURL(blob);
12435
- const a = document.createElement('a');
12436
- a.href = url;
12437
- a.download = filename;
12438
- document.body.appendChild(a);
12439
- a.click();
12440
- document.body.removeChild(a);
12441
- URL.revokeObjectURL(url);
12442
- }
12443
- const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expressionSamples: expressionSamples, tabState: tabState, onOpenFields: onOpenFields, onLoad: onLoad, onReorder: onReorder, onAddRangeQuery: onAddRangeQuery })=>{
12444
- const selected = tabState && tabState.selectedFields || [];
12445
- const rows = tabState && tabState.rows || [];
12446
- const fetchInfo = tabState && tabState.fetch || {
12447
- status: 'idle',
12448
- total: 0
12449
- };
12450
- const [scale, setScale] = (0, $gXNCa$react.useState)('linear');
12451
- const [selections, setSelections] = (0, $gXNCa$react.useState)({});
12452
- const [clearVersion, setClearVersion] = (0, $gXNCa$react.useState)(0);
12453
- const [hoveredId, setHoveredId] = (0, $gXNCa$react.useState)(null);
12454
- const [plotHeight, setPlotHeight] = (0, $gXNCa$react.useState)(320);
12455
- const resizeStateRef = (0, $gXNCa$react.useRef)(null);
12456
- // Drag the horizontal separator between the plot and the table to retune
12457
- // their relative sizes. Bounded so neither pane disappears entirely.
12458
- const startResize = (e)=>{
14007
+ const allBlocks = ui.ontology === 'all' ? Object.values(results) : results[ui.ontology] ? [
14008
+ results[ui.ontology]
14009
+ ] : [];
14010
+ // Hide ontologies that aren't used in this species at all.
14011
+ const blocks = allBlocks.filter((b)=>b.tested > 0);
14012
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
14013
+ className: "gsea-panel",
14014
+ children: [
14015
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
14016
+ className: "gsea-summary",
14017
+ children: [
14018
+ "Foreground: ",
14019
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("b", {
14020
+ children: t.fg.numFound.toLocaleString()
14021
+ }),
14022
+ " genes \xb7 Background: ",
14023
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("b", {
14024
+ children: t.bg.numFound.toLocaleString()
14025
+ }),
14026
+ " genes"
14027
+ ]
14028
+ }),
14029
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$ControlsBar, {
14030
+ ui: ui,
14031
+ onChange: onUiChange
14032
+ }),
14033
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Accordion), {
14034
+ alwaysOpen: true,
14035
+ defaultActiveKey: blocks.length === 1 ? blocks[0].ontology : undefined,
14036
+ children: blocks.map((b)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$OntologySection, {
14037
+ block: b,
14038
+ search: ui.search,
14039
+ onAddFilter: onAddFilter
14040
+ }, b.ontology))
14041
+ })
14042
+ ]
14043
+ });
14044
+ };
14045
+ const $f511198465122338$var$GseaViewCmp = (props)=>{
14046
+ const { grameneSearch: grameneSearch, grameneMaps: grameneMaps, grameneTaxonomy: grameneTaxonomy, gsea: gsea, gseaUI: ui, gseaResults: results, doSetGseaActiveTaxon: doSetGseaActiveTaxon, doSetGseaUI: doSetGseaUI, doFetchGseaForeground: doFetchGseaForeground, doFetchGseaBackground: doFetchGseaBackground, doAcceptGrameneSuggestion: doAcceptGrameneSuggestion } = props;
14047
+ const taxa = (0, $gXNCa$react.useMemo)(()=>{
14048
+ if (!grameneSearch || !grameneSearch.facet_counts) return [];
14049
+ const arr = grameneSearch.facet_counts.facet_fields.taxon_id || [];
14050
+ const ids = [];
14051
+ const counts = {};
14052
+ for(let i = 0; i < arr.length; i += 2){
14053
+ ids.push(arr[i]);
14054
+ counts[arr[i]] = +arr[i + 1];
14055
+ }
14056
+ if (grameneMaps) ids.sort((a, b)=>{
14057
+ const ma = grameneMaps[a] || grameneMaps[$f511198465122338$var$speciesTaxonId(a)];
14058
+ const mb = grameneMaps[b] || grameneMaps[$f511198465122338$var$speciesTaxonId(b)];
14059
+ return (ma && ma.left_index || 0) - (mb && mb.left_index || 0);
14060
+ });
14061
+ return ids.map((tid)=>({
14062
+ tid: tid,
14063
+ count: counts[tid]
14064
+ }));
14065
+ }, [
14066
+ grameneSearch,
14067
+ grameneMaps
14068
+ ]);
14069
+ const activeTaxon = gsea.activeTaxon;
14070
+ const [treeWidth, setTreeWidth] = (0, $gXNCa$react.useState)(280);
14071
+ const beginResize = (e)=>{
12459
14072
  e.preventDefault();
12460
- resizeStateRef.current = {
12461
- startY: e.clientY,
12462
- startHeight: plotHeight
12463
- };
14073
+ const startX = e.clientX;
14074
+ const startWidth = treeWidth;
12464
14075
  const onMove = (ev)=>{
12465
- const s = resizeStateRef.current;
12466
- if (!s) return;
12467
- const next = Math.max(120, Math.min(1200, s.startHeight + (ev.clientY - s.startY)));
12468
- setPlotHeight(next);
14076
+ const next = Math.max(150, Math.min(800, startWidth + (ev.clientX - startX)));
14077
+ setTreeWidth(next);
12469
14078
  };
12470
14079
  const onUp = ()=>{
12471
- resizeStateRef.current = null;
12472
14080
  document.removeEventListener('mousemove', onMove);
12473
14081
  document.removeEventListener('mouseup', onUp);
12474
- document.body.style.cursor = '';
12475
14082
  document.body.style.userSelect = '';
14083
+ document.body.style.cursor = '';
12476
14084
  };
12477
- document.body.style.cursor = 'row-resize';
12478
14085
  document.body.style.userSelect = 'none';
14086
+ document.body.style.cursor = 'col-resize';
12479
14087
  document.addEventListener('mousemove', onMove);
12480
14088
  document.addEventListener('mouseup', onUp);
12481
14089
  };
12482
- const hasBrush = Object.keys(selections).length > 0;
12483
- const filteredRows = (0, $gXNCa$react.useMemo)(()=>{
12484
- if (!hasBrush) return rows;
12485
- return rows.filter((r)=>$1fd2507769d5bd00$var$rowMatchesSelections(r, selections));
12486
- }, [
12487
- rows,
12488
- selections,
12489
- hasBrush
12490
- ]);
12491
- // Drop fields with no numeric data in the loaded rows so empty axes/columns
12492
- // don't clutter the visualization. Selected-but-empty fields stay in the
12493
- // underlying selection so a future load can repopulate them.
12494
- const visibleFields = (0, $gXNCa$react.useMemo)(()=>{
12495
- if (rows.length === 0 || selected.length === 0) return selected;
12496
- return selected.filter((f)=>rows.some((r)=>{
12497
- const v = r[f];
12498
- return v != null && !Array.isArray(v) && Number.isFinite(+v);
12499
- }));
14090
+ (0, $gXNCa$react.useEffect)(()=>{
14091
+ if (taxa.length === 0) return;
14092
+ if (!activeTaxon || !taxa.find((t)=>String(t.tid) === String(activeTaxon))) doSetGseaActiveTaxon(taxa[0].tid);
12500
14093
  }, [
12501
- rows,
12502
- selected
12503
- ]);
12504
- const handleReorder = onReorder ? (newVisibleOrder)=>{
12505
- const visibleSet = new Set(newVisibleOrder);
12506
- const hidden = selected.filter((f)=>!visibleSet.has(f));
12507
- onReorder([
12508
- ...newVisibleOrder,
12509
- ...hidden
12510
- ]);
12511
- } : undefined;
12512
- const axisLabels = (0, $gXNCa$react.useMemo)(()=>$1fd2507769d5bd00$var$buildAxisLabels(visibleFields, studies, expressionSamples), [
12513
- visibleFields,
12514
- studies,
12515
- expressionSamples
14094
+ taxa,
14095
+ activeTaxon,
14096
+ doSetGseaActiveTaxon
12516
14097
  ]);
12517
14098
  (0, $gXNCa$react.useEffect)(()=>{
12518
- if (rows.length === 0 && hasBrush) {
12519
- setSelections({});
12520
- setClearVersion((v)=>v + 1);
12521
- }
14099
+ if (!activeTaxon) return;
14100
+ const t = gsea.byTaxon[activeTaxon];
14101
+ if (!t) return;
14102
+ if (t.fg.status === 'idle') doFetchGseaForeground(activeTaxon);
14103
+ if (t.bg.status === 'idle') doFetchGseaBackground(activeTaxon);
12522
14104
  }, [
12523
- rows.length,
12524
- hasBrush
14105
+ activeTaxon,
14106
+ gsea,
14107
+ doFetchGseaForeground,
14108
+ doFetchGseaBackground
12525
14109
  ]);
14110
+ if (taxa.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
14111
+ className: "gsea-view",
14112
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
14113
+ children: "No species in the current results."
14114
+ })
14115
+ });
14116
+ if (!grameneTaxonomy) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
14117
+ className: "gsea-view",
14118
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
14119
+ children: "Loading taxonomy\u2026"
14120
+ })
14121
+ });
14122
+ const handleAddFilter = (row)=>{
14123
+ if (!doAcceptGrameneSuggestion) return;
14124
+ const label = row.term_name ? `${row.term_display_id} ${row.term_name}` : row.term_display_id;
14125
+ doAcceptGrameneSuggestion({
14126
+ fq_field: row.field,
14127
+ fq_value: String(row.term_id),
14128
+ name: label,
14129
+ category: row.ontology_label
14130
+ });
14131
+ };
12526
14132
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12527
- className: "exprviz-tab-panel",
14133
+ className: "gsea-view gsea-layout",
12528
14134
  children: [
12529
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12530
- className: "exprviz-toolbar",
12531
- children: [
12532
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Button), {
12533
- size: "sm",
12534
- onClick: onOpenFields,
12535
- children: [
12536
- "Select fields (",
12537
- selected.length,
12538
- " selected, ",
12539
- studies.length,
12540
- " studies)"
12541
- ]
12542
- }),
12543
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
12544
- size: "sm",
12545
- variant: "primary",
12546
- disabled: selected.length === 0 || fetchInfo.status === 'loading',
12547
- onClick: onLoad,
12548
- children: fetchInfo.status === 'loading' ? "Loading\u2026" : 'Load data'
12549
- }),
12550
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.ToggleButtonGroup), {
12551
- type: "radio",
12552
- name: `exprviz-scale-${taxon}`,
12553
- size: "sm",
12554
- value: scale,
12555
- onChange: setScale,
12556
- children: [
12557
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.ToggleButton), {
12558
- id: `exprviz-scale-${taxon}-lin`,
12559
- value: "linear",
12560
- variant: "outline-secondary",
12561
- children: "Linear"
12562
- }),
12563
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.ToggleButton), {
12564
- id: `exprviz-scale-${taxon}-log`,
12565
- value: "log",
12566
- variant: "outline-secondary",
12567
- children: "Log"
12568
- })
12569
- ]
12570
- }),
12571
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
12572
- size: "sm",
12573
- variant: "outline-secondary",
12574
- disabled: !hasBrush,
12575
- onClick: ()=>{
12576
- setClearVersion((v)=>v + 1);
12577
- setSelections({});
12578
- },
12579
- children: "Clear brushes"
12580
- }),
12581
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
12582
- size: "sm",
12583
- variant: "outline-secondary",
12584
- disabled: filteredRows.length === 0 || visibleFields.length === 0,
12585
- onClick: ()=>$1fd2507769d5bd00$var$downloadTsv(`expression_${taxon}.tsv`, filteredRows, visibleFields, studies, expressionSamples),
12586
- title: "Download the visible rows and columns as tab-delimited text",
12587
- children: "Download TSV"
12588
- }),
12589
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
12590
- size: "sm",
12591
- variant: "success",
12592
- disabled: !hasBrush || !onAddRangeQuery,
12593
- onClick: ()=>{
12594
- const terms = Object.keys(selections).map((field)=>{
12595
- const [lo, hi] = selections[field];
12596
- return {
12597
- category: 'Expression',
12598
- name: `${field}: ${$1fd2507769d5bd00$var$fmt(lo)}\u{2013}${$1fd2507769d5bd00$var$fmt(hi)}`,
12599
- fq_field: field,
12600
- fq_value: `[${lo} TO ${hi}]`
12601
- };
12602
- });
12603
- onAddRangeQuery(terms);
12604
- },
12605
- title: "Add brush ranges as an AND-conjunction filter on the search",
12606
- children: "Apply as filter"
12607
- }),
12608
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
12609
- className: "exprviz-status",
12610
- children: [
12611
- hasBrush ? `${filteredRows.length} of ${rows.length}` : rows.length,
12612
- fetchInfo.total ? ` / ${fetchInfo.total}` : '',
12613
- " genes",
12614
- hasBrush ? ' (brushed)' : ' loaded'
12615
- ]
12616
- })
12617
- ]
14135
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
14136
+ className: "gsea-layout-tree",
14137
+ style: {
14138
+ flex: `0 0 ${treeWidth}px`
14139
+ },
14140
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$SpeciesTree, {
14141
+ taxonomy: grameneTaxonomy,
14142
+ grameneMaps: grameneMaps,
14143
+ taxa: taxa,
14144
+ activeTaxon: activeTaxon,
14145
+ onSelect: (tid)=>doSetGseaActiveTaxon(String(tid))
14146
+ })
12618
14147
  }),
12619
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12620
- className: "exprviz-body",
12621
- children: [
12622
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12623
- className: "exprviz-plot",
12624
- style: {
12625
- height: plotHeight
12626
- },
12627
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $caf32827df861c4e$export$2e2bcd8739ae039), {
12628
- rows: rows,
12629
- fields: visibleFields,
12630
- scale: scale,
12631
- onBrushChange: setSelections,
12632
- onReorder: handleReorder,
12633
- clearVersion: clearVersion,
12634
- hoveredId: hoveredId,
12635
- axisLabels: axisLabels
12636
- })
12637
- }),
12638
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12639
- className: "exprviz-resizer",
12640
- role: "separator",
12641
- "aria-orientation": "horizontal",
12642
- "aria-label": "Resize plot",
12643
- onMouseDown: startResize,
12644
- title: "Drag to resize"
12645
- }),
12646
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12647
- className: "exprviz-table",
12648
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $4ab64e76c1caef59$export$2e2bcd8739ae039), {
12649
- rows: filteredRows,
12650
- fields: visibleFields,
12651
- onReorder: handleReorder,
12652
- studies: studies,
12653
- expressionSamples: expressionSamples,
12654
- onHoverRow: setHoveredId
12655
- })
12656
- })
12657
- ]
14148
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
14149
+ className: "gsea-splitter",
14150
+ onMouseDown: beginResize,
14151
+ title: "Drag to resize"
14152
+ }),
14153
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
14154
+ className: "gsea-layout-panel",
14155
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($f511198465122338$var$TaxonPanel, {
14156
+ taxon: activeTaxon ? String(activeTaxon) : null,
14157
+ gsea: gsea,
14158
+ results: results,
14159
+ ui: ui,
14160
+ onUiChange: doSetGseaUI,
14161
+ onAddFilter: handleAddFilter
14162
+ })
12658
14163
  })
12659
14164
  ]
12660
14165
  });
12661
14166
  };
12662
- var $1fd2507769d5bd00$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectExprViz', 'selectExprVizPivot', 'selectExprVizActiveTaxon', 'selectGrameneMaps', 'selectExpressionStudies', 'selectExpressionSamples', 'doSetExprVizActiveTaxon', 'doToggleExprVizFieldsModal', 'doFetchExprVizData', 'doReorderExprVizFields', 'doAddGrameneRangeQuery', $1fd2507769d5bd00$var$ExprVizViewCmp);
14167
+ var $f511198465122338$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneSearch', 'selectGrameneMaps', 'selectGrameneTaxonomy', 'selectGsea', 'selectGseaUI', 'selectGseaResults', 'doSetGseaActiveTaxon', 'doSetGseaUI', 'doFetchGseaForeground', 'doFetchGseaBackground', 'doAcceptGrameneSuggestion', $f511198465122338$var$GseaViewCmp);
12663
14168
 
12664
14169
 
12665
14170
 
@@ -12780,6 +14285,7 @@ const $693dd8c7a5607c3a$var$inventory = {
12780
14285
  attribs: (0, $67bf5a43401bffdc$export$2e2bcd8739ae039),
12781
14286
  expression: (0, $261baeb81c4d4d8a$export$2e2bcd8739ae039),
12782
14287
  exprViz: (0, $1fd2507769d5bd00$export$2e2bcd8739ae039),
14288
+ gsea: (0, $f511198465122338$export$2e2bcd8739ae039),
12783
14289
  userLists: (0, $0f50f369018a42ef$export$2e2bcd8739ae039),
12784
14290
  export: (0, $37b3bb0145d266b0$export$2e2bcd8739ae039)
12785
14291
  };