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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gramene-search",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "search wrapper for gramene",
5
5
  "source": "src/index.js",
6
6
  "main": "dist/index.js",
@@ -172,12 +172,15 @@ const expressionStudies = createAsyncResourceBundle( {
172
172
  .then(res => _.groupBy(res, 'taxon_id'))
173
173
  }
174
174
  });
175
+ // Only fetch when a view that consumes expression studies is enabled.
176
+ const EXPRESSION_VIEWS = ['exprViz', 'expression', 'export'];
175
177
  expressionStudies.reactExpressionStudies = createSelector(
176
178
  'selectExpressionStudiesShouldUpdate',
177
- (shouldUpdate) => {
178
- if (shouldUpdate) {
179
- return { actionCreator: 'doFetchExpressionStudies' }
180
- }
179
+ 'selectGrameneViewsOn',
180
+ (shouldUpdate, viewsOn) => {
181
+ if (!shouldUpdate) return;
182
+ if (!viewsOn || !EXPRESSION_VIEWS.some(id => viewsOn.has(id))) return;
183
+ return { actionCreator: 'doFetchExpressionStudies' }
181
184
  }
182
185
  );
183
186
 
@@ -202,10 +205,11 @@ const expressionSamples = createAsyncResourceBundle( {
202
205
  });
203
206
  expressionSamples.reactExpressionSamples = createSelector(
204
207
  'selectExpressionSamplesShouldUpdate',
205
- (shouldUpdate) => {
206
- if (shouldUpdate) {
207
- return { actionCreator: 'doFetchExpressionSamples' }
208
- }
208
+ 'selectGrameneViewsOn',
209
+ (shouldUpdate, viewsOn) => {
210
+ if (!shouldUpdate) return;
211
+ if (!viewsOn || !EXPRESSION_VIEWS.some(id => viewsOn.has(id))) return;
212
+ return { actionCreator: 'doFetchExpressionSamples' }
209
213
  }
210
214
  );
211
215
 
@@ -219,12 +223,15 @@ const curatedGenes = createAsyncResourceBundle( {
219
223
  .then(curation => _.keyBy(curation.genes, 'gene_id'))
220
224
  }
221
225
  });
226
+ // Curated annotations are consumed by GeneList rows and Homology details,
227
+ // both inside the gene-list view.
222
228
  curatedGenes.reactCuratedGenes = createSelector(
223
229
  'selectCuratedGenesShouldUpdate',
224
- (shouldUpdate) => {
225
- if (shouldUpdate) {
226
- return { actionCreator: 'doFetchCuratedGenes' }
227
- }
230
+ 'selectGrameneViewsOn',
231
+ (shouldUpdate, viewsOn) => {
232
+ if (!shouldUpdate) return;
233
+ if (!viewsOn || !viewsOn.has('list')) return;
234
+ return { actionCreator: 'doFetchCuratedGenes' }
228
235
  }
229
236
  );
230
237
 
@@ -245,12 +252,14 @@ const grameneGermplasm = createAsyncResourceBundle( {
245
252
  })
246
253
  }
247
254
  });
255
+ // Germplasm metadata is consumed by the VEP detail panel inside the gene list.
248
256
  grameneGermplasm.reactGrameneGermplasm = createSelector(
249
257
  'selectGrameneGermplasmShouldUpdate',
250
- (shouldUpdate) => {
251
- if (shouldUpdate) {
252
- return { actionCreator: 'doFetchGrameneGermplasm' }
253
- }
258
+ 'selectGrameneViewsOn',
259
+ (shouldUpdate, viewsOn) => {
260
+ if (!shouldUpdate) return;
261
+ if (!viewsOn || !viewsOn.has('list')) return;
262
+ return { actionCreator: 'doFetchGrameneGermplasm' }
254
263
  }
255
264
  );
256
265
  //
@@ -334,14 +343,12 @@ const grameneGeneAttribs = createAsyncResourceBundle( {
334
343
  grameneGeneAttribs.reactGrameneGeneAttribs = createSelector(
335
344
  'selectGrameneGeneAttribsShouldUpdate',
336
345
  'selectGrameneFiltersStatus',
337
- 'selectGrameneViews',
338
- (shouldUpdate, status, views) => {
339
- if (shouldUpdate && (status === 'finished' || status === 'ready')) {
340
- const byId = _.keyBy(views.options,'id');
341
- if (byId.attribs.show === "on") {
342
- return { actionCreator: 'doFetchGrameneGeneAttribs' }
343
- }
344
- }
346
+ 'selectGrameneViewsOn',
347
+ (shouldUpdate, status, viewsOn) => {
348
+ if (!shouldUpdate) return;
349
+ if (status !== 'finished' && status !== 'ready') return;
350
+ if (!viewsOn || !viewsOn.has('attribs')) return;
351
+ return { actionCreator: 'doFetchGrameneGeneAttribs' }
345
352
  }
346
353
  );
347
354
 
@@ -353,8 +360,8 @@ const grameneSearch = createAsyncResourceBundle({
353
360
  const offset = store.selectGrameneSearchOffset();
354
361
  const rows = store.selectGrameneSearchRows();
355
362
  const g = store.selectGrameneGenomes();
356
- const m = store.selectGrameneMaps();
357
- const taxa = Object.keys(g.active).filter(tid => !m[tid].hidden);
363
+ const m = store.selectGrameneMaps() || {};
364
+ const taxa = Object.keys((g && g.active) || {}).filter(tid => m[tid] && !m[tid].hidden);
358
365
  let fq='';
359
366
  if (taxa.length) {
360
367
  console.log('search add a fq for ',taxa);
@@ -181,10 +181,13 @@ const exporter = {
181
181
  'selectGrameneFiltersQueryString',
182
182
  'selectGrameneGenomes',
183
183
  'selectGrameneMaps',
184
- (exp, filtersStatus, q, g, m) => {
184
+ 'selectGrameneViewsOn',
185
+ (exp, filtersStatus, q, g, m, viewsOn) => {
185
186
  if (!exp || !exp.preview) return;
186
187
  if (filtersStatus === 'init') return;
187
188
  if (exp.preview.status === 'loading') return;
189
+ // Don't run the preview query unless the user has the exporter view open.
190
+ if (!viewsOn || !viewsOn.has('export')) return;
188
191
  const maps = m || {};
189
192
  const taxa = Object.keys((g && g.active) || {}).filter(tid => maps[tid] && !maps[tid].hidden);
190
193
  const sig = `${q}|${taxa.sort().join(',')}`;
@@ -188,6 +188,19 @@ const grameneFieldCatalog = createAsyncResourceBundle({
188
188
  }
189
189
  });
190
190
 
191
+ // Auto-fetch the field catalog only when a view that needs it (exprViz fields
192
+ // modal, exporter field tree) is enabled. Other views never read it.
193
+ const FIELD_CATALOG_VIEWS = ['exprViz', 'export'];
194
+ grameneFieldCatalog.reactGrameneFieldCatalog = createSelector(
195
+ 'selectGrameneFieldCatalogShouldUpdate',
196
+ 'selectGrameneViewsOn',
197
+ (shouldUpdate, viewsOn) => {
198
+ if (!shouldUpdate) return;
199
+ if (!viewsOn || !FIELD_CATALOG_VIEWS.some(id => viewsOn.has(id))) return;
200
+ return { actionCreator: 'doFetchGrameneFieldCatalog' };
201
+ }
202
+ );
203
+
191
204
  function assayFactorLabel(assay) {
192
205
  if (!assay) return '';
193
206
  const factors = Array.isArray(assay.factor) ? assay.factor : [];
@@ -149,6 +149,18 @@ const grameneViews = {
149
149
  }
150
150
  return { ...raw, options };
151
151
  }
152
+ ),
153
+ // Set of view ids whose `show` is currently 'on'. Used by data bundles to
154
+ // gate their auto-fetch reactors so we don't pay the cost of loading data
155
+ // for views the user has turned off.
156
+ selectGrameneViewsOn: createSelector(
157
+ 'selectGrameneViews',
158
+ (views) => {
159
+ const ids = new Set();
160
+ const opts = (views && views.options) || [];
161
+ for (const v of opts) if (v && v.show === 'on') ids.add(v.id);
162
+ return ids;
163
+ }
152
164
  )
153
165
  };
154
166
 
@@ -1,12 +1,11 @@
1
- import React, { useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
2
  import { AgGridReact } from 'ag-grid-react';
3
- import { OverlayTrigger, Popover } from 'react-bootstrap';
4
3
  import 'ag-grid-community/styles/ag-grid.css';
5
4
  import 'ag-grid-community/styles/ag-theme-quartz.css';
6
5
 
7
6
  const baseColDefs = [
8
7
  { field: 'id', headerName: 'Gene ID', pinned: 'left', width: 160, suppressMovable: true },
9
- { field: 'name', headerName: 'Name', width: 140, suppressMovable: true }
8
+ { field: 'name', headerName: 'Name', pinned: 'left', width: 140, suppressMovable: true }
10
9
  ];
11
10
 
12
11
  function arraysEqual(a, b) {
@@ -49,114 +48,258 @@ function buildFieldInfo(fields, studies, expressionSamples) {
49
48
  return info;
50
49
  }
51
50
 
52
- // Custom header: re-implements sort click + menu button so ag-grid's column
53
- // drag/menu/filter still work, with an info icon as the popover trigger.
54
- const HeaderWithInfo = (props) => {
55
- const { displayName, enableSorting, enableMenu, showColumnMenu, progressSort, column, info } = props;
56
- const [sort, setSort] = useState(column && column.getSort ? column.getSort() : null);
57
- const menuRef = useRef(null);
58
-
59
- useEffect(() => {
60
- if (!column || !column.addEventListener) return;
61
- const handler = () => setSort(column.getSort());
62
- column.addEventListener('sortChanged', handler);
63
- return () => column.removeEventListener('sortChanged', handler);
64
- }, [column]);
65
-
66
- const onSortClick = (event) => {
67
- if (enableSorting && progressSort) progressSort(event.shiftKey);
68
- };
69
-
70
- const onMenuClick = (event) => {
71
- event.stopPropagation();
72
- if (showColumnMenu && menuRef.current) showColumnMenu(menuRef.current);
73
- };
74
-
75
- const factorEntries = info ? Object.entries(info.factors || {}) : [];
76
- const charEntries = info ? Object.entries(info.characteristics || {}) : [];
77
- const popover = info ? (
78
- <Popover id={`exprviz-header-${info.studyId}-${info.group}`} className="exprviz-header-popover">
79
- <Popover.Header as="h6">{info.studyDescription}</Popover.Header>
80
- <Popover.Body>
81
- <div><strong>Study:</strong> {info.studyId}</div>
82
- <div><strong>Group:</strong> {info.group} ({info.replicates} {info.replicates === 1 ? 'rep' : 'reps'})</div>
83
- {factorEntries.length > 0 && (
84
- <div className="exprviz-header-section">
85
- <strong>Factors</strong>
86
- <ul>{factorEntries.map(([t, v]) => <li key={t}><em>{t}:</em> {v}</li>)}</ul>
87
- </div>
88
- )}
89
- {charEntries.length > 0 && (
90
- <div className="exprviz-header-section">
91
- <strong>Characteristics</strong>
92
- <ul>{charEntries.map(([t, v]) => <li key={t}><em>{t}:</em> {v}</li>)}</ul>
93
- </div>
94
- )}
95
- </Popover.Body>
96
- </Popover>
97
- ) : null;
51
+ // Plain right-aligned label cell for the metadata header rows above the
52
+ // Gene ID / Name columns. Rendered as a React component (instead of
53
+ // relying on ag-grid's default group label) so the alignment is controlled
54
+ // by our own DOM and isn't fighting the quartz theme's CSS.
55
+ const LabelHeaderGroup = (props) => {
56
+ const { displayName } = props;
57
+ return <div className="exprviz-label-header">{displayName}</div>;
58
+ };
98
59
 
60
+ // Same idea, but for the cells that toggle a section between expanded and
61
+ // collapsed. The caret + label sit at the right edge.
62
+ const ToggleHeaderGroup = (props) => {
63
+ const { displayName, onToggle, expanded, suffix } = props;
99
64
  return (
100
- <div className="exprviz-header">
101
- <span
102
- className="exprviz-header-text"
103
- onClick={onSortClick}
104
- style={{ cursor: enableSorting ? 'pointer' : 'default' }}
105
- >
106
- {displayName}
107
- {sort === 'asc' && <span className="exprviz-header-sort"> ▲</span>}
108
- {sort === 'desc' && <span className="exprviz-header-sort"> ▼</span>}
109
- </span>
110
- {info && (
111
- <OverlayTrigger
112
- trigger={['hover', 'focus']}
113
- placement="bottom"
114
- overlay={popover}
115
- delay={{ show: 200, hide: 100 }}
116
- >
117
- <span
118
- className="exprviz-header-info"
119
- onClick={e => e.stopPropagation()}
120
- onMouseDown={e => e.stopPropagation()}
121
- aria-label="More info"
122
- >ⓘ</span>
123
- </OverlayTrigger>
124
- )}
125
- {enableMenu && (
126
- <span
127
- ref={menuRef}
128
- className="exprviz-header-menu ag-icon ag-icon-menu"
129
- onClick={onMenuClick}
130
- />
131
- )}
65
+ <div
66
+ className="exprviz-toggle-header"
67
+ onClick={onToggle}
68
+ role="button"
69
+ tabIndex={0}
70
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle && onToggle(); } }}
71
+ title={expanded ? 'Click to collapse' : 'Click to expand'}
72
+ >
73
+ <span className="exprviz-toggle-caret">{expanded ? '▼' : '▶'}</span>
74
+ {' '}{displayName}{suffix ? ` ${suffix}` : ''}
132
75
  </div>
133
76
  );
134
77
  };
135
78
 
79
+ function exprValueFormatter(p) {
80
+ const v = p.value;
81
+ if (v == null) return '';
82
+ if (Array.isArray(v)) return v.join(', ');
83
+ if (typeof v === 'object') return JSON.stringify(v);
84
+ return String(v);
85
+ }
86
+
87
+ // Discover every distinct factor and characteristic type across the loaded
88
+ // fields. Each becomes one header row (with adjacent equal-value cells
89
+ // merged). Order is alphabetical for stability across reloads.
90
+ function collectMetadataTypes(fields, fieldInfo) {
91
+ const factorTypes = new Set();
92
+ const charTypes = new Set();
93
+ for (const f of fields || []) {
94
+ const info = fieldInfo[f];
95
+ if (!info) continue;
96
+ Object.keys(info.factors || {}).forEach(t => factorTypes.add(t));
97
+ Object.keys(info.characteristics || {}).forEach(t => charTypes.add(t));
98
+ }
99
+ return {
100
+ factorTypes: Array.from(factorTypes).sort(),
101
+ charTypes: Array.from(charTypes).sort()
102
+ };
103
+ }
104
+
105
+ // Wrap a leaf column in N nested column groups so it participates at every
106
+ // header row, with a custom label per level. Lets the Gene ID / Name columns
107
+ // carry the row labels ("Study"/"Factor"/"Characteristic" and the type name).
108
+ function wrapLeafWithLabels(leaf, labels) {
109
+ let wrapped = leaf;
110
+ for (let i = labels.length - 1; i >= 0; i--) {
111
+ const label = labels[i];
112
+ const cls = label.headerClass
113
+ ? `${label.headerClass} exprviz-hg-labels`
114
+ : 'exprviz-hg-labels';
115
+ wrapped = {
116
+ headerName: label.headerName,
117
+ headerClass: cls,
118
+ // Always render through one of our components so the alignment and
119
+ // spacing are controlled by our own DOM, not ag-grid's defaults.
120
+ headerGroupComponent: label.headerGroupComponent || LabelHeaderGroup,
121
+ headerGroupComponentParams: label.headerGroupComponentParams,
122
+ children: [wrapped]
123
+ };
124
+ }
125
+ return wrapped;
126
+ }
127
+
128
+ // Build the column tree:
129
+ // row 1 — Study | Title | <study description>…
130
+ // row 2..(2+F-1) — Factor | <factor type> | <value or blank>…
131
+ // row 2+F..end-1 — Characteristic | <char type> | <value or blank>…
132
+ // leaf row — Gene ID | Name | g3, g4, …
133
+ // Adjacent expression cells sharing the same value at a given level merge
134
+ // (because they're children of one group definition). Blank values for a
135
+ // type that's defined only in other studies render as empty cells, and
136
+ // adjacent blanks under the same parent merge automatically.
137
+ function buildColumnDefs(fields, fieldInfo, expanded, toggles) {
138
+ if (!fields || fields.length === 0) return baseColDefs;
139
+ const { factorTypes, charTypes } = collectMetadataTypes(fields, fieldInfo);
140
+
141
+ const levels = [{ kind: 'study', getValue: (info) => (info && info.studyDescription) || '' }];
142
+ if (expanded.factors) {
143
+ for (const t of factorTypes) {
144
+ levels.push({
145
+ kind: 'factor', type: t,
146
+ getValue: (info) => (info && info.factors && info.factors[t]) || '',
147
+ headerClass: 'exprviz-hg-factors'
148
+ });
149
+ }
150
+ } else if (factorTypes.length > 0) {
151
+ // One placeholder level standing in for the collapsed factor rows.
152
+ levels.push({
153
+ kind: 'factors-collapsed',
154
+ getValue: () => '',
155
+ headerClass: 'exprviz-hg-factors exprviz-hg-collapsed'
156
+ });
157
+ }
158
+ if (expanded.chars) {
159
+ for (const t of charTypes) {
160
+ levels.push({
161
+ kind: 'char', type: t,
162
+ getValue: (info) => (info && info.characteristics && info.characteristics[t]) || '',
163
+ headerClass: 'exprviz-hg-chars'
164
+ });
165
+ }
166
+ } else if (charTypes.length > 0) {
167
+ levels.push({
168
+ kind: 'chars-collapsed',
169
+ getValue: () => '',
170
+ headerClass: 'exprviz-hg-chars exprviz-hg-collapsed'
171
+ });
172
+ }
173
+
174
+ // Walk fields, opening a new group at level i (and resetting all levels
175
+ // below) the first time the value at level i differs from the previous
176
+ // field's. Same value → same parent group → ag-grid merges the cells.
177
+ const exprTopGroups = [];
178
+ const currentGroups = new Array(levels.length).fill(null);
179
+ const currentKeys = new Array(levels.length).fill(undefined);
180
+
181
+ for (const f of fields) {
182
+ const info = fieldInfo[f] || {};
183
+ let firstChange = levels.length;
184
+ for (let i = 0; i < levels.length; i++) {
185
+ const key = levels[i].getValue(info);
186
+ if (currentGroups[i] === null || currentKeys[i] !== key) {
187
+ firstChange = i;
188
+ break;
189
+ }
190
+ }
191
+ for (let i = firstChange; i < levels.length; i++) {
192
+ const key = levels[i].getValue(info);
193
+ const group = {
194
+ headerName: key,
195
+ headerClass: levels[i].headerClass,
196
+ children: []
197
+ };
198
+ currentGroups[i] = group;
199
+ currentKeys[i] = key;
200
+ if (i === 0) exprTopGroups.push(group);
201
+ else currentGroups[i - 1].children.push(group);
202
+ }
203
+
204
+ const leaf = {
205
+ field: f,
206
+ headerName: info.group || f.replace(/__expr$/, ''),
207
+ width: 160,
208
+ suppressMovable: false,
209
+ valueFormatter: exprValueFormatter
210
+ };
211
+ currentGroups[levels.length - 1].children.push(leaf);
212
+ }
213
+
214
+ // Labels for the two pinned columns. The leftmost column carries the row
215
+ // category ("Study" / "Factor" / "Characteristic"); the next column shows
216
+ // the specific row name (the literal "Title" for the study row, then each
217
+ // factor/characteristic type name). The first row of each
218
+ // factor/characteristic section gets a clickable caret that toggles the
219
+ // section between expanded (one row per type) and collapsed (one
220
+ // placeholder row).
221
+ const studyLabels = [{ headerName: 'Study' }];
222
+ const titleLabels = [{ headerName: 'Title' }];
223
+
224
+ if (expanded.factors) {
225
+ factorTypes.forEach((t, i) => {
226
+ studyLabels.push({
227
+ headerName: 'Factor',
228
+ headerClass: 'exprviz-hg-factors',
229
+ ...(i === 0 ? {
230
+ headerGroupComponent: ToggleHeaderGroup,
231
+ headerGroupComponentParams: { onToggle: toggles.toggleFactors, expanded: true }
232
+ } : {})
233
+ });
234
+ titleLabels.push({ headerName: t, headerClass: 'exprviz-hg-factors' });
235
+ });
236
+ } else if (factorTypes.length > 0) {
237
+ studyLabels.push({
238
+ headerName: 'Factors',
239
+ headerClass: 'exprviz-hg-factors exprviz-hg-collapsed',
240
+ headerGroupComponent: ToggleHeaderGroup,
241
+ headerGroupComponentParams: {
242
+ onToggle: toggles.toggleFactors,
243
+ expanded: false,
244
+ suffix: `(${factorTypes.length})`
245
+ }
246
+ });
247
+ titleLabels.push({ headerName: '', headerClass: 'exprviz-hg-factors exprviz-hg-collapsed' });
248
+ }
249
+
250
+ if (expanded.chars) {
251
+ charTypes.forEach((t, i) => {
252
+ studyLabels.push({
253
+ headerName: 'Characteristic',
254
+ headerClass: 'exprviz-hg-chars',
255
+ ...(i === 0 ? {
256
+ headerGroupComponent: ToggleHeaderGroup,
257
+ headerGroupComponentParams: { onToggle: toggles.toggleChars, expanded: true }
258
+ } : {})
259
+ });
260
+ titleLabels.push({ headerName: t, headerClass: 'exprviz-hg-chars' });
261
+ });
262
+ } else if (charTypes.length > 0) {
263
+ studyLabels.push({
264
+ headerName: 'Characteristics',
265
+ headerClass: 'exprviz-hg-chars exprviz-hg-collapsed',
266
+ headerGroupComponent: ToggleHeaderGroup,
267
+ headerGroupComponentParams: {
268
+ onToggle: toggles.toggleChars,
269
+ expanded: false,
270
+ suffix: `(${charTypes.length})`
271
+ }
272
+ });
273
+ titleLabels.push({ headerName: '', headerClass: 'exprviz-hg-chars exprviz-hg-collapsed' });
274
+ }
275
+
276
+ const idCol = wrapLeafWithLabels(baseColDefs[0], studyLabels);
277
+ const nameCol = wrapLeafWithLabels(baseColDefs[1], titleLabels);
278
+
279
+ return [idCol, nameCol, ...exprTopGroups];
280
+ }
281
+
136
282
  const ExprTable = ({ rows, fields, onReorder, studies, expressionSamples, onHoverRow }) => {
137
283
  const fieldInfo = useMemo(
138
284
  () => buildFieldInfo(fields, studies, expressionSamples),
139
285
  [fields, studies, expressionSamples]
140
286
  );
141
287
 
142
- const columnDefs = useMemo(() => {
143
- const expressionCols = (fields || []).map(f => ({
144
- field: f,
145
- headerName: f.replace(/__expr$/, ''),
146
- width: 160,
147
- suppressMovable: false,
148
- headerComponent: HeaderWithInfo,
149
- headerComponentParams: { info: fieldInfo[f] },
150
- valueFormatter: p => {
151
- const v = p.value;
152
- if (v == null) return '';
153
- if (Array.isArray(v)) return v.join(', ');
154
- if (typeof v === 'object') return JSON.stringify(v);
155
- return String(v);
156
- }
157
- }));
158
- return [...baseColDefs, ...expressionCols];
159
- }, [fields, fieldInfo]);
288
+ // Default: factor rows expanded, characteristic rows collapsed (per spec).
289
+ const [expanded, setExpanded] = useState({ factors: true, chars: false });
290
+ const toggleFactors = useCallback(
291
+ () => setExpanded(e => ({ ...e, factors: !e.factors })),
292
+ []
293
+ );
294
+ const toggleChars = useCallback(
295
+ () => setExpanded(e => ({ ...e, chars: !e.chars })),
296
+ []
297
+ );
298
+
299
+ const columnDefs = useMemo(
300
+ () => buildColumnDefs(fields, fieldInfo, expanded, { toggleFactors, toggleChars }),
301
+ [fields, fieldInfo, expanded, toggleFactors, toggleChars]
302
+ );
160
303
 
161
304
  const onColumnMoved = (e) => {
162
305
  if (!onReorder || !e.finished) return;
@@ -179,10 +322,12 @@ const ExprTable = ({ rows, fields, onReorder, studies, expressionSamples, onHove
179
322
  <AgGridReact
180
323
  rowData={rows}
181
324
  columnDefs={columnDefs}
182
- defaultColDef={{ resizable: true, sortable: true, filter: true }}
325
+ defaultColDef={{ resizable: true, sortable: true, filter: false, suppressMenu: true }}
183
326
  animateRows={false}
184
327
  suppressFieldDotNotation={true}
185
328
  suppressDragLeaveHidesColumns={true}
329
+ suppressColumnVirtualisation={true}
330
+ groupHeaderHeight={24}
186
331
  onColumnMoved={onColumnMoved}
187
332
  onCellMouseOver={e => onHoverRow && onHoverRow(e.data && e.data.id)}
188
333
  onCellMouseOut={() => onHoverRow && onHoverRow(null)}