gramene-search 2.0.2 → 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/.claude/settings.local.json +8 -1
- package/.parcel-cache/83e7562660f7cc15-BundleGraph +0 -0
- package/.parcel-cache/d3a1b9507cb44047-AssetGraph +0 -0
- package/.parcel-cache/data.mdb +0 -0
- package/.parcel-cache/dc1da35000e13623-RequestGraph +0 -0
- package/.parcel-cache/lock.mdb +0 -0
- package/.parcel-cache/snapshot-dc1da35000e13623.txt +2 -2
- package/dist/index.css +431 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +3555 -1119
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bundles/api.js +33 -26
- package/src/bundles/exporter.js +4 -1
- package/src/bundles/exprViz.js +422 -0
- package/src/bundles/index.js +2 -1
- package/src/bundles/swaggerFields.js +13 -0
- package/src/bundles/views.js +30 -12
- package/src/components/exprViz/ExprTable.js +339 -0
- package/src/components/exprViz/ExprVizView.js +400 -0
- package/src/components/exprViz/FieldsModal.js +441 -0
- package/src/components/exprViz/ParallelCoordsPlot.js +407 -0
- package/src/components/exprViz/styles.css +471 -0
- package/src/components/geneSearchUI.js +2 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { AgGridReact } from 'ag-grid-react';
|
|
3
|
+
import 'ag-grid-community/styles/ag-grid.css';
|
|
4
|
+
import 'ag-grid-community/styles/ag-theme-quartz.css';
|
|
5
|
+
|
|
6
|
+
const baseColDefs = [
|
|
7
|
+
{ field: 'id', headerName: 'Gene ID', pinned: 'left', width: 160, suppressMovable: true },
|
|
8
|
+
{ field: 'name', headerName: 'Name', pinned: 'left', width: 140, suppressMovable: true }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function arraysEqual(a, b) {
|
|
12
|
+
if (a.length !== b.length) return false;
|
|
13
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildFieldInfo(fields, studies, expressionSamples) {
|
|
18
|
+
const info = {};
|
|
19
|
+
if (!fields || !studies || !expressionSamples) return info;
|
|
20
|
+
const wanted = new Set(fields);
|
|
21
|
+
for (const study of studies) {
|
|
22
|
+
const studyId = study._id;
|
|
23
|
+
const samples = expressionSamples[studyId];
|
|
24
|
+
if (!samples) continue;
|
|
25
|
+
const byGroup = {};
|
|
26
|
+
for (const s of samples) if (!byGroup[s.group]) byGroup[s.group] = s;
|
|
27
|
+
for (const group of Object.keys(byGroup)) {
|
|
28
|
+
const fieldName = `${studyId.replace(/-/g, '_')}_${group}__expr`;
|
|
29
|
+
if (!wanted.has(fieldName)) continue;
|
|
30
|
+
const sample = byGroup[group];
|
|
31
|
+
const factors = {};
|
|
32
|
+
(sample.factor || []).forEach(f => { factors[f.type] = f.label; });
|
|
33
|
+
const characteristics = {};
|
|
34
|
+
(sample.characteristic || []).forEach(c => {
|
|
35
|
+
if (factors[c.type] != null) return;
|
|
36
|
+
characteristics[c.type] = c.label;
|
|
37
|
+
});
|
|
38
|
+
info[fieldName] = {
|
|
39
|
+
studyId,
|
|
40
|
+
studyDescription: study.description || studyId,
|
|
41
|
+
group,
|
|
42
|
+
replicates: samples.filter(s => s.group === group).length,
|
|
43
|
+
factors,
|
|
44
|
+
characteristics
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return info;
|
|
49
|
+
}
|
|
50
|
+
|
|
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
|
+
};
|
|
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;
|
|
64
|
+
return (
|
|
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}` : ''}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
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
|
+
|
|
282
|
+
const ExprTable = ({ rows, fields, onReorder, studies, expressionSamples, onHoverRow }) => {
|
|
283
|
+
const fieldInfo = useMemo(
|
|
284
|
+
() => buildFieldInfo(fields, studies, expressionSamples),
|
|
285
|
+
[fields, studies, expressionSamples]
|
|
286
|
+
);
|
|
287
|
+
|
|
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
|
+
);
|
|
303
|
+
|
|
304
|
+
const onColumnMoved = (e) => {
|
|
305
|
+
if (!onReorder || !e.finished) return;
|
|
306
|
+
const allCols = e.api.getAllGridColumns ? e.api.getAllGridColumns() : (e.columnApi && e.columnApi.getAllGridColumns && e.columnApi.getAllGridColumns());
|
|
307
|
+
if (!allCols) return;
|
|
308
|
+
const next = allCols
|
|
309
|
+
.map(c => c.getColId())
|
|
310
|
+
.filter(id => fields.includes(id));
|
|
311
|
+
if (!arraysEqual(next, fields)) {
|
|
312
|
+
onReorder(next);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
if (!rows || rows.length === 0) {
|
|
317
|
+
return <div className="exprviz-table-empty"><em>No data loaded.</em></div>;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<div className="ag-theme-quartz exprviz-aggrid">
|
|
322
|
+
<AgGridReact
|
|
323
|
+
rowData={rows}
|
|
324
|
+
columnDefs={columnDefs}
|
|
325
|
+
defaultColDef={{ resizable: true, sortable: true, filter: false, suppressMenu: true }}
|
|
326
|
+
animateRows={false}
|
|
327
|
+
suppressFieldDotNotation={true}
|
|
328
|
+
suppressDragLeaveHidesColumns={true}
|
|
329
|
+
suppressColumnVirtualisation={true}
|
|
330
|
+
groupHeaderHeight={24}
|
|
331
|
+
onColumnMoved={onColumnMoved}
|
|
332
|
+
onCellMouseOver={e => onHoverRow && onHoverRow(e.data && e.data.id)}
|
|
333
|
+
onCellMouseOut={() => onHoverRow && onHoverRow(null)}
|
|
334
|
+
/>
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export default ExprTable;
|