@vitessce/gl 3.5.8 → 3.5.10

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.
@@ -0,0 +1,254 @@
1
+ import { ContourLayer } from '@deck.gl/aggregation-layers';
2
+ import { LineLayer, TextLayer } from '@deck.gl/layers';
3
+ import { point as turfPoint, polygons } from '@turf/helpers';
4
+ import { getGeom } from '@turf/invariant';
5
+ import { flattenReduce } from '@turf/meta';
6
+ import { union } from '@turf/union';
7
+ import { area } from '@turf/area';
8
+ import { distance } from '@turf/distance';
9
+ import { range } from 'lodash-es';
10
+ import { AXIS_FONT_FAMILY } from './heatmap-constants.js';
11
+
12
+
13
+ const DEFAULT_THRESHOLD = 1;
14
+
15
+ const defaultProps = {
16
+ // for text
17
+ obsSetPath: { type: 'object' },
18
+ sampleSetPath: { type: 'object' },
19
+ circleInfo: { type: 'object' },
20
+ circlePointSet: { type: 'object' },
21
+ obsSetLabelsVisible: { type: 'boolean' },
22
+ obsSetLabelSize: { type: 'number', min: 1, max: 100, value: 12 },
23
+
24
+ // grid aggregation
25
+ cellSize: { type: 'number', min: 1, max: 1000, value: 1000 },
26
+ getPosition: { type: 'accessor', value: x => x.position },
27
+ getWeight: { type: 'accessor', value: 1 },
28
+ gpuAggregation: true,
29
+ aggregation: 'SUM',
30
+
31
+ // contour lines
32
+ contours: {
33
+ type: 'object',
34
+ value: [{ threshold: DEFAULT_THRESHOLD }],
35
+ optional: true,
36
+ compare: 3,
37
+ },
38
+
39
+ zOffset: 0.005,
40
+ };
41
+
42
+ const MIN_AREA_THRESHOLD = 10;
43
+ const MAX_NUM_VERTICES = 36;
44
+
45
+ /**
46
+ * Get the polygon with the maximum area,
47
+ * and also return its area value.
48
+ * @param {{contour, vertices}[]} levelToUse The
49
+ * contourPolygons array filtered to a given level.
50
+ * @returns {[number, object]} A tuple of
51
+ * [area value, Turf polygon object].
52
+ */
53
+ function getMaxAreaPolygon(levelToUse) {
54
+ if (levelToUse.length === 0) {
55
+ return [0, null];
56
+ }
57
+ const levelPolygons = levelToUse.map(d => (
58
+ [
59
+ // The vertices are 3D, but we only need 2D.
60
+ [
61
+ ...d.vertices.map(v => ([v[0], v[1]])),
62
+ // Need four vertices (not 3), so repeat the first vertex.
63
+ [d.vertices[0][0], d.vertices[0][1]],
64
+ ],
65
+ ]
66
+ ));
67
+ const levelPolygonsUnion = union(polygons(levelPolygons));
68
+ const [maxAreaValue, maxAreaPolygon] = flattenReduce(
69
+ levelPolygonsUnion,
70
+ (previousValue, currentFeature) => {
71
+ if (getGeom(currentFeature).type === 'Polygon') {
72
+ const currArea = area(currentFeature);
73
+ if (currArea >= previousValue[0]) {
74
+ return [currArea, currentFeature];
75
+ }
76
+ }
77
+ return previousValue;
78
+ },
79
+ // Initial value
80
+ [0, null],
81
+ );
82
+ return [maxAreaValue, maxAreaPolygon];
83
+ }
84
+
85
+ /**
86
+ * Get the area, polygon, and level associated with
87
+ * the "polygon of maximum area". (Not actually maximum
88
+ * among all levels but the max within a given level)
89
+ * @param {{contour, vertices}[]} contourPolygons The
90
+ * contourPolygons value within state.contourData
91
+ * @returns {[number, object, number]} A tuple of
92
+ * [area value, Turf polygon object, level index].
93
+ */
94
+ function getMaxAreaPolygonAndLevel(contourPolygons) {
95
+ const l2 = contourPolygons?.filter(d => d.contour.i === 2) ?? [];
96
+ const [l2area, l2polygon] = getMaxAreaPolygon(l2);
97
+ if (Math.log10(l2area) >= MIN_AREA_THRESHOLD) {
98
+ return [l2area, l2polygon, 2];
99
+ }
100
+ const l1 = contourPolygons?.filter(d => d.contour.i === 1) ?? [];
101
+ const [l1area, l1polygon] = getMaxAreaPolygon(l1);
102
+ if (Math.log10(l1area) >= MIN_AREA_THRESHOLD) {
103
+ return [l1area, l1polygon, 1];
104
+ }
105
+ const l0 = contourPolygons?.filter(d => d.contour.i === 0) ?? [];
106
+ const [l0area, l0polygon] = getMaxAreaPolygon(l0);
107
+ return [l0area, l0polygon, 0];
108
+ }
109
+
110
+ // Reference: https://github.com/visgl/deck.gl/blob/v8.9.36/modules/aggregation-layers/src/contour-layer/contour-layer.ts
111
+ export default class ContourLayerWithText extends ContourLayer {
112
+ /**
113
+ * Construct line and text layers to render.
114
+ * @returns {object[]} Array of DeckGL layers.
115
+ */
116
+ getLineAndTextLayers() {
117
+ const lineAndTextLayers = [];
118
+
119
+ const { contourPolygons } = this.state.contourData;
120
+ const { obsSetPath, contours, circleInfo, circlePointSet, obsSetLabelSize } = this.props;
121
+
122
+ if (!circleInfo) {
123
+ return lineAndTextLayers;
124
+ }
125
+
126
+ const obsSetName = obsSetPath?.at(-1);
127
+ const [maxAreaValue, maxAreaPolygon, levelI] = getMaxAreaPolygonAndLevel(contourPolygons);
128
+
129
+ // Get a circle polygon which outlines the whole plot.
130
+ const { center, steps, polygon: circlePolygon } = circleInfo;
131
+
132
+ if (maxAreaValue > 0 && maxAreaPolygon) {
133
+ let minDist = Infinity;
134
+ let minCirclePoint;
135
+ let minCirclePointI;
136
+ let minPolygonPoint;
137
+
138
+ const numVertices = maxAreaPolygon.geometry.coordinates[0].length;
139
+
140
+ // Only consider a maximum of 36 vertices from the polygon,
141
+ // since it may be complex with many vertices.
142
+ const polygonVertices = numVertices > MAX_NUM_VERTICES
143
+ ? range(MAX_NUM_VERTICES).map(
144
+ i => maxAreaPolygon
145
+ .geometry
146
+ .coordinates[0][Math.floor(i * numVertices / MAX_NUM_VERTICES)],
147
+ )
148
+ : [...maxAreaPolygon.geometry.coordinates[0]];
149
+
150
+ // Find the pair (vertex on circle, vertex on contourPolygon) which
151
+ // have the shortest distance.
152
+ circlePolygon.geometry.coordinates[0].forEach((circleCoord, circlePointI) => {
153
+ const circlePoint = turfPoint(circleCoord);
154
+ polygonVertices.forEach((polyCoord) => {
155
+ const polyPoint = turfPoint(polyCoord);
156
+ const dist = distance(circlePoint, polyPoint);
157
+ if (dist < minDist) {
158
+ minDist = dist;
159
+ minCirclePoint = circleCoord;
160
+ minCirclePointI = circlePointI;
161
+ minPolygonPoint = polyCoord;
162
+ }
163
+ });
164
+ });
165
+
166
+ // If this circle vertex has already been used, then its index
167
+ // will be in the circlePointSet, so we instead try the subsequent
168
+ // vertex until we find one that is unused
169
+ // (or until the set is full, in which case we must reuse a vertex).
170
+ while (circlePointSet.size < steps && circlePointSet.has(minCirclePointI)) {
171
+ const nextCirclePointI = (minCirclePointI + 1) % steps;
172
+ minCirclePointI = nextCirclePointI;
173
+ }
174
+ circlePointSet.add(minCirclePointI);
175
+ minCirclePoint = circlePolygon.geometry.coordinates[0][minCirclePointI];
176
+
177
+ // Compute the angle formed by the line
178
+ // (from the circle's center point).
179
+ const angleRadians = Math.atan2(
180
+ minCirclePoint[1] - center[1],
181
+ minCirclePoint[0] - center[0],
182
+ );
183
+ let angleDegrees = angleRadians * 180 / Math.PI;
184
+
185
+ // Create the line and text layers.
186
+ lineAndTextLayers.push(new LineLayer({
187
+ id: `line-${obsSetName}`,
188
+ data: [
189
+ {
190
+ from: minPolygonPoint,
191
+ to: minCirclePoint,
192
+ color: contours[levelI].color,
193
+ },
194
+ ],
195
+ getColor: d => d.color,
196
+ getSourcePosition: d => d.from,
197
+ getTargetPosition: d => d.to,
198
+ getWidth: levelI + 0.5,
199
+ }));
200
+
201
+ // Determine textAnchor based on angle formed on circle.
202
+ let textAnchor = 'start';
203
+ if (angleDegrees < 0) {
204
+ angleDegrees += 360;
205
+ }
206
+ if (angleDegrees > 90 && angleDegrees < 270) {
207
+ angleDegrees -= 180;
208
+ textAnchor = 'end';
209
+ }
210
+ lineAndTextLayers.push(new TextLayer({
211
+ id: `text-${obsSetName}`,
212
+ data: [
213
+ {
214
+ label: obsSetName,
215
+ to: minCirclePoint,
216
+ color: contours[levelI].color,
217
+ },
218
+ ],
219
+ getPosition: d => d.to,
220
+ getText: d => d.label,
221
+ getColor: d => d.color,
222
+ getSize: obsSetLabelSize,
223
+ getAngle: -angleDegrees,
224
+ getTextAnchor: textAnchor,
225
+ getAlignmentBaseline: 'center',
226
+ fontFamily: AXIS_FONT_FAMILY,
227
+ fontWeight: 'normal',
228
+ // maxWidth: 80 * obsSetLabelSize,
229
+ }));
230
+ }
231
+
232
+ return lineAndTextLayers;
233
+ }
234
+
235
+ /**
236
+ * Overrides the ContourLayer renderLayers function,
237
+ * so that the custom text/line layers can be
238
+ * appended to the contour line/polygon layers.
239
+ * @returns {object[]} Array of DeckGL layers.
240
+ */
241
+ renderLayers() {
242
+ const { obsSetLabelsVisible } = this.props;
243
+ const contourLayers = super.renderLayers();
244
+ const lineAndTextLayers = obsSetLabelsVisible
245
+ ? this.getLineAndTextLayers()
246
+ : [];
247
+ return [
248
+ ...contourLayers,
249
+ ...lineAndTextLayers,
250
+ ];
251
+ }
252
+ }
253
+ ContourLayerWithText.layerName = 'ContourLayerWithText';
254
+ ContourLayerWithText.defaultProps = defaultProps;
@@ -4,10 +4,10 @@
4
4
  // https://github.com/uber/nebula.gl/blob/8e9c2ec8d7cf4ca7050909ed826eb847d5e2cd9c/modules/layers/src/layers/selection-layer.js
5
5
  import { CompositeLayer } from 'deck.gl';
6
6
  import { polygon as turfPolygon, point as turfPoint } from '@turf/helpers';
7
- import booleanWithin from '@turf/boolean-within';
8
- import booleanContains from '@turf/boolean-contains';
9
- import booleanOverlap from '@turf/boolean-overlap';
10
- import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
7
+ import { booleanWithin } from '@turf/boolean-within';
8
+ import { booleanContains } from '@turf/boolean-contains';
9
+ import { booleanOverlap } from '@turf/boolean-overlap';
10
+ import { booleanPointInPolygon } from '@turf/boolean-point-in-polygon';
11
11
  import { ScatterplotLayer } from '@deck.gl/layers';
12
12
  import { SELECTION_TYPE } from 'nebula.gl';
13
13
  import { EditableGeoJsonLayer } from '@nebula.gl/layers';
package/src/index.js CHANGED
@@ -16,7 +16,7 @@ export { default as ScaledExpressionExtension } from './ScaledExpressionExtensio
16
16
  export { default as SelectionExtension } from './SelectionExtension/index.js';
17
17
  export { default as BitmaskLayer } from './BitmaskLayer.js';
18
18
  export { default as BitmaskLayerBeta } from './BitmaskLayerBeta.js';
19
-
19
+ export { default as ContourLayerWithText } from './ContourLayerWithText.js';
20
20
 
21
21
  export {
22
22
  TILE_SIZE, MAX_ROW_AGG, MIN_ROW_AGG,