@vitessce/statistical-plots 3.5.9 → 3.5.11

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 (67) hide show
  1. package/dist/{deflate-19841f78.js → deflate-b8c6daae.js} +1 -1
  2. package/dist/{index-dc733355.js → index-134a71c6.js} +33739 -15506
  3. package/dist/index.js +6 -5
  4. package/dist/{jpeg-a83077be.js → jpeg-1b818d11.js} +1 -1
  5. package/dist/{lerc-1edd075a.js → lerc-f38cbdfc.js} +1 -1
  6. package/dist/{lzw-9572eac3.js → lzw-1120aba9.js} +1 -1
  7. package/dist/{packbits-cce11fbc.js → packbits-2d02b5e3.js} +1 -1
  8. package/dist/{raw-f7587aff.js → raw-0887baec.js} +1 -1
  9. package/dist/{webimage-8d38cd8b.js → webimage-533922a5.js} +1 -1
  10. package/dist-tsc/CellSetCompositionBarPlot.d.ts.map +1 -1
  11. package/dist-tsc/CellSetCompositionBarPlot.js +31 -10
  12. package/dist-tsc/CellSetCompositionBarPlotSubscriber.js +1 -1
  13. package/dist-tsc/CellSetExpressionPlot.d.ts.map +1 -1
  14. package/dist-tsc/CellSetExpressionPlot.js +26 -10
  15. package/dist-tsc/CellSetExpressionPlotOptions.d.ts.map +1 -1
  16. package/dist-tsc/CellSetExpressionPlotOptions.js +11 -4
  17. package/dist-tsc/CellSetExpressionPlotSubscriber.d.ts.map +1 -1
  18. package/dist-tsc/CellSetExpressionPlotSubscriber.js +46 -11
  19. package/dist-tsc/DotPlot.d.ts.map +1 -1
  20. package/dist-tsc/DotPlot.js +59 -6
  21. package/dist-tsc/DotPlotSubscriber.d.ts.map +1 -1
  22. package/dist-tsc/DotPlotSubscriber.js +1 -1
  23. package/dist-tsc/FeatureSetEnrichmentBarPlot.d.ts.map +1 -1
  24. package/dist-tsc/FeatureSetEnrichmentBarPlot.js +7 -6
  25. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.d.ts.map +1 -1
  26. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.js +5 -2
  27. package/dist-tsc/FeatureStatsTable.d.ts +2 -0
  28. package/dist-tsc/FeatureStatsTable.d.ts.map +1 -0
  29. package/dist-tsc/FeatureStatsTable.js +81 -0
  30. package/dist-tsc/FeatureStatsTableSubscriber.d.ts +2 -0
  31. package/dist-tsc/FeatureStatsTableSubscriber.d.ts.map +1 -0
  32. package/dist-tsc/FeatureStatsTableSubscriber.js +28 -0
  33. package/dist-tsc/Treemap.d.ts.map +1 -1
  34. package/dist-tsc/Treemap.js +17 -3
  35. package/dist-tsc/TreemapSubscriber.d.ts.map +1 -1
  36. package/dist-tsc/TreemapSubscriber.js +15 -9
  37. package/dist-tsc/VolcanoPlot.d.ts.map +1 -1
  38. package/dist-tsc/VolcanoPlot.js +15 -46
  39. package/dist-tsc/VolcanoPlotSubscriber.d.ts.map +1 -1
  40. package/dist-tsc/VolcanoPlotSubscriber.js +4 -2
  41. package/dist-tsc/expr-hooks.d.ts.map +1 -1
  42. package/dist-tsc/expr-hooks.test.js +2 -1
  43. package/dist-tsc/index.d.ts +1 -0
  44. package/dist-tsc/index.js +1 -0
  45. package/dist-tsc/utils.d.ts +1 -0
  46. package/dist-tsc/utils.d.ts.map +1 -1
  47. package/dist-tsc/utils.js +56 -0
  48. package/package.json +8 -7
  49. package/src/CellSetCompositionBarPlot.js +38 -12
  50. package/src/CellSetCompositionBarPlotSubscriber.js +1 -1
  51. package/src/CellSetExpressionPlot.js +33 -10
  52. package/src/CellSetExpressionPlotOptions.js +39 -2
  53. package/src/CellSetExpressionPlotSubscriber.js +56 -13
  54. package/src/DotPlot.js +81 -11
  55. package/src/DotPlotSubscriber.js +3 -1
  56. package/src/FeatureSetEnrichmentBarPlot.js +7 -6
  57. package/src/FeatureSetEnrichmentBarPlotSubscriber.js +5 -2
  58. package/src/FeatureStatsTable.js +116 -0
  59. package/src/FeatureStatsTableSubscriber.js +133 -0
  60. package/src/Treemap.js +21 -3
  61. package/src/TreemapSubscriber.js +26 -11
  62. package/src/VolcanoPlot.js +16 -64
  63. package/src/VolcanoPlotSubscriber.js +6 -1
  64. package/src/expr-hooks.js +0 -1
  65. package/src/expr-hooks.test.js +2 -1
  66. package/src/index.js +1 -0
  67. package/src/utils.js +82 -1
@@ -119,8 +119,11 @@ export function FeatureSetEnrichmentBarPlotSubscriber(props) {
119
119
  // Will not work since transformFeature currently:
120
120
  // - matches based on kgId (rather than term)
121
121
  // - only knows about Reactome pathways (not GO terms).
122
- // console.log(await transformFeature(kgNode, targetFeatureType))
123
- // setFeatureSelection(featureIds);
122
+ const targetsInPathway = await transformFeature(kgNode, targetFeatureType);
123
+ const featureIds = targetsInPathway
124
+ .filter((d, i) => i < 10) // TODO: do not limit the number of genes here
125
+ .map(d => d.label);
126
+ setFeatureSelection(featureIds);
124
127
  }, [setFeatureSelection]);
125
128
 
126
129
  // TODO: support the following options
@@ -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')
@@ -141,9 +141,6 @@ export function TreemapSubscriber(props) {
141
141
  [sampleSets],
142
142
  );
143
143
 
144
- const obsCount = obsIndex?.length || 0;
145
- const sampleCount = sampleIndex?.length || 0;
146
-
147
144
  // TODO: use obsFilter / sampleFilter to display
148
145
  // _all_ cells/samples in gray / transparent in background,
149
146
  // and use obsSetSelection/sampleSetSelection to display
@@ -173,16 +170,16 @@ export function TreemapSubscriber(props) {
173
170
  });
174
171
  });
175
172
 
176
- const sampleSetSizes = treeToSetSizesBySetNames(
173
+ const sampleSetSizes = hasSampleSetSelection ? treeToSetSizesBySetNames(
177
174
  mergedSampleSets, sampleSetSelection, sampleSetSelection, sampleSetColor, theme,
178
- );
175
+ ) : null;
179
176
 
180
177
  sampleSetKeys.forEach((sampleSetKey) => {
181
- const sampleSetSize = sampleSetSizes.find(d => isEqual(d.setNamePath, sampleSetKey))?.size;
178
+ const sampleSetSize = sampleSetSizes?.find(d => isEqual(d.setNamePath, sampleSetKey))?.size;
182
179
  sampleResult.set(sampleSetKey, sampleSetSize || 0);
183
180
  });
184
181
 
185
- if (mergedObsSets && obsSetSelection) {
182
+ if (mergedObsSets && obsSetSelection && obsIndex) {
186
183
  const sampleIdToSetMap = sampleSets && sampleSetSelection
187
184
  ? treeToSelectedSetMap(sampleSets, sampleSetSelection)
188
185
  : null;
@@ -193,7 +190,9 @@ export function TreemapSubscriber(props) {
193
190
 
194
191
  const cellSet = cellIdToSetMap?.get(obsId);
195
192
  const sampleId = sampleEdges?.get(obsId);
196
- const sampleSet = sampleId ? sampleIdToSetMap?.get(sampleId) : null;
193
+ const sampleSet = sampleId && hasSampleSetSelection
194
+ ? sampleIdToSetMap?.get(sampleId)
195
+ : null;
197
196
 
198
197
  if (hasSampleSetSelection && !sampleSet) {
199
198
  // Skip this sample if it is not in the selected sample set.
@@ -211,10 +210,20 @@ export function TreemapSubscriber(props) {
211
210
  ];
212
211
  }, [obsIndex, sampleEdges, sampleSets, obsSetColor,
213
212
  sampleSetColor, mergedObsSets, obsSetSelection, mergedSampleSets,
214
- sampleSetSelection,
213
+ sampleSetSelection, obsIndex,
215
214
  // TODO: consider filtering-related coordination values
216
215
  ]);
217
216
 
217
+ const totalObsCount = obsIndex?.length || 0;
218
+ const totalSampleCount = sampleIndex?.length || 0;
219
+
220
+ const selectedObsCount = obsCounts.reduce((a, h) => a + h.value, 0);
221
+ const selectedSampleCount = sampleCounts.reduce((a, h) => a + h.value, 0);
222
+
223
+ const unselectedObsCount = totalObsCount - selectedObsCount;
224
+ const unselectedSampleCount = totalSampleCount - selectedSampleCount;
225
+
226
+
218
227
  const onNodeClick = useCallback((obsSetPath) => {
219
228
  setObsSetSelection([obsSetPath]);
220
229
  }, [setObsSetSelection]);
@@ -222,12 +231,13 @@ export function TreemapSubscriber(props) {
222
231
  return (
223
232
  <TitleInfo
224
233
  title={`Treemap of ${capitalize(plur(obsType, 2))}`}
225
- info={`${commaNumber(obsCount)} ${plur(obsType, obsCount)} from ${commaNumber(sampleCount)} ${plur(sampleType, sampleCount)}`}
234
+ info={`${commaNumber(selectedObsCount)} ${plur(obsType, selectedObsCount)} from ${commaNumber(selectedSampleCount)} ${plur(sampleType, selectedSampleCount)}`}
226
235
  removeGridComponent={removeGridComponent}
227
236
  urls={urls}
228
237
  theme={theme}
229
238
  isReady={isReady}
230
239
  helpText={helpText}
240
+ withPadding={false}
231
241
  options={(
232
242
  <TreemapOptions
233
243
  obsType={obsType}
@@ -252,7 +262,7 @@ export function TreemapSubscriber(props) {
252
262
  hierarchyLevels={hierarchyLevels || DEFAULT_HIERARCHY_LEVELS}
253
263
  theme={theme}
254
264
  width={width}
255
- height={height}
265
+ height={Math.max(height * (selectedObsCount / totalObsCount), 40)}
256
266
  obsType={obsType}
257
267
  sampleType={sampleType}
258
268
  obsSetColor={obsSetColor}
@@ -262,6 +272,11 @@ export function TreemapSubscriber(props) {
262
272
  onNodeClick={onNodeClick}
263
273
  />
264
274
  </div>
275
+ <div style={{ position: 'absolute', right: '2px', bottom: '2px', fontSize: '10px' }}>
276
+ {unselectedObsCount > 0 ? (
277
+ <span>{`${commaNumber(unselectedObsCount)} ${plur(obsType, unselectedObsCount)} from ${commaNumber(unselectedSampleCount)} ${plur(sampleType, unselectedSampleCount)} currently omitted`}</span>
278
+ ) : null}
279
+ </div>
265
280
  </TitleInfo>
266
281
  );
267
282
  }
@@ -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/expr-hooks.js CHANGED
@@ -92,7 +92,6 @@ export function summarizeStratifiedExpressionData(
92
92
  stratifiedResult, keepZeros,
93
93
  ) {
94
94
  const summarizedResult = new InternMap([], JSON.stringify);
95
-
96
95
  Array.from(stratifiedResult.entries()).forEach(([cellSetKey, firstLevelInternMap]) => {
97
96
  summarizedResult.set(cellSetKey, new InternMap([], JSON.stringify));
98
97
  Array.from(firstLevelInternMap.entries()).forEach(([sampleSetKey, secondLevelInternMap]) => {
@@ -78,6 +78,7 @@ describe('Utility functions for processing expression data for statistical plots
78
78
  ];
79
79
  const featureValueTransform = null;
80
80
  const featureValueTransformCoefficient = 1;
81
+ const featureAggregationStrategy = 'first';
81
82
 
82
83
  const [result] = stratifyExpressionData(
83
84
  sampleEdges, sampleSets, sampleSetSelection,
@@ -86,7 +87,7 @@ describe('Utility functions for processing expression data for statistical plots
86
87
  featureValueTransform, featureValueTransformCoefficient,
87
88
  );
88
89
  const aggregateData = aggregateStratifiedExpressionData(
89
- result, geneSelection,
90
+ result, geneSelection, featureAggregationStrategy,
90
91
  );
91
92
  const summaryResult = summarizeStratifiedExpressionData(aggregateData, true);
92
93
 
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';