@vitessce/statistical-plots 3.3.11 → 3.4.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.
- package/dist/index.js +511 -168
- package/dist-tsc/FeatureBarPlot.d.ts +2 -0
- package/dist-tsc/FeatureBarPlot.d.ts.map +1 -0
- package/dist-tsc/FeatureBarPlot.js +173 -0
- package/dist-tsc/FeatureBarPlotSubscriber.d.ts +2 -0
- package/dist-tsc/FeatureBarPlotSubscriber.d.ts.map +1 -0
- package/dist-tsc/FeatureBarPlotSubscriber.js +62 -0
- package/dist-tsc/index.d.ts +1 -0
- package/dist-tsc/index.js +1 -0
- package/package.json +7 -6
- package/src/FeatureBarPlot.js +225 -0
- package/src/FeatureBarPlotSubscriber.js +157 -0
- package/src/index.js +1 -0
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"FeatureBarPlot.d.ts","sourceRoot":"","sources":["../src/FeatureBarPlot.js"],"names":[],"mappings":"AAwBA,gEAwMC"}
|
@@ -0,0 +1,173 @@
|
|
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 { scale as vega_scale } from 'vega-scale';
|
7
|
+
import { axisBottom, axisLeft } from 'd3-axis';
|
8
|
+
import { ascending } from 'd3-array';
|
9
|
+
import { select } from 'd3-selection';
|
10
|
+
import { capitalize } from '@vitessce/utils';
|
11
|
+
const scaleBand = vega_scale('band');
|
12
|
+
const OBS_KEY = 'obsId';
|
13
|
+
const FEATURE_KEY = 'value';
|
14
|
+
function componentToHex(c) {
|
15
|
+
const hex = c.toString(16);
|
16
|
+
return hex.length === 1 ? `0${hex}` : hex;
|
17
|
+
}
|
18
|
+
function rgbToHex(color) {
|
19
|
+
return `#${componentToHex(color[0])}${componentToHex(color[1])}${componentToHex(color[2])}`;
|
20
|
+
}
|
21
|
+
export default function FeatureBarPlot(props) {
|
22
|
+
const { yMin, yMax, yUnits, jitter, colors, data, theme, width, height, marginTop = 5, marginRight = 5, marginLeft = 80, marginBottom, obsType, cellHighlight, cellSetSelection, additionalCellSets, cellSetColor, featureType, featureValueType, featureName, onBarSelect, onBarHighlight, } = props;
|
23
|
+
// TODO: use a more descriptive name than setsSave.
|
24
|
+
const setsSave = useMemo(() => {
|
25
|
+
const result = new Map();
|
26
|
+
cellSetSelection.forEach((obsSetPath) => {
|
27
|
+
// TODO: this does not use the full set path for comparison.
|
28
|
+
const selectedElement = obsSetPath[1];
|
29
|
+
// TODO: this is only considering the first set grouping in the tree.
|
30
|
+
// TODO: use sets-utils to traverse sets tree.
|
31
|
+
additionalCellSets.tree[0].children.forEach((child) => {
|
32
|
+
if (child.name === selectedElement) {
|
33
|
+
child.set.forEach(([obsId]) => {
|
34
|
+
const info = { name: '', id: '', color: [] };
|
35
|
+
info.name = selectedElement;
|
36
|
+
info.id = obsId;
|
37
|
+
cellSetColor.forEach((color) => {
|
38
|
+
if (color.path[1] === selectedElement) {
|
39
|
+
info.color = color.color;
|
40
|
+
}
|
41
|
+
});
|
42
|
+
result.set(info.id, info);
|
43
|
+
});
|
44
|
+
}
|
45
|
+
});
|
46
|
+
});
|
47
|
+
}, [cellSetSelection, additionalCellSets, cellSetColor]);
|
48
|
+
const svgRef = useRef();
|
49
|
+
// Get the max characters in an axis label for autsizing the bottom margin.
|
50
|
+
const maxCharactersForLabel = useMemo(() => data.reduce((acc, val) => {
|
51
|
+
// eslint-disable-next-line no-param-reassign
|
52
|
+
acc = acc === undefined || val[OBS_KEY].length > acc ? val[OBS_KEY].length : acc;
|
53
|
+
return acc;
|
54
|
+
}, 0), [data]);
|
55
|
+
useEffect(() => {
|
56
|
+
const domElement = svgRef.current;
|
57
|
+
const unitSuffix = yUnits ? ` (${yUnits})` : '';
|
58
|
+
const yTitle = `${capitalize(featureName)}${unitSuffix}`;
|
59
|
+
const xTitle = `${capitalize(obsType)}`;
|
60
|
+
// Use a square-root term because the angle of the labels is 45 degrees (see below)
|
61
|
+
// so the perpendicular distance to the bottom of the labels is proportional to the
|
62
|
+
// square root of the length of the labels along the imaginary hypotenuse.
|
63
|
+
// 30 is an estimate of the pixel size of a given character and seems to work well.
|
64
|
+
const autoMarginBottom = marginBottom
|
65
|
+
|| 30 + Math.sqrt(maxCharactersForLabel / 2) * 30;
|
66
|
+
const foregroundColor = (theme === 'dark' ? 'lightgray' : 'black');
|
67
|
+
data.sort((a, b) => ascending(a[FEATURE_KEY], b[FEATURE_KEY]));
|
68
|
+
const svg = select(domElement);
|
69
|
+
svg.selectAll('g').remove();
|
70
|
+
svg
|
71
|
+
.attr('width', width)
|
72
|
+
.attr('height', height);
|
73
|
+
const g = svg
|
74
|
+
.append('g')
|
75
|
+
.attr('width', width)
|
76
|
+
.attr('height', height);
|
77
|
+
const innerWidth = width - marginLeft;
|
78
|
+
const innerHeight = height - autoMarginBottom;
|
79
|
+
const xScale = scaleBand()
|
80
|
+
.range([marginLeft, width - marginRight])
|
81
|
+
.domain(data.map(d => d[OBS_KEY]))
|
82
|
+
.padding(0.1);
|
83
|
+
// For the y domain, use the yMin prop
|
84
|
+
// to support a use case such as 'Aspect Ratio',
|
85
|
+
// where the domain minimum should be 1 rather than 0.
|
86
|
+
const yScale = scaleLinear()
|
87
|
+
.domain([yMin, yMax])
|
88
|
+
.range([innerHeight, marginTop]);
|
89
|
+
// Bar areas
|
90
|
+
g
|
91
|
+
.selectAll('bar')
|
92
|
+
.data(data)
|
93
|
+
.enter()
|
94
|
+
.append('rect')
|
95
|
+
.attr('x', d => xScale(d[OBS_KEY]))
|
96
|
+
.attr('y', d => yScale(d[FEATURE_KEY]))
|
97
|
+
.attr('width', xScale.bandwidth())
|
98
|
+
.attr('height', d => innerHeight - yScale(d[FEATURE_KEY]))
|
99
|
+
.style('fill', (d) => {
|
100
|
+
if (d[OBS_KEY] === cellHighlight)
|
101
|
+
return 'orange';
|
102
|
+
if (setsSave.has(d[OBS_KEY])) {
|
103
|
+
const { color } = setsSave.get(d[OBS_KEY]);
|
104
|
+
return rgbToHex(color);
|
105
|
+
}
|
106
|
+
return foregroundColor;
|
107
|
+
})
|
108
|
+
.style('cursor', 'pointer')
|
109
|
+
// eslint-disable-next-line no-unused-vars
|
110
|
+
.on('click', (event, d) => {
|
111
|
+
onBarSelect(d[OBS_KEY]);
|
112
|
+
})
|
113
|
+
// eslint-disable-next-line no-unused-vars
|
114
|
+
.on('mouseover', (event, d) => {
|
115
|
+
onBarHighlight(d[OBS_KEY]);
|
116
|
+
})
|
117
|
+
.on('mouseout', () => {
|
118
|
+
onBarHighlight(null);
|
119
|
+
});
|
120
|
+
const axis = axisLeft(yScale);
|
121
|
+
axis.tickFormat(d => `${Math.round(d * 10000000)} µm`);
|
122
|
+
// Y-axis ticks
|
123
|
+
g
|
124
|
+
.append('g')
|
125
|
+
.attr('transform', `translate(${marginLeft},0)`)
|
126
|
+
.call(axis)
|
127
|
+
.selectAll('text')
|
128
|
+
.style('font-size', '11px');
|
129
|
+
// X-axis ticks
|
130
|
+
g
|
131
|
+
.append('g')
|
132
|
+
.attr('transform', `translate(0,${innerHeight})`)
|
133
|
+
.style('font-size', '14px')
|
134
|
+
.call(axisBottom(xScale))
|
135
|
+
.selectAll('text')
|
136
|
+
.style('font-size', '11px')
|
137
|
+
.attr('dx', '-6px')
|
138
|
+
.attr('dy', '6px')
|
139
|
+
.attr('transform', 'rotate(-45)')
|
140
|
+
.style('text-anchor', 'end');
|
141
|
+
// Y-axis title
|
142
|
+
g
|
143
|
+
.append('text')
|
144
|
+
.attr('text-anchor', 'middle')
|
145
|
+
.attr('x', -innerHeight / 2)
|
146
|
+
.attr('y', 15)
|
147
|
+
.attr('transform', 'rotate(-90)')
|
148
|
+
.text(yTitle)
|
149
|
+
.style('font-size', '12px')
|
150
|
+
.style('fill', foregroundColor);
|
151
|
+
// X-axis title
|
152
|
+
g
|
153
|
+
.append('text')
|
154
|
+
.attr('text-anchor', 'middle')
|
155
|
+
.attr('x', marginLeft + innerWidth / 2)
|
156
|
+
.attr('y', height - 10)
|
157
|
+
.text(xTitle)
|
158
|
+
.style('font-size', '12px')
|
159
|
+
.style('fill', foregroundColor);
|
160
|
+
}, [width, height, data, marginLeft, marginBottom, colors,
|
161
|
+
jitter, theme, yMin, marginTop, marginRight, featureType,
|
162
|
+
featureValueType, yUnits, obsType,
|
163
|
+
maxCharactersForLabel, yMax, featureName, onBarSelect, onBarHighlight,
|
164
|
+
cellHighlight, setsSave,
|
165
|
+
]);
|
166
|
+
return (_jsx("svg", { ref: svgRef, style: {
|
167
|
+
top: 0,
|
168
|
+
left: 0,
|
169
|
+
width: `${width}px`,
|
170
|
+
height: `${height}px`,
|
171
|
+
position: 'relative',
|
172
|
+
} }));
|
173
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"FeatureBarPlotSubscriber.d.ts","sourceRoot":"","sources":["../src/FeatureBarPlotSubscriber.js"],"names":[],"mappings":"AAkBA,kEA0IC"}
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
2
|
+
import React, { useMemo, useCallback } from 'react';
|
3
|
+
import { TitleInfo, useCoordination, useLoaders, useUrls, useReady, useGridItemSize, useFeatureSelection, useObsFeatureMatrixIndices, useFeatureLabelsData, } from '@vitessce/vit-s';
|
4
|
+
import { ViewType, COMPONENT_COORDINATION_TYPES } from '@vitessce/constants-internal';
|
5
|
+
import { setObsSelection } from '@vitessce/sets-utils';
|
6
|
+
import FeatureBarPlot from './FeatureBarPlot.js';
|
7
|
+
import { useStyles } from './styles.js';
|
8
|
+
export function FeatureBarPlotSubscriber(props) {
|
9
|
+
const { coordinationScopes, removeGridComponent, theme, yMin = 0, yUnits = null, } = props;
|
10
|
+
const classes = useStyles();
|
11
|
+
const loaders = useLoaders();
|
12
|
+
// Get "props" from the coordination space.
|
13
|
+
const [{ dataset, obsType, featureType, featureValueType, featureSelection: geneSelection, featureValueTransform, featureValueTransformCoefficient, obsHighlight: cellHighlight, additionalObsSets: additionalCellSets, obsSetSelection: cellSetSelection, obsSetColor: cellSetColor, }, { setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setAdditionalObsSets: setAdditionalCellSets, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.FEATURE_BAR_PLOT], coordinationScopes);
|
14
|
+
// console.log("BarPlot: " + cellHighlight)
|
15
|
+
const [width, height, containerRef] = useGridItemSize();
|
16
|
+
// Get data from loaders using the data hooks.
|
17
|
+
// eslint-disable-next-line no-unused-vars
|
18
|
+
const [expressionData, loadedFeatureSelection, featureSelectionStatus] = useFeatureSelection(loaders, dataset, false, geneSelection, { obsType, featureType, featureValueType });
|
19
|
+
// TODO: support multiple feature labels using featureLabelsType coordination values.
|
20
|
+
const [{ featureLabelsMap }, featureLabelsStatus, featureLabelsUrls] = useFeatureLabelsData(loaders, dataset, false, {}, {}, { featureType });
|
21
|
+
const [{ obsIndex }, matrixIndicesStatus, matrixIndicesUrls,] = useObsFeatureMatrixIndices(loaders, dataset, false, { obsType, featureType, featureValueType });
|
22
|
+
const isReady = useReady([
|
23
|
+
featureSelectionStatus,
|
24
|
+
matrixIndicesStatus,
|
25
|
+
featureLabelsStatus,
|
26
|
+
]);
|
27
|
+
const urls = useUrls([
|
28
|
+
featureLabelsUrls,
|
29
|
+
matrixIndicesUrls,
|
30
|
+
]);
|
31
|
+
const onBarSelect = useCallback((obsId) => {
|
32
|
+
const obsIdsToSelect = [obsId];
|
33
|
+
setObsSelection(obsIdsToSelect, additionalCellSets, cellSetColor, setCellSetSelection, setAdditionalCellSets, setCellSetColor, setCellColorEncoding);
|
34
|
+
}, [additionalCellSets, cellSetColor, setCellColorEncoding,
|
35
|
+
setAdditionalCellSets, setCellSetColor, setCellSetSelection]);
|
36
|
+
const onBarHighlight = useCallback((obsId) => {
|
37
|
+
setCellHighlight(obsId);
|
38
|
+
}, []);
|
39
|
+
const firstGeneSelected = geneSelection && geneSelection.length >= 1
|
40
|
+
? (featureLabelsMap?.get(geneSelection[0]) || geneSelection[0])
|
41
|
+
: null;
|
42
|
+
const [expressionArr, expressionMax] = useMemo(() => {
|
43
|
+
if (firstGeneSelected && expressionData && obsIndex) {
|
44
|
+
let exprMax = -Infinity;
|
45
|
+
const cellIndices = {};
|
46
|
+
for (let i = 0; i < obsIndex.length; i += 1) {
|
47
|
+
cellIndices[obsIndex[i]] = i;
|
48
|
+
}
|
49
|
+
const exprValues = obsIndex.map((obsId, cellIndex) => {
|
50
|
+
const value = expressionData[0][cellIndex];
|
51
|
+
exprMax = Math.max(value, exprMax);
|
52
|
+
return { obsId, value, feature: firstGeneSelected };
|
53
|
+
});
|
54
|
+
return [exprValues, exprMax];
|
55
|
+
}
|
56
|
+
return [null, null];
|
57
|
+
}, [expressionData, obsIndex, geneSelection, theme,
|
58
|
+
featureValueTransform, featureValueTransformCoefficient,
|
59
|
+
firstGeneSelected,
|
60
|
+
]);
|
61
|
+
return (_jsx(TitleInfo, { title: `Feature Values${(firstGeneSelected ? ` (${firstGeneSelected})` : '')}`, removeGridComponent: removeGridComponent, urls: urls, theme: theme, isReady: isReady, children: _jsx("div", { ref: containerRef, className: classes.vegaContainer, children: expressionArr ? (_jsx(FeatureBarPlot, { yMin: yMin, yMax: expressionMax, yUnits: yUnits, data: expressionArr, theme: theme, width: width, height: height, obsType: obsType, cellHighlight: cellHighlight, cellSetSelection: cellSetSelection, additionalCellSets: additionalCellSets, cellSetColor: cellSetColor, featureType: featureType, featureValueType: featureValueType, featureName: firstGeneSelected, onBarSelect: onBarSelect, onBarHighlight: onBarHighlight })) : (_jsxs("span", { children: ["Select a ", featureType, "."] })) }) }));
|
62
|
+
}
|
package/dist-tsc/index.d.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
export { CellSetExpressionPlotSubscriber } from "./CellSetExpressionPlotSubscriber.js";
|
2
2
|
export { CellSetSizesPlotSubscriber } from "./CellSetSizesPlotSubscriber.js";
|
3
3
|
export { ExpressionHistogramSubscriber } from "./ExpressionHistogramSubscriber.js";
|
4
|
+
export { FeatureBarPlotSubscriber } from "./FeatureBarPlotSubscriber.js";
|
4
5
|
export { default as CellSetSizesPlot } from "./CellSetSizesPlot.js";
|
5
6
|
export { default as CellSetExpressionPlot } from "./CellSetExpressionPlot.js";
|
6
7
|
export { default as ExpressionHistogram } from "./ExpressionHistogram.js";
|
package/dist-tsc/index.js
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
export { CellSetExpressionPlotSubscriber } from './CellSetExpressionPlotSubscriber.js';
|
2
2
|
export { CellSetSizesPlotSubscriber } from './CellSetSizesPlotSubscriber.js';
|
3
3
|
export { ExpressionHistogramSubscriber } from './ExpressionHistogramSubscriber.js';
|
4
|
+
export { FeatureBarPlotSubscriber } from './FeatureBarPlotSubscriber.js';
|
4
5
|
export { default as CellSetSizesPlot } from './CellSetSizesPlot.js';
|
5
6
|
export { default as CellSetExpressionPlot } from './CellSetExpressionPlot.js';
|
6
7
|
export { default as ExpressionHistogram } from './ExpressionHistogram.js';
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@vitessce/statistical-plots",
|
3
|
-
"version": "3.
|
3
|
+
"version": "3.4.0",
|
4
4
|
"author": "Gehlenborg Lab",
|
5
5
|
"homepage": "http://vitessce.io",
|
6
6
|
"repository": {
|
@@ -25,11 +25,12 @@
|
|
25
25
|
"vega-scale": "^6.0.0",
|
26
26
|
"lodash-es": "^4.17.21",
|
27
27
|
"react-aria": "^3.28.0",
|
28
|
-
"
|
29
|
-
"@vitessce/
|
30
|
-
"@vitessce/constants-internal": "3.
|
31
|
-
"@vitessce/utils": "3.
|
32
|
-
"@vitessce/vega": "3.
|
28
|
+
"d3-format": "^3.1.0",
|
29
|
+
"@vitessce/sets-utils": "3.4.0",
|
30
|
+
"@vitessce/constants-internal": "3.4.0",
|
31
|
+
"@vitessce/utils": "3.4.0",
|
32
|
+
"@vitessce/vega": "3.4.0",
|
33
|
+
"@vitessce/vit-s": "3.4.0"
|
33
34
|
},
|
34
35
|
"devDependencies": {
|
35
36
|
"react": "^18.0.0",
|
@@ -0,0 +1,225 @@
|
|
1
|
+
/* eslint-disable indent */
|
2
|
+
/* eslint-disable camelcase */
|
3
|
+
import React, { useMemo, useEffect, useRef } from 'react';
|
4
|
+
import { scaleLinear } from 'd3-scale';
|
5
|
+
import { scale as vega_scale } from 'vega-scale';
|
6
|
+
import { axisBottom, axisLeft } from 'd3-axis';
|
7
|
+
import { ascending } from 'd3-array';
|
8
|
+
import { select } from 'd3-selection';
|
9
|
+
import { capitalize } from '@vitessce/utils';
|
10
|
+
|
11
|
+
const scaleBand = vega_scale('band');
|
12
|
+
|
13
|
+
const OBS_KEY = 'obsId';
|
14
|
+
const FEATURE_KEY = 'value';
|
15
|
+
|
16
|
+
function componentToHex(c) {
|
17
|
+
const hex = c.toString(16);
|
18
|
+
return hex.length === 1 ? `0${hex}` : hex;
|
19
|
+
}
|
20
|
+
|
21
|
+
function rgbToHex(color) {
|
22
|
+
return `#${componentToHex(color[0])}${componentToHex(color[1])}${componentToHex(color[2])}`;
|
23
|
+
}
|
24
|
+
|
25
|
+
export default function FeatureBarPlot(props) {
|
26
|
+
const {
|
27
|
+
yMin,
|
28
|
+
yMax,
|
29
|
+
yUnits,
|
30
|
+
jitter,
|
31
|
+
colors,
|
32
|
+
data,
|
33
|
+
theme,
|
34
|
+
width,
|
35
|
+
height,
|
36
|
+
marginTop = 5,
|
37
|
+
marginRight = 5,
|
38
|
+
marginLeft = 80,
|
39
|
+
marginBottom,
|
40
|
+
obsType,
|
41
|
+
cellHighlight,
|
42
|
+
cellSetSelection,
|
43
|
+
additionalCellSets,
|
44
|
+
cellSetColor,
|
45
|
+
featureType,
|
46
|
+
featureValueType,
|
47
|
+
featureName,
|
48
|
+
onBarSelect,
|
49
|
+
onBarHighlight,
|
50
|
+
} = props;
|
51
|
+
|
52
|
+
// TODO: use a more descriptive name than setsSave.
|
53
|
+
const setsSave = useMemo(() => {
|
54
|
+
const result = new Map();
|
55
|
+
cellSetSelection.forEach((obsSetPath) => {
|
56
|
+
// TODO: this does not use the full set path for comparison.
|
57
|
+
const selectedElement = obsSetPath[1];
|
58
|
+
// TODO: this is only considering the first set grouping in the tree.
|
59
|
+
// TODO: use sets-utils to traverse sets tree.
|
60
|
+
additionalCellSets.tree[0].children.forEach((child) => {
|
61
|
+
if (child.name === selectedElement) {
|
62
|
+
child.set.forEach(([obsId]) => {
|
63
|
+
const info = { name: '', id: '', color: [] };
|
64
|
+
info.name = selectedElement;
|
65
|
+
info.id = obsId;
|
66
|
+
cellSetColor.forEach((color) => {
|
67
|
+
if (color.path[1] === selectedElement) {
|
68
|
+
info.color = color.color;
|
69
|
+
}
|
70
|
+
});
|
71
|
+
result.set(info.id, info);
|
72
|
+
});
|
73
|
+
}
|
74
|
+
});
|
75
|
+
});
|
76
|
+
}, [cellSetSelection, additionalCellSets, cellSetColor]);
|
77
|
+
|
78
|
+
const svgRef = useRef();
|
79
|
+
// Get the max characters in an axis label for autsizing the bottom margin.
|
80
|
+
const maxCharactersForLabel = useMemo(() => data.reduce((acc, val) => {
|
81
|
+
// eslint-disable-next-line no-param-reassign
|
82
|
+
acc = acc === undefined || val[OBS_KEY].length > acc ? val[OBS_KEY].length : acc;
|
83
|
+
return acc;
|
84
|
+
}, 0), [data]);
|
85
|
+
|
86
|
+
useEffect(() => {
|
87
|
+
const domElement = svgRef.current;
|
88
|
+
|
89
|
+
const unitSuffix = yUnits ? ` (${yUnits})` : '';
|
90
|
+
const yTitle = `${capitalize(featureName)}${unitSuffix}`;
|
91
|
+
|
92
|
+
const xTitle = `${capitalize(obsType)}`;
|
93
|
+
|
94
|
+
// Use a square-root term because the angle of the labels is 45 degrees (see below)
|
95
|
+
// so the perpendicular distance to the bottom of the labels is proportional to the
|
96
|
+
// square root of the length of the labels along the imaginary hypotenuse.
|
97
|
+
// 30 is an estimate of the pixel size of a given character and seems to work well.
|
98
|
+
const autoMarginBottom = marginBottom
|
99
|
+
|| 30 + Math.sqrt(maxCharactersForLabel / 2) * 30;
|
100
|
+
|
101
|
+
const foregroundColor = (theme === 'dark' ? 'lightgray' : 'black');
|
102
|
+
|
103
|
+
data.sort((a, b) => ascending(a[FEATURE_KEY], b[FEATURE_KEY]));
|
104
|
+
|
105
|
+
const svg = select(domElement);
|
106
|
+
svg.selectAll('g').remove();
|
107
|
+
svg
|
108
|
+
.attr('width', width)
|
109
|
+
.attr('height', height);
|
110
|
+
|
111
|
+
const g = svg
|
112
|
+
.append('g')
|
113
|
+
.attr('width', width)
|
114
|
+
.attr('height', height);
|
115
|
+
|
116
|
+
const innerWidth = width - marginLeft;
|
117
|
+
const innerHeight = height - autoMarginBottom;
|
118
|
+
|
119
|
+
const xScale = scaleBand()
|
120
|
+
.range([marginLeft, width - marginRight])
|
121
|
+
.domain(data.map(d => d[OBS_KEY]))
|
122
|
+
.padding(0.1);
|
123
|
+
|
124
|
+
// For the y domain, use the yMin prop
|
125
|
+
// to support a use case such as 'Aspect Ratio',
|
126
|
+
// where the domain minimum should be 1 rather than 0.
|
127
|
+
const yScale = scaleLinear()
|
128
|
+
.domain([yMin, yMax])
|
129
|
+
.range([innerHeight, marginTop]);
|
130
|
+
|
131
|
+
// Bar areas
|
132
|
+
g
|
133
|
+
.selectAll('bar')
|
134
|
+
.data(data)
|
135
|
+
.enter()
|
136
|
+
.append('rect')
|
137
|
+
.attr('x', d => xScale(d[OBS_KEY]))
|
138
|
+
.attr('y', d => yScale(d[FEATURE_KEY]))
|
139
|
+
.attr('width', xScale.bandwidth())
|
140
|
+
.attr('height', d => innerHeight - yScale(d[FEATURE_KEY]))
|
141
|
+
.style('fill', (d) => {
|
142
|
+
if (d[OBS_KEY] === cellHighlight) return 'orange';
|
143
|
+
if (setsSave.has(d[OBS_KEY])) {
|
144
|
+
const { color } = setsSave.get(d[OBS_KEY]);
|
145
|
+
return rgbToHex(color);
|
146
|
+
}
|
147
|
+
return foregroundColor;
|
148
|
+
})
|
149
|
+
.style('cursor', 'pointer')
|
150
|
+
// eslint-disable-next-line no-unused-vars
|
151
|
+
.on('click', (event, d) => {
|
152
|
+
onBarSelect(d[OBS_KEY]);
|
153
|
+
})
|
154
|
+
// eslint-disable-next-line no-unused-vars
|
155
|
+
.on('mouseover', (event, d) => {
|
156
|
+
onBarHighlight(d[OBS_KEY]);
|
157
|
+
})
|
158
|
+
.on('mouseout', () => {
|
159
|
+
onBarHighlight(null);
|
160
|
+
});
|
161
|
+
|
162
|
+
|
163
|
+
const axis = axisLeft(yScale);
|
164
|
+
axis.tickFormat(d => `${Math.round(d * 10000000)} µm`);
|
165
|
+
// Y-axis ticks
|
166
|
+
g
|
167
|
+
.append('g')
|
168
|
+
.attr('transform', `translate(${marginLeft},0)`)
|
169
|
+
.call(axis)
|
170
|
+
.selectAll('text')
|
171
|
+
.style('font-size', '11px');
|
172
|
+
|
173
|
+
// X-axis ticks
|
174
|
+
g
|
175
|
+
.append('g')
|
176
|
+
.attr('transform', `translate(0,${innerHeight})`)
|
177
|
+
.style('font-size', '14px')
|
178
|
+
.call(axisBottom(xScale))
|
179
|
+
.selectAll('text')
|
180
|
+
.style('font-size', '11px')
|
181
|
+
.attr('dx', '-6px')
|
182
|
+
.attr('dy', '6px')
|
183
|
+
.attr('transform', 'rotate(-45)')
|
184
|
+
.style('text-anchor', 'end');
|
185
|
+
|
186
|
+
// Y-axis title
|
187
|
+
g
|
188
|
+
.append('text')
|
189
|
+
.attr('text-anchor', 'middle')
|
190
|
+
.attr('x', -innerHeight / 2)
|
191
|
+
.attr('y', 15)
|
192
|
+
.attr('transform', 'rotate(-90)')
|
193
|
+
.text(yTitle)
|
194
|
+
.style('font-size', '12px')
|
195
|
+
.style('fill', foregroundColor);
|
196
|
+
|
197
|
+
// X-axis title
|
198
|
+
g
|
199
|
+
.append('text')
|
200
|
+
.attr('text-anchor', 'middle')
|
201
|
+
.attr('x', marginLeft + innerWidth / 2)
|
202
|
+
.attr('y', height - 10)
|
203
|
+
.text(xTitle)
|
204
|
+
.style('font-size', '12px')
|
205
|
+
.style('fill', foregroundColor);
|
206
|
+
}, [width, height, data, marginLeft, marginBottom, colors,
|
207
|
+
jitter, theme, yMin, marginTop, marginRight, featureType,
|
208
|
+
featureValueType, yUnits, obsType,
|
209
|
+
maxCharactersForLabel, yMax, featureName, onBarSelect, onBarHighlight,
|
210
|
+
cellHighlight, setsSave,
|
211
|
+
]);
|
212
|
+
|
213
|
+
return (
|
214
|
+
<svg
|
215
|
+
ref={svgRef}
|
216
|
+
style={{
|
217
|
+
top: 0,
|
218
|
+
left: 0,
|
219
|
+
width: `${width}px`,
|
220
|
+
height: `${height}px`,
|
221
|
+
position: 'relative',
|
222
|
+
}}
|
223
|
+
/>
|
224
|
+
);
|
225
|
+
}
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import React, { useMemo, useCallback } from 'react';
|
2
|
+
import {
|
3
|
+
TitleInfo,
|
4
|
+
useCoordination,
|
5
|
+
useLoaders,
|
6
|
+
useUrls,
|
7
|
+
useReady,
|
8
|
+
useGridItemSize,
|
9
|
+
useFeatureSelection,
|
10
|
+
useObsFeatureMatrixIndices,
|
11
|
+
useFeatureLabelsData,
|
12
|
+
} from '@vitessce/vit-s';
|
13
|
+
import { ViewType, COMPONENT_COORDINATION_TYPES } from '@vitessce/constants-internal';
|
14
|
+
import { setObsSelection } from '@vitessce/sets-utils';
|
15
|
+
import FeatureBarPlot from './FeatureBarPlot.js';
|
16
|
+
import { useStyles } from './styles.js';
|
17
|
+
|
18
|
+
|
19
|
+
export function FeatureBarPlotSubscriber(props) {
|
20
|
+
const {
|
21
|
+
coordinationScopes,
|
22
|
+
removeGridComponent,
|
23
|
+
theme,
|
24
|
+
yMin = 0,
|
25
|
+
yUnits = null,
|
26
|
+
} = props;
|
27
|
+
|
28
|
+
const classes = useStyles();
|
29
|
+
const loaders = useLoaders();
|
30
|
+
|
31
|
+
// Get "props" from the coordination space.
|
32
|
+
const [{
|
33
|
+
dataset,
|
34
|
+
obsType,
|
35
|
+
featureType,
|
36
|
+
featureValueType,
|
37
|
+
featureSelection: geneSelection,
|
38
|
+
featureValueTransform,
|
39
|
+
featureValueTransformCoefficient,
|
40
|
+
obsHighlight: cellHighlight,
|
41
|
+
additionalObsSets: additionalCellSets,
|
42
|
+
obsSetSelection: cellSetSelection,
|
43
|
+
obsSetColor: cellSetColor,
|
44
|
+
}, {
|
45
|
+
setObsSetSelection: setCellSetSelection,
|
46
|
+
setObsHighlight: setCellHighlight,
|
47
|
+
setObsSetColor: setCellSetColor,
|
48
|
+
setObsColorEncoding: setCellColorEncoding,
|
49
|
+
setAdditionalObsSets: setAdditionalCellSets,
|
50
|
+
}] = useCoordination(
|
51
|
+
COMPONENT_COORDINATION_TYPES[ViewType.FEATURE_BAR_PLOT],
|
52
|
+
coordinationScopes,
|
53
|
+
);
|
54
|
+
// console.log("BarPlot: " + cellHighlight)
|
55
|
+
const [width, height, containerRef] = useGridItemSize();
|
56
|
+
|
57
|
+
// Get data from loaders using the data hooks.
|
58
|
+
// eslint-disable-next-line no-unused-vars
|
59
|
+
const [expressionData, loadedFeatureSelection, featureSelectionStatus] = useFeatureSelection(
|
60
|
+
loaders, dataset, false, geneSelection,
|
61
|
+
{ obsType, featureType, featureValueType },
|
62
|
+
);
|
63
|
+
// TODO: support multiple feature labels using featureLabelsType coordination values.
|
64
|
+
const [{ featureLabelsMap }, featureLabelsStatus, featureLabelsUrls] = useFeatureLabelsData(
|
65
|
+
loaders, dataset, false, {}, {},
|
66
|
+
{ featureType },
|
67
|
+
);
|
68
|
+
const [
|
69
|
+
{ obsIndex }, matrixIndicesStatus, matrixIndicesUrls,
|
70
|
+
] = useObsFeatureMatrixIndices(
|
71
|
+
loaders, dataset, false,
|
72
|
+
{ obsType, featureType, featureValueType },
|
73
|
+
);
|
74
|
+
const isReady = useReady([
|
75
|
+
featureSelectionStatus,
|
76
|
+
matrixIndicesStatus,
|
77
|
+
featureLabelsStatus,
|
78
|
+
]);
|
79
|
+
const urls = useUrls([
|
80
|
+
featureLabelsUrls,
|
81
|
+
matrixIndicesUrls,
|
82
|
+
]);
|
83
|
+
|
84
|
+
const onBarSelect = useCallback((obsId) => {
|
85
|
+
const obsIdsToSelect = [obsId];
|
86
|
+
setObsSelection(
|
87
|
+
obsIdsToSelect, additionalCellSets, cellSetColor,
|
88
|
+
setCellSetSelection, setAdditionalCellSets, setCellSetColor,
|
89
|
+
setCellColorEncoding,
|
90
|
+
);
|
91
|
+
}, [additionalCellSets, cellSetColor, setCellColorEncoding,
|
92
|
+
setAdditionalCellSets, setCellSetColor, setCellSetSelection]);
|
93
|
+
|
94
|
+
const onBarHighlight = useCallback((obsId) => {
|
95
|
+
setCellHighlight(obsId);
|
96
|
+
}, []);
|
97
|
+
|
98
|
+
const firstGeneSelected = geneSelection && geneSelection.length >= 1
|
99
|
+
? (featureLabelsMap?.get(geneSelection[0]) || geneSelection[0])
|
100
|
+
: null;
|
101
|
+
|
102
|
+
const [expressionArr, expressionMax] = useMemo(() => {
|
103
|
+
if (firstGeneSelected && expressionData && obsIndex) {
|
104
|
+
let exprMax = -Infinity;
|
105
|
+
const cellIndices = {};
|
106
|
+
for (let i = 0; i < obsIndex.length; i += 1) {
|
107
|
+
cellIndices[obsIndex[i]] = i;
|
108
|
+
}
|
109
|
+
const exprValues = obsIndex.map((obsId, cellIndex) => {
|
110
|
+
const value = expressionData[0][cellIndex];
|
111
|
+
exprMax = Math.max(value, exprMax);
|
112
|
+
return { obsId, value, feature: firstGeneSelected };
|
113
|
+
});
|
114
|
+
return [exprValues, exprMax];
|
115
|
+
}
|
116
|
+
return [null, null];
|
117
|
+
}, [expressionData, obsIndex, geneSelection, theme,
|
118
|
+
featureValueTransform, featureValueTransformCoefficient,
|
119
|
+
firstGeneSelected,
|
120
|
+
]);
|
121
|
+
|
122
|
+
return (
|
123
|
+
<TitleInfo
|
124
|
+
title={`Feature Values${(firstGeneSelected ? ` (${firstGeneSelected})` : '')}`}
|
125
|
+
removeGridComponent={removeGridComponent}
|
126
|
+
urls={urls}
|
127
|
+
theme={theme}
|
128
|
+
isReady={isReady}
|
129
|
+
>
|
130
|
+
<div ref={containerRef} className={classes.vegaContainer}>
|
131
|
+
{expressionArr ? (
|
132
|
+
<FeatureBarPlot
|
133
|
+
yMin={yMin}
|
134
|
+
yMax={expressionMax}
|
135
|
+
yUnits={yUnits}
|
136
|
+
data={expressionArr}
|
137
|
+
theme={theme}
|
138
|
+
width={width}
|
139
|
+
height={height}
|
140
|
+
obsType={obsType}
|
141
|
+
cellHighlight={cellHighlight}
|
142
|
+
cellSetSelection={cellSetSelection}
|
143
|
+
additionalCellSets={additionalCellSets}
|
144
|
+
cellSetColor={cellSetColor}
|
145
|
+
featureType={featureType}
|
146
|
+
featureValueType={featureValueType}
|
147
|
+
featureName={firstGeneSelected}
|
148
|
+
onBarSelect={onBarSelect}
|
149
|
+
onBarHighlight={onBarHighlight}
|
150
|
+
/>
|
151
|
+
) : (
|
152
|
+
<span>Select a {featureType}.</span>
|
153
|
+
)}
|
154
|
+
</div>
|
155
|
+
</TitleInfo>
|
156
|
+
);
|
157
|
+
}
|