@vitessce/statistical-plots 3.5.7 → 3.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/{deflate-287e693d.js → deflate-19841f78.js} +1 -1
  2. package/dist/{index-1f1b6355.js → index-dc733355.js} +2084 -1065
  3. package/dist/index.js +9 -6
  4. package/dist/{jpeg-1b2c1d25.js → jpeg-a83077be.js} +1 -1
  5. package/dist/{lerc-4f010cd7.js → lerc-1edd075a.js} +1 -1
  6. package/dist/{lzw-e60fb582.js → lzw-9572eac3.js} +1 -1
  7. package/dist/{packbits-a8bfe098.js → packbits-cce11fbc.js} +1 -1
  8. package/dist/{raw-01dff90e.js → raw-f7587aff.js} +1 -1
  9. package/dist/{webimage-6b926ce3.js → webimage-8d38cd8b.js} +1 -1
  10. package/dist-tsc/CellSetCompositionBarPlot.d.ts +5 -0
  11. package/dist-tsc/CellSetCompositionBarPlot.d.ts.map +1 -0
  12. package/dist-tsc/CellSetCompositionBarPlot.js +166 -0
  13. package/dist-tsc/CellSetCompositionBarPlotSubscriber.d.ts +2 -0
  14. package/dist-tsc/CellSetCompositionBarPlotSubscriber.d.ts.map +1 -0
  15. package/dist-tsc/CellSetCompositionBarPlotSubscriber.js +40 -0
  16. package/dist-tsc/FeatureSetEnrichmentBarPlot.d.ts +5 -0
  17. package/dist-tsc/FeatureSetEnrichmentBarPlot.d.ts.map +1 -0
  18. package/dist-tsc/FeatureSetEnrichmentBarPlot.js +164 -0
  19. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.d.ts +2 -0
  20. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.d.ts.map +1 -0
  21. package/dist-tsc/FeatureSetEnrichmentBarPlotSubscriber.js +51 -0
  22. package/dist-tsc/Treemap.d.ts.map +1 -1
  23. package/dist-tsc/Treemap.js +11 -15
  24. package/dist-tsc/TreemapSubscriber.d.ts.map +1 -1
  25. package/dist-tsc/TreemapSubscriber.js +6 -2
  26. package/dist-tsc/VolcanoPlot.d.ts +2 -0
  27. package/dist-tsc/VolcanoPlot.d.ts.map +1 -0
  28. package/dist-tsc/VolcanoPlot.js +231 -0
  29. package/dist-tsc/VolcanoPlotOptions.d.ts +2 -0
  30. package/dist-tsc/VolcanoPlotOptions.d.ts.map +1 -0
  31. package/dist-tsc/VolcanoPlotOptions.js +23 -0
  32. package/dist-tsc/VolcanoPlotSubscriber.d.ts +2 -0
  33. package/dist-tsc/VolcanoPlotSubscriber.d.ts.map +1 -0
  34. package/dist-tsc/VolcanoPlotSubscriber.js +33 -0
  35. package/dist-tsc/index.d.ts +3 -0
  36. package/dist-tsc/index.js +3 -0
  37. package/dist-tsc/utils.d.ts +9 -0
  38. package/dist-tsc/utils.d.ts.map +1 -0
  39. package/dist-tsc/utils.js +40 -0
  40. package/package.json +7 -7
  41. package/src/CellSetCompositionBarPlot.js +205 -0
  42. package/src/CellSetCompositionBarPlotSubscriber.js +151 -0
  43. package/src/FeatureSetEnrichmentBarPlot.js +203 -0
  44. package/src/FeatureSetEnrichmentBarPlotSubscriber.js +166 -0
  45. package/src/Treemap.js +12 -19
  46. package/src/TreemapSubscriber.js +7 -1
  47. package/src/VolcanoPlot.js +316 -0
  48. package/src/VolcanoPlotOptions.js +136 -0
  49. package/src/VolcanoPlotSubscriber.js +162 -0
  50. package/src/index.js +3 -0
  51. package/src/utils.js +47 -0
@@ -0,0 +1,231 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /* eslint-disable indent */
3
+ /* eslint-disable camelcase */
4
+ import React, { useMemo, useEffect, useRef } from 'react';
5
+ import { scaleLinear } from 'd3-scale';
6
+ import { axisBottom, axisLeft } from 'd3-axis';
7
+ import { extent as d3_extent } from 'd3-array';
8
+ import { select } from 'd3-selection';
9
+ import { isEqual } from 'lodash-es';
10
+ import { capitalize, getDefaultForegroundColor } from '@vitessce/utils';
11
+ import { colorArrayToString } from '@vitessce/sets-utils';
12
+ import { getColorScale } from './utils.js';
13
+ export default function VolcanoPlot(props) {
14
+ const { theme, width, height, obsType, featureType, obsSetsColumnNameMapping, sampleSetsColumnNameMapping, sampleSetSelection, obsSetSelection, obsSetColor, sampleSetColor, data, marginTop = 5, marginRight = 5, marginLeft = 50, marginBottom = 50, onFeatureClick, featurePointSignificanceThreshold, featurePointFoldChangeThreshold, featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold, } = props;
15
+ const svgRef = useRef();
16
+ const computedData = useMemo(() => data.map(d => ({
17
+ ...d,
18
+ df: {
19
+ ...d.df,
20
+ minusLog10p: d.df.featureSignificance.map(v => -Math.log10(v)),
21
+ logFoldChange: d.df.featureFoldChange.map(v => Math.log2(v)),
22
+ },
23
+ })), [data]);
24
+ const [xExtent, yExtent] = useMemo(() => {
25
+ if (!computedData) {
26
+ return [null, null];
27
+ }
28
+ let xExtentResult = d3_extent(computedData.flatMap(d => d3_extent(d.df.logFoldChange)));
29
+ const xAbsMax = Math.max(Math.abs(xExtentResult[0]), Math.abs(xExtentResult[1]));
30
+ xExtentResult = [-xAbsMax, xAbsMax];
31
+ const yExtentResult = d3_extent(computedData.flatMap(d => d3_extent(d.df.minusLog10p.filter(v => Number.isFinite(v)))));
32
+ return [xExtentResult, yExtentResult];
33
+ }, [computedData]);
34
+ const [obsSetColorScale, sampleSetColorScale] = useMemo(() => [
35
+ getColorScale(obsSetSelection, obsSetColor, theme),
36
+ getColorScale(sampleSetSelection, sampleSetColor, theme),
37
+ ], [obsSetSelection, sampleSetSelection, sampleSetColor, obsSetColor, theme]);
38
+ useEffect(() => {
39
+ const domElement = svgRef.current;
40
+ const svg = select(domElement);
41
+ svg.selectAll('g').remove();
42
+ svg
43
+ .attr('width', width)
44
+ .attr('height', height)
45
+ .attr('viewBox', [0, 0, width, height])
46
+ .attr('style', 'font: 10px sans-serif');
47
+ if (!computedData || !xExtent || !yExtent) {
48
+ return;
49
+ }
50
+ // Render scatterplot
51
+ const innerWidth = width - marginLeft;
52
+ const innerHeight = height - marginBottom;
53
+ const xScale = scaleLinear()
54
+ .range([marginLeft, width - marginRight])
55
+ .domain(xExtent);
56
+ // For the y domain, use the yMin prop
57
+ // to support a use case such as 'Aspect Ratio',
58
+ // where the domain minimum should be 1 rather than 0.
59
+ const yScale = scaleLinear()
60
+ .domain(yExtent)
61
+ .range([innerHeight, marginTop])
62
+ .clamp(true);
63
+ // Add the axes.
64
+ svg.append('g')
65
+ .attr('transform', `translate(0,${height - marginBottom})`)
66
+ .call(axisBottom(xScale));
67
+ svg.append('g')
68
+ .attr('transform', `translate(${marginLeft},0)`)
69
+ .call(axisLeft(yScale));
70
+ // Axis titles
71
+ const titleG = svg.append('g');
72
+ const fgColor = colorArrayToString(getDefaultForegroundColor(theme));
73
+ // Y-axis title
74
+ titleG
75
+ .append('text')
76
+ .attr('text-anchor', 'middle')
77
+ .attr('x', -innerHeight / 2)
78
+ .attr('y', 15)
79
+ .attr('transform', 'rotate(-90)')
80
+ .text('-log10 p-value')
81
+ .style('font-size', '12px')
82
+ .style('fill', fgColor);
83
+ // X-axis title
84
+ titleG
85
+ .append('text')
86
+ .attr('text-anchor', 'middle')
87
+ .attr('x', marginLeft + innerWidth / 2)
88
+ .attr('y', height - 10)
89
+ .text('log2 fold-change')
90
+ .style('font-size', '12px')
91
+ .style('fill', fgColor);
92
+ // Get a mapping from column name to group name.
93
+ const obsSetsColumnNameMappingReversed = Object.fromEntries(Object
94
+ .entries(obsSetsColumnNameMapping)
95
+ .map(([key, value]) => ([value, key])));
96
+ const sampleSetsColumnNameMappingReversed = Object.fromEntries(Object
97
+ .entries(sampleSetsColumnNameMapping)
98
+ .map(([key, value]) => ([value, key])));
99
+ // Horizontal and vertical rules to indicate currently-selected thresholds
100
+ // Vertical lines
101
+ const ruleColor = 'silver';
102
+ const ruleDash = '2,2';
103
+ titleG.append('line')
104
+ .attr('x1', xScale(featurePointFoldChangeThreshold))
105
+ .attr('x2', xScale(featurePointFoldChangeThreshold))
106
+ .attr('y1', yScale.range()[0])
107
+ .attr('y2', yScale.range()[1])
108
+ .style('stroke', ruleColor)
109
+ .style('stroke-dasharray', ruleDash);
110
+ titleG.append('line')
111
+ .attr('x1', xScale(-featurePointFoldChangeThreshold))
112
+ .attr('x2', xScale(-featurePointFoldChangeThreshold))
113
+ .attr('y1', yScale.range()[0])
114
+ .attr('y2', yScale.range()[1])
115
+ .style('stroke', ruleColor)
116
+ .style('stroke-dasharray', ruleDash);
117
+ // Horizontal lines
118
+ titleG.append('line')
119
+ .attr('x1', xScale.range()[0])
120
+ .attr('x2', xScale.range()[1])
121
+ .attr('y1', yScale(-Math.log10(featurePointSignificanceThreshold)))
122
+ .attr('y2', yScale(-Math.log10(featurePointSignificanceThreshold)))
123
+ .style('stroke', ruleColor)
124
+ .style('stroke-dasharray', ruleDash);
125
+ // Upregulated/downregulated and sampleSet directional indicators.
126
+ const lhsText = sampleSetSelection && sampleSetSelection.length === 2
127
+ ? sampleSetSelection[0].at(-1)
128
+ : '__rest__';
129
+ // eslint-disable-next-line no-nested-ternary
130
+ const rhsText = sampleSetSelection && sampleSetSelection.length === 2
131
+ ? sampleSetSelection[1].at(-1)
132
+ : (obsSetSelection && obsSetSelection.length === 1
133
+ ? obsSetSelection?.[0]?.at(-1)
134
+ : `${capitalize(obsType)} Set`);
135
+ titleG
136
+ .append('text')
137
+ .attr('text-anchor', 'start')
138
+ .attr('x', marginLeft)
139
+ .attr('y', height - 10)
140
+ .text(`\u2190 ${lhsText}`)
141
+ .style('font-size', '12px')
142
+ .style('fill', fgColor);
143
+ titleG
144
+ .append('text')
145
+ .attr('text-anchor', 'end')
146
+ .attr('x', marginLeft + innerWidth)
147
+ .attr('y', height - 10)
148
+ .text(`${rhsText} \u2192`)
149
+ .style('font-size', '12px')
150
+ .style('fill', fgColor);
151
+ const g = svg.append('g');
152
+ // Append a circle for each data point.
153
+ computedData.forEach((comparisonObject) => {
154
+ const obsSetG = g.append('g');
155
+ const { df, metadata } = comparisonObject;
156
+ const coordinationValues = metadata.coordination_values;
157
+ const rawObsSetPath = coordinationValues.obsSetFilter
158
+ ? coordinationValues.obsSetFilter[0]
159
+ : coordinationValues.obsSetSelection[0];
160
+ const obsSetPath = [...rawObsSetPath];
161
+ obsSetPath[0] = obsSetsColumnNameMappingReversed[rawObsSetPath[0]];
162
+ // Swap the foldchange direction if backwards with
163
+ // respect to the current sampleSetSelection pair.
164
+ // TODO: move this swapping into the computedData useMemo?
165
+ let shouldSwapFoldChangeDirection = false;
166
+ if (coordinationValues.sampleSetFilter
167
+ && coordinationValues.sampleSetFilter.length === 2) {
168
+ const rawSampleSetPathA = coordinationValues.sampleSetFilter[0];
169
+ const sampleSetPathA = [...rawSampleSetPathA];
170
+ sampleSetPathA[0] = sampleSetsColumnNameMappingReversed[rawSampleSetPathA[0]];
171
+ const rawSampleSetPathB = coordinationValues.sampleSetFilter[1];
172
+ const sampleSetPathB = [...rawSampleSetPathB];
173
+ sampleSetPathB[0] = sampleSetsColumnNameMappingReversed[rawSampleSetPathB[0]];
174
+ if (isEqual(sampleSetPathA, sampleSetSelection[1])
175
+ && isEqual(sampleSetPathB, sampleSetSelection[0])) {
176
+ shouldSwapFoldChangeDirection = true;
177
+ }
178
+ }
179
+ const filteredDf = df.featureId.map((featureId, i) => ({
180
+ featureId,
181
+ logFoldChange: df.logFoldChange[i] * (shouldSwapFoldChangeDirection ? -1 : 1),
182
+ featureSignificance: df.featureSignificance[i],
183
+ minusLog10p: df.minusLog10p[i],
184
+ })).filter(d => ((Math.abs(d.logFoldChange) >= (featurePointFoldChangeThreshold ?? 1.0))
185
+ && (d.featureSignificance <= (featurePointSignificanceThreshold ?? 0.05))));
186
+ const color = obsSetColorScale(obsSetPath);
187
+ obsSetG.append('g')
188
+ .selectAll('circle')
189
+ .data(filteredDf)
190
+ .join('circle')
191
+ .attr('cx', d => xScale(d.logFoldChange))
192
+ .attr('cy', d => yScale(d.minusLog10p))
193
+ .attr('r', 3)
194
+ .attr('opacity', 0.5)
195
+ .attr('fill', color)
196
+ .on('click', (event, d) => {
197
+ onFeatureClick(d.featureId);
198
+ });
199
+ const textElements = obsSetG.append('g')
200
+ .selectAll('text')
201
+ .data(filteredDf)
202
+ .join('text')
203
+ .text(d => d.featureId)
204
+ .attr('text-anchor', d => (d.logFoldChange < 0 ? 'end' : 'start'))
205
+ .attr('x', d => xScale(d.logFoldChange))
206
+ .attr('y', d => yScale(d.minusLog10p))
207
+ .style('display', d => ((Math.abs(d.logFoldChange) < (featureLabelFoldChangeThreshold ?? 5.0)
208
+ || (d.featureSignificance >= (featureLabelSignificanceThreshold ?? 0.01))) ? 'none' : undefined))
209
+ .attr('fill', color)
210
+ .on('click', (event, d) => {
211
+ onFeatureClick(d.featureId);
212
+ });
213
+ textElements.append('title')
214
+ .text(d => `${featureType}: ${d.featureId}\nin ${obsSetPath?.at(-1)}\nlog2 fold-change: ${d.logFoldChange}\np-value: ${d.featureSignificance}`);
215
+ });
216
+ }, [width, height, theme, sampleSetColor, sampleSetSelection,
217
+ obsSetSelection, obsSetColor, featureType, computedData,
218
+ xExtent, yExtent, obsType,
219
+ marginLeft, marginBottom, marginTop, marginRight,
220
+ obsSetColorScale, sampleSetColorScale, onFeatureClick,
221
+ featurePointSignificanceThreshold, featurePointFoldChangeThreshold,
222
+ featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold,
223
+ ]);
224
+ return (_jsx("svg", { ref: svgRef, style: {
225
+ top: 0,
226
+ left: 0,
227
+ width: `${width}px`,
228
+ height: `${height}px`,
229
+ position: 'relative',
230
+ } }));
231
+ }
@@ -0,0 +1,2 @@
1
+ export default function VolcanoPlotOptions(props: any): JSX.Element;
2
+ //# sourceMappingURL=VolcanoPlotOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VolcanoPlotOptions.d.ts","sourceRoot":"","sources":["../src/VolcanoPlotOptions.js"],"names":[],"mappings":"AAOA,oEAgIC"}
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { useId } from 'react-aria';
4
+ import { TableCell, TableRow, Slider } from '@material-ui/core';
5
+ import { usePlotOptionsStyles, OptionsContainer, } from '@vitessce/vit-s';
6
+ export default function VolcanoPlotOptions(props) {
7
+ const { children, featurePointSignificanceThreshold, featurePointFoldChangeThreshold, featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold, setFeaturePointSignificanceThreshold, setFeaturePointFoldChangeThreshold, setFeatureLabelSignificanceThreshold, setFeatureLabelFoldChangeThreshold, } = props;
8
+ const volcanoOptionsId = useId();
9
+ const classes = usePlotOptionsStyles();
10
+ function handlePointSignificanceChange(event, value) {
11
+ setFeaturePointSignificanceThreshold(10 ** -value);
12
+ }
13
+ function handlePointFoldChangeChange(event, value) {
14
+ setFeaturePointFoldChangeThreshold(value);
15
+ }
16
+ function handleLabelSignificanceChange(event, value) {
17
+ setFeatureLabelSignificanceThreshold(10 ** -value);
18
+ }
19
+ function handleLabelFoldChangeChange(event, value) {
20
+ setFeatureLabelFoldChangeThreshold(value);
21
+ }
22
+ return (_jsxs(OptionsContainer, { children: [children, _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `volcano-label-significance-${volcanoOptionsId}`, children: "Label Significance Threshold" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Slider, { classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: -Math.log10(featureLabelSignificanceThreshold), onChange: handleLabelSignificanceChange, "aria-label": "Volcano plot label significance threshold slider", id: `volcano-label-significance-${volcanoOptionsId}`, valueLabelDisplay: "auto", step: 1, min: 0, max: 100 }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `volcano-label-fc-${volcanoOptionsId}`, children: "Label Fold-Change Threshold" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Slider, { classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: featureLabelFoldChangeThreshold, onChange: handleLabelFoldChangeChange, "aria-label": "Volcano plot label fold-change threshold slider", id: `volcano-label-fc-${volcanoOptionsId}`, valueLabelDisplay: "auto", step: 0.5, min: 0, max: 50 }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `volcano-point-significance-${volcanoOptionsId}`, children: "Point Significance Threshold" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Slider, { classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: -Math.log10(featurePointSignificanceThreshold), onChange: handlePointSignificanceChange, "aria-label": "Volcano plot point significance threshold slider", id: `volcano-point-significance-${volcanoOptionsId}`, valueLabelDisplay: "auto", step: 1, min: 0, max: 100 }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `volcano-point-fc-${volcanoOptionsId}`, children: "Point Fold-Change Threshold" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(Slider, { classes: { root: classes.slider, valueLabel: classes.sliderValueLabel }, value: featurePointFoldChangeThreshold, onChange: handlePointFoldChangeChange, "aria-label": "Volcano plot point fold-change threshold slider", id: `volcano-point-fc-${volcanoOptionsId}`, valueLabelDisplay: "auto", step: 0.5, min: 0, max: 50 }) })] })] }));
23
+ }
@@ -0,0 +1,2 @@
1
+ export function VolcanoPlotSubscriber(props: any): JSX.Element;
2
+ //# sourceMappingURL=VolcanoPlotSubscriber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VolcanoPlotSubscriber.d.ts","sourceRoot":"","sources":["../src/VolcanoPlotSubscriber.js"],"names":[],"mappings":"AAuBA,+DA0IC"}
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* eslint-disable no-unused-vars */
3
+ import React, { useMemo, useCallback } from 'react';
4
+ import { TitleInfo, useCoordination, useLoaders, useReady, useGridItemSize, useFeatureStatsData, useMatchingLoader, useColumnNameMapping, } from '@vitessce/vit-s';
5
+ import { ViewType, COMPONENT_COORDINATION_TYPES, ViewHelpMapping, DataType, } from '@vitessce/constants-internal';
6
+ import VolcanoPlot from './VolcanoPlot.js';
7
+ import { useStyles } from './styles.js';
8
+ import VolcanoPlotOptions from './VolcanoPlotOptions.js';
9
+ import { useRawSetPaths } from './utils.js';
10
+ export function VolcanoPlotSubscriber(props) {
11
+ const { coordinationScopes, removeGridComponent, theme, helpText = ViewHelpMapping.VOLCANO_PLOT, } = props;
12
+ const classes = useStyles();
13
+ const loaders = useLoaders();
14
+ // Get "props" from the coordination space.
15
+ const [{ dataset, obsType, sampleType, featureType, featureValueType, obsFilter: cellFilter, obsHighlight: cellHighlight, obsSetSelection, obsSetColor, obsColorEncoding: cellColorEncoding, additionalObsSets: additionalCellSets, featurePointSignificanceThreshold, featurePointFoldChangeThreshold, featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold, featureValueTransform, featureValueTransformCoefficient, gatingFeatureSelectionX, gatingFeatureSelectionY, featureSelection, sampleSetSelection, sampleSetColor, }, { setObsFilter: setCellFilter, setObsSetSelection, setObsHighlight: setCellHighlight, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setAdditionalObsSets: setAdditionalCellSets, setFeaturePointSignificanceThreshold, setFeaturePointFoldChangeThreshold, setFeatureLabelSignificanceThreshold, setFeatureLabelFoldChangeThreshold, setFeatureValueTransform, setFeatureValueTransformCoefficient, setGatingFeatureSelectionX, setGatingFeatureSelectionY, setFeatureSelection, setSampleSetSelection, setSampleSetColor, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.VOLCANO_PLOT], coordinationScopes);
16
+ const [width, height, containerRef] = useGridItemSize();
17
+ const obsSetsLoader = useMatchingLoader(loaders, dataset, DataType.OBS_SETS, { obsType });
18
+ const sampleSetsLoader = useMatchingLoader(loaders, dataset, DataType.SAMPLE_SETS, { sampleType });
19
+ const obsSetsColumnNameMapping = useColumnNameMapping(obsSetsLoader);
20
+ const sampleSetsColumnNameMapping = useColumnNameMapping(sampleSetsLoader);
21
+ const rawSampleSetSelection = useRawSetPaths(sampleSetsColumnNameMapping, sampleSetSelection);
22
+ const rawObsSetSelection = useRawSetPaths(obsSetsColumnNameMapping, obsSetSelection);
23
+ const [{ featureStats }, featureStatsStatus] = useFeatureStatsData(loaders, dataset, false, { obsType, featureType, sampleType },
24
+ // These volcanoOptions are passed to FeatureStatsAnndataLoader.loadMulti():
25
+ { sampleSetSelection: rawSampleSetSelection, obsSetSelection: rawObsSetSelection });
26
+ const isReady = useReady([
27
+ featureStatsStatus,
28
+ ]);
29
+ const onFeatureClick = useCallback((featureId) => {
30
+ setFeatureSelection([featureId]);
31
+ }, [setFeatureSelection]);
32
+ return (_jsx(TitleInfo, { title: "Volcano Plot", removeGridComponent: removeGridComponent, theme: theme, isReady: isReady, helpText: helpText, options: (_jsx(VolcanoPlotOptions, { obsType: obsType, featureType: featureType, featurePointSignificanceThreshold: featurePointSignificanceThreshold, featurePointFoldChangeThreshold: featurePointFoldChangeThreshold, featureLabelSignificanceThreshold: featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold: featureLabelFoldChangeThreshold, setFeaturePointSignificanceThreshold: setFeaturePointSignificanceThreshold, setFeaturePointFoldChangeThreshold: setFeaturePointFoldChangeThreshold, setFeatureLabelSignificanceThreshold: setFeatureLabelSignificanceThreshold, setFeatureLabelFoldChangeThreshold: setFeatureLabelFoldChangeThreshold })), children: _jsx("div", { ref: containerRef, className: classes.vegaContainer, children: featureStats ? (_jsx(VolcanoPlot, { theme: theme, width: width, height: height, obsType: obsType, featureType: featureType, obsSetsColumnNameMapping: obsSetsColumnNameMapping, sampleSetsColumnNameMapping: sampleSetsColumnNameMapping, sampleSetSelection: sampleSetSelection, obsSetSelection: obsSetSelection, obsSetColor: obsSetColor, sampleSetColor: sampleSetColor, data: featureStats, onFeatureClick: onFeatureClick, featurePointSignificanceThreshold: featurePointSignificanceThreshold, featurePointFoldChangeThreshold: featurePointFoldChangeThreshold, featureLabelSignificanceThreshold: featureLabelSignificanceThreshold, featureLabelFoldChangeThreshold: featureLabelFoldChangeThreshold })) : (_jsxs("span", { children: ["Select at least one ", obsType, " set."] })) }) }));
33
+ }
@@ -4,6 +4,9 @@ export { ExpressionHistogramSubscriber } from "./ExpressionHistogramSubscriber.j
4
4
  export { DotPlotSubscriber } from "./DotPlotSubscriber.js";
5
5
  export { FeatureBarPlotSubscriber } from "./FeatureBarPlotSubscriber.js";
6
6
  export { TreemapSubscriber } from "./TreemapSubscriber.js";
7
+ export { VolcanoPlotSubscriber } from "./VolcanoPlotSubscriber.js";
8
+ export { CellSetCompositionBarPlotSubscriber } from "./CellSetCompositionBarPlotSubscriber.js";
9
+ export { FeatureSetEnrichmentBarPlotSubscriber } from "./FeatureSetEnrichmentBarPlotSubscriber.js";
7
10
  export { default as CellSetSizesPlot } from "./CellSetSizesPlot.js";
8
11
  export { default as CellSetExpressionPlot } from "./CellSetExpressionPlot.js";
9
12
  export { default as ExpressionHistogram } from "./ExpressionHistogram.js";
package/dist-tsc/index.js CHANGED
@@ -4,6 +4,9 @@ export { ExpressionHistogramSubscriber } from './ExpressionHistogramSubscriber.j
4
4
  export { DotPlotSubscriber } from './DotPlotSubscriber.js';
5
5
  export { FeatureBarPlotSubscriber } from './FeatureBarPlotSubscriber.js';
6
6
  export { TreemapSubscriber } from './TreemapSubscriber.js';
7
+ export { VolcanoPlotSubscriber } from './VolcanoPlotSubscriber.js';
8
+ export { CellSetCompositionBarPlotSubscriber } from './CellSetCompositionBarPlotSubscriber.js';
9
+ export { FeatureSetEnrichmentBarPlotSubscriber } from './FeatureSetEnrichmentBarPlotSubscriber.js';
7
10
  export { default as CellSetSizesPlot } from './CellSetSizesPlot.js';
8
11
  export { default as CellSetExpressionPlot } from './CellSetExpressionPlot.js';
9
12
  export { default as ExpressionHistogram } from './ExpressionHistogram.js';
@@ -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.7",
3
+ "version": "3.5.9",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -29,12 +29,12 @@
29
29
  "react-aria": "^3.28.0",
30
30
  "internmap": "^2.0.3",
31
31
  "uuid": "^9.0.0",
32
- "@vitessce/constants-internal": "3.5.7",
33
- "@vitessce/sets-utils": "3.5.7",
34
- "@vitessce/utils": "3.5.7",
35
- "@vitessce/vega": "3.5.7",
36
- "@vitessce/vit-s": "3.5.7",
37
- "@vitessce/gl": "3.5.7"
32
+ "@vitessce/constants-internal": "3.5.9",
33
+ "@vitessce/sets-utils": "3.5.9",
34
+ "@vitessce/utils": "3.5.9",
35
+ "@vitessce/vega": "3.5.9",
36
+ "@vitessce/vit-s": "3.5.9",
37
+ "@vitessce/gl": "3.5.9"
38
38
  },
39
39
  "devDependencies": {
40
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
+ }