@vitessce/statistical-plots 2.0.3-beta.0 → 3.0.0

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 (43) hide show
  1. package/dist/index.js +85871 -6
  2. package/dist-tsc/CellSetExpressionPlot.d.ts +33 -0
  3. package/dist-tsc/CellSetExpressionPlot.d.ts.map +1 -0
  4. package/{dist → dist-tsc}/CellSetExpressionPlot.js +3 -3
  5. package/dist-tsc/CellSetExpressionPlotOptions.d.ts +2 -0
  6. package/dist-tsc/CellSetExpressionPlotOptions.d.ts.map +1 -0
  7. package/{dist → dist-tsc}/CellSetExpressionPlotOptions.js +1 -3
  8. package/dist-tsc/CellSetExpressionPlotSubscriber.d.ts +16 -0
  9. package/dist-tsc/CellSetExpressionPlotSubscriber.d.ts.map +1 -0
  10. package/{dist → dist-tsc}/CellSetExpressionPlotSubscriber.js +7 -11
  11. package/dist-tsc/CellSetSizesPlot.d.ts +29 -0
  12. package/dist-tsc/CellSetSizesPlot.d.ts.map +1 -0
  13. package/dist-tsc/CellSetSizesPlot.js +149 -0
  14. package/dist-tsc/CellSetSizesPlotSubscriber.d.ts +18 -0
  15. package/dist-tsc/CellSetSizesPlotSubscriber.d.ts.map +1 -0
  16. package/dist-tsc/CellSetSizesPlotSubscriber.js +77 -0
  17. package/dist-tsc/ExpressionHistogram.d.ts +34 -0
  18. package/dist-tsc/ExpressionHistogram.d.ts.map +1 -0
  19. package/dist-tsc/ExpressionHistogram.js +93 -0
  20. package/dist-tsc/ExpressionHistogramSubscriber.d.ts +16 -0
  21. package/dist-tsc/ExpressionHistogramSubscriber.d.ts.map +1 -0
  22. package/{dist → dist-tsc}/ExpressionHistogramSubscriber.js +21 -14
  23. package/dist-tsc/index.d.ts +7 -0
  24. package/dist-tsc/index.d.ts.map +1 -0
  25. package/dist-tsc/index.js +6 -0
  26. package/dist-tsc/styles.d.ts +2 -0
  27. package/dist-tsc/styles.d.ts.map +1 -0
  28. package/{dist → dist-tsc}/styles.js +1 -1
  29. package/package.json +20 -11
  30. package/src/CellSetExpressionPlot.js +278 -0
  31. package/src/CellSetExpressionPlotOptions.js +73 -0
  32. package/src/CellSetExpressionPlotSubscriber.js +220 -0
  33. package/src/CellSetSizesPlot.js +173 -0
  34. package/src/CellSetSizesPlotSubscriber.js +151 -0
  35. package/src/ExpressionHistogram.js +120 -0
  36. package/src/ExpressionHistogramSubscriber.js +136 -0
  37. package/src/index.js +6 -0
  38. package/src/styles.js +9 -0
  39. package/dist/CellSetSizesPlot.js +0 -77
  40. package/dist/CellSetSizesPlotSubscriber.js +0 -44
  41. package/dist/DotPlot.js +0 -110
  42. package/dist/DotPlotSubscriber.js +0 -126
  43. package/dist/ExpressionHistogram.js +0 -49
@@ -0,0 +1,220 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ TitleInfo,
4
+ useCoordination, useLoaders,
5
+ useUrls, useReady, useGridItemSize,
6
+ useFeatureSelection, useObsSetsData,
7
+ useObsFeatureMatrixIndices,
8
+ useFeatureLabelsData,
9
+ } from '@vitessce/vit-s';
10
+ import { ViewType, COMPONENT_COORDINATION_TYPES } from '@vitessce/constants-internal';
11
+ import { VALUE_TRANSFORM_OPTIONS, capitalize, getValueTransformFunction } from '@vitessce/utils';
12
+ import { treeToObjectsBySetNames, treeToSetSizesBySetNames, mergeObsSets } from '@vitessce/sets-utils';
13
+ import CellSetExpressionPlotOptions from './CellSetExpressionPlotOptions.js';
14
+ import CellSetExpressionPlot from './CellSetExpressionPlot.js';
15
+ import { useStyles } from './styles.js';
16
+
17
+ /**
18
+ * Get expression data for the cells
19
+ * in the selected cell sets.
20
+ * @param {object} expressionMatrix
21
+ * @param {string[]} expressionMatrix.rows Cell IDs.
22
+ * @param {string[]} expressionMatrix.cols Gene names.
23
+ * @param {Uint8Array} expressionMatrix.matrix The
24
+ * flattened expression matrix as a typed array.
25
+ * @param {object} cellSets The cell sets from the dataset.
26
+ * @param {object} additionalCellSets The user-defined cell sets
27
+ * from the coordination space.
28
+ * @param {array} geneSelection Array of selected genes.
29
+ * @param {array} cellSetSelection Array of selected cell set paths.
30
+ * @param {object[]} cellSetColor Array of objects with properties
31
+ * @param {string|null} featureValueTransform The name of the
32
+ * feature value transform function.
33
+ * @param {number} featureValueTransformCoefficient A coefficient
34
+ * to be used in the transform function.
35
+ * @param {string} theme "light" or "dark" for the vitessce theme
36
+ * `path` and `color`.
37
+ */
38
+ function useExpressionByCellSet(
39
+ expressionData, obsIndex, cellSets, additionalCellSets,
40
+ geneSelection, cellSetSelection, cellSetColor,
41
+ featureValueTransform, featureValueTransformCoefficient,
42
+ theme,
43
+ ) {
44
+ const mergedCellSets = useMemo(
45
+ () => mergeObsSets(cellSets, additionalCellSets),
46
+ [cellSets, additionalCellSets],
47
+ );
48
+
49
+ // From the expression matrix and the list of selected genes / cell sets,
50
+ // generate the array of data points for the plot.
51
+ const [expressionArr, expressionMax] = useMemo(() => {
52
+ if (mergedCellSets && cellSetSelection
53
+ && geneSelection && geneSelection.length >= 1
54
+ && expressionData
55
+ ) {
56
+ const cellObjects = treeToObjectsBySetNames(
57
+ mergedCellSets, cellSetSelection, cellSetColor, theme,
58
+ );
59
+
60
+ const firstGeneSelected = geneSelection[0];
61
+ // Create new cellColors map based on the selected gene.
62
+ let exprMax = -Infinity;
63
+ const cellIndices = {};
64
+ for (let i = 0; i < obsIndex.length; i += 1) {
65
+ cellIndices[obsIndex[i]] = i;
66
+ }
67
+ const exprValues = cellObjects.map((cell) => {
68
+ const cellIndex = cellIndices[cell.obsId];
69
+ const value = expressionData[0][cellIndex];
70
+ const transformFunction = getValueTransformFunction(
71
+ featureValueTransform, featureValueTransformCoefficient,
72
+ );
73
+ const transformedValue = transformFunction(value);
74
+ exprMax = Math.max(transformedValue, exprMax);
75
+ return { value: transformedValue, gene: firstGeneSelected, set: cell.name };
76
+ });
77
+ return [exprValues, exprMax];
78
+ }
79
+ return [null, null];
80
+ }, [expressionData, obsIndex, geneSelection, theme,
81
+ mergedCellSets, cellSetSelection, cellSetColor,
82
+ featureValueTransform, featureValueTransformCoefficient,
83
+ ]);
84
+
85
+ // From the cell sets hierarchy and the list of selected cell sets,
86
+ // generate the array of set sizes data points for the bar plot.
87
+ const setArr = useMemo(() => (mergedCellSets && cellSetSelection && cellSetColor
88
+ ? treeToSetSizesBySetNames(
89
+ mergedCellSets, cellSetSelection, cellSetSelection, cellSetColor, theme,
90
+ )
91
+ : []
92
+ ), [mergedCellSets, cellSetSelection, cellSetColor, theme]);
93
+
94
+ return [expressionArr, setArr, expressionMax];
95
+ }
96
+
97
+
98
+ /**
99
+ * A subscriber component for `CellSetExpressionPlot`,
100
+ * which listens for gene selection updates and
101
+ * `GRID_RESIZE` events.
102
+ * @param {object} props
103
+ * @param {function} props.removeGridComponent The grid component removal function.
104
+ * @param {object} props.coordinationScopes An object mapping coordination
105
+ * types to coordination scopes.
106
+ * @param {string} props.theme The name of the current Vitessce theme.
107
+ */
108
+ export function CellSetExpressionPlotSubscriber(props) {
109
+ const {
110
+ coordinationScopes,
111
+ removeGridComponent,
112
+ theme,
113
+ } = props;
114
+
115
+ const classes = useStyles();
116
+ const loaders = useLoaders();
117
+
118
+ // Get "props" from the coordination space.
119
+ const [{
120
+ dataset,
121
+ obsType,
122
+ featureType,
123
+ featureValueType,
124
+ featureSelection: geneSelection,
125
+ featureValueTransform,
126
+ featureValueTransformCoefficient,
127
+ obsSetSelection: cellSetSelection,
128
+ obsSetColor: cellSetColor,
129
+ additionalObsSets: additionalCellSets,
130
+ }, {
131
+ setFeatureValueTransform,
132
+ setFeatureValueTransformCoefficient,
133
+ }] = useCoordination(
134
+ COMPONENT_COORDINATION_TYPES[ViewType.OBS_SET_FEATURE_VALUE_DISTRIBUTION],
135
+ coordinationScopes,
136
+ );
137
+
138
+ const [width, height, containerRef] = useGridItemSize();
139
+ const [urls, addUrl] = useUrls(loaders, dataset);
140
+
141
+ const transformOptions = VALUE_TRANSFORM_OPTIONS;
142
+
143
+ // Get data from loaders using the data hooks.
144
+ // eslint-disable-next-line no-unused-vars
145
+ const [expressionData, loadedFeatureSelection, featureSelectionStatus] = useFeatureSelection(
146
+ loaders, dataset, false, geneSelection,
147
+ { obsType, featureType, featureValueType },
148
+ );
149
+ // TODO: support multiple feature labels using featureLabelsType coordination values.
150
+ const [{ featureLabelsMap }, featureLabelsStatus] = useFeatureLabelsData(
151
+ loaders, dataset, addUrl, false, {}, {},
152
+ { featureType },
153
+ );
154
+ const [{ obsIndex }, matrixIndicesStatus] = useObsFeatureMatrixIndices(
155
+ loaders, dataset, addUrl, false,
156
+ { obsType, featureType, featureValueType },
157
+ );
158
+ const [{ obsSets: cellSets }, obsSetsStatus] = useObsSetsData(
159
+ loaders, dataset, addUrl, true, {}, {},
160
+ { obsType },
161
+ );
162
+ const isReady = useReady([
163
+ featureSelectionStatus,
164
+ matrixIndicesStatus,
165
+ obsSetsStatus,
166
+ featureLabelsStatus,
167
+ ]);
168
+
169
+ const [expressionArr, setArr, expressionMax] = useExpressionByCellSet(
170
+ expressionData, obsIndex, cellSets, additionalCellSets,
171
+ geneSelection, cellSetSelection, cellSetColor,
172
+ featureValueTransform, featureValueTransformCoefficient,
173
+ theme,
174
+ );
175
+
176
+ const firstGeneSelected = geneSelection && geneSelection.length >= 1
177
+ ? (featureLabelsMap?.get(geneSelection[0]) || geneSelection[0])
178
+ : null;
179
+ const selectedTransformName = transformOptions.find(
180
+ o => o.value === featureValueTransform,
181
+ )?.name;
182
+
183
+
184
+ return (
185
+ <TitleInfo
186
+ title={`Expression by ${capitalize(obsType)} Set${(firstGeneSelected ? ` (${firstGeneSelected})` : '')}`}
187
+ removeGridComponent={removeGridComponent}
188
+ urls={urls}
189
+ theme={theme}
190
+ isReady={isReady}
191
+ options={(
192
+ <CellSetExpressionPlotOptions
193
+ featureValueTransform={featureValueTransform}
194
+ setFeatureValueTransform={setFeatureValueTransform}
195
+ featureValueTransformCoefficient={featureValueTransformCoefficient}
196
+ setFeatureValueTransformCoefficient={setFeatureValueTransformCoefficient}
197
+ transformOptions={transformOptions}
198
+ />
199
+ )}
200
+ >
201
+ <div ref={containerRef} className={classes.vegaContainer}>
202
+ {expressionArr ? (
203
+ <CellSetExpressionPlot
204
+ domainMax={expressionMax}
205
+ colors={setArr}
206
+ data={expressionArr}
207
+ theme={theme}
208
+ width={width}
209
+ height={height}
210
+ obsType={obsType}
211
+ featureValueType={featureValueType}
212
+ featureValueTransformName={selectedTransformName}
213
+ />
214
+ ) : (
215
+ <span>Select a {featureType}.</span>
216
+ )}
217
+ </div>
218
+ </TitleInfo>
219
+ );
220
+ }
@@ -0,0 +1,173 @@
1
+ import React, { useCallback } from 'react';
2
+ import { clamp } from 'lodash-es';
3
+ import { VegaPlot, VEGA_THEMES } from '@vitessce/vega';
4
+ import { colorArrayToString } from '@vitessce/sets-utils';
5
+ import { capitalize, getDefaultColor } from '@vitessce/utils';
6
+
7
+ /**
8
+ * Cell set sizes displayed as a bar chart,
9
+ * implemented with the VegaPlot component.
10
+ * @param {object} props
11
+ * @param {object[]} props.data The set size data, an array
12
+ * of objects with properties `name`, `key`, `color`, and `size`.
13
+ * @param {string} props.theme The name of the current Vitessce theme.
14
+ * @param {number} props.width The container width.
15
+ * @param {number} props.height The container height.
16
+ * @param {number} props.marginRight The size of the margin
17
+ * on the right side of the plot, to account for the vega menu button.
18
+ * By default, 90.
19
+ * @param {number} props.marginBottom The size of the margin
20
+ * on the bottom of the plot, to account for long x-axis labels.
21
+ * By default, 120.
22
+ * @param {number} props.keyLength The length of the `key` property of
23
+ * each data point. Assumes all key strings have the same length.
24
+ * By default, 36.
25
+ */
26
+ export default function CellSetSizesPlot(props) {
27
+ const {
28
+ data: rawData,
29
+ theme,
30
+ width,
31
+ height,
32
+ marginRight = 90,
33
+ marginBottom = 120,
34
+ keyLength = 36,
35
+ obsType,
36
+ onBarSelect,
37
+ } = props;
38
+
39
+ // Add a property `keyName` which concatenates the key and the name,
40
+ // which is both unique and can easily be converted
41
+ // back to the name by taking a substring.
42
+ // Add a property `colorString` which contains the `[r, g, b]` color
43
+ // after converting to a color hex string.
44
+ const data = rawData.map(d => ({
45
+ ...d,
46
+ keyName: d.key + d.name,
47
+ colorString: colorArrayToString(d.color),
48
+ }));
49
+
50
+ // Get an array of keys for sorting purposes.
51
+ const keys = data.map(d => d.keyName);
52
+
53
+ const colorScale = {
54
+ // Manually set the color scale so that Vega-Lite does
55
+ // not choose the colors automatically.
56
+ domain: data.map(d => d.key),
57
+ range: data.map((d) => {
58
+ const [r, g, b] = !d.isGrayedOut ? d.color : getDefaultColor(theme);
59
+ return `rgba(${r}, ${g}, ${b}, 1)`;
60
+ }),
61
+ };
62
+ const captializedObsType = capitalize(obsType);
63
+
64
+ const spec = {
65
+ mark: { type: 'bar', stroke: 'black', cursor: 'pointer' },
66
+ params: [
67
+ {
68
+ name: 'highlight',
69
+ select: {
70
+ type: 'point',
71
+ on: 'mouseover',
72
+ },
73
+ },
74
+ {
75
+ name: 'select',
76
+ select: 'point',
77
+ },
78
+ {
79
+ name: 'bar_select',
80
+ select: {
81
+ type: 'point',
82
+ on: 'click[event.shiftKey === false]',
83
+ fields: ['setNamePath', 'isGrayedOut'],
84
+ empty: 'none',
85
+ },
86
+ },
87
+ {
88
+ name: 'shift_bar_select',
89
+ select: {
90
+ type: 'point',
91
+ on: 'click[event.shiftKey]',
92
+ fields: ['setNamePath', 'isGrayedOut'],
93
+ empty: 'none',
94
+ },
95
+ },
96
+ ],
97
+ encoding: {
98
+ x: {
99
+ field: 'keyName',
100
+ type: 'nominal',
101
+ axis: { labelExpr: `substring(datum.label, ${keyLength})` },
102
+ title: 'Cell Set',
103
+ sort: keys,
104
+ },
105
+ y: {
106
+ field: 'size',
107
+ type: 'quantitative',
108
+ title: `${captializedObsType} Set Size`,
109
+ },
110
+ color: {
111
+ field: 'key',
112
+ type: 'nominal',
113
+ scale: colorScale,
114
+ legend: null,
115
+ },
116
+ tooltip: {
117
+ field: 'size',
118
+ type: 'quantitative',
119
+ },
120
+ fillOpacity: {
121
+ condition: {
122
+ param: 'select',
123
+ value: 1,
124
+ },
125
+ value: 0.3,
126
+ },
127
+ strokeWidth: {
128
+ condition: [
129
+ {
130
+ param: 'select',
131
+ empty: false,
132
+ value: 1,
133
+ },
134
+ {
135
+ param: 'highlight',
136
+ empty: false,
137
+ value: 2,
138
+ },
139
+ ],
140
+ value: 0,
141
+ },
142
+ },
143
+ width: clamp(width - marginRight, 10, Infinity),
144
+ height: clamp(height - marginBottom, 10, Infinity),
145
+ config: VEGA_THEMES[theme],
146
+ };
147
+
148
+ const handleSignal = (name, value) => {
149
+ if (name === 'bar_select') {
150
+ onBarSelect(value.setNamePath, value.isGrayedOut[0]);
151
+ } else if (name === 'shift_bar_select') {
152
+ const isGrayedOut = false;
153
+ const selectOnlyEnabled = true;
154
+ onBarSelect(value.setNamePath, isGrayedOut, selectOnlyEnabled);
155
+ }
156
+ };
157
+
158
+ const signalListeners = { bar_select: handleSignal, shift_bar_select: handleSignal };
159
+ const getTooltipText = useCallback(item => ({
160
+ [`${captializedObsType} Set`]: item.datum.name,
161
+ [`${captializedObsType} Set Size`]: item.datum.size,
162
+ }
163
+ ), [captializedObsType]);
164
+
165
+ return (
166
+ <VegaPlot
167
+ data={data}
168
+ spec={spec}
169
+ signalListeners={signalListeners}
170
+ getTooltipText={getTooltipText}
171
+ />
172
+ );
173
+ }
@@ -0,0 +1,151 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import {
3
+ TitleInfo,
4
+ useCoordination, useLoaders,
5
+ useUrls, useReady, useGridItemSize,
6
+ useObsSetsData,
7
+ } from '@vitessce/vit-s';
8
+ import { isEqual } from 'lodash-es';
9
+ import { ViewType, COMPONENT_COORDINATION_TYPES } from '@vitessce/constants-internal';
10
+ import {
11
+ mergeObsSets, treeToSetSizesBySetNames, filterPathsByExpansionAndSelection, findChangedHierarchy,
12
+ } from '@vitessce/sets-utils';
13
+ import { capitalize } from '@vitessce/utils';
14
+ import CellSetSizesPlot from './CellSetSizesPlot.js';
15
+ import { useStyles } from './styles.js';
16
+
17
+ /**
18
+ * A subscriber component for `CellSetSizePlot`,
19
+ * which listens for cell sets data updates and
20
+ * `GRID_RESIZE` events.
21
+ * @param {object} props
22
+ * @param {function} props.removeGridComponent The grid component removal function.
23
+ * @param {function} props.onReady The function to call when the subscriptions
24
+ * have been made.
25
+ * @param {string} props.theme The name of the current Vitessce theme.
26
+ * @param {string} props.title The component title.
27
+ */
28
+ export function CellSetSizesPlotSubscriber(props) {
29
+ const {
30
+ coordinationScopes,
31
+ removeGridComponent,
32
+ theme,
33
+ title: titleOverride,
34
+ } = props;
35
+
36
+ const classes = useStyles();
37
+
38
+ const loaders = useLoaders();
39
+
40
+ // Get "props" from the coordination space.
41
+ const [{
42
+ dataset,
43
+ obsType,
44
+ obsSetSelection: cellSetSelection,
45
+ obsSetColor: cellSetColor,
46
+ additionalObsSets: additionalCellSets,
47
+ obsSetExpansion: cellSetExpansion,
48
+ }, {
49
+ setObsSetSelection: setCellSetSelection,
50
+ setObsSetColor: setCellSetColor,
51
+ }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.OBS_SET_SIZES], coordinationScopes);
52
+
53
+ const title = titleOverride || `${capitalize(obsType)} Set Sizes`;
54
+
55
+ const [width, height, containerRef] = useGridItemSize();
56
+ const [urls, addUrl] = useUrls(loaders, dataset);
57
+
58
+ // the name of the hierarchy that was clicked on last
59
+ const [currentHierarchy, setCurrentHierarchy] = useState([]);
60
+ // the previous cell set that was selected
61
+ const [prevCellSetSelection, setPrevCellSetSelection] = useState([]);
62
+
63
+ // Get data from loaders using the data hooks.
64
+ const [{ obsSets: cellSets }, obsSetsStatus] = useObsSetsData(
65
+ loaders, dataset, addUrl, true,
66
+ { setObsSetSelection: setCellSetSelection, setObsSetColor: setCellSetColor },
67
+ { obsSetSelection: cellSetSelection, obsSetColor: cellSetColor },
68
+ { obsType },
69
+ );
70
+ const isReady = useReady([
71
+ obsSetsStatus,
72
+ ]);
73
+
74
+ const mergedCellSets = useMemo(
75
+ () => mergeObsSets(cellSets, additionalCellSets),
76
+ [cellSets, additionalCellSets],
77
+ );
78
+
79
+ const data = useMemo(() => {
80
+ if (cellSetSelection && cellSetColor && mergedCellSets && cellSets) {
81
+ let newHierarchy = currentHierarchy;
82
+
83
+ if (cellSetSelection) {
84
+ const changedHierarchy = findChangedHierarchy(prevCellSetSelection, cellSetSelection);
85
+ setPrevCellSetSelection(cellSetSelection);
86
+
87
+ if (changedHierarchy) {
88
+ setCurrentHierarchy(changedHierarchy);
89
+ newHierarchy = changedHierarchy;
90
+ }
91
+ }
92
+
93
+ const cellSetPaths = filterPathsByExpansionAndSelection(
94
+ mergedCellSets,
95
+ newHierarchy,
96
+ cellSetExpansion,
97
+ cellSetSelection,
98
+ );
99
+
100
+ if (mergedCellSets && cellSets && cellSetSelection && cellSetColor) {
101
+ return treeToSetSizesBySetNames(
102
+ mergedCellSets,
103
+ cellSetPaths,
104
+ cellSetSelection,
105
+ cellSetColor,
106
+ theme,
107
+ );
108
+ }
109
+ }
110
+ return [];
111
+ }, [
112
+ mergedCellSets,
113
+ cellSetSelection,
114
+ cellSetExpansion,
115
+ cellSetColor,
116
+ theme,
117
+ ]);
118
+
119
+ const onBarSelect = (setNamePath, wasGrayedOut, selectOnlyEnabled = false) => {
120
+ if (selectOnlyEnabled) {
121
+ setCellSetSelection([setNamePath]);
122
+ return;
123
+ }
124
+ if (!wasGrayedOut) {
125
+ setCellSetSelection(cellSetSelection.filter(d => !isEqual(d, setNamePath)));
126
+ } else if (wasGrayedOut) {
127
+ setCellSetSelection([...cellSetSelection, setNamePath]);
128
+ }
129
+ };
130
+
131
+ return (
132
+ <TitleInfo
133
+ title={title}
134
+ removeGridComponent={removeGridComponent}
135
+ urls={urls}
136
+ theme={theme}
137
+ isReady={isReady}
138
+ >
139
+ <div ref={containerRef} className={classes.vegaContainer}>
140
+ <CellSetSizesPlot
141
+ data={data}
142
+ onBarSelect={onBarSelect}
143
+ theme={theme}
144
+ width={width}
145
+ height={height}
146
+ obsType={obsType}
147
+ />
148
+ </div>
149
+ </TitleInfo>
150
+ );
151
+ }
@@ -0,0 +1,120 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { clamp, debounce } from 'lodash-es';
3
+ import { VegaPlot, VEGA_THEMES } from '@vitessce/vega';
4
+
5
+ /**
6
+ * We use debounce, so that onSelect is called only after the user has finished the selection.
7
+ * Due to vega-lite limitations, we cannot use the vega-lite signals to implement this.
8
+ * See this issue: https://github.com/vega/vega-lite/issues/5728
9
+ * See this for reference on what is supported: https://vega.github.io/vega-lite/docs/selection.html
10
+ */
11
+
12
+ /**
13
+ * Gene expression histogram displayed as a bar chart,
14
+ * implemented with the VegaPlot component.
15
+ * @param {object} props
16
+ * @param {string[]} props.geneSelection The list of genes
17
+ * currently selected.
18
+ * @param {object[]} props.data The expression data, an array
19
+ * of objects with properties `value` and `gene`.
20
+ * @param {string} props.theme The name of the current Vitessce theme.
21
+ * @param {number} props.width The container width.
22
+ * @param {number} props.height The container height.
23
+ * @param {number} props.marginRight The size of the margin
24
+ * on the right side of the plot, to account for the vega menu button.
25
+ * By default, 90.
26
+ * @param {number} props.marginBottom The size of the margin
27
+ * on the bottom of the plot, to account for long x-axis labels.
28
+ * By default, 50.
29
+ */
30
+ export default function ExpressionHistogram(props) {
31
+ const {
32
+ geneSelection,
33
+ data,
34
+ theme,
35
+ width,
36
+ height,
37
+ marginRight = 90,
38
+ marginBottom = 50,
39
+ onSelect,
40
+ } = props;
41
+
42
+ const [selectedRanges, setSelectedRanges] = useState([]);
43
+
44
+ const xTitle = geneSelection && geneSelection.length >= 1
45
+ ? 'Normalized Expression Value'
46
+ : 'Total Normalized Transcript Count';
47
+
48
+ const spec = {
49
+ data: { values: data },
50
+ mark: 'bar',
51
+ encoding: {
52
+ x: {
53
+ field: 'value',
54
+ type: 'quantitative',
55
+ bin: { maxbins: 50 },
56
+ title: xTitle,
57
+ },
58
+ y: {
59
+ type: 'quantitative',
60
+ aggregate: 'count',
61
+ title: 'Number of Cells',
62
+ },
63
+ color: { value: 'gray' },
64
+ opacity: {
65
+ condition: { selection: 'brush', value: 1 },
66
+ value: 0.7,
67
+ },
68
+ },
69
+ params: [
70
+ {
71
+ name: 'brush',
72
+ select: { type: 'interval', encodings: ['x'] },
73
+ },
74
+ ],
75
+ width: clamp(width - marginRight, 10, Infinity),
76
+ height: clamp(height - marginBottom, 10, Infinity),
77
+ config: VEGA_THEMES[theme],
78
+ };
79
+
80
+
81
+ const handleSignal = (name, value) => {
82
+ if (name === 'brush') {
83
+ setSelectedRanges(value.value);
84
+ }
85
+ };
86
+
87
+
88
+ // eslint-disable-next-line react-hooks/exhaustive-deps
89
+ const debouncedOnSelect = useCallback(debounce((ranges, latestOnSelect) => {
90
+ latestOnSelect(ranges);
91
+ // We set a debounce timer of 1000ms: the assumption here is that the user has
92
+ // finished the selection when there's been no mouse movement on the histogram for a second.
93
+ // We do not pass any dependencies for the useCallback
94
+ // since we only want to define the debounced function once (on the initial render).
95
+ }, 1000), []);
96
+
97
+ useEffect(() => {
98
+ if (!selectedRanges || selectedRanges.length === 0) return () => {};
99
+
100
+ // Call the debounced function instead of directly calling onSelect
101
+ debouncedOnSelect(selectedRanges, onSelect);
102
+
103
+ // Clean up the debounce timer when the component unmounts or the dependency changes
104
+ return () => {
105
+ debouncedOnSelect.cancel();
106
+ };
107
+ // We only want to call the debounced function when the selectedRanges changes.
108
+ // eslint-disable-next-line react-hooks/exhaustive-deps
109
+ }, [selectedRanges]);
110
+
111
+ const signalListeners = { brush: handleSignal };
112
+
113
+ return (
114
+ <VegaPlot
115
+ data={data}
116
+ signalListeners={signalListeners}
117
+ spec={spec}
118
+ />
119
+ );
120
+ }