@vitessce/statistical-plots 3.5.7 → 3.5.9

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 (51) hide show
  1. package/dist/{deflate-287e693d.js → deflate-19841f78.js} +1 -1
  2. package/dist/{index-1f1b6355.js → index-dc733355.js} +2084 -1065
  3. package/dist/index.js +9 -6
  4. package/dist/{jpeg-1b2c1d25.js → jpeg-a83077be.js} +1 -1
  5. package/dist/{lerc-4f010cd7.js → lerc-1edd075a.js} +1 -1
  6. package/dist/{lzw-e60fb582.js → lzw-9572eac3.js} +1 -1
  7. package/dist/{packbits-a8bfe098.js → packbits-cce11fbc.js} +1 -1
  8. package/dist/{raw-01dff90e.js → raw-f7587aff.js} +1 -1
  9. package/dist/{webimage-6b926ce3.js → webimage-8d38cd8b.js} +1 -1
  10. package/dist-tsc/CellSetCompositionBarPlot.d.ts +5 -0
  11. package/dist-tsc/CellSetCompositionBarPlot.d.ts.map +1 -0
  12. package/dist-tsc/CellSetCompositionBarPlot.js +166 -0
  13. package/dist-tsc/CellSetCompositionBarPlotSubscriber.d.ts +2 -0
  14. package/dist-tsc/CellSetCompositionBarPlotSubscriber.d.ts.map +1 -0
  15. package/dist-tsc/CellSetCompositionBarPlotSubscriber.js +40 -0
  16. package/dist-tsc/FeatureSetEnrichmentBarPlot.d.ts +5 -0
  17. package/dist-tsc/FeatureSetEnrichmentBarPlot.d.ts.map +1 -0
  18. package/dist-tsc/FeatureSetEnrichmentBarPlot.js +164 -0
  19. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.d.ts +2 -0
  20. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.d.ts.map +1 -0
  21. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.js +51 -0
  22. package/dist-tsc/Treemap.d.ts.map +1 -1
  23. package/dist-tsc/Treemap.js +11 -15
  24. package/dist-tsc/TreemapSubscriber.d.ts.map +1 -1
  25. package/dist-tsc/TreemapSubscriber.js +6 -2
  26. package/dist-tsc/VolcanoPlot.d.ts +2 -0
  27. package/dist-tsc/VolcanoPlot.d.ts.map +1 -0
  28. package/dist-tsc/VolcanoPlot.js +231 -0
  29. package/dist-tsc/VolcanoPlotOptions.d.ts +2 -0
  30. package/dist-tsc/VolcanoPlotOptions.d.ts.map +1 -0
  31. package/dist-tsc/VolcanoPlotOptions.js +23 -0
  32. package/dist-tsc/VolcanoPlotSubscriber.d.ts +2 -0
  33. package/dist-tsc/VolcanoPlotSubscriber.d.ts.map +1 -0
  34. package/dist-tsc/VolcanoPlotSubscriber.js +33 -0
  35. package/dist-tsc/index.d.ts +3 -0
  36. package/dist-tsc/index.js +3 -0
  37. package/dist-tsc/utils.d.ts +9 -0
  38. package/dist-tsc/utils.d.ts.map +1 -0
  39. package/dist-tsc/utils.js +40 -0
  40. package/package.json +7 -7
  41. package/src/CellSetCompositionBarPlot.js +205 -0
  42. package/src/CellSetCompositionBarPlotSubscriber.js +151 -0
  43. package/src/FeatureSetEnrichmentBarPlot.js +203 -0
  44. package/src/FeatureSetEnrichmentBarPlotSubscriber.js +166 -0
  45. package/src/Treemap.js +12 -19
  46. package/src/TreemapSubscriber.js +7 -1
  47. package/src/VolcanoPlot.js +316 -0
  48. package/src/VolcanoPlotOptions.js +136 -0
  49. package/src/VolcanoPlotSubscriber.js +162 -0
  50. package/src/index.js +3 -0
  51. package/src/utils.js +47 -0
@@ -0,0 +1,316 @@
1
+ /* eslint-disable indent */
2
+ /* eslint-disable camelcase */
3
+ import React, { useMemo, useEffect, useRef } from 'react';
4
+ import { scaleLinear } from 'd3-scale';
5
+ import { axisBottom, axisLeft } from 'd3-axis';
6
+ import { extent as d3_extent } from 'd3-array';
7
+ import { select } from 'd3-selection';
8
+ import { isEqual } from 'lodash-es';
9
+ import { capitalize, getDefaultForegroundColor } from '@vitessce/utils';
10
+ import { colorArrayToString } from '@vitessce/sets-utils';
11
+ import { getColorScale } from './utils.js';
12
+
13
+ export default function VolcanoPlot(props) {
14
+ const {
15
+ theme,
16
+ width,
17
+ height,
18
+ obsType,
19
+ featureType,
20
+ obsSetsColumnNameMapping,
21
+ sampleSetsColumnNameMapping,
22
+ sampleSetSelection,
23
+ obsSetSelection,
24
+ obsSetColor,
25
+ sampleSetColor,
26
+ data,
27
+ marginTop = 5,
28
+ marginRight = 5,
29
+ marginLeft = 50,
30
+ marginBottom = 50,
31
+ onFeatureClick,
32
+ featurePointSignificanceThreshold,
33
+ featurePointFoldChangeThreshold,
34
+ featureLabelSignificanceThreshold,
35
+ featureLabelFoldChangeThreshold,
36
+ } = props;
37
+
38
+ const svgRef = useRef();
39
+
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]);
48
+
49
+ const [xExtent, yExtent] = useMemo(() => {
50
+ if (!computedData) {
51
+ return [null, null];
52
+ }
53
+ let xExtentResult = d3_extent(
54
+ computedData.flatMap(d => d3_extent(d.df.logFoldChange)),
55
+ );
56
+ const xAbsMax = Math.max(Math.abs(xExtentResult[0]), Math.abs(xExtentResult[1]));
57
+ xExtentResult = [-xAbsMax, xAbsMax];
58
+
59
+ const yExtentResult = d3_extent(
60
+ computedData.flatMap(d => d3_extent(d.df.minusLog10p.filter(v => Number.isFinite(v)))),
61
+ );
62
+ return [xExtentResult, yExtentResult];
63
+ }, [computedData]);
64
+
65
+ const [obsSetColorScale, sampleSetColorScale] = useMemo(() => [
66
+ getColorScale(obsSetSelection, obsSetColor, theme),
67
+ getColorScale(sampleSetSelection, sampleSetColor, theme),
68
+ ], [obsSetSelection, sampleSetSelection, sampleSetColor, obsSetColor, theme]);
69
+
70
+ useEffect(() => {
71
+ const domElement = svgRef.current;
72
+
73
+ const svg = select(domElement);
74
+ svg.selectAll('g').remove();
75
+ svg
76
+ .attr('width', width)
77
+ .attr('height', height)
78
+ .attr('viewBox', [0, 0, width, height])
79
+ .attr('style', 'font: 10px sans-serif');
80
+
81
+ if (!computedData || !xExtent || !yExtent) {
82
+ return;
83
+ }
84
+
85
+ // Render scatterplot
86
+ const innerWidth = width - marginLeft;
87
+ const innerHeight = height - marginBottom;
88
+
89
+ const xScale = scaleLinear()
90
+ .range([marginLeft, width - marginRight])
91
+ .domain(xExtent);
92
+
93
+ // For the y domain, use the yMin prop
94
+ // to support a use case such as 'Aspect Ratio',
95
+ // where the domain minimum should be 1 rather than 0.
96
+ const yScale = scaleLinear()
97
+ .domain(yExtent)
98
+ .range([innerHeight, marginTop])
99
+ .clamp(true);
100
+
101
+ // Add the axes.
102
+ svg.append('g')
103
+ .attr('transform', `translate(0,${height - marginBottom})`)
104
+ .call(axisBottom(xScale));
105
+
106
+ svg.append('g')
107
+ .attr('transform', `translate(${marginLeft},0)`)
108
+ .call(axisLeft(yScale));
109
+
110
+ // Axis titles
111
+ const titleG = svg.append('g');
112
+ const fgColor = colorArrayToString(
113
+ getDefaultForegroundColor(theme),
114
+ );
115
+
116
+ // Y-axis title
117
+ titleG
118
+ .append('text')
119
+ .attr('text-anchor', 'middle')
120
+ .attr('x', -innerHeight / 2)
121
+ .attr('y', 15)
122
+ .attr('transform', 'rotate(-90)')
123
+ .text('-log10 p-value')
124
+ .style('font-size', '12px')
125
+ .style('fill', fgColor);
126
+
127
+ // X-axis title
128
+ titleG
129
+ .append('text')
130
+ .attr('text-anchor', 'middle')
131
+ .attr('x', marginLeft + innerWidth / 2)
132
+ .attr('y', height - 10)
133
+ .text('log2 fold-change')
134
+ .style('font-size', '12px')
135
+ .style('fill', fgColor);
136
+
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
+ // Horizontal and vertical rules to indicate currently-selected thresholds
151
+ // Vertical lines
152
+ const ruleColor = 'silver';
153
+ const ruleDash = '2,2';
154
+ titleG.append('line')
155
+ .attr('x1', xScale(featurePointFoldChangeThreshold))
156
+ .attr('x2', xScale(featurePointFoldChangeThreshold))
157
+ .attr('y1', yScale.range()[0])
158
+ .attr('y2', yScale.range()[1])
159
+ .style('stroke', ruleColor)
160
+ .style('stroke-dasharray', ruleDash);
161
+ titleG.append('line')
162
+ .attr('x1', xScale(-featurePointFoldChangeThreshold))
163
+ .attr('x2', xScale(-featurePointFoldChangeThreshold))
164
+ .attr('y1', yScale.range()[0])
165
+ .attr('y2', yScale.range()[1])
166
+ .style('stroke', ruleColor)
167
+ .style('stroke-dasharray', ruleDash);
168
+ // Horizontal lines
169
+ titleG.append('line')
170
+ .attr('x1', xScale.range()[0])
171
+ .attr('x2', xScale.range()[1])
172
+ .attr('y1', yScale(-Math.log10(featurePointSignificanceThreshold)))
173
+ .attr('y2', yScale(-Math.log10(featurePointSignificanceThreshold)))
174
+ .style('stroke', ruleColor)
175
+ .style('stroke-dasharray', ruleDash);
176
+
177
+
178
+ // Upregulated/downregulated and sampleSet directional indicators.
179
+ const lhsText = sampleSetSelection && sampleSetSelection.length === 2
180
+ ? sampleSetSelection[0].at(-1)
181
+ : '__rest__';
182
+
183
+ // eslint-disable-next-line no-nested-ternary
184
+ const rhsText = sampleSetSelection && sampleSetSelection.length === 2
185
+ ? sampleSetSelection[1].at(-1)
186
+ : (obsSetSelection && obsSetSelection.length === 1
187
+ ? obsSetSelection?.[0]?.at(-1)
188
+ : `${capitalize(obsType)} Set`
189
+ );
190
+
191
+
192
+ titleG
193
+ .append('text')
194
+ .attr('text-anchor', 'start')
195
+ .attr('x', marginLeft)
196
+ .attr('y', height - 10)
197
+ .text(`\u2190 ${lhsText}`)
198
+ .style('font-size', '12px')
199
+ .style('fill', fgColor);
200
+
201
+ titleG
202
+ .append('text')
203
+ .attr('text-anchor', 'end')
204
+ .attr('x', marginLeft + innerWidth)
205
+ .attr('y', height - 10)
206
+ .text(`${rhsText} \u2192`)
207
+ .style('font-size', '12px')
208
+ .style('fill', fgColor);
209
+
210
+
211
+ const g = svg.append('g');
212
+
213
+ // Append a circle for each data point.
214
+ computedData.forEach((comparisonObject) => {
215
+ const obsSetG = g.append('g');
216
+
217
+ const { df, metadata } = comparisonObject;
218
+ const coordinationValues = metadata.coordination_values;
219
+
220
+ const rawObsSetPath = coordinationValues.obsSetFilter
221
+ ? coordinationValues.obsSetFilter[0]
222
+ : coordinationValues.obsSetSelection[0];
223
+ const obsSetPath = [...rawObsSetPath];
224
+ obsSetPath[0] = obsSetsColumnNameMappingReversed[rawObsSetPath[0]];
225
+
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
+ const color = obsSetColorScale(obsSetPath);
261
+
262
+ obsSetG.append('g')
263
+ .selectAll('circle')
264
+ .data(filteredDf)
265
+ .join('circle')
266
+ .attr('cx', d => xScale(d.logFoldChange))
267
+ .attr('cy', d => yScale(d.minusLog10p))
268
+ .attr('r', 3)
269
+ .attr('opacity', 0.5)
270
+ .attr('fill', color)
271
+ .on('click', (event, d) => {
272
+ onFeatureClick(d.featureId);
273
+ });
274
+
275
+ const textElements = obsSetG.append('g')
276
+ .selectAll('text')
277
+ .data(filteredDf)
278
+ .join('text')
279
+ .text(d => d.featureId)
280
+ .attr('text-anchor', d => (d.logFoldChange < 0 ? 'end' : 'start'))
281
+ .attr('x', d => xScale(d.logFoldChange))
282
+ .attr('y', d => yScale(d.minusLog10p))
283
+ .style('display', d => ((
284
+ Math.abs(d.logFoldChange) < (featureLabelFoldChangeThreshold ?? 5.0)
285
+ || (d.featureSignificance >= (featureLabelSignificanceThreshold ?? 0.01))
286
+ ) ? 'none' : undefined))
287
+ .attr('fill', color)
288
+ .on('click', (event, d) => {
289
+ onFeatureClick(d.featureId);
290
+ });
291
+
292
+ textElements.append('title')
293
+ .text(d => `${featureType}: ${d.featureId}\nin ${obsSetPath?.at(-1)}\nlog2 fold-change: ${d.logFoldChange}\np-value: ${d.featureSignificance}`);
294
+ });
295
+ }, [width, height, theme, sampleSetColor, sampleSetSelection,
296
+ obsSetSelection, obsSetColor, featureType, computedData,
297
+ xExtent, yExtent, obsType,
298
+ marginLeft, marginBottom, marginTop, marginRight,
299
+ obsSetColorScale, sampleSetColorScale, onFeatureClick,
300
+ featurePointSignificanceThreshold, featurePointFoldChangeThreshold,
301
+ featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold,
302
+ ]);
303
+
304
+ return (
305
+ <svg
306
+ ref={svgRef}
307
+ style={{
308
+ top: 0,
309
+ left: 0,
310
+ width: `${width}px`,
311
+ height: `${height}px`,
312
+ position: 'relative',
313
+ }}
314
+ />
315
+ );
316
+ }
@@ -0,0 +1,136 @@
1
+ import React from 'react';
2
+ import { useId } from 'react-aria';
3
+ import { TableCell, TableRow, Slider } from '@material-ui/core';
4
+ import {
5
+ usePlotOptionsStyles, OptionsContainer,
6
+ } from '@vitessce/vit-s';
7
+
8
+ export default function VolcanoPlotOptions(props) {
9
+ const {
10
+ children,
11
+
12
+ featurePointSignificanceThreshold,
13
+ featurePointFoldChangeThreshold,
14
+ featureLabelSignificanceThreshold,
15
+ featureLabelFoldChangeThreshold,
16
+
17
+ setFeaturePointSignificanceThreshold,
18
+ setFeaturePointFoldChangeThreshold,
19
+ setFeatureLabelSignificanceThreshold,
20
+ setFeatureLabelFoldChangeThreshold,
21
+ } = props;
22
+
23
+ const volcanoOptionsId = useId();
24
+ const classes = usePlotOptionsStyles();
25
+
26
+ function handlePointSignificanceChange(event, value) {
27
+ setFeaturePointSignificanceThreshold(10 ** -value);
28
+ }
29
+
30
+ function handlePointFoldChangeChange(event, value) {
31
+ setFeaturePointFoldChangeThreshold(value);
32
+ }
33
+
34
+ function handleLabelSignificanceChange(event, value) {
35
+ setFeatureLabelSignificanceThreshold(10 ** -value);
36
+ }
37
+
38
+ function handleLabelFoldChangeChange(event, value) {
39
+ setFeatureLabelFoldChangeThreshold(value);
40
+ }
41
+
42
+
43
+ return (
44
+ <OptionsContainer>
45
+ {children}
46
+ <TableRow>
47
+ <TableCell className={classes.labelCell} variant="head" scope="row">
48
+ <label
49
+ htmlFor={`volcano-label-significance-${volcanoOptionsId}`}
50
+ >
51
+ Label Significance Threshold
52
+ </label>
53
+ </TableCell>
54
+ <TableCell className={classes.inputCell} variant="body">
55
+ <Slider
56
+ classes={{ root: classes.slider, valueLabel: classes.sliderValueLabel }}
57
+ value={-Math.log10(featureLabelSignificanceThreshold)}
58
+ onChange={handleLabelSignificanceChange}
59
+ aria-label="Volcano plot label significance threshold slider"
60
+ id={`volcano-label-significance-${volcanoOptionsId}`}
61
+ valueLabelDisplay="auto"
62
+ step={1}
63
+ min={0}
64
+ max={100}
65
+ />
66
+ </TableCell>
67
+ </TableRow>
68
+ <TableRow>
69
+ <TableCell className={classes.labelCell} variant="head" scope="row">
70
+ <label
71
+ htmlFor={`volcano-label-fc-${volcanoOptionsId}`}
72
+ >
73
+ Label Fold-Change Threshold
74
+ </label>
75
+ </TableCell>
76
+ <TableCell className={classes.inputCell} variant="body">
77
+ <Slider
78
+ classes={{ root: classes.slider, valueLabel: classes.sliderValueLabel }}
79
+ value={featureLabelFoldChangeThreshold}
80
+ onChange={handleLabelFoldChangeChange}
81
+ aria-label="Volcano plot label fold-change threshold slider"
82
+ id={`volcano-label-fc-${volcanoOptionsId}`}
83
+ valueLabelDisplay="auto"
84
+ step={0.5}
85
+ min={0}
86
+ max={50}
87
+ />
88
+ </TableCell>
89
+ </TableRow>
90
+ <TableRow>
91
+ <TableCell className={classes.labelCell} variant="head" scope="row">
92
+ <label
93
+ htmlFor={`volcano-point-significance-${volcanoOptionsId}`}
94
+ >
95
+ Point Significance Threshold
96
+ </label>
97
+ </TableCell>
98
+ <TableCell className={classes.inputCell} variant="body">
99
+ <Slider
100
+ classes={{ root: classes.slider, valueLabel: classes.sliderValueLabel }}
101
+ value={-Math.log10(featurePointSignificanceThreshold)}
102
+ onChange={handlePointSignificanceChange}
103
+ aria-label="Volcano plot point significance threshold slider"
104
+ id={`volcano-point-significance-${volcanoOptionsId}`}
105
+ valueLabelDisplay="auto"
106
+ step={1}
107
+ min={0}
108
+ max={100}
109
+ />
110
+ </TableCell>
111
+ </TableRow>
112
+ <TableRow>
113
+ <TableCell className={classes.labelCell} variant="head" scope="row">
114
+ <label
115
+ htmlFor={`volcano-point-fc-${volcanoOptionsId}`}
116
+ >
117
+ Point Fold-Change Threshold
118
+ </label>
119
+ </TableCell>
120
+ <TableCell className={classes.inputCell} variant="body">
121
+ <Slider
122
+ classes={{ root: classes.slider, valueLabel: classes.sliderValueLabel }}
123
+ value={featurePointFoldChangeThreshold}
124
+ onChange={handlePointFoldChangeChange}
125
+ aria-label="Volcano plot point fold-change threshold slider"
126
+ id={`volcano-point-fc-${volcanoOptionsId}`}
127
+ valueLabelDisplay="auto"
128
+ step={0.5}
129
+ min={0}
130
+ max={50}
131
+ />
132
+ </TableCell>
133
+ </TableRow>
134
+ </OptionsContainer>
135
+ );
136
+ }
@@ -0,0 +1,162 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import React, { useMemo, useCallback } from 'react';
3
+ import {
4
+ TitleInfo,
5
+ useCoordination,
6
+ useLoaders,
7
+ useReady,
8
+ useGridItemSize,
9
+ useFeatureStatsData,
10
+ useMatchingLoader,
11
+ useColumnNameMapping,
12
+ } from '@vitessce/vit-s';
13
+ import {
14
+ ViewType,
15
+ COMPONENT_COORDINATION_TYPES,
16
+ ViewHelpMapping,
17
+ DataType,
18
+ } from '@vitessce/constants-internal';
19
+ import VolcanoPlot from './VolcanoPlot.js';
20
+ import { useStyles } from './styles.js';
21
+ import VolcanoPlotOptions from './VolcanoPlotOptions.js';
22
+ import { useRawSetPaths } from './utils.js';
23
+
24
+ export function VolcanoPlotSubscriber(props) {
25
+ const {
26
+ coordinationScopes,
27
+ removeGridComponent,
28
+ theme,
29
+ helpText = ViewHelpMapping.VOLCANO_PLOT,
30
+ } = props;
31
+
32
+ const classes = useStyles();
33
+ const loaders = useLoaders();
34
+
35
+ // Get "props" from the coordination space.
36
+ const [{
37
+ dataset,
38
+ obsType,
39
+ sampleType,
40
+ featureType,
41
+ featureValueType,
42
+ obsFilter: cellFilter,
43
+ obsHighlight: cellHighlight,
44
+ obsSetSelection,
45
+ obsSetColor,
46
+ obsColorEncoding: cellColorEncoding,
47
+ additionalObsSets: additionalCellSets,
48
+ featurePointSignificanceThreshold,
49
+ featurePointFoldChangeThreshold,
50
+ featureLabelSignificanceThreshold,
51
+ featureLabelFoldChangeThreshold,
52
+ featureValueTransform,
53
+ featureValueTransformCoefficient,
54
+ gatingFeatureSelectionX,
55
+ gatingFeatureSelectionY,
56
+ featureSelection,
57
+ sampleSetSelection,
58
+ sampleSetColor,
59
+ }, {
60
+ setObsFilter: setCellFilter,
61
+ setObsSetSelection,
62
+ setObsHighlight: setCellHighlight,
63
+ setObsSetColor: setCellSetColor,
64
+ setObsColorEncoding: setCellColorEncoding,
65
+ setAdditionalObsSets: setAdditionalCellSets,
66
+ setFeaturePointSignificanceThreshold,
67
+ setFeaturePointFoldChangeThreshold,
68
+ setFeatureLabelSignificanceThreshold,
69
+ setFeatureLabelFoldChangeThreshold,
70
+ setFeatureValueTransform,
71
+ setFeatureValueTransformCoefficient,
72
+ setGatingFeatureSelectionX,
73
+ setGatingFeatureSelectionY,
74
+ setFeatureSelection,
75
+ setSampleSetSelection,
76
+ setSampleSetColor,
77
+ }] = useCoordination(
78
+ COMPONENT_COORDINATION_TYPES[ViewType.VOLCANO_PLOT],
79
+ coordinationScopes,
80
+ );
81
+ const [width, height, containerRef] = useGridItemSize();
82
+
83
+ const obsSetsLoader = useMatchingLoader(
84
+ loaders, dataset, DataType.OBS_SETS, { obsType },
85
+ );
86
+ const sampleSetsLoader = useMatchingLoader(
87
+ loaders, dataset, DataType.SAMPLE_SETS, { sampleType },
88
+ );
89
+ const obsSetsColumnNameMapping = useColumnNameMapping(obsSetsLoader);
90
+ const sampleSetsColumnNameMapping = useColumnNameMapping(sampleSetsLoader);
91
+
92
+ const rawSampleSetSelection = useRawSetPaths(sampleSetsColumnNameMapping, sampleSetSelection);
93
+ const rawObsSetSelection = useRawSetPaths(obsSetsColumnNameMapping, obsSetSelection);
94
+
95
+ const [{ featureStats }, featureStatsStatus] = useFeatureStatsData(
96
+ loaders, dataset, false,
97
+ { obsType, featureType, sampleType },
98
+ // These volcanoOptions are passed to FeatureStatsAnndataLoader.loadMulti():
99
+ { sampleSetSelection: rawSampleSetSelection, obsSetSelection: rawObsSetSelection },
100
+ );
101
+
102
+ const isReady = useReady([
103
+ featureStatsStatus,
104
+ ]);
105
+
106
+ const onFeatureClick = useCallback((featureId) => {
107
+ setFeatureSelection([featureId]);
108
+ }, [setFeatureSelection]);
109
+
110
+ return (
111
+ <TitleInfo
112
+ title="Volcano Plot"
113
+ removeGridComponent={removeGridComponent}
114
+ theme={theme}
115
+ isReady={isReady}
116
+ helpText={helpText}
117
+ options={(
118
+ <VolcanoPlotOptions
119
+ obsType={obsType}
120
+ featureType={featureType}
121
+
122
+ featurePointSignificanceThreshold={featurePointSignificanceThreshold}
123
+ featurePointFoldChangeThreshold={featurePointFoldChangeThreshold}
124
+ featureLabelSignificanceThreshold={featureLabelSignificanceThreshold}
125
+ featureLabelFoldChangeThreshold={featureLabelFoldChangeThreshold}
126
+
127
+ setFeaturePointSignificanceThreshold={setFeaturePointSignificanceThreshold}
128
+ setFeaturePointFoldChangeThreshold={setFeaturePointFoldChangeThreshold}
129
+ setFeatureLabelSignificanceThreshold={setFeatureLabelSignificanceThreshold}
130
+ setFeatureLabelFoldChangeThreshold={setFeatureLabelFoldChangeThreshold}
131
+ />
132
+ )}
133
+ >
134
+ <div ref={containerRef} className={classes.vegaContainer}>
135
+ {featureStats ? (
136
+ <VolcanoPlot
137
+ theme={theme}
138
+ width={width}
139
+ height={height}
140
+ obsType={obsType}
141
+ featureType={featureType}
142
+ obsSetsColumnNameMapping={obsSetsColumnNameMapping}
143
+ sampleSetsColumnNameMapping={sampleSetsColumnNameMapping}
144
+ sampleSetSelection={sampleSetSelection}
145
+ obsSetSelection={obsSetSelection}
146
+ obsSetColor={obsSetColor}
147
+ sampleSetColor={sampleSetColor}
148
+ data={featureStats}
149
+ onFeatureClick={onFeatureClick}
150
+
151
+ featurePointSignificanceThreshold={featurePointSignificanceThreshold}
152
+ featurePointFoldChangeThreshold={featurePointFoldChangeThreshold}
153
+ featureLabelSignificanceThreshold={featureLabelSignificanceThreshold}
154
+ featureLabelFoldChangeThreshold={featureLabelFoldChangeThreshold}
155
+ />
156
+ ) : (
157
+ <span>Select at least one {obsType} set.</span>
158
+ )}
159
+ </div>
160
+ </TitleInfo>
161
+ );
162
+ }
package/src/index.js CHANGED
@@ -4,6 +4,9 @@ export { ExpressionHistogramSubscriber } from './ExpressionHistogramSubscriber.j
4
4
  export { DotPlotSubscriber } from './DotPlotSubscriber.js';
5
5
  export { FeatureBarPlotSubscriber } from './FeatureBarPlotSubscriber.js';
6
6
  export { TreemapSubscriber } from './TreemapSubscriber.js';
7
+ export { VolcanoPlotSubscriber } from './VolcanoPlotSubscriber.js';
8
+ export { CellSetCompositionBarPlotSubscriber } from './CellSetCompositionBarPlotSubscriber.js';
9
+ export { FeatureSetEnrichmentBarPlotSubscriber } from './FeatureSetEnrichmentBarPlotSubscriber.js';
7
10
  export { default as CellSetSizesPlot } from './CellSetSizesPlot.js';
8
11
  export { default as CellSetExpressionPlot } from './CellSetExpressionPlot.js';
9
12
  export { default as ExpressionHistogram } from './ExpressionHistogram.js';
package/src/utils.js ADDED
@@ -0,0 +1,47 @@
1
+ import { useMemo } from 'react';
2
+ import { isEqual } from 'lodash-es';
3
+ import { colorArrayToString } from '@vitessce/sets-utils';
4
+ import { getDefaultColor } from '@vitessce/utils';
5
+
6
+
7
+ function createOrdinalScale(domainArr, rangeArr) {
8
+ return (queryVal) => {
9
+ const i = domainArr.findIndex(domainVal => isEqual(domainVal, queryVal));
10
+ return rangeArr[i];
11
+ };
12
+ }
13
+
14
+ // Create a d3-scale ordinal scale mapping set paths to color strings.
15
+ export function getColorScale(setSelectionArr, setColorArr, theme) {
16
+ /*
17
+ // The equality seems incorrect with d3.scaleOrdinal
18
+ return scaleOrdinal()
19
+ .domain(setSelectionArr || [])
20
+ .range(
21
+ */
22
+ const domainArr = setSelectionArr || [];
23
+ const rangeArr = setSelectionArr
24
+ ?.map(setNamePath => (
25
+ setColorArr?.find(d => isEqual(d.path, setNamePath))?.color
26
+ || getDefaultColor(theme)
27
+ ))
28
+ ?.map(colorArrayToString) || [];
29
+ return createOrdinalScale(domainArr, rangeArr);
30
+ }
31
+
32
+
33
+ /**
34
+ * Transform set paths which use group names to those which use column names.
35
+ * @param {Record<string, string>} columnNameMapping Return value of useColumnNameMapping.
36
+ * @param {string[][]} setPaths Array of set paths, such as obsSetSelection.
37
+ * @returns {string[][]} Transformed set paths.
38
+ */
39
+ export function useRawSetPaths(columnNameMapping, setPaths) {
40
+ return useMemo(() => setPaths?.map((setPath) => {
41
+ const newSetPath = [...setPath];
42
+ if (newSetPath?.[0] && columnNameMapping[newSetPath[0]]) {
43
+ newSetPath[0] = columnNameMapping[newSetPath[0]];
44
+ }
45
+ return newSetPath;
46
+ }), [columnNameMapping, setPaths]);
47
+ }