@hema-to/regl-scatterplot 1.14.1-hemato.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.
@@ -0,0 +1,271 @@
1
+ /* eslint-env worker */
2
+ /* eslint no-restricted-globals: 1 */
3
+
4
+ const worker = function worker() {
5
+ const state = {};
6
+
7
+ /**
8
+ * Catmull-Rom interpolation
9
+ * @param {number} t - Progress value
10
+ * @param {array} p0 - First point
11
+ * @param {array} p1 - Second point
12
+ * @param {array} p2 - Third point
13
+ * @param {array} p3 - Forth point
14
+ * @return {number} Interpolated value
15
+ */
16
+ const catmullRom = (t, p0, p1, p2, p3) => {
17
+ const v0 = (p2 - p0) * 0.5;
18
+ const v1 = (p3 - p1) * 0.5;
19
+ return (
20
+ (2 * p1 - 2 * p2 + v0 + v1) * t * t * t +
21
+ (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t * t +
22
+ v0 * t +
23
+ p1
24
+ );
25
+ };
26
+
27
+ /**
28
+ * Interpolate a point with Catmull-Rom
29
+ * @param {number} t - Progress value
30
+ * @param {array} points - Key points
31
+ * @param {number} maxPointIdx - Highest point index. Same as array.length - 1
32
+ * @return {array} Interpolated point
33
+ */
34
+ const interpolatePoint = (t, points, maxPointIdx) => {
35
+ const p = maxPointIdx * t;
36
+
37
+ const intPoint = Math.floor(p);
38
+ const weight = p - intPoint;
39
+
40
+ const p0 = points[Math.max(0, intPoint - 1)];
41
+ const p1 = points[intPoint];
42
+ const p2 = points[Math.min(maxPointIdx, intPoint + 1)];
43
+ const p3 = points[Math.min(maxPointIdx, intPoint + 2)];
44
+
45
+ return [
46
+ catmullRom(weight, p0[0], p1[0], p2[0], p3[0]),
47
+ catmullRom(weight, p0[1], p1[1], p2[1], p3[1]),
48
+ ];
49
+ };
50
+
51
+ /**
52
+ * Square distance
53
+ * @param {number} x1 - First x coordinate
54
+ * @param {number} y1 - First y coordinate
55
+ * @param {number} x2 - Second x coordinate
56
+ * @param {number} y2 - Second y coordinate
57
+ * @return {number} Distance
58
+ */
59
+ const sqDist = (x1, y1, x2, y2) => (x1 - x2) ** 2 + (y1 - y2) ** 2;
60
+
61
+ /**
62
+ * Douglas Peucker square segment distance
63
+ * Implementation from https://github.com/mourner/simplify-js
64
+ * @author Vladimir Agafonkin
65
+ * @copyright Vladimir Agafonkin 2013
66
+ * @license BSD
67
+ * @param {array} p - Point
68
+ * @param {array} p1 - First boundary point
69
+ * @param {array} p2 - Second boundary point
70
+ * @return {number} Distance
71
+ */
72
+ const sqSegDist = (p, p1, p2) => {
73
+ let x = p1[0];
74
+ let y = p1[1];
75
+ let dx = p2[0] - x;
76
+ let dy = p2[1] - y;
77
+
78
+ if (dx !== 0 || dy !== 0) {
79
+ const t = ((p[0] - x) * dx + (p[1] - y) * dy) / (dx * dx + dy * dy);
80
+
81
+ if (t > 1) {
82
+ x = p2[0];
83
+ y = p2[1];
84
+ } else if (t > 0) {
85
+ x += dx * t;
86
+ y += dy * t;
87
+ }
88
+ }
89
+
90
+ dx = p[0] - x;
91
+ dy = p[1] - y;
92
+
93
+ return dx * dx + dy * dy;
94
+ };
95
+
96
+ /**
97
+ * Douglas Peucker step function
98
+ * Implementation from https://github.com/mourner/simplify-js
99
+ * @author Vladimir Agafonkin
100
+ * @copyright Vladimir Agafonkin 2013
101
+ * @license BSD
102
+ * @param {[type]} points [description]
103
+ * @param {[type]} first [description]
104
+ * @param {[type]} last [description]
105
+ * @param {[type]} tolerance [description]
106
+ * @param {[type]} simplified [description]
107
+ * @return {[type]} [description]
108
+ */
109
+ // biome-ignore lint/style/useNamingConvention: DP stands for Douglas Peucker
110
+ const simplifyDPStep = (points, first, last, tolerance, simplified) => {
111
+ let maxDist = tolerance;
112
+ let index;
113
+
114
+ for (let i = first + 1; i < last; i++) {
115
+ const dist = sqSegDist(points[i], points[first], points[last]);
116
+
117
+ if (dist > maxDist) {
118
+ index = i;
119
+ maxDist = dist;
120
+ }
121
+ }
122
+
123
+ if (maxDist > tolerance) {
124
+ if (index - first > 1) {
125
+ simplifyDPStep(points, first, index, tolerance, simplified);
126
+ }
127
+ simplified.push(points[index]);
128
+ if (last - index > 1) {
129
+ simplifyDPStep(points, index, last, tolerance, simplified);
130
+ }
131
+ }
132
+ };
133
+
134
+ /**
135
+ * Douglas Peucker. Implementation from https://github.com/mourner/simplify-js
136
+ * @author Vladimir Agafonkin
137
+ * @copyright Vladimir Agafonkin 2013
138
+ * @license BSD
139
+ * @param {array} points - List of points to be simplified
140
+ * @param {number} tolerance - Tolerance level. Points below this distance level will be ignored
141
+ * @return {array} Simplified point list
142
+ */
143
+ const simplifyDouglasPeucker = (points, tolerance) => {
144
+ const last = points.length - 1;
145
+ const simplified = [points[0]];
146
+
147
+ simplifyDPStep(points, 0, last, tolerance, simplified);
148
+ simplified.push(points[last]);
149
+
150
+ return simplified;
151
+ };
152
+
153
+ /**
154
+ * Interpolate intermediate points between key points
155
+ * @param {array} points - Fixed key points
156
+ * @param {number} options.maxIntPointsPerSegment - Maximum number of points between two key points
157
+ * @param {number} options.tolerance - Simplification tolerance
158
+ * @return {array} Interpolated points including key points
159
+ */
160
+ const interpolatePoints = (
161
+ points,
162
+ { maxIntPointsPerSegment = 100, tolerance = 0.002 } = {},
163
+ ) => {
164
+ const numPoints = points.length;
165
+ const maxPointIdx = numPoints - 1;
166
+
167
+ const maxOutPoints = maxPointIdx * maxIntPointsPerSegment + 1;
168
+ const sqTolerance = tolerance ** 2;
169
+
170
+ let outPoints = [];
171
+ let prevPoint;
172
+
173
+ // Generate interpolated points where the squared-distance between points
174
+ // is larger than sqTolerance
175
+ for (let i = 0; i < numPoints - 1; i++) {
176
+ let segmentPoints = [points[i].slice(0, 2)];
177
+ prevPoint = points[i];
178
+
179
+ for (let j = 1; j < maxIntPointsPerSegment; j++) {
180
+ const t = (i * maxIntPointsPerSegment + j) / maxOutPoints;
181
+ const intPoint = interpolatePoint(t, points, maxPointIdx);
182
+
183
+ // Check squared distance simplification
184
+ if (
185
+ sqDist(prevPoint[0], prevPoint[1], intPoint[0], intPoint[1]) >
186
+ sqTolerance
187
+ ) {
188
+ segmentPoints.push(intPoint);
189
+ prevPoint = intPoint;
190
+ }
191
+ }
192
+
193
+ // Add next key point. Needed for the simplification algorithm
194
+ segmentPoints.push(points[i + 1]);
195
+ // Simplify interpolated points using the douglas-peuckner algorithm
196
+ segmentPoints = simplifyDouglasPeucker(segmentPoints, sqTolerance);
197
+ // Add simplified points without the last key point, which is added
198
+ // anyway in the next segment
199
+ outPoints = outPoints.concat(
200
+ segmentPoints.slice(0, segmentPoints.length - 1),
201
+ );
202
+ }
203
+ outPoints.push(points[points.length - 1].slice(0, 2));
204
+
205
+ return outPoints.flat();
206
+ };
207
+
208
+ /**
209
+ * Group points by line assignment (the fifth component of a point)
210
+ * @param {array} points - Flat list of points
211
+ * @return {array} List of lists of ordered points by line
212
+ */
213
+ const groupPoints = (points) => {
214
+ const groupedPoints = {};
215
+
216
+ const isOrdered = !Number.isNaN(+points[0][5]);
217
+ // biome-ignore lint/complexity/noForEach: somehow for .. of does not work in a worker
218
+ points.forEach((point) => {
219
+ const segId = point[4];
220
+
221
+ if (!groupedPoints[segId]) {
222
+ groupedPoints[segId] = [];
223
+ }
224
+
225
+ if (isOrdered) {
226
+ groupedPoints[segId][point[5]] = point;
227
+ } else {
228
+ groupedPoints[segId].push(point);
229
+ }
230
+ });
231
+
232
+ // The filtering ensures that non-existing array entries are removed
233
+ // biome-ignore lint/complexity/noForEach: somehow for .. of does not work in a worker
234
+ Object.entries(groupedPoints).forEach((idPoints) => {
235
+ groupedPoints[idPoints[0]] = idPoints[1].filter((v) => v);
236
+ // Store the first point as the reference
237
+ groupedPoints[idPoints[0]].reference = idPoints[1][0];
238
+ });
239
+
240
+ return groupedPoints;
241
+ };
242
+
243
+ self.onmessage = function onmessage(event) {
244
+ const numPoints = event.data.points ? +event.data.points.length : 0;
245
+
246
+ if (!numPoints) {
247
+ self.postMessage({ error: new Error('No points provided') });
248
+ }
249
+
250
+ state.points = event.data.points;
251
+
252
+ const groupedPoints = groupPoints(event.data.points);
253
+
254
+ self.postMessage({
255
+ points: Object.entries(groupedPoints).reduce(
256
+ (curvePoints, idAndPoints) => {
257
+ curvePoints[idAndPoints[0]] = interpolatePoints(
258
+ idAndPoints[1],
259
+ event.data.options,
260
+ );
261
+ // Make sure the reference is passed on
262
+ curvePoints[idAndPoints[0]].reference = idAndPoints[1].reference;
263
+ return curvePoints;
264
+ },
265
+ {},
266
+ ),
267
+ });
268
+ };
269
+ };
270
+
271
+ export default worker;
@@ -0,0 +1,23 @@
1
+ import { createWorker } from '@flekschas/utils';
2
+ import workerFn from './spline-curve-worker.js';
3
+
4
+ const createSplineCurve = (
5
+ points,
6
+ options = { tolerance: 0.002, maxIntPointsPerSegment: 100 },
7
+ ) =>
8
+ new Promise((resolve, reject) => {
9
+ const worker = createWorker(workerFn);
10
+
11
+ worker.onmessage = (e) => {
12
+ if (e.data.error) {
13
+ reject(e.data.error);
14
+ } else {
15
+ resolve(e.data.points);
16
+ }
17
+ worker.terminate();
18
+ };
19
+
20
+ worker.postMessage({ points, options });
21
+ });
22
+
23
+ export default createSplineCurve;
package/src/types.d.ts ADDED
@@ -0,0 +1,306 @@
1
+ type Hex = string;
2
+ type Rgb = [number, number, number];
3
+ type Rgba = [number, number, number, number];
4
+
5
+ type Color = Hex | Rgb | Rgba;
6
+ type ColorMap = Color | Color[];
7
+
8
+ type Category = 'category' | 'value1' | 'valueA' | 'valueZ' | 'z';
9
+ type Value = 'value' | 'value2' | 'valueB' | 'valueW' | 'w';
10
+ type DataEncoding = Category | Value;
11
+ type PointDataEncoding = DataEncoding | 'inherit' | 'segment';
12
+
13
+ type KeyAction = 'lasso' | 'rotate' | 'merge' | 'intersect';
14
+ type KeyMap = Record<'alt' | 'cmd' | 'ctrl' | 'meta' | 'shift', KeyAction>;
15
+
16
+ type MouseMode = 'panZoom' | 'lasso' | 'rotate';
17
+
18
+ type PointScaleMode = 'constant' | 'asinh' | 'linear';
19
+
20
+ // biome-ignore lint/style/useNamingConvention: ZWData are three words, z, w, and data
21
+ type ZWDataType = 'continuous' | 'categorical';
22
+
23
+ // biome-ignore lint/suspicious/noExplicitAny: Untyped external library
24
+ type Camera2D = any; // Needs to be typed at some point
25
+ type Scale = import('d3-scale').ScaleContinuousNumeric<number, number>;
26
+
27
+ type PointsObject = {
28
+ x: ArrayLike<number>;
29
+ y: ArrayLike<number>;
30
+ line?: ArrayLike<number>;
31
+ lineOrder?: ArrayLike<number>;
32
+ } & {
33
+ [Key in Category | Value]?: ArrayLike<number>;
34
+ };
35
+
36
+ export type Points = number[][] | PointsObject;
37
+
38
+ type PointOptions = {
39
+ color: ColorMap;
40
+ colorActive: Color;
41
+ colorHover: Color;
42
+ outlineWidth: number;
43
+ size: number | number[];
44
+ sizeSelected: number;
45
+ };
46
+
47
+ type PointConnectionOptions = {
48
+ color: ColorMap;
49
+ colorActive: Color;
50
+ colorHover: Color;
51
+ opacity: number | number[];
52
+ opacityActive: number;
53
+ size: number | number[];
54
+ sizeActive: number;
55
+ maxIntPointsPerSegment: number;
56
+ tolerance: number;
57
+ // Nullifiable
58
+ colorBy: null | PointDataEncoding;
59
+ opacityBy: null | PointDataEncoding;
60
+ sizeBy: null | PointDataEncoding;
61
+ };
62
+
63
+ type LassoOptions = {
64
+ color: Color;
65
+ lineWidth: number;
66
+ minDelay: number;
67
+ minDist: number;
68
+ mode: 'intersect' | 'merge' | 'remove' | null;
69
+ clearEvent: 'lassoEnd' | 'deselect';
70
+ initiator: boolean;
71
+ initiatorParentElement: HTMLElement;
72
+ onLongPress: boolean;
73
+ longPressTime: number;
74
+ longPressAfterEffectTime: number;
75
+ longPressEffectDelay: number;
76
+ longPressRevertEffectTime: number;
77
+ };
78
+
79
+ type CameraOptions = {
80
+ target: [number, number];
81
+ distance: number;
82
+ rotation: number;
83
+ view: Float32Array;
84
+ };
85
+
86
+ // biome-ignore lint/correctness/noUnusedVariables: Imported from ./index.js
87
+ type Rect = {
88
+ x: number;
89
+ y: number;
90
+ width: number;
91
+ height: number;
92
+ };
93
+
94
+ interface BaseAnnotation {
95
+ lineColor?: Color;
96
+ lineWidth?: number;
97
+ }
98
+
99
+ // biome-ignore lint/style/useNamingConvention: HLine stands for HorizontalLine
100
+ interface AnnotationHLine extends BaseAnnotation {
101
+ y: number;
102
+ x1?: number;
103
+ x2?: number;
104
+ }
105
+
106
+ // biome-ignore lint/style/useNamingConvention: HLine stands for VerticalLine
107
+ interface AnnotationVLine extends BaseAnnotation {
108
+ x: number;
109
+ y1?: number;
110
+ y2?: number;
111
+ }
112
+
113
+ interface AnnotationDomRect extends BaseAnnotation {
114
+ x: number;
115
+ y: number;
116
+ width: number;
117
+ height: number;
118
+ }
119
+
120
+ interface AnnotationRect extends BaseAnnotation {
121
+ x1: number;
122
+ y1: number;
123
+ x2: number;
124
+ y2: number;
125
+ }
126
+
127
+ interface AnnotationPolygon extends BaseAnnotation {
128
+ vertices: [number, number][];
129
+ }
130
+
131
+ // biome-ignore lint/correctness/noUnusedVariables: Imported from ./index.js
132
+ type Annotation =
133
+ | AnnotationHLine
134
+ | AnnotationVLine
135
+ | AnnotationDomRect
136
+ | AnnotationRect
137
+ | AnnotationPolygon;
138
+
139
+ interface BaseOptions {
140
+ backgroundColor: Color;
141
+ deselectOnDblClick: boolean;
142
+ deselectOnEscape: boolean;
143
+ actionKeyMap: KeyMap;
144
+ keyMap: KeyMap;
145
+ mouseMode: MouseMode;
146
+ showPointConnections: boolean;
147
+ showReticle: boolean;
148
+ reticleColor: Color;
149
+ opacity: number | number[];
150
+ opacityByDensityFill: number;
151
+ opacityInactiveMax: number;
152
+ opacityInactiveScale: number;
153
+ height: 'auto' | number;
154
+ width: 'auto' | number;
155
+ gamma: number;
156
+ aspectRatio: number;
157
+ annotationLineColor: Color;
158
+ annotationLineWidth: number;
159
+ // biome-ignore lint/style/useNamingConvention: HVLine stands for HorizontalVerticalLine
160
+ annotationHVLineLimit: number;
161
+ // Nullifiable
162
+ backgroundImage: null | import('regl').Texture2D | string;
163
+ colorBy: null | DataEncoding;
164
+ sizeBy: null | DataEncoding;
165
+ opacityBy: null | DataEncoding;
166
+ xScale: null | Scale;
167
+ yScale: null | Scale;
168
+ pointScaleMode: PointScaleMode;
169
+ cameraIsFixed: boolean;
170
+ antiAliasing: number;
171
+ pixelAligned: boolean;
172
+ }
173
+
174
+ // biome-ignore lint/style/useNamingConvention: KDBush is a library name
175
+ export interface CreateKDBushOptions {
176
+ node: number;
177
+ useWorker: boolean;
178
+ }
179
+
180
+ /**
181
+ * Helper type. Adds a prefix to keys of Options.
182
+ *
183
+ * type A = { a: number; b: string };
184
+ * WithPrefix<'myPrefix', A> === { myPrefixA: number, myPrefixB: string };
185
+ */
186
+ type WithPrefix<
187
+ Name extends string,
188
+ Options extends Record<string, unknown>,
189
+ > = {
190
+ [Key in keyof Options as `${Name}${Capitalize<string & Key>}`]: Options[Key];
191
+ };
192
+
193
+ export type Settable = BaseOptions &
194
+ WithPrefix<'point', PointOptions> &
195
+ WithPrefix<'pointConnection', PointConnectionOptions> &
196
+ WithPrefix<'lasso', LassoOptions> &
197
+ WithPrefix<'camera', CameraOptions>;
198
+
199
+ export type RendererOptions = {
200
+ regl: import('regl').Regl;
201
+ canvas: HTMLCanvasElement;
202
+ gamma: number;
203
+ };
204
+
205
+ export type Properties = {
206
+ renderer: ReturnType<typeof import('./renderer').createRenderer>;
207
+ canvas: HTMLCanvasElement;
208
+ regl: import('regl').Regl;
209
+ syncEvents: boolean;
210
+ version: string;
211
+ lassoInitiatorElement: HTMLElement;
212
+ lassoLongPressIndicatorParentElement: HTMLElement;
213
+ camera: Camera2D;
214
+ performanceMode: boolean;
215
+ renderPointsAsSquares: boolean;
216
+ disableAlphaBlending: boolean;
217
+ opacityByDensityDebounceTime: number;
218
+ spatialIndex: ArrayBuffer;
219
+ spatialIndexUseWorker: undefined | boolean;
220
+ points: [number, number][];
221
+ pointsInView: number[];
222
+ isDestroyed: boolean;
223
+ isPointsDrawn: boolean;
224
+ isPointsFiltered: boolean;
225
+ hoveredPoint: number;
226
+ filteredPoints: number[];
227
+ selectedPoints: number[];
228
+ } & Settable;
229
+
230
+ // Options for plot.{draw, select, hover}
231
+ export interface ScatterplotMethodOptions {
232
+ draw: Partial<{
233
+ transition: boolean;
234
+ transitionDuration: number;
235
+ transitionEasing: (t: number) => number;
236
+ preventFilterReset: boolean;
237
+ hover: number;
238
+ select: number | number[];
239
+ filter: number | number[];
240
+ zDataType: ZWDataType;
241
+ wDataType: ZWDataType;
242
+ spatialIndex: ArrayBuffer;
243
+ }>;
244
+ hover: Partial<{
245
+ showReticleOnce: boolean;
246
+ preventEvent: boolean;
247
+ }>;
248
+ select: Partial<{
249
+ merge: boolean;
250
+ remove: boolean;
251
+ preventEvent: boolean;
252
+ }>;
253
+ filter: Partial<{
254
+ preventEvent: boolean;
255
+ }>;
256
+ preventEvent: Partial<{
257
+ preventEvent: boolean;
258
+ }>;
259
+ zoomToPoints: Partial<{
260
+ padding: number;
261
+ transition: boolean;
262
+ transitionDuration: number;
263
+ transitionEasing: (t: number) => number;
264
+ }>;
265
+ zoomToArea: Partial<{
266
+ transition: boolean;
267
+ transitionDuration: number;
268
+ transitionEasing: (t: number) => number;
269
+ }>;
270
+ zoomToLocation: Partial<{
271
+ transition: boolean;
272
+ transitionDuration: number;
273
+ transitionEasing: (t: number) => number;
274
+ }>;
275
+ export: Partial<{
276
+ scale: number;
277
+ antiAliasing: number;
278
+ pixelAligned: boolean;
279
+ }>;
280
+ }
281
+
282
+ export type Events = import('pub-sub-es').Event<
283
+ | 'init'
284
+ | 'destroy'
285
+ | 'backgroundImageReady'
286
+ | 'deselect'
287
+ | 'unfilter'
288
+ | 'lassoStart'
289
+ | 'transitionStart'
290
+ | 'pointConnectionsDraw',
291
+ undefined
292
+ > &
293
+ import('pub-sub-es').Event<
294
+ 'lassoEnd' | 'lassoExtend',
295
+ { coordinates: number[] }
296
+ > &
297
+ import('pub-sub-es').Event<'pointOver' | 'pointOut', number> &
298
+ import('pub-sub-es').Event<'select' | 'focus', { points: number[] }> &
299
+ import('pub-sub-es').Event<'points', { points: number[][] }> &
300
+ import('pub-sub-es').Event<'transitionEnd', import('regl').Regl> &
301
+ import('pub-sub-es').Event<
302
+ 'view' | 'draw' | 'drawing',
303
+ Pick<Properties, 'camera' | 'xScale' | 'yScale'> & {
304
+ view: Properties['cameraView'];
305
+ }
306
+ >;