@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
package/dist-tsc/index.js CHANGED
@@ -3,6 +3,10 @@ export { CellSetSizesPlotSubscriber } from './CellSetSizesPlotSubscriber.js';
3
3
  export { ExpressionHistogramSubscriber } from './ExpressionHistogramSubscriber.js';
4
4
  export { DotPlotSubscriber } from './DotPlotSubscriber.js';
5
5
  export { FeatureBarPlotSubscriber } from './FeatureBarPlotSubscriber.js';
6
+ export { TreemapSubscriber } from './TreemapSubscriber.js';
7
+ export { VolcanoPlotSubscriber } from './VolcanoPlotSubscriber.js';
8
+ export { CellSetCompositionBarPlotSubscriber } from './CellSetCompositionBarPlotSubscriber.js';
9
+ export { FeatureSetEnrichmentBarPlotSubscriber } from './FeatureSetEnrichmentBarPlotSubscriber.js';
6
10
  export { default as CellSetSizesPlot } from './CellSetSizesPlot.js';
7
11
  export { default as CellSetExpressionPlot } from './CellSetExpressionPlot.js';
8
12
  export { default as ExpressionHistogram } from './ExpressionHistogram.js';
@@ -0,0 +1,9 @@
1
+ export function getColorScale(setSelectionArr: any, setColorArr: any, theme: any): (queryVal: any) => any;
2
+ /**
3
+ * Transform set paths which use group names to those which use column names.
4
+ * @param {Record<string, string>} columnNameMapping Return value of useColumnNameMapping.
5
+ * @param {string[][]} setPaths Array of set paths, such as obsSetSelection.
6
+ * @returns {string[][]} Transformed set paths.
7
+ */
8
+ export function useRawSetPaths(columnNameMapping: Record<string, string>, setPaths: string[][]): string[][];
9
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.js"],"names":[],"mappings":"AAcA,0GAeC;AAGD;;;;;GAKG;AACH,kDAJW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,YACtB,MAAM,EAAE,EAAE,GACR,MAAM,EAAE,EAAE,CAUtB"}
@@ -0,0 +1,40 @@
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
+ function createOrdinalScale(domainArr, rangeArr) {
6
+ return (queryVal) => {
7
+ const i = domainArr.findIndex(domainVal => isEqual(domainVal, queryVal));
8
+ return rangeArr[i];
9
+ };
10
+ }
11
+ // Create a d3-scale ordinal scale mapping set paths to color strings.
12
+ export function getColorScale(setSelectionArr, setColorArr, theme) {
13
+ /*
14
+ // The equality seems incorrect with d3.scaleOrdinal
15
+ return scaleOrdinal()
16
+ .domain(setSelectionArr || [])
17
+ .range(
18
+ */
19
+ const domainArr = setSelectionArr || [];
20
+ const rangeArr = setSelectionArr
21
+ ?.map(setNamePath => (setColorArr?.find(d => isEqual(d.path, setNamePath))?.color
22
+ || getDefaultColor(theme)))
23
+ ?.map(colorArrayToString) || [];
24
+ return createOrdinalScale(domainArr, rangeArr);
25
+ }
26
+ /**
27
+ * Transform set paths which use group names to those which use column names.
28
+ * @param {Record<string, string>} columnNameMapping Return value of useColumnNameMapping.
29
+ * @param {string[][]} setPaths Array of set paths, such as obsSetSelection.
30
+ * @returns {string[][]} Transformed set paths.
31
+ */
32
+ export function useRawSetPaths(columnNameMapping, setPaths) {
33
+ return useMemo(() => setPaths?.map((setPath) => {
34
+ const newSetPath = [...setPath];
35
+ if (newSetPath?.[0] && columnNameMapping[newSetPath[0]]) {
36
+ newSetPath[0] = columnNameMapping[newSetPath[0]];
37
+ }
38
+ return newSetPath;
39
+ }), [columnNameMapping, setPaths]);
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/statistical-plots",
3
- "version": "3.5.6",
3
+ "version": "3.5.8",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -23,17 +23,18 @@
23
23
  "d3-axis": "^3.0.0",
24
24
  "d3-selection": "^3.0.0",
25
25
  "d3-format": "^3.1.0",
26
+ "d3-hierarchy": "^3.1.2",
26
27
  "vega-scale": "^6.0.0",
27
28
  "lodash-es": "^4.17.21",
28
29
  "react-aria": "^3.28.0",
29
30
  "internmap": "^2.0.3",
30
31
  "uuid": "^9.0.0",
31
- "@vitessce/constants-internal": "3.5.6",
32
- "@vitessce/sets-utils": "3.5.6",
33
- "@vitessce/utils": "3.5.6",
34
- "@vitessce/vega": "3.5.6",
35
- "@vitessce/vit-s": "3.5.6",
36
- "@vitessce/gl": "3.5.6"
32
+ "@vitessce/constants-internal": "3.5.8",
33
+ "@vitessce/sets-utils": "3.5.8",
34
+ "@vitessce/utils": "3.5.8",
35
+ "@vitessce/vega": "3.5.8",
36
+ "@vitessce/vit-s": "3.5.8",
37
+ "@vitessce/gl": "3.5.8"
37
38
  },
38
39
  "devDependencies": {
39
40
  "react": "^18.0.0",
@@ -0,0 +1,205 @@
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { clamp, isEqual } from 'lodash-es';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { VegaPlot, VEGA_THEMES } from '@vitessce/vega';
5
+ import { capitalize } from '@vitessce/utils';
6
+ import { getColorScale } from './utils.js';
7
+
8
+ /**
9
+ * Cell set composition results displayed using a bar chart.
10
+ */
11
+ export default function CellSetCompositionBarPlot(props) {
12
+ const {
13
+ data,
14
+ theme,
15
+ width,
16
+ height,
17
+ marginRight = 200,
18
+ marginBottom = 120,
19
+ keyLength = 36,
20
+ obsType,
21
+ onBarSelect,
22
+ obsSetsColumnNameMappingReversed,
23
+ sampleSetsColumnNameMappingReversed,
24
+ sampleSetSelection,
25
+ obsSetSelection,
26
+ obsSetColor,
27
+ sampleSetColor,
28
+ } = props;
29
+
30
+ const [obsSetColorScale, sampleSetColorScale] = useMemo(() => [
31
+ getColorScale(obsSetSelection, obsSetColor, theme),
32
+ getColorScale(sampleSetSelection, sampleSetColor, theme),
33
+ ], [obsSetSelection, sampleSetSelection, sampleSetColor, obsSetColor, theme]);
34
+
35
+ const computedData = useMemo(() => {
36
+ if (Array.isArray(data) && data.length === 1) {
37
+ // We expect only one returned data frame.
38
+ const { df, metadata } = data[0];
39
+ // Return in array-of-objects form that Vega-Lite likes.
40
+
41
+ const referenceCellType = metadata?.analysis_params?.reference_cell_type;
42
+ const coordinationValues = metadata?.coordination_values;
43
+ const obsSetColumnName = coordinationValues?.obsSetSelection?.[0]?.[0];
44
+ const obsSetGroupName = obsSetsColumnNameMappingReversed?.[obsSetColumnName];
45
+
46
+ const sampleSetColumnName = coordinationValues?.sampleSetFilter?.[0]?.[0];
47
+ const sampleSetGroupName = sampleSetsColumnNameMappingReversed?.[sampleSetColumnName];
48
+
49
+ // See https://github.com/keller-mark/compasce/issues/30 which should simplify this logic once implemented,
50
+ // so that we would no longer need to load/check the covariate column in the frontend.
51
+ const covariatePrefix = `${sampleSetColumnName}T.`;
52
+ const firstCovariateValue = df.covariate?.[0]?.substring(covariatePrefix.length);
53
+ const firstCovariateSetPath = [sampleSetGroupName, firstCovariateValue];
54
+
55
+ let shouldSwapFoldChangeDirection = false;
56
+ if (isEqual(firstCovariateSetPath, sampleSetSelection[0])) {
57
+ shouldSwapFoldChangeDirection = true;
58
+ }
59
+
60
+ return df.obsSetId.map((obsSetId, i) => {
61
+ const key = uuidv4();
62
+ const isReferenceSet = (obsSetId === referenceCellType);
63
+ const name = `${obsSetId}${(isReferenceSet ? ' (reference set)' : '')}`;
64
+ const obsSetPath = [obsSetGroupName, obsSetId];
65
+ const color = obsSetColorScale(obsSetPath);
66
+ return {
67
+ name,
68
+ // Reconstruct set path array.
69
+ obsSetPath,
70
+ color,
71
+ // Unique key per bar
72
+ key,
73
+ // Add a property `keyName` which concatenates the key and the name,
74
+ // which is both unique and can easily be converted
75
+ // back to the name by taking a substring.
76
+ keyName: `${key}${name}`,
77
+ // Swap direction of foldChange/logFC if necessary
78
+ obsSetFoldChange: df.obsSetFoldChange[i] * (shouldSwapFoldChangeDirection ? -1 : 1),
79
+ logFoldChange: (
80
+ Math.log2(df.obsSetFoldChange[i]) * (shouldSwapFoldChangeDirection ? -1 : 1)
81
+ ),
82
+ interceptExpectedSample: df.interceptExpectedSample[i],
83
+ effectExpectedSample: df.effectExpectedSample[i],
84
+ isCredibleEffect: df.isCredibleEffect[i],
85
+ // Boolean flag for wasReferenceObsSet (check metadata)
86
+ isReferenceSet: (obsSetId === referenceCellType),
87
+ };
88
+ }).filter(d => obsSetSelection
89
+ ?.find(setNamePath => isEqual(setNamePath, d.obsSetPath)));
90
+ }
91
+ return null;
92
+ }, [data, sampleSetSelection, obsSetsColumnNameMappingReversed,
93
+ sampleSetsColumnNameMappingReversed, obsSetSelection,
94
+ obsSetColorScale, sampleSetColorScale,
95
+ ]);
96
+
97
+ // Get an array of keys for sorting purposes.
98
+ const keys = computedData.map(d => d.keyName);
99
+
100
+ const colorScale = {
101
+ // Manually set the color scale so that Vega-Lite does
102
+ // not choose the colors automatically.
103
+ domain: computedData.map(d => d.key),
104
+ range: computedData.map(d => d.color),
105
+ };
106
+ const captializedObsType = capitalize(obsType);
107
+
108
+ const opacityScale = {
109
+ domain: [true, false],
110
+ range: [1.0, 0.3],
111
+ };
112
+ const strokeWidthScale = {
113
+ domain: [true, false],
114
+ range: [2.0, 0.5],
115
+ };
116
+
117
+ const spec = {
118
+ mark: { type: 'bar', stroke: 'black', cursor: 'pointer' },
119
+ params: [
120
+ {
121
+ name: 'bar_select',
122
+ select: {
123
+ type: 'point',
124
+ on: 'click[event.shiftKey === false]',
125
+ fields: ['obsSetPath'],
126
+ empty: 'none',
127
+ },
128
+ },
129
+ {
130
+ name: 'shift_bar_select',
131
+ select: {
132
+ type: 'point',
133
+ on: 'click[event.shiftKey]',
134
+ fields: ['obsSetPath'],
135
+ empty: 'none',
136
+ },
137
+ },
138
+ ],
139
+ encoding: {
140
+ y: {
141
+ field: 'keyName',
142
+ type: 'nominal',
143
+ axis: { labelExpr: `substring(datum.label, ${keyLength})` },
144
+ title: `${captializedObsType} Set`,
145
+ sort: keys,
146
+ },
147
+ x: {
148
+ // TODO: support using intercept+effect here based on user-selected options?
149
+ field: 'logFoldChange',
150
+ type: 'quantitative',
151
+ title: 'Log fold-change',
152
+ },
153
+ color: {
154
+ field: 'key',
155
+ type: 'nominal',
156
+ scale: colorScale,
157
+ legend: null,
158
+ },
159
+ fillOpacity: {
160
+ field: 'isCredibleEffect',
161
+ type: 'nominal',
162
+ scale: opacityScale,
163
+ },
164
+ strokeWidth: {
165
+ field: 'isReferenceSet',
166
+ type: 'nominal',
167
+ scale: strokeWidthScale,
168
+ },
169
+ tooltip: {
170
+ field: 'effectExpectedSample',
171
+ type: 'quantitative',
172
+ },
173
+ },
174
+ // TODO: for width, also subtract length of longest y-axis set name label.
175
+ width: clamp(width - marginRight, 10, Infinity),
176
+ height: clamp(height - marginBottom, 10, Infinity),
177
+ config: VEGA_THEMES[theme],
178
+ };
179
+
180
+ const handleSignal = (name, value) => {
181
+ if (name === 'bar_select') {
182
+ onBarSelect(value.obsSetPath);
183
+ } else if (name === 'shift_bar_select') {
184
+ onBarSelect(value.obsSetPath, true);
185
+ }
186
+ };
187
+
188
+ const signalListeners = { bar_select: handleSignal, shift_bar_select: handleSignal };
189
+ const getTooltipText = useCallback(item => ({
190
+ [`${captializedObsType} Set`]: item.datum.name,
191
+ 'Log fold-change': item.datum.logFoldChange,
192
+ interceptExpectedSample: item.datum.interceptExpectedSample,
193
+ effectExpectedSample: item.datum.effectExpectedSample,
194
+ }
195
+ ), [captializedObsType]);
196
+
197
+ return (
198
+ <VegaPlot
199
+ data={computedData}
200
+ spec={spec}
201
+ signalListeners={signalListeners}
202
+ getTooltipText={getTooltipText}
203
+ />
204
+ );
205
+ }
@@ -0,0 +1,151 @@
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
+ useObsSetStatsData,
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 { capitalize } from '@vitessce/utils';
20
+ import CellSetCompositionBarPlot from './CellSetCompositionBarPlot.js';
21
+ import { useStyles } from './styles.js';
22
+ import { useRawSetPaths } from './utils.js';
23
+
24
+
25
+ export function CellSetCompositionBarPlotSubscriber(props) {
26
+ const {
27
+ coordinationScopes,
28
+ removeGridComponent,
29
+ theme,
30
+ helpText = ViewHelpMapping.OBS_SET_COMPOSITION_BAR_PLOT,
31
+ } = props;
32
+
33
+ const classes = useStyles();
34
+ const loaders = useLoaders();
35
+
36
+ // Get "props" from the coordination space.
37
+ const [{
38
+ dataset,
39
+ obsType,
40
+ sampleType,
41
+ featureType,
42
+ featureValueType,
43
+ obsFilter: cellFilter,
44
+ obsHighlight: cellHighlight,
45
+ obsSetSelection,
46
+ obsSetColor,
47
+ obsColorEncoding: cellColorEncoding,
48
+ additionalObsSets: additionalCellSets,
49
+ featurePointSignificanceThreshold,
50
+ featurePointFoldChangeThreshold,
51
+ featureLabelSignificanceThreshold,
52
+ featureLabelFoldChangeThreshold,
53
+ featureValueTransform,
54
+ featureValueTransformCoefficient,
55
+ gatingFeatureSelectionX,
56
+ gatingFeatureSelectionY,
57
+ featureSelection,
58
+ sampleSetSelection,
59
+ sampleSetColor,
60
+ }, {
61
+ setObsFilter: setCellFilter,
62
+ setObsSetSelection,
63
+ setObsHighlight: setCellHighlight,
64
+ setObsSetColor: setCellSetColor,
65
+ setObsColorEncoding: setCellColorEncoding,
66
+ setAdditionalObsSets: setAdditionalCellSets,
67
+ setFeaturePointSignificanceThreshold,
68
+ setFeaturePointFoldChangeThreshold,
69
+ setFeatureLabelSignificanceThreshold,
70
+ setFeatureLabelFoldChangeThreshold,
71
+ setFeatureValueTransform,
72
+ setFeatureValueTransformCoefficient,
73
+ setGatingFeatureSelectionX,
74
+ setGatingFeatureSelectionY,
75
+ setFeatureSelection,
76
+ setSampleSetSelection,
77
+ setSampleSetColor,
78
+ }] = useCoordination(
79
+ COMPONENT_COORDINATION_TYPES[ViewType.OBS_SET_COMPOSITION_BAR_PLOT],
80
+ coordinationScopes,
81
+ );
82
+ const [width, height, containerRef] = useGridItemSize();
83
+
84
+ const obsSetsLoader = useMatchingLoader(
85
+ loaders, dataset, DataType.OBS_SETS, { obsType },
86
+ );
87
+ const sampleSetsLoader = useMatchingLoader(
88
+ loaders, dataset, DataType.SAMPLE_SETS, { sampleType },
89
+ );
90
+ const obsSetsColumnNameMapping = useColumnNameMapping(obsSetsLoader);
91
+ const obsSetsColumnNameMappingReversed = useColumnNameMapping(obsSetsLoader, true);
92
+ const sampleSetsColumnNameMapping = useColumnNameMapping(sampleSetsLoader);
93
+ const sampleSetsColumnNameMappingReversed = useColumnNameMapping(sampleSetsLoader, true);
94
+
95
+ const rawSampleSetSelection = useRawSetPaths(sampleSetsColumnNameMapping, sampleSetSelection);
96
+ const rawObsSetSelection = useRawSetPaths(obsSetsColumnNameMapping, obsSetSelection);
97
+
98
+ const [{ obsSetStats }, obsSetStatsStatus] = useObsSetStatsData(
99
+ loaders, dataset, false,
100
+ { obsType, sampleType },
101
+ // These volcanoOptions are passed to ObsSetStatsAnndataLoader.loadMulti():
102
+ { sampleSetSelection: rawSampleSetSelection, obsSetSelection: rawObsSetSelection },
103
+ );
104
+
105
+ const isReady = useReady([
106
+ obsSetStatsStatus,
107
+ ]);
108
+
109
+ // Support a click handler which selects individual cell set bars.
110
+ const onBarSelect = useCallback((setNamePath, isShiftDown = false) => {
111
+ // TODO: Implement different behavior when isShiftDown
112
+ setObsSetSelection([setNamePath]);
113
+ }, [setObsSetSelection]);
114
+
115
+ // TODO: support the following options
116
+ // - Use logFoldChange vs. intercept+effect for the bar y-value.
117
+ // - Boolean flag to allow hiding non-significant bars.
118
+
119
+ return (
120
+ <TitleInfo
121
+ title={`${capitalize(obsType)} Set Composition Analysis Plot`}
122
+ removeGridComponent={removeGridComponent}
123
+ theme={theme}
124
+ isReady={isReady}
125
+ helpText={helpText}
126
+ >
127
+ <div ref={containerRef} className={classes.vegaContainer}>
128
+ {obsSetStats ? (
129
+ <CellSetCompositionBarPlot
130
+ theme={theme}
131
+ width={width}
132
+ height={height}
133
+ obsType={obsType}
134
+ obsSetsColumnNameMapping={obsSetsColumnNameMapping}
135
+ obsSetsColumnNameMappingReversed={obsSetsColumnNameMappingReversed}
136
+ sampleSetsColumnNameMapping={sampleSetsColumnNameMapping}
137
+ sampleSetsColumnNameMappingReversed={sampleSetsColumnNameMappingReversed}
138
+ sampleSetSelection={sampleSetSelection}
139
+ obsSetSelection={obsSetSelection}
140
+ obsSetColor={obsSetColor}
141
+ sampleSetColor={sampleSetColor}
142
+ data={obsSetStats}
143
+ onBarSelect={onBarSelect}
144
+ />
145
+ ) : (
146
+ <span>Select at least one {obsType} set.</span>
147
+ )}
148
+ </div>
149
+ </TitleInfo>
150
+ );
151
+ }
@@ -124,7 +124,10 @@ export default function CellSetExpressionPlot(props) {
124
124
  .range(
125
125
  // TODO: check for full path equality here.
126
126
  sampleSetNames
127
- .map(name => sampleSetColor?.find(d => d.path.at(-1) === name).color)
127
+ .map(name => (
128
+ sampleSetColor?.find(d => d.path.at(-1) === name)?.color
129
+ || [125, 125, 125]
130
+ ))
128
131
  .map(colorArrayToString),
129
132
  );
130
133
  }
@@ -0,0 +1,203 @@
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { clamp } from 'lodash-es';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { VegaPlot, VEGA_THEMES } from '@vitessce/vega';
5
+ import { capitalize } from '@vitessce/utils';
6
+ import { getColorScale } from './utils.js';
7
+
8
+ /**
9
+ * Feature set enrichment test results displayed using a bar chart.
10
+ */
11
+ export default function FeatureSetEnrichmentBarPlot(props) {
12
+ const {
13
+ data,
14
+ theme,
15
+ width,
16
+ height,
17
+ marginRight = 200,
18
+ marginBottom = 120,
19
+ keyLength = 36,
20
+ featureType,
21
+ onBarSelect,
22
+ obsSetsColumnNameMappingReversed,
23
+ sampleSetsColumnNameMappingReversed,
24
+ sampleSetSelection,
25
+ obsSetSelection,
26
+ obsSetColor,
27
+ sampleSetColor,
28
+ pValueThreshold,
29
+ } = props;
30
+
31
+ const [obsSetColorScale, sampleSetColorScale] = useMemo(() => [
32
+ getColorScale(obsSetSelection, obsSetColor, theme),
33
+ getColorScale(sampleSetSelection, sampleSetColor, theme),
34
+ ], [obsSetSelection, sampleSetSelection, sampleSetColor, obsSetColor, theme]);
35
+
36
+ const computedData = useMemo(() => {
37
+ if (Array.isArray(data)) {
38
+ let result = [];
39
+ data.forEach((comparisonObject) => {
40
+ const { df, metadata } = comparisonObject;
41
+ const coordinationValues = metadata?.coordination_values;
42
+
43
+ const rawObsSetPath = coordinationValues.obsSetFilter
44
+ ? coordinationValues.obsSetFilter[0]
45
+ : coordinationValues.obsSetSelection[0];
46
+ const obsSetPath = [...rawObsSetPath];
47
+ obsSetPath[0] = obsSetsColumnNameMappingReversed[rawObsSetPath[0]];
48
+
49
+ const color = obsSetColorScale(obsSetPath);
50
+
51
+ df.featureSetName.forEach((featureSetName, i) => {
52
+ const key = uuidv4();
53
+ result.push({
54
+ key,
55
+ name: featureSetName,
56
+ term: df.featureSetTerm[i],
57
+ color,
58
+ obsSetPath,
59
+ obsSetPaths: [obsSetPath],
60
+ obsSetNameToPval: { [obsSetPath.at(-1)]: df.featureSetSignificance[i] },
61
+ keyName: `${key}${featureSetName}`,
62
+ featureSetSignificance: df.featureSetSignificance[i],
63
+ minusLog10p: -Math.log10(df.featureSetSignificance[i]),
64
+ // Color based on obsSet
65
+ });
66
+ });
67
+ });
68
+
69
+ // TODO: instead of filtering, perhaps use virtual scrolling
70
+ // (would require custom renderer / not using Vega-Lite).
71
+ result = result
72
+ .map(d => ({
73
+ ...d,
74
+ minusLog10p: Math.min(50, d.minusLog10p), // Clamp infinite values at 50
75
+ }))
76
+ .filter(d => d.featureSetSignificance <= pValueThreshold)
77
+ .toSorted((a, b) => a.featureSetSignificance - b.featureSetSignificance)
78
+ .reduce((a, h) => {
79
+ // Only add the pathway once if it appears for multiple cell types?
80
+ const match = a.find(d => d.name === h.name);
81
+ if (match) {
82
+ match.obsSetPaths.push(h.obsSetPath);
83
+ match.obsSetNameToPval[h.obsSetPath.at(-1)] = h.featureSetSignificance;
84
+ return a;
85
+ }
86
+ return [...a, h];
87
+ }, []);
88
+
89
+ const MAX_ROWS = 25;
90
+ result = result.slice(0, MAX_ROWS);
91
+ return result;
92
+ }
93
+ return null;
94
+ }, [data, sampleSetSelection, obsSetsColumnNameMappingReversed,
95
+ sampleSetsColumnNameMappingReversed, obsSetSelection,
96
+ obsSetColorScale, sampleSetColorScale, pValueThreshold,
97
+ ]);
98
+
99
+ // Get an array of keys for sorting purposes.
100
+ const keys = computedData.map(d => d.keyName);
101
+
102
+ const colorScale = {
103
+ // Manually set the color scale so that Vega-Lite does
104
+ // not choose the colors automatically.
105
+ domain: computedData.map(d => d.key),
106
+ range: computedData.map(d => d.color),
107
+ };
108
+ const captializedFeatureType = capitalize(featureType);
109
+
110
+ const spec = {
111
+ mark: { type: 'bar', stroke: 'black', cursor: 'pointer' },
112
+ params: [
113
+ {
114
+ name: 'bar_select',
115
+ select: {
116
+ type: 'point',
117
+ on: 'click[event.shiftKey === false]',
118
+ fields: ['name'],
119
+ empty: 'none',
120
+ },
121
+ },
122
+ {
123
+ name: 'shift_bar_select',
124
+ select: {
125
+ type: 'point',
126
+ on: 'click[event.shiftKey]',
127
+ fields: ['name'],
128
+ empty: 'none',
129
+ },
130
+ },
131
+ ],
132
+ encoding: {
133
+ y: {
134
+ field: 'keyName',
135
+ type: 'nominal',
136
+ axis: { labelExpr: `substring(datum.label, ${keyLength})` },
137
+ title: `${captializedFeatureType} Set`,
138
+ sort: keys,
139
+ },
140
+ x: {
141
+ field: 'minusLog10p',
142
+ type: 'quantitative',
143
+ title: '- log10 p-value',
144
+ },
145
+ color: {
146
+ field: 'key',
147
+ type: 'nominal',
148
+ scale: colorScale,
149
+ legend: null,
150
+ },
151
+ /*
152
+ fillOpacity: {
153
+ field: 'isCredibleEffect',
154
+ type: 'nominal',
155
+ scale: opacityScale,
156
+ },
157
+ strokeWidth: {
158
+ field: 'isReferenceSet',
159
+ type: 'nominal',
160
+ scale: strokeWidthScale,
161
+ },
162
+ */
163
+ tooltip: {
164
+ field: 'featureSetSignificance',
165
+ type: 'quantitative',
166
+ },
167
+ },
168
+ // TODO: for width, also subtract length of longest y-axis set name label.
169
+ width: clamp(width - marginRight, 10, Infinity),
170
+ height: clamp(height - marginBottom, 10, Infinity),
171
+ config: VEGA_THEMES[theme],
172
+ };
173
+
174
+ const handleSignal = (name, value) => {
175
+ if (name === 'bar_select') {
176
+ onBarSelect(value.obsSetPath);
177
+ } else if (name === 'shift_bar_select') {
178
+ onBarSelect(value.obsSetPath, true);
179
+ }
180
+ };
181
+
182
+ const signalListeners = { bar_select: handleSignal, shift_bar_select: handleSignal };
183
+ const getTooltipText = useCallback(item => ({
184
+ [`${captializedFeatureType} Set`]: item.datum.name,
185
+ 'Ontology Term': item.datum.term,
186
+ ...Object.fromEntries(
187
+ Object.entries(item.datum.obsSetNameToPval).map(([cellSetName, pVal]) => ([
188
+ `p-value for ${cellSetName}`,
189
+ pVal,
190
+ ])),
191
+ ),
192
+ }
193
+ ), [captializedFeatureType]);
194
+
195
+ return (
196
+ <VegaPlot
197
+ data={computedData}
198
+ spec={spec}
199
+ signalListeners={signalListeners}
200
+ getTooltipText={getTooltipText}
201
+ />
202
+ );
203
+ }