@vitessce/heatmap 2.0.2 → 2.0.3
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/deflate.1b70f605.mjs +13 -0
- package/dist/index.35c24bfa.mjs +132140 -0
- package/dist/index.mjs +9 -0
- package/dist/jpeg.1b7865ea.mjs +840 -0
- package/dist/lerc.ff3140f6.mjs +1943 -0
- package/dist/lzw.1036ab46.mjs +128 -0
- package/dist/packbits.088a4e84.mjs +30 -0
- package/dist/pako.esm.4b234125.mjs +3940 -0
- package/dist/raw.b3ce459e.mjs +12 -0
- package/dist/webimage.4cecc301.mjs +32 -0
- package/{dist → dist-tsc}/index.js +0 -0
- package/package.json +12 -12
- package/src/Heatmap.js +857 -0
- package/src/Heatmap.test.fixtures.js +21 -0
- package/src/Heatmap.test.jsx +31 -0
- package/src/HeatmapOptions.js +71 -0
- package/src/HeatmapSubscriber.js +253 -0
- package/src/HeatmapTooltipSubscriber.js +51 -0
- package/src/HeatmapWorkerPool.js +52 -0
- package/src/heatmap-indexing.pdf +0 -0
- package/src/heatmap-indexing.tex +65 -0
- package/src/index.js +2 -0
- package/src/utils.js +247 -0
- package/src/utils.test.js +75 -0
- package/dist/Heatmap.js +0 -689
- package/dist/Heatmap.test.fixtures.js +0 -20
- package/dist/Heatmap.test.js +0 -16
- package/dist/HeatmapOptions.js +0 -22
- package/dist/HeatmapSubscriber.js +0 -104
- package/dist/HeatmapTooltipSubscriber.js +0 -24
- package/dist/HeatmapWorkerPool.js +0 -49
- package/dist/utils.js +0 -181
- package/dist/utils.test.js +0 -71
package/src/utils.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import clamp from 'lodash/clamp';
|
|
2
|
+
import {
|
|
3
|
+
AXIS_LABEL_TEXT_SIZE,
|
|
4
|
+
AXIS_FONT_FAMILY,
|
|
5
|
+
AXIS_PADDING,
|
|
6
|
+
AXIS_MIN_SIZE,
|
|
7
|
+
AXIS_MAX_SIZE,
|
|
8
|
+
} from '@vitessce/gl';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Called before a layer is drawn to determine whether it should be rendered.
|
|
12
|
+
* Reference: https://deck.gl/docs/api-reference/core/deck#layerfilter
|
|
13
|
+
* @param {object} params A viewport, layer pair.
|
|
14
|
+
* @param {object} params.layer The layer to check.
|
|
15
|
+
* @param {object} params.viewport The viewport to check.
|
|
16
|
+
* @returns {boolean} Should this layer be rendered in this viewport?
|
|
17
|
+
*/
|
|
18
|
+
export function layerFilter({ layer, viewport }) {
|
|
19
|
+
if (viewport.id === 'axisLeft') {
|
|
20
|
+
return layer.id.startsWith('axisLeft');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (viewport.id === 'axisTop') {
|
|
24
|
+
return layer.id.startsWith('axisTop');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (viewport.id === 'cellColorLabel') {
|
|
28
|
+
return layer.id.startsWith('cellColorLabel');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (viewport.id === 'heatmap') {
|
|
32
|
+
return layer.id.startsWith('heatmap');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (viewport.id.startsWith('colorsLeft')) {
|
|
36
|
+
const matches = viewport.id.match(/-(\d)/);
|
|
37
|
+
if (matches) return layer.id.startsWith(`colorsLeftLayer-${matches[1]}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (viewport.id.startsWith('colorsTop')) {
|
|
41
|
+
const matches = viewport.id.match(/-(\d)/);
|
|
42
|
+
if (matches) return layer.id.startsWith(`colorsTopLayer-${matches[1]}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Uses canvas.measureText to compute and return the width of the given text
|
|
50
|
+
* of given font in pixels.
|
|
51
|
+
*
|
|
52
|
+
* @param {String} text The text to be rendered.
|
|
53
|
+
* @param {String} font The css font descriptor that text is to be rendered
|
|
54
|
+
* with (e.g. "bold 14px verdana").
|
|
55
|
+
*
|
|
56
|
+
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
|
57
|
+
*/
|
|
58
|
+
function getTextWidth(text, font) {
|
|
59
|
+
// re-use canvas object for better performance
|
|
60
|
+
const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement('canvas'));
|
|
61
|
+
const context = canvas.getContext('2d');
|
|
62
|
+
context.font = font;
|
|
63
|
+
const metrics = context.measureText(text);
|
|
64
|
+
return metrics.width;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the size of the left and top heatmap axes,
|
|
69
|
+
* taking into account the maximum label string lengths.
|
|
70
|
+
* @param {boolean} transpose Is the heatmap transposed?
|
|
71
|
+
* @param {String} longestGeneLabel longest gene label
|
|
72
|
+
* @param {String} longestCellLabel longest cell label
|
|
73
|
+
* @param {boolean} hideObservationLabels are cell labels hidden?
|
|
74
|
+
* @param {boolean} hideVariableLabels are gene labels hidden?
|
|
75
|
+
* Increases vertical space for heatmap
|
|
76
|
+
* @returns {number[]} [axisOffsetLeft, axisOffsetTop]
|
|
77
|
+
*/
|
|
78
|
+
export function getAxisSizes(
|
|
79
|
+
transpose, longestGeneLabel, longestCellLabel,
|
|
80
|
+
hideObservationLabels, hideVariableLabels,
|
|
81
|
+
) {
|
|
82
|
+
const font = `${AXIS_LABEL_TEXT_SIZE}pt ${AXIS_FONT_FAMILY}`;
|
|
83
|
+
const geneLabelMaxWidth = hideVariableLabels
|
|
84
|
+
? 0 : getTextWidth(longestGeneLabel, font) + AXIS_PADDING;
|
|
85
|
+
const cellLabelMaxWidth = hideObservationLabels
|
|
86
|
+
? 0 : getTextWidth(longestCellLabel, font) + AXIS_PADDING;
|
|
87
|
+
|
|
88
|
+
const axisOffsetLeft = clamp(
|
|
89
|
+
(transpose ? geneLabelMaxWidth : cellLabelMaxWidth),
|
|
90
|
+
AXIS_MIN_SIZE,
|
|
91
|
+
AXIS_MAX_SIZE,
|
|
92
|
+
);
|
|
93
|
+
const axisOffsetTop = clamp(
|
|
94
|
+
(transpose ? cellLabelMaxWidth : geneLabelMaxWidth),
|
|
95
|
+
AXIS_MIN_SIZE,
|
|
96
|
+
AXIS_MAX_SIZE,
|
|
97
|
+
);
|
|
98
|
+
return [axisOffsetLeft, axisOffsetTop];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Convert a mouse coordinate (x, y) to a heatmap coordinate (col index, row index).
|
|
103
|
+
* @param {number} mouseX The mouse X of interest.
|
|
104
|
+
* @param {number} mouseY The mouse Y of interest.
|
|
105
|
+
* @param {object} param2 An object containing current sizes and scale factors.
|
|
106
|
+
* @returns {number[]} [colI, rowI]
|
|
107
|
+
*/
|
|
108
|
+
export function mouseToHeatmapPosition(mouseX, mouseY, {
|
|
109
|
+
offsetLeft, offsetTop, targetX, targetY, scaleFactor, matrixWidth, matrixHeight, numRows, numCols,
|
|
110
|
+
}) {
|
|
111
|
+
// TODO: use linear algebra
|
|
112
|
+
const viewMouseX = mouseX - offsetLeft;
|
|
113
|
+
const viewMouseY = mouseY - offsetTop;
|
|
114
|
+
|
|
115
|
+
if (viewMouseX < 0 || viewMouseY < 0) {
|
|
116
|
+
// The mouse is outside the heatmap.
|
|
117
|
+
return [null, null];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Determine the rowI and colI values based on the current viewState.
|
|
121
|
+
const bboxTargetX = targetX * scaleFactor + matrixWidth * scaleFactor / 2;
|
|
122
|
+
const bboxTargetY = targetY * scaleFactor + matrixHeight * scaleFactor / 2;
|
|
123
|
+
|
|
124
|
+
const bboxLeft = bboxTargetX - matrixWidth / 2;
|
|
125
|
+
const bboxTop = bboxTargetY - matrixHeight / 2;
|
|
126
|
+
|
|
127
|
+
const zoomedOffsetLeft = bboxLeft / (matrixWidth * scaleFactor);
|
|
128
|
+
const zoomedOffsetTop = bboxTop / (matrixHeight * scaleFactor);
|
|
129
|
+
|
|
130
|
+
const zoomedViewMouseX = viewMouseX / (matrixWidth * scaleFactor);
|
|
131
|
+
const zoomedViewMouseY = viewMouseY / (matrixHeight * scaleFactor);
|
|
132
|
+
|
|
133
|
+
const zoomedMouseX = zoomedOffsetLeft + zoomedViewMouseX;
|
|
134
|
+
const zoomedMouseY = zoomedOffsetTop + zoomedViewMouseY;
|
|
135
|
+
|
|
136
|
+
const rowI = Math.floor(zoomedMouseY * numRows);
|
|
137
|
+
const colI = Math.floor(zoomedMouseX * numCols);
|
|
138
|
+
return [colI, rowI];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Convert a heatmap coordinate (col index, row index) to a mouse coordinate (x, y).
|
|
143
|
+
* @param {number} colI The column index of interest.
|
|
144
|
+
* @param {number} rowI The row index of interest.
|
|
145
|
+
* @param {object} param2 An object containing current sizes and scale factors.
|
|
146
|
+
* @returns {number[]} [x, y]
|
|
147
|
+
*/
|
|
148
|
+
export function heatmapToMousePosition(colI, rowI, {
|
|
149
|
+
offsetLeft, offsetTop, targetX, targetY, scaleFactor, matrixWidth, matrixHeight, numRows, numCols,
|
|
150
|
+
}) {
|
|
151
|
+
// TODO: use linear algebra
|
|
152
|
+
let zoomedMouseY = null;
|
|
153
|
+
let zoomedMouseX = null;
|
|
154
|
+
|
|
155
|
+
if (rowI !== null) {
|
|
156
|
+
const minY = -matrixHeight * scaleFactor / 2;
|
|
157
|
+
const maxY = matrixHeight * scaleFactor / 2;
|
|
158
|
+
const totalHeight = maxY - minY;
|
|
159
|
+
|
|
160
|
+
const minInViewY = (targetY * scaleFactor) - (matrixHeight / 2);
|
|
161
|
+
const maxInViewY = (targetY * scaleFactor) + (matrixHeight / 2);
|
|
162
|
+
const inViewHeight = maxInViewY - minInViewY;
|
|
163
|
+
|
|
164
|
+
const normalizedRowY = (rowI + 0.5) / numRows;
|
|
165
|
+
const globalRowY = minY + (normalizedRowY * totalHeight);
|
|
166
|
+
|
|
167
|
+
if (minInViewY <= globalRowY && globalRowY <= maxInViewY) {
|
|
168
|
+
zoomedMouseY = offsetTop + ((globalRowY - minInViewY) / inViewHeight) * matrixHeight;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (colI !== null) {
|
|
172
|
+
const minX = -matrixWidth * scaleFactor / 2;
|
|
173
|
+
const maxX = matrixWidth * scaleFactor / 2;
|
|
174
|
+
const totalWidth = maxX - minX;
|
|
175
|
+
|
|
176
|
+
const minInViewX = (targetX * scaleFactor) - (matrixWidth / 2);
|
|
177
|
+
const maxInViewX = (targetX * scaleFactor) + (matrixWidth / 2);
|
|
178
|
+
const inViewWidth = maxInViewX - minInViewX;
|
|
179
|
+
|
|
180
|
+
const normalizedRowX = (colI + 0.5) / numCols;
|
|
181
|
+
const globalRowX = minX + (normalizedRowX * totalWidth);
|
|
182
|
+
|
|
183
|
+
if (minInViewX <= globalRowX && globalRowX <= maxInViewX) {
|
|
184
|
+
zoomedMouseX = offsetLeft + ((globalRowX - minInViewX) / inViewWidth) * matrixWidth;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [zoomedMouseX, zoomedMouseY];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Convert a mouse coordinate (x, y) to a heatmap color bar coordinate (cell index, track index).
|
|
192
|
+
* @param {number} mouseX The mouse X of interest.
|
|
193
|
+
* @param {number} mouseY The mouse Y of interest.
|
|
194
|
+
* @param {object} param2 An object containing current sizes and scale factors.
|
|
195
|
+
* @returns {number[]} [cellI, trackI]
|
|
196
|
+
*/
|
|
197
|
+
export function mouseToCellColorPosition(mouseX, mouseY, {
|
|
198
|
+
axisOffsetTop,
|
|
199
|
+
axisOffsetLeft,
|
|
200
|
+
offsetTop,
|
|
201
|
+
offsetLeft,
|
|
202
|
+
colorBarSize,
|
|
203
|
+
numCellColorTracks,
|
|
204
|
+
transpose,
|
|
205
|
+
targetX,
|
|
206
|
+
targetY,
|
|
207
|
+
scaleFactor,
|
|
208
|
+
matrixWidth,
|
|
209
|
+
matrixHeight,
|
|
210
|
+
numRows,
|
|
211
|
+
numCols,
|
|
212
|
+
}) {
|
|
213
|
+
const cellPosition = transpose ? mouseX - offsetLeft : mouseY - offsetTop;
|
|
214
|
+
const trackPosition = transpose ? mouseY - axisOffsetTop : mouseX - axisOffsetLeft;
|
|
215
|
+
|
|
216
|
+
const tracksWidth = numCellColorTracks * colorBarSize;
|
|
217
|
+
|
|
218
|
+
// outside of cell color tracks
|
|
219
|
+
if (cellPosition < 0 || trackPosition < 0 || trackPosition >= tracksWidth) {
|
|
220
|
+
return [null, null];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Determine the trackI and cellI values based on the current viewState.
|
|
224
|
+
const trackI = Math.floor(trackPosition / colorBarSize);
|
|
225
|
+
|
|
226
|
+
let cellI;
|
|
227
|
+
if (transpose) {
|
|
228
|
+
const viewMouseX = mouseX - offsetLeft;
|
|
229
|
+
const bboxTargetX = targetX * scaleFactor + matrixWidth * scaleFactor / 2;
|
|
230
|
+
const bboxLeft = bboxTargetX - matrixWidth / 2;
|
|
231
|
+
const zoomedOffsetLeft = bboxLeft / (matrixWidth * scaleFactor);
|
|
232
|
+
const zoomedViewMouseX = viewMouseX / (matrixWidth * scaleFactor);
|
|
233
|
+
const zoomedMouseX = zoomedOffsetLeft + zoomedViewMouseX;
|
|
234
|
+
cellI = Math.floor(zoomedMouseX * numCols);
|
|
235
|
+
return [cellI, trackI];
|
|
236
|
+
}
|
|
237
|
+
// Not transposed
|
|
238
|
+
const viewMouseY = mouseY - axisOffsetTop;
|
|
239
|
+
const bboxTargetY = targetY * scaleFactor + matrixHeight * scaleFactor / 2;
|
|
240
|
+
const bboxTop = bboxTargetY - matrixHeight / 2;
|
|
241
|
+
const zoomedOffsetTop = bboxTop / (matrixHeight * scaleFactor);
|
|
242
|
+
const zoomedViewMouseY = viewMouseY / (matrixHeight * scaleFactor);
|
|
243
|
+
const zoomedMouseY = zoomedOffsetTop + zoomedViewMouseY;
|
|
244
|
+
cellI = Math.floor(zoomedMouseY * numRows);
|
|
245
|
+
|
|
246
|
+
return [cellI, trackI];
|
|
247
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { mouseToHeatmapPosition, heatmapToMousePosition } from './utils';
|
|
2
|
+
|
|
3
|
+
describe('heatmap tooltip utils', () => {
|
|
4
|
+
it('transforms mouse coordinates to row and column indices when zoomed out', () => {
|
|
5
|
+
const mouseX = 35;
|
|
6
|
+
const mouseY = 78;
|
|
7
|
+
const [colI, rowI] = mouseToHeatmapPosition(mouseX, mouseY, {
|
|
8
|
+
offsetLeft: 10,
|
|
9
|
+
offsetTop: 10,
|
|
10
|
+
targetX: 0,
|
|
11
|
+
targetY: 0,
|
|
12
|
+
scaleFactor: 1,
|
|
13
|
+
matrixWidth: 100,
|
|
14
|
+
matrixHeight: 100,
|
|
15
|
+
numRows: 5,
|
|
16
|
+
numCols: 4,
|
|
17
|
+
});
|
|
18
|
+
expect(colI).toEqual(1);
|
|
19
|
+
expect(rowI).toEqual(3);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('transforms mouse coordinates to row and column indices when zoomed in', () => {
|
|
23
|
+
const mouseX = 35;
|
|
24
|
+
const mouseY = 78;
|
|
25
|
+
const [colI, rowI] = mouseToHeatmapPosition(mouseX, mouseY, {
|
|
26
|
+
offsetLeft: 10,
|
|
27
|
+
offsetTop: 10,
|
|
28
|
+
targetX: 21,
|
|
29
|
+
targetY: -11,
|
|
30
|
+
scaleFactor: 4,
|
|
31
|
+
matrixWidth: 100,
|
|
32
|
+
matrixHeight: 100,
|
|
33
|
+
numRows: 5,
|
|
34
|
+
numCols: 4,
|
|
35
|
+
});
|
|
36
|
+
expect(colI).toEqual(2);
|
|
37
|
+
expect(rowI).toEqual(2);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('transforms row and column indices when zoomed out', () => {
|
|
41
|
+
const colI = 1;
|
|
42
|
+
const rowI = 3;
|
|
43
|
+
const [mouseX, mouseY] = heatmapToMousePosition(colI, rowI, {
|
|
44
|
+
offsetLeft: 10,
|
|
45
|
+
offsetTop: 10,
|
|
46
|
+
targetX: 0,
|
|
47
|
+
targetY: 0,
|
|
48
|
+
scaleFactor: 1,
|
|
49
|
+
matrixWidth: 100,
|
|
50
|
+
matrixHeight: 100,
|
|
51
|
+
numRows: 5,
|
|
52
|
+
numCols: 4,
|
|
53
|
+
});
|
|
54
|
+
expect(mouseX).toEqual(47.5);
|
|
55
|
+
expect(mouseY).toEqual(80);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('transforms row and column indices when zoomed in', () => {
|
|
59
|
+
const colI = 2;
|
|
60
|
+
const rowI = 2;
|
|
61
|
+
const [mouseX, mouseY] = heatmapToMousePosition(colI, rowI, {
|
|
62
|
+
offsetLeft: 10,
|
|
63
|
+
offsetTop: 10,
|
|
64
|
+
targetX: 21,
|
|
65
|
+
targetY: -11,
|
|
66
|
+
scaleFactor: 4,
|
|
67
|
+
matrixWidth: 100,
|
|
68
|
+
matrixHeight: 100,
|
|
69
|
+
numRows: 5,
|
|
70
|
+
numCols: 4,
|
|
71
|
+
});
|
|
72
|
+
expect(mouseX).toEqual(26);
|
|
73
|
+
expect(mouseY).toEqual(104);
|
|
74
|
+
});
|
|
75
|
+
});
|