gramene-search 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -351,8 +351,16 @@ const $9d9aeaf9299e61a1$var$expressionStudies = (0, $gXNCa$reduxbundler.createAs
351
351
  return fetch(`${store.selectGrameneAPI()}/experiments?rows=-1`).then((res)=>res.json()).then((res)=>(0, ($parcel$interopDefault($gXNCa$lodash))).groupBy(res, 'taxon_id'));
352
352
  }
353
353
  });
354
- $9d9aeaf9299e61a1$var$expressionStudies.reactExpressionStudies = (0, $gXNCa$reduxbundler.createSelector)('selectExpressionStudiesShouldUpdate', (shouldUpdate)=>{
355
- if (shouldUpdate) return {
354
+ // Only fetch when a view that consumes expression studies is enabled.
355
+ const $9d9aeaf9299e61a1$var$EXPRESSION_VIEWS = [
356
+ 'exprViz',
357
+ 'expression',
358
+ 'export'
359
+ ];
360
+ $9d9aeaf9299e61a1$var$expressionStudies.reactExpressionStudies = (0, $gXNCa$reduxbundler.createSelector)('selectExpressionStudiesShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
361
+ if (!shouldUpdate) return;
362
+ if (!viewsOn || !$9d9aeaf9299e61a1$var$EXPRESSION_VIEWS.some((id)=>viewsOn.has(id))) return;
363
+ return {
356
364
  actionCreator: 'doFetchExpressionStudies'
357
365
  };
358
366
  });
@@ -372,8 +380,10 @@ const $9d9aeaf9299e61a1$var$expressionSamples = (0, $gXNCa$reduxbundler.createAs
372
380
  });
373
381
  }
374
382
  });
375
- $9d9aeaf9299e61a1$var$expressionSamples.reactExpressionSamples = (0, $gXNCa$reduxbundler.createSelector)('selectExpressionSamplesShouldUpdate', (shouldUpdate)=>{
376
- if (shouldUpdate) return {
383
+ $9d9aeaf9299e61a1$var$expressionSamples.reactExpressionSamples = (0, $gXNCa$reduxbundler.createSelector)('selectExpressionSamplesShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
384
+ if (!shouldUpdate) return;
385
+ if (!viewsOn || !$9d9aeaf9299e61a1$var$EXPRESSION_VIEWS.some((id)=>viewsOn.has(id))) return;
386
+ return {
377
387
  actionCreator: 'doFetchExpressionSamples'
378
388
  };
379
389
  });
@@ -385,8 +395,12 @@ const $9d9aeaf9299e61a1$var$curatedGenes = (0, $gXNCa$reduxbundler.createAsyncRe
385
395
  return fetch(`https://devdata.gramene.org/curation/curations?rows=0&minFlagged=0&since=12-12-2029`).then((res)=>res.json()).then((curation)=>(0, ($parcel$interopDefault($gXNCa$lodash))).keyBy(curation.genes, 'gene_id'));
386
396
  }
387
397
  });
388
- $9d9aeaf9299e61a1$var$curatedGenes.reactCuratedGenes = (0, $gXNCa$reduxbundler.createSelector)('selectCuratedGenesShouldUpdate', (shouldUpdate)=>{
389
- if (shouldUpdate) return {
398
+ // Curated annotations are consumed by GeneList rows and Homology details,
399
+ // both inside the gene-list view.
400
+ $9d9aeaf9299e61a1$var$curatedGenes.reactCuratedGenes = (0, $gXNCa$reduxbundler.createSelector)('selectCuratedGenesShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
401
+ if (!shouldUpdate) return;
402
+ if (!viewsOn || !viewsOn.has('list')) return;
403
+ return {
390
404
  actionCreator: 'doFetchCuratedGenes'
391
405
  };
392
406
  });
@@ -403,8 +417,11 @@ const $9d9aeaf9299e61a1$var$grameneGermplasm = (0, $gXNCa$reduxbundler.createAsy
403
417
  });
404
418
  }
405
419
  });
406
- $9d9aeaf9299e61a1$var$grameneGermplasm.reactGrameneGermplasm = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneGermplasmShouldUpdate', (shouldUpdate)=>{
407
- if (shouldUpdate) return {
420
+ // Germplasm metadata is consumed by the VEP detail panel inside the gene list.
421
+ $9d9aeaf9299e61a1$var$grameneGermplasm.reactGrameneGermplasm = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneGermplasmShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
422
+ if (!shouldUpdate) return;
423
+ if (!viewsOn || !viewsOn.has('list')) return;
424
+ return {
408
425
  actionCreator: 'doFetchGrameneGermplasm'
409
426
  };
410
427
  });
@@ -522,13 +539,13 @@ const $9d9aeaf9299e61a1$var$grameneGeneAttribs = (0, $gXNCa$reduxbundler.createA
522
539
  return fetch(`${store.selectGrameneAPI()}/geneAttributes`).then((res)=>$9d9aeaf9299e61a1$var$geneAttribs);
523
540
  }
524
541
  });
525
- $9d9aeaf9299e61a1$var$grameneGeneAttribs.reactGrameneGeneAttribs = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneGeneAttribsShouldUpdate', 'selectGrameneFiltersStatus', 'selectGrameneViews', (shouldUpdate, status, views)=>{
526
- if (shouldUpdate && (status === 'finished' || status === 'ready')) {
527
- const byId = (0, ($parcel$interopDefault($gXNCa$lodash))).keyBy(views.options, 'id');
528
- if (byId.attribs.show === "on") return {
529
- actionCreator: 'doFetchGrameneGeneAttribs'
530
- };
531
- }
542
+ $9d9aeaf9299e61a1$var$grameneGeneAttribs.reactGrameneGeneAttribs = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneGeneAttribsShouldUpdate', 'selectGrameneFiltersStatus', 'selectGrameneViewsOn', (shouldUpdate, status, viewsOn)=>{
543
+ if (!shouldUpdate) return;
544
+ if (status !== 'finished' && status !== 'ready') return;
545
+ if (!viewsOn || !viewsOn.has('attribs')) return;
546
+ return {
547
+ actionCreator: 'doFetchGrameneGeneAttribs'
548
+ };
532
549
  });
533
550
  const $9d9aeaf9299e61a1$var$grameneSearch = (0, $gXNCa$reduxbundler.createAsyncResourceBundle)({
534
551
  name: 'grameneSearch',
@@ -538,8 +555,8 @@ const $9d9aeaf9299e61a1$var$grameneSearch = (0, $gXNCa$reduxbundler.createAsyncR
538
555
  const offset = store.selectGrameneSearchOffset();
539
556
  const rows = store.selectGrameneSearchRows();
540
557
  const g = store.selectGrameneGenomes();
541
- const m = store.selectGrameneMaps();
542
- const taxa = Object.keys(g.active).filter((tid)=>!m[tid].hidden);
558
+ const m = store.selectGrameneMaps() || {};
559
+ const taxa = Object.keys(g && g.active || {}).filter((tid)=>m[tid] && !m[tid].hidden);
543
560
  let fq = '';
544
561
  if (taxa.length) {
545
562
  console.log('search add a fq for ', taxa);
@@ -1568,6 +1585,15 @@ const $24971af0a229e0e3$var$grameneViews = {
1568
1585
  ...raw,
1569
1586
  options: options
1570
1587
  };
1588
+ }),
1589
+ // Set of view ids whose `show` is currently 'on'. Used by data bundles to
1590
+ // gate their auto-fetch reactors so we don't pay the cost of loading data
1591
+ // for views the user has turned off.
1592
+ selectGrameneViewsOn: (0, $gXNCa$reduxbundler.createSelector)('selectGrameneViews', (views)=>{
1593
+ const ids = new Set();
1594
+ const opts = views && views.options || [];
1595
+ for (const v of opts)if (v && v.show === 'on') ids.add(v.id);
1596
+ return ids;
1571
1597
  })
1572
1598
  };
1573
1599
  var $24971af0a229e0e3$export$2e2bcd8739ae039 = $24971af0a229e0e3$var$grameneViews;
@@ -2287,6 +2313,19 @@ const $0f839422d0d8c772$var$grameneFieldCatalog = (0, $gXNCa$reduxbundler.create
2287
2313
  return fetch(url).then((r)=>r.text()).then((text)=>$0f839422d0d8c772$var$parseCsvHeader(text)).catch(()=>null);
2288
2314
  }
2289
2315
  });
2316
+ // Auto-fetch the field catalog only when a view that needs it (exprViz fields
2317
+ // modal, exporter field tree) is enabled. Other views never read it.
2318
+ const $0f839422d0d8c772$var$FIELD_CATALOG_VIEWS = [
2319
+ 'exprViz',
2320
+ 'export'
2321
+ ];
2322
+ $0f839422d0d8c772$var$grameneFieldCatalog.reactGrameneFieldCatalog = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneFieldCatalogShouldUpdate', 'selectGrameneViewsOn', (shouldUpdate, viewsOn)=>{
2323
+ if (!shouldUpdate) return;
2324
+ if (!viewsOn || !$0f839422d0d8c772$var$FIELD_CATALOG_VIEWS.some((id)=>viewsOn.has(id))) return;
2325
+ return {
2326
+ actionCreator: 'doFetchGrameneFieldCatalog'
2327
+ };
2328
+ });
2290
2329
  function $0f839422d0d8c772$var$assayFactorLabel(assay) {
2291
2330
  if (!assay) return '';
2292
2331
  const factors = Array.isArray(assay.factor) ? assay.factor : [];
@@ -3662,10 +3701,12 @@ const $1508f5a42be6e7b5$var$exporter = {
3662
3701
  });
3663
3702
  }, $1508f5a42be6e7b5$var$PREVIEW_DEBOUNCE_MS);
3664
3703
  },
3665
- reactExporterPreview: (0, $gXNCa$reduxbundler.createSelector)('selectExporter', 'selectGrameneFiltersStatus', 'selectGrameneFiltersQueryString', 'selectGrameneGenomes', 'selectGrameneMaps', (exp, filtersStatus, q, g, m)=>{
3704
+ reactExporterPreview: (0, $gXNCa$reduxbundler.createSelector)('selectExporter', 'selectGrameneFiltersStatus', 'selectGrameneFiltersQueryString', 'selectGrameneGenomes', 'selectGrameneMaps', 'selectGrameneViewsOn', (exp, filtersStatus, q, g, m, viewsOn)=>{
3666
3705
  if (!exp || !exp.preview) return;
3667
3706
  if (filtersStatus === 'init') return;
3668
3707
  if (exp.preview.status === 'loading') return;
3708
+ // Don't run the preview query unless the user has the exporter view open.
3709
+ if (!viewsOn || !viewsOn.has('export')) return;
3669
3710
  const maps = m || {};
3670
3711
  const taxa = Object.keys(g && g.active || {}).filter((tid)=>maps[tid] && !maps[tid].hidden);
3671
3712
  const sig = `${q}|${taxa.sort().join(',')}`;
@@ -11002,6 +11043,21 @@ const $4d0c2e01f58b53b1$var$FieldsModalCmp = (props)=>{
11002
11043
  records,
11003
11044
  selections
11004
11045
  ]);
11046
+ // Distinct values per property type across the unfiltered candidate set.
11047
+ // A type with a single value across every record can't discriminate (e.g.
11048
+ // "Organism" when every field belongs to the same species), so we hide it
11049
+ // from the property tree.
11050
+ const totalValueCountByKey = (0, $gXNCa$react.useMemo)(()=>{
11051
+ const m = {};
11052
+ for (const r of records)for (const [k, v] of Object.entries(r.props)){
11053
+ if (v == null) continue;
11054
+ if (!m[k]) m[k] = new Set();
11055
+ m[k].add(v);
11056
+ }
11057
+ return m;
11058
+ }, [
11059
+ records
11060
+ ]);
11005
11061
  // Reset state when modal (re)opens for a taxon.
11006
11062
  (0, $gXNCa$react.useEffect)(()=>{
11007
11063
  if (open && taxon) {
@@ -11169,6 +11225,13 @@ const $4d0c2e01f58b53b1$var$FieldsModalCmp = (props)=>{
11169
11225
  }),
11170
11226
  propTree.map((grp)=>{
11171
11227
  const typeRows = grp.types.map((t)=>{
11228
+ // Hide property types that can't discriminate among the
11229
+ // candidates: zero values, or a single value shared by every
11230
+ // record. The selection-aware count (valueSetByKey) is what
11231
+ // we display; the unfiltered count gates visibility so a type
11232
+ // doesn't pop in/out as the user clicks values.
11233
+ const totalDistinct = totalValueCountByKey[t.key] && totalValueCountByKey[t.key].size || 0;
11234
+ if (totalDistinct < 2) return null;
11172
11235
  const numValues = valueSetByKey[t.key] && valueSetByKey[t.key].size || 0;
11173
11236
  if (numValues === 0) return null;
11174
11237
  const typeLabelMatches = isSearching && t.label.toLowerCase().includes(searchLc);
@@ -11239,37 +11302,32 @@ const $4d0c2e01f58b53b1$var$FieldsModalCmp = (props)=>{
11239
11302
  }),
11240
11303
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11241
11304
  className: "exprviz-fields-preview",
11242
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("table", {
11305
+ children: orderedSelectedKeys.length === 0 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11306
+ className: "exprviz-fields-placeholder",
11307
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11308
+ children: "Select a property type to preview the property values for each matching field."
11309
+ })
11310
+ }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("table", {
11243
11311
  className: "exprviz-fields-table",
11244
11312
  children: [
11245
11313
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("thead", {
11246
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tr", {
11247
- children: [
11248
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("th", {
11249
- children: "Field"
11250
- }),
11251
- orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("th", {
11252
- children: $4d0c2e01f58b53b1$var$labelForKey(k, propTree)
11253
- }, k))
11254
- ]
11314
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("tr", {
11315
+ children: orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("th", {
11316
+ children: $4d0c2e01f58b53b1$var$labelForKey(k, propTree)
11317
+ }, k))
11255
11318
  })
11256
11319
  }),
11257
11320
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tbody", {
11258
11321
  children: [
11259
- matchingFields.map((f)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tr", {
11260
- children: [
11261
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11262
- title: f.fieldName,
11263
- children: f.fieldName.replace(/__expr$/, '')
11264
- }),
11265
- orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11266
- children: f.props[k] || ''
11267
- }, k))
11268
- ]
11322
+ matchingFields.map((f)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("tr", {
11323
+ title: f.fieldName,
11324
+ children: orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11325
+ children: f.props[k] || ''
11326
+ }, k))
11269
11327
  }, f.fieldName)),
11270
11328
  matchingFields.length === 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("tr", {
11271
11329
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11272
- colSpan: 1 + orderedSelectedKeys.length,
11330
+ colSpan: orderedSelectedKeys.length,
11273
11331
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11274
11332
  children: "No matching fields"
11275
11333
  })
@@ -11316,7 +11374,6 @@ var $4d0c2e01f58b53b1$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
11316
11374
 
11317
11375
 
11318
11376
 
11319
-
11320
11377
  const $4ab64e76c1caef59$var$baseColDefs = [
11321
11378
  {
11322
11379
  field: 'id',
@@ -11328,6 +11385,7 @@ const $4ab64e76c1caef59$var$baseColDefs = [
11328
11385
  {
11329
11386
  field: 'name',
11330
11387
  headerName: 'Name',
11388
+ pinned: 'left',
11331
11389
  width: 140,
11332
11390
  suppressMovable: true
11333
11391
  }
@@ -11372,189 +11430,287 @@ function $4ab64e76c1caef59$var$buildFieldInfo(fields, studies, expressionSamples
11372
11430
  }
11373
11431
  return info;
11374
11432
  }
11375
- // Custom header: re-implements sort click + menu button so ag-grid's column
11376
- // drag/menu/filter still work, with an info icon as the popover trigger.
11377
- const $4ab64e76c1caef59$var$HeaderWithInfo = (props)=>{
11378
- const { displayName: displayName, enableSorting: enableSorting, enableMenu: enableMenu, showColumnMenu: showColumnMenu, progressSort: progressSort, column: column, info: info } = props;
11379
- const [sort, setSort] = (0, $gXNCa$react.useState)(column && column.getSort ? column.getSort() : null);
11380
- const menuRef = (0, $gXNCa$react.useRef)(null);
11381
- (0, $gXNCa$react.useEffect)(()=>{
11382
- if (!column || !column.addEventListener) return;
11383
- const handler = ()=>setSort(column.getSort());
11384
- column.addEventListener('sortChanged', handler);
11385
- return ()=>column.removeEventListener('sortChanged', handler);
11386
- }, [
11387
- column
11388
- ]);
11389
- const onSortClick = (event)=>{
11390
- if (enableSorting && progressSort) progressSort(event.shiftKey);
11391
- };
11392
- const onMenuClick = (event)=>{
11393
- event.stopPropagation();
11394
- if (showColumnMenu && menuRef.current) showColumnMenu(menuRef.current);
11395
- };
11396
- const factorEntries = info ? Object.entries(info.factors || {}) : [];
11397
- const charEntries = info ? Object.entries(info.characteristics || {}) : [];
11398
- const popover = info ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Popover), {
11399
- id: `exprviz-header-${info.studyId}-${info.group}`,
11400
- className: "exprviz-header-popover",
11401
- children: [
11402
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Popover).Header, {
11403
- as: "h6",
11404
- children: info.studyDescription
11405
- }),
11406
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Popover).Body, {
11407
- children: [
11408
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11409
- children: [
11410
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11411
- children: "Study:"
11412
- }),
11413
- " ",
11414
- info.studyId
11415
- ]
11416
- }),
11417
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11418
- children: [
11419
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11420
- children: "Group:"
11421
- }),
11422
- " ",
11423
- info.group,
11424
- " (",
11425
- info.replicates,
11426
- " ",
11427
- info.replicates === 1 ? 'rep' : 'reps',
11428
- ")"
11429
- ]
11430
- }),
11431
- factorEntries.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11432
- className: "exprviz-header-section",
11433
- children: [
11434
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11435
- children: "Factors"
11436
- }),
11437
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("ul", {
11438
- children: factorEntries.map(([t, v])=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
11439
- children: [
11440
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
11441
- children: [
11442
- t,
11443
- ":"
11444
- ]
11445
- }),
11446
- " ",
11447
- v
11448
- ]
11449
- }, t))
11450
- })
11451
- ]
11452
- }),
11453
- charEntries.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11454
- className: "exprviz-header-section",
11455
- children: [
11456
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11457
- children: "Characteristics"
11458
- }),
11459
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("ul", {
11460
- children: charEntries.map(([t, v])=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
11461
- children: [
11462
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
11463
- children: [
11464
- t,
11465
- ":"
11466
- ]
11467
- }),
11468
- " ",
11469
- v
11470
- ]
11471
- }, t))
11472
- })
11473
- ]
11474
- })
11475
- ]
11476
- })
11477
- ]
11478
- }) : null;
11433
+ // Plain right-aligned label cell for the metadata header rows above the
11434
+ // Gene ID / Name columns. Rendered as a React component (instead of
11435
+ // relying on ag-grid's default group label) so the alignment is controlled
11436
+ // by our own DOM and isn't fighting the quartz theme's CSS.
11437
+ const $4ab64e76c1caef59$var$LabelHeaderGroup = (props)=>{
11438
+ const { displayName: displayName } = props;
11439
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11440
+ className: "exprviz-label-header",
11441
+ children: displayName
11442
+ });
11443
+ };
11444
+ // Same idea, but for the cells that toggle a section between expanded and
11445
+ // collapsed. The caret + label sit at the right edge.
11446
+ const $4ab64e76c1caef59$var$ToggleHeaderGroup = (props)=>{
11447
+ const { displayName: displayName, onToggle: onToggle, expanded: expanded, suffix: suffix } = props;
11479
11448
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11480
- className: "exprviz-header",
11449
+ className: "exprviz-toggle-header",
11450
+ onClick: onToggle,
11451
+ role: "button",
11452
+ tabIndex: 0,
11453
+ onKeyDown: (e)=>{
11454
+ if (e.key === 'Enter' || e.key === ' ') {
11455
+ e.preventDefault();
11456
+ onToggle && onToggle();
11457
+ }
11458
+ },
11459
+ title: expanded ? 'Click to collapse' : 'Click to expand',
11481
11460
  children: [
11482
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
11483
- className: "exprviz-header-text",
11484
- onClick: onSortClick,
11485
- style: {
11486
- cursor: enableSorting ? 'pointer' : 'default'
11487
- },
11488
- children: [
11489
- displayName,
11490
- sort === 'asc' && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11491
- className: "exprviz-header-sort",
11492
- children: " \u25B2"
11493
- }),
11494
- sort === 'desc' && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11495
- className: "exprviz-header-sort",
11496
- children: " \u25BC"
11497
- })
11498
- ]
11499
- }),
11500
- info && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.OverlayTrigger), {
11501
- trigger: [
11502
- 'hover',
11503
- 'focus'
11504
- ],
11505
- placement: "bottom",
11506
- overlay: popover,
11507
- delay: {
11508
- show: 200,
11509
- hide: 100
11510
- },
11511
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11512
- className: "exprviz-header-info",
11513
- onClick: (e)=>e.stopPropagation(),
11514
- onMouseDown: (e)=>e.stopPropagation(),
11515
- "aria-label": "More info",
11516
- children: "\u24D8"
11517
- })
11461
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11462
+ className: "exprviz-toggle-caret",
11463
+ children: expanded ? "\u25BC" : "\u25B6"
11518
11464
  }),
11519
- enableMenu && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11520
- ref: menuRef,
11521
- className: "exprviz-header-menu ag-icon ag-icon-menu",
11522
- onClick: onMenuClick
11523
- })
11465
+ ' ',
11466
+ displayName,
11467
+ suffix ? ` ${suffix}` : ''
11524
11468
  ]
11525
11469
  });
11526
11470
  };
11471
+ function $4ab64e76c1caef59$var$exprValueFormatter(p) {
11472
+ const v = p.value;
11473
+ if (v == null) return '';
11474
+ if (Array.isArray(v)) return v.join(', ');
11475
+ if (typeof v === 'object') return JSON.stringify(v);
11476
+ return String(v);
11477
+ }
11478
+ // Discover every distinct factor and characteristic type across the loaded
11479
+ // fields. Each becomes one header row (with adjacent equal-value cells
11480
+ // merged). Order is alphabetical for stability across reloads.
11481
+ function $4ab64e76c1caef59$var$collectMetadataTypes(fields, fieldInfo) {
11482
+ const factorTypes = new Set();
11483
+ const charTypes = new Set();
11484
+ for (const f of fields || []){
11485
+ const info = fieldInfo[f];
11486
+ if (!info) continue;
11487
+ Object.keys(info.factors || {}).forEach((t)=>factorTypes.add(t));
11488
+ Object.keys(info.characteristics || {}).forEach((t)=>charTypes.add(t));
11489
+ }
11490
+ return {
11491
+ factorTypes: Array.from(factorTypes).sort(),
11492
+ charTypes: Array.from(charTypes).sort()
11493
+ };
11494
+ }
11495
+ // Wrap a leaf column in N nested column groups so it participates at every
11496
+ // header row, with a custom label per level. Lets the Gene ID / Name columns
11497
+ // carry the row labels ("Study"/"Factor"/"Characteristic" and the type name).
11498
+ function $4ab64e76c1caef59$var$wrapLeafWithLabels(leaf, labels) {
11499
+ let wrapped = leaf;
11500
+ for(let i = labels.length - 1; i >= 0; i--){
11501
+ const label = labels[i];
11502
+ const cls = label.headerClass ? `${label.headerClass} exprviz-hg-labels` : 'exprviz-hg-labels';
11503
+ wrapped = {
11504
+ headerName: label.headerName,
11505
+ headerClass: cls,
11506
+ // Always render through one of our components so the alignment and
11507
+ // spacing are controlled by our own DOM, not ag-grid's defaults.
11508
+ headerGroupComponent: label.headerGroupComponent || $4ab64e76c1caef59$var$LabelHeaderGroup,
11509
+ headerGroupComponentParams: label.headerGroupComponentParams,
11510
+ children: [
11511
+ wrapped
11512
+ ]
11513
+ };
11514
+ }
11515
+ return wrapped;
11516
+ }
11517
+ // Build the column tree:
11518
+ // row 1 — Study | Title | <study description>…
11519
+ // row 2..(2+F-1) — Factor | <factor type> | <value or blank>…
11520
+ // row 2+F..end-1 — Characteristic | <char type> | <value or blank>…
11521
+ // leaf row — Gene ID | Name | g3, g4, …
11522
+ // Adjacent expression cells sharing the same value at a given level merge
11523
+ // (because they're children of one group definition). Blank values for a
11524
+ // type that's defined only in other studies render as empty cells, and
11525
+ // adjacent blanks under the same parent merge automatically.
11526
+ function $4ab64e76c1caef59$var$buildColumnDefs(fields, fieldInfo, expanded, toggles) {
11527
+ if (!fields || fields.length === 0) return $4ab64e76c1caef59$var$baseColDefs;
11528
+ const { factorTypes: factorTypes, charTypes: charTypes } = $4ab64e76c1caef59$var$collectMetadataTypes(fields, fieldInfo);
11529
+ const levels = [
11530
+ {
11531
+ kind: 'study',
11532
+ getValue: (info)=>info && info.studyDescription || ''
11533
+ }
11534
+ ];
11535
+ if (expanded.factors) for (const t of factorTypes)levels.push({
11536
+ kind: 'factor',
11537
+ type: t,
11538
+ getValue: (info)=>info && info.factors && info.factors[t] || '',
11539
+ headerClass: 'exprviz-hg-factors'
11540
+ });
11541
+ else if (factorTypes.length > 0) // One placeholder level standing in for the collapsed factor rows.
11542
+ levels.push({
11543
+ kind: 'factors-collapsed',
11544
+ getValue: ()=>'',
11545
+ headerClass: 'exprviz-hg-factors exprviz-hg-collapsed'
11546
+ });
11547
+ if (expanded.chars) for (const t of charTypes)levels.push({
11548
+ kind: 'char',
11549
+ type: t,
11550
+ getValue: (info)=>info && info.characteristics && info.characteristics[t] || '',
11551
+ headerClass: 'exprviz-hg-chars'
11552
+ });
11553
+ else if (charTypes.length > 0) levels.push({
11554
+ kind: 'chars-collapsed',
11555
+ getValue: ()=>'',
11556
+ headerClass: 'exprviz-hg-chars exprviz-hg-collapsed'
11557
+ });
11558
+ // Walk fields, opening a new group at level i (and resetting all levels
11559
+ // below) the first time the value at level i differs from the previous
11560
+ // field's. Same value → same parent group → ag-grid merges the cells.
11561
+ const exprTopGroups = [];
11562
+ const currentGroups = new Array(levels.length).fill(null);
11563
+ const currentKeys = new Array(levels.length).fill(undefined);
11564
+ for (const f of fields){
11565
+ const info = fieldInfo[f] || {};
11566
+ let firstChange = levels.length;
11567
+ for(let i = 0; i < levels.length; i++){
11568
+ const key = levels[i].getValue(info);
11569
+ if (currentGroups[i] === null || currentKeys[i] !== key) {
11570
+ firstChange = i;
11571
+ break;
11572
+ }
11573
+ }
11574
+ for(let i = firstChange; i < levels.length; i++){
11575
+ const key = levels[i].getValue(info);
11576
+ const group = {
11577
+ headerName: key,
11578
+ headerClass: levels[i].headerClass,
11579
+ children: []
11580
+ };
11581
+ currentGroups[i] = group;
11582
+ currentKeys[i] = key;
11583
+ if (i === 0) exprTopGroups.push(group);
11584
+ else currentGroups[i - 1].children.push(group);
11585
+ }
11586
+ const leaf = {
11587
+ field: f,
11588
+ headerName: info.group || f.replace(/__expr$/, ''),
11589
+ width: 160,
11590
+ suppressMovable: false,
11591
+ valueFormatter: $4ab64e76c1caef59$var$exprValueFormatter
11592
+ };
11593
+ currentGroups[levels.length - 1].children.push(leaf);
11594
+ }
11595
+ // Labels for the two pinned columns. The leftmost column carries the row
11596
+ // category ("Study" / "Factor" / "Characteristic"); the next column shows
11597
+ // the specific row name (the literal "Title" for the study row, then each
11598
+ // factor/characteristic type name). The first row of each
11599
+ // factor/characteristic section gets a clickable caret that toggles the
11600
+ // section between expanded (one row per type) and collapsed (one
11601
+ // placeholder row).
11602
+ const studyLabels = [
11603
+ {
11604
+ headerName: 'Study'
11605
+ }
11606
+ ];
11607
+ const titleLabels = [
11608
+ {
11609
+ headerName: 'Title'
11610
+ }
11611
+ ];
11612
+ if (expanded.factors) factorTypes.forEach((t, i)=>{
11613
+ studyLabels.push({
11614
+ headerName: 'Factor',
11615
+ headerClass: 'exprviz-hg-factors',
11616
+ ...i === 0 ? {
11617
+ headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
11618
+ headerGroupComponentParams: {
11619
+ onToggle: toggles.toggleFactors,
11620
+ expanded: true
11621
+ }
11622
+ } : {}
11623
+ });
11624
+ titleLabels.push({
11625
+ headerName: t,
11626
+ headerClass: 'exprviz-hg-factors'
11627
+ });
11628
+ });
11629
+ else if (factorTypes.length > 0) {
11630
+ studyLabels.push({
11631
+ headerName: 'Factors',
11632
+ headerClass: 'exprviz-hg-factors exprviz-hg-collapsed',
11633
+ headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
11634
+ headerGroupComponentParams: {
11635
+ onToggle: toggles.toggleFactors,
11636
+ expanded: false,
11637
+ suffix: `(${factorTypes.length})`
11638
+ }
11639
+ });
11640
+ titleLabels.push({
11641
+ headerName: '',
11642
+ headerClass: 'exprviz-hg-factors exprviz-hg-collapsed'
11643
+ });
11644
+ }
11645
+ if (expanded.chars) charTypes.forEach((t, i)=>{
11646
+ studyLabels.push({
11647
+ headerName: 'Characteristic',
11648
+ headerClass: 'exprviz-hg-chars',
11649
+ ...i === 0 ? {
11650
+ headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
11651
+ headerGroupComponentParams: {
11652
+ onToggle: toggles.toggleChars,
11653
+ expanded: true
11654
+ }
11655
+ } : {}
11656
+ });
11657
+ titleLabels.push({
11658
+ headerName: t,
11659
+ headerClass: 'exprviz-hg-chars'
11660
+ });
11661
+ });
11662
+ else if (charTypes.length > 0) {
11663
+ studyLabels.push({
11664
+ headerName: 'Characteristics',
11665
+ headerClass: 'exprviz-hg-chars exprviz-hg-collapsed',
11666
+ headerGroupComponent: $4ab64e76c1caef59$var$ToggleHeaderGroup,
11667
+ headerGroupComponentParams: {
11668
+ onToggle: toggles.toggleChars,
11669
+ expanded: false,
11670
+ suffix: `(${charTypes.length})`
11671
+ }
11672
+ });
11673
+ titleLabels.push({
11674
+ headerName: '',
11675
+ headerClass: 'exprviz-hg-chars exprviz-hg-collapsed'
11676
+ });
11677
+ }
11678
+ const idCol = $4ab64e76c1caef59$var$wrapLeafWithLabels($4ab64e76c1caef59$var$baseColDefs[0], studyLabels);
11679
+ const nameCol = $4ab64e76c1caef59$var$wrapLeafWithLabels($4ab64e76c1caef59$var$baseColDefs[1], titleLabels);
11680
+ return [
11681
+ idCol,
11682
+ nameCol,
11683
+ ...exprTopGroups
11684
+ ];
11685
+ }
11527
11686
  const $4ab64e76c1caef59$var$ExprTable = ({ rows: rows, fields: fields, onReorder: onReorder, studies: studies, expressionSamples: expressionSamples, onHoverRow: onHoverRow })=>{
11528
11687
  const fieldInfo = (0, $gXNCa$react.useMemo)(()=>$4ab64e76c1caef59$var$buildFieldInfo(fields, studies, expressionSamples), [
11529
11688
  fields,
11530
11689
  studies,
11531
11690
  expressionSamples
11532
11691
  ]);
11533
- const columnDefs = (0, $gXNCa$react.useMemo)(()=>{
11534
- const expressionCols = (fields || []).map((f)=>({
11535
- field: f,
11536
- headerName: f.replace(/__expr$/, ''),
11537
- width: 160,
11538
- suppressMovable: false,
11539
- headerComponent: $4ab64e76c1caef59$var$HeaderWithInfo,
11540
- headerComponentParams: {
11541
- info: fieldInfo[f]
11542
- },
11543
- valueFormatter: (p)=>{
11544
- const v = p.value;
11545
- if (v == null) return '';
11546
- if (Array.isArray(v)) return v.join(', ');
11547
- if (typeof v === 'object') return JSON.stringify(v);
11548
- return String(v);
11549
- }
11550
- }));
11551
- return [
11552
- ...$4ab64e76c1caef59$var$baseColDefs,
11553
- ...expressionCols
11554
- ];
11555
- }, [
11692
+ // Default: factor rows expanded, characteristic rows collapsed (per spec).
11693
+ const [expanded, setExpanded] = (0, $gXNCa$react.useState)({
11694
+ factors: true,
11695
+ chars: false
11696
+ });
11697
+ const toggleFactors = (0, $gXNCa$react.useCallback)(()=>setExpanded((e)=>({
11698
+ ...e,
11699
+ factors: !e.factors
11700
+ })), []);
11701
+ const toggleChars = (0, $gXNCa$react.useCallback)(()=>setExpanded((e)=>({
11702
+ ...e,
11703
+ chars: !e.chars
11704
+ })), []);
11705
+ const columnDefs = (0, $gXNCa$react.useMemo)(()=>$4ab64e76c1caef59$var$buildColumnDefs(fields, fieldInfo, expanded, {
11706
+ toggleFactors: toggleFactors,
11707
+ toggleChars: toggleChars
11708
+ }), [
11556
11709
  fields,
11557
- fieldInfo
11710
+ fieldInfo,
11711
+ expanded,
11712
+ toggleFactors,
11713
+ toggleChars
11558
11714
  ]);
11559
11715
  const onColumnMoved = (e)=>{
11560
11716
  if (!onReorder || !e.finished) return;
@@ -11577,11 +11733,14 @@ const $4ab64e76c1caef59$var$ExprTable = ({ rows: rows, fields: fields, onReorder
11577
11733
  defaultColDef: {
11578
11734
  resizable: true,
11579
11735
  sortable: true,
11580
- filter: true
11736
+ filter: false,
11737
+ suppressMenu: true
11581
11738
  },
11582
11739
  animateRows: false,
11583
11740
  suppressFieldDotNotation: true,
11584
11741
  suppressDragLeaveHidesColumns: true,
11742
+ suppressColumnVirtualisation: true,
11743
+ groupHeaderHeight: 24,
11585
11744
  onColumnMoved: onColumnMoved,
11586
11745
  onCellMouseOver: (e)=>onHoverRow && onHoverRow(e.data && e.data.id),
11587
11746
  onCellMouseOut: ()=>onHoverRow && onHoverRow(null)
@@ -11647,12 +11806,41 @@ function $caf32827df861c4e$var$logTickFormat(v) {
11647
11806
  if (a >= 0.01 && a < 10000) return $gXNCa$d3.format('~g')(v);
11648
11807
  return $gXNCa$d3.format('.0e')(v);
11649
11808
  }
11650
- const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null })=>{
11809
+ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null, axisLabels: axisLabels = null })=>{
11651
11810
  const svgRef = (0, $gXNCa$react.useRef)(null);
11652
11811
  const containerRef = (0, $gXNCa$react.useRef)(null);
11653
11812
  // selections in data domain: { [field]: [lo, hi] }
11654
11813
  const selectionsRef = (0, $gXNCa$react.useRef)({});
11655
11814
  const lastClearRef = (0, $gXNCa$react.useRef)(0);
11815
+ // Track container size so the d3 render reruns when the user drags the
11816
+ // pane resizer (or when the window is resized). The values themselves
11817
+ // aren't read inside the effect — the effect always reads clientWidth/
11818
+ // clientHeight — but listing them in the deps array is what triggers it.
11819
+ const [size, setSize] = (0, $gXNCa$react.useState)({
11820
+ w: 0,
11821
+ h: 0
11822
+ });
11823
+ // Custom HTML tooltip for axis labels — gives us bold labels and structured
11824
+ // sections, which the native SVG <title> can't do.
11825
+ const [tooltip, setTooltip] = (0, $gXNCa$react.useState)(null);
11826
+ (0, $gXNCa$react.useEffect)(()=>{
11827
+ const el = containerRef.current;
11828
+ if (!el || typeof ResizeObserver === 'undefined') return;
11829
+ const ro = new ResizeObserver((entries)=>{
11830
+ for (const entry of entries){
11831
+ const { width: width, height: height } = entry.contentRect;
11832
+ setSize((prev)=>{
11833
+ if (Math.abs(prev.w - width) < 1 && Math.abs(prev.h - height) < 1) return prev;
11834
+ return {
11835
+ w: width,
11836
+ h: height
11837
+ };
11838
+ });
11839
+ }
11840
+ });
11841
+ ro.observe(el);
11842
+ return ()=>ro.disconnect();
11843
+ }, []);
11656
11844
  (0, $gXNCa$react.useEffect)(()=>{
11657
11845
  if (clearVersion !== lastClearRef.current) {
11658
11846
  selectionsRef.current = {};
@@ -11749,9 +11937,32 @@ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields,
11749
11937
  if (scale === 'log') axisGen.tickValues($caf32827df861c4e$var$logTickValues(yByField[f].domain())).tickFormat($caf32827df861c4e$var$logTickFormat);
11750
11938
  else axisGen.ticks(5);
11751
11939
  ax.call(axisGen);
11752
- const label = ax.append('text').attr('class', 'exprviz-pc-axis-label').attr('x', 4).attr('y', -4).attr('text-anchor', 'start').attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', '#333').style('font-size', '10px').style('cursor', 'grab').text(f.replace(/__expr$/, ''));
11940
+ // Compact axis label. Hovering the label or its drag-handle rect shows
11941
+ // a custom HTML tooltip (rendered outside the SVG by React) that can
11942
+ // include bold labels and section headings.
11943
+ const labelInfo = axisLabels && axisLabels[f] || {
11944
+ short: f.replace(/__expr$/, ''),
11945
+ structured: {
11946
+ studyTitle: f,
11947
+ group: '',
11948
+ factors: [],
11949
+ characteristics: []
11950
+ }
11951
+ };
11952
+ const showTip = (event)=>setTooltip({
11953
+ x: event.clientX,
11954
+ y: event.clientY,
11955
+ info: labelInfo.structured
11956
+ });
11957
+ const moveTip = (event)=>setTooltip((t)=>t ? {
11958
+ ...t,
11959
+ x: event.clientX,
11960
+ y: event.clientY
11961
+ } : null);
11962
+ const hideTip = ()=>setTooltip(null);
11963
+ ax.append('text').attr('class', 'exprviz-pc-axis-label').attr('x', 4).attr('y', -4).attr('text-anchor', 'start').attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', '#333').style('font-size', '10px').style('cursor', 'grab').text(labelInfo.short).on('mouseenter', showTip).on('mousemove', moveTip).on('mouseleave', hideTip);
11753
11964
  // hit area for grabbing — sits along the rotated label
11754
- ax.append('rect').attr('class', 'exprviz-pc-axis-handle').attr('x', 0).attr('y', -11).attr('width', 140).attr('height', 14).attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', 'transparent').style('cursor', 'grab');
11965
+ ax.append('rect').attr('class', 'exprviz-pc-axis-handle').attr('x', 0).attr('y', -11).attr('width', 140).attr('height', 14).attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', 'transparent').style('cursor', 'grab').on('mouseenter', showTip).on('mousemove', moveTip).on('mouseleave', hideTip);
11755
11966
  const brush = $gXNCa$d3.brushY().extent([
11756
11967
  [
11757
11968
  -$caf32827df861c4e$var$BRUSH_WIDTH / 2,
@@ -11844,7 +12055,10 @@ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields,
11844
12055
  scale,
11845
12056
  onBrushChange,
11846
12057
  onReorder,
11847
- clearVersion
12058
+ clearVersion,
12059
+ axisLabels,
12060
+ size.w,
12061
+ size.h
11848
12062
  ]);
11849
12063
  // Highlight the polyline matching the hovered row id without rebuilding the
11850
12064
  // SVG. Raises the highlighted path so it draws above its neighbors.
@@ -11870,15 +12084,114 @@ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields,
11870
12084
  children: "Select fields to plot."
11871
12085
  })
11872
12086
  });
11873
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12087
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11874
12088
  ref: containerRef,
11875
12089
  className: "exprviz-pc-container",
11876
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("svg", {
11877
- ref: svgRef,
11878
- width: "100%",
11879
- height: "100%",
11880
- preserveAspectRatio: "none"
11881
- })
12090
+ children: [
12091
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("svg", {
12092
+ ref: svgRef,
12093
+ width: "100%",
12094
+ height: "100%",
12095
+ preserveAspectRatio: "none"
12096
+ }),
12097
+ tooltip && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($caf32827df861c4e$var$AxisTooltip, {
12098
+ x: tooltip.x,
12099
+ y: tooltip.y,
12100
+ info: tooltip.info
12101
+ })
12102
+ ]
12103
+ });
12104
+ };
12105
+ // Position-fixed so it can escape the plot pane's clipping. Offset slightly
12106
+ // from the cursor and clamped to the viewport so it never spills off-screen.
12107
+ const $caf32827df861c4e$var$AxisTooltip = ({ x: x, y: y, info: info })=>{
12108
+ const ref = (0, $gXNCa$react.useRef)(null);
12109
+ const [pos, setPos] = (0, $gXNCa$react.useState)({
12110
+ left: x + 12,
12111
+ top: y + 12
12112
+ });
12113
+ (0, $gXNCa$react.useEffect)(()=>{
12114
+ const el = ref.current;
12115
+ if (!el) return;
12116
+ const w = el.offsetWidth;
12117
+ const h = el.offsetHeight;
12118
+ const vw = window.innerWidth;
12119
+ const vh = window.innerHeight;
12120
+ let left = x + 12;
12121
+ let top = y + 12;
12122
+ if (left + w > vw - 4) left = Math.max(4, x - 12 - w);
12123
+ if (top + h > vh - 4) top = Math.max(4, y - 12 - h);
12124
+ setPos({
12125
+ left: left,
12126
+ top: top
12127
+ });
12128
+ }, [
12129
+ x,
12130
+ y,
12131
+ info
12132
+ ]);
12133
+ const { studyTitle: studyTitle, group: group, factors: factors, characteristics: characteristics } = info;
12134
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12135
+ ref: ref,
12136
+ className: "exprviz-pc-tooltip",
12137
+ style: pos,
12138
+ children: [
12139
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12140
+ children: [
12141
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
12142
+ className: "exprviz-pc-tip-key",
12143
+ children: "Study:"
12144
+ }),
12145
+ " ",
12146
+ studyTitle,
12147
+ group ? ` (${group})` : ''
12148
+ ]
12149
+ }),
12150
+ factors.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
12151
+ children: [
12152
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12153
+ className: "exprviz-pc-tip-section",
12154
+ children: "Factors"
12155
+ }),
12156
+ factors.map((p, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12157
+ className: "exprviz-pc-tip-row",
12158
+ children: [
12159
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
12160
+ className: "exprviz-pc-tip-key",
12161
+ children: [
12162
+ p.name,
12163
+ ":"
12164
+ ]
12165
+ }),
12166
+ " ",
12167
+ p.value
12168
+ ]
12169
+ }, `f-${i}`))
12170
+ ]
12171
+ }),
12172
+ characteristics.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
12173
+ children: [
12174
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12175
+ className: "exprviz-pc-tip-section",
12176
+ children: "Characteristics"
12177
+ }),
12178
+ characteristics.map((p, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12179
+ className: "exprviz-pc-tip-row",
12180
+ children: [
12181
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
12182
+ className: "exprviz-pc-tip-key",
12183
+ children: [
12184
+ p.name,
12185
+ ":"
12186
+ ]
12187
+ }),
12188
+ " ",
12189
+ p.value
12190
+ ]
12191
+ }, `c-${i}`))
12192
+ ]
12193
+ })
12194
+ ]
11882
12195
  });
11883
12196
  };
11884
12197
  var $caf32827df861c4e$export$2e2bcd8739ae039 = $caf32827df861c4e$var$ParallelCoordsPlot;
@@ -11975,6 +12288,75 @@ const $1fd2507769d5bd00$var$ExprVizViewCmp = (props)=>{
11975
12288
  ]
11976
12289
  });
11977
12290
  };
12291
+ // Compact axis labels for the parallel-coords plot. The raw Solr field name
12292
+ // (e.g. "E_CURD_148_g5__expr") is uninformative; we prefer the assay's
12293
+ // factor labels, falling back to "organism part" then to the group id.
12294
+ // `full` is exposed via an SVG <title> so the user can hover to see study,
12295
+ // group, and every factor/characteristic.
12296
+ const $1fd2507769d5bd00$var$AXIS_LABEL_MAX = 22;
12297
+ function $1fd2507769d5bd00$var$truncateLabel(s, n) {
12298
+ if (!s) return '';
12299
+ return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
12300
+ }
12301
+ function $1fd2507769d5bd00$var$compactAssayLabel(assay, group) {
12302
+ if (!assay) return group || '';
12303
+ const factorVals = (assay.factor || []).map((f)=>f && f.label).filter(Boolean);
12304
+ if (factorVals.length) return factorVals.join('; ');
12305
+ const chars = assay.characteristic || [];
12306
+ const organ = chars.find((c)=>c && c.type === 'organism part');
12307
+ if (organ && organ.label) return organ.label;
12308
+ const firstChar = chars.find((c)=>c && c.label);
12309
+ if (firstChar) return firstChar.label;
12310
+ return group || '';
12311
+ }
12312
+ function $1fd2507769d5bd00$var$assayPairs(list) {
12313
+ return (list || []).filter((x)=>x && x.label).map((x)=>({
12314
+ name: x.type || '',
12315
+ value: x.label
12316
+ }));
12317
+ }
12318
+ function $1fd2507769d5bd00$var$buildAxisLabels(fields, studies, expressionSamples) {
12319
+ const labels = {};
12320
+ if (!fields) return labels;
12321
+ const studyById = {};
12322
+ (studies || []).forEach((s)=>{
12323
+ if (s && s._id) studyById[s._id] = s;
12324
+ });
12325
+ const findAssay = (studyId, group)=>{
12326
+ const arr = expressionSamples && expressionSamples[studyId];
12327
+ return arr ? arr.find((a)=>a.group === group) : null;
12328
+ };
12329
+ for (const f of fields){
12330
+ const m = f.match(/^(.+?)_g(\d+)__expr$/);
12331
+ if (!m) {
12332
+ labels[f] = {
12333
+ short: $1fd2507769d5bd00$var$truncateLabel(f.replace(/__expr$/, ''), $1fd2507769d5bd00$var$AXIS_LABEL_MAX),
12334
+ structured: {
12335
+ studyTitle: f,
12336
+ group: '',
12337
+ factors: [],
12338
+ characteristics: []
12339
+ }
12340
+ };
12341
+ continue;
12342
+ }
12343
+ const expId = m[1].replace(/_/g, '-');
12344
+ const group = 'g' + m[2];
12345
+ const assay = findAssay(expId, group);
12346
+ const study = studyById[expId];
12347
+ const studyName = study && study.description || expId;
12348
+ labels[f] = {
12349
+ short: $1fd2507769d5bd00$var$truncateLabel($1fd2507769d5bd00$var$compactAssayLabel(assay, group), $1fd2507769d5bd00$var$AXIS_LABEL_MAX),
12350
+ structured: {
12351
+ studyTitle: studyName,
12352
+ group: group,
12353
+ factors: $1fd2507769d5bd00$var$assayPairs(assay && assay.factor),
12354
+ characteristics: $1fd2507769d5bd00$var$assayPairs(assay && assay.characteristic)
12355
+ }
12356
+ };
12357
+ }
12358
+ return labels;
12359
+ }
11978
12360
  function $1fd2507769d5bd00$var$rowMatchesSelections(row, selections) {
11979
12361
  for (const f of Object.keys(selections)){
11980
12362
  const v = row[f];
@@ -12038,6 +12420,34 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
12038
12420
  const [selections, setSelections] = (0, $gXNCa$react.useState)({});
12039
12421
  const [clearVersion, setClearVersion] = (0, $gXNCa$react.useState)(0);
12040
12422
  const [hoveredId, setHoveredId] = (0, $gXNCa$react.useState)(null);
12423
+ const [plotHeight, setPlotHeight] = (0, $gXNCa$react.useState)(320);
12424
+ const resizeStateRef = (0, $gXNCa$react.useRef)(null);
12425
+ // Drag the horizontal separator between the plot and the table to retune
12426
+ // their relative sizes. Bounded so neither pane disappears entirely.
12427
+ const startResize = (e)=>{
12428
+ e.preventDefault();
12429
+ resizeStateRef.current = {
12430
+ startY: e.clientY,
12431
+ startHeight: plotHeight
12432
+ };
12433
+ const onMove = (ev)=>{
12434
+ const s = resizeStateRef.current;
12435
+ if (!s) return;
12436
+ const next = Math.max(120, Math.min(1200, s.startHeight + (ev.clientY - s.startY)));
12437
+ setPlotHeight(next);
12438
+ };
12439
+ const onUp = ()=>{
12440
+ resizeStateRef.current = null;
12441
+ document.removeEventListener('mousemove', onMove);
12442
+ document.removeEventListener('mouseup', onUp);
12443
+ document.body.style.cursor = '';
12444
+ document.body.style.userSelect = '';
12445
+ };
12446
+ document.body.style.cursor = 'row-resize';
12447
+ document.body.style.userSelect = 'none';
12448
+ document.addEventListener('mousemove', onMove);
12449
+ document.addEventListener('mouseup', onUp);
12450
+ };
12041
12451
  const hasBrush = Object.keys(selections).length > 0;
12042
12452
  const filteredRows = (0, $gXNCa$react.useMemo)(()=>{
12043
12453
  if (!hasBrush) return rows;
@@ -12068,6 +12478,11 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
12068
12478
  ...hidden
12069
12479
  ]);
12070
12480
  } : undefined;
12481
+ const axisLabels = (0, $gXNCa$react.useMemo)(()=>$1fd2507769d5bd00$var$buildAxisLabels(visibleFields, studies, expressionSamples), [
12482
+ visibleFields,
12483
+ studies,
12484
+ expressionSamples
12485
+ ]);
12071
12486
  (0, $gXNCa$react.useEffect)(()=>{
12072
12487
  if (rows.length === 0 && hasBrush) {
12073
12488
  setSelections({});
@@ -12175,6 +12590,9 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
12175
12590
  children: [
12176
12591
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12177
12592
  className: "exprviz-plot",
12593
+ style: {
12594
+ height: plotHeight
12595
+ },
12178
12596
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $caf32827df861c4e$export$2e2bcd8739ae039), {
12179
12597
  rows: rows,
12180
12598
  fields: visibleFields,
@@ -12182,9 +12600,18 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
12182
12600
  onBrushChange: setSelections,
12183
12601
  onReorder: handleReorder,
12184
12602
  clearVersion: clearVersion,
12185
- hoveredId: hoveredId
12603
+ hoveredId: hoveredId,
12604
+ axisLabels: axisLabels
12186
12605
  })
12187
12606
  }),
12607
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12608
+ className: "exprviz-resizer",
12609
+ role: "separator",
12610
+ "aria-orientation": "horizontal",
12611
+ "aria-label": "Resize plot",
12612
+ onMouseDown: startResize,
12613
+ title: "Drag to resize"
12614
+ }),
12188
12615
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12189
12616
  className: "exprviz-table",
12190
12617
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $4ab64e76c1caef59$export$2e2bcd8739ae039), {