gramene-search 2.34.0 → 2.35.1

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/README.md CHANGED
@@ -46,4 +46,39 @@ Run the sorghum demo against a swagger that serves <code>/primers</code>:
46
46
  PRIMERS_API=https://data.sorghumbase.org/sorghum_v11a SUBSITE=sorghum npx parcel src/sorghum.html --port 1234
47
47
  ```
48
48
  In the sorghum demo (<code>src/demo.js</code>) the tab is shown only when <code>PRIMERS_API</code> is set;
49
- it becomes <code>details.primers.apiBase</code>.
49
+ it becomes <code>details.primers.apiBase</code>.
50
+
51
+ ## Expression tab
52
+ The Expression gene-detail tab's "All Studies" and "Paralogs" sub-tabs draw Expression Atlas heatmaps
53
+ (with the anatomogram) in the page using <code>ExpressionAtlasHeatmap</code> from
54
+ <a href="https://github.com/warelab/atlas-heatmap">gramene-atlas-heatmap</a>, warelab's React 18 fork of
55
+ EBI's atlas-heatmap; the anatomogram comes from
56
+ <a href="https://github.com/warelab/anatomogram">gramene-anatomogram</a>. They replace the old
57
+ iframe to <code>dev.gramene.org/static/atlasWidget.html</code>.
58
+
59
+ - <code>atlasUrl</code> in the site configuration names the gramene-swagger <code>/gxa/</code> instance to query,
60
+ e.g. <code>'https://data.sorghumbase.org/sorghum_v11/gxa/'</code>. Unset, the tab queries
61
+ <code>https://data.sorghumbase.org/auth_testing/gxa/</code>, the old widget's default.
62
+ - All Studies sends the gene's <code>atlas_id</code> (or its id); Paralogs sends the within-species paralogs
63
+ for the experiment picked in the selector.
64
+ - Links open in a new tab. <code>resolveUrl</code> in <code>Expression.js</code> points row, experiment,
65
+ "more information", Expression Atlas and download links at EBI, because gramene-swagger returns
66
+ relative gene links and an empty <code>geneQuery</code>.
67
+ - The chosen sub-tab and experiment are kept in <code>uiViewState.byGene[geneId].expression</code> and saved
68
+ with shared views.
69
+ - Styles are injected at runtime; Bootstrap 5 CSS and react-bootstrap 2 come from the host.
70
+
71
+ <code>gramene-atlas-heatmap</code> is a dependency (<code>^6.0.0</code>) and brings in
72
+ <code>gramene-anatomogram</code>. To try unreleased fork changes, install packed tarballs of both packages over
73
+ the registry versions (not <code>npm link</code> or <code>file:</code>, which load a second React); the next
74
+ <code>npm install</code> puts the registry versions back.
75
+ ```bash
76
+ cd ../anatomogram && npm run pack:local # gramene-anatomogram-3.0.0.tgz
77
+ cd ../atlas-heatmap && npm run pack:local # gramene-atlas-heatmap-6.0.0.tgz
78
+ cd ../gramene-search && npm install --no-save ../anatomogram/gramene-anatomogram-3.0.0.tgz \
79
+ ../atlas-heatmap/gramene-atlas-heatmap-6.0.0.tgz && rm -rf .parcel-cache*
80
+ ```
81
+ Run the sorghum demo against another atlas instance:
82
+ ```bash
83
+ ATLAS_URL=https://data.sorghumbase.org/sorghum_v11/gxa/ SUBSITE=sorghum npx parcel src/sorghum.html --port 1235
84
+ ```
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ var $gXNCa$reactswitch = require("react-switch");
17
17
  var $gXNCa$reacticonsio5 = require("react-icons/io5");
18
18
  var $gXNCa$reacticonsbs = require("react-icons/bs");
19
19
  var $gXNCa$reacticonsgr = require("react-icons/gr");
20
+ var $gXNCa$grameneatlasheatmap = require("gramene-atlas-heatmap");
20
21
  var $gXNCa$gramenegenetreevis = require("gramene-genetree-vis");
21
22
  var $gXNCa$gramenetreesclientsrcgenetree = require("gramene-trees-client/src/genetree");
22
23
  var $gXNCa$tbrowse = require("tbrowse");
@@ -7836,6 +7837,7 @@ var $541b8b0d8c5501d2$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
7836
7837
 
7837
7838
 
7838
7839
 
7840
+
7839
7841
  // eFP Browser embed — was its own package (gramene-efp-browser@1.0.11),
7840
7842
  // absorbed here on 2026-05-29 to drop the unmaintained nwb/webpack-4 build
7841
7843
  // pipeline and remove a dependency that needed --openssl-legacy-provider.
@@ -8056,156 +8058,181 @@ function $59a241f3912e631e$export$1bc0f3596ef2efe9(gene) {
8056
8058
  }
8057
8059
 
8058
8060
 
8059
- function $9e29a4f60318db7a$var$DynamicIframe(props) {
8060
- // Create a ref for the iframe element
8061
- const iframeRef = (0, $gXNCa$react.useRef)(null);
8062
- const [iframeHeight, setIframeHeight] = (0, $gXNCa$react.useState)(500); // Default height
8063
- (0, $gXNCa$react.useEffect)(()=>{
8064
- const handleMessage = (event)=>{
8065
- if (event.data.type === 'heightChange') setIframeHeight(event.data.height + 44);
8066
- };
8067
- window.addEventListener("message", handleMessage);
8068
- return ()=>window.removeEventListener("message", handleMessage);
8069
- }, []);
8070
- return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("iframe", {
8071
- ref: iframeRef,
8072
- src: props.url,
8073
- title: "Dynamic Iframe",
8074
- style: {
8075
- width: '100%',
8076
- height: `${iframeHeight}px`,
8077
- border: 'none'
8078
- }
8079
- });
8080
- }
8061
+ // "All Studies" and "Paralogs" draw Expression Atlas heatmaps in the page with
8062
+ // gramene-atlas-heatmap (warelab's React 18 fork of EBI's atlas-heatmap; the
8063
+ // anatomogram comes from gramene-anatomogram). configuration.atlasUrl names the
8064
+ // gramene-swagger /gxa/ instance to query; sites that leave it unset get the
8065
+ // instance the old dev.gramene.org iframe widget fell back to.
8066
+ const $9e29a4f60318db7a$var$DEFAULT_ATLAS_URL = 'https://data.sorghumbase.org/auth_testing/gxa/';
8067
+ const $9e29a4f60318db7a$var$EBI_GXA = 'https://www.ebi.ac.uk/gxa/';
8068
+ const $9e29a4f60318db7a$var$genesOf = (query)=>query && query.gene ? query.gene.split(' ') : [];
8069
+ const $9e29a4f60318db7a$var$editSearch = (url, edit)=>{
8070
+ try {
8071
+ const u = new URL(url);
8072
+ edit(u.searchParams);
8073
+ return u.href;
8074
+ } catch (e) {
8075
+ return undefined;
8076
+ }
8077
+ };
8078
+ // gramene-swagger echoes geneQuery back as [null,...], returns relative row uris
8079
+ // (genes/<id>) that only some instances redirect to EBI, and atlasUrl itself is
8080
+ // not a browsable page, so point the heatmap's links at EBI's Expression Atlas.
8081
+ // undefined keeps the heatmap's own URL.
8082
+ const $9e29a4f60318db7a$var$resolveUrl = (kind, url, context)=>{
8083
+ const genes = $9e29a4f60318db7a$var$genesOf(context.query);
8084
+ const withGenes = (params)=>params.set('geneQuery', JSON.stringify(genes.map((value)=>({
8085
+ value: value
8086
+ }))));
8087
+ switch(kind){
8088
+ case 'row':
8089
+ {
8090
+ const uri = context.row && context.row.uri;
8091
+ return uri && !/^https?:/i.test(uri) ? new URL(uri, $9e29a4f60318db7a$var$EBI_GXA).href : undefined;
8092
+ }
8093
+ case 'atlas':
8094
+ return $9e29a4f60318db7a$var$EBI_GXA;
8095
+ case 'experiment':
8096
+ return $9e29a4f60318db7a$var$editSearch(url, withGenes);
8097
+ case 'moreInformation':
8098
+ return context.experiment ? $9e29a4f60318db7a$var$editSearch(url, withGenes) : `${$9e29a4f60318db7a$var$EBI_GXA}genes/${encodeURIComponent(genes[0])}`;
8099
+ case 'download':
8100
+ return $9e29a4f60318db7a$var$editSearch(url, (params)=>params.delete('geneQuery'));
8101
+ default:
8102
+ return undefined;
8103
+ }
8104
+ };
8105
+ const $9e29a4f60318db7a$var$HEATMAP_OPTIONS = {
8106
+ showAnatomogram: true,
8107
+ isWidget: true,
8108
+ showControlMenu: true,
8109
+ linkTarget: '_blank',
8110
+ resolveUrl: $9e29a4f60318db7a$var$resolveUrl
8111
+ };
8112
+ const $9e29a4f60318db7a$var$studyLabel = (e)=>`${e.type}:${e.description || e._id}`;
8081
8113
  const $9e29a4f60318db7a$var$Detail = (props)=>{
8082
8114
  const geneId = props.searchResult.id;
8083
8115
  const gene = props.geneDocs[geneId];
8084
8116
  // User-selected view state (active sub-tab, chosen GXA experiment, chosen eFP
8085
8117
  // study) lives in the uiViewState bundle keyed by geneId, so the shareable-
8086
- // views snapshot can round-trip it. Fetched data + the dev-only Local API
8087
- // toggle stay in local state. Defaults match the old local-state initials.
8118
+ // views snapshot can round-trip it.
8088
8119
  const expr = props.uiViewState && props.uiViewState.byGene[geneId] && props.uiViewState.byGene[geneId].expression || {};
8089
8120
  const activeTab = expr.activeTab || 'gene';
8090
8121
  const atlasExperiment = expr.atlasExperiment || null;
8091
- const setActiveTab = (k)=>props.doSetExpressionState({
8122
+ const setExpression = (patch)=>props.doSetExpressionState({
8092
8123
  geneId: geneId,
8093
- patch: {
8094
- activeTab: k
8095
- }
8124
+ patch: patch
8096
8125
  });
8097
- const setAtlasExperiment = (v)=>props.doSetExpressionState({
8098
- geneId: geneId,
8099
- patch: {
8100
- atlasExperiment: v
8101
- }
8102
- });
8103
- const [atlasExperimentList, setAtlasExperimentList] = (0, $gXNCa$react.useState)([]);
8104
- const [atlasFacets, setAtlasFacets] = (0, $gXNCa$react.useState)(null);
8105
- const [isLocal, setIsLocal] = (0, $gXNCa$react.useState)(false);
8106
- const handleLocalAPIChange = (event)=>{
8107
- setIsLocal(event.target.checked);
8108
- };
8109
8126
  // The expressionStudies resource is otherwise fetched only when a top-level
8110
8127
  // expression view (exprViz/expression/export) is on — but this per-gene
8111
8128
  // Expression detail also needs it (the Paralogs sub-tab's experiment list and
8112
- // the atlasExperiment selection both derive from it). Self-fetch on mount so
8113
- // opening the detail populates the studies even when no such view is enabled.
8129
+ // the atlasExperiment selection both derive from it). Fetch it here when it is
8130
+ // missing or stale: it is persisted, and a browser that cached the list before
8131
+ // studies were added to the release would otherwise keep offering the old list.
8114
8132
  (0, $gXNCa$react.useEffect)(()=>{
8115
- if (!props.expressionStudies && props.doFetchExpressionStudies) props.doFetchExpressionStudies();
8133
+ if (props.expressionStudiesShouldUpdate && props.doFetchExpressionStudies) props.doFetchExpressionStudies();
8116
8134
  }, [
8117
- props.expressionStudies
8135
+ props.expressionStudiesShouldUpdate
8118
8136
  ]);
8137
+ const studies = props.expressionStudies && props.expressionStudies[Math.floor(gene.taxon_id / 1000)];
8138
+ const inGxa = props.searchResult.expressed_in_gxa_attr_ss;
8139
+ const experiments = (0, $gXNCa$react.useMemo)(()=>{
8140
+ if (!studies) return [];
8141
+ const inGxaSet = inGxa && new Set(inGxa);
8142
+ // copy before sorting: the studies array belongs to the expressionStudies bundle
8143
+ const list = inGxaSet ? studies.filter((e)=>inGxaSet.has(e._id)) : studies.slice();
8144
+ return list.sort((a, b)=>$9e29a4f60318db7a$var$studyLabel(a) < $9e29a4f60318db7a$var$studyLabel(b) ? -1 : 1);
8145
+ }, [
8146
+ studies,
8147
+ inGxa
8148
+ ]);
8149
+ // Only pick a default experiment when the user (or a restored snapshot)
8150
+ // hasn't already chosen one — otherwise we'd clobber a saved selection
8151
+ // the moment the studies list loads.
8119
8152
  (0, $gXNCa$react.useEffect)(()=>{
8120
- if (!props.expressionStudies) return;
8121
- const tid = Math.floor(gene.taxon_id / 1000);
8122
- if (props.expressionStudies[tid]) {
8123
- let facets = {
8124
- Differential: {},
8125
- Baseline: {}
8126
- };
8127
- let eList = props.expressionStudies[tid].sort((a, b)=>{
8128
- const a_name = `${a.type}:${a.description || a._id}`;
8129
- const b_name = `${b.type}:${b.description || b._id}`;
8130
- return a_name < b_name ? -1 : 1;
8131
- });
8132
- if (props.searchResult.hasOwnProperty('expressed_in_gxa_attr_ss')) {
8133
- const in_gxa = new Set(props.searchResult.expressed_in_gxa_attr_ss);
8134
- eList = eList.filter((e)=>in_gxa.has(e._id));
8135
- }
8136
- eList.forEach((e)=>{
8137
- e.factors.forEach((factor)=>facets[e.type][factor] = 1);
8138
- });
8139
- setAtlasExperimentList(eList);
8140
- setAtlasFacets(facets);
8141
- // Only pick a default experiment when the user (or a restored snapshot)
8142
- // hasn't already chosen one — otherwise we'd clobber a saved selection
8143
- // the moment the studies list loads.
8144
- if (!atlasExperiment) {
8145
- let refExp = eList.filter((e)=>e.isRef);
8146
- if (refExp.length === 1) setAtlasExperiment(refExp[0]._id);
8147
- else // no reference experiment - choose first
8148
- setAtlasExperiment(eList[0]._id);
8149
- }
8150
- }
8153
+ if (atlasExperiment || experiments.length === 0) return;
8154
+ const refExp = experiments.filter((e)=>e.isRef);
8155
+ setExpression({
8156
+ atlasExperiment: (refExp.length === 1 ? refExp[0] : experiments[0])._id
8157
+ });
8151
8158
  }, [
8152
- props.expressionStudies
8159
+ experiments,
8160
+ atlasExperiment
8153
8161
  ]);
8154
- // Which Expression Atlas instance the embedded widget should query. The
8155
- // widget falls back to its own default when `atlasUrl` is absent, so sites
8156
- // that don't configure one are unaffected — only pass it when set.
8157
- const atlasUrl = props.configuration && props.configuration.atlasUrl || null;
8158
- const atlasParam = atlasUrl ? `&atlasUrl=${encodeURIComponent(atlasUrl)}` : '';
8159
- let paralogs_url;
8160
- let gene_url = `https://dev.gramene.org/static/atlasWidget.html?genes=${gene.atlas_id || gene._id}&localAPI=${isLocal}${atlasParam}`;
8161
- let paralogs = [];
8162
- const haveParalogs = props.grameneParalogs && props.grameneParalogs[gene._id];
8163
- if (haveParalogs) paralogs = props.grameneParalogs[gene._id];
8162
+ const paralogs = props.grameneParalogs && props.grameneParalogs[gene._id];
8164
8163
  (0, $gXNCa$react.useEffect)(()=>{
8165
- if (!haveParalogs && gene.homology) props.doRequestParalogs(gene._id, gene.homology.supertree, gene.taxon_id);
8164
+ if (!paralogs && gene.homology) props.doRequestParalogs(gene._id, gene.homology.supertree, gene.taxon_id);
8166
8165
  }, [
8167
8166
  gene._id,
8168
- haveParalogs
8167
+ !!paralogs
8169
8168
  ]);
8170
- // if (gene.homology && gene.homology.homologous_genes && gene.homology.homologous_genes.within_species_paralog) {
8171
- // paralogs = gene.homology.homologous_genes.within_species_paralog;
8172
- // }
8173
- if (paralogs.length > 0 && atlasExperiment) paralogs_url = `https://dev.gramene.org/static/atlasWidget.html?genes=${paralogs.join(' ')}&experiment=${atlasExperiment}&localAPI=${isLocal}${atlasParam}`;
8169
+ const atlasUrl = props.configuration && props.configuration.atlasUrl || $9e29a4f60318db7a$var$DEFAULT_ATLAS_URL;
8170
+ const geneQuery = gene.atlas_id || gene._id;
8171
+ const paralogQuery = paralogs && paralogs.length > 0 ? paralogs.join(' ') : null;
8172
+ const allStudiesQuery = (0, $gXNCa$react.useMemo)(()=>({
8173
+ gene: geneQuery
8174
+ }), [
8175
+ geneQuery
8176
+ ]);
8177
+ const paralogsQuery = (0, $gXNCa$react.useMemo)(()=>({
8178
+ gene: paralogQuery
8179
+ }), [
8180
+ paralogQuery
8181
+ ]);
8182
+ const knownExperiment = experiments.some((e)=>e._id === atlasExperiment);
8183
+ // Only the active sub-tab's heatmap is mounted; the keys give each query its
8184
+ // own fetch, zoom and filters.
8174
8185
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Tabs), {
8175
8186
  activeKey: activeTab,
8176
- onSelect: (k)=>setActiveTab(k),
8187
+ onSelect: (k)=>setExpression({
8188
+ activeTab: k
8189
+ }),
8177
8190
  children: [
8178
- paralogs_url && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Tab), {
8191
+ paralogQuery && atlasExperiment && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Tab), {
8179
8192
  tabClassName: "gxa",
8180
8193
  eventKey: "paralogs",
8181
- title: `Paralogs`,
8194
+ title: "Paralogs",
8182
8195
  children: [
8183
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Select, {
8196
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Select, {
8197
+ className: "mb-2",
8184
8198
  "aria-label": "experiment selector",
8185
- placeholder: "Select experiment",
8186
- value: atlasExperiment || '',
8187
- onChange: (e)=>setAtlasExperiment(e.target.value),
8188
- children: atlasExperimentList.map((e, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("option", {
8189
- value: e._id,
8190
- children: [
8191
- e.type,
8192
- ": ",
8193
- e.description || e._id
8194
- ]
8195
- }, idx))
8199
+ value: atlasExperiment,
8200
+ onChange: (e)=>setExpression({
8201
+ atlasExperiment: e.target.value
8202
+ }),
8203
+ children: [
8204
+ !knownExperiment && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("option", {
8205
+ value: atlasExperiment,
8206
+ children: atlasExperiment
8207
+ }),
8208
+ experiments.map((e)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("option", {
8209
+ value: e._id,
8210
+ children: [
8211
+ e.type,
8212
+ ": ",
8213
+ e.description || e._id
8214
+ ]
8215
+ }, e._id))
8216
+ ]
8196
8217
  }),
8197
- activeTab === "paralogs" && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($9e29a4f60318db7a$var$DynamicIframe, {
8198
- url: paralogs_url
8199
- })
8218
+ activeTab === "paralogs" && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$grameneatlasheatmap.ExpressionAtlasHeatmap), {
8219
+ ...$9e29a4f60318db7a$var$HEATMAP_OPTIONS,
8220
+ atlasUrl: atlasUrl,
8221
+ query: paralogsQuery,
8222
+ experiment: atlasExperiment
8223
+ }, `${atlasUrl} ${atlasExperiment} ${paralogQuery}`)
8200
8224
  ]
8201
8225
  }, "gxaparalogs"),
8202
8226
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tab), {
8203
8227
  tabClassName: "gxa",
8204
8228
  eventKey: "gene",
8205
8229
  title: "All Studies",
8206
- children: activeTab === "gene" && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($9e29a4f60318db7a$var$DynamicIframe, {
8207
- url: gene_url
8208
- })
8230
+ children: activeTab === "gene" && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$grameneatlasheatmap.ExpressionAtlasHeatmap), {
8231
+ ...$9e29a4f60318db7a$var$HEATMAP_OPTIONS,
8232
+ atlasUrl: atlasUrl,
8233
+ query: allStudiesQuery,
8234
+ experiment: false
8235
+ }, `${atlasUrl} all ${geneQuery}`)
8209
8236
  }, "gxa"),
8210
8237
  (0, $59a241f3912e631e$export$1bc0f3596ef2efe9)(gene) && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tab), {
8211
8238
  tabClassName: "eFP",
@@ -8214,19 +8241,15 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
8214
8241
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $59a241f3912e631e$export$2e2bcd8739ae039), {
8215
8242
  gene: gene,
8216
8243
  study: expr.barStudy,
8217
- onStudyChange: (v)=>props.doSetExpressionState({
8218
- geneId: geneId,
8219
- patch: {
8220
- barStudy: v
8221
- }
8244
+ onStudyChange: (v)=>setExpression({
8245
+ barStudy: v
8222
8246
  })
8223
8247
  })
8224
8248
  }, "bar")
8225
8249
  ]
8226
8250
  });
8227
8251
  };
8228
- var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'selectGrameneParalogs', 'selectExpressionStudies', 'selectUiViewState', 'doRequestParalogs', 'doFetchExpressionStudies', 'doSetExpressionState', //'doRequestParalogExpression',
8229
- $9e29a4f60318db7a$var$Detail);
8252
+ var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'selectGrameneParalogs', 'selectExpressionStudies', 'selectExpressionStudiesShouldUpdate', 'selectUiViewState', 'doRequestParalogs', 'doFetchExpressionStudies', 'doSetExpressionState', $9e29a4f60318db7a$var$Detail);
8230
8253
 
8231
8254
 
8232
8255