gramene-search 2.20.2 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -849,6 +849,7 @@ function $af4441dd29af05df$var$markSubtree(node, source, invert) {
849
849
  $af4441dd29af05df$var$markSubtree(child, source, invert);
850
850
  });
851
851
  }
852
+ const $af4441dd29af05df$export$6b95404109a3e2ec = 'Genomic intervals';
852
853
  const $af4441dd29af05df$export$da3188ffd2da2525 = {
853
854
  orthologs: {
854
855
  label: 'orthologs',
@@ -1037,6 +1038,52 @@ const $af4441dd29af05df$var$grameneFilters = {
1037
1038
  return newState;
1038
1039
  }
1039
1040
  break;
1041
+ case 'GRAMENE_FILTER_INTERVAL_ADDED':
1042
+ {
1043
+ // Intervals accumulate into a single OR group, because ANDing two
1044
+ // disjoint intervals matches nothing — no gene sits in both. The group
1045
+ // is tagged with `category`, one of the keys viewSnapshot keeps, so a
1046
+ // restored view merges into it instead of starting a second group; were
1047
+ // that tag ever lost the worst case is an extra group, never a corrupt
1048
+ // tree.
1049
+ newState = Object.assign({}, state, {
1050
+ status: 'search',
1051
+ showMarked: true,
1052
+ searchOffset: 0
1053
+ });
1054
+ const { map: map, mapLabel: mapLabel, region: region, start: start, end: end } = payload;
1055
+ // One leaf per interval, not four. getQuery has a raw-passthrough branch
1056
+ // for a `location` leaf whose value starts with "(map:" — every other
1057
+ // branch would escape the colons — so the whole clause rides in one node
1058
+ // and the Filters panel shows one readable row per interval.
1059
+ //
1060
+ // Overlap, not containment: start <= interval end AND end >= interval
1061
+ // start, so a gene that merely crosses the boundary still matches.
1062
+ const intervalNode = {
1063
+ fq_field: 'location',
1064
+ fq_value: `(map:${map} AND region:${region}` + ` AND start:[* TO ${end}] AND end:[${start} TO *])`,
1065
+ name: `${mapLabel} ${region}:${start.toLocaleString()}-${end.toLocaleString()}`,
1066
+ category: 'Genomic interval',
1067
+ negate: false,
1068
+ showMenu: false
1069
+ };
1070
+ let group = (newState.children || []).find((c)=>c && c.category === $af4441dd29af05df$export$6b95404109a3e2ec && Array.isArray(c.children));
1071
+ if (group) group.children.push(intervalNode);
1072
+ else newState.children.push({
1073
+ operation: 'OR',
1074
+ category: $af4441dd29af05df$export$6b95404109a3e2ec,
1075
+ negate: false,
1076
+ showMenu: false,
1077
+ children: [
1078
+ intervalNode
1079
+ ]
1080
+ });
1081
+ // Inserting a nested subtree invalidates the hand-computed indices the
1082
+ // append actions rely on, so renumber the whole tree.
1083
+ $af4441dd29af05df$var$reindexTree(newState, 0);
1084
+ $af4441dd29af05df$var$markSubtree(newState, newState, false);
1085
+ return newState;
1086
+ }
1040
1087
  case 'GRAMENE_FILTER_EXPANSION_SET':
1041
1088
  {
1042
1089
  // Setting or clearing an expansion is just a field on the node — the tree
@@ -1353,6 +1400,25 @@ const $af4441dd29af05df$var$grameneFilters = {
1353
1400
  ]
1354
1401
  });
1355
1402
  },
1403
+ // Add one genomic interval. Accumulates into a single OR group; see the
1404
+ // GRAMENE_FILTER_INTERVAL_ADDED reducer for why they cannot be ANDed.
1405
+ doAddGrameneInterval: (interval)=>({ dispatch: dispatch })=>{
1406
+ dispatch({
1407
+ type: 'BATCH_ACTIONS',
1408
+ actions: [
1409
+ {
1410
+ type: 'GRAMENE_SEARCH_CLEARED'
1411
+ },
1412
+ {
1413
+ type: 'GRAMENE_TAXONOMY_CLEARED'
1414
+ },
1415
+ {
1416
+ type: 'GRAMENE_FILTER_INTERVAL_ADDED',
1417
+ payload: interval
1418
+ }
1419
+ ]
1420
+ });
1421
+ },
1356
1422
  doToggleGrameneFilterMenu: (node)=>({ dispatch: dispatch })=>{
1357
1423
  dispatch({
1358
1424
  type: 'GRAMENE_FILTER_MENU_TOGGLED',
@@ -20789,6 +20855,181 @@ var $7d7b7dad5e71dfd3$export$2e2bcd8739ae039 = $7d7b7dad5e71dfd3$var$Facets;
20789
20855
 
20790
20856
 
20791
20857
 
20858
+ // Sequences that aren't a placed chromosome. Excluded from the dropdown because
20859
+ // an interval on them is not something a user can reason about positionally.
20860
+ // Organelles (C, M, Mt, Pt, mitochondrion_genome) are deliberately kept — they
20861
+ // are real, coordinate-addressable sequences.
20862
+ const $909272efb41be16f$var$UNPLACED = /^(unanchored|u)$/i;
20863
+ function $909272efb41be16f$export$ac97d17982e0cb21(grameneMaps, targetTaxonId) {
20864
+ const genomes = Object.values(grameneMaps || {}).filter((m)=>m && m._id && m.regions);
20865
+ const byLabel = (a, b)=>String(a.display_name || '').localeCompare(String(b.display_name || ''));
20866
+ const reference = genomes.filter((m)=>String(m.taxon_id) === String(targetTaxonId));
20867
+ const refIds = new Set(reference.map((m)=>m.taxon_id));
20868
+ const anchors = genomes.filter((m)=>m.is_anchor && !refIds.has(m.taxon_id)).sort(byLabel);
20869
+ const anchorIds = new Set(anchors.map((m)=>m.taxon_id));
20870
+ const rest = genomes.filter((m)=>!refIds.has(m.taxon_id) && !anchorIds.has(m.taxon_id)).sort(byLabel);
20871
+ return {
20872
+ reference: reference,
20873
+ anchors: anchors,
20874
+ rest: rest
20875
+ };
20876
+ }
20877
+ function $909272efb41be16f$export$94955a075dcb14b2(genome) {
20878
+ const regions = genome && genome.regions || {};
20879
+ const names = regions.names || [];
20880
+ const lengths = regions.lengths || [];
20881
+ return names.map((name, i)=>({
20882
+ name: name,
20883
+ length: lengths[i]
20884
+ })).filter((r)=>!$909272efb41be16f$var$UNPLACED.test(String(r.name)));
20885
+ }
20886
+ const $909272efb41be16f$var$GenomicIntervalForm = ({ grameneMaps: grameneMaps, targetTaxonId: targetTaxonId, onAdd: onAdd })=>{
20887
+ const [open, setOpen] = (0, $gXNCa$react.useState)(false);
20888
+ const [taxonId, setTaxonId] = (0, $gXNCa$react.useState)('');
20889
+ const [region, setRegion] = (0, $gXNCa$react.useState)('');
20890
+ const [start, setStart] = (0, $gXNCa$react.useState)('');
20891
+ const [end, setEnd] = (0, $gXNCa$react.useState)('');
20892
+ const { reference: reference, anchors: anchors, rest: rest } = (0, $gXNCa$react.useMemo)(()=>$909272efb41be16f$export$ac97d17982e0cb21(grameneMaps, targetTaxonId), [
20893
+ grameneMaps,
20894
+ targetTaxonId
20895
+ ]);
20896
+ // Default to the site's reference genome once maps have loaded.
20897
+ const effectiveTaxon = taxonId || reference[0] && String(reference[0].taxon_id) || '';
20898
+ const genome = effectiveTaxon ? (grameneMaps || {})[effectiveTaxon] : null;
20899
+ const chromosomes = (0, $gXNCa$react.useMemo)(()=>$909272efb41be16f$export$94955a075dcb14b2(genome), [
20900
+ genome
20901
+ ]);
20902
+ const effectiveRegion = region || chromosomes[0] && chromosomes[0].name || '';
20903
+ const chrLength = (chromosomes.find((c)=>String(c.name) === String(effectiveRegion)) || {}).length;
20904
+ const s = start === '' ? null : Number(start);
20905
+ const e = end === '' ? null : Number(end);
20906
+ // Validated against the assembly's own region lengths, so the message can name
20907
+ // the actual limit rather than just refusing.
20908
+ const error = (()=>{
20909
+ if (!genome) return null;
20910
+ if (s === null || e === null) return null;
20911
+ if (!Number.isFinite(s) || !Number.isFinite(e)) return 'Positions must be numbers.';
20912
+ if (s < 1 || e < 1) return 'Positions start at 1.';
20913
+ if (s > e) return 'Start must not be greater than end.';
20914
+ if (chrLength && e > chrLength) return `${effectiveRegion} is ${chrLength.toLocaleString()} bp; end is beyond it.`;
20915
+ return null;
20916
+ })();
20917
+ const ready = !!genome && !!effectiveRegion && s !== null && e !== null && !error;
20918
+ const submit = ()=>{
20919
+ if (!ready) return;
20920
+ onAdd({
20921
+ map: genome._id,
20922
+ mapLabel: genome.display_name || genome._id,
20923
+ region: effectiveRegion,
20924
+ start: s,
20925
+ end: e
20926
+ });
20927
+ setStart('');
20928
+ setEnd('');
20929
+ };
20930
+ const genomeOption = (m)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("option", {
20931
+ value: m.taxon_id,
20932
+ children: m.display_name || m.system_name
20933
+ }, m.taxon_id);
20934
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20935
+ className: "gramene-interval",
20936
+ children: [
20937
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20938
+ className: "gramene-interval-toggle",
20939
+ onClick: ()=>setOpen(!open),
20940
+ children: [
20941
+ open ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronDown), {}) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronRight), {}),
20942
+ " Add genomic interval"
20943
+ ]
20944
+ }),
20945
+ open && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20946
+ className: "gramene-interval-form",
20947
+ children: [
20948
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Select, {
20949
+ size: "sm",
20950
+ className: "mb-1",
20951
+ value: effectiveTaxon,
20952
+ onChange: (ev)=>{
20953
+ setTaxonId(ev.target.value);
20954
+ setRegion('');
20955
+ },
20956
+ children: [
20957
+ reference.map(genomeOption),
20958
+ anchors.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("optgroup", {
20959
+ label: "Reference genomes",
20960
+ children: anchors.map(genomeOption)
20961
+ }),
20962
+ rest.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("optgroup", {
20963
+ label: "Other genomes",
20964
+ children: rest.map(genomeOption)
20965
+ })
20966
+ ]
20967
+ }),
20968
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Select, {
20969
+ size: "sm",
20970
+ className: "mb-1",
20971
+ value: effectiveRegion,
20972
+ onChange: (ev)=>setRegion(ev.target.value),
20973
+ children: chromosomes.map((c)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("option", {
20974
+ value: c.name,
20975
+ children: [
20976
+ c.name,
20977
+ c.length ? ` (${Math.round(c.length / 1e6)} Mb)` : ''
20978
+ ]
20979
+ }, c.name))
20980
+ }),
20981
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20982
+ className: "gramene-interval-range",
20983
+ children: [
20984
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
20985
+ size: "sm",
20986
+ type: "number",
20987
+ min: "1",
20988
+ placeholder: "start",
20989
+ value: start,
20990
+ onChange: (ev)=>setStart(ev.target.value)
20991
+ }),
20992
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
20993
+ className: "gramene-interval-dash",
20994
+ children: "\u2013"
20995
+ }),
20996
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
20997
+ size: "sm",
20998
+ type: "number",
20999
+ min: "1",
21000
+ placeholder: "end",
21001
+ value: end,
21002
+ onChange: (ev)=>setEnd(ev.target.value),
21003
+ onKeyDown: (ev)=>{
21004
+ if (ev.key === 'Enter') submit();
21005
+ }
21006
+ })
21007
+ ]
21008
+ }),
21009
+ error && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21010
+ className: "gramene-interval-error",
21011
+ children: error
21012
+ }),
21013
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21014
+ size: "sm",
21015
+ variant: "primary",
21016
+ className: "gramene-interval-submit",
21017
+ disabled: !ready,
21018
+ onClick: submit,
21019
+ children: "Add interval"
21020
+ })
21021
+ ]
21022
+ })
21023
+ ]
21024
+ });
21025
+ };
21026
+ var $909272efb41be16f$export$2e2bcd8739ae039 = $909272efb41be16f$var$GenomicIntervalForm;
21027
+
21028
+
21029
+
21030
+
21031
+
21032
+
20792
21033
 
20793
21034
 
20794
21035
 
@@ -21423,10 +21664,15 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
21423
21664
  unmarkTargets: props.doUnmarkGrameneFilterTargets,
21424
21665
  toggleMenu: props.doToggleGrameneFilterMenu
21425
21666
  };
21667
+ const intervalForm = /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $909272efb41be16f$export$2e2bcd8739ae039), {
21668
+ grameneMaps: props.grameneMaps,
21669
+ targetTaxonId: props.targetTaxonId,
21670
+ onAdd: props.doAddGrameneInterval
21671
+ });
21426
21672
  const containerClass = props.configuration.id === 'sorghum' ? 'sorghumbase-filter-container' : 'gramene-filter-container';
21427
21673
  if (props.grameneFilters.rightIdx > 1) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21428
21674
  className: containerClass,
21429
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$CollapsibleSection, {
21675
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)($693dd8c7a5607c3a$var$CollapsibleSection, {
21430
21676
  title: "Filters",
21431
21677
  extra: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
21432
21678
  style: {
@@ -21439,26 +21685,32 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
21439
21685
  },
21440
21686
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsTrash), {})
21441
21687
  }),
21442
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$Filter, {
21443
- node: props.grameneFilters,
21444
- moveCopyMode: props.grameneFilters.moveCopyMode,
21445
- showMarked: props.grameneFilters.showMarked,
21446
- actions: actions
21447
- })
21688
+ children: [
21689
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$Filter, {
21690
+ node: props.grameneFilters,
21691
+ moveCopyMode: props.grameneFilters.moveCopyMode,
21692
+ showMarked: props.grameneFilters.showMarked,
21693
+ actions: actions
21694
+ }),
21695
+ intervalForm
21696
+ ]
21448
21697
  })
21449
21698
  });
21450
21699
  else return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21451
21700
  className: containerClass,
21452
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$CollapsibleSection, {
21701
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)($693dd8c7a5607c3a$var$CollapsibleSection, {
21453
21702
  title: "Filters",
21454
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21455
- className: "gramene-filter gramene-filter-AND",
21456
- children: "No filters defined"
21457
- })
21703
+ children: [
21704
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21705
+ className: "gramene-filter gramene-filter-AND",
21706
+ children: "No filters defined"
21707
+ }),
21708
+ intervalForm
21709
+ ]
21458
21710
  })
21459
21711
  });
21460
21712
  };
21461
- const $693dd8c7a5607c3a$export$92a576f896a9bf72 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneFilters', 'selectConfiguration', 'doNegateGrameneFilter', 'doDeleteGrameneFilter', 'doChangeGrameneFilterOperation', 'doExpandGrameneFilter', 'doRemoveGrameneExpansion', 'doMoveOrCopyGrameneFilter', 'doMarkGrameneFilterTargets', 'doUnmarkGrameneFilterTargets', 'doToggleGrameneFilterMenu', 'doClearGrameneFilters', $693dd8c7a5607c3a$var$FiltersCmp);
21713
+ const $693dd8c7a5607c3a$export$92a576f896a9bf72 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneFilters', 'selectConfiguration', 'doNegateGrameneFilter', 'doDeleteGrameneFilter', 'doChangeGrameneFilterOperation', 'doExpandGrameneFilter', 'doRemoveGrameneExpansion', 'doMoveOrCopyGrameneFilter', 'doMarkGrameneFilterTargets', 'doUnmarkGrameneFilterTargets', 'doToggleGrameneFilterMenu', 'selectGrameneMaps', 'selectTargetTaxonId', 'doAddGrameneInterval', 'doClearGrameneFilters', $693dd8c7a5607c3a$var$FiltersCmp);
21462
21714
  const $693dd8c7a5607c3a$var$ResultsCmp = (props)=>{
21463
21715
  let activeViews = props.grameneViews.options;
21464
21716
  return props.grameneFilters.rightIdx > 0 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {