@vitessce/statistical-plots 3.5.8 → 3.5.10
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/{deflate-1679ef33.js → deflate-ad0dcbe4.js} +1 -1
- package/dist/{index-0f4fe21d.js → index-b8398176.js} +33547 -15443
- package/dist/index.js +6 -5
- package/dist/{jpeg-280f0ee1.js → jpeg-81bd1053.js} +1 -1
- package/dist/{lerc-12264a36.js → lerc-b15c3a4c.js} +1 -1
- package/dist/{lzw-70f852cc.js → lzw-503cb795.js} +1 -1
- package/dist/{packbits-393c67b2.js → packbits-40cbad40.js} +1 -1
- package/dist/{raw-d8d7ab7f.js → raw-9b8d9daf.js} +1 -1
- package/dist/{webimage-5d24a8e2.js → webimage-bbc59b4a.js} +1 -1
- package/dist-tsc/CellSetCompositionBarPlot.js +3 -3
- package/dist-tsc/CellSetCompositionBarPlotSubscriber.js +1 -1
- package/dist-tsc/CellSetExpressionPlot.d.ts.map +1 -1
- package/dist-tsc/CellSetExpressionPlot.js +26 -10
- package/dist-tsc/CellSetExpressionPlotSubscriber.d.ts.map +1 -1
- package/dist-tsc/CellSetExpressionPlotSubscriber.js +5 -2
- package/dist-tsc/DotPlot.d.ts.map +1 -1
- package/dist-tsc/DotPlot.js +54 -5
- package/dist-tsc/DotPlotSubscriber.d.ts.map +1 -1
- package/dist-tsc/DotPlotSubscriber.js +1 -1
- package/dist-tsc/FeatureSetEnrichmentBarPlot.js +1 -1
- package/dist-tsc/FeatureStatsTable.d.ts +2 -0
- package/dist-tsc/FeatureStatsTable.d.ts.map +1 -0
- package/dist-tsc/FeatureStatsTable.js +81 -0
- package/dist-tsc/FeatureStatsTableSubscriber.d.ts +2 -0
- package/dist-tsc/FeatureStatsTableSubscriber.d.ts.map +1 -0
- package/dist-tsc/FeatureStatsTableSubscriber.js +28 -0
- package/dist-tsc/Treemap.d.ts.map +1 -1
- package/dist-tsc/Treemap.js +26 -6
- package/dist-tsc/TreemapSubscriber.d.ts.map +1 -1
- package/dist-tsc/TreemapSubscriber.js +10 -5
- package/dist-tsc/VolcanoPlot.d.ts.map +1 -1
- package/dist-tsc/VolcanoPlot.js +18 -48
- package/dist-tsc/VolcanoPlotSubscriber.d.ts.map +1 -1
- package/dist-tsc/VolcanoPlotSubscriber.js +4 -2
- package/dist-tsc/index.d.ts +1 -0
- package/dist-tsc/index.js +1 -0
- package/dist-tsc/utils.d.ts +1 -0
- package/dist-tsc/utils.d.ts.map +1 -1
- package/dist-tsc/utils.js +56 -0
- package/package.json +8 -7
- package/src/CellSetCompositionBarPlot.js +3 -3
- package/src/CellSetCompositionBarPlotSubscriber.js +1 -1
- package/src/CellSetExpressionPlot.js +33 -10
- package/src/CellSetExpressionPlotSubscriber.js +10 -3
- package/src/DotPlot.js +77 -9
- package/src/DotPlotSubscriber.js +3 -1
- package/src/FeatureSetEnrichmentBarPlot.js +1 -1
- package/src/FeatureStatsTable.js +116 -0
- package/src/FeatureStatsTableSubscriber.js +133 -0
- package/src/Treemap.js +31 -5
- package/src/TreemapSubscriber.js +12 -5
- package/src/VolcanoPlot.js +21 -66
- package/src/VolcanoPlotSubscriber.js +6 -1
- package/src/index.js +1 -0
- package/src/utils.js +82 -1
@@ -0,0 +1,116 @@
|
|
1
|
+
import React, { useMemo, useState, useCallback } from 'react';
|
2
|
+
import { DataGrid } from '@mui/x-data-grid';
|
3
|
+
import { capitalize } from '@vitessce/utils';
|
4
|
+
import { useFilteredVolcanoData } from './utils.js';
|
5
|
+
|
6
|
+
const ROW_ID_DELIMITER = '___';
|
7
|
+
const INITIAL_SORT_MODEL = [
|
8
|
+
// We initially set the sorting this way
|
9
|
+
{ field: 'logFoldChange', sort: 'desc' },
|
10
|
+
];
|
11
|
+
|
12
|
+
export default function FeatureStatsTable(props) {
|
13
|
+
const {
|
14
|
+
obsType,
|
15
|
+
featureType,
|
16
|
+
obsSetsColumnNameMappingReversed,
|
17
|
+
sampleSetsColumnNameMappingReversed,
|
18
|
+
sampleSetSelection,
|
19
|
+
data,
|
20
|
+
setFeatureSelection,
|
21
|
+
featurePointSignificanceThreshold,
|
22
|
+
featurePointFoldChangeThreshold,
|
23
|
+
} = props;
|
24
|
+
|
25
|
+
const [
|
26
|
+
// eslint-disable-next-line no-unused-vars
|
27
|
+
computedData,
|
28
|
+
filteredData,
|
29
|
+
] = useFilteredVolcanoData({
|
30
|
+
data,
|
31
|
+
obsSetsColumnNameMappingReversed,
|
32
|
+
sampleSetsColumnNameMappingReversed,
|
33
|
+
featurePointFoldChangeThreshold,
|
34
|
+
featurePointSignificanceThreshold,
|
35
|
+
sampleSetSelection,
|
36
|
+
});
|
37
|
+
|
38
|
+
// Reference: https://v4.mui.com/api/data-grid/data-grid/
|
39
|
+
const columns = useMemo(() => ([
|
40
|
+
{
|
41
|
+
field: 'featureId',
|
42
|
+
headerName: capitalize(featureType),
|
43
|
+
width: 200,
|
44
|
+
editable: false,
|
45
|
+
},
|
46
|
+
{
|
47
|
+
field: 'logFoldChange',
|
48
|
+
headerName: 'Log Fold-Change',
|
49
|
+
width: 200,
|
50
|
+
editable: false,
|
51
|
+
},
|
52
|
+
{
|
53
|
+
field: 'featureSignificance',
|
54
|
+
headerName: 'P-value',
|
55
|
+
width: 200,
|
56
|
+
editable: false,
|
57
|
+
},
|
58
|
+
{
|
59
|
+
field: 'obsSetName',
|
60
|
+
headerName: `${capitalize(obsType)} Set`,
|
61
|
+
width: 200,
|
62
|
+
editable: false,
|
63
|
+
},
|
64
|
+
]), [obsType, featureType]);
|
65
|
+
|
66
|
+
const rows = useMemo(() => {
|
67
|
+
let result = [];
|
68
|
+
if (filteredData) {
|
69
|
+
filteredData.forEach((comparisonObject) => {
|
70
|
+
const { df, metadata } = comparisonObject;
|
71
|
+
|
72
|
+
const coordinationValues = metadata.coordination_values;
|
73
|
+
|
74
|
+
const rawObsSetPath = coordinationValues.obsSetFilter
|
75
|
+
? coordinationValues.obsSetFilter[0]
|
76
|
+
: coordinationValues.obsSetSelection[0];
|
77
|
+
const obsSetPath = [...rawObsSetPath];
|
78
|
+
obsSetPath[0] = obsSetsColumnNameMappingReversed[rawObsSetPath[0]];
|
79
|
+
const obsSetName = obsSetPath.at(-1);
|
80
|
+
|
81
|
+
result = result.concat(df.map(row => ({
|
82
|
+
...row,
|
83
|
+
id: `${row.featureId}${ROW_ID_DELIMITER}${obsSetName}`,
|
84
|
+
obsSetName,
|
85
|
+
})));
|
86
|
+
});
|
87
|
+
}
|
88
|
+
return result;
|
89
|
+
}, [filteredData, obsSetsColumnNameMappingReversed]);
|
90
|
+
|
91
|
+
const onSelectionModelChange = useCallback((rowIds) => {
|
92
|
+
const featureIds = rowIds.map(rowId => rowId.split(ROW_ID_DELIMITER)[0]);
|
93
|
+
setFeatureSelection(featureIds);
|
94
|
+
}, []);
|
95
|
+
|
96
|
+
const rowSelectionModel = useMemo(() => [], []);
|
97
|
+
|
98
|
+
const [sortModel, setSortModel] = useState(INITIAL_SORT_MODEL);
|
99
|
+
|
100
|
+
const getRowId = useCallback(row => row.id, []);
|
101
|
+
|
102
|
+
return (
|
103
|
+
<DataGrid
|
104
|
+
density="compact"
|
105
|
+
rows={rows}
|
106
|
+
columns={columns}
|
107
|
+
pageSize={10}
|
108
|
+
// checkboxSelection // TODO: uncomment to enable multiple-row selection
|
109
|
+
onSelectionModelChange={onSelectionModelChange}
|
110
|
+
rowSelectionModel={rowSelectionModel}
|
111
|
+
getRowId={getRowId}
|
112
|
+
sortModel={sortModel}
|
113
|
+
onSortModelChange={setSortModel}
|
114
|
+
/>
|
115
|
+
);
|
116
|
+
}
|
@@ -0,0 +1,133 @@
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
2
|
+
import React from 'react';
|
3
|
+
import {
|
4
|
+
TitleInfo,
|
5
|
+
useCoordination,
|
6
|
+
useLoaders,
|
7
|
+
useReady,
|
8
|
+
useFeatureStatsData,
|
9
|
+
useMatchingLoader,
|
10
|
+
useColumnNameMapping,
|
11
|
+
} from '@vitessce/vit-s';
|
12
|
+
import {
|
13
|
+
ViewType,
|
14
|
+
COMPONENT_COORDINATION_TYPES,
|
15
|
+
ViewHelpMapping,
|
16
|
+
DataType,
|
17
|
+
} from '@vitessce/constants-internal';
|
18
|
+
import FeatureStatsTable from './FeatureStatsTable.js';
|
19
|
+
import { useRawSetPaths } from './utils.js';
|
20
|
+
|
21
|
+
export function FeatureStatsTableSubscriber(props) {
|
22
|
+
const {
|
23
|
+
title = 'Differential Expression Results',
|
24
|
+
coordinationScopes,
|
25
|
+
removeGridComponent,
|
26
|
+
theme,
|
27
|
+
helpText = ViewHelpMapping.FEATURE_STATS_TABLE,
|
28
|
+
} = props;
|
29
|
+
|
30
|
+
const loaders = useLoaders();
|
31
|
+
|
32
|
+
// Get "props" from the coordination space.
|
33
|
+
const [{
|
34
|
+
dataset,
|
35
|
+
obsType,
|
36
|
+
sampleType,
|
37
|
+
featureType,
|
38
|
+
featureValueType,
|
39
|
+
obsFilter: cellFilter,
|
40
|
+
obsHighlight: cellHighlight,
|
41
|
+
obsSetSelection,
|
42
|
+
obsSetColor,
|
43
|
+
obsColorEncoding: cellColorEncoding,
|
44
|
+
additionalObsSets: additionalCellSets,
|
45
|
+
featurePointSignificanceThreshold,
|
46
|
+
featurePointFoldChangeThreshold,
|
47
|
+
featureValueTransform,
|
48
|
+
featureValueTransformCoefficient,
|
49
|
+
gatingFeatureSelectionX,
|
50
|
+
gatingFeatureSelectionY,
|
51
|
+
featureSelection,
|
52
|
+
sampleSetSelection,
|
53
|
+
sampleSetColor,
|
54
|
+
}, {
|
55
|
+
setObsFilter: setCellFilter,
|
56
|
+
setObsSetSelection,
|
57
|
+
setObsHighlight: setCellHighlight,
|
58
|
+
setObsSetColor: setCellSetColor,
|
59
|
+
setObsColorEncoding: setCellColorEncoding,
|
60
|
+
setAdditionalObsSets: setAdditionalCellSets,
|
61
|
+
setFeaturePointSignificanceThreshold,
|
62
|
+
setFeaturePointFoldChangeThreshold,
|
63
|
+
setFeatureValueTransform,
|
64
|
+
setFeatureValueTransformCoefficient,
|
65
|
+
setGatingFeatureSelectionX,
|
66
|
+
setGatingFeatureSelectionY,
|
67
|
+
setFeatureSelection,
|
68
|
+
setSampleSetSelection,
|
69
|
+
setSampleSetColor,
|
70
|
+
}] = useCoordination(
|
71
|
+
COMPONENT_COORDINATION_TYPES[ViewType.FEATURE_STATS_TABLE],
|
72
|
+
coordinationScopes,
|
73
|
+
);
|
74
|
+
|
75
|
+
const obsSetsLoader = useMatchingLoader(
|
76
|
+
loaders, dataset, DataType.OBS_SETS, { obsType },
|
77
|
+
);
|
78
|
+
const sampleSetsLoader = useMatchingLoader(
|
79
|
+
loaders, dataset, DataType.SAMPLE_SETS, { sampleType },
|
80
|
+
);
|
81
|
+
const obsSetsColumnNameMapping = useColumnNameMapping(obsSetsLoader);
|
82
|
+
const obsSetsColumnNameMappingReversed = useColumnNameMapping(obsSetsLoader, true);
|
83
|
+
const sampleSetsColumnNameMapping = useColumnNameMapping(sampleSetsLoader);
|
84
|
+
const sampleSetsColumnNameMappingReversed = useColumnNameMapping(sampleSetsLoader, true);
|
85
|
+
|
86
|
+
const rawSampleSetSelection = useRawSetPaths(sampleSetsColumnNameMapping, sampleSetSelection);
|
87
|
+
const rawObsSetSelection = useRawSetPaths(obsSetsColumnNameMapping, obsSetSelection);
|
88
|
+
|
89
|
+
const [{ featureStats }, featureStatsStatus] = useFeatureStatsData(
|
90
|
+
loaders, dataset, false,
|
91
|
+
{ obsType, featureType, sampleType },
|
92
|
+
// These volcanoOptions are passed to FeatureStatsAnndataLoader.loadMulti():
|
93
|
+
{ sampleSetSelection: rawSampleSetSelection, obsSetSelection: rawObsSetSelection },
|
94
|
+
);
|
95
|
+
|
96
|
+
const isReady = useReady([
|
97
|
+
featureStatsStatus,
|
98
|
+
]);
|
99
|
+
|
100
|
+
return (
|
101
|
+
<TitleInfo
|
102
|
+
title={title}
|
103
|
+
removeGridComponent={removeGridComponent}
|
104
|
+
theme={theme}
|
105
|
+
isReady={isReady}
|
106
|
+
helpText={helpText}
|
107
|
+
withPadding={false}
|
108
|
+
>
|
109
|
+
{featureStats ? (
|
110
|
+
<FeatureStatsTable
|
111
|
+
theme={theme}
|
112
|
+
obsType={obsType}
|
113
|
+
featureType={featureType}
|
114
|
+
obsSetsColumnNameMapping={obsSetsColumnNameMapping}
|
115
|
+
obsSetsColumnNameMappingReversed={obsSetsColumnNameMappingReversed}
|
116
|
+
sampleSetsColumnNameMapping={sampleSetsColumnNameMapping}
|
117
|
+
sampleSetsColumnNameMappingReversed={sampleSetsColumnNameMappingReversed}
|
118
|
+
sampleSetSelection={sampleSetSelection}
|
119
|
+
obsSetSelection={obsSetSelection}
|
120
|
+
obsSetColor={obsSetColor}
|
121
|
+
sampleSetColor={sampleSetColor}
|
122
|
+
data={featureStats}
|
123
|
+
featureSelection={featureSelection}
|
124
|
+
setFeatureSelection={setFeatureSelection}
|
125
|
+
featurePointSignificanceThreshold={featurePointSignificanceThreshold}
|
126
|
+
featurePointFoldChangeThreshold={featurePointFoldChangeThreshold}
|
127
|
+
/>
|
128
|
+
) : (
|
129
|
+
<p style={{ padding: '12px' }}>Select at least one {obsType} set.</p>
|
130
|
+
)}
|
131
|
+
</TitleInfo>
|
132
|
+
);
|
133
|
+
}
|
package/src/Treemap.js
CHANGED
@@ -47,6 +47,7 @@ export default function Treemap(props) {
|
|
47
47
|
marginRight = 5,
|
48
48
|
marginLeft = 80,
|
49
49
|
marginBottom,
|
50
|
+
onNodeClick,
|
50
51
|
} = props;
|
51
52
|
|
52
53
|
const hierarchyData = useMemo(() => {
|
@@ -104,6 +105,10 @@ export default function Treemap(props) {
|
|
104
105
|
useEffect(() => {
|
105
106
|
const domElement = svgRef.current;
|
106
107
|
|
108
|
+
if (!width || !height) {
|
109
|
+
return;
|
110
|
+
}
|
111
|
+
|
107
112
|
const svg = select(domElement);
|
108
113
|
svg.selectAll('g').remove();
|
109
114
|
svg
|
@@ -112,7 +117,7 @@ export default function Treemap(props) {
|
|
112
117
|
.attr('viewBox', [0, 0, width, height])
|
113
118
|
.attr('style', 'font: 10px sans-serif');
|
114
119
|
|
115
|
-
if (!treemapLeaves || !obsSetSelection
|
120
|
+
if (!treemapLeaves || !obsSetSelection) {
|
116
121
|
return;
|
117
122
|
}
|
118
123
|
|
@@ -154,7 +159,14 @@ export default function Treemap(props) {
|
|
154
159
|
.attr('fill', d => colorScale(getPathForColoring(d)))
|
155
160
|
.attr('fill-opacity', 0.8)
|
156
161
|
.attr('width', d => d.x1 - d.x0)
|
157
|
-
.attr('height', d => d.y1 - d.y0)
|
162
|
+
.attr('height', d => d.y1 - d.y0)
|
163
|
+
.on('click', (e, d) => {
|
164
|
+
const obsSetPath = (hierarchyLevels[0] === 'obsSet'
|
165
|
+
? d.parent?.data?.[0]
|
166
|
+
: d.data?.[0]
|
167
|
+
);
|
168
|
+
onNodeClick(obsSetPath);
|
169
|
+
});
|
158
170
|
|
159
171
|
// Append a clipPath to ensure text does not overflow.
|
160
172
|
leaf.append('clipPath')
|
@@ -166,14 +178,28 @@ export default function Treemap(props) {
|
|
166
178
|
.append('use')
|
167
179
|
.attr('xlink:href', d => d.leafUid.href);
|
168
180
|
|
181
|
+
const hasSampleSetSelection = Array.isArray(sampleSetSelection);
|
182
|
+
|
169
183
|
// Append multiline text.
|
170
184
|
leaf.append('text')
|
171
185
|
.attr('clip-path', d => `url(${d.clipUid.href})`)
|
172
186
|
.selectAll('tspan')
|
173
187
|
.data(d => ([
|
174
188
|
// Each element in this array corresponds to a line of text.
|
175
|
-
|
176
|
-
|
189
|
+
...(
|
190
|
+
hasSampleSetSelection
|
191
|
+
? ([
|
192
|
+
d.data?.[0]?.at(-1),
|
193
|
+
d.parent?.data?.[0]?.at(-1),
|
194
|
+
]) : ([
|
195
|
+
// Only use the cell set name
|
196
|
+
// for the line of text
|
197
|
+
// (since no sample set selection)
|
198
|
+
hierarchyLevels[0] === 'obsSet'
|
199
|
+
? d.parent?.data?.[0].at(-1)
|
200
|
+
: d.data?.[0].at(-1),
|
201
|
+
])
|
202
|
+
),
|
177
203
|
`${d.data?.[1].toLocaleString()} ${plur(obsType, d.data?.[1])}`,
|
178
204
|
]))
|
179
205
|
.join('tspan')
|
@@ -184,7 +210,7 @@ export default function Treemap(props) {
|
|
184
210
|
}, [width, height, marginLeft, marginBottom, theme, marginTop, marginRight,
|
185
211
|
obsType, sampleType, treemapLeaves, sampleSetColor, sampleSetSelection,
|
186
212
|
obsSetSelection, obsSetColor, obsSetColorScale, sampleSetColorScale,
|
187
|
-
obsColorEncoding, hierarchyLevels,
|
213
|
+
obsColorEncoding, hierarchyLevels, onNodeClick,
|
188
214
|
]);
|
189
215
|
|
190
216
|
return (
|
package/src/TreemapSubscriber.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
/* eslint-disable no-unused-vars */
|
2
|
-
import React, { useMemo } from 'react';
|
2
|
+
import React, { useMemo, useCallback } from 'react';
|
3
3
|
import {
|
4
4
|
TitleInfo,
|
5
5
|
useCoordination,
|
@@ -173,12 +173,12 @@ export function TreemapSubscriber(props) {
|
|
173
173
|
});
|
174
174
|
});
|
175
175
|
|
176
|
-
const sampleSetSizes = treeToSetSizesBySetNames(
|
176
|
+
const sampleSetSizes = hasSampleSetSelection ? treeToSetSizesBySetNames(
|
177
177
|
mergedSampleSets, sampleSetSelection, sampleSetSelection, sampleSetColor, theme,
|
178
|
-
);
|
178
|
+
) : null;
|
179
179
|
|
180
180
|
sampleSetKeys.forEach((sampleSetKey) => {
|
181
|
-
const sampleSetSize = sampleSetSizes
|
181
|
+
const sampleSetSize = sampleSetSizes?.find(d => isEqual(d.setNamePath, sampleSetKey))?.size;
|
182
182
|
sampleResult.set(sampleSetKey, sampleSetSize || 0);
|
183
183
|
});
|
184
184
|
|
@@ -193,7 +193,9 @@ export function TreemapSubscriber(props) {
|
|
193
193
|
|
194
194
|
const cellSet = cellIdToSetMap?.get(obsId);
|
195
195
|
const sampleId = sampleEdges?.get(obsId);
|
196
|
-
const sampleSet = sampleId
|
196
|
+
const sampleSet = sampleId && hasSampleSetSelection
|
197
|
+
? sampleIdToSetMap?.get(sampleId)
|
198
|
+
: null;
|
197
199
|
|
198
200
|
if (hasSampleSetSelection && !sampleSet) {
|
199
201
|
// Skip this sample if it is not in the selected sample set.
|
@@ -215,6 +217,10 @@ export function TreemapSubscriber(props) {
|
|
215
217
|
// TODO: consider filtering-related coordination values
|
216
218
|
]);
|
217
219
|
|
220
|
+
const onNodeClick = useCallback((obsSetPath) => {
|
221
|
+
setObsSetSelection([obsSetPath]);
|
222
|
+
}, [setObsSetSelection]);
|
223
|
+
|
218
224
|
return (
|
219
225
|
<TitleInfo
|
220
226
|
title={`Treemap of ${capitalize(plur(obsType, 2))}`}
|
@@ -255,6 +261,7 @@ export function TreemapSubscriber(props) {
|
|
255
261
|
sampleSetColor={sampleSetColor}
|
256
262
|
obsSetSelection={obsSetSelection}
|
257
263
|
sampleSetSelection={sampleSetSelection}
|
264
|
+
onNodeClick={onNodeClick}
|
258
265
|
/>
|
259
266
|
</div>
|
260
267
|
</TitleInfo>
|
package/src/VolcanoPlot.js
CHANGED
@@ -5,9 +5,9 @@ import { scaleLinear } from 'd3-scale';
|
|
5
5
|
import { axisBottom, axisLeft } from 'd3-axis';
|
6
6
|
import { extent as d3_extent } from 'd3-array';
|
7
7
|
import { select } from 'd3-selection';
|
8
|
-
import {
|
9
|
-
import {
|
10
|
-
import { getColorScale } from './utils.js';
|
8
|
+
import { capitalize, getDefaultForegroundColor } from '@vitessce/utils';
|
9
|
+
import { colorArrayToString } from '@vitessce/sets-utils';
|
10
|
+
import { getColorScale, useFilteredVolcanoData } from './utils.js';
|
11
11
|
|
12
12
|
export default function VolcanoPlot(props) {
|
13
13
|
const {
|
@@ -16,8 +16,8 @@ export default function VolcanoPlot(props) {
|
|
16
16
|
height,
|
17
17
|
obsType,
|
18
18
|
featureType,
|
19
|
-
|
20
|
-
|
19
|
+
obsSetsColumnNameMappingReversed,
|
20
|
+
sampleSetsColumnNameMappingReversed,
|
21
21
|
sampleSetSelection,
|
22
22
|
obsSetSelection,
|
23
23
|
obsSetColor,
|
@@ -36,14 +36,14 @@ export default function VolcanoPlot(props) {
|
|
36
36
|
|
37
37
|
const svgRef = useRef();
|
38
38
|
|
39
|
-
const computedData =
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
39
|
+
const [computedData, filteredData] = useFilteredVolcanoData({
|
40
|
+
data,
|
41
|
+
obsSetsColumnNameMappingReversed,
|
42
|
+
sampleSetsColumnNameMappingReversed,
|
43
|
+
featurePointFoldChangeThreshold,
|
44
|
+
featurePointSignificanceThreshold,
|
45
|
+
sampleSetSelection,
|
46
|
+
});
|
47
47
|
|
48
48
|
const [xExtent, yExtent] = useMemo(() => {
|
49
49
|
if (!computedData) {
|
@@ -77,7 +77,7 @@ export default function VolcanoPlot(props) {
|
|
77
77
|
.attr('viewBox', [0, 0, width, height])
|
78
78
|
.attr('style', 'font: 10px sans-serif');
|
79
79
|
|
80
|
-
if (!
|
80
|
+
if (!filteredData || !xExtent || !yExtent) {
|
81
81
|
return;
|
82
82
|
}
|
83
83
|
|
@@ -108,7 +108,9 @@ export default function VolcanoPlot(props) {
|
|
108
108
|
|
109
109
|
// Axis titles
|
110
110
|
const titleG = svg.append('g');
|
111
|
-
const fgColor =
|
111
|
+
const fgColor = colorArrayToString(
|
112
|
+
getDefaultForegroundColor(theme),
|
113
|
+
);
|
112
114
|
|
113
115
|
// Y-axis title
|
114
116
|
titleG
|
@@ -131,19 +133,6 @@ export default function VolcanoPlot(props) {
|
|
131
133
|
.style('font-size', '12px')
|
132
134
|
.style('fill', fgColor);
|
133
135
|
|
134
|
-
// Get a mapping from column name to group name.
|
135
|
-
const obsSetsColumnNameMappingReversed = Object.fromEntries(
|
136
|
-
Object
|
137
|
-
.entries(obsSetsColumnNameMapping)
|
138
|
-
.map(([key, value]) => ([value, key])),
|
139
|
-
);
|
140
|
-
|
141
|
-
const sampleSetsColumnNameMappingReversed = Object.fromEntries(
|
142
|
-
Object
|
143
|
-
.entries(sampleSetsColumnNameMapping)
|
144
|
-
.map(([key, value]) => ([value, key])),
|
145
|
-
);
|
146
|
-
|
147
136
|
// Horizontal and vertical rules to indicate currently-selected thresholds
|
148
137
|
// Vertical lines
|
149
138
|
const ruleColor = 'silver';
|
@@ -185,7 +174,6 @@ export default function VolcanoPlot(props) {
|
|
185
174
|
: `${capitalize(obsType)} Set`
|
186
175
|
);
|
187
176
|
|
188
|
-
|
189
177
|
titleG
|
190
178
|
.append('text')
|
191
179
|
.attr('text-anchor', 'start')
|
@@ -208,10 +196,10 @@ export default function VolcanoPlot(props) {
|
|
208
196
|
const g = svg.append('g');
|
209
197
|
|
210
198
|
// Append a circle for each data point.
|
211
|
-
|
199
|
+
filteredData.forEach((comparisonObject) => {
|
212
200
|
const obsSetG = g.append('g');
|
213
201
|
|
214
|
-
const { df, metadata } = comparisonObject;
|
202
|
+
const { df: filteredDf, metadata } = comparisonObject;
|
215
203
|
const coordinationValues = metadata.coordination_values;
|
216
204
|
|
217
205
|
const rawObsSetPath = coordinationValues.obsSetFilter
|
@@ -220,40 +208,6 @@ export default function VolcanoPlot(props) {
|
|
220
208
|
const obsSetPath = [...rawObsSetPath];
|
221
209
|
obsSetPath[0] = obsSetsColumnNameMappingReversed[rawObsSetPath[0]];
|
222
210
|
|
223
|
-
// Swap the foldchange direction if backwards with
|
224
|
-
// respect to the current sampleSetSelection pair.
|
225
|
-
// TODO: move this swapping into the computedData useMemo?
|
226
|
-
let shouldSwapFoldChangeDirection = false;
|
227
|
-
if (
|
228
|
-
coordinationValues.sampleSetFilter
|
229
|
-
&& coordinationValues.sampleSetFilter.length === 2
|
230
|
-
) {
|
231
|
-
const rawSampleSetPathA = coordinationValues.sampleSetFilter[0];
|
232
|
-
const sampleSetPathA = [...rawSampleSetPathA];
|
233
|
-
sampleSetPathA[0] = sampleSetsColumnNameMappingReversed[rawSampleSetPathA[0]];
|
234
|
-
|
235
|
-
const rawSampleSetPathB = coordinationValues.sampleSetFilter[1];
|
236
|
-
const sampleSetPathB = [...rawSampleSetPathB];
|
237
|
-
sampleSetPathB[0] = sampleSetsColumnNameMappingReversed[rawSampleSetPathB[0]];
|
238
|
-
|
239
|
-
if (
|
240
|
-
isEqual(sampleSetPathA, sampleSetSelection[1])
|
241
|
-
&& isEqual(sampleSetPathB, sampleSetSelection[0])
|
242
|
-
) {
|
243
|
-
shouldSwapFoldChangeDirection = true;
|
244
|
-
}
|
245
|
-
}
|
246
|
-
|
247
|
-
const filteredDf = df.featureId.map((featureId, i) => ({
|
248
|
-
featureId,
|
249
|
-
logFoldChange: df.logFoldChange[i] * (shouldSwapFoldChangeDirection ? -1 : 1),
|
250
|
-
featureSignificance: df.featureSignificance[i],
|
251
|
-
minusLog10p: df.minusLog10p[i],
|
252
|
-
})).filter(d => (
|
253
|
-
(Math.abs(d.logFoldChange) >= (featurePointFoldChangeThreshold ?? 1.0))
|
254
|
-
&& (d.featureSignificance <= (featurePointSignificanceThreshold ?? 0.05))
|
255
|
-
));
|
256
|
-
|
257
211
|
const color = obsSetColorScale(obsSetPath);
|
258
212
|
|
259
213
|
obsSetG.append('g')
|
@@ -290,12 +244,13 @@ export default function VolcanoPlot(props) {
|
|
290
244
|
.text(d => `${featureType}: ${d.featureId}\nin ${obsSetPath?.at(-1)}\nlog2 fold-change: ${d.logFoldChange}\np-value: ${d.featureSignificance}`);
|
291
245
|
});
|
292
246
|
}, [width, height, theme, sampleSetColor, sampleSetSelection,
|
293
|
-
obsSetSelection, obsSetColor, featureType,
|
247
|
+
obsSetSelection, obsSetColor, featureType, filteredData,
|
294
248
|
xExtent, yExtent, obsType,
|
295
249
|
marginLeft, marginBottom, marginTop, marginRight,
|
296
250
|
obsSetColorScale, sampleSetColorScale, onFeatureClick,
|
297
251
|
featurePointSignificanceThreshold, featurePointFoldChangeThreshold,
|
298
252
|
featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold,
|
253
|
+
obsSetsColumnNameMappingReversed,
|
299
254
|
]);
|
300
255
|
|
301
256
|
return (
|
@@ -23,6 +23,7 @@ import { useRawSetPaths } from './utils.js';
|
|
23
23
|
|
24
24
|
export function VolcanoPlotSubscriber(props) {
|
25
25
|
const {
|
26
|
+
title = 'Volcano Plot',
|
26
27
|
coordinationScopes,
|
27
28
|
removeGridComponent,
|
28
29
|
theme,
|
@@ -87,7 +88,9 @@ export function VolcanoPlotSubscriber(props) {
|
|
87
88
|
loaders, dataset, DataType.SAMPLE_SETS, { sampleType },
|
88
89
|
);
|
89
90
|
const obsSetsColumnNameMapping = useColumnNameMapping(obsSetsLoader);
|
91
|
+
const obsSetsColumnNameMappingReversed = useColumnNameMapping(obsSetsLoader, true);
|
90
92
|
const sampleSetsColumnNameMapping = useColumnNameMapping(sampleSetsLoader);
|
93
|
+
const sampleSetsColumnNameMappingReversed = useColumnNameMapping(sampleSetsLoader, true);
|
91
94
|
|
92
95
|
const rawSampleSetSelection = useRawSetPaths(sampleSetsColumnNameMapping, sampleSetSelection);
|
93
96
|
const rawObsSetSelection = useRawSetPaths(obsSetsColumnNameMapping, obsSetSelection);
|
@@ -109,7 +112,7 @@ export function VolcanoPlotSubscriber(props) {
|
|
109
112
|
|
110
113
|
return (
|
111
114
|
<TitleInfo
|
112
|
-
title=
|
115
|
+
title={title}
|
113
116
|
removeGridComponent={removeGridComponent}
|
114
117
|
theme={theme}
|
115
118
|
isReady={isReady}
|
@@ -140,7 +143,9 @@ export function VolcanoPlotSubscriber(props) {
|
|
140
143
|
obsType={obsType}
|
141
144
|
featureType={featureType}
|
142
145
|
obsSetsColumnNameMapping={obsSetsColumnNameMapping}
|
146
|
+
obsSetsColumnNameMappingReversed={obsSetsColumnNameMappingReversed}
|
143
147
|
sampleSetsColumnNameMapping={sampleSetsColumnNameMapping}
|
148
|
+
sampleSetsColumnNameMappingReversed={sampleSetsColumnNameMappingReversed}
|
144
149
|
sampleSetSelection={sampleSetSelection}
|
145
150
|
obsSetSelection={obsSetSelection}
|
146
151
|
obsSetColor={obsSetColor}
|
package/src/index.js
CHANGED
@@ -7,6 +7,7 @@ export { TreemapSubscriber } from './TreemapSubscriber.js';
|
|
7
7
|
export { VolcanoPlotSubscriber } from './VolcanoPlotSubscriber.js';
|
8
8
|
export { CellSetCompositionBarPlotSubscriber } from './CellSetCompositionBarPlotSubscriber.js';
|
9
9
|
export { FeatureSetEnrichmentBarPlotSubscriber } from './FeatureSetEnrichmentBarPlotSubscriber.js';
|
10
|
+
export { FeatureStatsTableSubscriber } from './FeatureStatsTableSubscriber.js';
|
10
11
|
export { default as CellSetSizesPlot } from './CellSetSizesPlot.js';
|
11
12
|
export { default as CellSetExpressionPlot } from './CellSetExpressionPlot.js';
|
12
13
|
export { default as ExpressionHistogram } from './ExpressionHistogram.js';
|
package/src/utils.js
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
+
/* eslint-disable camelcase */
|
1
2
|
import { useMemo } from 'react';
|
2
3
|
import { isEqual } from 'lodash-es';
|
3
4
|
import { colorArrayToString } from '@vitessce/sets-utils';
|
4
5
|
import { getDefaultColor } from '@vitessce/utils';
|
5
6
|
|
6
|
-
|
7
7
|
function createOrdinalScale(domainArr, rangeArr) {
|
8
8
|
return (queryVal) => {
|
9
9
|
const i = domainArr.findIndex(domainVal => isEqual(domainVal, queryVal));
|
@@ -45,3 +45,84 @@ export function useRawSetPaths(columnNameMapping, setPaths) {
|
|
45
45
|
return newSetPath;
|
46
46
|
}), [columnNameMapping, setPaths]);
|
47
47
|
}
|
48
|
+
|
49
|
+
// Data transformation hook function that is used both here
|
50
|
+
// and in the FeatureStatsTable view.
|
51
|
+
export function useFilteredVolcanoData(props) {
|
52
|
+
const {
|
53
|
+
data,
|
54
|
+
obsSetsColumnNameMappingReversed,
|
55
|
+
sampleSetsColumnNameMappingReversed,
|
56
|
+
featurePointFoldChangeThreshold,
|
57
|
+
featurePointSignificanceThreshold,
|
58
|
+
sampleSetSelection,
|
59
|
+
} = props;
|
60
|
+
|
61
|
+
|
62
|
+
const computedData = useMemo(() => data.map((d) => {
|
63
|
+
const { metadata } = d;
|
64
|
+
|
65
|
+
const coordinationValues = metadata.coordination_values;
|
66
|
+
|
67
|
+
const rawObsSetPath = coordinationValues.obsSetFilter
|
68
|
+
? coordinationValues.obsSetFilter[0]
|
69
|
+
: coordinationValues.obsSetSelection[0];
|
70
|
+
const obsSetPath = [...rawObsSetPath];
|
71
|
+
obsSetPath[0] = obsSetsColumnNameMappingReversed[rawObsSetPath[0]];
|
72
|
+
|
73
|
+
// Swap the foldchange direction if backwards with
|
74
|
+
// respect to the current sampleSetSelection pair.
|
75
|
+
// TODO: move this swapping into the computedData useMemo?
|
76
|
+
let shouldSwapFoldChangeDirection = false;
|
77
|
+
if (
|
78
|
+
coordinationValues.sampleSetFilter
|
79
|
+
&& coordinationValues.sampleSetFilter.length === 2
|
80
|
+
) {
|
81
|
+
const rawSampleSetPathA = coordinationValues.sampleSetFilter[0];
|
82
|
+
const sampleSetPathA = [...rawSampleSetPathA];
|
83
|
+
sampleSetPathA[0] = sampleSetsColumnNameMappingReversed[rawSampleSetPathA[0]];
|
84
|
+
|
85
|
+
const rawSampleSetPathB = coordinationValues.sampleSetFilter[1];
|
86
|
+
const sampleSetPathB = [...rawSampleSetPathB];
|
87
|
+
sampleSetPathB[0] = sampleSetsColumnNameMappingReversed[rawSampleSetPathB[0]];
|
88
|
+
|
89
|
+
if (
|
90
|
+
isEqual(sampleSetPathA, sampleSetSelection[1])
|
91
|
+
&& isEqual(sampleSetPathB, sampleSetSelection[0])
|
92
|
+
) {
|
93
|
+
shouldSwapFoldChangeDirection = true;
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
return ({
|
98
|
+
...d,
|
99
|
+
df: {
|
100
|
+
...d.df,
|
101
|
+
minusLog10p: d.df.featureSignificance.map(v => -Math.log10(v)),
|
102
|
+
logFoldChange: d.df.featureFoldChange.map(v => (
|
103
|
+
Math.log2(v) * (shouldSwapFoldChangeDirection ? -1 : 1)
|
104
|
+
)),
|
105
|
+
},
|
106
|
+
});
|
107
|
+
}), [
|
108
|
+
data, obsSetsColumnNameMappingReversed, sampleSetsColumnNameMappingReversed,
|
109
|
+
sampleSetSelection,
|
110
|
+
]);
|
111
|
+
|
112
|
+
const filteredData = useMemo(() => computedData.map(obj => ({
|
113
|
+
...obj,
|
114
|
+
// Instead of an object of one array per column,
|
115
|
+
// this is now an array of one object per row.
|
116
|
+
df: obj.df.featureId.map((featureId, i) => ({
|
117
|
+
featureId,
|
118
|
+
logFoldChange: obj.df.logFoldChange[i],
|
119
|
+
featureSignificance: obj.df.featureSignificance[i],
|
120
|
+
minusLog10p: obj.df.minusLog10p[i],
|
121
|
+
})).filter(d => (
|
122
|
+
(Math.abs(d.logFoldChange) >= (featurePointFoldChangeThreshold ?? 1.0))
|
123
|
+
&& (d.featureSignificance <= (featurePointSignificanceThreshold ?? 0.05))
|
124
|
+
)),
|
125
|
+
})), [computedData, featurePointFoldChangeThreshold, featurePointSignificanceThreshold]);
|
126
|
+
|
127
|
+
return [computedData, filteredData];
|
128
|
+
}
|