forked from vitessce/vitessce
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlayer-controller.js
More file actions
156 lines (143 loc) · 5.31 KB
/
Copy pathlayer-controller.js
File metadata and controls
156 lines (143 loc) · 5.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { getChannelStats } from '@vivjs/loaders';
import { Matrix4 } from 'math.gl';
// Returns an rgb string for display, and changes the color (arr)
// to use a grey for light theme + white color or if the colormap is on.
export const toRgbUIString = (on, arr, theme) => {
const color = on || (theme !== 'dark' && arr?.every(i => i === 255))
? [220, 220, 220]
: arr;
return `rgb(${color})`;
};
/**
* Ensure that the channel selection object does not have
* extra dimension keys, as this will cause Viv functions
* to throw errors about not being able to access the data.
* @param {*} loader
* @param {object} selection Mapping from dimension label to slice index.
* @returns {object} The filtered selection.
*/
export function filterSelection(loader, selection) {
if (!selection) {
return selection;
}
const data = Array.isArray(loader) ? loader[loader.length - 1] : loader;
const { labels } = data;
return Object.fromEntries(Object.entries(selection).filter(([key]) => labels.includes(key)));
}
async function getSingleSelectionStats2D({ loader, selection }) {
const data = Array.isArray(loader) ? loader[loader.length - 1] : loader;
const filteredSelection = filterSelection(loader, selection);
const raster = await data.getRaster({ selection: filteredSelection });
const selectionStats = getChannelStats(raster.data);
const { domain, contrastLimits: slider } = selectionStats;
return { domain, slider };
}
async function getSingleSelectionStats3D({ loader, selection }) {
const lowResSource = loader[loader.length - 1];
const filteredSelection = filterSelection(lowResSource, selection);
const { shape, labels } = lowResSource;
const sizeZ = shape[labels.indexOf('z')];
const raster0 = await lowResSource.getRaster({
selection: { ...filteredSelection, z: 0 },
});
const rasterMid = await lowResSource.getRaster({
selection: { ...filteredSelection, z: Math.floor(sizeZ / 2) },
});
const rasterTop = await lowResSource.getRaster({
selection: { ...filteredSelection, z: Math.max(0, sizeZ - 1) },
});
const stats0 = getChannelStats(raster0.data);
const statsMid = getChannelStats(rasterMid.data);
const statsTop = getChannelStats(rasterTop.data);
return {
domain: [
Math.min(stats0.domain[0], statsMid.domain[0], statsTop.domain[0]),
Math.max(stats0.domain[1], statsMid.domain[1], statsTop.domain[1]),
],
slider: [
Math.min(
stats0.contrastLimits[0],
statsMid.contrastLimits[0],
statsTop.contrastLimits[0],
),
Math.max(
stats0.contrastLimits[1],
statsMid.contrastLimits[1],
statsTop.contrastLimits[1],
),
],
};
}
/**
* Get bounding cube for a given loader i.e [[0, width], [0, height], [0, depth]]
* @param {Object} loader PixelSource|PixelSource[]
* @param {[]} selection Selection for stats.
* @param {boolean} use3d Whether or not to get 3d stats.
* @returns {Object} { domains, sliders }
*/
export const getSingleSelectionStats = async ({ loader, selection, use3d }) => {
const getStats = use3d
? getSingleSelectionStats3D
: getSingleSelectionStats2D;
return getStats({ loader, selection });
};
export const getMultiSelectionStats = async ({ loader, selections, use3d }) => {
const stats = await Promise.all(
selections.map(selection => getSingleSelectionStats({ loader, selection, use3d })),
);
const domains = stats.map(stat => stat.domain);
const sliders = stats.map(stat => stat.slider);
return { domains, sliders };
};
/**
* Get physical size scaling Matrix4
* @param {Object} loader PixelSource
* @returns {Object} matrix
*/
export function getPhysicalSizeScalingMatrix(loader) {
const { x, y, z } = loader?.meta?.physicalSizes ?? {};
if (x?.size && y?.size && z?.size) {
const min = Math.min(z.size, x.size, y.size);
const ratio = [x.size / min, y.size / min, z.size / min];
return new Matrix4().scale(ratio);
}
return new Matrix4().identity();
}
/**
* Get bounding cube for a given loader
* @param {Object} loader PixelSource|PixelSource[]
* @returns {Array} [0, width], [0, height], [0, depth]]
*/
export function getBoundingCube(loader) {
const source = Array.isArray(loader) ? loader[0] : loader;
const { shape, labels } = source;
const physicalSizeScalingMatrix = getPhysicalSizeScalingMatrix(source);
const xSlice = [0, physicalSizeScalingMatrix[0] * shape[labels.indexOf('x')]];
const ySlice = [0, physicalSizeScalingMatrix[5] * shape[labels.indexOf('y')]];
const zSlice = [
0,
physicalSizeScalingMatrix[10] * shape[labels.indexOf('z')],
];
return [xSlice, ySlice, zSlice];
}
export function abbreviateNumber(value) {
// Return an abbreviated representation of value, in 5 characters or less.
const maxLength = 5;
let maxNaiveDigits = maxLength;
/* eslint-disable no-plusplus */
if (!Number.isInteger(value)) {
--maxNaiveDigits;
} // Wasted on "."
if (value < 1) {
--maxNaiveDigits;
} // Wasted on "0."
/* eslint-disable no-plusplus */
const naive = Intl.NumberFormat('en-US', {
maximumSignificantDigits: maxNaiveDigits,
useGrouping: false,
}).format(value);
if (naive.length <= maxLength) return naive;
// "e+9" consumes 3 characters, so if we even had two significant digits,
// it would take take us to six characters, including the decimal point.
return value.toExponential(0);
}