@vitessce/statistical-plots 3.5.6 → 3.5.8

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 (60) hide show
  1. package/dist/{deflate-70ede287.js → deflate-1679ef33.js} +1 -1
  2. package/dist/{index-07a7a93a.js → index-0f4fe21d.js} +2926 -1090
  3. package/dist/index.js +10 -6
  4. package/dist/{jpeg-00a52550.js → jpeg-280f0ee1.js} +1 -1
  5. package/dist/{lerc-55c1ff7e.js → lerc-12264a36.js} +1 -1
  6. package/dist/{lzw-b906a3b9.js → lzw-70f852cc.js} +1 -1
  7. package/dist/{packbits-10b5f4aa.js → packbits-393c67b2.js} +1 -1
  8. package/dist/{raw-9317b0fd.js → raw-d8d7ab7f.js} +1 -1
  9. package/dist/{webimage-1c43145b.js → webimage-5d24a8e2.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/CellSetExpressionPlot.d.ts.map +1 -1
  17. package/dist-tsc/CellSetExpressionPlot.js +2 -1
  18. package/dist-tsc/FeatureSetEnrichmentBarPlot.d.ts +5 -0
  19. package/dist-tsc/FeatureSetEnrichmentBarPlot.d.ts.map +1 -0
  20. package/dist-tsc/FeatureSetEnrichmentBarPlot.js +164 -0
  21. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.d.ts +2 -0
  22. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.d.ts.map +1 -0
  23. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.js +51 -0
  24. package/dist-tsc/Treemap.d.ts +11 -0
  25. package/dist-tsc/Treemap.d.ts.map +1 -0
  26. package/dist-tsc/Treemap.js +154 -0
  27. package/dist-tsc/TreemapOptions.d.ts +2 -0
  28. package/dist-tsc/TreemapOptions.d.ts.map +1 -0
  29. package/dist-tsc/TreemapOptions.js +29 -0
  30. package/dist-tsc/TreemapSubscriber.d.ts +2 -0
  31. package/dist-tsc/TreemapSubscriber.d.ts.map +1 -0
  32. package/dist-tsc/TreemapSubscriber.js +103 -0
  33. package/dist-tsc/VolcanoPlot.d.ts +2 -0
  34. package/dist-tsc/VolcanoPlot.d.ts.map +1 -0
  35. package/dist-tsc/VolcanoPlot.js +230 -0
  36. package/dist-tsc/VolcanoPlotOptions.d.ts +2 -0
  37. package/dist-tsc/VolcanoPlotOptions.d.ts.map +1 -0
  38. package/dist-tsc/VolcanoPlotOptions.js +23 -0
  39. package/dist-tsc/VolcanoPlotSubscriber.d.ts +2 -0
  40. package/dist-tsc/VolcanoPlotSubscriber.d.ts.map +1 -0
  41. package/dist-tsc/VolcanoPlotSubscriber.js +33 -0
  42. package/dist-tsc/index.d.ts +4 -0
  43. package/dist-tsc/index.js +4 -0
  44. package/dist-tsc/utils.d.ts +9 -0
  45. package/dist-tsc/utils.d.ts.map +1 -0
  46. package/dist-tsc/utils.js +40 -0
  47. package/package.json +8 -7
  48. package/src/CellSetCompositionBarPlot.js +205 -0
  49. package/src/CellSetCompositionBarPlotSubscriber.js +151 -0
  50. package/src/CellSetExpressionPlot.js +4 -1
  51. package/src/FeatureSetEnrichmentBarPlot.js +203 -0
  52. package/src/FeatureSetEnrichmentBarPlotSubscriber.js +166 -0
  53. package/src/Treemap.js +202 -0
  54. package/src/TreemapOptions.js +90 -0
  55. package/src/TreemapSubscriber.js +262 -0
  56. package/src/VolcanoPlot.js +313 -0
  57. package/src/VolcanoPlotOptions.js +136 -0
  58. package/src/VolcanoPlotSubscriber.js +162 -0
  59. package/src/index.js +4 -0
  60. package/src/utils.js +47 -0
@@ -0,0 +1,262 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import React, { useMemo } from 'react';
3
+ import {
4
+ TitleInfo,
5
+ useCoordination,
6
+ useLoaders,
7
+ useUrls,
8
+ useReady,
9
+ useGridItemSize,
10
+ useObsFeatureMatrixIndices,
11
+ useObsSetsData,
12
+ useSampleEdgesData,
13
+ useSampleSetsData,
14
+ } from '@vitessce/vit-s';
15
+ import { ViewType, COMPONENT_COORDINATION_TYPES, ViewHelpMapping } from '@vitessce/constants-internal';
16
+ import { treeToSelectedSetMap, treeToSetSizesBySetNames, mergeObsSets } from '@vitessce/sets-utils';
17
+ import { pluralize as plur, commaNumber, unnestMap, capitalize } from '@vitessce/utils';
18
+ import { InternMap } from 'internmap';
19
+ import { isEqual } from 'lodash-es';
20
+ import Treemap from './Treemap.js';
21
+ import { useStyles } from './styles.js';
22
+ import TreemapOptions from './TreemapOptions.js';
23
+
24
+ const DEFAULT_HIERARCHY_LEVELS = ['obsSet', 'sampleSet'];
25
+
26
+ export function TreemapSubscriber(props) {
27
+ const {
28
+ coordinationScopes,
29
+ removeGridComponent,
30
+ theme,
31
+ helpText = ViewHelpMapping.TREEMAP,
32
+ } = props;
33
+
34
+ const classes = useStyles();
35
+ const loaders = useLoaders();
36
+
37
+ // Get "props" from the coordination space.
38
+ const [{
39
+ dataset,
40
+ obsType,
41
+ featureType,
42
+ featureValueType,
43
+ obsFilter,
44
+ obsHighlight,
45
+ obsSetSelection,
46
+ obsSetFilter,
47
+ obsSelection,
48
+ obsSelectionMode,
49
+ obsSetHighlight,
50
+ obsSetColor,
51
+ obsColorEncoding,
52
+ additionalObsSets,
53
+ sampleType,
54
+ sampleSetSelection,
55
+ sampleSetFilter,
56
+ sampleSetColor,
57
+ sampleSelection,
58
+ sampleSelectionMode,
59
+ sampleFilter,
60
+ sampleFilterMode,
61
+ sampleHighlight,
62
+ hierarchyLevels,
63
+ }, {
64
+ setObsFilter,
65
+ setObsSelection,
66
+ setObsSetFilter,
67
+ setObsSetSelection,
68
+ setObsSelectionMode,
69
+ setObsFilterMode,
70
+ setObsHighlight,
71
+ setObsSetColor,
72
+ setObsColorEncoding,
73
+ setAdditionalObsSets,
74
+ setSampleFilter,
75
+ setSampleSetFilter,
76
+ setSampleFilterMode,
77
+ setSampleSelection,
78
+ setSampleSetSelection,
79
+ setSampleSelectionMode,
80
+ setSampleHighlight,
81
+ setSampleSetColor,
82
+ setHierarchyLevels,
83
+ }] = useCoordination(
84
+ COMPONENT_COORDINATION_TYPES[ViewType.TREEMAP],
85
+ coordinationScopes,
86
+ );
87
+
88
+ const [width, height, containerRef] = useGridItemSize();
89
+
90
+ // TODO: how to deal with multimodal cases (multiple obsIndex, one per modality)?
91
+ const [{ obsIndex }, matrixIndicesStatus, matrixIndicesUrls] = useObsFeatureMatrixIndices(
92
+ loaders, dataset, false,
93
+ { obsType, featureType, featureValueType },
94
+ );
95
+ const [{ obsSets }, obsSetsStatus, obsSetsUrls] = useObsSetsData(
96
+ loaders, dataset, true, {}, {},
97
+ { obsType },
98
+ );
99
+
100
+ const [{ sampleIndex, sampleSets }, sampleSetsStatus, sampleSetsUrls] = useSampleSetsData(
101
+ loaders,
102
+ dataset,
103
+ // TODO: support `false`, i.e., configurations in which
104
+ // there are no sampleSets
105
+ true,
106
+ { setSampleSetColor },
107
+ { sampleSetColor },
108
+ { sampleType },
109
+ );
110
+
111
+ const [{ sampleEdges }, sampleEdgesStatus, sampleEdgesUrls] = useSampleEdgesData(
112
+ loaders,
113
+ dataset,
114
+ // TODO: support `false`, i.e., configurations in which
115
+ // there are no sampleEdges
116
+ true,
117
+ {},
118
+ {},
119
+ { obsType, sampleType },
120
+ );
121
+
122
+ const isReady = useReady([
123
+ matrixIndicesStatus,
124
+ obsSetsStatus,
125
+ sampleSetsStatus,
126
+ sampleEdgesStatus,
127
+ ]);
128
+ const urls = useUrls([
129
+ matrixIndicesUrls,
130
+ obsSetsUrls,
131
+ sampleSetsUrls,
132
+ sampleEdgesUrls,
133
+ ]);
134
+
135
+ const mergedObsSets = useMemo(
136
+ () => mergeObsSets(obsSets, additionalObsSets),
137
+ [obsSets, additionalObsSets],
138
+ );
139
+ const mergedSampleSets = useMemo(
140
+ () => mergeObsSets(sampleSets, null),
141
+ [sampleSets],
142
+ );
143
+
144
+ const obsCount = obsIndex?.length || 0;
145
+ const sampleCount = sampleIndex?.length || 0;
146
+
147
+ // TODO: use obsFilter / sampleFilter to display
148
+ // _all_ cells/samples in gray / transparent in background,
149
+ // and use obsSetSelection/sampleSetSelection to display
150
+ // the _selected_ samples in color in the foreground.
151
+ const [obsCounts, sampleCounts] = useMemo(() => {
152
+ const obsResult = new InternMap([], JSON.stringify);
153
+ const sampleResult = new InternMap([], JSON.stringify);
154
+
155
+ const hasSampleSetSelection = (
156
+ Array.isArray(sampleSetSelection)
157
+ && sampleSetSelection.length > 0
158
+ );
159
+ const hasCellSetSelection = (
160
+ Array.isArray(obsSetSelection)
161
+ && obsSetSelection.length > 0
162
+ );
163
+
164
+ const sampleSetKeys = hasSampleSetSelection ? sampleSetSelection : [null];
165
+ const cellSetKeys = hasCellSetSelection ? obsSetSelection : [null];
166
+
167
+ // First level: cell set
168
+ cellSetKeys.forEach((cellSetKey) => {
169
+ obsResult.set(cellSetKey, new InternMap([], JSON.stringify));
170
+ // Second level: sample set
171
+ sampleSetKeys.forEach((sampleSetKey) => {
172
+ obsResult.get(cellSetKey).set(sampleSetKey, 0);
173
+ });
174
+ });
175
+
176
+ const sampleSetSizes = treeToSetSizesBySetNames(
177
+ mergedSampleSets, sampleSetSelection, sampleSetSelection, sampleSetColor, theme,
178
+ );
179
+
180
+ sampleSetKeys.forEach((sampleSetKey) => {
181
+ const sampleSetSize = sampleSetSizes.find(d => isEqual(d.setNamePath, sampleSetKey))?.size;
182
+ sampleResult.set(sampleSetKey, sampleSetSize || 0);
183
+ });
184
+
185
+ if (mergedObsSets && obsSetSelection) {
186
+ const sampleIdToSetMap = sampleSets && sampleSetSelection
187
+ ? treeToSelectedSetMap(sampleSets, sampleSetSelection)
188
+ : null;
189
+ const cellIdToSetMap = treeToSelectedSetMap(mergedObsSets, obsSetSelection);
190
+
191
+ for (let i = 0; i < obsIndex.length; i += 1) {
192
+ const obsId = obsIndex[i];
193
+
194
+ const cellSet = cellIdToSetMap?.get(obsId);
195
+ const sampleId = sampleEdges?.get(obsId);
196
+ const sampleSet = sampleId ? sampleIdToSetMap?.get(sampleId) : null;
197
+
198
+ if (hasSampleSetSelection && !sampleSet) {
199
+ // Skip this sample if it is not in the selected sample set.
200
+ // eslint-disable-next-line no-continue
201
+ continue;
202
+ }
203
+ const prevObsCount = obsResult.get(cellSet)?.get(sampleSet);
204
+ obsResult.get(cellSet)?.set(sampleSet, prevObsCount + 1);
205
+ }
206
+ }
207
+
208
+ return [
209
+ unnestMap(obsResult, ['obsSetPath', 'sampleSetPath', 'value']),
210
+ unnestMap(sampleResult, ['sampleSetPath', 'value']),
211
+ ];
212
+ }, [obsIndex, sampleEdges, sampleSets, obsSetColor,
213
+ sampleSetColor, mergedObsSets, obsSetSelection, mergedSampleSets,
214
+ sampleSetSelection,
215
+ // TODO: consider filtering-related coordination values
216
+ ]);
217
+
218
+ return (
219
+ <TitleInfo
220
+ title={`Treemap of ${capitalize(plur(obsType, 2))}`}
221
+ info={`${commaNumber(obsCount)} ${plur(obsType, obsCount)} from ${commaNumber(sampleCount)} ${plur(sampleType, sampleCount)}`}
222
+ removeGridComponent={removeGridComponent}
223
+ urls={urls}
224
+ theme={theme}
225
+ isReady={isReady}
226
+ helpText={helpText}
227
+ options={(
228
+ <TreemapOptions
229
+ obsType={obsType}
230
+ sampleType={sampleType}
231
+ obsColorEncoding={obsColorEncoding}
232
+ setObsColorEncoding={setObsColorEncoding}
233
+ hierarchyLevels={hierarchyLevels || DEFAULT_HIERARCHY_LEVELS}
234
+ setHierarchyLevels={setHierarchyLevels}
235
+ // TODO:
236
+ // - Add option to only include cells in treemap which express selected gene
237
+ // above some threshold (kind of like a dot plot)
238
+ // - Add option to _only_ consider sampleSets or obsSets
239
+ // (not both sampleSets and obsSets)
240
+ />
241
+ )}
242
+ >
243
+ <div ref={containerRef} className={classes.vegaContainer}>
244
+ <Treemap
245
+ obsCounts={obsCounts}
246
+ sampleCounts={sampleCounts}
247
+ obsColorEncoding={obsColorEncoding}
248
+ hierarchyLevels={hierarchyLevels || DEFAULT_HIERARCHY_LEVELS}
249
+ theme={theme}
250
+ width={width}
251
+ height={height}
252
+ obsType={obsType}
253
+ sampleType={sampleType}
254
+ obsSetColor={obsSetColor}
255
+ sampleSetColor={sampleSetColor}
256
+ obsSetSelection={obsSetSelection}
257
+ sampleSetSelection={sampleSetSelection}
258
+ />
259
+ </div>
260
+ </TitleInfo>
261
+ );
262
+ }
@@ -0,0 +1,313 @@
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 } from '@vitessce/utils';
10
+ import { getColorScale } from './utils.js';
11
+
12
+ export default function VolcanoPlot(props) {
13
+ const {
14
+ theme,
15
+ width,
16
+ height,
17
+ obsType,
18
+ featureType,
19
+ obsSetsColumnNameMapping,
20
+ sampleSetsColumnNameMapping,
21
+ sampleSetSelection,
22
+ obsSetSelection,
23
+ obsSetColor,
24
+ sampleSetColor,
25
+ data,
26
+ marginTop = 5,
27
+ marginRight = 5,
28
+ marginLeft = 50,
29
+ marginBottom = 50,
30
+ onFeatureClick,
31
+ featurePointSignificanceThreshold,
32
+ featurePointFoldChangeThreshold,
33
+ featureLabelSignificanceThreshold,
34
+ featureLabelFoldChangeThreshold,
35
+ } = props;
36
+
37
+ const svgRef = useRef();
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]);
47
+
48
+ const [xExtent, yExtent] = useMemo(() => {
49
+ if (!computedData) {
50
+ return [null, null];
51
+ }
52
+ let xExtentResult = d3_extent(
53
+ computedData.flatMap(d => d3_extent(d.df.logFoldChange)),
54
+ );
55
+ const xAbsMax = Math.max(Math.abs(xExtentResult[0]), Math.abs(xExtentResult[1]));
56
+ xExtentResult = [-xAbsMax, xAbsMax];
57
+
58
+ const yExtentResult = d3_extent(
59
+ computedData.flatMap(d => d3_extent(d.df.minusLog10p.filter(v => Number.isFinite(v)))),
60
+ );
61
+ return [xExtentResult, yExtentResult];
62
+ }, [computedData]);
63
+
64
+ const [obsSetColorScale, sampleSetColorScale] = useMemo(() => [
65
+ getColorScale(obsSetSelection, obsSetColor, theme),
66
+ getColorScale(sampleSetSelection, sampleSetColor, theme),
67
+ ], [obsSetSelection, sampleSetSelection, sampleSetColor, obsSetColor, theme]);
68
+
69
+ useEffect(() => {
70
+ const domElement = svgRef.current;
71
+
72
+ const svg = select(domElement);
73
+ svg.selectAll('g').remove();
74
+ svg
75
+ .attr('width', width)
76
+ .attr('height', height)
77
+ .attr('viewBox', [0, 0, width, height])
78
+ .attr('style', 'font: 10px sans-serif');
79
+
80
+ if (!computedData || !xExtent || !yExtent) {
81
+ return;
82
+ }
83
+
84
+ // Render scatterplot
85
+ const innerWidth = width - marginLeft;
86
+ const innerHeight = height - marginBottom;
87
+
88
+ const xScale = scaleLinear()
89
+ .range([marginLeft, width - marginRight])
90
+ .domain(xExtent);
91
+
92
+ // For the y domain, use the yMin prop
93
+ // to support a use case such as 'Aspect Ratio',
94
+ // where the domain minimum should be 1 rather than 0.
95
+ const yScale = scaleLinear()
96
+ .domain(yExtent)
97
+ .range([innerHeight, marginTop])
98
+ .clamp(true);
99
+
100
+ // Add the axes.
101
+ svg.append('g')
102
+ .attr('transform', `translate(0,${height - marginBottom})`)
103
+ .call(axisBottom(xScale));
104
+
105
+ svg.append('g')
106
+ .attr('transform', `translate(${marginLeft},0)`)
107
+ .call(axisLeft(yScale));
108
+
109
+ // Axis titles
110
+ const titleG = svg.append('g');
111
+ const fgColor = 'black'; // TODO: use theme to determine this
112
+
113
+ // Y-axis title
114
+ titleG
115
+ .append('text')
116
+ .attr('text-anchor', 'middle')
117
+ .attr('x', -innerHeight / 2)
118
+ .attr('y', 15)
119
+ .attr('transform', 'rotate(-90)')
120
+ .text('-log10 p-value')
121
+ .style('font-size', '12px')
122
+ .style('fill', fgColor);
123
+
124
+ // X-axis title
125
+ titleG
126
+ .append('text')
127
+ .attr('text-anchor', 'middle')
128
+ .attr('x', marginLeft + innerWidth / 2)
129
+ .attr('y', height - 10)
130
+ .text('log2 fold-change')
131
+ .style('font-size', '12px')
132
+ .style('fill', fgColor);
133
+
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
+ // Horizontal and vertical rules to indicate currently-selected thresholds
148
+ // Vertical lines
149
+ const ruleColor = 'silver';
150
+ const ruleDash = '2,2';
151
+ titleG.append('line')
152
+ .attr('x1', xScale(featurePointFoldChangeThreshold))
153
+ .attr('x2', xScale(featurePointFoldChangeThreshold))
154
+ .attr('y1', yScale.range()[0])
155
+ .attr('y2', yScale.range()[1])
156
+ .style('stroke', ruleColor)
157
+ .style('stroke-dasharray', ruleDash);
158
+ titleG.append('line')
159
+ .attr('x1', xScale(-featurePointFoldChangeThreshold))
160
+ .attr('x2', xScale(-featurePointFoldChangeThreshold))
161
+ .attr('y1', yScale.range()[0])
162
+ .attr('y2', yScale.range()[1])
163
+ .style('stroke', ruleColor)
164
+ .style('stroke-dasharray', ruleDash);
165
+ // Horizontal lines
166
+ titleG.append('line')
167
+ .attr('x1', xScale.range()[0])
168
+ .attr('x2', xScale.range()[1])
169
+ .attr('y1', yScale(-Math.log10(featurePointSignificanceThreshold)))
170
+ .attr('y2', yScale(-Math.log10(featurePointSignificanceThreshold)))
171
+ .style('stroke', ruleColor)
172
+ .style('stroke-dasharray', ruleDash);
173
+
174
+
175
+ // Upregulated/downregulated and sampleSet directional indicators.
176
+ const lhsText = sampleSetSelection && sampleSetSelection.length === 2
177
+ ? sampleSetSelection[0].at(-1)
178
+ : '__rest__';
179
+
180
+ // eslint-disable-next-line no-nested-ternary
181
+ const rhsText = sampleSetSelection && sampleSetSelection.length === 2
182
+ ? sampleSetSelection[1].at(-1)
183
+ : (obsSetSelection && obsSetSelection.length === 1
184
+ ? obsSetSelection?.[0]?.at(-1)
185
+ : `${capitalize(obsType)} Set`
186
+ );
187
+
188
+
189
+ titleG
190
+ .append('text')
191
+ .attr('text-anchor', 'start')
192
+ .attr('x', marginLeft)
193
+ .attr('y', height - 10)
194
+ .text(`\u2190 ${lhsText}`)
195
+ .style('font-size', '12px')
196
+ .style('fill', fgColor);
197
+
198
+ titleG
199
+ .append('text')
200
+ .attr('text-anchor', 'end')
201
+ .attr('x', marginLeft + innerWidth)
202
+ .attr('y', height - 10)
203
+ .text(`${rhsText} \u2192`)
204
+ .style('font-size', '12px')
205
+ .style('fill', fgColor);
206
+
207
+
208
+ const g = svg.append('g');
209
+
210
+ // Append a circle for each data point.
211
+ computedData.forEach((comparisonObject) => {
212
+ const obsSetG = g.append('g');
213
+
214
+ const { df, metadata } = comparisonObject;
215
+ const coordinationValues = metadata.coordination_values;
216
+
217
+ const rawObsSetPath = coordinationValues.obsSetFilter
218
+ ? coordinationValues.obsSetFilter[0]
219
+ : coordinationValues.obsSetSelection[0];
220
+ const obsSetPath = [...rawObsSetPath];
221
+ obsSetPath[0] = obsSetsColumnNameMappingReversed[rawObsSetPath[0]];
222
+
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
+ const color = obsSetColorScale(obsSetPath);
258
+
259
+ obsSetG.append('g')
260
+ .selectAll('circle')
261
+ .data(filteredDf)
262
+ .join('circle')
263
+ .attr('cx', d => xScale(d.logFoldChange))
264
+ .attr('cy', d => yScale(d.minusLog10p))
265
+ .attr('r', 3)
266
+ .attr('opacity', 0.5)
267
+ .attr('fill', color)
268
+ .on('click', (event, d) => {
269
+ onFeatureClick(d.featureId);
270
+ });
271
+
272
+ const textElements = obsSetG.append('g')
273
+ .selectAll('text')
274
+ .data(filteredDf)
275
+ .join('text')
276
+ .text(d => d.featureId)
277
+ .attr('text-anchor', d => (d.logFoldChange < 0 ? 'end' : 'start'))
278
+ .attr('x', d => xScale(d.logFoldChange))
279
+ .attr('y', d => yScale(d.minusLog10p))
280
+ .style('display', d => ((
281
+ Math.abs(d.logFoldChange) < (featureLabelFoldChangeThreshold ?? 5.0)
282
+ || (d.featureSignificance >= (featureLabelSignificanceThreshold ?? 0.01))
283
+ ) ? 'none' : undefined))
284
+ .attr('fill', color)
285
+ .on('click', (event, d) => {
286
+ onFeatureClick(d.featureId);
287
+ });
288
+
289
+ textElements.append('title')
290
+ .text(d => `${featureType}: ${d.featureId}\nin ${obsSetPath?.at(-1)}\nlog2 fold-change: ${d.logFoldChange}\np-value: ${d.featureSignificance}`);
291
+ });
292
+ }, [width, height, theme, sampleSetColor, sampleSetSelection,
293
+ obsSetSelection, obsSetColor, featureType, computedData,
294
+ xExtent, yExtent, obsType,
295
+ marginLeft, marginBottom, marginTop, marginRight,
296
+ obsSetColorScale, sampleSetColorScale, onFeatureClick,
297
+ featurePointSignificanceThreshold, featurePointFoldChangeThreshold,
298
+ featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold,
299
+ ]);
300
+
301
+ return (
302
+ <svg
303
+ ref={svgRef}
304
+ style={{
305
+ top: 0,
306
+ left: 0,
307
+ width: `${width}px`,
308
+ height: `${height}px`,
309
+ position: 'relative',
310
+ }}
311
+ />
312
+ );
313
+ }
@@ -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
+ }