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 +591 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
7506
|
-
...$64fad37f770d2bfe$var$TBROWSE_BASE_ZONES
|
|
7507
|
-
|
|
7508
|
-
|
|
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:
|
|
7781
|
-
onViewStateChange:
|
|
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
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
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[
|
|
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
|
-
|
|
8616
|
-
|
|
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
|