gramene-search 2.5.3 → 2.7.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
@@ -7417,6 +7417,460 @@ const $5c2c79352d3d7b81$export$b2e089eb3692b073 = (props)=>/*#__PURE__*/ (0, $gX
7417
7417
  });
7418
7418
 
7419
7419
 
7420
+ // Host-defined tbrowse zone for the Homology gene tree, redesigned to compare
7421
+ // expression across a gene family. Three parts, aligned to the tree leaves:
7422
+ // 1. a gene × organ heatmap of ordinal expression levels (expr_organ_level),
7423
+ // with markers on each gene's tissue-specific / -enhanced organs;
7424
+ // 2. a Max TPM column (expr_max_tpm), log-scaled magnitude;
7425
+ // 3. a Stress column of ↑activated / ↓repressed condition chips
7426
+ // (expr_activated_by / expr_repressed_by).
7427
+ // Data is threaded in via hostData.exprAttrs (built by buildExprData from the
7428
+ // tree-scoped /search response), mirroring how the neighborhood / genome zones
7429
+ // receive their async data. Color/legend conventions follow exprViz/HeatmapPlot.
7430
+
7431
+
7432
+
7433
+ // Canonical anatomical ordering (vegetative → reproductive → seed). Organs not
7434
+ // listed here are appended alphabetically so nothing is ever dropped.
7435
+ const $cd8bc494277e92a4$var$ORGAN_ORDER = [
7436
+ 'root',
7437
+ 'shoot',
7438
+ 'stem',
7439
+ 'leaf',
7440
+ 'meristem',
7441
+ 'vasculature',
7442
+ 'tuber',
7443
+ 'cotyledon',
7444
+ 'inflorescence',
7445
+ 'flower',
7446
+ 'anther_pollen',
7447
+ 'fruit',
7448
+ 'pericarp',
7449
+ 'seed',
7450
+ 'endosperm',
7451
+ 'embryo'
7452
+ ];
7453
+ // Short column-header codes; unknown organs fall back to their first 3 letters.
7454
+ const $cd8bc494277e92a4$var$ORGAN_ABBR = {
7455
+ root: 'rt',
7456
+ shoot: 'sht',
7457
+ stem: 'stm',
7458
+ leaf: 'lf',
7459
+ meristem: 'mer',
7460
+ vasculature: 'vas',
7461
+ tuber: 'tbr',
7462
+ cotyledon: 'cot',
7463
+ inflorescence: 'inf',
7464
+ flower: 'flw',
7465
+ anther_pollen: 'ant',
7466
+ fruit: 'frt',
7467
+ pericarp: 'per',
7468
+ seed: 'sd',
7469
+ endosperm: 'end',
7470
+ embryo: 'emb'
7471
+ };
7472
+ const $cd8bc494277e92a4$var$abbr = (o)=>$cd8bc494277e92a4$var$ORGAN_ABBR[o] || o.slice(0, 3);
7473
+ // Ordinal expression level → color. not_expressed gets a distinct pale tint (it
7474
+ // IS a measurement); an organ a species doesn't report stays transparent (= not
7475
+ // assayed). Ramp matches HeatmapPlot's pale→dark blue.
7476
+ const $cd8bc494277e92a4$var$LEVEL_ORDER = [
7477
+ 'not_expressed',
7478
+ 'low',
7479
+ 'medium',
7480
+ 'high',
7481
+ 'very_high'
7482
+ ];
7483
+ const $cd8bc494277e92a4$var$LEVEL_COLOR = {
7484
+ not_expressed: '#eef2f6',
7485
+ low: '#cfe0ee',
7486
+ medium: '#8fbbdc',
7487
+ high: '#3f86c2',
7488
+ very_high: '#0a3d72'
7489
+ };
7490
+ const $cd8bc494277e92a4$var$LEVEL_LABEL = {
7491
+ not_expressed: 'not expressed',
7492
+ low: 'low',
7493
+ medium: 'medium',
7494
+ high: 'high',
7495
+ very_high: 'very high'
7496
+ };
7497
+ // Stress chips: activated (induced) = warm, repressed = cool.
7498
+ const $cd8bc494277e92a4$var$STRESS = {
7499
+ up: {
7500
+ bg: '#fdecea',
7501
+ fg: '#c0392b'
7502
+ },
7503
+ down: {
7504
+ bg: '#eaf2fb',
7505
+ fg: '#2e6fae'
7506
+ }
7507
+ };
7508
+ const $cd8bc494277e92a4$var$MARKER = '#d35400'; // specific-to dot / enhanced-in outline
7509
+ const $cd8bc494277e92a4$var$ORGAN_CELL_W = 16;
7510
+ const $cd8bc494277e92a4$var$MAXTPM_W = 46;
7511
+ const $cd8bc494277e92a4$var$STRESS_MIN = 120;
7512
+ function $cd8bc494277e92a4$export$965a6b1d7408d496(docs, tree) {
7513
+ const nodeOf = {};
7514
+ const nodeGene = {}; // leaf nodeId -> gene id, for every leaf (hover readout)
7515
+ if (tree && tree.nodes) Object.values(tree.nodes).forEach((n)=>{
7516
+ if (n.isLeaf && n.geneId) {
7517
+ nodeOf[n.geneId] = n.id;
7518
+ nodeGene[n.id] = n.geneId;
7519
+ }
7520
+ });
7521
+ const organSet = new Set();
7522
+ const byNode = {};
7523
+ let tpmMin = Infinity;
7524
+ let tpmMax = -Infinity;
7525
+ (docs || []).forEach((d)=>{
7526
+ if (!d || !d.id) return;
7527
+ const nodeId = nodeOf[d.id];
7528
+ if (!nodeId) return;
7529
+ const organLevels = {};
7530
+ (d.expr_organ_level__attr_ss || []).forEach((t)=>{
7531
+ const i = t.lastIndexOf(':');
7532
+ if (i < 0) return;
7533
+ const organ = t.slice(0, i);
7534
+ organLevels[organ] = t.slice(i + 1);
7535
+ organSet.add(organ);
7536
+ });
7537
+ const maxTpm = Number.isFinite(+d.expr_max_tpm__attr_f) ? +d.expr_max_tpm__attr_f : null;
7538
+ if (maxTpm !== null) {
7539
+ if (maxTpm < tpmMin) tpmMin = maxTpm;
7540
+ if (maxTpm > tpmMax) tpmMax = maxTpm;
7541
+ }
7542
+ byNode[nodeId] = {
7543
+ cls: d.expr_class__attr_ss || [],
7544
+ organLevels: organLevels,
7545
+ specificTo: new Set(d.expr_specific_to__attr_ss || []),
7546
+ enhancedIn: new Set(d.expr_enhanced_in__attr_ss || []),
7547
+ maxTpm: maxTpm,
7548
+ activatedBy: d.expr_activated_by__attr_ss || [],
7549
+ repressedBy: d.expr_repressed_by__attr_ss || []
7550
+ };
7551
+ });
7552
+ const known = $cd8bc494277e92a4$var$ORGAN_ORDER.filter((o)=>organSet.has(o));
7553
+ const unknown = [
7554
+ ...organSet
7555
+ ].filter((o)=>!$cd8bc494277e92a4$var$ORGAN_ORDER.includes(o)).sort();
7556
+ return {
7557
+ organs: [
7558
+ ...known,
7559
+ ...unknown
7560
+ ],
7561
+ byNode: byNode,
7562
+ nodeGene: nodeGene,
7563
+ maxTpm: {
7564
+ min: tpmMin === Infinity ? 0 : tpmMin,
7565
+ max: tpmMax === -Infinity ? 0 : tpmMax
7566
+ }
7567
+ };
7568
+ }
7569
+ // Shared row-highlight background, matching the other zones so hovering any zone
7570
+ // lights up the same row across all of them.
7571
+ function $cd8bc494277e92a4$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
7572
+ if (isSelected) return 'var(--tbrowse-row-select-bg)';
7573
+ if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
7574
+ if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
7575
+ return 'transparent';
7576
+ }
7577
+ // Log-scaled fraction of v within [min,max], for the Max TPM heat background.
7578
+ function $cd8bc494277e92a4$var$tpmFraction(v, range) {
7579
+ if (!Number.isFinite(v)) return null;
7580
+ const lo = Math.log10((range.min || 0) + 1);
7581
+ const hi = Math.log10((range.max || 0) + 1);
7582
+ if (hi <= lo) return 0.5;
7583
+ return Math.max(0, Math.min(1, (Math.log10(v + 1) - lo) / (hi - lo)));
7584
+ }
7585
+ function $cd8bc494277e92a4$var$fmtTpm(v) {
7586
+ if (!Number.isFinite(v)) return '';
7587
+ return v >= 10 ? String(Math.round(v)) : String(Math.round(v * 10) / 10);
7588
+ }
7589
+ function $cd8bc494277e92a4$var$fmtClass(cls) {
7590
+ return cls && cls.length ? cls.map((c)=>c.replace(/_/g, ' ')).join(', ') : null;
7591
+ }
7592
+ const $cd8bc494277e92a4$var$gridBorder = '1px solid var(--tbrowse-grid-line, rgba(0,0,0,0.06))';
7593
+ const $cd8bc494277e92a4$var$stressWidth = (width, organCount)=>Math.max($cd8bc494277e92a4$var$STRESS_MIN, width - organCount * $cd8bc494277e92a4$var$ORGAN_CELL_W - $cd8bc494277e92a4$var$MAXTPM_W);
7594
+ const $cd8bc494277e92a4$var$ExprHeader = ({ zoneState: zoneState, setZoneState: setZoneState, width: width, data: data, hoveredNodeId: hoveredNodeId })=>{
7595
+ const ea = data.hostData && data.hostData.exprAttrs || {};
7596
+ const organs = ea.organs || [];
7597
+ const gid = hoveredNodeId && ea.nodeGene && ea.nodeGene[hoveredNodeId];
7598
+ const gene = hoveredNodeId && ea.byNode && ea.byNode[hoveredNodeId];
7599
+ const clsText = gene && $cd8bc494277e92a4$var$fmtClass(gene.cls);
7600
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
7601
+ style: {
7602
+ height: '100%',
7603
+ display: 'flex',
7604
+ flexDirection: 'column',
7605
+ justifyContent: 'flex-end',
7606
+ overflow: 'hidden'
7607
+ },
7608
+ children: [
7609
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
7610
+ style: {
7611
+ display: 'flex',
7612
+ alignItems: 'center',
7613
+ gap: 8,
7614
+ padding: '0 4px',
7615
+ fontSize: 12
7616
+ },
7617
+ children: [
7618
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$tbrowse.EditableZoneName), {
7619
+ defaultName: "Expression",
7620
+ customName: zoneState?.name,
7621
+ onChange: (next)=>setZoneState((s)=>({
7622
+ ...s ?? {},
7623
+ name: next
7624
+ }))
7625
+ }),
7626
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
7627
+ style: {
7628
+ display: 'flex',
7629
+ alignItems: 'center',
7630
+ gap: 1,
7631
+ fontSize: 9,
7632
+ opacity: 0.85
7633
+ },
7634
+ title: "expression level",
7635
+ children: $cd8bc494277e92a4$var$LEVEL_ORDER.map((lv)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
7636
+ title: $cd8bc494277e92a4$var$LEVEL_LABEL[lv],
7637
+ style: {
7638
+ width: 10,
7639
+ height: 10,
7640
+ background: $cd8bc494277e92a4$var$LEVEL_COLOR[lv],
7641
+ border: $cd8bc494277e92a4$var$gridBorder,
7642
+ display: 'inline-block'
7643
+ }
7644
+ }, lv))
7645
+ })
7646
+ ]
7647
+ }),
7648
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
7649
+ title: gid ? clsText ? `${gid} \xb7 ${clsText}` : gid : 'hover a node to see its gene and expression class',
7650
+ style: {
7651
+ padding: '0 4px 2px',
7652
+ fontSize: 11,
7653
+ overflow: 'hidden',
7654
+ whiteSpace: 'nowrap',
7655
+ textOverflow: 'ellipsis'
7656
+ },
7657
+ children: gid ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
7658
+ children: [
7659
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("b", {
7660
+ children: gid
7661
+ }),
7662
+ clsText ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
7663
+ style: {
7664
+ opacity: 0.7
7665
+ },
7666
+ children: ` \xb7 ${clsText}`
7667
+ }) : null
7668
+ ]
7669
+ }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
7670
+ style: {
7671
+ opacity: 0.5
7672
+ },
7673
+ children: "hover a node\u2026"
7674
+ })
7675
+ }),
7676
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
7677
+ style: {
7678
+ display: 'flex',
7679
+ width: width
7680
+ },
7681
+ children: [
7682
+ organs.map((o)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
7683
+ title: o.replace(/_/g, ' '),
7684
+ style: {
7685
+ width: $cd8bc494277e92a4$var$ORGAN_CELL_W,
7686
+ fontSize: 8,
7687
+ textAlign: 'center',
7688
+ overflow: 'hidden',
7689
+ whiteSpace: 'nowrap',
7690
+ borderRight: $cd8bc494277e92a4$var$gridBorder,
7691
+ opacity: 0.8
7692
+ },
7693
+ children: $cd8bc494277e92a4$var$abbr(o)
7694
+ }, o)),
7695
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
7696
+ style: {
7697
+ width: $cd8bc494277e92a4$var$MAXTPM_W,
7698
+ fontSize: 9,
7699
+ fontWeight: 600,
7700
+ textAlign: 'right',
7701
+ paddingRight: 4,
7702
+ opacity: 0.8
7703
+ },
7704
+ children: "TPM"
7705
+ }),
7706
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
7707
+ style: {
7708
+ width: $cd8bc494277e92a4$var$stressWidth(width, organs.length),
7709
+ fontSize: 9,
7710
+ fontWeight: 600,
7711
+ paddingLeft: 4,
7712
+ opacity: 0.8
7713
+ },
7714
+ children: "Stress"
7715
+ })
7716
+ ]
7717
+ })
7718
+ ]
7719
+ });
7720
+ };
7721
+ const $cd8bc494277e92a4$var$OrganCell = ({ organ: organ, gene: gene })=>{
7722
+ const level = gene && gene.organLevels[organ];
7723
+ const specific = !!(gene && gene.specificTo.has(organ));
7724
+ const enhanced = !!(gene && gene.enhancedIn.has(organ));
7725
+ const title = level ? `${organ.replace(/_/g, ' ')}: ${$cd8bc494277e92a4$var$LEVEL_LABEL[level] || level}${specific ? " \xb7 specific" : enhanced ? " \xb7 enhanced" : ''}` : `${organ.replace(/_/g, ' ')}: not assayed`;
7726
+ const style = {
7727
+ position: 'relative',
7728
+ width: $cd8bc494277e92a4$var$ORGAN_CELL_W,
7729
+ height: '100%',
7730
+ boxSizing: 'border-box',
7731
+ background: level ? $cd8bc494277e92a4$var$LEVEL_COLOR[level] || 'transparent' : 'transparent',
7732
+ borderRight: $cd8bc494277e92a4$var$gridBorder
7733
+ };
7734
+ // enhanced-in: thin outline; specific-to (stronger): corner dot.
7735
+ if (enhanced && !specific) style.boxShadow = `inset 0 0 0 1px ${$cd8bc494277e92a4$var$MARKER}`;
7736
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
7737
+ title: title,
7738
+ style: style,
7739
+ children: specific && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
7740
+ style: {
7741
+ position: 'absolute',
7742
+ top: 1,
7743
+ right: 1,
7744
+ width: 4,
7745
+ height: 4,
7746
+ borderRadius: '50%',
7747
+ background: $cd8bc494277e92a4$var$MARKER
7748
+ }
7749
+ })
7750
+ });
7751
+ };
7752
+ const $cd8bc494277e92a4$var$StressCell = ({ gene: gene, width: width })=>{
7753
+ const up = gene && gene.activatedBy || [];
7754
+ const down = gene && gene.repressedBy || [];
7755
+ const title = [
7756
+ up.length ? `\u{2191} ${up.join(', ')}` : '',
7757
+ down.length ? `\u{2193} ${down.join(', ')}` : ''
7758
+ ].filter(Boolean).join(' ');
7759
+ const chip = (c, dir, key)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
7760
+ style: {
7761
+ fontSize: 9,
7762
+ lineHeight: '14px',
7763
+ padding: '0 3px',
7764
+ borderRadius: 2,
7765
+ whiteSpace: 'nowrap',
7766
+ background: $cd8bc494277e92a4$var$STRESS[dir].bg,
7767
+ color: $cd8bc494277e92a4$var$STRESS[dir].fg
7768
+ },
7769
+ children: (dir === 'up' ? "\u2191" : "\u2193") + c
7770
+ }, key);
7771
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
7772
+ title: title,
7773
+ style: {
7774
+ width: width,
7775
+ display: 'flex',
7776
+ flexWrap: 'nowrap',
7777
+ gap: 2,
7778
+ overflow: 'hidden',
7779
+ alignItems: 'center',
7780
+ paddingLeft: 4,
7781
+ boxSizing: 'border-box'
7782
+ },
7783
+ children: [
7784
+ up.map((c, i)=>chip(c, 'up', `u${i}`)),
7785
+ down.map((c, i)=>chip(c, 'down', `d${i}`))
7786
+ ]
7787
+ });
7788
+ };
7789
+ const $cd8bc494277e92a4$var$ExprBody = ({ visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode })=>{
7790
+ const ea = data.hostData && data.hostData.exprAttrs || {};
7791
+ const organs = ea.organs || [];
7792
+ const byNode = ea.byNode || {};
7793
+ const tpmRange = ea.maxTpm || {
7794
+ min: 0,
7795
+ max: 0
7796
+ };
7797
+ const sW = $cd8bc494277e92a4$var$stressWidth(width, organs.length);
7798
+ const totalHeight = visibleRows.length ? visibleRows[visibleRows.length - 1].y + visibleRows[visibleRows.length - 1].height : 0;
7799
+ const rows = visibleRows.slice(rowRange.startIndex, rowRange.endIndex);
7800
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
7801
+ style: {
7802
+ position: 'relative',
7803
+ width: width,
7804
+ height: totalHeight,
7805
+ overflow: 'hidden'
7806
+ },
7807
+ children: rows.map((r)=>{
7808
+ const gene = byNode[r.nodeId];
7809
+ const background = $cd8bc494277e92a4$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId)));
7810
+ const tpmFrac = gene ? $cd8bc494277e92a4$var$tpmFraction(gene.maxTpm, tpmRange) : null;
7811
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
7812
+ onMouseEnter: ()=>onHoverNode(r.nodeId),
7813
+ onMouseLeave: ()=>onHoverNode(null),
7814
+ onClick: ()=>onSelectNode(r.nodeId),
7815
+ style: {
7816
+ position: 'absolute',
7817
+ top: r.y,
7818
+ height: r.height,
7819
+ left: 0,
7820
+ width: width,
7821
+ display: 'flex',
7822
+ boxSizing: 'border-box',
7823
+ background: background,
7824
+ cursor: 'pointer',
7825
+ opacity: r.opacity ?? 1
7826
+ },
7827
+ children: [
7828
+ organs.map((o)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($cd8bc494277e92a4$var$OrganCell, {
7829
+ organ: o,
7830
+ gene: gene
7831
+ }, o)),
7832
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
7833
+ title: gene && Number.isFinite(gene.maxTpm) ? `max TPM: ${gene.maxTpm}` : '',
7834
+ style: {
7835
+ width: $cd8bc494277e92a4$var$MAXTPM_W,
7836
+ height: '100%',
7837
+ boxSizing: 'border-box',
7838
+ borderRight: $cd8bc494277e92a4$var$gridBorder,
7839
+ display: 'flex',
7840
+ alignItems: 'center',
7841
+ justifyContent: 'flex-end',
7842
+ paddingRight: 4,
7843
+ fontSize: 9,
7844
+ fontVariantNumeric: 'tabular-nums',
7845
+ color: 'var(--tbrowse-text)',
7846
+ background: tpmFrac === null ? 'transparent' : `rgba(33, 102, 172, ${(0.1 + 0.65 * tpmFrac).toFixed(3)})`
7847
+ },
7848
+ children: gene ? $cd8bc494277e92a4$var$fmtTpm(gene.maxTpm) : ''
7849
+ }),
7850
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($cd8bc494277e92a4$var$StressCell, {
7851
+ gene: gene,
7852
+ width: sW
7853
+ })
7854
+ ]
7855
+ }, r.nodeId);
7856
+ })
7857
+ });
7858
+ };
7859
+ const $cd8bc494277e92a4$export$44e8c3b1eee47e9e = {
7860
+ id: 'expression',
7861
+ displayName: 'Expression',
7862
+ Header: $cd8bc494277e92a4$var$ExprHeader,
7863
+ Body: $cd8bc494277e92a4$var$ExprBody,
7864
+ defaultWidth: 70,
7865
+ minWidth: 280,
7866
+ defaultZoneState: {},
7867
+ // Stay hidden until its async data lands; tbrowse's auto-enable effect flips
7868
+ // it on once isAvailable() turns true (same lifecycle as neighborhood/genome).
7869
+ isAvailable: (data)=>Boolean(data.hostData && data.hostData.exprAttrs && data.hostData.exprAttrs.organs && data.hostData.exprAttrs.organs.length),
7870
+ defaultVisible: false
7871
+ };
7872
+
7873
+
7420
7874
 
7421
7875
  const $047461923b1badda$export$54387eca9e368edc = (config)=>{
7422
7876
  if (!config) return null;
@@ -7480,7 +7934,10 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
7480
7934
  neighborhoodStatus: undefined,
7481
7935
  geneStructures: null,
7482
7936
  geneStructuresTreeId: null,
7483
- geneStructuresStatus: undefined
7937
+ geneStructuresStatus: undefined,
7938
+ exprAttrs: null,
7939
+ exprAttrsTreeId: null,
7940
+ exprAttrsStatus: undefined
7484
7941
  };
7485
7942
  if (!props.geneDocs.hasOwnProperty(props.searchResult.id)) props.requestGene(props.searchResult.id);
7486
7943
  this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(props.grameneTaxonomy));
@@ -7501,11 +7958,19 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
7501
7958
  isGenomeZoneEnabled() {
7502
7959
  return !!(this.props.configuration && this.props.configuration.enable_tbrowse_genome_zone);
7503
7960
  }
7961
+ // The expression-attributes zone is opt-in per site via
7962
+ // `enable_tbrowse_expression_zone` (defaults to false) — the expr_*__attr_*
7963
+ // fields are sorghum-specific.
7964
+ isExpressionZoneEnabled() {
7965
+ return !!(this.props.configuration && this.props.configuration.enable_tbrowse_expression_zone);
7966
+ }
7504
7967
  getTbrowseZones() {
7505
- return this.isGenomeZoneEnabled() ? [
7506
- ...$64fad37f770d2bfe$var$TBROWSE_BASE_ZONES,
7507
- $64fad37f770d2bfe$var$genomeZone
7508
- ] : $64fad37f770d2bfe$var$TBROWSE_BASE_ZONES;
7968
+ const zones = [
7969
+ ...$64fad37f770d2bfe$var$TBROWSE_BASE_ZONES
7970
+ ];
7971
+ if (this.isGenomeZoneEnabled()) zones.push($64fad37f770d2bfe$var$genomeZone);
7972
+ if (this.isExpressionZoneEnabled()) zones.push((0, $cd8bc494277e92a4$export$44e8c3b1eee47e9e));
7973
+ return zones;
7509
7974
  }
7510
7975
  getHeight() {
7511
7976
  const h = this.getHomologySlice().height;
@@ -7529,6 +7994,42 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
7529
7994
  tbrowse: viewState
7530
7995
  });
7531
7996
  }
7997
+ // When the user has limited the search to a subset of genomes
7998
+ // (grameneGenomes.active), prune the gene tree to genes from those genomes
7999
+ // — mirroring the taxon_id filter api.js applies to the search itself, and
8000
+ // the genomesOfInterest TreeVis already honours. Returns the ids of the
8001
+ // highest nodes whose entire subtree lies outside the active set (maximal
8002
+ // excluded clades), rather than individual leaves: tbrowse then sheds one
8003
+ // stub per clade instead of one per leaf, and its single-marker count is
8004
+ // meaningful. The gene of interest is always kept so the tree never
8005
+ // collapses to nothing. Empty active set means "no limit" → full tree.
8006
+ getGenomePrunedIds() {
8007
+ const active = this.props.grameneGenomes && this.props.grameneGenomes.active || {};
8008
+ const activeIds = Object.keys(active);
8009
+ if (activeIds.length === 0) return [];
8010
+ if (!this._tbrowseData) return [];
8011
+ const activeSet = new Set(activeIds.map(String));
8012
+ const keepGeneId = this.gene && this.gene._id;
8013
+ const nodes = this._tbrowseData.tree.nodes;
8014
+ const children = {};
8015
+ Object.values(nodes).forEach((n)=>{
8016
+ if (n.parentId != null) (children[n.parentId] = children[n.parentId] || []).push(n.id);
8017
+ });
8018
+ // hasKept(node): does the subtree contain a leaf we must keep (its taxon
8019
+ // is active, or it's the gene of interest)? Memoised post-order walk.
8020
+ const memo = {};
8021
+ const hasKept = (id)=>{
8022
+ if (id in memo) return memo[id];
8023
+ const n = nodes[id];
8024
+ const v = n.isLeaf ? n.taxonomyId != null && activeSet.has(String(n.taxonomyId)) || n.geneId === keepGeneId : (children[id] || []).some(hasKept);
8025
+ memo[id] = v;
8026
+ return v;
8027
+ };
8028
+ // A node is a maximal excluded clade when it keeps nothing but its parent
8029
+ // does (or it's the root) — i.e. the highest fully-excluded node on its
8030
+ // path. The root always keeps the gene of interest, so it is never pruned.
8031
+ return Object.values(nodes).filter((n)=>!hasKept(n.id) && (n.parentId == null || hasKept(n.parentId))).map((n)=>n.id);
8032
+ }
7532
8033
  // Seed the bundle with a pivot-computed initial tbrowse view state once the
7533
8034
  // tree data is available and the user is actually looking at the tbrowse
7534
8035
  // viewer. Called from lifecycle (not render) to avoid dispatching mid-render.
@@ -7605,6 +8106,8 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
7605
8106
  this.fetchNeighborhood(treeId);
7606
8107
  // Only pay for gene-structure data when the genome zone is actually enabled.
7607
8108
  if (this.isGenomeZoneEnabled()) this.fetchGeneStructures(treeId, this._tbrowseData.tree);
8109
+ // Same for the per-gene expression attributes zone.
8110
+ if (this.isExpressionZoneEnabled()) this.fetchExprAttrs(treeId, this._tbrowseData.tree);
7608
8111
  }
7609
8112
  fetchNeighborhood(treeId) {
7610
8113
  if (this._neighborhoodFetchedFor === treeId) return;
@@ -7683,6 +8186,49 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
7683
8186
  this._geneStructuresFetchedFor = null;
7684
8187
  });
7685
8188
  }
8189
+ // Fetch the expression attributes (expr_*__attr_*) for this tree's genes in a
8190
+ // single query: scope by gene_tree and the `expression_attributes` capability
8191
+ // so only genes that actually have the data come back (rows high enough to
8192
+ // cover the whole tree). Uses /search (not /genes, whose trimmed docs omit the
8193
+ // attr fields) with an fl glob so the columns are discovered from the data
8194
+ // rather than hard-coded. `tree` is still needed to map gene ids to leaf node
8195
+ // ids (see buildExprByNode).
8196
+ fetchExprAttrs(treeId, tree) {
8197
+ if (this._exprAttrsFetchedFor === treeId) return;
8198
+ this._exprAttrsFetchedFor = treeId;
8199
+ this.setState({
8200
+ exprAttrsStatus: 'loading'
8201
+ });
8202
+ const api = this.props.grameneAPI;
8203
+ const url = new URL(`${api}/search`);
8204
+ url.searchParams.set('q', '*:*');
8205
+ url.searchParams.append('fq', `gene_tree:${treeId}`);
8206
+ url.searchParams.append('fq', 'capabilities:expression_attributes');
8207
+ url.searchParams.set('fl', 'id,expr_*__attr_*');
8208
+ url.searchParams.set('rows', '10000');
8209
+ fetch(url.toString(), {
8210
+ headers: {
8211
+ Accept: 'application/json'
8212
+ }
8213
+ }).then((res)=>{
8214
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
8215
+ return res.json();
8216
+ }).then((json)=>{
8217
+ if (this._exprAttrsFetchedFor !== treeId) return;
8218
+ const docs = json && json.response && Array.isArray(json.response.docs) ? json.response.docs : [];
8219
+ this.setState({
8220
+ exprAttrs: (0, $cd8bc494277e92a4$export$965a6b1d7408d496)(docs, tree),
8221
+ exprAttrsTreeId: treeId,
8222
+ exprAttrsStatus: 'ready'
8223
+ });
8224
+ }).catch((err)=>{
8225
+ console.warn('tbrowse expression-attrs fetch failed:', err);
8226
+ if (this._exprAttrsFetchedFor === treeId) this.setState({
8227
+ exprAttrsStatus: 'error'
8228
+ });
8229
+ this._exprAttrsFetchedFor = null;
8230
+ });
8231
+ }
7686
8232
  startResize(e) {
7687
8233
  e.preventDefault();
7688
8234
  const startY = e.clientY;
@@ -7746,18 +8292,36 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
7746
8292
  }
7747
8293
  const neighborhood = this.state.neighborhoodTreeId === treeId ? this.state.neighborhood : undefined;
7748
8294
  const geneStructures = this.state.geneStructuresTreeId === treeId ? this.state.geneStructures : undefined;
8295
+ const exprAttrs = this.state.exprAttrsTreeId === treeId ? this.state.exprAttrs : undefined;
7749
8296
  // Per-zone status for the TBrowse toolbar — pulses while a fetch
7750
8297
  // is in flight, turns red on failure. Tracked per-tree so a
7751
8298
  // tree-change resets any stale 'ready'/'error' from the prior gene.
7752
8299
  const zoneStatus = {
7753
8300
  neighborhood: this.state.neighborhoodTreeId === treeId ? this.state.neighborhoodStatus : this.state.neighborhoodStatus === 'loading' ? 'loading' : undefined,
7754
- genome: this.state.geneStructuresTreeId === treeId ? this.state.geneStructuresStatus : this.state.geneStructuresStatus === 'loading' ? 'loading' : undefined
8301
+ genome: this.state.geneStructuresTreeId === treeId ? this.state.geneStructuresStatus : this.state.geneStructuresStatus === 'loading' ? 'loading' : undefined,
8302
+ expression: this.state.exprAttrsTreeId === treeId ? this.state.exprAttrsStatus : this.state.exprAttrsStatus === 'loading' ? 'loading' : undefined
7755
8303
  };
7756
8304
  // Bundle-driven (controlled) view state. If we're rendering tbrowse before
7757
8305
  // componentDidMount/Update has seeded the bundle slice, skip this turn and
7758
8306
  // let the re-render with the seeded state do the work.
7759
8307
  const tbrowseVS = this.getHomologySlice().tbrowse;
7760
8308
  if (!tbrowseVS) return null;
8309
+ // Layer the genome-limit prune set on top of the user's own prunes for the
8310
+ // controlled view state, but persist only the user's prunes (strip the
8311
+ // genome-derived ones in onViewStateChange) so the bundle stays free of
8312
+ // filter-derived state and re-applies cleanly when the filter changes.
8313
+ const genomePrunedIds = this.getGenomePrunedIds();
8314
+ const effectiveVS = genomePrunedIds.length ? {
8315
+ ...tbrowseVS,
8316
+ prunedNodeIds: (0, ($parcel$interopDefault($gXNCa$lodash))).union(tbrowseVS.prunedNodeIds || [], genomePrunedIds)
8317
+ } : tbrowseVS;
8318
+ const onViewStateChange = genomePrunedIds.length ? (next)=>{
8319
+ const genomeSet = new Set(genomePrunedIds);
8320
+ this.setTbrowseViewState({
8321
+ ...next,
8322
+ prunedNodeIds: (next.prunedNodeIds || []).filter((id)=>!genomeSet.has(id))
8323
+ });
8324
+ } : (next)=>this.setTbrowseViewState(next);
7761
8325
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
7762
8326
  children: [
7763
8327
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
@@ -7775,10 +8339,13 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
7775
8339
  exonJunctions: this._tbrowseData.exonJunctions,
7776
8340
  neighborhood: neighborhood,
7777
8341
  geneStructures: geneStructures,
8342
+ hostData: exprAttrs ? {
8343
+ exprAttrs: exprAttrs
8344
+ } : undefined,
7778
8345
  zones: this.getTbrowseZones(),
7779
8346
  nodeOfInterest: this.gene._id,
7780
- viewState: tbrowseVS,
7781
- onViewStateChange: (next)=>this.setTbrowseViewState(next),
8347
+ viewState: effectiveVS,
8348
+ onViewStateChange: onViewStateChange,
7782
8349
  defaultOpenSections: {
7783
8350
  zones: true,
7784
8351
  search: true
@@ -8558,18 +9125,20 @@ class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$
8558
9125
  this.props.doRequestGramenePathways(this.pathwayIds);
8559
9126
  }
8560
9127
  makeTaxonSpecific(docs, taxon_id) {
8561
- let lineageField = 'lineage_' + taxon_id;
8562
- if (!docs[0].hasOwnProperty(lineageField)) {
8563
- let tid = Math.floor(taxon_id / 1000);
8564
- lineageField = 'lineage_' + tid;
8565
- }
9128
+ // Each pathway doc may carry lineage under lineage_<taxon_id> or
9129
+ // lineage_<floor(taxon_id/1000)>. Resolve per-doc rather than inferring
9130
+ // from docs[0] — when docs is heterogeneous (some species-specific, some
9131
+ // generic), inferring from the first entry leaves the rest with undefined
9132
+ // lineage and crashes getHierarchy.
9133
+ const primaryField = 'lineage_' + taxon_id;
9134
+ const fallbackField = 'lineage_' + Math.floor(taxon_id / 1000);
8566
9135
  return docs.map((doc)=>{
8567
9136
  let tsDoc = (0, ($parcel$interopDefault($gXNCa$lodash))).pick(doc, [
8568
9137
  '_id',
8569
9138
  'name',
8570
9139
  'type'
8571
9140
  ]);
8572
- tsDoc.lineage = doc[lineageField];
9141
+ tsDoc.lineage = doc[primaryField] || doc[fallbackField];
8573
9142
  return tsDoc;
8574
9143
  });
8575
9144
  }
@@ -8579,6 +9148,7 @@ class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$
8579
9148
  this.pathwayIds.forEach((pwyId)=>{
8580
9149
  if (pathways[pwyId]) {
8581
9150
  const pwy = pathways[pwyId];
9151
+ if (!pwy.lineage) return; // no lineage for this species — skip
8582
9152
  pwy.lineage.forEach((line)=>{
8583
9153
  const parentOffset = line.length - 2;
8584
9154
  nodes.push({
@@ -8612,8 +9182,13 @@ class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$
8612
9182
  if (node.type === "Pathway") this.loadDiagram(this.stableId(node.id));
8613
9183
  else {
8614
9184
  const reaction = this.stableId(node.id);
8615
- const pathway = this.stableId(node.parent.split("/").pop());
8616
- this.loadDiagram(pathway, reaction);
9185
+ // node.parent is a slash-delimited ancestor path supplied by
9186
+ // react-simple-tree-menu; for a root-level Reaction with no parent
9187
+ // pathway in the tree, fall back to loading the reaction directly
9188
+ // rather than crashing on undefined.split.
9189
+ const parentKey = typeof node.parent === 'string' ? node.parent.split("/").pop() : null;
9190
+ if (parentKey) this.loadDiagram(this.stableId(parentKey), reaction);
9191
+ else this.loadDiagram(reaction);
8617
9192
  }
8618
9193
  this.setState({
8619
9194
  selectedNode: selectedNode