@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.
Files changed (55) hide show
  1. package/dist/{deflate-1679ef33.js → deflate-ad0dcbe4.js} +1 -1
  2. package/dist/{index-0f4fe21d.js → index-b8398176.js} +33547 -15443
  3. package/dist/index.js +6 -5
  4. package/dist/{jpeg-280f0ee1.js → jpeg-81bd1053.js} +1 -1
  5. package/dist/{lerc-12264a36.js → lerc-b15c3a4c.js} +1 -1
  6. package/dist/{lzw-70f852cc.js → lzw-503cb795.js} +1 -1
  7. package/dist/{packbits-393c67b2.js → packbits-40cbad40.js} +1 -1
  8. package/dist/{raw-d8d7ab7f.js → raw-9b8d9daf.js} +1 -1
  9. package/dist/{webimage-5d24a8e2.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/FeatureSetEnrichmentBarPlot.js +1 -1
  21. package/dist-tsc/FeatureStatsTable.d.ts +2 -0
  22. package/dist-tsc/FeatureStatsTable.d.ts.map +1 -0
  23. package/dist-tsc/FeatureStatsTable.js +81 -0
  24. package/dist-tsc/FeatureStatsTableSubscriber.d.ts +2 -0
  25. package/dist-tsc/FeatureStatsTableSubscriber.d.ts.map +1 -0
  26. package/dist-tsc/FeatureStatsTableSubscriber.js +28 -0
  27. package/dist-tsc/Treemap.d.ts.map +1 -1
  28. package/dist-tsc/Treemap.js +26 -6
  29. package/dist-tsc/TreemapSubscriber.d.ts.map +1 -1
  30. package/dist-tsc/TreemapSubscriber.js +10 -5
  31. package/dist-tsc/VolcanoPlot.d.ts.map +1 -1
  32. package/dist-tsc/VolcanoPlot.js +18 -48
  33. package/dist-tsc/VolcanoPlotSubscriber.d.ts.map +1 -1
  34. package/dist-tsc/VolcanoPlotSubscriber.js +4 -2
  35. package/dist-tsc/index.d.ts +1 -0
  36. package/dist-tsc/index.js +1 -0
  37. package/dist-tsc/utils.d.ts +1 -0
  38. package/dist-tsc/utils.d.ts.map +1 -1
  39. package/dist-tsc/utils.js +56 -0
  40. package/package.json +8 -7
  41. package/src/CellSetCompositionBarPlot.js +3 -3
  42. package/src/CellSetCompositionBarPlotSubscriber.js +1 -1
  43. package/src/CellSetExpressionPlot.js +33 -10
  44. package/src/CellSetExpressionPlotSubscriber.js +10 -3
  45. package/src/DotPlot.js +77 -9
  46. package/src/DotPlotSubscriber.js +3 -1
  47. package/src/FeatureSetEnrichmentBarPlot.js +1 -1
  48. package/src/FeatureStatsTable.js +116 -0
  49. package/src/FeatureStatsTableSubscriber.js +133 -0
  50. package/src/Treemap.js +31 -5
  51. package/src/TreemapSubscriber.js +12 -5
  52. package/src/VolcanoPlot.js +21 -66
  53. package/src/VolcanoPlotSubscriber.js +6 -1
  54. package/src/index.js +1 -0
  55. 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 || !sampleSetSelection) {
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
- d.data?.[0]?.at(-1),
176
- 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
+ ),
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 (
@@ -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.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.
@@ -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>
@@ -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 { isEqual } from 'lodash-es';
9
- import { capitalize } from '@vitessce/utils';
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
- obsSetsColumnNameMapping,
20
- sampleSetsColumnNameMapping,
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 = useMemo(() => data.map(d => ({
40
- ...d,
41
- df: {
42
- ...d.df,
43
- minusLog10p: d.df.featureSignificance.map(v => -Math.log10(v)),
44
- logFoldChange: d.df.featureFoldChange.map(v => Math.log2(v)),
45
- },
46
- })), [data]);
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 (!computedData || !xExtent || !yExtent) {
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 = 'black'; // TODO: use theme to determine this
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
- computedData.forEach((comparisonObject) => {
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, computedData,
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="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
+ }