gramene-search 2.0.1 → 2.0.4
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 +5 -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 +314 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +2029 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bundles/exprViz.js +422 -0
- package/src/bundles/index.js +2 -1
- package/src/bundles/views.js +23 -13
- package/src/components/exprViz/ExprTable.js +194 -0
- package/src/components/exprViz/ExprVizView.js +294 -0
- package/src/components/exprViz/FieldsModal.js +414 -0
- package/src/components/exprViz/ParallelCoordsPlot.js +314 -0
- package/src/components/exprViz/styles.css +316 -0
- package/src/components/geneSearchUI.js +2 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { AgGridReact } from 'ag-grid-react';
|
|
3
|
+
import { OverlayTrigger, Popover } from 'react-bootstrap';
|
|
4
|
+
import 'ag-grid-community/styles/ag-grid.css';
|
|
5
|
+
import 'ag-grid-community/styles/ag-theme-quartz.css';
|
|
6
|
+
|
|
7
|
+
const baseColDefs = [
|
|
8
|
+
{ field: 'id', headerName: 'Gene ID', pinned: 'left', width: 160, suppressMovable: true },
|
|
9
|
+
{ field: 'name', headerName: 'Name', width: 140, suppressMovable: true }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function arraysEqual(a, b) {
|
|
13
|
+
if (a.length !== b.length) return false;
|
|
14
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildFieldInfo(fields, studies, expressionSamples) {
|
|
19
|
+
const info = {};
|
|
20
|
+
if (!fields || !studies || !expressionSamples) return info;
|
|
21
|
+
const wanted = new Set(fields);
|
|
22
|
+
for (const study of studies) {
|
|
23
|
+
const studyId = study._id;
|
|
24
|
+
const samples = expressionSamples[studyId];
|
|
25
|
+
if (!samples) continue;
|
|
26
|
+
const byGroup = {};
|
|
27
|
+
for (const s of samples) if (!byGroup[s.group]) byGroup[s.group] = s;
|
|
28
|
+
for (const group of Object.keys(byGroup)) {
|
|
29
|
+
const fieldName = `${studyId.replace(/-/g, '_')}_${group}__expr`;
|
|
30
|
+
if (!wanted.has(fieldName)) continue;
|
|
31
|
+
const sample = byGroup[group];
|
|
32
|
+
const factors = {};
|
|
33
|
+
(sample.factor || []).forEach(f => { factors[f.type] = f.label; });
|
|
34
|
+
const characteristics = {};
|
|
35
|
+
(sample.characteristic || []).forEach(c => {
|
|
36
|
+
if (factors[c.type] != null) return;
|
|
37
|
+
characteristics[c.type] = c.label;
|
|
38
|
+
});
|
|
39
|
+
info[fieldName] = {
|
|
40
|
+
studyId,
|
|
41
|
+
studyDescription: study.description || studyId,
|
|
42
|
+
group,
|
|
43
|
+
replicates: samples.filter(s => s.group === group).length,
|
|
44
|
+
factors,
|
|
45
|
+
characteristics
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return info;
|
|
50
|
+
}
|
|
51
|
+
|
|
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;
|
|
98
|
+
|
|
99
|
+
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
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const ExprTable = ({ rows, fields, onReorder, studies, expressionSamples, onHoverRow }) => {
|
|
137
|
+
const fieldInfo = useMemo(
|
|
138
|
+
() => buildFieldInfo(fields, studies, expressionSamples),
|
|
139
|
+
[fields, studies, expressionSamples]
|
|
140
|
+
);
|
|
141
|
+
|
|
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]);
|
|
160
|
+
|
|
161
|
+
const onColumnMoved = (e) => {
|
|
162
|
+
if (!onReorder || !e.finished) return;
|
|
163
|
+
const allCols = e.api.getAllGridColumns ? e.api.getAllGridColumns() : (e.columnApi && e.columnApi.getAllGridColumns && e.columnApi.getAllGridColumns());
|
|
164
|
+
if (!allCols) return;
|
|
165
|
+
const next = allCols
|
|
166
|
+
.map(c => c.getColId())
|
|
167
|
+
.filter(id => fields.includes(id));
|
|
168
|
+
if (!arraysEqual(next, fields)) {
|
|
169
|
+
onReorder(next);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (!rows || rows.length === 0) {
|
|
174
|
+
return <div className="exprviz-table-empty"><em>No data loaded.</em></div>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className="ag-theme-quartz exprviz-aggrid">
|
|
179
|
+
<AgGridReact
|
|
180
|
+
rowData={rows}
|
|
181
|
+
columnDefs={columnDefs}
|
|
182
|
+
defaultColDef={{ resizable: true, sortable: true, filter: true }}
|
|
183
|
+
animateRows={false}
|
|
184
|
+
suppressFieldDotNotation={true}
|
|
185
|
+
suppressDragLeaveHidesColumns={true}
|
|
186
|
+
onColumnMoved={onColumnMoved}
|
|
187
|
+
onCellMouseOver={e => onHoverRow && onHoverRow(e.data && e.data.id)}
|
|
188
|
+
onCellMouseOut={() => onHoverRow && onHoverRow(null)}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export default ExprTable;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { connect } from 'redux-bundler-react';
|
|
3
|
+
import { Tabs, Tab, Button, ToggleButton, ToggleButtonGroup } from 'react-bootstrap';
|
|
4
|
+
import FieldsModal from './FieldsModal';
|
|
5
|
+
import ExprTable from './ExprTable';
|
|
6
|
+
import ParallelCoordsPlot from './ParallelCoordsPlot';
|
|
7
|
+
import './styles.css';
|
|
8
|
+
|
|
9
|
+
function speciesTaxonId(tid) {
|
|
10
|
+
const n = +tid;
|
|
11
|
+
return n > 1000000 ? Math.floor(n / 1000) : n;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function genomeName(grameneMaps, tid) {
|
|
15
|
+
if (!grameneMaps) return tid;
|
|
16
|
+
const direct = grameneMaps[tid];
|
|
17
|
+
if (direct && direct.display_name) return direct.display_name;
|
|
18
|
+
const sp = grameneMaps[speciesTaxonId(tid)];
|
|
19
|
+
if (sp && sp.display_name) return sp.display_name;
|
|
20
|
+
return tid;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ExprVizViewCmp = props => {
|
|
24
|
+
const {
|
|
25
|
+
exprVizPivot: pivot,
|
|
26
|
+
exprViz,
|
|
27
|
+
exprVizActiveTaxon: activeTaxon,
|
|
28
|
+
grameneMaps,
|
|
29
|
+
expressionStudies,
|
|
30
|
+
expressionSamples,
|
|
31
|
+
doSetExprVizActiveTaxon,
|
|
32
|
+
doToggleExprVizFieldsModal,
|
|
33
|
+
doFetchExprVizData
|
|
34
|
+
} = props;
|
|
35
|
+
|
|
36
|
+
const studiesFor = tid => {
|
|
37
|
+
if (!expressionStudies) return [];
|
|
38
|
+
return expressionStudies[tid] || expressionStudies[speciesTaxonId(tid)] || [];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const taxa = useMemo(() => {
|
|
42
|
+
const ids = Object.keys(pivot.data || {});
|
|
43
|
+
if (!grameneMaps) return ids;
|
|
44
|
+
return ids.sort((a, b) => {
|
|
45
|
+
const ma = grameneMaps[a] || grameneMaps[speciesTaxonId(a)];
|
|
46
|
+
const mb = grameneMaps[b] || grameneMaps[speciesTaxonId(b)];
|
|
47
|
+
return ((ma && ma.left_index) || 0) - ((mb && mb.left_index) || 0);
|
|
48
|
+
});
|
|
49
|
+
}, [pivot.data, grameneMaps]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (taxa.length === 0) return;
|
|
53
|
+
if (!activeTaxon || !taxa.includes(String(activeTaxon))) {
|
|
54
|
+
doSetExprVizActiveTaxon(taxa[0]);
|
|
55
|
+
}
|
|
56
|
+
}, [taxa, activeTaxon, doSetExprVizActiveTaxon]);
|
|
57
|
+
|
|
58
|
+
if (pivot.status === 'loading') {
|
|
59
|
+
return <div className="exprviz-view"><em>Loading studies…</em></div>;
|
|
60
|
+
}
|
|
61
|
+
if (pivot.status === 'error') {
|
|
62
|
+
return <div className="exprviz-view"><em>Error: {pivot.error}</em></div>;
|
|
63
|
+
}
|
|
64
|
+
if (taxa.length === 0) {
|
|
65
|
+
return <div className="exprviz-view"><em>No expression studies for current results.</em></div>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="exprviz-view">
|
|
70
|
+
<Tabs
|
|
71
|
+
activeKey={activeTaxon || taxa[0]}
|
|
72
|
+
onSelect={k => doSetExprVizActiveTaxon(k)}
|
|
73
|
+
className="exprviz-tabs"
|
|
74
|
+
>
|
|
75
|
+
{taxa.map(tid => {
|
|
76
|
+
const studies = studiesFor(tid);
|
|
77
|
+
const taxName = genomeName(grameneMaps, tid);
|
|
78
|
+
const geneCount = pivot.data[tid] || 0;
|
|
79
|
+
return (
|
|
80
|
+
<Tab
|
|
81
|
+
key={tid}
|
|
82
|
+
eventKey={tid}
|
|
83
|
+
title={`${taxName} (${studies.length} studies · ${geneCount} genes)`}
|
|
84
|
+
>
|
|
85
|
+
<TaxonPanel
|
|
86
|
+
taxon={tid}
|
|
87
|
+
studies={studies}
|
|
88
|
+
expressionSamples={expressionSamples}
|
|
89
|
+
tabState={exprViz.byTaxon[tid]}
|
|
90
|
+
onOpenFields={() => doToggleExprVizFieldsModal(tid, true)}
|
|
91
|
+
onLoad={() => doFetchExprVizData(tid)}
|
|
92
|
+
onReorder={(next) => props.doReorderExprVizFields(tid, next)}
|
|
93
|
+
onAddRangeQuery={props.doAddGrameneRangeQuery}
|
|
94
|
+
/>
|
|
95
|
+
</Tab>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</Tabs>
|
|
99
|
+
<FieldsModal/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function rowMatchesSelections(row, selections) {
|
|
105
|
+
for (const f of Object.keys(selections)) {
|
|
106
|
+
const v = row[f];
|
|
107
|
+
if (v == null || Array.isArray(v)) return false;
|
|
108
|
+
const n = +v;
|
|
109
|
+
if (!Number.isFinite(n)) return false;
|
|
110
|
+
const [lo, hi] = selections[f];
|
|
111
|
+
if (n < lo || n > hi) return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fmt(n) {
|
|
117
|
+
if (!Number.isFinite(n)) return String(n);
|
|
118
|
+
const a = Math.abs(n);
|
|
119
|
+
if (a !== 0 && (a < 0.001 || a >= 1e6)) return n.toExponential(3);
|
|
120
|
+
return Number(n.toFixed(4)).toString();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function tsvCell(v) {
|
|
124
|
+
if (v == null) return '';
|
|
125
|
+
if (Array.isArray(v)) return v.join(',');
|
|
126
|
+
const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
127
|
+
return s.replace(/[\t\r\n]+/g, ' ');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function downloadTsv(filename, rows, fields) {
|
|
131
|
+
const cols = ['id', 'name', ...fields];
|
|
132
|
+
const headerLabels = ['id', 'name', ...fields.map(f => f.replace(/__expr$/, ''))];
|
|
133
|
+
const lines = [headerLabels.join('\t')];
|
|
134
|
+
for (const r of rows) lines.push(cols.map(c => tsvCell(r[c])).join('\t'));
|
|
135
|
+
const blob = new Blob([lines.join('\n') + '\n'], { type: 'text/tab-separated-values' });
|
|
136
|
+
const url = URL.createObjectURL(blob);
|
|
137
|
+
const a = document.createElement('a');
|
|
138
|
+
a.href = url;
|
|
139
|
+
a.download = filename;
|
|
140
|
+
document.body.appendChild(a);
|
|
141
|
+
a.click();
|
|
142
|
+
document.body.removeChild(a);
|
|
143
|
+
URL.revokeObjectURL(url);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const TaxonPanel = ({ taxon, studies, expressionSamples, tabState, onOpenFields, onLoad, onReorder, onAddRangeQuery }) => {
|
|
147
|
+
const selected = (tabState && tabState.selectedFields) || [];
|
|
148
|
+
const rows = (tabState && tabState.rows) || [];
|
|
149
|
+
const fetchInfo = (tabState && tabState.fetch) || { status: 'idle', total: 0 };
|
|
150
|
+
const [scale, setScale] = useState('linear');
|
|
151
|
+
const [selections, setSelections] = useState({});
|
|
152
|
+
const [clearVersion, setClearVersion] = useState(0);
|
|
153
|
+
const [hoveredId, setHoveredId] = useState(null);
|
|
154
|
+
|
|
155
|
+
const hasBrush = Object.keys(selections).length > 0;
|
|
156
|
+
const filteredRows = useMemo(() => {
|
|
157
|
+
if (!hasBrush) return rows;
|
|
158
|
+
return rows.filter(r => rowMatchesSelections(r, selections));
|
|
159
|
+
}, [rows, selections, hasBrush]);
|
|
160
|
+
|
|
161
|
+
// Drop fields with no numeric data in the loaded rows so empty axes/columns
|
|
162
|
+
// don't clutter the visualization. Selected-but-empty fields stay in the
|
|
163
|
+
// underlying selection so a future load can repopulate them.
|
|
164
|
+
const visibleFields = useMemo(() => {
|
|
165
|
+
if (rows.length === 0 || selected.length === 0) return selected;
|
|
166
|
+
return selected.filter(f => rows.some(r => {
|
|
167
|
+
const v = r[f];
|
|
168
|
+
return v != null && !Array.isArray(v) && Number.isFinite(+v);
|
|
169
|
+
}));
|
|
170
|
+
}, [rows, selected]);
|
|
171
|
+
|
|
172
|
+
const handleReorder = onReorder
|
|
173
|
+
? (newVisibleOrder) => {
|
|
174
|
+
const visibleSet = new Set(newVisibleOrder);
|
|
175
|
+
const hidden = selected.filter(f => !visibleSet.has(f));
|
|
176
|
+
onReorder([...newVisibleOrder, ...hidden]);
|
|
177
|
+
}
|
|
178
|
+
: undefined;
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (rows.length === 0 && hasBrush) {
|
|
182
|
+
setSelections({});
|
|
183
|
+
setClearVersion(v => v + 1);
|
|
184
|
+
}
|
|
185
|
+
}, [rows.length, hasBrush]);
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div className="exprviz-tab-panel">
|
|
189
|
+
<div className="exprviz-toolbar">
|
|
190
|
+
<Button size="sm" onClick={onOpenFields}>
|
|
191
|
+
Select fields ({selected.length} selected, {studies.length} studies)
|
|
192
|
+
</Button>
|
|
193
|
+
<Button
|
|
194
|
+
size="sm"
|
|
195
|
+
variant="primary"
|
|
196
|
+
disabled={selected.length === 0 || fetchInfo.status === 'loading'}
|
|
197
|
+
onClick={onLoad}
|
|
198
|
+
>
|
|
199
|
+
{fetchInfo.status === 'loading' ? 'Loading…' : 'Load data'}
|
|
200
|
+
</Button>
|
|
201
|
+
<ToggleButtonGroup
|
|
202
|
+
type="radio"
|
|
203
|
+
name={`exprviz-scale-${taxon}`}
|
|
204
|
+
size="sm"
|
|
205
|
+
value={scale}
|
|
206
|
+
onChange={setScale}
|
|
207
|
+
>
|
|
208
|
+
<ToggleButton id={`exprviz-scale-${taxon}-lin`} value="linear" variant="outline-secondary">Linear</ToggleButton>
|
|
209
|
+
<ToggleButton id={`exprviz-scale-${taxon}-log`} value="log" variant="outline-secondary">Log</ToggleButton>
|
|
210
|
+
</ToggleButtonGroup>
|
|
211
|
+
<Button
|
|
212
|
+
size="sm"
|
|
213
|
+
variant="outline-secondary"
|
|
214
|
+
disabled={!hasBrush}
|
|
215
|
+
onClick={() => { setClearVersion(v => v + 1); setSelections({}); }}
|
|
216
|
+
>
|
|
217
|
+
Clear brushes
|
|
218
|
+
</Button>
|
|
219
|
+
<Button
|
|
220
|
+
size="sm"
|
|
221
|
+
variant="outline-secondary"
|
|
222
|
+
disabled={filteredRows.length === 0 || visibleFields.length === 0}
|
|
223
|
+
onClick={() => downloadTsv(`expression_${taxon}.tsv`, filteredRows, visibleFields)}
|
|
224
|
+
title="Download the visible rows and columns as tab-delimited text"
|
|
225
|
+
>
|
|
226
|
+
Download TSV
|
|
227
|
+
</Button>
|
|
228
|
+
<Button
|
|
229
|
+
size="sm"
|
|
230
|
+
variant="success"
|
|
231
|
+
disabled={!hasBrush || !onAddRangeQuery}
|
|
232
|
+
onClick={() => {
|
|
233
|
+
const terms = Object.keys(selections).map(field => {
|
|
234
|
+
const [lo, hi] = selections[field];
|
|
235
|
+
return {
|
|
236
|
+
category: 'Expression',
|
|
237
|
+
name: `${field}: ${fmt(lo)}–${fmt(hi)}`,
|
|
238
|
+
fq_field: field,
|
|
239
|
+
fq_value: `[${lo} TO ${hi}]`
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
onAddRangeQuery(terms);
|
|
243
|
+
}}
|
|
244
|
+
title="Add brush ranges as an AND-conjunction filter on the search"
|
|
245
|
+
>
|
|
246
|
+
Apply as filter
|
|
247
|
+
</Button>
|
|
248
|
+
<span className="exprviz-status">
|
|
249
|
+
{hasBrush ? `${filteredRows.length} of ${rows.length}` : rows.length}
|
|
250
|
+
{fetchInfo.total ? ` / ${fetchInfo.total}` : ''} genes
|
|
251
|
+
{hasBrush ? ' (brushed)' : ' loaded'}
|
|
252
|
+
</span>
|
|
253
|
+
</div>
|
|
254
|
+
<div className="exprviz-body">
|
|
255
|
+
<div className="exprviz-plot">
|
|
256
|
+
<ParallelCoordsPlot
|
|
257
|
+
rows={rows}
|
|
258
|
+
fields={visibleFields}
|
|
259
|
+
scale={scale}
|
|
260
|
+
onBrushChange={setSelections}
|
|
261
|
+
onReorder={handleReorder}
|
|
262
|
+
clearVersion={clearVersion}
|
|
263
|
+
hoveredId={hoveredId}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
<div className="exprviz-table">
|
|
267
|
+
<ExprTable
|
|
268
|
+
rows={filteredRows}
|
|
269
|
+
fields={visibleFields}
|
|
270
|
+
onReorder={handleReorder}
|
|
271
|
+
studies={studies}
|
|
272
|
+
expressionSamples={expressionSamples}
|
|
273
|
+
onHoverRow={setHoveredId}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export default connect(
|
|
282
|
+
'selectExprViz',
|
|
283
|
+
'selectExprVizPivot',
|
|
284
|
+
'selectExprVizActiveTaxon',
|
|
285
|
+
'selectGrameneMaps',
|
|
286
|
+
'selectExpressionStudies',
|
|
287
|
+
'selectExpressionSamples',
|
|
288
|
+
'doSetExprVizActiveTaxon',
|
|
289
|
+
'doToggleExprVizFieldsModal',
|
|
290
|
+
'doFetchExprVizData',
|
|
291
|
+
'doReorderExprVizFields',
|
|
292
|
+
'doAddGrameneRangeQuery',
|
|
293
|
+
ExprVizViewCmp
|
|
294
|
+
);
|