@vitessce/statistical-plots 3.5.9 → 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.
Files changed (53) hide show
  1. package/dist/{deflate-19841f78.js → deflate-ad0dcbe4.js} +1 -1
  2. package/dist/{index-dc733355.js → index-b8398176.js} +33528 -15439
  3. package/dist/index.js +6 -5
  4. package/dist/{jpeg-a83077be.js → jpeg-81bd1053.js} +1 -1
  5. package/dist/{lerc-1edd075a.js → lerc-b15c3a4c.js} +1 -1
  6. package/dist/{lzw-9572eac3.js → lzw-503cb795.js} +1 -1
  7. package/dist/{packbits-cce11fbc.js → packbits-40cbad40.js} +1 -1
  8. package/dist/{raw-f7587aff.js → raw-9b8d9daf.js} +1 -1
  9. package/dist/{webimage-8d38cd8b.js → webimage-bbc59b4a.js} +1 -1
  10. package/dist-tsc/CellSetCompositionBarPlot.js +3 -3
  11. package/dist-tsc/CellSetCompositionBarPlotSubscriber.js +1 -1
  12. package/dist-tsc/CellSetExpressionPlot.d.ts.map +1 -1
  13. package/dist-tsc/CellSetExpressionPlot.js +26 -10
  14. package/dist-tsc/CellSetExpressionPlotSubscriber.d.ts.map +1 -1
  15. package/dist-tsc/CellSetExpressionPlotSubscriber.js +5 -2
  16. package/dist-tsc/DotPlot.d.ts.map +1 -1
  17. package/dist-tsc/DotPlot.js +54 -5
  18. package/dist-tsc/DotPlotSubscriber.d.ts.map +1 -1
  19. package/dist-tsc/DotPlotSubscriber.js +1 -1
  20. package/dist-tsc/FeatureStatsTable.d.ts +2 -0
  21. package/dist-tsc/FeatureStatsTable.d.ts.map +1 -0
  22. package/dist-tsc/FeatureStatsTable.js +81 -0
  23. package/dist-tsc/FeatureStatsTableSubscriber.d.ts +2 -0
  24. package/dist-tsc/FeatureStatsTableSubscriber.d.ts.map +1 -0
  25. package/dist-tsc/FeatureStatsTableSubscriber.js +28 -0
  26. package/dist-tsc/Treemap.d.ts.map +1 -1
  27. package/dist-tsc/Treemap.js +17 -3
  28. package/dist-tsc/TreemapSubscriber.d.ts.map +1 -1
  29. package/dist-tsc/TreemapSubscriber.js +5 -3
  30. package/dist-tsc/VolcanoPlot.d.ts.map +1 -1
  31. package/dist-tsc/VolcanoPlot.js +15 -46
  32. package/dist-tsc/VolcanoPlotSubscriber.d.ts.map +1 -1
  33. package/dist-tsc/VolcanoPlotSubscriber.js +4 -2
  34. package/dist-tsc/index.d.ts +1 -0
  35. package/dist-tsc/index.js +1 -0
  36. package/dist-tsc/utils.d.ts +1 -0
  37. package/dist-tsc/utils.d.ts.map +1 -1
  38. package/dist-tsc/utils.js +56 -0
  39. package/package.json +8 -7
  40. package/src/CellSetCompositionBarPlot.js +3 -3
  41. package/src/CellSetCompositionBarPlotSubscriber.js +1 -1
  42. package/src/CellSetExpressionPlot.js +33 -10
  43. package/src/CellSetExpressionPlotSubscriber.js +10 -3
  44. package/src/DotPlot.js +77 -9
  45. package/src/DotPlotSubscriber.js +3 -1
  46. package/src/FeatureStatsTable.js +116 -0
  47. package/src/FeatureStatsTableSubscriber.js +133 -0
  48. package/src/Treemap.js +21 -3
  49. package/src/TreemapSubscriber.js +6 -4
  50. package/src/VolcanoPlot.js +16 -64
  51. package/src/VolcanoPlotSubscriber.js +6 -1
  52. package/src/index.js +1 -0
  53. 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
@@ -105,6 +105,10 @@ export default function Treemap(props) {
105
105
  useEffect(() => {
106
106
  const domElement = svgRef.current;
107
107
 
108
+ if (!width || !height) {
109
+ return;
110
+ }
111
+
108
112
  const svg = select(domElement);
109
113
  svg.selectAll('g').remove();
110
114
  svg
@@ -113,7 +117,7 @@ export default function Treemap(props) {
113
117
  .attr('viewBox', [0, 0, width, height])
114
118
  .attr('style', 'font: 10px sans-serif');
115
119
 
116
- if (!treemapLeaves || !obsSetSelection || !sampleSetSelection) {
120
+ if (!treemapLeaves || !obsSetSelection) {
117
121
  return;
118
122
  }
119
123
 
@@ -174,14 +178,28 @@ export default function Treemap(props) {
174
178
  .append('use')
175
179
  .attr('xlink:href', d => d.leafUid.href);
176
180
 
181
+ const hasSampleSetSelection = Array.isArray(sampleSetSelection);
182
+
177
183
  // Append multiline text.
178
184
  leaf.append('text')
179
185
  .attr('clip-path', d => `url(${d.clipUid.href})`)
180
186
  .selectAll('tspan')
181
187
  .data(d => ([
182
188
  // Each element in this array corresponds to a line of text.
183
- d.data?.[0]?.at(-1),
184
- d.parent?.data?.[0]?.at(-1),
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
+ ),
185
203
  `${d.data?.[1].toLocaleString()} ${plur(obsType, d.data?.[1])}`,
186
204
  ]))
187
205
  .join('tspan')
@@ -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.find(d => isEqual(d.setNamePath, sampleSetKey))?.size;
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 ? sampleIdToSetMap?.get(sampleId) : null;
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.
@@ -5,10 +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 { isEqual } from 'lodash-es';
9
8
  import { capitalize, getDefaultForegroundColor } from '@vitessce/utils';
10
9
  import { colorArrayToString } from '@vitessce/sets-utils';
11
- import { getColorScale } from './utils.js';
10
+ import { getColorScale, useFilteredVolcanoData } from './utils.js';
12
11
 
13
12
  export default function VolcanoPlot(props) {
14
13
  const {
@@ -17,8 +16,8 @@ export default function VolcanoPlot(props) {
17
16
  height,
18
17
  obsType,
19
18
  featureType,
20
- obsSetsColumnNameMapping,
21
- sampleSetsColumnNameMapping,
19
+ obsSetsColumnNameMappingReversed,
20
+ sampleSetsColumnNameMappingReversed,
22
21
  sampleSetSelection,
23
22
  obsSetSelection,
24
23
  obsSetColor,
@@ -37,14 +36,14 @@ export default function VolcanoPlot(props) {
37
36
 
38
37
  const svgRef = useRef();
39
38
 
40
- const computedData = useMemo(() => data.map(d => ({
41
- ...d,
42
- df: {
43
- ...d.df,
44
- minusLog10p: d.df.featureSignificance.map(v => -Math.log10(v)),
45
- logFoldChange: d.df.featureFoldChange.map(v => Math.log2(v)),
46
- },
47
- })), [data]);
39
+ const [computedData, filteredData] = useFilteredVolcanoData({
40
+ data,
41
+ obsSetsColumnNameMappingReversed,
42
+ sampleSetsColumnNameMappingReversed,
43
+ featurePointFoldChangeThreshold,
44
+ featurePointSignificanceThreshold,
45
+ sampleSetSelection,
46
+ });
48
47
 
49
48
  const [xExtent, yExtent] = useMemo(() => {
50
49
  if (!computedData) {
@@ -78,7 +77,7 @@ export default function VolcanoPlot(props) {
78
77
  .attr('viewBox', [0, 0, width, height])
79
78
  .attr('style', 'font: 10px sans-serif');
80
79
 
81
- if (!computedData || !xExtent || !yExtent) {
80
+ if (!filteredData || !xExtent || !yExtent) {
82
81
  return;
83
82
  }
84
83
 
@@ -134,19 +133,6 @@ export default function VolcanoPlot(props) {
134
133
  .style('font-size', '12px')
135
134
  .style('fill', fgColor);
136
135
 
137
- // Get a mapping from column name to group name.
138
- const obsSetsColumnNameMappingReversed = Object.fromEntries(
139
- Object
140
- .entries(obsSetsColumnNameMapping)
141
- .map(([key, value]) => ([value, key])),
142
- );
143
-
144
- const sampleSetsColumnNameMappingReversed = Object.fromEntries(
145
- Object
146
- .entries(sampleSetsColumnNameMapping)
147
- .map(([key, value]) => ([value, key])),
148
- );
149
-
150
136
  // Horizontal and vertical rules to indicate currently-selected thresholds
151
137
  // Vertical lines
152
138
  const ruleColor = 'silver';
@@ -188,7 +174,6 @@ export default function VolcanoPlot(props) {
188
174
  : `${capitalize(obsType)} Set`
189
175
  );
190
176
 
191
-
192
177
  titleG
193
178
  .append('text')
194
179
  .attr('text-anchor', 'start')
@@ -211,10 +196,10 @@ export default function VolcanoPlot(props) {
211
196
  const g = svg.append('g');
212
197
 
213
198
  // Append a circle for each data point.
214
- computedData.forEach((comparisonObject) => {
199
+ filteredData.forEach((comparisonObject) => {
215
200
  const obsSetG = g.append('g');
216
201
 
217
- const { df, metadata } = comparisonObject;
202
+ const { df: filteredDf, metadata } = comparisonObject;
218
203
  const coordinationValues = metadata.coordination_values;
219
204
 
220
205
  const rawObsSetPath = coordinationValues.obsSetFilter
@@ -223,40 +208,6 @@ export default function VolcanoPlot(props) {
223
208
  const obsSetPath = [...rawObsSetPath];
224
209
  obsSetPath[0] = obsSetsColumnNameMappingReversed[rawObsSetPath[0]];
225
210
 
226
- // Swap the foldchange direction if backwards with
227
- // respect to the current sampleSetSelection pair.
228
- // TODO: move this swapping into the computedData useMemo?
229
- let shouldSwapFoldChangeDirection = false;
230
- if (
231
- coordinationValues.sampleSetFilter
232
- && coordinationValues.sampleSetFilter.length === 2
233
- ) {
234
- const rawSampleSetPathA = coordinationValues.sampleSetFilter[0];
235
- const sampleSetPathA = [...rawSampleSetPathA];
236
- sampleSetPathA[0] = sampleSetsColumnNameMappingReversed[rawSampleSetPathA[0]];
237
-
238
- const rawSampleSetPathB = coordinationValues.sampleSetFilter[1];
239
- const sampleSetPathB = [...rawSampleSetPathB];
240
- sampleSetPathB[0] = sampleSetsColumnNameMappingReversed[rawSampleSetPathB[0]];
241
-
242
- if (
243
- isEqual(sampleSetPathA, sampleSetSelection[1])
244
- && isEqual(sampleSetPathB, sampleSetSelection[0])
245
- ) {
246
- shouldSwapFoldChangeDirection = true;
247
- }
248
- }
249
-
250
- const filteredDf = df.featureId.map((featureId, i) => ({
251
- featureId,
252
- logFoldChange: df.logFoldChange[i] * (shouldSwapFoldChangeDirection ? -1 : 1),
253
- featureSignificance: df.featureSignificance[i],
254
- minusLog10p: df.minusLog10p[i],
255
- })).filter(d => (
256
- (Math.abs(d.logFoldChange) >= (featurePointFoldChangeThreshold ?? 1.0))
257
- && (d.featureSignificance <= (featurePointSignificanceThreshold ?? 0.05))
258
- ));
259
-
260
211
  const color = obsSetColorScale(obsSetPath);
261
212
 
262
213
  obsSetG.append('g')
@@ -293,12 +244,13 @@ export default function VolcanoPlot(props) {
293
244
  .text(d => `${featureType}: ${d.featureId}\nin ${obsSetPath?.at(-1)}\nlog2 fold-change: ${d.logFoldChange}\np-value: ${d.featureSignificance}`);
294
245
  });
295
246
  }, [width, height, theme, sampleSetColor, sampleSetSelection,
296
- obsSetSelection, obsSetColor, featureType, computedData,
247
+ obsSetSelection, obsSetColor, featureType, filteredData,
297
248
  xExtent, yExtent, obsType,
298
249
  marginLeft, marginBottom, marginTop, marginRight,
299
250
  obsSetColorScale, sampleSetColorScale, onFeatureClick,
300
251
  featurePointSignificanceThreshold, featurePointFoldChangeThreshold,
301
252
  featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold,
253
+ obsSetsColumnNameMappingReversed,
302
254
  ]);
303
255
 
304
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="Volcano Plot"
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
+ }