gramene-search 2.36.1 → 2.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5236,16 +5236,17 @@ const $4b8473139e48e77e$export$705aff68da3ddba0 = [
5236
5236
  // Paging mirrors exprViz's doFetchExprVizData: a recursive fetchPage(offset)
5237
5237
  // with a request-id guard so a superseded fetch can never write stale rows.
5238
5238
  //
5239
- // The offered columns are deliberately limited to two field-catalog groups —
5240
- // Core identifiers and Expression attributes — and *all* of them are fetched up
5241
- // front. That keeps the column picker a pure visibility control: toggling a
5242
- // column is instant and never triggers a refetch.
5239
+ // The offered columns are deliberately limited to a few field-catalog groups —
5240
+ // Core identifiers, Expression attributes and MAKER transcript metrics — and
5241
+ // *all* of them are fetched up front. That keeps the column picker a pure
5242
+ // visibility control: toggling a column is instant and never triggers a refetch.
5243
5243
  const $12feb7ca48271e17$var$PAGE_SIZE = 1000;
5244
5244
  const $12feb7ca48271e17$var$MAX_GENES = 5000;
5245
5245
  // The catalog groups whose fields the column picker offers.
5246
5246
  const $12feb7ca48271e17$export$53e90de8d659db95 = [
5247
5247
  'core',
5248
- 'exprattrs'
5248
+ 'exprattrs',
5249
+ 'MAKER'
5249
5250
  ];
5250
5251
  // The 'core' group's fields (bundles/../fieldCatalog.overlay.json).
5251
5252
  const $12feb7ca48271e17$export$9d77d82cc773e55e = [
@@ -5260,12 +5261,28 @@ const $12feb7ca48271e17$export$9d77d82cc773e55e = [
5260
5261
  'taxon_id',
5261
5262
  'db_type'
5262
5263
  ];
5264
+ // The 'MAKER' group's fields: MAKER's annotation quality metrics (AED and the
5265
+ // quality index QI1-QI9). Only some releases have them (e.g. SorghumBase); Solr
5266
+ // returns nothing for them elsewhere, and the picker then hides the empty group.
5267
+ const $12feb7ca48271e17$export$3ae897f4a2495276 = [
5268
+ 'MAKER__AED__attr_f',
5269
+ 'MAKER__QI1__attr_i',
5270
+ 'MAKER__QI2__attr_f',
5271
+ 'MAKER__QI3__attr_f',
5272
+ 'MAKER__QI4__attr_f',
5273
+ 'MAKER__QI5__attr_f',
5274
+ 'MAKER__QI6__attr_f',
5275
+ 'MAKER__QI7__attr_i',
5276
+ 'MAKER__QI8__attr_i',
5277
+ 'MAKER__QI9__attr_i'
5278
+ ];
5263
5279
  // Always fetched, so visibility toggles never need the network. taxon_id is
5264
5280
  // needed by the genome filter even when its column is hidden.
5265
5281
  const $12feb7ca48271e17$var$FETCH_FIELDS = [
5266
5282
  ...new Set([
5267
5283
  ...$12feb7ca48271e17$export$9d77d82cc773e55e,
5268
- ...(0, $4b8473139e48e77e$export$705aff68da3ddba0)
5284
+ ...(0, $4b8473139e48e77e$export$705aff68da3ddba0),
5285
+ ...$12feb7ca48271e17$export$3ae897f4a2495276
5269
5286
  ])
5270
5287
  ];
5271
5288
  // Default columns, in display order. The two stress fields are adjacent so the
@@ -8158,39 +8175,43 @@ const $9e29a4f60318db7a$var$editSearch = (url, edit)=>{
8158
8175
  // (genes/<id>) that only some instances redirect to EBI, and atlasUrl itself is
8159
8176
  // not a browsable page, so point the heatmap's links at EBI's Expression Atlas.
8160
8177
  // JGI studies link to Phytozome, which takes no geneQuery. undefined keeps the
8161
- // heatmap's own URL.
8162
- const $9e29a4f60318db7a$var$resolveUrl = (kind, url, context)=>{
8163
- const genes = $9e29a4f60318db7a$var$genesOf(context);
8164
- const withGenes = (params)=>{
8165
- if (genes.length) params.set('geneQuery', JSON.stringify(genes.map((value)=>({
8166
- value: value
8167
- }))));
8178
+ // heatmap's own URL, null drops the link. isJgiExperiment(accession) tells the
8179
+ // JGI studies apart: they have no full data download (their payload's download
8180
+ // URL is Phytozome's page for the genome), so the Download dialog of a Paralogs
8181
+ // heatmap drawn from one leaves out "Full experiment data on Expression Atlas".
8182
+ const $9e29a4f60318db7a$var$isJgiUrl = (url)=>/^https?:\/\/([^/]+\.)?jgi\.doe\.gov\//i.test(url);
8183
+ const $9e29a4f60318db7a$var$makeResolveUrl = (isJgiExperiment)=>(kind, url, context)=>{
8184
+ const genes = $9e29a4f60318db7a$var$genesOf(context);
8185
+ const withGenes = (params)=>{
8186
+ if (genes.length) params.set('geneQuery', JSON.stringify(genes.map((value)=>({
8187
+ value: value
8188
+ }))));
8189
+ };
8190
+ switch(kind){
8191
+ case 'row':
8192
+ {
8193
+ const uri = context.row && context.row.uri;
8194
+ return uri && !/^https?:/i.test(uri) ? new URL(uri, $9e29a4f60318db7a$var$EBI_GXA).href : undefined;
8195
+ }
8196
+ case 'atlas':
8197
+ return $9e29a4f60318db7a$var$EBI_GXA;
8198
+ case 'experiment':
8199
+ return $9e29a4f60318db7a$var$isEbiUrl(url) ? $9e29a4f60318db7a$var$editSearch(url, withGenes) : undefined;
8200
+ case 'moreInformation':
8201
+ if (context.experiment) return $9e29a4f60318db7a$var$isEbiUrl(url) ? $9e29a4f60318db7a$var$editSearch(url, withGenes) : undefined;
8202
+ return genes.length ? `${$9e29a4f60318db7a$var$EBI_GXA}genes/${encodeURIComponent(genes[0])}` : undefined;
8203
+ case 'download':
8204
+ if (isJgiExperiment(context.experiment) || $9e29a4f60318db7a$var$isJgiUrl(url)) return null;
8205
+ return $9e29a4f60318db7a$var$editSearch(url, (params)=>params.delete('geneQuery'));
8206
+ default:
8207
+ return undefined;
8208
+ }
8168
8209
  };
8169
- switch(kind){
8170
- case 'row':
8171
- {
8172
- const uri = context.row && context.row.uri;
8173
- return uri && !/^https?:/i.test(uri) ? new URL(uri, $9e29a4f60318db7a$var$EBI_GXA).href : undefined;
8174
- }
8175
- case 'atlas':
8176
- return $9e29a4f60318db7a$var$EBI_GXA;
8177
- case 'experiment':
8178
- return $9e29a4f60318db7a$var$isEbiUrl(url) ? $9e29a4f60318db7a$var$editSearch(url, withGenes) : undefined;
8179
- case 'moreInformation':
8180
- if (context.experiment) return $9e29a4f60318db7a$var$isEbiUrl(url) ? $9e29a4f60318db7a$var$editSearch(url, withGenes) : undefined;
8181
- return genes.length ? `${$9e29a4f60318db7a$var$EBI_GXA}genes/${encodeURIComponent(genes[0])}` : undefined;
8182
- case 'download':
8183
- return $9e29a4f60318db7a$var$editSearch(url, (params)=>params.delete('geneQuery'));
8184
- default:
8185
- return undefined;
8186
- }
8187
- };
8188
8210
  const $9e29a4f60318db7a$var$HEATMAP_OPTIONS = {
8189
8211
  showAnatomogram: true,
8190
8212
  isWidget: true,
8191
8213
  showControlMenu: true,
8192
- linkTarget: '_blank',
8193
- resolveUrl: $9e29a4f60318db7a$var$resolveUrl
8214
+ linkTarget: '_blank'
8194
8215
  };
8195
8216
  const $9e29a4f60318db7a$var$studyLabel = (e)=>`${e.type}:${e.description || e._id}`;
8196
8217
  const $9e29a4f60318db7a$var$jgiStudyLabel = (e)=>e.description ? `${e.name || e._id}: ${e.description}` : e.name || e._id;
@@ -8326,6 +8347,13 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
8326
8347
  }, [
8327
8348
  allJgiKey
8328
8349
  ]);
8350
+ // A new resolveUrl does not redraw a heatmap: it asks the latest one for its links.
8351
+ const resolveUrl = (0, $gXNCa$react.useMemo)(()=>{
8352
+ const accessions = new Set(allJgiStudies.map((e)=>e._id));
8353
+ return $9e29a4f60318db7a$var$makeResolveUrl((accession)=>accessions.has(accession));
8354
+ }, [
8355
+ allJgiKey
8356
+ ]);
8329
8357
  // The EBI Studies heatmap waits for the studies list, which names the JGI
8330
8358
  // studies its filter drops: drawn before the list is in (a first visit; the
8331
8359
  // list is persisted), it would show the JGI rows and then redraw without
@@ -8388,9 +8416,11 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
8388
8416
  }),
8389
8417
  activeTab === "paralogs" && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$grameneatlasheatmap.ExpressionAtlasHeatmap), {
8390
8418
  ...$9e29a4f60318db7a$var$HEATMAP_OPTIONS,
8419
+ resolveUrl: resolveUrl,
8391
8420
  atlasUrl: atlasUrl,
8392
8421
  query: paralogsQuery,
8393
- experiment: atlasExperiment
8422
+ experiment: atlasExperiment,
8423
+ downloadFileName: `${geneQuery}-paralogs-${atlasExperiment}`
8394
8424
  }, `${atlasUrl} ${atlasExperiment} ${paralogQuery}`)
8395
8425
  ]
8396
8426
  }, "gxaparalogs"),
@@ -8406,10 +8436,12 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
8406
8436
  }),
8407
8437
  activeTab === "gene" && studiesSettled && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$grameneatlasheatmap.ExpressionAtlasHeatmap), {
8408
8438
  ...$9e29a4f60318db7a$var$HEATMAP_OPTIONS,
8439
+ resolveUrl: resolveUrl,
8409
8440
  atlasUrl: atlasUrl,
8410
8441
  query: allStudiesQuery,
8411
8442
  experiment: false,
8412
- filterRows: filterRows
8443
+ filterRows: filterRows,
8444
+ downloadFileName: `${geneQuery}-ebi-studies`
8413
8445
  }, `${atlasUrl} all ${geneQuery}`)
8414
8446
  ]
8415
8447
  }, "gxa"),
@@ -8453,8 +8485,9 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
8453
8485
  rowFactor: jgiAxesOfStudy.rowFactor,
8454
8486
  columnFactor: jgiAxesOfStudy.columnFactor,
8455
8487
  onChangeFactors: setJgiAxes,
8488
+ downloadFileName: `${geneQuery}-${jgiExperiment}`,
8456
8489
  linkTarget: "_blank",
8457
- resolveUrl: $9e29a4f60318db7a$var$resolveUrl
8490
+ resolveUrl: resolveUrl
8458
8491
  }, `${atlasUrl} ${jgiExperiment} ${geneQuery}`)
8459
8492
  ]
8460
8493
  }, "jgi"),
@@ -13300,6 +13333,7 @@ function $76e15aea9236bd47$var$findRoots(tax) {
13300
13333
  }
13301
13334
  // Post-order: attach .leafIds (taxon_ids of maps in subtree) and .isLeaf
13302
13335
  function $76e15aea9236bd47$var$annotate(node, tax, maps) {
13336
+ maps = maps || {};
13303
13337
  const leafIds = [];
13304
13338
  if (maps[node._id]) {
13305
13339
  node.isLeaf = true;
@@ -13313,9 +13347,20 @@ function $76e15aea9236bd47$var$annotate(node, tax, maps) {
13313
13347
  });
13314
13348
  node.leafIds = leafIds;
13315
13349
  }
13350
+ // The taxonomy's root nodes, annotated with the genomes (maps) under them. The
13351
+ // modal is mounted with the search status, so the taxonomy and the maps can
13352
+ // arrive in either order: missing maps count as no genomes, and the roots are
13353
+ // rebuilt when either changes.
13354
+ function $76e15aea9236bd47$var$buildRoots({ grameneMaps: grameneMaps, grameneTaxonomy: grameneTaxonomy }) {
13355
+ if (!grameneTaxonomy) return [];
13356
+ const roots = $76e15aea9236bd47$var$findRoots(grameneTaxonomy);
13357
+ roots.forEach((r)=>$76e15aea9236bd47$var$annotate(r, grameneTaxonomy, grameneMaps || {}));
13358
+ return roots;
13359
+ }
13316
13360
  // Walk down the spine through single-child internals until a branch point,
13317
13361
  // leaf, or node that is itself a genome.
13318
13362
  function $76e15aea9236bd47$var$compressChain(node, tax, maps) {
13363
+ maps = maps || {};
13319
13364
  const chain = [
13320
13365
  node
13321
13366
  ];
@@ -13467,17 +13512,11 @@ function $76e15aea9236bd47$var$computeOpenState(props) {
13467
13512
  class $76e15aea9236bd47$var$TaxonomyModal extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
13468
13513
  constructor(props){
13469
13514
  super(props);
13470
- const { grameneMaps: grameneMaps, grameneTaxonomy: grameneTaxonomy } = props;
13471
- let roots = [];
13472
- if (grameneTaxonomy) {
13473
- roots = $76e15aea9236bd47$var$findRoots(grameneTaxonomy);
13474
- roots.forEach((r)=>$76e15aea9236bd47$var$annotate(r, grameneTaxonomy, grameneMaps));
13475
- }
13476
13515
  const { selected: selected, expanded: expanded } = $76e15aea9236bd47$var$computeOpenState(props);
13477
13516
  this.state = {
13478
13517
  selected: selected,
13479
13518
  expanded: expanded,
13480
- roots: roots,
13519
+ roots: $76e15aea9236bd47$var$buildRoots(props),
13481
13520
  query: '',
13482
13521
  highlightId: null,
13483
13522
  activeIdx: 0
@@ -13522,6 +13561,16 @@ class $76e15aea9236bd47$var$TaxonomyModal extends (0, ($parcel$interopDefault($g
13522
13561
  }
13523
13562
  }
13524
13563
  componentDidUpdate(prevProps, prevState) {
13564
+ // Maps or taxonomy arrived (or changed): rebuild the tree and reseed the
13565
+ // selection, which was empty if the modal mounted before the maps loaded.
13566
+ if (prevProps.grameneMaps !== this.props.grameneMaps || prevProps.grameneTaxonomy !== this.props.grameneTaxonomy) {
13567
+ const { selected: selected, expanded: expanded } = $76e15aea9236bd47$var$computeOpenState(this.props);
13568
+ this.setState({
13569
+ roots: $76e15aea9236bd47$var$buildRoots(this.props),
13570
+ selected: selected,
13571
+ expanded: expanded
13572
+ });
13573
+ }
13525
13574
  // On each hidden→shown transition, reseed selection from current active
13526
13575
  // genomes and expand every ancestor so selected taxa are visible.
13527
13576
  const wasShown = prevProps.grameneGenomes && prevProps.grameneGenomes.show;
@@ -13593,7 +13642,7 @@ class $76e15aea9236bd47$var$TaxonomyModal extends (0, ($parcel$interopDefault($g
13593
13642
  }
13594
13643
  selectAll() {
13595
13644
  const s = new Set();
13596
- Object.values(this.props.grameneMaps).forEach((m)=>{
13645
+ Object.values(this.props.grameneMaps || {}).forEach((m)=>{
13597
13646
  if (!m.hidden) s.add(m.taxon_id);
13598
13647
  });
13599
13648
  this.setState({
@@ -13610,7 +13659,7 @@ class $76e15aea9236bd47$var$TaxonomyModal extends (0, ($parcel$interopDefault($g
13610
13659
  this.state.selected.forEach((id)=>{
13611
13660
  active[id] = true;
13612
13661
  });
13613
- if (Object.keys(active).length === 0) Object.values(this.props.grameneMaps).forEach((m)=>{
13662
+ if (Object.keys(active).length === 0) Object.values(this.props.grameneMaps || {}).forEach((m)=>{
13614
13663
  if (!m.hidden) active[m.taxon_id] = true;
13615
13664
  });
13616
13665
  this.props.doUpdateGrameneGenomes(active);
@@ -13628,7 +13677,7 @@ class $76e15aea9236bd47$var$TaxonomyModal extends (0, ($parcel$interopDefault($g
13628
13677
  const someOn = selCount > 0 && !allOn;
13629
13678
  const visibleKids = (terminal.children || []).map((cid)=>grameneTaxonomy[cid]).filter((c)=>c && c.leafIds.length > 0);
13630
13679
  const hasKids = visibleKids.length > 0;
13631
- const terminalIsGenome = !!grameneMaps[terminal._id];
13680
+ const terminalIsGenome = !!(grameneMaps && grameneMaps[terminal._id]);
13632
13681
  const isOpen = expanded.has(terminal._id);
13633
13682
  // Deepest clade colour in the chain wins; falls back to inherited.
13634
13683
  const color = chain.reduce((acc, n)=>$76e15aea9236bd47$var$CLADE_COLORS[n.name] || acc, inheritedColor);
@@ -18689,6 +18738,83 @@ const $8f8e530286a37ce9$var$WIDTHS = {
18689
18738
  expr_enhanced_in__attr_ss: 150,
18690
18739
  expr_high_in__attr_ss: 150
18691
18740
  };
18741
+ // MAKER annotation metrics: AED and the QI fractions are 0-1 (2 decimals); the
18742
+ // other QIs are lengths or counts (integers).
18743
+ const $8f8e530286a37ce9$var$MAKER_SET = new Set((0, $12feb7ca48271e17$export$3ae897f4a2495276));
18744
+ const $8f8e530286a37ce9$var$fmtMaker = (f, v)=>{
18745
+ if (v == null || v === '' || !Number.isFinite(+v)) return '';
18746
+ return f.endsWith('__attr_f') ? (+v).toFixed(2) : String(Math.round(+v));
18747
+ };
18748
+ const $8f8e530286a37ce9$var$makerShortName = (f)=>f.replace(/^MAKER__/, '').replace(/__attr_[a-z]$/, '');
18749
+ const $8f8e530286a37ce9$var$makerTooltip = (f)=>f === 'MAKER__AED__attr_f' ? 'MAKER Annotation Edit Distance: 0 = fully supported by evidence, 1 = no support' : `MAKER quality index ${$8f8e530286a37ce9$var$makerShortName(f)}`;
18750
+ // Summary-statistics rows, pinned above the genes. The header row collapses the
18751
+ // rest; stats cover every loaded gene, for each visible column whose values are
18752
+ // all numbers (taxon_id is an identifier, not a measurement).
18753
+ const $8f8e530286a37ce9$var$STAT_ROWS = [
18754
+ {
18755
+ key: 'n',
18756
+ label: 'N'
18757
+ },
18758
+ {
18759
+ key: 'min',
18760
+ label: 'Min'
18761
+ },
18762
+ {
18763
+ key: 'max',
18764
+ label: 'Max'
18765
+ },
18766
+ {
18767
+ key: 'median',
18768
+ label: 'Median'
18769
+ },
18770
+ {
18771
+ key: 'mean',
18772
+ label: 'Mean'
18773
+ },
18774
+ {
18775
+ key: 'sd',
18776
+ label: 'Std dev'
18777
+ }
18778
+ ];
18779
+ const $8f8e530286a37ce9$var$NOT_MEASUREMENTS = new Set([
18780
+ 'taxon_id'
18781
+ ]);
18782
+ const $8f8e530286a37ce9$var$numericFieldsOf = (rows, fields)=>fields.filter((f)=>{
18783
+ if ($8f8e530286a37ce9$var$NOT_MEASUREMENTS.has(f)) return false;
18784
+ let numbers = 0;
18785
+ for (const r of rows){
18786
+ const v = r[f];
18787
+ if (v == null || v === '') continue;
18788
+ if (typeof v !== 'number' || !Number.isFinite(v)) return false;
18789
+ numbers++;
18790
+ }
18791
+ return numbers > 0;
18792
+ });
18793
+ const $8f8e530286a37ce9$var$statsOf = (values)=>{
18794
+ const n = values.length;
18795
+ if (!n) return {
18796
+ n: 0
18797
+ };
18798
+ const sorted = values.slice().sort((a, b)=>a - b);
18799
+ const mean = values.reduce((a, b)=>a + b, 0) / n;
18800
+ const median = n % 2 ? sorted[(n - 1) / 2] : (sorted[n / 2 - 1] + sorted[n / 2]) / 2;
18801
+ const sd = n > 1 ? Math.sqrt(values.reduce((a, v)=>a + (v - mean) * (v - mean), 0) / (n - 1)) : null;
18802
+ return {
18803
+ n: n,
18804
+ min: sorted[0],
18805
+ max: sorted[n - 1],
18806
+ median: median,
18807
+ mean: mean,
18808
+ sd: sd
18809
+ };
18810
+ };
18811
+ // Mean and std dev of a whole-number column still need decimals.
18812
+ const $8f8e530286a37ce9$var$fmtDecimal = (v)=>{
18813
+ if (v == null || !Number.isFinite(v)) return '';
18814
+ const a = Math.abs(v);
18815
+ return a !== 0 && (a < 0.01 || a >= 1e6) ? v.toPrecision(3) : v.toFixed(2);
18816
+ };
18817
+ const $8f8e530286a37ce9$var$isIntegerField = (f, rows)=>f.endsWith('__attr_i') || rows.every((r)=>r[f] == null || Number.isInteger(r[f]));
18692
18818
  const $8f8e530286a37ce9$var$joinValues = (p)=>Array.isArray(p.value) ? p.value.map((v)=>String(v).replace(/_/g, ' ')).join(', ') : p.value ?? '';
18693
18819
  const $8f8e530286a37ce9$var$StressChip = ({ c: c, dir: dir })=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
18694
18820
  style: {
@@ -18759,6 +18885,7 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
18759
18885
  const [fieldQuery, setFieldQuery] = (0, $gXNCa$react.useState)('');
18760
18886
  const [popover, setPopover] = (0, $gXNCa$react.useState)(null);
18761
18887
  const popRef = (0, $gXNCa$react.useRef)(null);
18888
+ const [statsOpen, setStatsOpen] = (0, $gXNCa$react.useState)(true);
18762
18889
  const { docs: docs, total: total, truncated: truncated, status: status, error: error, visibleFields: visibleFields } = attrTable;
18763
18890
  const visibleSet = (0, $gXNCa$react.useMemo)(()=>new Set(visibleFields), [
18764
18891
  visibleFields
@@ -18793,15 +18920,13 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
18793
18920
  }, [
18794
18921
  docs
18795
18922
  ]);
18796
- // Only the Core identifiers + Expression attributes groups are offered.
18923
+ // Only the groups the bundle fetches are offered (Core identifiers, Expression
18924
+ // attributes, MAKER transcript metrics); the catalog drops groups a site lacks.
18797
18925
  const pickerCatalog = (0, $gXNCa$react.useMemo)(()=>{
18798
18926
  if (!fieldCatalog || !fieldCatalog.groups) return null;
18799
18927
  return {
18800
18928
  ...fieldCatalog,
18801
- groups: fieldCatalog.groups.filter((g)=>[
18802
- 'core',
18803
- 'exprattrs'
18804
- ].includes(g.id))
18929
+ groups: fieldCatalog.groups.filter((g)=>(0, $12feb7ca48271e17$export$53e90de8d659db95).includes(g.id))
18805
18930
  };
18806
18931
  }, [
18807
18932
  fieldCatalog
@@ -18828,10 +18953,11 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
18828
18953
  headerTooltip: (0, $4b8473139e48e77e$export$50c1f966fd3af83e)(o),
18829
18954
  width: 22,
18830
18955
  minWidth: 18,
18831
- valueGetter: (p)=>p.data && p.data._organ[o] || '',
18956
+ valueGetter: (p)=>p.data && p.data._organ && p.data._organ[o] || '',
18832
18957
  valueFormatter: ()=>'',
18833
18958
  tooltipValueGetter: (p)=>{
18834
- const lvl = p.data && p.data._organ[o];
18959
+ if (p.node && p.node.rowPinned) return undefined;
18960
+ const lvl = p.data && p.data._organ && p.data._organ[o];
18835
18961
  if (!lvl) return `${(0, $4b8473139e48e77e$export$50c1f966fd3af83e)(o)}: not assayed`;
18836
18962
  const sp = p.data._specific.has(o) ? " \xb7 specific" : p.data._enhanced.has(o) ? " \xb7 enhanced" : '';
18837
18963
  return `${(0, $4b8473139e48e77e$export$50c1f966fd3af83e)(o)}: ${(0, $4b8473139e48e77e$export$433053e2c141d410)[lvl] || lvl}${sp}`;
@@ -18884,6 +19010,18 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
18884
19010
  });
18885
19011
  return;
18886
19012
  }
19013
+ if ($8f8e530286a37ce9$var$MAKER_SET.has(f)) {
19014
+ cols.push({
19015
+ colId: f,
19016
+ field: f,
19017
+ headerName: labelOf(f),
19018
+ headerTooltip: $8f8e530286a37ce9$var$makerTooltip(f),
19019
+ width: 110,
19020
+ type: 'numericColumn',
19021
+ valueFormatter: (p)=>$8f8e530286a37ce9$var$fmtMaker(f, p.value)
19022
+ });
19023
+ return;
19024
+ }
18887
19025
  cols.push({
18888
19026
  colId: f,
18889
19027
  field: f,
@@ -18903,6 +19041,110 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
18903
19041
  tpmRange,
18904
19042
  fieldCatalogByName
18905
19043
  ]);
19044
+ // Visible columns whose values are all numbers get summary statistics.
19045
+ const numericFields = (0, $gXNCa$react.useMemo)(()=>{
19046
+ const fields = columnDefs.filter((c)=>c.field).map((c)=>c.field);
19047
+ return new Set($8f8e530286a37ce9$var$numericFieldsOf(rows, fields));
19048
+ }, [
19049
+ columnDefs,
19050
+ rows
19051
+ ]);
19052
+ const stats = (0, $gXNCa$react.useMemo)(()=>{
19053
+ const out = {};
19054
+ numericFields.forEach((f)=>{
19055
+ out[f] = $8f8e530286a37ce9$var$statsOf(rows.map((r)=>r[f]).filter((v)=>typeof v === 'number' && Number.isFinite(v)));
19056
+ });
19057
+ return out;
19058
+ }, [
19059
+ numericFields,
19060
+ rows
19061
+ ]);
19062
+ const pinnedTopRows = (0, $gXNCa$react.useMemo)(()=>{
19063
+ if (!numericFields.size || !rows.length) return [];
19064
+ const header = {
19065
+ _stat: 'header'
19066
+ };
19067
+ if (!statsOpen) return [
19068
+ header
19069
+ ];
19070
+ return [
19071
+ header,
19072
+ ...$8f8e530286a37ce9$var$STAT_ROWS.map(({ key: key, label: label })=>{
19073
+ const row = {
19074
+ _stat: key,
19075
+ _statLabel: label
19076
+ };
19077
+ numericFields.forEach((f)=>{
19078
+ row[f] = stats[f][key];
19079
+ });
19080
+ return row;
19081
+ })
19082
+ ];
19083
+ }, [
19084
+ numericFields,
19085
+ stats,
19086
+ statsOpen,
19087
+ rows.length
19088
+ ]);
19089
+ // The first column carries the stats rows' labels; numeric columns show their
19090
+ // statistics, formatted like the column (N as a count, mean/sd with decimals).
19091
+ const gridColumnDefs = (0, $gXNCa$react.useMemo)(()=>columnDefs.map((col, i)=>{
19092
+ const first = i === 0;
19093
+ const numeric = !!col.field && numericFields.has(col.field);
19094
+ if (!first && !numeric) return col;
19095
+ const intField = numeric && $8f8e530286a37ce9$var$isIntegerField(col.field, rows);
19096
+ const statText = (p)=>{
19097
+ const v = p.value;
19098
+ if (v == null || v === '') return '';
19099
+ const k = p.data._stat;
19100
+ if (k === 'n') return Number(v).toLocaleString();
19101
+ if (k === 'mean' || k === 'sd' || k === 'median' && intField && !Number.isInteger(v)) return col.field === 'expr_max_tpm__attr_f' ? (0, $4b8473139e48e77e$export$8eaa32ef86afdd9c)(v) : $8f8e530286a37ce9$var$fmtDecimal(v);
19102
+ return col.valueFormatter ? col.valueFormatter({
19103
+ ...p,
19104
+ value: v
19105
+ }) : String(v);
19106
+ };
19107
+ return {
19108
+ ...col,
19109
+ valueFormatter: (p)=>{
19110
+ if (!(p.node && p.node.rowPinned)) return col.valueFormatter ? col.valueFormatter(p) : p.value;
19111
+ return numeric ? statText(p) : '';
19112
+ },
19113
+ cellStyle: (p)=>p.node && p.node.rowPinned ? null : typeof col.cellStyle === 'function' ? col.cellStyle(p) : col.cellStyle,
19114
+ cellRenderer: (p)=>{
19115
+ if (p.node && p.node.rowPinned) {
19116
+ const k = p.data._stat;
19117
+ if (first && k === 'header') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
19118
+ className: "attrtable-stats-toggle",
19119
+ title: statsOpen ? 'Hide summary statistics' : 'Show summary statistics',
19120
+ children: [
19121
+ statsOpen ? "\u25BE" : "\u25B8",
19122
+ " Summary statistics (",
19123
+ rows.length.toLocaleString(),
19124
+ " genes)"
19125
+ ]
19126
+ });
19127
+ if (first) {
19128
+ const value = numeric ? statText(p) : '';
19129
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
19130
+ className: "attrtable-stats-label",
19131
+ children: [
19132
+ p.data._statLabel,
19133
+ value ? `: ${value}` : ''
19134
+ ]
19135
+ });
19136
+ }
19137
+ return k === 'header' ? '' : p.valueFormatted;
19138
+ }
19139
+ return col.cellRenderer ? /*#__PURE__*/ (0, ($parcel$interopDefault($gXNCa$react))).createElement(col.cellRenderer, p) : p.valueFormatted ?? p.value;
19140
+ }
19141
+ };
19142
+ }), [
19143
+ columnDefs,
19144
+ numericFields,
19145
+ rows,
19146
+ statsOpen
19147
+ ]);
18906
19148
  // Build the click-popover for a cell: a title (the column meaning) and one
18907
19149
  // filterable item per value. Each item carries the fq_field / fq_value the
18908
19150
  // "add filter" button hands to doAcceptGrameneSuggestion.
@@ -18971,6 +19213,27 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
18971
19213
  ]
18972
19214
  };
18973
19215
  }
19216
+ if ($8f8e530286a37ce9$var$MAKER_SET.has(colId)) {
19217
+ // AED: lower is better supported, so offer "at most"; the QI metrics "at least".
19218
+ const v = data[colId];
19219
+ if (!Number.isFinite(+v)) return {
19220
+ title: labelOf(colId),
19221
+ items: []
19222
+ };
19223
+ const shown = $8f8e530286a37ce9$var$fmtMaker(colId, +v);
19224
+ const short = $8f8e530286a37ce9$var$makerShortName(colId);
19225
+ return colId === 'MAKER__AED__attr_f' ? {
19226
+ title: labelOf(colId),
19227
+ items: [
19228
+ mk(`\u{2264} ${shown}`, colId, `[* TO ${v}]`, 'MAKER', `${short} \u{2264} ${shown}`)
19229
+ ]
19230
+ } : {
19231
+ title: labelOf(colId),
19232
+ items: [
19233
+ mk(`\u{2265} ${shown}`, colId, `[${v} TO *]`, 'MAKER', `${short} \u{2265} ${shown}`)
19234
+ ]
19235
+ };
19236
+ }
18974
19237
  // Generic (Expression class, and any core / added attribute).
18975
19238
  const label = labelOf(colId);
18976
19239
  const raw = data[colId];
@@ -18983,6 +19246,11 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
18983
19246
  };
18984
19247
  };
18985
19248
  const onCellClicked = (e)=>{
19249
+ if (e.node && e.node.rowPinned) {
19250
+ if (e.data && e.data._stat === 'header') setStatsOpen((v)=>!v);
19251
+ setPopover(null);
19252
+ return;
19253
+ }
18986
19254
  const desc = buildCellPopover(e.column.getColId(), e.data);
18987
19255
  if (!desc || !desc.items.length) {
18988
19256
  setPopover(null);
@@ -19145,7 +19413,9 @@ const $8f8e530286a37ce9$var$AttrTableViewCmp = (props)=>{
19145
19413
  className: `ag-theme-quartz attrtable-aggrid${organShown ? ' attrtable-tall-header' : ''}`,
19146
19414
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$aggridreact.AgGridReact), {
19147
19415
  rowData: rows,
19148
- columnDefs: columnDefs,
19416
+ columnDefs: gridColumnDefs,
19417
+ pinnedTopRowData: pinnedTopRows,
19418
+ getRowClass: (p)=>p.node.rowPinned ? p.data._stat === 'header' ? 'attrtable-stats-header' : 'attrtable-stats-row' : undefined,
19149
19419
  defaultColDef: $8f8e530286a37ce9$var$DEFAULT_COL_DEF,
19150
19420
  animateRows: false,
19151
19421
  suppressFieldDotNotation: true,