@vitessce/statistical-plots 3.5.9 → 3.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/{deflate-19841f78.js → deflate-b8c6daae.js} +1 -1
  2. package/dist/{index-dc733355.js → index-134a71c6.js} +33739 -15506
  3. package/dist/index.js +6 -5
  4. package/dist/{jpeg-a83077be.js → jpeg-1b818d11.js} +1 -1
  5. package/dist/{lerc-1edd075a.js → lerc-f38cbdfc.js} +1 -1
  6. package/dist/{lzw-9572eac3.js → lzw-1120aba9.js} +1 -1
  7. package/dist/{packbits-cce11fbc.js → packbits-2d02b5e3.js} +1 -1
  8. package/dist/{raw-f7587aff.js → raw-0887baec.js} +1 -1
  9. package/dist/{webimage-8d38cd8b.js → webimage-533922a5.js} +1 -1
  10. package/dist-tsc/CellSetCompositionBarPlot.d.ts.map +1 -1
  11. package/dist-tsc/CellSetCompositionBarPlot.js +31 -10
  12. package/dist-tsc/CellSetCompositionBarPlotSubscriber.js +1 -1
  13. package/dist-tsc/CellSetExpressionPlot.d.ts.map +1 -1
  14. package/dist-tsc/CellSetExpressionPlot.js +26 -10
  15. package/dist-tsc/CellSetExpressionPlotOptions.d.ts.map +1 -1
  16. package/dist-tsc/CellSetExpressionPlotOptions.js +11 -4
  17. package/dist-tsc/CellSetExpressionPlotSubscriber.d.ts.map +1 -1
  18. package/dist-tsc/CellSetExpressionPlotSubscriber.js +46 -11
  19. package/dist-tsc/DotPlot.d.ts.map +1 -1
  20. package/dist-tsc/DotPlot.js +59 -6
  21. package/dist-tsc/DotPlotSubscriber.d.ts.map +1 -1
  22. package/dist-tsc/DotPlotSubscriber.js +1 -1
  23. package/dist-tsc/FeatureSetEnrichmentBarPlot.d.ts.map +1 -1
  24. package/dist-tsc/FeatureSetEnrichmentBarPlot.js +7 -6
  25. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.d.ts.map +1 -1
  26. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.js +5 -2
  27. package/dist-tsc/FeatureStatsTable.d.ts +2 -0
  28. package/dist-tsc/FeatureStatsTable.d.ts.map +1 -0
  29. package/dist-tsc/FeatureStatsTable.js +81 -0
  30. package/dist-tsc/FeatureStatsTableSubscriber.d.ts +2 -0
  31. package/dist-tsc/FeatureStatsTableSubscriber.d.ts.map +1 -0
  32. package/dist-tsc/FeatureStatsTableSubscriber.js +28 -0
  33. package/dist-tsc/Treemap.d.ts.map +1 -1
  34. package/dist-tsc/Treemap.js +17 -3
  35. package/dist-tsc/TreemapSubscriber.d.ts.map +1 -1
  36. package/dist-tsc/TreemapSubscriber.js +15 -9
  37. package/dist-tsc/VolcanoPlot.d.ts.map +1 -1
  38. package/dist-tsc/VolcanoPlot.js +15 -46
  39. package/dist-tsc/VolcanoPlotSubscriber.d.ts.map +1 -1
  40. package/dist-tsc/VolcanoPlotSubscriber.js +4 -2
  41. package/dist-tsc/expr-hooks.d.ts.map +1 -1
  42. package/dist-tsc/expr-hooks.test.js +2 -1
  43. package/dist-tsc/index.d.ts +1 -0
  44. package/dist-tsc/index.js +1 -0
  45. package/dist-tsc/utils.d.ts +1 -0
  46. package/dist-tsc/utils.d.ts.map +1 -1
  47. package/dist-tsc/utils.js +56 -0
  48. package/package.json +8 -7
  49. package/src/CellSetCompositionBarPlot.js +38 -12
  50. package/src/CellSetCompositionBarPlotSubscriber.js +1 -1
  51. package/src/CellSetExpressionPlot.js +33 -10
  52. package/src/CellSetExpressionPlotOptions.js +39 -2
  53. package/src/CellSetExpressionPlotSubscriber.js +56 -13
  54. package/src/DotPlot.js +81 -11
  55. package/src/DotPlotSubscriber.js +3 -1
  56. package/src/FeatureSetEnrichmentBarPlot.js +7 -6
  57. package/src/FeatureSetEnrichmentBarPlotSubscriber.js +5 -2
  58. package/src/FeatureStatsTable.js +116 -0
  59. package/src/FeatureStatsTableSubscriber.js +133 -0
  60. package/src/Treemap.js +21 -3
  61. package/src/TreemapSubscriber.js +26 -11
  62. package/src/VolcanoPlot.js +16 -64
  63. package/src/VolcanoPlotSubscriber.js +6 -1
  64. package/src/expr-hooks.js +0 -1
  65. package/src/expr-hooks.test.js +2 -1
  66. package/src/index.js +1 -0
  67. package/src/utils.js +82 -1
@@ -1,10 +1,13 @@
1
1
  import React, { useCallback, useMemo } from 'react';
2
2
  import { clamp, isEqual } from 'lodash-es';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
+ import { extent } from 'd3-array';
4
5
  import { VegaPlot, VEGA_THEMES } from '@vitessce/vega';
5
6
  import { capitalize } from '@vitessce/utils';
6
7
  import { getColorScale } from './utils.js';
7
8
 
9
+ const MAX_BAR_SIZE = 40;
10
+
8
11
  /**
9
12
  * Cell set composition results displayed using a bar chart.
10
13
  */
@@ -13,9 +16,9 @@ export default function CellSetCompositionBarPlot(props) {
13
16
  data,
14
17
  theme,
15
18
  width,
16
- height,
19
+ height: heightProp,
17
20
  marginRight = 200,
18
- marginBottom = 120,
21
+ marginBottom = 60,
19
22
  keyLength = 36,
20
23
  obsType,
21
24
  onBarSelect,
@@ -27,6 +30,13 @@ export default function CellSetCompositionBarPlot(props) {
27
30
  sampleSetColor,
28
31
  } = props;
29
32
 
33
+ const height = (
34
+ Array.isArray(obsSetSelection)
35
+ && ((heightProp - marginBottom) / obsSetSelection.length >= MAX_BAR_SIZE)
36
+ )
37
+ ? MAX_BAR_SIZE * obsSetSelection.length + marginBottom
38
+ : heightProp;
39
+
30
40
  const [obsSetColorScale, sampleSetColorScale] = useMemo(() => [
31
41
  getColorScale(obsSetSelection, obsSetColor, theme),
32
42
  getColorScale(sampleSetSelection, sampleSetColor, theme),
@@ -39,21 +49,17 @@ export default function CellSetCompositionBarPlot(props) {
39
49
  // Return in array-of-objects form that Vega-Lite likes.
40
50
 
41
51
  const referenceCellType = metadata?.analysis_params?.reference_cell_type;
52
+ const covariateValue = metadata?.analysis_params?.covariate_value;
42
53
  const coordinationValues = metadata?.coordination_values;
43
54
  const obsSetColumnName = coordinationValues?.obsSetSelection?.[0]?.[0];
44
55
  const obsSetGroupName = obsSetsColumnNameMappingReversed?.[obsSetColumnName];
45
56
 
46
57
  const sampleSetColumnName = coordinationValues?.sampleSetFilter?.[0]?.[0];
47
58
  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];
59
+ const covariateSetPath = [sampleSetGroupName, covariateValue];
54
60
 
55
61
  let shouldSwapFoldChangeDirection = false;
56
- if (isEqual(firstCovariateSetPath, sampleSetSelection[0])) {
62
+ if (isEqual(covariateSetPath, sampleSetSelection[0])) {
57
63
  shouldSwapFoldChangeDirection = true;
58
64
  }
59
65
 
@@ -95,13 +101,13 @@ export default function CellSetCompositionBarPlot(props) {
95
101
  ]);
96
102
 
97
103
  // Get an array of keys for sorting purposes.
98
- const keys = computedData.map(d => d.keyName);
104
+ const keys = computedData?.map(d => d.keyName);
99
105
 
100
106
  const colorScale = {
101
107
  // Manually set the color scale so that Vega-Lite does
102
108
  // not choose the colors automatically.
103
- domain: computedData.map(d => d.key),
104
- range: computedData.map(d => d.color),
109
+ domain: computedData?.map(d => d.key),
110
+ range: computedData?.map(d => d.color),
105
111
  };
106
112
  const captializedObsType = capitalize(obsType);
107
113
 
@@ -114,6 +120,24 @@ export default function CellSetCompositionBarPlot(props) {
114
120
  range: [2.0, 0.5],
115
121
  };
116
122
 
123
+ const xExtent = useMemo(() => {
124
+ if (computedData) {
125
+ const [min, max] = extent(computedData.map(d => d.logFoldChange));
126
+ const buffer = 1.05; // Ensure some extra space
127
+ const minAbs = Math.abs(min) * buffer;
128
+ const maxAbs = Math.abs(max) * buffer;
129
+ if (minAbs > maxAbs) {
130
+ return [-minAbs, minAbs];
131
+ }
132
+ return [-maxAbs, maxAbs];
133
+ }
134
+ return undefined;
135
+ }, [computedData]);
136
+
137
+ const xScale = {
138
+ domain: xExtent,
139
+ };
140
+
117
141
  const spec = {
118
142
  mark: { type: 'bar', stroke: 'black', cursor: 'pointer' },
119
143
  params: [
@@ -149,6 +173,7 @@ export default function CellSetCompositionBarPlot(props) {
149
173
  field: 'logFoldChange',
150
174
  type: 'quantitative',
151
175
  title: 'Log fold-change',
176
+ scale: xScale,
152
177
  },
153
178
  color: {
154
179
  field: 'key',
@@ -165,6 +190,7 @@ export default function CellSetCompositionBarPlot(props) {
165
190
  field: 'isReferenceSet',
166
191
  type: 'nominal',
167
192
  scale: strokeWidthScale,
193
+ legend: null,
168
194
  },
169
195
  tooltip: {
170
196
  field: 'effectExpectedSample',
@@ -143,7 +143,7 @@ export function CellSetCompositionBarPlotSubscriber(props) {
143
143
  onBarSelect={onBarSelect}
144
144
  />
145
145
  ) : (
146
- <span>Select at least one {obsType} set.</span>
146
+ <span>Select at least one {obsType} set and a pair of {sampleType} sets.</span>
147
147
  )}
148
148
  </div>
149
149
  </TitleInfo>
@@ -8,6 +8,7 @@ import { area as d3_area, curveBasis } from 'd3-shape';
8
8
  import { select } from 'd3-selection';
9
9
  import { colorArrayToString } from '@vitessce/sets-utils';
10
10
  import { capitalize } from '@vitessce/utils';
11
+ import { getColorScale } from './utils.js';
11
12
 
12
13
  const scaleBand = vega_scale('band');
13
14
 
@@ -35,9 +36,11 @@ const scaleBand = vega_scale('band');
35
36
  export default function CellSetExpressionPlot(props) {
36
37
  const {
37
38
  yMin: yMinProp,
39
+ xAxisTitle = null,
38
40
  yUnits,
39
41
  jitter,
40
- cellSetSelection,
42
+ obsSetSelection,
43
+ obsSetColor,
41
44
  sampleSetSelection,
42
45
  sampleSetColor,
43
46
  colors,
@@ -57,18 +60,22 @@ export default function CellSetExpressionPlot(props) {
57
60
 
58
61
  const svgRef = useRef();
59
62
 
63
+ const obsSetColorScale = useMemo(() => getColorScale(
64
+ obsSetSelection, obsSetColor, theme,
65
+ ), [obsSetSelection, obsSetColor, theme]);
66
+
60
67
  // Get the max characters in an axis label for autsizing the bottom margin.
61
68
  const maxCharactersForLabel = useMemo(() => {
62
- if (!cellSetSelection) {
69
+ if (!obsSetSelection) {
63
70
  return 0;
64
71
  }
65
- const cellSetNames = cellSetSelection.map(d => d.at(-1));
72
+ const cellSetNames = obsSetSelection.map(d => d.at(-1));
66
73
  return cellSetNames.reduce((acc, name) => {
67
74
  // eslint-disable-next-line no-param-reassign
68
75
  acc = acc === undefined || name.length > acc ? name.length : acc;
69
76
  return acc;
70
77
  }, 0);
71
- }, [cellSetSelection]);
78
+ }, [obsSetSelection]);
72
79
 
73
80
  const isStratified = (Array.isArray(sampleSetSelection) && sampleSetSelection.length === 2);
74
81
 
@@ -81,7 +88,7 @@ export default function CellSetExpressionPlot(props) {
81
88
  const unitSuffix = yUnits ? ` (${yUnits})` : '';
82
89
  const yTitle = `${transformPrefix}${capitalize(featureValueType)}${unitSuffix}`;
83
90
 
84
- const xTitle = `${capitalize(obsType)} Set`;
91
+ const xTitle = xAxisTitle ?? `${capitalize(obsType)} Set`;
85
92
 
86
93
  // Use a square-root term because the angle of the labels is 45 degrees (see below)
87
94
  // so the perpendicular distance to the bottom of the labels is proportional to the
@@ -146,7 +153,7 @@ export default function CellSetExpressionPlot(props) {
146
153
 
147
154
  const xGroup = scaleBand()
148
155
  .range([marginLeft, width - marginRight])
149
- .domain(cellSetSelection)
156
+ .domain(obsSetSelection)
150
157
  .padding(0.1);
151
158
 
152
159
 
@@ -314,11 +321,12 @@ export default function CellSetExpressionPlot(props) {
314
321
  .style('font-size', '11px');
315
322
 
316
323
  // X-axis ticks
317
- g
324
+ const xTickG = g
318
325
  .append('g')
319
326
  .attr('transform', `translate(0,${innerHeight})`)
320
- .style('font-size', '14px')
321
- .call(axisBottom(xGroup).tickFormat(d => d.at(-1)))
327
+ .style('font-size', '14px');
328
+
329
+ xTickG.call(axisBottom(xGroup).tickFormat(d => d.at(-1)))
322
330
  .selectAll('text')
323
331
  .style('font-size', '11px')
324
332
  .attr('dx', '-6px')
@@ -326,6 +334,20 @@ export default function CellSetExpressionPlot(props) {
326
334
  .attr('transform', 'rotate(-45)')
327
335
  .style('text-anchor', 'end');
328
336
 
337
+ if (isStratified) {
338
+ // Associate each X tick with a cell type color,
339
+ // since in the stratified case the violins are colored
340
+ // by sample set.
341
+ const tickWidth = xGroup.bandwidth();
342
+ xTickG.selectAll('.tick')
343
+ .append('rect')
344
+ .attr('x', -tickWidth / 2)
345
+ // .attr("y", -innerHeight)
346
+ .attr('width', tickWidth)
347
+ .attr('height', 4)
348
+ .style('fill', d => obsSetColorScale(d));
349
+ }
350
+
329
351
  // Y-axis title
330
352
  g
331
353
  .append('text')
@@ -394,7 +416,8 @@ export default function CellSetExpressionPlot(props) {
394
416
  }, [width, height, data, marginLeft, marginBottom, colors,
395
417
  jitter, theme, yMinProp, marginTop, marginRight, featureType,
396
418
  featureValueType, featureValueTransformName, yUnits, obsType,
397
- maxCharactersForLabel, sampleSetSelection,
419
+ maxCharactersForLabel, sampleSetSelection, isStratified,
420
+ obsSetColorScale,
398
421
  ]);
399
422
 
400
423
  return (
@@ -3,6 +3,10 @@ import { useId } from 'react-aria';
3
3
  import { TableCell, TableRow, TextField, Slider } from '@material-ui/core';
4
4
  import { usePlotOptionsStyles, OptionsContainer, OptionSelect } from '@vitessce/vit-s';
5
5
  import { GLSL_COLORMAPS } from '@vitessce/gl';
6
+ import { capitalize } from '@vitessce/utils';
7
+
8
+
9
+ const FEATURE_AGGREGATION_STRATEGIES = ['first', 'last', 'sum', 'mean'];
6
10
 
7
11
  export default function CellSetExpressionPlotOptions(props) {
8
12
  const {
@@ -15,6 +19,8 @@ export default function CellSetExpressionPlotOptions(props) {
15
19
  setFeatureValuePositivityThreshold,
16
20
  featureValueColormap,
17
21
  setFeatureValueColormap,
22
+ featureAggregationStrategy,
23
+ setFeatureAggregationStrategy,
18
24
  } = props;
19
25
 
20
26
  const cellSetExpressionPlotOptionsId = useId();
@@ -25,9 +31,13 @@ export default function CellSetExpressionPlotOptions(props) {
25
31
  setFeatureValueColormap(event.target.value);
26
32
  }
27
33
 
28
- const handleTransformChange = (event) => {
34
+ function handleTransformChange(event) {
29
35
  setFeatureValueTransform(event.target.value === '' ? null : event.target.value);
30
- };
36
+ }
37
+
38
+ function handleFeatureAggregationStrategyChange(event) {
39
+ setFeatureAggregationStrategy(event.target.value);
40
+ }
31
41
 
32
42
  function handlePositivityThresholdChange(event, value) {
33
43
  setFeatureValuePositivityThreshold(value);
@@ -119,6 +129,33 @@ export default function CellSetExpressionPlotOptions(props) {
119
129
  />
120
130
  </TableCell>
121
131
  </TableRow>
132
+ {setFeatureAggregationStrategy ? (
133
+ <TableRow>
134
+ <TableCell className={classes.labelCell} variant="head" scope="row">
135
+ <label
136
+ htmlFor={`feature-aggregation-strategy-${cellSetExpressionPlotOptionsId}`}
137
+ >
138
+ Feature Aggregation Strategy
139
+ </label>
140
+ </TableCell>
141
+ <TableCell className={classes.inputCell} variant="body">
142
+ <OptionSelect
143
+ className={classes.select}
144
+ value={featureAggregationStrategy ?? 'first'}
145
+ onChange={handleFeatureAggregationStrategyChange}
146
+ inputProps={{
147
+ id: `feature-aggregation-strategy-${cellSetExpressionPlotOptionsId}`,
148
+ }}
149
+ >
150
+ {FEATURE_AGGREGATION_STRATEGIES.map(opt => (
151
+ <option key={opt} value={opt}>
152
+ {capitalize(opt)}
153
+ </option>
154
+ ))}
155
+ </OptionSelect>
156
+ </TableCell>
157
+ </TableRow>
158
+ ) : null}
122
159
  {setFeatureValuePositivityThreshold ? (
123
160
  <TableRow key="transform-coefficient-option-row">
124
161
  <TableCell className={classes.labelCell}>
@@ -26,6 +26,28 @@ import {
26
26
  histogramStratifiedExpressionData,
27
27
  } from './expr-hooks.js';
28
28
 
29
+ const DEFAULT_FEATURE_AGGREGATION_STRATEGY = 'first';
30
+
31
+ function featureSummary(geneSelection, featureAggregationStrategy) {
32
+ if (featureAggregationStrategy === 'first') {
33
+ return geneSelection?.[0];
34
+ } if (featureAggregationStrategy === 'last') {
35
+ return geneSelection?.at(-1);
36
+ } if (typeof featureAggregationStrategy === 'number') {
37
+ const i = featureAggregationStrategy;
38
+ return geneSelection?.[i];
39
+ } if (featureAggregationStrategy === 'sum') {
40
+ // TODO: make these .join()-ed labels more scalable,
41
+ // in particular, if more than 10 or so elements.
42
+ return geneSelection?.join(' + ');
43
+ } if (featureAggregationStrategy === 'mean') {
44
+ return `Mean of ${geneSelection?.join(', ')}`;
45
+ } if (featureAggregationStrategy === 'difference') {
46
+ return geneSelection?.join(' - ');
47
+ }
48
+ return '';
49
+ }
50
+
29
51
  /**
30
52
  * Get expression data for the cells
31
53
  * in the selected cell sets.
@@ -50,7 +72,7 @@ function useExpressionByCellSet(
50
72
  expressionData, obsIndex, cellSets, additionalCellSets,
51
73
  geneSelection, cellSetSelection, cellSetColor,
52
74
  featureValueTransform, featureValueTransformCoefficient,
53
- theme, yMinProp,
75
+ theme, yMinProp, featureAggregationStrategy,
54
76
  ) {
55
77
  const mergedCellSets = useMemo(
56
78
  () => mergeObsSets(cellSets, additionalCellSets),
@@ -68,7 +90,7 @@ function useExpressionByCellSet(
68
90
  );
69
91
  if (stratifiedData) {
70
92
  const aggregateData = aggregateStratifiedExpressionData(
71
- stratifiedData, geneSelection,
93
+ stratifiedData, geneSelection, featureAggregationStrategy,
72
94
  );
73
95
  const summarizedData = summarizeStratifiedExpressionData(
74
96
  aggregateData, true,
@@ -83,6 +105,7 @@ function useExpressionByCellSet(
83
105
  mergedCellSets, cellSetSelection, cellSetColor,
84
106
  featureValueTransform, featureValueTransformCoefficient,
85
107
  yMinProp, sampleEdges, sampleSets, sampleSetSelection,
108
+ featureAggregationStrategy,
86
109
  ]);
87
110
 
88
111
  // From the cell sets hierarchy and the list of selected cell sets,
@@ -114,6 +137,8 @@ export function CellSetExpressionPlotSubscriber(props) {
114
137
  downloadButtonVisible,
115
138
  removeGridComponent,
116
139
  theme,
140
+ title,
141
+ xAxisTitle,
117
142
  jitter = false,
118
143
  yMin = null,
119
144
  yUnits = null,
@@ -138,10 +163,12 @@ export function CellSetExpressionPlotSubscriber(props) {
138
163
  sampleType,
139
164
  sampleSetSelection,
140
165
  sampleSetColor,
166
+ featureAggregationStrategy,
141
167
  }, {
142
168
  setFeatureValueTransform,
143
169
  setFeatureValueTransformCoefficient,
144
170
  setSampleSetColor,
171
+ setFeatureAggregationStrategy,
145
172
  }] = useCoordination(
146
173
  COMPONENT_COORDINATION_TYPES[ViewType.OBS_SET_FEATURE_VALUE_DISTRIBUTION],
147
174
  coordinationScopes,
@@ -204,29 +231,41 @@ export function CellSetExpressionPlotSubscriber(props) {
204
231
  sampleEdgesUrls,
205
232
  ]);
206
233
 
234
+ const featureAggregationStrategyToUse = featureAggregationStrategy
235
+ ?? DEFAULT_FEATURE_AGGREGATION_STRATEGY;
236
+
207
237
  const [histogramData, setArr, exprMax] = useExpressionByCellSet(
208
238
  sampleEdges, sampleSets, sampleSetSelection,
209
239
  expressionData, obsIndex, cellSets, additionalCellSets,
210
240
  geneSelection, cellSetSelection, cellSetColor,
211
241
  featureValueTransform, featureValueTransformCoefficient,
212
- theme, yMin,
242
+ theme, yMin, featureAggregationStrategyToUse,
213
243
  );
214
244
 
215
- const firstGeneSelected = geneSelection && geneSelection.length >= 1
216
- ? (
217
- featureLabelsMap?.get(geneSelection[0])
218
- || featureLabelsMap?.get(cleanFeatureId(geneSelection[0]))
219
- || geneSelection[0]
220
- )
221
- : null;
245
+ const featureSuffix = useMemo(() => {
246
+ const cleanedGeneSelection = geneSelection?.map(geneName => (
247
+ featureLabelsMap?.get(geneName)
248
+ || featureLabelsMap?.get(cleanFeatureId(geneName))
249
+ || geneName
250
+ ));
251
+ if (Array.isArray(cleanedGeneSelection)) {
252
+ return featureSummary(cleanedGeneSelection, featureAggregationStrategyToUse);
253
+ }
254
+ return null;
255
+ }, [geneSelection, featureAggregationStrategyToUse]);
256
+
257
+
222
258
  const selectedTransformName = transformOptions.find(
223
259
  o => o.value === featureValueTransform,
224
260
  )?.name;
225
-
261
+ // Use empty string when firstGeneSelected is null
262
+ const titleSuffix = featureSuffix ? ` (${featureSuffix})` : '';
226
263
 
227
264
  return (
228
265
  <TitleInfo
229
- title={`Expression by ${capitalize(obsType)} Set${(firstGeneSelected ? ` (${firstGeneSelected})` : '')}`}
266
+ title={title ? `${title}${titleSuffix}`
267
+ : `Expression by ${capitalize(obsType)} Set${titleSuffix}`
268
+ }
230
269
  closeButtonVisible={closeButtonVisible}
231
270
  downloadButtonVisible={downloadButtonVisible}
232
271
  removeGridComponent={removeGridComponent}
@@ -241,6 +280,8 @@ export function CellSetExpressionPlotSubscriber(props) {
241
280
  featureValueTransformCoefficient={featureValueTransformCoefficient}
242
281
  setFeatureValueTransformCoefficient={setFeatureValueTransformCoefficient}
243
282
  transformOptions={transformOptions}
283
+ featureAggregationStrategy={featureAggregationStrategy}
284
+ setFeatureAggregationStrategy={setFeatureAggregationStrategy}
244
285
  />
245
286
  )}
246
287
  >
@@ -250,7 +291,8 @@ export function CellSetExpressionPlotSubscriber(props) {
250
291
  yMin={yMin}
251
292
  yUnits={yUnits}
252
293
  jitter={jitter}
253
- cellSetSelection={cellSetSelection}
294
+ obsSetSelection={cellSetSelection}
295
+ obsSetColor={cellSetColor}
254
296
  sampleSetSelection={sampleSetSelection}
255
297
  sampleSetColor={sampleSetColor}
256
298
  colors={setArr}
@@ -263,6 +305,7 @@ export function CellSetExpressionPlotSubscriber(props) {
263
305
  featureType={featureType}
264
306
  featureValueType={featureValueType}
265
307
  featureValueTransformName={selectedTransformName}
308
+ xAxisTitle={xAxisTitle}
266
309
  />
267
310
  ) : (
268
311
  <span>Select a {featureType}.</span>
package/src/DotPlot.js CHANGED
@@ -1,7 +1,9 @@
1
- import React from 'react';
1
+ import React, { useRef, useEffect, useCallback, useState, useMemo } from 'react';
2
2
  import { clamp } from 'lodash-es';
3
3
  import { VegaPlot, VEGA_THEMES } from '@vitessce/vega';
4
4
  import { capitalize, pluralize as plur } from '@vitessce/utils';
5
+ import { select } from 'd3-selection';
6
+ import { getColorScale } from './utils.js';
5
7
 
6
8
  /**
7
9
  * Gene expression dot plot,
@@ -32,14 +34,19 @@ export default function DotPlot(props) {
32
34
  marginRight,
33
35
  marginBottom,
34
36
  obsType,
37
+ sampleType,
35
38
  keyLength = 36,
36
39
  featureType,
37
40
  featureValueType,
38
41
  featureValueTransformName,
39
- featureValueColormap,
40
- cellSetSelection,
42
+ // TODO: re-enable featureValueColormap coordination
43
+ // featureValueColormap,
44
+ obsSetSelection,
45
+ obsSetColor,
41
46
  } = props;
42
47
 
48
+ const vegaContainerRef = useRef();
49
+
43
50
  // Add a property `keyGroup` and `keyFeature` which concatenates the key and the name,
44
51
  // which is both unique and can easily be converted
45
52
  // back to the name by taking a substring.
@@ -80,18 +87,17 @@ export default function DotPlot(props) {
80
87
  || 30 + Math.sqrt(maxCharactersForSampleSet / 2) * 30;
81
88
 
82
89
  const plotWidth = transpose
83
- ? clamp(width - autoMarginForFeature - 180, 10, Infinity) / (cellSetSelection?.length || 1)
90
+ ? clamp(width - autoMarginForFeature - 180, 10, Infinity) / (obsSetSelection?.length || 1)
84
91
  : clamp(width - autoMarginForGroup - autoMarginForSampleSet - 200, 10, Infinity);
85
92
  const plotHeight = transpose
86
93
  ? clamp((height - autoMarginForGroup - autoMarginForSampleSet - 50), 10, Infinity)
87
- : clamp((height - autoMarginForFeature - 80), 10, Infinity) / (cellSetSelection?.length || 1);
94
+ : clamp((height - autoMarginForFeature - 80), 10, Infinity) / (obsSetSelection?.length || 1);
88
95
 
89
96
  // Get an array of keys for sorting purposes.
90
97
  const groupKeys = data.map(d => d.keyGroup);
91
98
  const featureKeys = data.map(d => d.keyFeature);
92
99
  const groupSecondaryKeys = data.map(d => d.keyGroupSecondary);
93
100
 
94
-
95
101
  const meanTransform = (featureValueTransformName && featureValueTransformName !== 'None')
96
102
  // Mean Log-Transformed Normalized Expression
97
103
  ? [`Mean ${featureValueTransformName}-transformed`, `normalized ${featureValueType}`, 'in set']
@@ -129,7 +135,8 @@ export default function DotPlot(props) {
129
135
  type: 'quantitative',
130
136
  title: meanTransform,
131
137
  scale: {
132
- scheme: featureValueColormap,
138
+ // scheme: featureValueColormap,
139
+ scheme: 'greys',
133
140
  },
134
141
  legend: {
135
142
  direction: 'horizontal',
@@ -150,6 +157,11 @@ export default function DotPlot(props) {
150
157
  legend: {
151
158
  symbolFillColor: 'white',
152
159
  },
160
+ scale: { domain: [0, 100] },
161
+ },
162
+ tooltip: {
163
+ field: 'pctPosInGroup',
164
+ type: 'quantitative',
153
165
  },
154
166
  },
155
167
  width: plotWidth,
@@ -166,10 +178,68 @@ export default function DotPlot(props) {
166
178
  },
167
179
  };
168
180
 
181
+ const getTooltipText = useCallback(item => ({
182
+ [`${capitalize(featureType)}`]: item.datum.feature,
183
+ [`${capitalize(obsType)} Set`]: item.datum.group,
184
+ ...(isStratified
185
+ ? ({ [`${capitalize(sampleType)} Set`]: item.datum.secondaryGroup })
186
+ : {}
187
+ ),
188
+ [`Percentage of ${plur(obsType, 2)} in set`]: item.datum.pctPosInGroup,
189
+ [meanTransform.join(' ')]: item.datum.meanExpInGroup,
190
+ }), [featureType, obsType, featureValueType, featureValueTransformName]);
191
+
192
+ const obsSetColorScale = useMemo(() => getColorScale(
193
+ obsSetSelection, obsSetColor, theme,
194
+ ), [obsSetSelection, obsSetColor, theme]);
195
+
196
+ const [vegaRenderIncrement, setVegaRenderIncrement] = useState(0);
197
+
198
+ useEffect(() => {
199
+ // If the dot plot is stratified by both obsSet and sampleSet,
200
+ // then we want to add cell set colors.
201
+ // TODO: do we also want to add these color bars
202
+ // when only stratified by obsSet?
203
+ const domElement = vegaContainerRef.current;
204
+
205
+ // Here, we assume that the Vega SVG renderer is being used.
206
+ const svg = select(domElement)
207
+ .select('svg');
208
+ // We use the following CSS selector to identify all of
209
+ // the <line> elements that we are interested to modify.
210
+ const tickEls = svg.selectAll('g.root g.column_footer g.role-axis g.role-axis-domain line');
211
+
212
+ tickEls
213
+ .attr('stroke-width', 5)
214
+ .attr('dy', 2.5)
215
+ .attr('stroke', (d, i) => {
216
+ const obsSetPath = obsSetSelection?.[i];
217
+ return obsSetColorScale(obsSetPath);
218
+ });
219
+ }, [vegaContainerRef, vegaRenderIncrement, obsSetSelection, obsSetColorScale]);
220
+
221
+
222
+ // We want to increment the counter whenever we detect that VegaPlot
223
+ // has re-rendered.
224
+ const onNewView = useCallback(() => {
225
+ setVegaRenderIncrement(prev => prev + 1);
226
+ }, []);
227
+
228
+ // This is kind of hacky, since it is possible that the useEffect runs prior
229
+ // to the Vega rendering, but in practice it seems to work.
230
+ useEffect(() => {
231
+ setVegaRenderIncrement(prev => prev + 1);
232
+ }, [rawData]);
233
+
169
234
  return (
170
- <VegaPlot
171
- data={data}
172
- spec={spec}
173
- />
235
+ <div ref={vegaContainerRef}>
236
+ <VegaPlot
237
+ data={data}
238
+ spec={spec}
239
+ onNewView={onNewView}
240
+ getTooltipText={getTooltipText}
241
+ renderer="svg"
242
+ />
243
+ </div>
174
244
  );
175
245
  }
@@ -160,11 +160,13 @@ export function DotPlotSubscriber(props) {
160
160
  width={width}
161
161
  height={height}
162
162
  obsType={obsType}
163
+ sampleType={sampleType}
163
164
  featureType={featureType}
164
165
  featureValueType={featureValueType}
165
166
  featureValueTransformName={selectedTransformName}
166
167
  featureValueColormap={featureValueColormap}
167
- cellSetSelection={cellSetSelection}
168
+ obsSetSelection={cellSetSelection}
169
+ obsSetColor={cellSetColor}
168
170
  />
169
171
  ) : (
170
172
  <span>Select at least one {featureType}.</span>
@@ -14,7 +14,7 @@ export default function FeatureSetEnrichmentBarPlot(props) {
14
14
  theme,
15
15
  width,
16
16
  height,
17
- marginRight = 200,
17
+ marginRight = 300,
18
18
  marginBottom = 120,
19
19
  keyLength = 36,
20
20
  featureType,
@@ -86,7 +86,7 @@ export default function FeatureSetEnrichmentBarPlot(props) {
86
86
  return [...a, h];
87
87
  }, []);
88
88
 
89
- const MAX_ROWS = 25;
89
+ const MAX_ROWS = 50;
90
90
  result = result.slice(0, MAX_ROWS);
91
91
  return result;
92
92
  }
@@ -115,7 +115,7 @@ export default function FeatureSetEnrichmentBarPlot(props) {
115
115
  select: {
116
116
  type: 'point',
117
117
  on: 'click[event.shiftKey === false]',
118
- fields: ['name'],
118
+ fields: ['name', 'term'],
119
119
  empty: 'none',
120
120
  },
121
121
  },
@@ -124,7 +124,7 @@ export default function FeatureSetEnrichmentBarPlot(props) {
124
124
  select: {
125
125
  type: 'point',
126
126
  on: 'click[event.shiftKey]',
127
- fields: ['name'],
127
+ fields: ['name', 'term'],
128
128
  empty: 'none',
129
129
  },
130
130
  },
@@ -173,9 +173,10 @@ export default function FeatureSetEnrichmentBarPlot(props) {
173
173
 
174
174
  const handleSignal = (name, value) => {
175
175
  if (name === 'bar_select') {
176
- onBarSelect(value.obsSetPath);
176
+ onBarSelect(value.name?.[0], value.term?.[0]);
177
177
  } else if (name === 'shift_bar_select') {
178
- onBarSelect(value.obsSetPath, true);
178
+ // Name and term may be arrays
179
+ onBarSelect(value.name, value.term, true);
179
180
  }
180
181
  };
181
182