gramene-search 2.39.1 → 2.40.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 +245 -494
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ var $gXNCa$gramenetreesclientsrctaxonomy = require("gramene-trees-client/src/tax
|
|
|
12
12
|
var $gXNCa$gramenetaxonomywithgenomes = require("gramene-taxonomy-with-genomes");
|
|
13
13
|
var $gXNCa$reactga4 = require("react-ga4");
|
|
14
14
|
var $gXNCa$moneyclip = require("money-clip");
|
|
15
|
+
var $gXNCa$tbrowsecore = require("tbrowse-core");
|
|
15
16
|
var $gXNCa$reactbootstrap = require("react-bootstrap");
|
|
16
17
|
var $gXNCa$reactswitch = require("react-switch");
|
|
17
18
|
var $gXNCa$reacticonsio5 = require("react-icons/io5");
|
|
@@ -6627,6 +6628,7 @@ var $7f865ea0feda21af$export$2e2bcd8739ae039 = $7f865ea0feda21af$var$uiViewState
|
|
|
6627
6628
|
// `viewState` is null until the view has been interacted with (or restored);
|
|
6628
6629
|
// the container falls back to its built-in default and computes initial pixel
|
|
6629
6630
|
// widths in that case.
|
|
6631
|
+
|
|
6630
6632
|
const $6048209b532f201d$var$taxTreeView = {
|
|
6631
6633
|
name: 'taxTreeView',
|
|
6632
6634
|
getReducer: ()=>{
|
|
@@ -6651,12 +6653,18 @@ const $6048209b532f201d$var$taxTreeView = {
|
|
|
6651
6653
|
type: 'TAXTREE_VIEW_SET',
|
|
6652
6654
|
payload: patch
|
|
6653
6655
|
}),
|
|
6654
|
-
// Re-apply persisted state from a saved-view snapshot.
|
|
6656
|
+
// Re-apply persisted state from a saved-view snapshot. The tbrowse ViewState
|
|
6657
|
+
// is run through tbrowse-core's tolerant deserialiser so a blob saved by an
|
|
6658
|
+
// older viewer (missing arrays / fields added since) still restores.
|
|
6655
6659
|
doApplyTaxTreeSnapshot: (snap)=>({ dispatch: dispatch })=>{
|
|
6656
6660
|
if (!snap || typeof snap !== 'object') return;
|
|
6661
|
+
const payload = snap.viewState && typeof snap.viewState === 'object' ? {
|
|
6662
|
+
...snap,
|
|
6663
|
+
viewState: (0, $gXNCa$tbrowsecore.normalizeViewState)(snap.viewState)
|
|
6664
|
+
} : snap;
|
|
6657
6665
|
dispatch({
|
|
6658
6666
|
type: 'TAXTREE_VIEW_SET',
|
|
6659
|
-
payload:
|
|
6667
|
+
payload: payload
|
|
6660
6668
|
});
|
|
6661
6669
|
},
|
|
6662
6670
|
selectTaxTreeView: (state)=>state.taxTreeView
|
|
@@ -6664,6 +6672,7 @@ const $6048209b532f201d$var$taxTreeView = {
|
|
|
6664
6672
|
var $6048209b532f201d$export$2e2bcd8739ae039 = $6048209b532f201d$var$taxTreeView;
|
|
6665
6673
|
|
|
6666
6674
|
|
|
6675
|
+
|
|
6667
6676
|
// Serializer/deserializer for shareable gene-search views.
|
|
6668
6677
|
//
|
|
6669
6678
|
// `selectViewSnapshot` produces a versioned JSON-safe blob from the current
|
|
@@ -6987,10 +6996,7 @@ function $472fe3745b238881$var$buildSnapshot(state) {
|
|
|
6987
6996
|
collapseEmpties: tv.collapseEmpties !== false,
|
|
6988
6997
|
comparaOnly: tv.comparaOnly !== false,
|
|
6989
6998
|
...hasVS ? {
|
|
6990
|
-
viewState:
|
|
6991
|
-
...tv.viewState,
|
|
6992
|
-
prunedNodeIds: []
|
|
6993
|
-
}
|
|
6999
|
+
viewState: (0, $gXNCa$tbrowsecore.stripPrunesForSnapshot)(tv.viewState)
|
|
6994
7000
|
} : {}
|
|
6995
7001
|
};
|
|
6996
7002
|
}
|
|
@@ -8593,6 +8599,7 @@ var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
|
|
|
8593
8599
|
|
|
8594
8600
|
|
|
8595
8601
|
|
|
8602
|
+
|
|
8596
8603
|
const $5c2c79352d3d7b81$export$ec6b54b688be1708 = ({ fullscreen: fullscreen, onExitFullscreen: onExitFullscreen, title: title, className: className, children: children })=>{
|
|
8597
8604
|
const [stableNode] = (0, $gXNCa$react.useState)(()=>typeof document !== 'undefined' ? document.createElement('div') : null);
|
|
8598
8605
|
const inlineRef = (0, $gXNCa$react.useRef)(null);
|
|
@@ -8776,33 +8783,28 @@ const $5c2c79352d3d7b81$export$b2e089eb3692b073 = (props)=>/*#__PURE__*/ (0, $gX
|
|
|
8776
8783
|
|
|
8777
8784
|
|
|
8778
8785
|
|
|
8786
|
+
|
|
8779
8787
|
const $cd8bc494277e92a4$var$ORGAN_CELL_W = 16;
|
|
8780
8788
|
const $cd8bc494277e92a4$var$MAXTPM_W = 46;
|
|
8781
8789
|
const $cd8bc494277e92a4$var$STRESS_MIN = 120;
|
|
8782
8790
|
function $cd8bc494277e92a4$export$965a6b1d7408d496(docs, tree) {
|
|
8783
|
-
const
|
|
8784
|
-
const nodeGene = {}; // leaf nodeId -> gene id
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
nodeGene[n.id] = n.geneId;
|
|
8789
|
-
}
|
|
8791
|
+
const hasTree = Boolean(tree && tree.nodes);
|
|
8792
|
+
const nodeGene = hasTree ? (0, $gXNCa$tbrowsecore.geneIdByLeafNodeId)(tree) : {}; // leaf nodeId -> gene id (hover readout)
|
|
8793
|
+
const byGeneId = {};
|
|
8794
|
+
(docs || []).forEach((d)=>{
|
|
8795
|
+
if (d && d.id) byGeneId[d.id] = (0, $4b8473139e48e77e$export$69fb5d19b5db1cfa)(d);
|
|
8790
8796
|
});
|
|
8797
|
+
// Docs whose gene is not a leaf of this tree are dropped (joinByLeaf's `unmatched`).
|
|
8798
|
+
const byNode = hasTree ? (0, $gXNCa$tbrowsecore.joinByLeaf)(tree, byGeneId).byNode : {};
|
|
8791
8799
|
const organSet = new Set();
|
|
8792
|
-
const byNode = {};
|
|
8793
8800
|
let tpmMin = Infinity;
|
|
8794
8801
|
let tpmMax = -Infinity;
|
|
8795
|
-
(
|
|
8796
|
-
if (!d || !d.id) return;
|
|
8797
|
-
const nodeId = nodeOf[d.id];
|
|
8798
|
-
if (!nodeId) return;
|
|
8799
|
-
const attrs = (0, $4b8473139e48e77e$export$69fb5d19b5db1cfa)(d);
|
|
8802
|
+
Object.values(byNode).forEach((attrs)=>{
|
|
8800
8803
|
Object.keys(attrs.organLevels).forEach((o)=>organSet.add(o));
|
|
8801
8804
|
if (attrs.maxTpm !== null) {
|
|
8802
8805
|
if (attrs.maxTpm < tpmMin) tpmMin = attrs.maxTpm;
|
|
8803
8806
|
if (attrs.maxTpm > tpmMax) tpmMax = attrs.maxTpm;
|
|
8804
8807
|
}
|
|
8805
|
-
byNode[nodeId] = attrs;
|
|
8806
8808
|
});
|
|
8807
8809
|
return {
|
|
8808
8810
|
organs: (0, $4b8473139e48e77e$export$65382efdcdd96f8c)(organSet),
|
|
@@ -8814,14 +8816,6 @@ function $cd8bc494277e92a4$export$965a6b1d7408d496(docs, tree) {
|
|
|
8814
8816
|
}
|
|
8815
8817
|
};
|
|
8816
8818
|
}
|
|
8817
|
-
// Shared row-highlight background, matching the other zones so hovering any zone
|
|
8818
|
-
// lights up the same row across all of them.
|
|
8819
|
-
function $cd8bc494277e92a4$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
|
|
8820
|
-
if (isSelected) return 'var(--tbrowse-row-select-bg)';
|
|
8821
|
-
if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
|
|
8822
|
-
if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
|
|
8823
|
-
return 'transparent';
|
|
8824
|
-
}
|
|
8825
8819
|
const $cd8bc494277e92a4$var$gridBorder = '1px solid var(--tbrowse-grid-line, rgba(0,0,0,0.06))';
|
|
8826
8820
|
const $cd8bc494277e92a4$var$stressWidth = (width, organCount)=>Math.max($cd8bc494277e92a4$var$STRESS_MIN, width - organCount * $cd8bc494277e92a4$var$ORGAN_CELL_W - $cd8bc494277e92a4$var$MAXTPM_W);
|
|
8827
8821
|
const $cd8bc494277e92a4$var$ExprHeader = ({ zoneState: zoneState, setZoneState: setZoneState, width: width, data: data, hoveredNodeId: hoveredNodeId })=>{
|
|
@@ -9016,8 +9010,12 @@ const $cd8bc494277e92a4$var$ExprBody = ({ visibleRows: visibleRows, rowRange: ro
|
|
|
9016
9010
|
max: 0
|
|
9017
9011
|
};
|
|
9018
9012
|
const sW = $cd8bc494277e92a4$var$stressWidth(width, organs.length);
|
|
9019
|
-
const totalHeight =
|
|
9020
|
-
const rows =
|
|
9013
|
+
const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
|
|
9014
|
+
const rows = (0, $gXNCa$tbrowse.rowsWithFlags)(visibleRows, rowRange, {
|
|
9015
|
+
hoveredNodeId: hoveredNodeId,
|
|
9016
|
+
hoveredSubtreeIds: hoveredSubtreeIds,
|
|
9017
|
+
selectedNodeId: selectedNodeId
|
|
9018
|
+
});
|
|
9021
9019
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
9022
9020
|
style: {
|
|
9023
9021
|
position: 'relative',
|
|
@@ -9025,9 +9023,8 @@ const $cd8bc494277e92a4$var$ExprBody = ({ visibleRows: visibleRows, rowRange: ro
|
|
|
9025
9023
|
height: totalHeight,
|
|
9026
9024
|
overflow: 'hidden'
|
|
9027
9025
|
},
|
|
9028
|
-
children: rows.map((r)=>{
|
|
9026
|
+
children: rows.map(({ row: r, background: background })=>{
|
|
9029
9027
|
const gene = byNode[r.nodeId];
|
|
9030
|
-
const background = $cd8bc494277e92a4$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId)));
|
|
9031
9028
|
const tpmFrac = gene ? (0, $4b8473139e48e77e$export$fc40d061e6c16645)(gene.maxTpm, tpmRange) : null;
|
|
9032
9029
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
9033
9030
|
onMouseEnter: ()=>onHoverNode(r.nodeId),
|
|
@@ -9131,17 +9128,6 @@ const $047461923b1badda$export$964d88edb00bbcaa = (suggestion)=>{
|
|
|
9131
9128
|
|
|
9132
9129
|
|
|
9133
9130
|
|
|
9134
|
-
const $64fad37f770d2bfe$var$genomeZone = (0, $gXNCa$tbrowse.createGenomeZone)({
|
|
9135
|
-
id: 'genome'
|
|
9136
|
-
});
|
|
9137
|
-
// The genome (gene-structure) zone is gated behind the site-config flag
|
|
9138
|
-
// `enable_tbrowse_genome_zone` (defaults to false) — see getTbrowseZones().
|
|
9139
|
-
const $64fad37f770d2bfe$var$TBROWSE_BASE_ZONES = [
|
|
9140
|
-
(0, $gXNCa$tbrowse.treeZone),
|
|
9141
|
-
(0, $gXNCa$tbrowse.labelsZone),
|
|
9142
|
-
(0, $gXNCa$tbrowse.msaZone),
|
|
9143
|
-
(0, $gXNCa$tbrowse.neighborhoodZone)
|
|
9144
|
-
];
|
|
9145
9131
|
// A gene's trees: the main Compara tree (homology.gene_tree) and/or trees in
|
|
9146
9132
|
// clade-specific tree sets, homology.<tree_set> — e.g. a wheat gene may carry
|
|
9147
9133
|
// gene_tree, homology.wheat = {id: 'EplWheatGT_…'} and homology.barley. Solr
|
|
@@ -9174,27 +9160,72 @@ const $64fad37f770d2bfe$var$treeLabel = (ref)=>{
|
|
|
9174
9160
|
const set = ref.field === 'gene_tree' ? 'Compara' : ref.field.charAt(0).toUpperCase() + ref.field.slice(1);
|
|
9175
9161
|
return ref.rootTaxonName ? `${set} \xb7 ${ref.rootTaxonName}` : set;
|
|
9176
9162
|
};
|
|
9163
|
+
const $64fad37f770d2bfe$var$genomeZone = (0, $gXNCa$tbrowse.createGenomeZone)({
|
|
9164
|
+
id: 'genome'
|
|
9165
|
+
});
|
|
9166
|
+
// The genome (gene-structure) zone is gated behind the site-config flag
|
|
9167
|
+
// `enable_tbrowse_genome_zone` (defaults to false) — see getTbrowseZones().
|
|
9168
|
+
const $64fad37f770d2bfe$var$TBROWSE_BASE_ZONES = [
|
|
9169
|
+
(0, $gXNCa$tbrowse.treeZone),
|
|
9170
|
+
(0, $gXNCa$tbrowse.labelsZone),
|
|
9171
|
+
(0, $gXNCa$tbrowse.msaZone),
|
|
9172
|
+
(0, $gXNCa$tbrowse.neighborhoodZone)
|
|
9173
|
+
];
|
|
9174
|
+
// The expression-attributes zone's data, as a slot on tbrowse-core's Gramene
|
|
9175
|
+
// gene-tree source: one tree-scoped /search for the expr_*__attr_* fields
|
|
9176
|
+
// (fetchGrameneExprAttrDocs), shaped by buildExprData and delivered through
|
|
9177
|
+
// hostData.exprAttrs — the same lifecycle as the built-in neighborhood and
|
|
9178
|
+
// gene-structure slots. Enabled per site via the ctx flag the component
|
|
9179
|
+
// derives from `enable_tbrowse_expression_zone`. Scoped by root.meta, which
|
|
9180
|
+
// carries the Solr field holding the tree id: gene_tree, or a clade tree set
|
|
9181
|
+
// such as wheat.
|
|
9182
|
+
const $64fad37f770d2bfe$var$exprAttrsSlot = (0, $gXNCa$tbrowsecore.defineSlot)({
|
|
9183
|
+
id: 'exprAttrs',
|
|
9184
|
+
enabled: (ctx)=>ctx.expression === true,
|
|
9185
|
+
run: async ({ ctx: ctx, fetch: fetch, signal: signal, tree: tree, root: root })=>(0, $cd8bc494277e92a4$export$965a6b1d7408d496)(await (0, $gXNCa$tbrowsecore.fetchGrameneExprAttrDocs)(ctx.base, root.meta.treeId, {
|
|
9186
|
+
treeField: root.meta.treeField,
|
|
9187
|
+
fetch: fetch,
|
|
9188
|
+
signal: signal
|
|
9189
|
+
}), tree),
|
|
9190
|
+
toHostData: (v)=>({
|
|
9191
|
+
hostData: {
|
|
9192
|
+
exprAttrs: v
|
|
9193
|
+
}
|
|
9194
|
+
})
|
|
9195
|
+
});
|
|
9196
|
+
const $64fad37f770d2bfe$var$homologySource = (0, $gXNCa$tbrowsecore.extendSource)((0, $gXNCa$tbrowsecore.grameneGeneTreeSource), {
|
|
9197
|
+
id: 'gramene-homology',
|
|
9198
|
+
slots: [
|
|
9199
|
+
$64fad37f770d2bfe$var$exprAttrsSlot
|
|
9200
|
+
]
|
|
9201
|
+
});
|
|
9202
|
+
// Host zone id → the slot(s) feeding it, so the TBrowse toolbar shows the
|
|
9203
|
+
// per-zone loading / error affordances (TBrowseProps.zoneStatus).
|
|
9204
|
+
const $64fad37f770d2bfe$var$ZONE_SLOTS = {
|
|
9205
|
+
neighborhood: [
|
|
9206
|
+
'neighborhood'
|
|
9207
|
+
],
|
|
9208
|
+
genome: [
|
|
9209
|
+
'geneStructures'
|
|
9210
|
+
],
|
|
9211
|
+
expression: [
|
|
9212
|
+
'exprAttrs'
|
|
9213
|
+
]
|
|
9214
|
+
};
|
|
9177
9215
|
class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
|
|
9178
9216
|
constructor(props){
|
|
9179
9217
|
super(props);
|
|
9180
|
-
//
|
|
9218
|
+
// The tbrowse data — tree, MSA, domains and junctions adapted from the
|
|
9219
|
+
// stored genetree doc, then the neighborhood / gene-structure / expression
|
|
9220
|
+
// slots — is loaded by a tbrowse-core loader kept on the instance (one per
|
|
9221
|
+
// mount); its snapshot is mirrored into state so renders follow it. The
|
|
9181
9222
|
// user-controlled view state (viewer toggle, resize height, tbrowse
|
|
9182
9223
|
// ViewState) is lifted into the uiViewState bundle, keyed by geneId.
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
// prop. They start `undefined` (no signal); flipped to 'loading'
|
|
9187
|
-
// when the fetch kicks off and 'ready' / 'error' on settle.
|
|
9224
|
+
this.loader = (0, $gXNCa$tbrowsecore.createLoader)($64fad37f770d2bfe$var$homologySource, {
|
|
9225
|
+
zoneSlots: $64fad37f770d2bfe$var$ZONE_SLOTS
|
|
9226
|
+
});
|
|
9188
9227
|
this.state = {
|
|
9189
|
-
|
|
9190
|
-
neighborhoodTreeId: null,
|
|
9191
|
-
neighborhoodStatus: undefined,
|
|
9192
|
-
geneStructures: null,
|
|
9193
|
-
geneStructuresTreeId: null,
|
|
9194
|
-
geneStructuresStatus: undefined,
|
|
9195
|
-
exprAttrs: null,
|
|
9196
|
-
exprAttrsTreeId: null,
|
|
9197
|
-
exprAttrsStatus: undefined
|
|
9228
|
+
snap: this.loader.getSnapshot()
|
|
9198
9229
|
};
|
|
9199
9230
|
if (!props.geneDocs.hasOwnProperty(props.searchResult.id)) props.requestGene(props.searchResult.id);
|
|
9200
9231
|
this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(props.grameneTaxonomy));
|
|
@@ -9264,248 +9295,95 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
9264
9295
|
// When the user has limited the search to a subset of genomes
|
|
9265
9296
|
// (grameneGenomes.active), prune the gene tree to genes from those genomes
|
|
9266
9297
|
// — mirroring the taxon_id filter api.js applies to the search itself, and
|
|
9267
|
-
// the genomesOfInterest TreeVis already honours.
|
|
9268
|
-
//
|
|
9269
|
-
//
|
|
9270
|
-
//
|
|
9271
|
-
//
|
|
9272
|
-
//
|
|
9298
|
+
// the genomesOfInterest TreeVis already honours. tbrowse-core's
|
|
9299
|
+
// maximalExcludedClades returns the ids of the highest nodes whose entire
|
|
9300
|
+
// subtree lies outside the active set (maximal excluded clades), rather
|
|
9301
|
+
// than individual leaves: tbrowse then sheds one stub per clade instead of
|
|
9302
|
+
// one per leaf, and its single-marker count is meaningful. The gene of
|
|
9303
|
+
// interest is always kept so the tree never collapses to nothing. Empty
|
|
9304
|
+
// active set means "no limit" → full tree.
|
|
9273
9305
|
getGenomePrunedIds() {
|
|
9274
9306
|
const active = this.props.grameneGenomes && this.props.grameneGenomes.active || {};
|
|
9275
9307
|
const activeIds = Object.keys(active);
|
|
9276
9308
|
if (activeIds.length === 0) return [];
|
|
9277
|
-
|
|
9309
|
+
const snap = this.state.snap;
|
|
9310
|
+
if (!snap.tree) return [];
|
|
9278
9311
|
const activeSet = new Set(activeIds.map(String));
|
|
9279
9312
|
const keepGeneId = this.gene && this.gene._id;
|
|
9280
|
-
|
|
9281
|
-
const children = {};
|
|
9282
|
-
Object.values(nodes).forEach((n)=>{
|
|
9283
|
-
if (n.parentId != null) (children[n.parentId] = children[n.parentId] || []).push(n.id);
|
|
9284
|
-
});
|
|
9285
|
-
// hasKept(node): does the subtree contain a leaf we must keep (its taxon
|
|
9286
|
-
// is active, or it's the gene of interest)? Memoised post-order walk.
|
|
9287
|
-
const memo = {};
|
|
9288
|
-
const hasKept = (id)=>{
|
|
9289
|
-
if (id in memo) return memo[id];
|
|
9290
|
-
const n = nodes[id];
|
|
9291
|
-
const v = n.isLeaf ? n.taxonomyId != null && activeSet.has(String(n.taxonomyId)) || n.geneId === keepGeneId : (children[id] || []).some(hasKept);
|
|
9292
|
-
memo[id] = v;
|
|
9293
|
-
return v;
|
|
9294
|
-
};
|
|
9295
|
-
// A node is a maximal excluded clade when it keeps nothing but its parent
|
|
9296
|
-
// does (or it's the root) — i.e. the highest fully-excluded node on its
|
|
9297
|
-
// path. The root always keeps the gene of interest, so it is never pruned.
|
|
9298
|
-
return Object.values(nodes).filter((n)=>!hasKept(n.id) && (n.parentId == null || hasKept(n.parentId))).map((n)=>n.id);
|
|
9313
|
+
return (0, $gXNCa$tbrowsecore.maximalExcludedClades)(snap.tree, (leaf)=>leaf.taxonomyId != null && activeSet.has(String(leaf.taxonomyId)) || leaf.geneId === keepGeneId);
|
|
9299
9314
|
}
|
|
9300
9315
|
// Seed the bundle with a pivot-computed initial tbrowse view state once the
|
|
9301
|
-
//
|
|
9302
|
-
// viewer
|
|
9316
|
+
// loader has the tree and the user is actually looking at the tbrowse
|
|
9317
|
+
// viewer, from lifecycle (not render) to avoid dispatching mid-render. The
|
|
9318
|
+
// dispatch lands synchronously in the per-gene slice, so the slice check is
|
|
9319
|
+
// the dedupe. buildInitialViewState is the same routine the chassis runs in
|
|
9320
|
+
// uncontrolled mode (each zone's fr-share, initial visibility, default
|
|
9321
|
+
// per-zone state); the pivot is computed for THIS gene — the loader is
|
|
9322
|
+
// shared by every gene of the tree, so its own pivot may be another gene's.
|
|
9303
9323
|
maybeSeedTbrowseViewState() {
|
|
9304
9324
|
if (this.getViewer() !== 'tbrowse') return;
|
|
9305
9325
|
if (this.getHomologySlice().tbrowse) return;
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
const
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
if (!raw || !raw.taxon_id) return;
|
|
9314
|
-
const adapted = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
|
|
9315
|
-
raw
|
|
9316
|
-
]);
|
|
9317
|
-
const pivot = (0, $gXNCa$tbrowse.computePivotState)(adapted.tree, gene._id);
|
|
9318
|
-
this.setTbrowseViewState({
|
|
9319
|
-
selectedNodeId: null,
|
|
9320
|
-
collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
|
|
9321
|
-
prunedNodeIds: [],
|
|
9322
|
-
swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
|
|
9323
|
-
compressedNodeIds: [],
|
|
9324
|
-
nodeOfInterestId: pivot ? pivot.targetId : null,
|
|
9325
|
-
// Use each zone's intended fr-share + initial visibility from its
|
|
9326
|
-
// definition (tree:30 / labels:20 / msa:50 / neighborhood:30 /
|
|
9327
|
-
// genome:32) instead of uniform 25 across the board. Gives the MSA
|
|
9328
|
-
// a wider initial pane and the tree a narrower one.
|
|
9329
|
-
//
|
|
9330
|
-
// `defaultVisible ?? true` matches tbrowse's own buildInitialViewState
|
|
9331
|
-
// logic — zones default to visible unless their definition opts out
|
|
9332
|
-
// (e.g. neighborhood and genome opt out so they only appear once
|
|
9333
|
-
// their async data lands; tbrowse's Layout auto-flips them on then).
|
|
9334
|
-
zones: this.getTbrowseZones().map((z)=>({
|
|
9335
|
-
id: z.id,
|
|
9336
|
-
width: z.defaultWidth,
|
|
9337
|
-
visible: z.defaultVisible ?? true
|
|
9338
|
-
})),
|
|
9339
|
-
zoneStates: {},
|
|
9340
|
-
search: null
|
|
9341
|
-
});
|
|
9326
|
+
if (!this.gene) return;
|
|
9327
|
+
// The live snapshot, not the mirrored one: right after a tree switch the
|
|
9328
|
+
// loader has already restarted but state.snap still holds the old tree.
|
|
9329
|
+
const snap = this.loader.getSnapshot();
|
|
9330
|
+
const ref = this.selectedTreeRef(this.gene);
|
|
9331
|
+
if (!ref || snap.status !== 'ready' || !snap.tree || !snap.root || snap.root.meta.treeId !== ref.id) return;
|
|
9332
|
+
this.setTbrowseViewState((0, $gXNCa$tbrowsecore.applyPivot)((0, $gXNCa$tbrowsecore.buildInitialViewState)(this.getTbrowseZones()), (0, $gXNCa$tbrowsecore.computePivotState)(snap.tree, this.gene._id)));
|
|
9342
9333
|
}
|
|
9343
9334
|
componentDidMount() {
|
|
9335
|
+
this._unsubscribe = this.loader.subscribe(()=>this.setState({
|
|
9336
|
+
snap: this.loader.getSnapshot()
|
|
9337
|
+
}));
|
|
9338
|
+
this.maybeStartLoader();
|
|
9344
9339
|
this.maybeSeedTbrowseViewState();
|
|
9345
|
-
this.maybeFetchTbrowseData();
|
|
9346
9340
|
}
|
|
9347
9341
|
componentDidUpdate() {
|
|
9342
|
+
this.maybeStartLoader();
|
|
9348
9343
|
this.maybeSeedTbrowseViewState();
|
|
9349
|
-
this.maybeFetchTbrowseData();
|
|
9350
9344
|
}
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
//
|
|
9356
|
-
|
|
9345
|
+
componentWillUnmount() {
|
|
9346
|
+
if (this._unsubscribe) this._unsubscribe();
|
|
9347
|
+
this.loader.dispose();
|
|
9348
|
+
}
|
|
9349
|
+
// Start the loader (or restart it when the gene's tree changes) from
|
|
9350
|
+
// lifecycle, not render. Gated on the user actually being on the tbrowse
|
|
9351
|
+
// viewer + the tree doc being loaded — otherwise we'd pay network cost for
|
|
9352
|
+
// genes the user never looks at. The stored genetree doc is handed over as
|
|
9353
|
+
// `rawGenetree`, so the source makes no /genes or /genetrees request of its
|
|
9354
|
+
// own (docs.js's taxon-name rewrite stays upstream); the neighborhood,
|
|
9355
|
+
// gene-structure and expression slots are the only fetches, and the loader
|
|
9356
|
+
// retries their transient failures with backoff.
|
|
9357
|
+
maybeStartLoader() {
|
|
9357
9358
|
if (this.getViewer() !== 'tbrowse') return;
|
|
9358
9359
|
const id = this.getGeneId();
|
|
9359
9360
|
if (!this.props.geneDocs.hasOwnProperty(id)) return;
|
|
9360
9361
|
const gene = this.props.geneDocs[id];
|
|
9362
|
+
// The selected tree: gene_tree, or a clade tree set (homology.wheat,
|
|
9363
|
+
// homology.oat, …); none for genes with homologues but no tree. A switch
|
|
9364
|
+
// restarts the loader (new generation, the old slots are aborted).
|
|
9361
9365
|
const ref = this.selectedTreeRef(gene);
|
|
9362
9366
|
if (!ref) return;
|
|
9363
9367
|
const treeId = ref.id;
|
|
9364
9368
|
const raw = this.props.grameneTrees[treeId];
|
|
9365
9369
|
if (!raw || !raw.taxon_id) return;
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
this.
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
this._neighborhoodFetchedFor = treeId;
|
|
9384
|
-
this.setState({
|
|
9385
|
-
neighborhoodStatus: 'loading'
|
|
9386
|
-
});
|
|
9387
|
-
const api = this.props.grameneAPI;
|
|
9388
|
-
const url = new URL(`${api}/search`);
|
|
9389
|
-
const fl = 'id,name,gene_tree,gene_idx,region,start,end,strand,biotype,system_name,description';
|
|
9390
|
-
url.searchParams.set('fl', field === 'gene_tree' ? fl : `${fl},${field}`);
|
|
9391
|
-
// cache=false: as a cached fq the graph query caches its seed inside its own
|
|
9392
|
-
// filterCache computeIfAbsent, and Solr's FastLRUCache throws "Recursive
|
|
9393
|
-
// update" (HTTP 500) whenever the two keys collide — for some trees, until
|
|
9394
|
-
// the seed happens to be cached by another request.
|
|
9395
|
-
url.searchParams.set('fq', `{!graph from=compara_neighbors_10 to=compara_idx_multi maxDepth=1 cache=false}${field}:${treeId}`);
|
|
9396
|
-
url.searchParams.set('rows', '100000');
|
|
9397
|
-
url.searchParams.set('start', '0');
|
|
9398
|
-
fetch(url.toString(), {
|
|
9399
|
-
headers: {
|
|
9400
|
-
Accept: 'application/json'
|
|
9401
|
-
}
|
|
9402
|
-
}).then((res)=>{
|
|
9403
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
9404
|
-
return res.json();
|
|
9405
|
-
}).then((json)=>{
|
|
9406
|
-
if (this._neighborhoodFetchedFor !== treeId) return;
|
|
9407
|
-
// The neighborhood zone colours genes by family from doc.gene_tree; in
|
|
9408
|
-
// a clade tree set the family id lives under the set's own field.
|
|
9409
|
-
if (field !== 'gene_tree' && json && json.response && Array.isArray(json.response.docs)) json.response.docs.forEach((d)=>{
|
|
9410
|
-
if (d[field] && !d.gene_tree) d.gene_tree = d[field];
|
|
9411
|
-
});
|
|
9412
|
-
this.setState({
|
|
9413
|
-
neighborhood: (0, $gXNCa$tbrowse.fromGrameneNeighborhood)(json),
|
|
9414
|
-
neighborhoodTreeId: treeId,
|
|
9415
|
-
neighborhoodStatus: 'ready'
|
|
9416
|
-
});
|
|
9417
|
-
}).catch((err)=>{
|
|
9418
|
-
console.warn('tbrowse neighborhood fetch failed:', err);
|
|
9419
|
-
// Surface the failure via the zoneStatus prop. Reset the
|
|
9420
|
-
// dedupe key so a re-mount or tree-change can retry.
|
|
9421
|
-
if (this._neighborhoodFetchedFor === treeId) this.setState({
|
|
9422
|
-
neighborhoodStatus: 'error'
|
|
9423
|
-
});
|
|
9424
|
-
this._neighborhoodFetchedFor = null;
|
|
9425
|
-
});
|
|
9426
|
-
}
|
|
9427
|
-
fetchGeneStructures(treeId, tree) {
|
|
9428
|
-
if (this._geneStructuresFetchedFor === treeId) return;
|
|
9429
|
-
this._geneStructuresFetchedFor = treeId;
|
|
9430
|
-
const ids = Object.values(tree.nodes).filter((n)=>n.isLeaf && n.geneId).map((n)=>n.geneId);
|
|
9431
|
-
if (ids.length === 0) return;
|
|
9432
|
-
this.setState({
|
|
9433
|
-
geneStructuresStatus: 'loading'
|
|
9434
|
-
});
|
|
9435
|
-
const api = this.props.grameneAPI;
|
|
9436
|
-
const BATCH_SIZE = 50;
|
|
9437
|
-
const batches = [];
|
|
9438
|
-
for(let i = 0; i < ids.length; i += BATCH_SIZE)batches.push(ids.slice(i, i + BATCH_SIZE));
|
|
9439
|
-
const fetchBatch = (batch)=>{
|
|
9440
|
-
const url = new URL(`${api}/genes`);
|
|
9441
|
-
url.searchParams.set('idList', batch.join(','));
|
|
9442
|
-
url.searchParams.set('rows', '-1');
|
|
9443
|
-
return fetch(url.toString(), {
|
|
9444
|
-
headers: {
|
|
9445
|
-
Accept: 'application/json'
|
|
9446
|
-
}
|
|
9447
|
-
}).then((res)=>{
|
|
9448
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
9449
|
-
return res.json();
|
|
9450
|
-
});
|
|
9451
|
-
};
|
|
9452
|
-
Promise.all(batches.map(fetchBatch)).then((results)=>{
|
|
9453
|
-
if (this._geneStructuresFetchedFor !== treeId) return;
|
|
9454
|
-
const combined = [].concat(...results.map((r)=>Array.isArray(r) ? r : []));
|
|
9455
|
-
this.setState({
|
|
9456
|
-
geneStructures: (0, $gXNCa$tbrowse.fromGrameneGeneStructures)(combined),
|
|
9457
|
-
geneStructuresTreeId: treeId,
|
|
9458
|
-
geneStructuresStatus: 'ready'
|
|
9459
|
-
});
|
|
9460
|
-
}).catch((err)=>{
|
|
9461
|
-
console.warn('tbrowse gene-structures fetch failed:', err);
|
|
9462
|
-
if (this._geneStructuresFetchedFor === treeId) this.setState({
|
|
9463
|
-
geneStructuresStatus: 'error'
|
|
9464
|
-
});
|
|
9465
|
-
this._geneStructuresFetchedFor = null;
|
|
9466
|
-
});
|
|
9467
|
-
}
|
|
9468
|
-
// Fetch the expression attributes (expr_*__attr_*) for this tree's genes in a
|
|
9469
|
-
// single query: scope by the tree's field and the `expression_attributes` capability
|
|
9470
|
-
// so only genes that actually have the data come back (rows high enough to
|
|
9471
|
-
// cover the whole tree). Uses /search (not /genes, whose trimmed docs omit the
|
|
9472
|
-
// attr fields) with an fl glob so the columns are discovered from the data
|
|
9473
|
-
// rather than hard-coded. `tree` is still needed to map gene ids to leaf node
|
|
9474
|
-
// ids (see buildExprByNode).
|
|
9475
|
-
fetchExprAttrs(treeId, tree, field = 'gene_tree') {
|
|
9476
|
-
if (this._exprAttrsFetchedFor === treeId) return;
|
|
9477
|
-
this._exprAttrsFetchedFor = treeId;
|
|
9478
|
-
this.setState({
|
|
9479
|
-
exprAttrsStatus: 'loading'
|
|
9480
|
-
});
|
|
9481
|
-
const api = this.props.grameneAPI;
|
|
9482
|
-
const url = new URL(`${api}/search`);
|
|
9483
|
-
url.searchParams.set('q', '*:*');
|
|
9484
|
-
url.searchParams.append('fq', `${field}:${treeId}`);
|
|
9485
|
-
url.searchParams.append('fq', 'capabilities:expression_attributes');
|
|
9486
|
-
url.searchParams.set('fl', 'id,expr_*__attr_*');
|
|
9487
|
-
url.searchParams.set('rows', '10000');
|
|
9488
|
-
fetch(url.toString(), {
|
|
9489
|
-
headers: {
|
|
9490
|
-
Accept: 'application/json'
|
|
9491
|
-
}
|
|
9492
|
-
}).then((res)=>{
|
|
9493
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
9494
|
-
return res.json();
|
|
9495
|
-
}).then((json)=>{
|
|
9496
|
-
if (this._exprAttrsFetchedFor !== treeId) return;
|
|
9497
|
-
const docs = json && json.response && Array.isArray(json.response.docs) ? json.response.docs : [];
|
|
9498
|
-
this.setState({
|
|
9499
|
-
exprAttrs: (0, $cd8bc494277e92a4$export$965a6b1d7408d496)(docs, tree),
|
|
9500
|
-
exprAttrsTreeId: treeId,
|
|
9501
|
-
exprAttrsStatus: 'ready'
|
|
9502
|
-
});
|
|
9503
|
-
}).catch((err)=>{
|
|
9504
|
-
console.warn('tbrowse expression-attrs fetch failed:', err);
|
|
9505
|
-
if (this._exprAttrsFetchedFor === treeId) this.setState({
|
|
9506
|
-
exprAttrsStatus: 'error'
|
|
9507
|
-
});
|
|
9508
|
-
this._exprAttrsFetchedFor = null;
|
|
9370
|
+
const key = `${ref.field}:${treeId}`;
|
|
9371
|
+
if (this._loaderStartedFor === key) return;
|
|
9372
|
+
this._loaderStartedFor = key;
|
|
9373
|
+
this.loader.start({
|
|
9374
|
+
base: this.props.grameneAPI,
|
|
9375
|
+
treeId: treeId,
|
|
9376
|
+
treeField: ref.field,
|
|
9377
|
+
geneId: gene._id,
|
|
9378
|
+
rawGenetree: raw,
|
|
9379
|
+
// Only pay for gene-structure data when the genome zone is enabled; the
|
|
9380
|
+
// request shape (50 ids per /genes call, no fl) is what this tab has
|
|
9381
|
+
// always sent.
|
|
9382
|
+
geneStructures: this.isGenomeZoneEnabled(),
|
|
9383
|
+
geneStructureChunkSize: 50,
|
|
9384
|
+
geneStructureFl: null,
|
|
9385
|
+
// Same for the per-gene expression attributes zone.
|
|
9386
|
+
expression: this.isExpressionZoneEnabled()
|
|
9509
9387
|
});
|
|
9510
9388
|
}
|
|
9511
9389
|
startResize(e) {
|
|
@@ -9558,49 +9436,30 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
9558
9436
|
});
|
|
9559
9437
|
}
|
|
9560
9438
|
renderTBrowse() {
|
|
9561
|
-
const
|
|
9562
|
-
|
|
9563
|
-
//
|
|
9564
|
-
// the
|
|
9565
|
-
//
|
|
9566
|
-
if (this._tbrowseTreeId !== treeId) {
|
|
9567
|
-
this._tbrowseTreeId = treeId;
|
|
9568
|
-
this._tbrowseData = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
|
|
9569
|
-
this.props.grameneTrees[treeId]
|
|
9570
|
-
]);
|
|
9571
|
-
}
|
|
9572
|
-
const neighborhood = this.state.neighborhoodTreeId === treeId ? this.state.neighborhood : undefined;
|
|
9573
|
-
const geneStructures = this.state.geneStructuresTreeId === treeId ? this.state.geneStructures : undefined;
|
|
9574
|
-
const exprAttrs = this.state.exprAttrsTreeId === treeId ? this.state.exprAttrs : undefined;
|
|
9575
|
-
// Per-zone status for the TBrowse toolbar — pulses while a fetch
|
|
9576
|
-
// is in flight, turns red on failure. Tracked per-tree so a
|
|
9577
|
-
// tree-change resets any stale 'ready'/'error' from the prior gene.
|
|
9578
|
-
const zoneStatus = {
|
|
9579
|
-
neighborhood: this.state.neighborhoodTreeId === treeId ? this.state.neighborhoodStatus : this.state.neighborhoodStatus === 'loading' ? 'loading' : undefined,
|
|
9580
|
-
genome: this.state.geneStructuresTreeId === treeId ? this.state.geneStructuresStatus : this.state.geneStructuresStatus === 'loading' ? 'loading' : undefined,
|
|
9581
|
-
expression: this.state.exprAttrsTreeId === treeId ? this.state.exprAttrsStatus : this.state.exprAttrsStatus === 'loading' ? 'loading' : undefined
|
|
9582
|
-
};
|
|
9583
|
-
// Bundle-driven (controlled) view state. If we're rendering tbrowse before
|
|
9584
|
-
// componentDidMount/Update has seeded the bundle slice, skip this turn and
|
|
9585
|
-
// let the re-render with the seeded state do the work.
|
|
9439
|
+
const snap = this.state.snap;
|
|
9440
|
+
const hostProps = (0, $gXNCa$tbrowsecore.hostPropsFrom)(snap);
|
|
9441
|
+
// Nothing to draw until the loader has adapted the tree (one tick after
|
|
9442
|
+
// start(), since the stored doc needs no fetch) and the bundle slice has
|
|
9443
|
+
// been seeded by maybeSeedTbrowseViewState (lifecycle, not render).
|
|
9586
9444
|
const tbrowseVS = this.getHomologySlice().tbrowse;
|
|
9587
|
-
|
|
9445
|
+
// Also wait until the snapshot holds the SELECTED tree, so a switch never
|
|
9446
|
+
// draws the previous tree under the new selection.
|
|
9447
|
+
const isSelected = snap.root && this.treeRef && snap.root.meta.treeId === this.treeRef.id;
|
|
9448
|
+
if (!hostProps || !tbrowseVS || !isSelected) // Reserve the pane's height so the card doesn't jump when the tree lands.
|
|
9449
|
+
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
9450
|
+
className: "gene-genetree",
|
|
9451
|
+
style: {
|
|
9452
|
+
height: this.getHeight(),
|
|
9453
|
+
width: '100%'
|
|
9454
|
+
},
|
|
9455
|
+
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Spinner), {})
|
|
9456
|
+
});
|
|
9588
9457
|
// Layer the genome-limit prune set on top of the user's own prunes for the
|
|
9589
|
-
// controlled view state, but persist only the user's prunes (
|
|
9590
|
-
// genome-derived ones in onViewStateChange) so the bundle stays
|
|
9591
|
-
// filter-derived state and re-applies cleanly when the filter
|
|
9592
|
-
|
|
9593
|
-
const
|
|
9594
|
-
...tbrowseVS,
|
|
9595
|
-
prunedNodeIds: (0, ($parcel$interopDefault($gXNCa$lodash))).union(tbrowseVS.prunedNodeIds || [], genomePrunedIds)
|
|
9596
|
-
} : tbrowseVS;
|
|
9597
|
-
const onViewStateChange = genomePrunedIds.length ? (next)=>{
|
|
9598
|
-
const genomeSet = new Set(genomePrunedIds);
|
|
9599
|
-
this.setTbrowseViewState({
|
|
9600
|
-
...next,
|
|
9601
|
-
prunedNodeIds: (next.prunedNodeIds || []).filter((id)=>!genomeSet.has(id))
|
|
9602
|
-
});
|
|
9603
|
-
} : (next)=>this.setTbrowseViewState(next);
|
|
9458
|
+
// controlled view state, but persist only the user's prunes (the layer
|
|
9459
|
+
// strips the genome-derived ones in onViewStateChange) so the bundle stays
|
|
9460
|
+
// free of filter-derived state and re-applies cleanly when the filter
|
|
9461
|
+
// changes.
|
|
9462
|
+
const pruneLayer = (0, $gXNCa$tbrowsecore.createDerivedPruneLayer)(this.getGenomePrunedIds());
|
|
9604
9463
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
|
|
9605
9464
|
children: [
|
|
9606
9465
|
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
@@ -9610,27 +9469,16 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
9610
9469
|
width: '100%'
|
|
9611
9470
|
},
|
|
9612
9471
|
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$tbrowse.TBrowse), {
|
|
9613
|
-
|
|
9614
|
-
taxonomy: this._tbrowseData.taxonomy,
|
|
9615
|
-
msa: this._tbrowseData.msa,
|
|
9616
|
-
geneMetadata: this._tbrowseData.geneMetadata,
|
|
9617
|
-
proteinDomains: this._tbrowseData.proteinDomains,
|
|
9618
|
-
exonJunctions: this._tbrowseData.exonJunctions,
|
|
9619
|
-
neighborhood: neighborhood,
|
|
9620
|
-
geneStructures: geneStructures,
|
|
9621
|
-
hostData: exprAttrs ? {
|
|
9622
|
-
exprAttrs: exprAttrs
|
|
9623
|
-
} : undefined,
|
|
9472
|
+
...hostProps,
|
|
9624
9473
|
zones: this.getTbrowseZones(),
|
|
9625
9474
|
nodeOfInterest: this.gene._id,
|
|
9626
|
-
viewState:
|
|
9627
|
-
onViewStateChange:
|
|
9475
|
+
viewState: pruneLayer.effective(tbrowseVS),
|
|
9476
|
+
onViewStateChange: (next)=>this.setTbrowseViewState(pruneLayer.onChange(next)),
|
|
9628
9477
|
densityControls: true,
|
|
9629
9478
|
defaultOpenSections: {
|
|
9630
9479
|
zones: true,
|
|
9631
9480
|
search: true
|
|
9632
|
-
}
|
|
9633
|
-
zoneStatus: zoneStatus
|
|
9481
|
+
}
|
|
9634
9482
|
})
|
|
9635
9483
|
}),
|
|
9636
9484
|
this.renderResizeHandle()
|
|
@@ -9862,7 +9710,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
9862
9710
|
});
|
|
9863
9711
|
}
|
|
9864
9712
|
}
|
|
9865
|
-
var $64fad37f770d2bfe$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneTaxonomy', 'selectGrameneTrees', 'selectGrameneGenomes', 'selectGrameneAPI', 'selectConfiguration', 'selectCuration', 'selectUiViewState', 'doRequestGrameneTree', 'doSetHomologyViewer', '
|
|
9713
|
+
var $64fad37f770d2bfe$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneTaxonomy', 'selectGrameneTrees', 'selectGrameneGenomes', 'selectGrameneAPI', 'selectConfiguration', 'selectCuration', 'selectUiViewState', 'doRequestGrameneTree', 'doSetHomologyViewer', 'doSetHomologyHeight', 'doSetHomologyTbrowseViewState', 'doSetHomologyTree', 'doAcceptGrameneSuggestion', 'doReplaceGrameneFilters', $64fad37f770d2bfe$var$Homology);
|
|
9866
9714
|
|
|
9867
9715
|
|
|
9868
9716
|
|
|
@@ -20522,69 +20370,21 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
|
|
|
20522
20370
|
|
|
20523
20371
|
|
|
20524
20372
|
|
|
20373
|
+
|
|
20525
20374
|
// Adapter: gramene taxonomy-with-genomes tree → tbrowse `Tree` + `Taxonomy`.
|
|
20526
20375
|
//
|
|
20527
20376
|
// `grameneTaxDist` (from the grameneTaxDist bundle's selectGrameneTaxDist) is
|
|
20528
|
-
// the root node of a gramene-taxonomy-with-genomes tree.
|
|
20529
|
-
//
|
|
20530
|
-
//
|
|
20531
|
-
//
|
|
20532
|
-
// tbrowse
|
|
20533
|
-
|
|
20534
|
-
|
|
20535
|
-
// NodeIds off the (unique) NCBI taxon id and set `taxonomyId` to the same id.
|
|
20536
|
-
//
|
|
20537
|
-
// Branch lengths are uniform (taxonomy carries none); the tree zone is driven
|
|
20538
|
-
// in 'cladogram' mode for this view, which ignores `distance` and aligns the
|
|
20539
|
-
// genome rows at the right edge regardless.
|
|
20540
|
-
/**
|
|
20541
|
-
* @param {object} taxDist - root node of the gramene taxDist tree
|
|
20542
|
-
* (the object returned by selectGrameneTaxDist).
|
|
20543
|
-
* @returns {{ tree: {rootId: string, nodes: object}, taxonomy: object,
|
|
20544
|
-
* leafTaxonIds: string[] } | null}
|
|
20545
|
-
*/ function $dc4f9c952681c07f$export$5fb520c9ccc6bdef(taxDist) {
|
|
20377
|
+
// the root node of a gramene-taxonomy-with-genomes (tree-model) tree. The
|
|
20378
|
+
// conversion itself — node ids = String(NCBI taxon id), uniform branch lengths
|
|
20379
|
+
// (the tree zone runs in 'cladogram' mode for this view), `model.name` exposed
|
|
20380
|
+
// as both scientific and common name, duplicate-id guard — is
|
|
20381
|
+
// tbrowse-core's `fromTreeModel`, which was lifted from this file.
|
|
20382
|
+
|
|
20383
|
+
function $dc4f9c952681c07f$export$5fb520c9ccc6bdef(taxDist) {
|
|
20546
20384
|
if (!taxDist || !taxDist.model) return null;
|
|
20547
|
-
|
|
20548
|
-
|
|
20549
|
-
|
|
20550
|
-
const rootId = String(taxDist.model.id);
|
|
20551
|
-
const walk = (node, parentId)=>{
|
|
20552
|
-
if (!node || !node.model) return;
|
|
20553
|
-
const id = String(node.model.id);
|
|
20554
|
-
// Guard against the (shouldn't-happen) cyclic / duplicate id case so a
|
|
20555
|
-
// malformed taxonomy can't spin the recursion.
|
|
20556
|
-
if (nodes[id]) return;
|
|
20557
|
-
const kids = node.children || [];
|
|
20558
|
-
const isLeaf = kids.length === 0;
|
|
20559
|
-
nodes[id] = {
|
|
20560
|
-
id: id,
|
|
20561
|
-
parentId: parentId,
|
|
20562
|
-
distance: parentId === null ? 0 : 1,
|
|
20563
|
-
isLeaf: isLeaf,
|
|
20564
|
-
taxonomyId: id
|
|
20565
|
-
};
|
|
20566
|
-
// `model.name` is the NCBI scientific name for clades and is overwritten
|
|
20567
|
-
// with the genome's display_name for genome leaves upstream in
|
|
20568
|
-
// selectGrameneTaxDist. Expose it as both names so the labels zone shows
|
|
20569
|
-
// something whichever field the user has active.
|
|
20570
|
-
const name = node.model.name;
|
|
20571
|
-
taxonomy[id] = {
|
|
20572
|
-
scientificName: name,
|
|
20573
|
-
commonName: name,
|
|
20574
|
-
rank: node.model.rank
|
|
20575
|
-
};
|
|
20576
|
-
if (isLeaf) leafTaxonIds.push(id);
|
|
20577
|
-
for (const k of kids)walk(k, id);
|
|
20578
|
-
};
|
|
20579
|
-
walk(taxDist, null);
|
|
20580
|
-
return {
|
|
20581
|
-
tree: {
|
|
20582
|
-
rootId: rootId,
|
|
20583
|
-
nodes: nodes
|
|
20584
|
-
},
|
|
20585
|
-
taxonomy: taxonomy,
|
|
20586
|
-
leafTaxonIds: leafTaxonIds
|
|
20587
|
-
};
|
|
20385
|
+
return (0, $gXNCa$tbrowsecore.fromTreeModel)(taxDist, {
|
|
20386
|
+
taxonomyIdAs: 'string'
|
|
20387
|
+
});
|
|
20588
20388
|
}
|
|
20589
20389
|
|
|
20590
20390
|
|
|
@@ -20611,6 +20411,7 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
|
|
|
20611
20411
|
|
|
20612
20412
|
|
|
20613
20413
|
|
|
20414
|
+
|
|
20614
20415
|
// ── colour model (ported from gramene-search-vis util/colors.js) ──────────
|
|
20615
20416
|
// schemeCategory10, kept as RGB triples so we need no d3 dependency here.
|
|
20616
20417
|
const $bfd29d54a0f0e853$var$REGION_RGB = [
|
|
@@ -21138,12 +20939,6 @@ const $bfd29d54a0f0e853$var$BinsHeader = ({ data: data, zoneState: zoneState, se
|
|
|
21138
20939
|
// lets the row-highlight background (rendered in the layer beneath the canvas)
|
|
21139
20940
|
// show as bands above/below the strip on hover/select.
|
|
21140
20941
|
const $bfd29d54a0f0e853$var$ROW_PAD_Y = 2;
|
|
21141
|
-
function $bfd29d54a0f0e853$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
|
|
21142
|
-
if (isSelected) return 'var(--tbrowse-row-select-bg)';
|
|
21143
|
-
if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
|
|
21144
|
-
if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
|
|
21145
|
-
return 'transparent';
|
|
21146
|
-
}
|
|
21147
20942
|
const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
21148
20943
|
const { visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, zoneState: zoneState, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode } = props;
|
|
21149
20944
|
const canvasRef = (0, $gXNCa$react.useRef)(null);
|
|
@@ -21157,7 +20952,7 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
21157
20952
|
const snap = (0, $gXNCa$react.useSyncExternalStore)(store ? store.subscribe : $bfd29d54a0f0e853$var$NOOP_SUB, store ? store.getState : $bfd29d54a0f0e853$var$NOOP_GET);
|
|
21158
20953
|
const transform = snap.transform || $bfd29d54a0f0e853$var$DEFAULT_TRANSFORM;
|
|
21159
20954
|
const mode = zoneState && zoneState.mode || 'select';
|
|
21160
|
-
const totalHeight =
|
|
20955
|
+
const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
|
|
21161
20956
|
const bins = data.hostData && data.hostData.bins;
|
|
21162
20957
|
const rows = visibleRows.slice(rowRange.startIndex, rowRange.endIndex);
|
|
21163
20958
|
const w = Math.max(1, Math.floor(width));
|
|
@@ -21410,7 +21205,11 @@ const $bfd29d54a0f0e853$var$BinsBody = (props)=>{
|
|
|
21410
21205
|
left: 0,
|
|
21411
21206
|
right: 0,
|
|
21412
21207
|
height: r.height,
|
|
21413
|
-
background:
|
|
21208
|
+
background: (0, $gXNCa$tbrowse.rowHighlightBackground)((0, $gXNCa$tbrowsecore.rowFlags)(r, {
|
|
21209
|
+
hoveredNodeId: hoveredNodeId,
|
|
21210
|
+
hoveredSubtreeIds: hoveredSubtreeIds,
|
|
21211
|
+
selectedNodeId: selectedNodeId
|
|
21212
|
+
})),
|
|
21414
21213
|
cursor: rowCursor,
|
|
21415
21214
|
opacity: r.opacity ?? 1
|
|
21416
21215
|
}
|
|
@@ -21450,37 +21249,12 @@ const $bfd29d54a0f0e853$export$d85d069f2280460c = {
|
|
|
21450
21249
|
// Host-defined tbrowse zone: a narrow, right-justified text column showing the
|
|
21451
21250
|
// number of matching genes per leaf (genome) — or, for a collapsed clade, the
|
|
21452
21251
|
// summed count over its subtree. Counts come from the taxon_id facet; the
|
|
21453
|
-
// per-node sums are
|
|
21454
|
-
// hostData.geneCounts = { [nodeId]: number }.
|
|
21252
|
+
// per-node sums are computed by tbrowse-core's `countsFromFacet` in the
|
|
21253
|
+
// container and handed in via hostData.geneCounts = { [nodeId]: number }.
|
|
21254
|
+
|
|
21455
21255
|
|
|
21456
21256
|
|
|
21457
21257
|
|
|
21458
|
-
function $15275c4f5939e11d$export$ce87f9041549c2e9(tree, taxonIdFacet) {
|
|
21459
|
-
if (!tree || !tree.nodes) return {};
|
|
21460
|
-
const leafCount = {};
|
|
21461
|
-
if (Array.isArray(taxonIdFacet)) for(let i = 0; i < taxonIdFacet.length; i += 2)leafCount[String(taxonIdFacet[i])] = +taxonIdFacet[i + 1];
|
|
21462
|
-
const childrenOf = {};
|
|
21463
|
-
for (const id of Object.keys(tree.nodes)){
|
|
21464
|
-
const p = tree.nodes[id].parentId;
|
|
21465
|
-
if (p !== null && p !== undefined) (childrenOf[p] = childrenOf[p] || []).push(id);
|
|
21466
|
-
}
|
|
21467
|
-
const out = {};
|
|
21468
|
-
const visit = (id)=>{
|
|
21469
|
-
const node = tree.nodes[id];
|
|
21470
|
-
if (!node) return 0;
|
|
21471
|
-
if (node.isLeaf) {
|
|
21472
|
-
const c = leafCount[id] || 0;
|
|
21473
|
-
out[id] = c;
|
|
21474
|
-
return c;
|
|
21475
|
-
}
|
|
21476
|
-
let sum = 0;
|
|
21477
|
-
for (const k of childrenOf[id] || [])sum += visit(k);
|
|
21478
|
-
out[id] = sum;
|
|
21479
|
-
return sum;
|
|
21480
|
-
};
|
|
21481
|
-
visit(tree.rootId);
|
|
21482
|
-
return out;
|
|
21483
|
-
}
|
|
21484
21258
|
const $15275c4f5939e11d$var$CountsHeader = ({ zoneState: zoneState, setZoneState: setZoneState })=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
21485
21259
|
style: {
|
|
21486
21260
|
padding: '0 10px',
|
|
@@ -21498,28 +21272,24 @@ const $15275c4f5939e11d$var$CountsHeader = ({ zoneState: zoneState, setZoneState
|
|
|
21498
21272
|
}))
|
|
21499
21273
|
})
|
|
21500
21274
|
});
|
|
21501
|
-
// Shared row-highlight background, matching the labels zone so hovering any
|
|
21502
|
-
// zone lights up the same row across all of them (state flows through the
|
|
21503
|
-
// tbrowse store's hoveredNodeId).
|
|
21504
|
-
function $15275c4f5939e11d$var$rowHighlight(isSelected, isExactHover, isInHoveredSubtree) {
|
|
21505
|
-
if (isSelected) return 'var(--tbrowse-row-select-bg)';
|
|
21506
|
-
if (isExactHover) return 'var(--tbrowse-row-hover-bg)';
|
|
21507
|
-
if (isInHoveredSubtree) return 'var(--tbrowse-row-subtree-bg)';
|
|
21508
|
-
return 'transparent';
|
|
21509
|
-
}
|
|
21510
21275
|
const $15275c4f5939e11d$var$CountsBody = ({ visibleRows: visibleRows, rowRange: rowRange, width: width, data: data, hoveredNodeId: hoveredNodeId, hoveredSubtreeIds: hoveredSubtreeIds, selectedNodeId: selectedNodeId, onHoverNode: onHoverNode, onSelectNode: onSelectNode })=>{
|
|
21511
|
-
const totalHeight =
|
|
21276
|
+
const totalHeight = (0, $gXNCa$tbrowsecore.rowsTotalHeight)(visibleRows);
|
|
21512
21277
|
const counts = data.hostData && data.hostData.geneCounts || {};
|
|
21513
|
-
|
|
21278
|
+
// Rows in the virtualised range with the shared hover/select background, so
|
|
21279
|
+
// hovering any zone lights up the same row across all of them.
|
|
21280
|
+
const rows = (0, $gXNCa$tbrowse.rowsWithFlags)(visibleRows, rowRange, {
|
|
21281
|
+
hoveredNodeId: hoveredNodeId,
|
|
21282
|
+
hoveredSubtreeIds: hoveredSubtreeIds,
|
|
21283
|
+
selectedNodeId: selectedNodeId
|
|
21284
|
+
});
|
|
21514
21285
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
21515
21286
|
style: {
|
|
21516
21287
|
position: 'relative',
|
|
21517
21288
|
width: width,
|
|
21518
21289
|
height: totalHeight
|
|
21519
21290
|
},
|
|
21520
|
-
children: rows.map((r)=>{
|
|
21291
|
+
children: rows.map(({ row: r, background: background })=>{
|
|
21521
21292
|
const n = counts[r.nodeId];
|
|
21522
|
-
const background = $15275c4f5939e11d$var$rowHighlight(selectedNodeId === r.nodeId, hoveredNodeId === r.nodeId, !!(hoveredSubtreeIds && hoveredSubtreeIds.has(r.nodeId)));
|
|
21523
21293
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
21524
21294
|
onMouseEnter: ()=>onHoverNode(r.nodeId),
|
|
21525
21295
|
onMouseLeave: ()=>onHoverNode(null),
|
|
@@ -21589,56 +21359,37 @@ function $15504f5eba8e73bd$var$binIdxsToSolr(idxs) {
|
|
|
21589
21359
|
}
|
|
21590
21360
|
return parts.join(' OR ');
|
|
21591
21361
|
}
|
|
21362
|
+
// The initial controlled ViewState: tbrowse-core's buildInitialViewState over
|
|
21363
|
+
// this view's zone list with the view's own fr-shares, and — passed through
|
|
21364
|
+
// `overrides` so they replace the seeded defaults verbatim — the per-zone states
|
|
21365
|
+
// exactly as this view has always persisted them. The tree zone tolerates a
|
|
21366
|
+
// partial state (reads with `?? default`); the labels zone reads `visibleFields`
|
|
21367
|
+
// directly, so it must be present. Drive the tree as a cladogram (taxonomy has
|
|
21368
|
+
// no branch lengths) and show the genome/species name in the labels column.
|
|
21592
21369
|
function $15504f5eba8e73bd$var$baseViewState() {
|
|
21593
|
-
return {
|
|
21594
|
-
|
|
21595
|
-
|
|
21596
|
-
|
|
21597
|
-
|
|
21598
|
-
|
|
21599
|
-
nodeOfInterestId: null,
|
|
21600
|
-
zones: [
|
|
21601
|
-
{
|
|
21602
|
-
id: 'tree',
|
|
21603
|
-
width: 30,
|
|
21604
|
-
visible: true
|
|
21605
|
-
},
|
|
21606
|
-
{
|
|
21607
|
-
id: 'labels',
|
|
21608
|
-
width: 22,
|
|
21609
|
-
visible: true
|
|
21610
|
-
},
|
|
21611
|
-
{
|
|
21612
|
-
id: 'counts',
|
|
21613
|
-
width: 10,
|
|
21614
|
-
visible: true
|
|
21615
|
-
},
|
|
21616
|
-
{
|
|
21617
|
-
id: 'bins',
|
|
21618
|
-
width: 60,
|
|
21619
|
-
visible: true
|
|
21620
|
-
}
|
|
21621
|
-
],
|
|
21622
|
-
// Controlled mode supplies zoneStates wholesale, so each must carry the
|
|
21623
|
-
// fields its zone reads. The tree zone tolerates a partial state (reads
|
|
21624
|
-
// with `?? default`); the labels zone reads `visibleFields` directly, so
|
|
21625
|
-
// it must be present. Drive the tree as a cladogram (taxonomy has no
|
|
21626
|
-
// branch lengths) and show the genome/species name in the labels column.
|
|
21627
|
-
zoneStates: {
|
|
21628
|
-
tree: {
|
|
21629
|
-
layoutMode: 'cladogram',
|
|
21630
|
-
showLeafExtensions: false
|
|
21631
|
-
},
|
|
21632
|
-
labels: {
|
|
21633
|
-
visibleFields: [
|
|
21634
|
-
'taxonomy.commonName'
|
|
21635
|
-
]
|
|
21636
|
-
},
|
|
21637
|
-
counts: {},
|
|
21638
|
-
bins: {}
|
|
21370
|
+
return (0, $gXNCa$tbrowsecore.buildInitialViewState)($15504f5eba8e73bd$var$ZONES, {
|
|
21371
|
+
widths: {
|
|
21372
|
+
tree: 30,
|
|
21373
|
+
labels: 22,
|
|
21374
|
+
counts: 10,
|
|
21375
|
+
bins: 60
|
|
21639
21376
|
},
|
|
21640
|
-
|
|
21641
|
-
|
|
21377
|
+
overrides: {
|
|
21378
|
+
zoneStates: {
|
|
21379
|
+
tree: {
|
|
21380
|
+
layoutMode: 'cladogram',
|
|
21381
|
+
showLeafExtensions: false
|
|
21382
|
+
},
|
|
21383
|
+
labels: {
|
|
21384
|
+
visibleFields: [
|
|
21385
|
+
'taxonomy.commonName'
|
|
21386
|
+
]
|
|
21387
|
+
},
|
|
21388
|
+
counts: {},
|
|
21389
|
+
bins: {}
|
|
21390
|
+
}
|
|
21391
|
+
}
|
|
21392
|
+
});
|
|
21642
21393
|
}
|
|
21643
21394
|
// The set of taxa to SHOW, mirroring the legacy TaxDist.js logic.
|
|
21644
21395
|
function $15504f5eba8e73bd$var$computeSelectedTaxa({ grameneSearch: grameneSearch, grameneGenomes: grameneGenomes, grameneMaps: grameneMaps }, treeNodes, collapseEmpties, comparaOnly, showCompara) {
|
|
@@ -21702,7 +21453,7 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
|
|
|
21702
21453
|
const facet = props.grameneSearch && props.grameneSearch.facet_counts && props.grameneSearch.facet_counts.facet_fields && props.grameneSearch.facet_counts.facet_fields.taxon_id;
|
|
21703
21454
|
return {
|
|
21704
21455
|
bins: (0, $bfd29d54a0f0e853$export$390e10a52782f018)(grameneTaxDist),
|
|
21705
|
-
geneCounts: adapter ? (0, $
|
|
21456
|
+
geneCounts: adapter ? (0, $gXNCa$tbrowsecore.countsFromFacet)(adapter.tree, facet, 'nodeId') : null,
|
|
21706
21457
|
binsUI: binsUiRef.current,
|
|
21707
21458
|
onApplyBinsFilter: onApplyBinsFilter
|
|
21708
21459
|
};
|
|
@@ -21728,7 +21479,7 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
|
|
|
21728
21479
|
const prunedNodeIds = (0, $gXNCa$react.useMemo)(()=>{
|
|
21729
21480
|
if (!adapter) return [];
|
|
21730
21481
|
const selected = $15504f5eba8e73bd$var$computeSelectedTaxa(props, adapter.tree.nodes, collapseEmpties, comparaOnly, showCompara);
|
|
21731
|
-
return adapter.
|
|
21482
|
+
return (0, $gXNCa$tbrowsecore.leavesFailing)(adapter.tree, (leaf)=>Boolean(selected[String(leaf.taxonomyId)]));
|
|
21732
21483
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21733
21484
|
}, [
|
|
21734
21485
|
adapter,
|
|
@@ -21740,7 +21491,9 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
|
|
|
21740
21491
|
props.grameneMaps
|
|
21741
21492
|
]);
|
|
21742
21493
|
// Effective (controlled) view state = user interactions + control-driven
|
|
21743
|
-
// pruning.
|
|
21494
|
+
// pruning. The derived set REPLACES prunedNodeIds (this view has no user
|
|
21495
|
+
// prunes of its own; the snapshot strips the field for the same reason).
|
|
21496
|
+
// Memoised so the reference is stable across unrelated renders.
|
|
21744
21497
|
const effectiveViewState = (0, $gXNCa$react.useMemo)(()=>({
|
|
21745
21498
|
...viewState,
|
|
21746
21499
|
prunedNodeIds: prunedNodeIds
|
|
@@ -21761,20 +21514,18 @@ const $15504f5eba8e73bd$var$TaxDistTbrowse = (props)=>{
|
|
|
21761
21514
|
const el = wrapperRef.current;
|
|
21762
21515
|
if (!el || el.clientWidth <= 0 || window.innerWidth < 800) return;
|
|
21763
21516
|
sizedRef.current = true;
|
|
21764
|
-
const
|
|
21517
|
+
const widths = (0, $gXNCa$tbrowsecore.pixelWidthsToFr)({
|
|
21765
21518
|
tree: 270,
|
|
21766
21519
|
labels: 200,
|
|
21767
21520
|
counts: 64
|
|
21768
|
-
};
|
|
21769
|
-
const used = TARGET.tree + TARGET.labels + TARGET.counts;
|
|
21770
|
-
const binsW = Math.max(160, el.clientWidth - used);
|
|
21521
|
+
}, el.clientWidth, 'bins', 160);
|
|
21771
21522
|
const base = $15504f5eba8e73bd$var$baseViewState();
|
|
21772
21523
|
setTaxTreeView({
|
|
21773
21524
|
viewState: {
|
|
21774
21525
|
...base,
|
|
21775
21526
|
zones: base.zones.map((z)=>({
|
|
21776
21527
|
...z,
|
|
21777
|
-
width:
|
|
21528
|
+
width: widths[z.id] ?? z.width
|
|
21778
21529
|
}))
|
|
21779
21530
|
}
|
|
21780
21531
|
});
|