gramene-search 2.0.4 → 2.0.6

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
@@ -351,8 +351,16 @@ const $9d9aeaf9299e61a1$var$expressionStudies = (0, $gXNCa$reduxbundler.createAs
351
351
  return fetch(`${store.selectGrameneAPI()}/experiments?rows=-1`).then((res)=>res.json()).then((res)=>(0, ($parcel$interopDefault($gXNCa$lodash))).groupBy(res, 'taxon_id'));
352
352
  }
353
353
  });
354
- $9d9aeaf9299e61a1$var$expressionStudies.reactExpressionStudies = (0, $gXNCa$reduxbundler.createSelector)('selectExpressionStudiesShouldUpdate', (shouldUpdate)=>{
355
- if (shouldUpdate) return {
354
+ // Only fetch when a view that consumes expression studies is enabled.
355
+ const $9d9aeaf9299e61a1$var$EXPRESSION_VIEWS = [
356
+ 'exprViz',
357
+ 'expression',
358
+ 'export'
359
+ ];
360
+ $9d9aeaf9299e61a1$var$expressionStudies.reactExpressionStudies = (0, $gXNCa$reduxbundler.createSelector)('selectExpressionStudiesShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
361
+ if (!shouldUpdate) return;
362
+ if (!viewsOn || !$9d9aeaf9299e61a1$var$EXPRESSION_VIEWS.some((id)=>viewsOn.has(id))) return;
363
+ return {
356
364
  actionCreator: 'doFetchExpressionStudies'
357
365
  };
358
366
  });
@@ -372,8 +380,10 @@ const $9d9aeaf9299e61a1$var$expressionSamples = (0, $gXNCa$reduxbundler.createAs
372
380
  });
373
381
  }
374
382
  });
375
- $9d9aeaf9299e61a1$var$expressionSamples.reactExpressionSamples = (0, $gXNCa$reduxbundler.createSelector)('selectExpressionSamplesShouldUpdate', (shouldUpdate)=>{
376
- if (shouldUpdate) return {
383
+ $9d9aeaf9299e61a1$var$expressionSamples.reactExpressionSamples = (0, $gXNCa$reduxbundler.createSelector)('selectExpressionSamplesShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
384
+ if (!shouldUpdate) return;
385
+ if (!viewsOn || !$9d9aeaf9299e61a1$var$EXPRESSION_VIEWS.some((id)=>viewsOn.has(id))) return;
386
+ return {
377
387
  actionCreator: 'doFetchExpressionSamples'
378
388
  };
379
389
  });
@@ -385,8 +395,12 @@ const $9d9aeaf9299e61a1$var$curatedGenes = (0, $gXNCa$reduxbundler.createAsyncRe
385
395
  return fetch(`https://devdata.gramene.org/curation/curations?rows=0&minFlagged=0&since=12-12-2029`).then((res)=>res.json()).then((curation)=>(0, ($parcel$interopDefault($gXNCa$lodash))).keyBy(curation.genes, 'gene_id'));
386
396
  }
387
397
  });
388
- $9d9aeaf9299e61a1$var$curatedGenes.reactCuratedGenes = (0, $gXNCa$reduxbundler.createSelector)('selectCuratedGenesShouldUpdate', (shouldUpdate)=>{
389
- if (shouldUpdate) return {
398
+ // Curated annotations are consumed by GeneList rows and Homology details,
399
+ // both inside the gene-list view.
400
+ $9d9aeaf9299e61a1$var$curatedGenes.reactCuratedGenes = (0, $gXNCa$reduxbundler.createSelector)('selectCuratedGenesShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
401
+ if (!shouldUpdate) return;
402
+ if (!viewsOn || !viewsOn.has('list')) return;
403
+ return {
390
404
  actionCreator: 'doFetchCuratedGenes'
391
405
  };
392
406
  });
@@ -403,8 +417,11 @@ const $9d9aeaf9299e61a1$var$grameneGermplasm = (0, $gXNCa$reduxbundler.createAsy
403
417
  });
404
418
  }
405
419
  });
406
- $9d9aeaf9299e61a1$var$grameneGermplasm.reactGrameneGermplasm = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneGermplasmShouldUpdate', (shouldUpdate)=>{
407
- if (shouldUpdate) return {
420
+ // Germplasm metadata is consumed by the VEP detail panel inside the gene list.
421
+ $9d9aeaf9299e61a1$var$grameneGermplasm.reactGrameneGermplasm = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneGermplasmShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
422
+ if (!shouldUpdate) return;
423
+ if (!viewsOn || !viewsOn.has('list')) return;
424
+ return {
408
425
  actionCreator: 'doFetchGrameneGermplasm'
409
426
  };
410
427
  });
@@ -522,13 +539,13 @@ const $9d9aeaf9299e61a1$var$grameneGeneAttribs = (0, $gXNCa$reduxbundler.createA
522
539
  return fetch(`${store.selectGrameneAPI()}/geneAttributes`).then((res)=>$9d9aeaf9299e61a1$var$geneAttribs);
523
540
  }
524
541
  });
525
- $9d9aeaf9299e61a1$var$grameneGeneAttribs.reactGrameneGeneAttribs = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneGeneAttribsShouldUpdate', 'selectGrameneFiltersStatus', 'selectGrameneViews', (shouldUpdate, status, views)=>{
526
- if (shouldUpdate && (status === 'finished' || status === 'ready')) {
527
- const byId = (0, ($parcel$interopDefault($gXNCa$lodash))).keyBy(views.options, 'id');
528
- if (byId.attribs.show === "on") return {
529
- actionCreator: 'doFetchGrameneGeneAttribs'
530
- };
531
- }
542
+ $9d9aeaf9299e61a1$var$grameneGeneAttribs.reactGrameneGeneAttribs = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneGeneAttribsShouldUpdate', 'selectGrameneFiltersStatus', 'selectGrameneViewsOn', (shouldUpdate, status, viewsOn)=>{
543
+ if (!shouldUpdate) return;
544
+ if (status !== 'finished' && status !== 'ready') return;
545
+ if (!viewsOn || !viewsOn.has('attribs')) return;
546
+ return {
547
+ actionCreator: 'doFetchGrameneGeneAttribs'
548
+ };
532
549
  });
533
550
  const $9d9aeaf9299e61a1$var$grameneSearch = (0, $gXNCa$reduxbundler.createAsyncResourceBundle)({
534
551
  name: 'grameneSearch',
@@ -538,8 +555,8 @@ const $9d9aeaf9299e61a1$var$grameneSearch = (0, $gXNCa$reduxbundler.createAsyncR
538
555
  const offset = store.selectGrameneSearchOffset();
539
556
  const rows = store.selectGrameneSearchRows();
540
557
  const g = store.selectGrameneGenomes();
541
- const m = store.selectGrameneMaps();
542
- const taxa = Object.keys(g.active).filter((tid)=>!m[tid].hidden);
558
+ const m = store.selectGrameneMaps() || {};
559
+ const taxa = Object.keys(g && g.active || {}).filter((tid)=>m[tid] && !m[tid].hidden);
543
560
  let fq = '';
544
561
  if (taxa.length) {
545
562
  console.log('search add a fq for ', taxa);
@@ -1568,6 +1585,15 @@ const $24971af0a229e0e3$var$grameneViews = {
1568
1585
  ...raw,
1569
1586
  options: options
1570
1587
  };
1588
+ }),
1589
+ // Set of view ids whose `show` is currently 'on'. Used by data bundles to
1590
+ // gate their auto-fetch reactors so we don't pay the cost of loading data
1591
+ // for views the user has turned off.
1592
+ selectGrameneViewsOn: (0, $gXNCa$reduxbundler.createSelector)('selectGrameneViews', (views)=>{
1593
+ const ids = new Set();
1594
+ const opts = views && views.options || [];
1595
+ for (const v of opts)if (v && v.show === 'on') ids.add(v.id);
1596
+ return ids;
1571
1597
  })
1572
1598
  };
1573
1599
  var $24971af0a229e0e3$export$2e2bcd8739ae039 = $24971af0a229e0e3$var$grameneViews;
@@ -2050,7 +2076,7 @@ var $0d54502f6cafe273$export$2e2bcd8739ae039 = $0d54502f6cafe273$var$grameneGeno
2050
2076
 
2051
2077
 
2052
2078
  var $65709bd8598fce20$exports = {};
2053
- $65709bd8598fce20$exports = JSON.parse('{"groups":{"core":{"label":"Core identifiers","order":0},"location":{"label":"Genomic location","order":1},"structure":{"label":"Gene structure","order":2},"homology":{"label":"Homology","order":3},"expression":{"label":"Gene expression","order":4},"differential":{"label":"Differential expression","order":5},"hierarchical":{"label":"Hierarchical annotations","order":6},"pathways":{"label":"Pathways","order":7},"GO":{"label":"Gene Ontology","order":7},"PO":{"label":"Plant Ontology","order":8},"lof":{"label":"Loss of function alleles","order":9},"MAKER":{"label":"MAKER transcript metrics","order":10},"xrefs":{"label":"External references","order":11},"other":{"label":"Other","order":99}},"patterns":[{"id":"homology","match":"^homology__(.+)$","group":"homology","multiValued":true,"labelTemplate":"Homology: $1"},{"id":"pathways","match":"^pathways__(.+)$","group":"pathways","multiValued":true,"labelTemplate":"Pathway: $1"},{"id":"maker","match":"^MAKER__(.+)__attr_([a-z])$","group":"MAKER","labelTemplate":"MAKER: $1"},{"id":"xrefs","match":"^(.+)__xrefs$","group":"xrefs","multiValued":true,"labelTemplate":"$1 (xrefs)"},{"id":"expr","match":"^(E[-_][A-Za-z0-9_-]+?)_g(\\\\d+)__expr$","group":"expression","expression":true,"labelTemplate":"$1 \xb7 g$2"},{"id":"diffexpr","match":"^(E[-_][A-Za-z0-9_-]+?)_g(\\\\d+)_g(\\\\d+)_(pval|logfc|l2fc)_attr_([a-z])$","group":"differential","diffExpression":true,"labelTemplate":"$1 \xb7 g$2 vs g$3 \xb7 $4"},{"id":"bins","match":"^(fixed|uniform)_([0-9a-zA-Z]+)__bin$","group":"bins","is_hidden":true,"labelTemplate":"$1 bin ($2)"},{"id":"neighbors","match":".*neighbors_[0-9]+$","group":"neighbors","is_hidden":true,"labelTemplate":"neighbors"},{"id":"vep_merged","match":"^VEP__merged__(NAT|EMS)__attr_ss$","group":"lof","multiValued":true,"labelTemplate":"Merged $1 accessions"},{"id":"vep_detail","match":"^VEP__(.+?)__(homo|het)__(.+?)__(\\\\d+)__attr_ss$","group":"lof","multiValued":true,"labelTemplate":"$1 ($2) $3/$4"},{"id":"generic_attr","match":"^(.+)__attr_([a-z])$","group":"other","labelTemplate":"$1"}],"fields":{"id":{"group":"core","label":"Gene ID","order":1},"name":{"group":"core","label":"Name","order":2},"description":{"group":"core","label":"Description","order":4},"summary":{"group":"core","label":"Summary","order":5},"synonyms":{"group":"core","label":"Synonyms","multiValued":true,"order":3},"biotype":{"group":"core","label":"Biotype","order":6},"taxon_id":{"group":"core","label":"Taxon ID","order":8},"system_name":{"group":"core","label":"System name","order":7},"db_type":{"group":"core","label":"DB type","order":9},"closest_rep_id":{"group":"homology","label":"Closest representative ID","order":1},"closest_rep_name":{"group":"homology","label":"Closest representative name","order":2},"closest_rep_identity":{"group":"homology","label":"Closest representative identity","order":3},"closest_rep_taxon_id":{"group":"homology","label":"Closest representative taxon","order":4},"closest_rep_description":{"group":"homology","label":"Closest representative description","order":5},"model_rep_id":{"group":"homology","label":"Model representative ID","order":6},"model_rep_name":{"group":"homology","label":"Model representative name","order":7},"model_rep_identity":{"group":"homology","label":"Model representative identity","order":8},"model_rep_taxon_id":{"group":"homology","label":"Model representative taxon","order":9},"model_rep_description":{"group":"homology","label":"Model representative description","order":10},"gene_tree":{"group":"homology","label":"Gene tree ID","order":11},"pan_tree":{"group":"homology","label":"Pan-gene tree ID","order":14},"capabilities":{"group":"other","label":"Capabilities","multiValued":true},"map":{"group":"location","label":"Map","order":1},"region":{"group":"location","label":"Region","order":2},"start":{"group":"location","label":"Start","order":3},"end":{"group":"location","label":"End","order":4},"strand":{"group":"location","label":"Strand","order":5},"GO__ancestors":{"group":"hierarchical","label":"GO terms","multiValued":true,"order":1},"PO__ancestors":{"group":"hierarchical","label":"PO terms","multiValued":true,"order":2},"TO__ancestors":{"group":"hierarchical","label":"TO terms","multiValued":true,"order":3},"QTL_TO__ancestors":{"group":"hierarchical","label":"QTL traits (TO)","multiValued":true,"order":7},"domains__ancestors":{"group":"hierarchical","label":"Domains","multiValued":true,"order":4},"taxonomy__ancestors":{"group":"hierarchical","label":"Taxonomy","multiValued":true,"order":6},"supertree_attr_s":{"group":"homology","label":"Supertree","order":13},"gene_tree_root_taxon_id":{"group":"homology","label":"Gene tree root taxon","order":12},"protein__length":{"group":"structure","label":"Protein length"},"transcript__count":{"group":"structure","label":"Transcript count"},"transcript__exons":{"group":"structure","label":"Exon count"},"transcript__length":{"group":"structure","label":"Transcript length"},"expressed_in_gxa_attr_ss":{"group":"expression","label":"Expressed in GXA","multiValued":true},"MAKER__AED__attr_f":{"group":"MAKER","label":"AED","description":"Annotation Edit Distance"},"MAKER__QI1__attr_i":{"group":"MAKER","label":"QI1: Length of the 5\' UTR"},"MAKER__QI2__attr_f":{"group":"MAKER","label":"QI2: Fraction of splice sites confirmed by EST"},"MAKER__QI3__attr_f":{"group":"MAKER","label":"QI3: Fraction of exons overlapping an EST"},"MAKER__QI4__attr_f":{"group":"MAKER","label":"QI4: Fraction of exons overlapping EST or protein"},"MAKER__QI5__attr_f":{"group":"MAKER","label":"QI5: Fraction of splice sites confirmed by SNAP"},"MAKER__QI6__attr_f":{"group":"MAKER","label":"QI6: Fraction of exons overlapping a SNAP"},"MAKER__QI7__attr_i":{"group":"MAKER","label":"QI7: Number of exons in the mRNA"},"MAKER__QI8__attr_i":{"group":"MAKER","label":"QI8: Length of the 3\' UTR"},"MAKER__QI9__attr_i":{"group":"MAKER","label":"QI9: Length of the protein sequence"},"homology__all_orthologs":{"group":"homology","label":"All orthologs","multiValued":true,"order":15},"homology__ortholog_one2one":{"group":"homology","label":"1:1 orthologs","multiValued":true,"order":16},"homology__ortholog_one2many":{"group":"homology","label":"1:many orthologs","multiValued":true,"order":17},"homology__ortholog_many2many":{"group":"homology","label":"Many:many orthologs","multiValued":true,"order":18},"homology__syntenic_ortholog_one2one":{"group":"homology","label":"Syntenic 1:1 orthologs","multiValued":true,"order":19},"homology__within_species_paralog":{"group":"homology","label":"Within-species paralogs","multiValued":true,"order":20},"pathways__ancestors":{"group":"hierarchical","label":"Pathways","multiValued":true,"order":5},"canonical_transcript__attr_s":{"group":"structure","label":"Canonical transcript ID"}},"hidden":["_version_","_terms","score","gene_idx","gene_idx_multi","species_idx","compara_idx","compara_idx_multi","_id","annotations","bins","gene_structure","homology","location","xrefs","domain_roots","familyRoot__ancestors","taxonomy__ancestors","capabilities","saved_search"]}');
2079
+ $65709bd8598fce20$exports = JSON.parse('{"groups":{"core":{"label":"Core identifiers","order":0},"location":{"label":"Genomic location","order":1},"structure":{"label":"Gene structure","order":2},"homology":{"label":"Homology","order":3},"expression":{"label":"Gene expression","order":4},"differential":{"label":"Differential expression","order":5},"hierarchical":{"label":"Hierarchical annotations","order":6},"pathways":{"label":"Pathways","order":7},"GO":{"label":"Gene Ontology","order":7},"PO":{"label":"Plant Ontology","order":8},"lof":{"label":"Loss of function alleles","order":9},"MAKER":{"label":"MAKER transcript metrics","order":10},"xrefs":{"label":"External references","order":11},"other":{"label":"Other","order":99}},"patterns":[{"id":"homology","match":"^homology__(.+)$","group":"homology","multiValued":true,"labelTemplate":"Homology: $1"},{"id":"pathways","match":"^pathways__(.+)$","group":"pathways","multiValued":true,"labelTemplate":"Pathway: $1"},{"id":"maker","match":"^MAKER__(.+)__attr_([a-z])$","group":"MAKER","labelTemplate":"MAKER: $1"},{"id":"xrefs","match":"^(.+)__xrefs$","group":"xrefs","multiValued":true,"labelTemplate":"$1 (xrefs)"},{"id":"expr","match":"^(E[-_][A-Za-z0-9_-]+?)_g(\\\\d+)__expr$","group":"expression","expression":true,"labelTemplate":"$1 \xb7 g$2"},{"id":"diffexpr","match":"^(E[-_][A-Za-z0-9_-]+?)_g(\\\\d+)_g(\\\\d+)_(pval|l2fc)_attr_([a-z])$","group":"differential","diffExpression":true,"labelTemplate":"$1 \xb7 g$2 vs g$3 \xb7 $4"},{"id":"bins","match":"^(fixed|uniform)_([0-9a-zA-Z]+)__bin$","group":"bins","is_hidden":true,"labelTemplate":"$1 bin ($2)"},{"id":"neighbors","match":".*neighbors_[0-9]+$","group":"neighbors","is_hidden":true,"labelTemplate":"neighbors"},{"id":"vep_merged","match":"^VEP__merged__(NAT|EMS)__attr_ss$","group":"lof","multiValued":true,"labelTemplate":"Merged $1 accessions"},{"id":"vep_detail","match":"^VEP__(.+?)__(homo|het)__(.+?)__(\\\\d+)__attr_ss$","group":"lof","multiValued":true,"labelTemplate":"$1 ($2) $3/$4"},{"id":"generic_attr","match":"^(.+)__attr_([a-z])$","group":"other","labelTemplate":"$1"}],"fields":{"id":{"group":"core","label":"Gene ID","order":1},"name":{"group":"core","label":"Name","order":2},"description":{"group":"core","label":"Description","order":4},"summary":{"group":"core","label":"Summary","order":5},"synonyms":{"group":"core","label":"Synonyms","multiValued":true,"order":3},"biotype":{"group":"core","label":"Biotype","order":6},"taxon_id":{"group":"core","label":"Taxon ID","order":8},"system_name":{"group":"core","label":"System name","order":7},"db_type":{"group":"core","label":"DB type","order":9},"closest_rep_id":{"group":"homology","label":"Closest representative ID","order":1},"closest_rep_name":{"group":"homology","label":"Closest representative name","order":2},"closest_rep_identity":{"group":"homology","label":"Closest representative identity","order":3},"closest_rep_taxon_id":{"group":"homology","label":"Closest representative taxon","order":4},"closest_rep_description":{"group":"homology","label":"Closest representative description","order":5},"model_rep_id":{"group":"homology","label":"Model representative ID","order":6},"model_rep_name":{"group":"homology","label":"Model representative name","order":7},"model_rep_identity":{"group":"homology","label":"Model representative identity","order":8},"model_rep_taxon_id":{"group":"homology","label":"Model representative taxon","order":9},"model_rep_description":{"group":"homology","label":"Model representative description","order":10},"gene_tree":{"group":"homology","label":"Gene tree ID","order":11},"pan_tree":{"group":"homology","label":"Pan-gene tree ID","order":14},"capabilities":{"group":"other","label":"Capabilities","multiValued":true},"map":{"group":"location","label":"Map","order":1},"region":{"group":"location","label":"Region","order":2},"start":{"group":"location","label":"Start","order":3},"end":{"group":"location","label":"End","order":4},"strand":{"group":"location","label":"Strand","order":5},"GO__ancestors":{"group":"hierarchical","label":"GO terms","multiValued":true,"order":1},"PO__ancestors":{"group":"hierarchical","label":"PO terms","multiValued":true,"order":2},"TO__ancestors":{"group":"hierarchical","label":"TO terms","multiValued":true,"order":3},"QTL_TO__ancestors":{"group":"hierarchical","label":"QTL traits (TO)","multiValued":true,"order":7},"domains__ancestors":{"group":"hierarchical","label":"Domains","multiValued":true,"order":4},"taxonomy__ancestors":{"group":"hierarchical","label":"Taxonomy","multiValued":true,"order":6},"supertree_attr_s":{"group":"homology","label":"Supertree","order":13},"gene_tree_root_taxon_id":{"group":"homology","label":"Gene tree root taxon","order":12},"protein__length":{"group":"structure","label":"Protein length"},"transcript__count":{"group":"structure","label":"Transcript count"},"transcript__exons":{"group":"structure","label":"Exon count"},"transcript__length":{"group":"structure","label":"Transcript length"},"expressed_in_gxa_attr_ss":{"group":"expression","label":"Expressed in GXA","multiValued":true},"MAKER__AED__attr_f":{"group":"MAKER","label":"AED","description":"Annotation Edit Distance"},"MAKER__QI1__attr_i":{"group":"MAKER","label":"QI1: Length of the 5\' UTR"},"MAKER__QI2__attr_f":{"group":"MAKER","label":"QI2: Fraction of splice sites confirmed by EST"},"MAKER__QI3__attr_f":{"group":"MAKER","label":"QI3: Fraction of exons overlapping an EST"},"MAKER__QI4__attr_f":{"group":"MAKER","label":"QI4: Fraction of exons overlapping EST or protein"},"MAKER__QI5__attr_f":{"group":"MAKER","label":"QI5: Fraction of splice sites confirmed by SNAP"},"MAKER__QI6__attr_f":{"group":"MAKER","label":"QI6: Fraction of exons overlapping a SNAP"},"MAKER__QI7__attr_i":{"group":"MAKER","label":"QI7: Number of exons in the mRNA"},"MAKER__QI8__attr_i":{"group":"MAKER","label":"QI8: Length of the 3\' UTR"},"MAKER__QI9__attr_i":{"group":"MAKER","label":"QI9: Length of the protein sequence"},"homology__all_orthologs":{"group":"homology","label":"All orthologs","multiValued":true,"order":15},"homology__ortholog_one2one":{"group":"homology","label":"1:1 orthologs","multiValued":true,"order":16},"homology__ortholog_one2many":{"group":"homology","label":"1:many orthologs","multiValued":true,"order":17},"homology__ortholog_many2many":{"group":"homology","label":"Many:many orthologs","multiValued":true,"order":18},"homology__syntenic_ortholog_one2one":{"group":"homology","label":"Syntenic 1:1 orthologs","multiValued":true,"order":19},"homology__within_species_paralog":{"group":"homology","label":"Within-species paralogs","multiValued":true,"order":20},"pathways__ancestors":{"group":"hierarchical","label":"Pathways","multiValued":true,"order":5},"canonical_transcript__attr_s":{"group":"structure","label":"Canonical transcript ID"}},"hidden":["_version_","_terms","score","gene_idx","gene_idx_multi","species_idx","compara_idx","compara_idx_multi","_id","annotations","bins","gene_structure","homology","location","xrefs","domain_roots","familyRoot__ancestors","taxonomy__ancestors","capabilities","saved_search"]}');
2054
2080
 
2055
2081
 
2056
2082
  const $49d5cbca2ec74b2f$export$428c2f647a2a7545 = {
@@ -2287,6 +2313,19 @@ const $0f839422d0d8c772$var$grameneFieldCatalog = (0, $gXNCa$reduxbundler.create
2287
2313
  return fetch(url).then((r)=>r.text()).then((text)=>$0f839422d0d8c772$var$parseCsvHeader(text)).catch(()=>null);
2288
2314
  }
2289
2315
  });
2316
+ // Auto-fetch the field catalog only when a view that needs it (exprViz fields
2317
+ // modal, exporter field tree) is enabled. Other views never read it.
2318
+ const $0f839422d0d8c772$var$FIELD_CATALOG_VIEWS = [
2319
+ 'exprViz',
2320
+ 'export'
2321
+ ];
2322
+ $0f839422d0d8c772$var$grameneFieldCatalog.reactGrameneFieldCatalog = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneFieldCatalogShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
2323
+ if (!shouldUpdate) return;
2324
+ if (!viewsOn || !$0f839422d0d8c772$var$FIELD_CATALOG_VIEWS.some((id)=>viewsOn.has(id))) return;
2325
+ return {
2326
+ actionCreator: 'doFetchGrameneFieldCatalog'
2327
+ };
2328
+ });
2290
2329
  function $0f839422d0d8c772$var$assayFactorLabel(assay) {
2291
2330
  if (!assay) return '';
2292
2331
  const factors = Array.isArray(assay.factor) ? assay.factor : [];
@@ -2340,14 +2379,13 @@ function $0f839422d0d8c772$var$buildSpeciesNameIndex(grameneMaps) {
2340
2379
  function $0f839422d0d8c772$var$fieldExperimentId(name) {
2341
2380
  let m = name.match(/^(\w+?)_g\d+__expr$/);
2342
2381
  if (m) return m[1].replace(/_/g, '-');
2343
- m = name.match(/^(\w+?)_g\d+_g\d+_(pval|logfc|l2fc)_attr_[a-z]$/);
2382
+ m = name.match(/^(\w+?)_g\d+_g\d+_(pval|l2fc)_attr_[a-z]$/);
2344
2383
  if (m) return m[1].replace(/_/g, '-');
2345
2384
  return null;
2346
2385
  }
2347
2386
  const $0f839422d0d8c772$var$STAT_RANK = {
2348
2387
  l2fc: 0,
2349
- logfc: 1,
2350
- pval: 2
2388
+ pval: 1
2351
2389
  };
2352
2390
  function $0f839422d0d8c772$var$collapseDiffExprInSubgroups(subgroups, fieldsOut, collator) {
2353
2391
  for (const taxGroup of subgroups)for (const expGroup of taxGroup.subgroups || []){
@@ -2384,7 +2422,7 @@ function $0f839422d0d8c772$var$collapseDiffExprInSubgroups(subgroups, fieldsOut,
2384
2422
  });
2385
2423
  const rep = names[0];
2386
2424
  const repEntry = fieldsOut[rep];
2387
- const label = (repEntry.label || rep).replace(/\s+\((?:pval|logfc|l2fc)\)/, '');
2425
+ const label = (repEntry.label || rep).replace(/\s+\((?:pval|l2fc)\)/, '');
2388
2426
  fieldsOut[rep] = {
2389
2427
  ...repEntry,
2390
2428
  label: label,
@@ -2687,15 +2725,12 @@ $0f839422d0d8c772$var$grameneFieldCatalog.selectFieldCatalog = (0, $gXNCa$reduxb
2687
2725
  const present = new Set(fieldNames);
2688
2726
  // Build a synthetic doc from the discovered field names; values carry the
2689
2727
  // right JS type so inferType picks the correct multiValued/type (arrays
2690
- // for multi-valued fields, scalars otherwise). Derive pval/logfc
2691
- // companions from every l2fc field.
2728
+ // for multi-valued fields, scalars otherwise). Derive the pval companion
2729
+ // from every l2fc field.
2692
2730
  const doc = {};
2693
2731
  for (const name of present){
2694
2732
  doc[name] = $0f839422d0d8c772$var$synthesizedValue(name);
2695
- if (/_l2fc_attr_f$/.test(name)) {
2696
- doc[name.replace('_l2fc_', '_pval_')] = 0;
2697
- doc[name.replace('_l2fc_', '_logfc_')] = 0;
2698
- }
2733
+ if (/_l2fc_attr_f$/.test(name)) doc[name.replace('_l2fc_', '_pval_')] = 0;
2699
2734
  }
2700
2735
  const catalog = $0f839422d0d8c772$var$buildCatalog([
2701
2736
  doc
@@ -2710,7 +2745,7 @@ var $0f839422d0d8c772$export$2e2bcd8739ae039 = $0f839422d0d8c772$var$grameneFiel
2710
2745
 
2711
2746
 
2712
2747
  const $6d28e8e62a4d602f$var$EXPR_FIELD_RE = /^(E[-_][A-Za-z0-9_-]+?)_g(\d+)__expr$/;
2713
- const $6d28e8e62a4d602f$var$DIFFEXPR_FIELD_RE = /^(E[-_][A-Za-z0-9_-]+?)_g(\d+)_g(\d+)_(pval|logfc|l2fc)_attr_([a-z])$/;
2748
+ const $6d28e8e62a4d602f$var$DIFFEXPR_FIELD_RE = /^(E[-_][A-Za-z0-9_-]+?)_g(\d+)_g(\d+)_(pval|l2fc)_attr_([a-z])$/;
2714
2749
  const $6d28e8e62a4d602f$export$adbe84e3322ddf23 = [
2715
2750
  'experiment',
2716
2751
  'experiment_name',
@@ -2835,7 +2870,7 @@ function $6d28e8e62a4d602f$export$ba4cd509d0763838(doc, diffExpressionFields, ex
2835
2870
  byContrast.set(key, entry);
2836
2871
  }
2837
2872
  if (parsed.stat === 'pval') entry.pval = val;
2838
- else entry.l2fc = val; // l2fc or logfc
2873
+ else entry.l2fc = val;
2839
2874
  }
2840
2875
  const maxPval = cutoffs && cutoffs.diffMaxPval;
2841
2876
  const maxPvalActive = maxPval !== null && maxPval !== undefined && maxPval !== '' && Number.isFinite(+maxPval);
@@ -3662,10 +3697,12 @@ const $1508f5a42be6e7b5$var$exporter = {
3662
3697
  });
3663
3698
  }, $1508f5a42be6e7b5$var$PREVIEW_DEBOUNCE_MS);
3664
3699
  },
3665
- reactExporterPreview: (0, $gXNCa$reduxbundler.createSelector)('selectExporter', 'selectGrameneFiltersStatus', 'selectGrameneFiltersQueryString', 'selectGrameneGenomes', 'selectGrameneMaps', (exp, filtersStatus, q, g, m)=>{
3700
+ reactExporterPreview: (0, $gXNCa$reduxbundler.createSelector)('selectExporter', 'selectGrameneFiltersStatus', 'selectGrameneFiltersQueryString', 'selectGrameneGenomes', 'selectGrameneMaps', 'selectGrameneViewsOn', (exp, filtersStatus, q, g, m, viewsOn)=>{
3666
3701
  if (!exp || !exp.preview) return;
3667
3702
  if (filtersStatus === 'init') return;
3668
3703
  if (exp.preview.status === 'loading') return;
3704
+ // Don't run the preview query unless the user has the exporter view open.
3705
+ if (!viewsOn || !viewsOn.has('export')) return;
3669
3706
  const maps = m || {};
3670
3707
  const taxa = Object.keys(g && g.active || {}).filter((tid)=>maps[tid] && !maps[tid].hidden);
3671
3708
  const sig = `${q}|${taxa.sort().join(',')}`;
@@ -11002,6 +11039,21 @@ const $4d0c2e01f58b53b1$var$FieldsModalCmp = (props)=>{
11002
11039
  records,
11003
11040
  selections
11004
11041
  ]);
11042
+ // Distinct values per property type across the unfiltered candidate set.
11043
+ // A type with a single value across every record can't discriminate (e.g.
11044
+ // "Organism" when every field belongs to the same species), so we hide it
11045
+ // from the property tree.
11046
+ const totalValueCountByKey = (0, $gXNCa$react.useMemo)(()=>{
11047
+ const m = {};
11048
+ for (const r of records)for (const [k, v] of Object.entries(r.props)){
11049
+ if (v == null) continue;
11050
+ if (!m[k]) m[k] = new Set();
11051
+ m[k].add(v);
11052
+ }
11053
+ return m;
11054
+ }, [
11055
+ records
11056
+ ]);
11005
11057
  // Reset state when modal (re)opens for a taxon.
11006
11058
  (0, $gXNCa$react.useEffect)(()=>{
11007
11059
  if (open && taxon) {
@@ -11169,6 +11221,13 @@ const $4d0c2e01f58b53b1$var$FieldsModalCmp = (props)=>{
11169
11221
  }),
11170
11222
  propTree.map((grp)=>{
11171
11223
  const typeRows = grp.types.map((t)=>{
11224
+ // Hide property types that can't discriminate among the
11225
+ // candidates: zero values, or a single value shared by every
11226
+ // record. The selection-aware count (valueSetByKey) is what
11227
+ // we display; the unfiltered count gates visibility so a type
11228
+ // doesn't pop in/out as the user clicks values.
11229
+ const totalDistinct = totalValueCountByKey[t.key] && totalValueCountByKey[t.key].size || 0;
11230
+ if (totalDistinct < 2) return null;
11172
11231
  const numValues = valueSetByKey[t.key] && valueSetByKey[t.key].size || 0;
11173
11232
  if (numValues === 0) return null;
11174
11233
  const typeLabelMatches = isSearching && t.label.toLowerCase().includes(searchLc);
@@ -11239,37 +11298,32 @@ const $4d0c2e01f58b53b1$var$FieldsModalCmp = (props)=>{
11239
11298
  }),
11240
11299
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11241
11300
  className: "exprviz-fields-preview",
11242
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("table", {
11301
+ children: orderedSelectedKeys.length === 0 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11302
+ className: "exprviz-fields-placeholder",
11303
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11304
+ children: "Select a property type to preview the property values for each matching field."
11305
+ })
11306
+ }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("table", {
11243
11307
  className: "exprviz-fields-table",
11244
11308
  children: [
11245
11309
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("thead", {
11246
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tr", {
11247
- children: [
11248
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("th", {
11249
- children: "Field"
11250
- }),
11251
- orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("th", {
11252
- children: $4d0c2e01f58b53b1$var$labelForKey(k, propTree)
11253
- }, k))
11254
- ]
11310
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("tr", {
11311
+ children: orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("th", {
11312
+ children: $4d0c2e01f58b53b1$var$labelForKey(k, propTree)
11313
+ }, k))
11255
11314
  })
11256
11315
  }),
11257
11316
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tbody", {
11258
11317
  children: [
11259
- matchingFields.map((f)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tr", {
11260
- children: [
11261
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11262
- title: f.fieldName,
11263
- children: f.fieldName.replace(/__expr$/, '')
11264
- }),
11265
- orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11266
- children: f.props[k] || ''
11267
- }, k))
11268
- ]
11318
+ matchingFields.map((f)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("tr", {
11319
+ title: f.fieldName,
11320
+ children: orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11321
+ children: f.props[k] || ''
11322
+ }, k))
11269
11323
  }, f.fieldName)),
11270
11324
  matchingFields.length === 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("tr", {
11271
11325
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11272
- colSpan: 1 + orderedSelectedKeys.length,
11326
+ colSpan: orderedSelectedKeys.length,
11273
11327
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11274
11328
  children: "No matching fields"
11275
11329
  })
@@ -11316,7 +11370,6 @@ var $4d0c2e01f58b53b1$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
11316
11370
 
11317
11371
 
11318
11372
 
11319
-
11320
11373
  const $4ab64e76c1caef59$var$baseColDefs = [
11321
11374
  {
11322
11375
  field: 'id',
@@ -11328,16 +11381,27 @@ const $4ab64e76c1caef59$var$baseColDefs = [
11328
11381
  {
11329
11382
  field: 'name',
11330
11383
  headerName: 'Name',
11384
+ pinned: 'left',
11331
11385
  width: 140,
11332
11386
  suppressMovable: true
11333
11387
  }
11334
11388
  ];
11389
+ // Hoisted so the reference is stable across renders. ag-grid otherwise sees a
11390
+ // "new" defaultColDef on every parent re-render (e.g. when hovering a row
11391
+ // triggers setHoveredId in the parent) and re-applies column state, which
11392
+ // snaps any user-resized columns back to their original widths.
11393
+ const $4ab64e76c1caef59$var$DEFAULT_COL_DEF = {
11394
+ resizable: true,
11395
+ sortable: true,
11396
+ filter: false,
11397
+ suppressMenu: true
11398
+ };
11335
11399
  function $4ab64e76c1caef59$var$arraysEqual(a, b) {
11336
11400
  if (a.length !== b.length) return false;
11337
11401
  for(let i = 0; i < a.length; i++)if (a[i] !== b[i]) return false;
11338
11402
  return true;
11339
11403
  }
11340
- function $4ab64e76c1caef59$var$buildFieldInfo(fields, studies, expressionSamples) {
11404
+ function $4ab64e76c1caef59$export$7b242440eb2c300d(fields, studies, expressionSamples) {
11341
11405
  const info = {};
11342
11406
  if (!fields || !studies || !expressionSamples) return info;
11343
11407
  const wanted = new Set(fields);
@@ -11372,189 +11436,287 @@ function $4ab64e76c1caef59$var$buildFieldInfo(fields, studies, expressionSamples
11372
11436
  }
11373
11437
  return info;
11374
11438
  }
11375
- // Custom header: re-implements sort click + menu button so ag-grid's column
11376
- // drag/menu/filter still work, with an info icon as the popover trigger.
11377
- const $4ab64e76c1caef59$var$HeaderWithInfo = (props)=>{
11378
- const { displayName: displayName, enableSorting: enableSorting, enableMenu: enableMenu, showColumnMenu: showColumnMenu, progressSort: progressSort, column: column, info: info } = props;
11379
- const [sort, setSort] = (0, $gXNCa$react.useState)(column && column.getSort ? column.getSort() : null);
11380
- const menuRef = (0, $gXNCa$react.useRef)(null);
11381
- (0, $gXNCa$react.useEffect)(()=>{
11382
- if (!column || !column.addEventListener) return;
11383
- const handler = ()=>setSort(column.getSort());
11384
- column.addEventListener('sortChanged', handler);
11385
- return ()=>column.removeEventListener('sortChanged', handler);
11386
- }, [
11387
- column
11388
- ]);
11389
- const onSortClick = (event)=>{
11390
- if (enableSorting && progressSort) progressSort(event.shiftKey);
11391
- };
11392
- const onMenuClick = (event)=>{
11393
- event.stopPropagation();
11394
- if (showColumnMenu && menuRef.current) showColumnMenu(menuRef.current);
11395
- };
11396
- const factorEntries = info ? Object.entries(info.factors || {}) : [];
11397
- const charEntries = info ? Object.entries(info.characteristics || {}) : [];
11398
- const popover = info ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Popover), {
11399
- id: `exprviz-header-${info.studyId}-${info.group}`,
11400
- className: "exprviz-header-popover",
11401
- children: [
11402
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Popover).Header, {
11403
- as: "h6",
11404
- children: info.studyDescription
11405
- }),
11406
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Popover).Body, {
11407
- children: [
11408
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11409
- children: [
11410
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11411
- children: "Study:"
11412
- }),
11413
- " ",
11414
- info.studyId
11415
- ]
11416
- }),
11417
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11418
- children: [
11419
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11420
- children: "Group:"
11421
- }),
11422
- " ",
11423
- info.group,
11424
- " (",
11425
- info.replicates,
11426
- " ",
11427
- info.replicates === 1 ? 'rep' : 'reps',
11428
- ")"
11429
- ]
11430
- }),
11431
- factorEntries.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11432
- className: "exprviz-header-section",
11433
- children: [
11434
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11435
- children: "Factors"
11436
- }),
11437
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("ul", {
11438
- children: factorEntries.map(([t, v])=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
11439
- children: [
11440
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
11441
- children: [
11442
- t,
11443
- ":"
11444
- ]
11445
- }),
11446
- " ",
11447
- v
11448
- ]
11449
- }, t))
11450
- })
11451
- ]
11452
- }),
11453
- charEntries.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11454
- className: "exprviz-header-section",
11455
- children: [
11456
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11457
- children: "Characteristics"
11458
- }),
11459
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("ul", {
11460
- children: charEntries.map(([t, v])=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
11461
- children: [
11462
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
11463
- children: [
11464
- t,
11465
- ":"
11466
- ]
11467
- }),
11468
- " ",
11469
- v
11470
- ]
11471
- }, t))
11472
- })
11473
- ]
11474
- })
11475
- ]
11476
- })
11477
- ]
11478
- }) : null;
11439
+ // Plain right-aligned label cell for the metadata header rows above the
11440
+ // Gene ID / Name columns. Rendered as a React component (instead of
11441
+ // relying on ag-grid's default group label) so the alignment is controlled
11442
+ // by our own DOM and isn't fighting the quartz theme's CSS.
11443
+ const $4ab64e76c1caef59$var$LabelHeaderGroup = (props)=>{
11444
+ const { displayName: displayName } = props;
11445
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11446
+ className: "exprviz-label-header",
11447
+ children: displayName
11448
+ });
11449
+ };
11450
+ // Same idea, but for the cells that toggle a section between expanded and
11451
+ // collapsed. The caret + label sit at the right edge.
11452
+ const $4ab64e76c1caef59$var$ToggleHeaderGroup = (props)=>{
11453
+ const { displayName: displayName, onToggle: onToggle, expanded: expanded, suffix: suffix } = props;
11479
11454
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11480
- className: "exprviz-header",
11455
+ className: "exprviz-toggle-header",
11456
+ onClick: onToggle,
11457
+ role: "button",
11458
+ tabIndex: 0,
11459
+ onKeyDown: (e)=>{
11460
+ if (e.key === 'Enter' || e.key === ' ') {
11461
+ e.preventDefault();
11462
+ onToggle && onToggle();
11463
+ }
11464
+ },
11465
+ title: expanded ? 'Click to collapse' : 'Click to expand',
11481
11466
  children: [
11482
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
11483
- className: "exprviz-header-text",
11484
- onClick: onSortClick,
11485
- style: {
11486
- cursor: enableSorting ? 'pointer' : 'default'
11487
- },
11488
- children: [
11489
- displayName,
11490
- sort === 'asc' && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11491
- className: "exprviz-header-sort",
11492
- children: " \u25B2"
11493
- }),
11494
- sort === 'desc' && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11495
- className: "exprviz-header-sort",
11496
- children: " \u25BC"
11497
- })
11498
- ]
11499
- }),
11500
- info && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.OverlayTrigger), {
11501
- trigger: [
11502
- 'hover',
11503
- 'focus'
11504
- ],
11505
- placement: "bottom",
11506
- overlay: popover,
11507
- delay: {
11508
- show: 200,
11509
- hide: 100
11510
- },
11511
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11512
- className: "exprviz-header-info",
11513
- onClick: (e)=>e.stopPropagation(),
11514
- onMouseDown: (e)=>e.stopPropagation(),
11515
- "aria-label": "More info",
11516
- children: "\u24D8"
11517
- })
11467
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11468
+ className: "exprviz-toggle-caret",
11469
+ children: expanded ? "\u25BC" : "\u25B6"
11518
11470
  }),
11519
- enableMenu && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11520
- ref: menuRef,
11521
- className: "exprviz-header-menu ag-icon ag-icon-menu",
11522
- onClick: onMenuClick
11523
- })
11471
+ ' ',
11472
+ displayName,
11473
+ suffix ? ` ${suffix}` : ''
11524
11474
  ]
11525
11475
  });
11526
11476
  };
11477
+ function $4ab64e76c1caef59$var$exprValueFormatter(p) {
11478
+ const v = p.value;
11479
+ if (v == null) return '';
11480
+ if (Array.isArray(v)) return v.join(', ');
11481
+ if (typeof v === 'object') return JSON.stringify(v);
11482
+ return String(v);
11483
+ }
11484
+ // Discover every distinct factor and characteristic type across the loaded
11485
+ // fields. Each becomes one header row (with adjacent equal-value cells
11486
+ // merged). Order is alphabetical for stability across reloads.
11487
+ function $4ab64e76c1caef59$var$collectMetadataTypes(fields, fieldInfo) {
11488
+ const factorTypes = new Set();
11489
+ const charTypes = new Set();
11490
+ for (const f of fields || []){
11491
+ const info = fieldInfo[f];
11492
+ if (!info) continue;
11493
+ Object.keys(info.factors || {}).forEach((t)=>factorTypes.add(t));
11494
+ Object.keys(info.characteristics || {}).forEach((t)=>charTypes.add(t));
11495
+ }
11496
+ return {
11497
+ factorTypes: Array.from(factorTypes).sort(),
11498
+ charTypes: Array.from(charTypes).sort()
11499
+ };
11500
+ }
11501
+ // Wrap a leaf column in N nested column groups so it participates at every
11502
+ // header row, with a custom label per level. Lets the Gene ID / Name columns
11503
+ // carry the row labels ("Study"/"Factor"/"Characteristic" and the type name).
11504
+ function $4ab64e76c1caef59$var$wrapLeafWithLabels(leaf, labels) {
11505
+ let wrapped = leaf;
11506
+ for(let i = labels.length - 1; i >= 0; i--){
11507
+ const label = labels[i];
11508
+ const cls = label.headerClass ? `${label.headerClass} exprviz-hg-labels` : 'exprviz-hg-labels';
11509
+ wrapped = {
11510
+ headerName: label.headerName,
11511
+ headerClass: cls,
11512
+ // Always render through one of our components so the alignment and
11513
+ // spacing are controlled by our own DOM, not ag-grid's defaults.
11514
+ headerGroupComponent: label.headerGroupComponent || $4ab64e76c1caef59$var$LabelHeaderGroup,
11515
+ headerGroupComponentParams: label.headerGroupComponentParams,
11516
+ children: [
11517
+ wrapped
11518
+ ]
11519
+ };
11520
+ }
11521
+ return wrapped;
11522
+ }
11523
+ // Build the column tree:
11524
+ // row 1 — Study | Title | <study description>…
11525
+ // row 2..(2+F-1) — Factor | <factor type> | <value or blank>…
11526
+ // row 2+F..end-1 — Characteristic | <char type> | <value or blank>…
11527
+ // leaf row — Gene ID | Name | g3, g4, …
11528
+ // Adjacent expression cells sharing the same value at a given level merge
11529
+ // (because they're children of one group definition). Blank values for a
11530
+ // type that's defined only in other studies render as empty cells, and
11531
+ // adjacent blanks under the same parent merge automatically.
11532
+ function $4ab64e76c1caef59$var$buildColumnDefs(fields, fieldInfo, expanded, toggles) {
11533
+ if (!fields || fields.length === 0) return $4ab64e76c1caef59$var$baseColDefs;
11534
+ const { factorTypes: factorTypes, charTypes: charTypes } = $4ab64e76c1caef59$var$collectMetadataTypes(fields, fieldInfo);
11535
+ const levels = [
11536
+ {
11537
+ kind: 'study',
11538
+ getValue: (info)=>info && info.studyDescription || ''
11539
+ }
11540
+ ];
11541
+ if (expanded.factors) for (const t of factorTypes)levels.push({
11542
+ kind: 'factor',
11543
+ type: t,
11544
+ getValue: (info)=>info && info.factors && info.factors[t] || '',
11545
+ headerClass: 'exprviz-hg-factors'
11546
+ });
11547
+ else if (factorTypes.length > 0) // One placeholder level standing in for the collapsed factor rows.
11548
+ levels.push({
11549
+ kind: 'factors-collapsed',
11550
+ getValue: ()=>'',
11551
+ headerClass: 'exprviz-hg-factors exprviz-hg-collapsed'
11552
+ });
11553
+ if (expanded.chars) for (const t of charTypes)levels.push({
11554
+ kind: 'char',
11555
+ type: t,
11556
+ getValue: (info)=>info && info.characteristics && info.characteristics[t] || '',
11557
+ headerClass: 'exprviz-hg-chars'
11558
+ });
11559
+ else if (charTypes.length > 0) levels.push({
11560
+ kind: 'chars-collapsed',
11561
+ getValue: ()=>'',
11562
+ headerClass: 'exprviz-hg-chars exprviz-hg-collapsed'
11563
+ });
11564
+ // Walk fields, opening a new group at level i (and resetting all levels
11565
+ // below) the first time the value at level i differs from the previous
11566
+ // field's. Same value → same parent group → ag-grid merges the cells.
11567
+ const exprTopGroups = [];
11568
+ const currentGroups = new Array(levels.length).fill(null);
11569
+ const currentKeys = new Array(levels.length).fill(undefined);
11570
+ for (const f of fields){
11571
+ const info = fieldInfo[f] || {};
11572
+ let firstChange = levels.length;
11573
+ for(let i = 0; i < levels.length; i++){
11574
+ const key = levels[i].getValue(info);
11575
+ if (currentGroups[i] === null || currentKeys[i] !== key) {
11576
+ firstChange = i;
11577
+ break;
11578
+ }
11579
+ }
11580
+ for(let i = firstChange; i < levels.length; i++){
11581
+ const key = levels[i].getValue(info);
11582
+ const group = {
11583
+ headerName: key,
11584
+ headerClass: levels[i].headerClass,
11585
+ children: []
11586
+ };
11587
+ currentGroups[i] = group;
11588
+ currentKeys[i] = key;
11589
+ if (i === 0) exprTopGroups.push(group);
11590
+ else currentGroups[i - 1].children.push(group);
11591
+ }
11592
+ const leaf = {
11593
+ field: f,
11594
+ headerName: info.group || f.replace(/__expr$/, ''),
11595
+ width: 160,
11596
+ suppressMovable: false,
11597
+ valueFormatter: $4ab64e76c1caef59$var$exprValueFormatter
11598
+ };
11599
+ currentGroups[levels.length - 1].children.push(leaf);
11600
+ }
11601
+ // Labels for the two pinned columns. The leftmost column carries the row
11602
+ // category ("Study" / "Factor" / "Characteristic"); the next column shows
11603
+ // the specific row name (the literal "Title" for the study row, then each
11604
+ // factor/characteristic type name). The first row of each
11605
+ // factor/characteristic section gets a clickable caret that toggles the
11606
+ // section between expanded (one row per type) and collapsed (one
11607
+ // placeholder row).
11608
+ const studyLabels = [
11609
+ {
11610
+ headerName: 'Study'
11611
+ }
11612
+ ];
11613
+ const titleLabels = [
11614
+ {
11615
+ headerName: 'Title'
11616
+ }
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
+ });
11634
+ });
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})`
11644
+ }
11645
+ });
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
+ } : {}
11662
+ });
11663
+ titleLabels.push({
11664
+ headerName: t,
11665
+ headerClass: 'exprviz-hg-chars'
11666
+ });
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})`
11677
+ }
11678
+ });
11679
+ titleLabels.push({
11680
+ headerName: '',
11681
+ headerClass: 'exprviz-hg-chars exprviz-hg-collapsed'
11682
+ });
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
+ }
11527
11692
  const $4ab64e76c1caef59$var$ExprTable = ({ rows: rows, fields: fields, onReorder: onReorder, studies: studies, expressionSamples: expressionSamples, onHoverRow: onHoverRow })=>{
11528
- const fieldInfo = (0, $gXNCa$react.useMemo)(()=>$4ab64e76c1caef59$var$buildFieldInfo(fields, studies, expressionSamples), [
11693
+ const fieldInfo = (0, $gXNCa$react.useMemo)(()=>$4ab64e76c1caef59$export$7b242440eb2c300d(fields, studies, expressionSamples), [
11529
11694
  fields,
11530
11695
  studies,
11531
11696
  expressionSamples
11532
11697
  ]);
11533
- const columnDefs = (0, $gXNCa$react.useMemo)(()=>{
11534
- const expressionCols = (fields || []).map((f)=>({
11535
- field: f,
11536
- headerName: f.replace(/__expr$/, ''),
11537
- width: 160,
11538
- suppressMovable: false,
11539
- headerComponent: $4ab64e76c1caef59$var$HeaderWithInfo,
11540
- headerComponentParams: {
11541
- info: fieldInfo[f]
11542
- },
11543
- valueFormatter: (p)=>{
11544
- const v = p.value;
11545
- if (v == null) return '';
11546
- if (Array.isArray(v)) return v.join(', ');
11547
- if (typeof v === 'object') return JSON.stringify(v);
11548
- return String(v);
11549
- }
11550
- }));
11551
- return [
11552
- ...$4ab64e76c1caef59$var$baseColDefs,
11553
- ...expressionCols
11554
- ];
11555
- }, [
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
+ }), [
11556
11715
  fields,
11557
- fieldInfo
11716
+ fieldInfo,
11717
+ expanded,
11718
+ toggleFactors,
11719
+ toggleChars
11558
11720
  ]);
11559
11721
  const onColumnMoved = (e)=>{
11560
11722
  if (!onReorder || !e.finished) return;
@@ -11574,14 +11736,12 @@ const $4ab64e76c1caef59$var$ExprTable = ({ rows: rows, fields: fields, onReorder
11574
11736
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$aggridreact.AgGridReact), {
11575
11737
  rowData: rows,
11576
11738
  columnDefs: columnDefs,
11577
- defaultColDef: {
11578
- resizable: true,
11579
- sortable: true,
11580
- filter: true
11581
- },
11739
+ defaultColDef: $4ab64e76c1caef59$var$DEFAULT_COL_DEF,
11582
11740
  animateRows: false,
11583
11741
  suppressFieldDotNotation: true,
11584
11742
  suppressDragLeaveHidesColumns: true,
11743
+ suppressColumnVirtualisation: true,
11744
+ groupHeaderHeight: 24,
11585
11745
  onColumnMoved: onColumnMoved,
11586
11746
  onCellMouseOver: (e)=>onHoverRow && onHoverRow(e.data && e.data.id),
11587
11747
  onCellMouseOut: ()=>onHoverRow && onHoverRow(null)
@@ -11647,12 +11807,41 @@ function $caf32827df861c4e$var$logTickFormat(v) {
11647
11807
  if (a >= 0.01 && a < 10000) return $gXNCa$d3.format('~g')(v);
11648
11808
  return $gXNCa$d3.format('.0e')(v);
11649
11809
  }
11650
- const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null })=>{
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 })=>{
11651
11811
  const svgRef = (0, $gXNCa$react.useRef)(null);
11652
11812
  const containerRef = (0, $gXNCa$react.useRef)(null);
11653
11813
  // selections in data domain: { [field]: [lo, hi] }
11654
11814
  const selectionsRef = (0, $gXNCa$react.useRef)({});
11655
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
+ }
11841
+ });
11842
+ ro.observe(el);
11843
+ return ()=>ro.disconnect();
11844
+ }, []);
11656
11845
  (0, $gXNCa$react.useEffect)(()=>{
11657
11846
  if (clearVersion !== lastClearRef.current) {
11658
11847
  selectionsRef.current = {};
@@ -11749,9 +11938,32 @@ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields,
11749
11938
  if (scale === 'log') axisGen.tickValues($caf32827df861c4e$var$logTickValues(yByField[f].domain())).tickFormat($caf32827df861c4e$var$logTickFormat);
11750
11939
  else axisGen.ticks(5);
11751
11940
  ax.call(axisGen);
11752
- const label = 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(f.replace(/__expr$/, ''));
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$/, ''),
11946
+ structured: {
11947
+ studyTitle: f,
11948
+ group: '',
11949
+ factors: [],
11950
+ characteristics: []
11951
+ }
11952
+ };
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);
11753
11965
  // hit area for grabbing — sits along the rotated label
11754
- 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');
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);
11755
11967
  const brush = $gXNCa$d3.brushY().extent([
11756
11968
  [
11757
11969
  -$caf32827df861c4e$var$BRUSH_WIDTH / 2,
@@ -11844,7 +12056,10 @@ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields,
11844
12056
  scale,
11845
12057
  onBrushChange,
11846
12058
  onReorder,
11847
- clearVersion
12059
+ clearVersion,
12060
+ axisLabels,
12061
+ size.w,
12062
+ size.h
11848
12063
  ]);
11849
12064
  // Highlight the polyline matching the hovered row id without rebuilding the
11850
12065
  // SVG. Raises the highlighted path so it draws above its neighbors.
@@ -11870,15 +12085,114 @@ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields,
11870
12085
  children: "Select fields to plot."
11871
12086
  })
11872
12087
  });
11873
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12088
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11874
12089
  ref: containerRef,
11875
12090
  className: "exprviz-pc-container",
11876
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("svg", {
11877
- ref: svgRef,
11878
- width: "100%",
11879
- height: "100%",
11880
- preserveAspectRatio: "none"
11881
- })
12091
+ children: [
12092
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("svg", {
12093
+ ref: svgRef,
12094
+ width: "100%",
12095
+ height: "100%",
12096
+ preserveAspectRatio: "none"
12097
+ }),
12098
+ tooltip && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($caf32827df861c4e$var$AxisTooltip, {
12099
+ x: tooltip.x,
12100
+ y: tooltip.y,
12101
+ info: tooltip.info
12102
+ })
12103
+ ]
12104
+ });
12105
+ };
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
+ });
12114
+ (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
+ });
12129
+ }, [
12130
+ x,
12131
+ y,
12132
+ info
12133
+ ]);
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,
12139
+ children: [
12140
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12141
+ children: [
12142
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
12143
+ className: "exprviz-pc-tip-key",
12144
+ children: "Study:"
12145
+ }),
12146
+ " ",
12147
+ studyTitle,
12148
+ group ? ` (${group})` : ''
12149
+ ]
12150
+ }),
12151
+ factors.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
12152
+ children: [
12153
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12154
+ className: "exprviz-pc-tip-section",
12155
+ children: "Factors"
12156
+ }),
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}`))
12171
+ ]
12172
+ }),
12173
+ characteristics.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
12174
+ children: [
12175
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12176
+ className: "exprviz-pc-tip-section",
12177
+ children: "Characteristics"
12178
+ }),
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}`))
12193
+ ]
12194
+ })
12195
+ ]
11882
12196
  });
11883
12197
  };
11884
12198
  var $caf32827df861c4e$export$2e2bcd8739ae039 = $caf32827df861c4e$var$ParallelCoordsPlot;
@@ -11975,6 +12289,75 @@ const $1fd2507769d5bd00$var$ExprVizViewCmp = (props)=>{
11975
12289
  ]
11976
12290
  });
11977
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;
12325
+ });
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
+ }
11978
12361
  function $1fd2507769d5bd00$var$rowMatchesSelections(row, selections) {
11979
12362
  for (const f of Object.keys(selections)){
11980
12363
  const v = row[f];
@@ -11998,20 +12381,50 @@ function $1fd2507769d5bd00$var$tsvCell(v) {
11998
12381
  const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
11999
12382
  return s.replace(/[\t\r\n]+/g, ' ');
12000
12383
  }
12001
- function $1fd2507769d5bd00$var$downloadTsv(filename, rows, fields) {
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) {
12002
12391
  const cols = [
12003
12392
  'id',
12004
12393
  'name',
12005
12394
  ...fields
12006
12395
  ];
12007
- const headerLabels = [
12008
- 'id',
12009
- 'name',
12010
- ...fields.map((f)=>f.replace(/__expr$/, ''))
12011
- ];
12012
- const lines = [
12013
- headerLabels.join('\t')
12014
- ];
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$/, '');
12426
+ })
12427
+ ].join('\t'));
12015
12428
  for (const r of rows)lines.push(cols.map((c)=>$1fd2507769d5bd00$var$tsvCell(r[c])).join('\t'));
12016
12429
  const blob = new Blob([
12017
12430
  lines.join('\n') + '\n'
@@ -12038,6 +12451,34 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
12038
12451
  const [selections, setSelections] = (0, $gXNCa$react.useState)({});
12039
12452
  const [clearVersion, setClearVersion] = (0, $gXNCa$react.useState)(0);
12040
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)=>{
12459
+ e.preventDefault();
12460
+ resizeStateRef.current = {
12461
+ startY: e.clientY,
12462
+ startHeight: plotHeight
12463
+ };
12464
+ 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);
12469
+ };
12470
+ const onUp = ()=>{
12471
+ resizeStateRef.current = null;
12472
+ document.removeEventListener('mousemove', onMove);
12473
+ document.removeEventListener('mouseup', onUp);
12474
+ document.body.style.cursor = '';
12475
+ document.body.style.userSelect = '';
12476
+ };
12477
+ document.body.style.cursor = 'row-resize';
12478
+ document.body.style.userSelect = 'none';
12479
+ document.addEventListener('mousemove', onMove);
12480
+ document.addEventListener('mouseup', onUp);
12481
+ };
12041
12482
  const hasBrush = Object.keys(selections).length > 0;
12042
12483
  const filteredRows = (0, $gXNCa$react.useMemo)(()=>{
12043
12484
  if (!hasBrush) return rows;
@@ -12068,6 +12509,11 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
12068
12509
  ...hidden
12069
12510
  ]);
12070
12511
  } : undefined;
12512
+ const axisLabels = (0, $gXNCa$react.useMemo)(()=>$1fd2507769d5bd00$var$buildAxisLabels(visibleFields, studies, expressionSamples), [
12513
+ visibleFields,
12514
+ studies,
12515
+ expressionSamples
12516
+ ]);
12071
12517
  (0, $gXNCa$react.useEffect)(()=>{
12072
12518
  if (rows.length === 0 && hasBrush) {
12073
12519
  setSelections({});
@@ -12136,7 +12582,7 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
12136
12582
  size: "sm",
12137
12583
  variant: "outline-secondary",
12138
12584
  disabled: filteredRows.length === 0 || visibleFields.length === 0,
12139
- onClick: ()=>$1fd2507769d5bd00$var$downloadTsv(`expression_${taxon}.tsv`, filteredRows, visibleFields),
12585
+ onClick: ()=>$1fd2507769d5bd00$var$downloadTsv(`expression_${taxon}.tsv`, filteredRows, visibleFields, studies, expressionSamples),
12140
12586
  title: "Download the visible rows and columns as tab-delimited text",
12141
12587
  children: "Download TSV"
12142
12588
  }),
@@ -12175,6 +12621,9 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
12175
12621
  children: [
12176
12622
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12177
12623
  className: "exprviz-plot",
12624
+ style: {
12625
+ height: plotHeight
12626
+ },
12178
12627
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $caf32827df861c4e$export$2e2bcd8739ae039), {
12179
12628
  rows: rows,
12180
12629
  fields: visibleFields,
@@ -12182,9 +12631,18 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
12182
12631
  onBrushChange: setSelections,
12183
12632
  onReorder: handleReorder,
12184
12633
  clearVersion: clearVersion,
12185
- hoveredId: hoveredId
12634
+ hoveredId: hoveredId,
12635
+ axisLabels: axisLabels
12186
12636
  })
12187
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
+ }),
12188
12646
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12189
12647
  className: "exprviz-table",
12190
12648
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $4ab64e76c1caef59$export$2e2bcd8739ae039), {