@vitessce/scatterplot 2.0.3-beta.0 → 3.0.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.
Files changed (73) hide show
  1. package/dist/deflate-24a61b7a.js +13 -0
  2. package/dist/index-d78d4568.js +142036 -0
  3. package/dist/index.js +15 -5
  4. package/dist/jpeg-19791032.js +840 -0
  5. package/dist/lerc-867fd2ef.js +2014 -0
  6. package/dist/lzw-40577136.js +128 -0
  7. package/dist/packbits-eab43a40.js +30 -0
  8. package/dist/pako.esm-68f84e2a.js +4022 -0
  9. package/dist/raw-66364181.js +12 -0
  10. package/dist/webimage-0fe27785.js +32 -0
  11. package/dist-tsc/EmptyMessage.d.ts +2 -0
  12. package/dist-tsc/EmptyMessage.d.ts.map +1 -0
  13. package/dist-tsc/Scatterplot.d.ts +10 -0
  14. package/dist-tsc/Scatterplot.d.ts.map +1 -0
  15. package/{dist → dist-tsc}/Scatterplot.js +12 -2
  16. package/dist-tsc/ScatterplotOptions.d.ts +2 -0
  17. package/dist-tsc/ScatterplotOptions.d.ts.map +1 -0
  18. package/{dist → dist-tsc}/ScatterplotOptions.js +13 -7
  19. package/dist-tsc/ScatterplotTooltipSubscriber.d.ts +2 -0
  20. package/dist-tsc/ScatterplotTooltipSubscriber.d.ts.map +1 -0
  21. package/dist-tsc/index.d.ts +6 -0
  22. package/dist-tsc/index.d.ts.map +1 -0
  23. package/dist-tsc/index.js +5 -0
  24. package/dist-tsc/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.d.ts +82 -0
  25. package/dist-tsc/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.d.ts.map +1 -0
  26. package/{dist → dist-tsc}/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +9 -4
  27. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.d.ts +4 -0
  28. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.d.ts.map +1 -0
  29. package/{dist → dist-tsc}/shared-spatial-scatterplot/ToolMenu.js +32 -7
  30. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.test.d.ts +2 -0
  31. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.test.d.ts.map +1 -0
  32. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.test.js +23 -0
  33. package/dist-tsc/shared-spatial-scatterplot/cursor.d.ts +4 -0
  34. package/dist-tsc/shared-spatial-scatterplot/cursor.d.ts.map +1 -0
  35. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.d.ts +3 -0
  36. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.d.ts.map +1 -0
  37. package/{dist → dist-tsc}/shared-spatial-scatterplot/dynamic-opacity.js +1 -1
  38. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.test.d.ts +2 -0
  39. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.test.d.ts.map +1 -0
  40. package/{dist → dist-tsc}/shared-spatial-scatterplot/dynamic-opacity.test.js +1 -1
  41. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.d.ts +13 -0
  42. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.d.ts.map +1 -0
  43. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.test.d.ts +2 -0
  44. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.test.d.ts.map +1 -0
  45. package/{dist → dist-tsc}/shared-spatial-scatterplot/force-collide-rects.test.js +1 -1
  46. package/dist-tsc/shared-spatial-scatterplot/index.d.ts +6 -0
  47. package/dist-tsc/shared-spatial-scatterplot/index.d.ts.map +1 -0
  48. package/dist-tsc/shared-spatial-scatterplot/index.js +5 -0
  49. package/dist-tsc/shared-spatial-scatterplot/quadtree.d.ts +10 -0
  50. package/dist-tsc/shared-spatial-scatterplot/quadtree.d.ts.map +1 -0
  51. package/{dist → dist-tsc}/shared-spatial-scatterplot/quadtree.js +1 -1
  52. package/package.json +25 -13
  53. package/src/EmptyMessage.js +11 -0
  54. package/src/Scatterplot.js +396 -0
  55. package/src/ScatterplotOptions.js +269 -0
  56. package/src/ScatterplotTooltipSubscriber.js +38 -0
  57. package/src/index.js +11 -0
  58. package/src/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +280 -0
  59. package/src/shared-spatial-scatterplot/ToolMenu.js +143 -0
  60. package/src/shared-spatial-scatterplot/ToolMenu.test.jsx +26 -0
  61. package/src/shared-spatial-scatterplot/cursor.js +23 -0
  62. package/src/shared-spatial-scatterplot/dynamic-opacity.js +58 -0
  63. package/src/shared-spatial-scatterplot/dynamic-opacity.test.js +33 -0
  64. package/src/shared-spatial-scatterplot/force-collide-rects.js +189 -0
  65. package/src/shared-spatial-scatterplot/force-collide-rects.test.js +72 -0
  66. package/src/shared-spatial-scatterplot/index.js +8 -0
  67. package/src/shared-spatial-scatterplot/quadtree.js +27 -0
  68. package/dist/shared-spatial-scatterplot/ToolMenu.test.js +0 -16
  69. package/dist/shared-spatial-scatterplot/index.js +0 -5
  70. /package/{dist → dist-tsc}/EmptyMessage.js +0 -0
  71. /package/{dist → dist-tsc}/ScatterplotTooltipSubscriber.js +0 -0
  72. /package/{dist → dist-tsc}/shared-spatial-scatterplot/cursor.js +0 -0
  73. /package/{dist → dist-tsc}/shared-spatial-scatterplot/force-collide-rects.js +0 -0
@@ -0,0 +1,143 @@
1
+ import React from 'react';
2
+ import clsx from 'clsx';
3
+ import { SELECTION_TYPE } from '@vitessce/gl';
4
+ import { PointerIconSVG, SelectLassoIconSVG } from '@vitessce/icons';
5
+ import { makeStyles } from '@material-ui/core';
6
+ import { CenterFocusStrong } from '@material-ui/icons';
7
+
8
+ const useStyles = makeStyles(() => ({
9
+ button: {
10
+ display: 'inline-flex',
11
+ '&:active': {
12
+ opacity: '.65',
13
+ extend: 'iconClicked',
14
+ },
15
+ },
16
+ tool: {
17
+ position: 'absolute',
18
+ display: 'inline',
19
+ zIndex: '1000',
20
+ opacity: '.65',
21
+ color: 'black',
22
+ '&:hover': {
23
+ opacity: '.90',
24
+ },
25
+ },
26
+ iconClicked: {
27
+ // Styles for the clicked state
28
+ boxShadow: 'none',
29
+ transform: 'scale(0.98)', // make the button slightly smaller
30
+ },
31
+ icon: {
32
+ // btn btn-outline-secondary mr-2 icon
33
+ padding: '0',
34
+ height: '2em',
35
+ width: '2em',
36
+ backgroundColor: 'white',
37
+
38
+ display: 'inline-block',
39
+ fontWeight: '400',
40
+ textAlign: 'center',
41
+ verticalAlign: 'middle',
42
+ cursor: 'pointer',
43
+ userSelect: 'none',
44
+ border: '1px solid #6c757d',
45
+ fontSize: '16px',
46
+ lineHeight: '1.5',
47
+ borderRadius: '4px',
48
+ transition: 'color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out',
49
+ color: '#6c757d',
50
+
51
+ marginRight: '8px',
52
+
53
+ '& > svg': {
54
+ verticalAlign: 'middle',
55
+ color: 'black',
56
+ },
57
+ '&:active': {
58
+ extend: 'iconClicked',
59
+ },
60
+
61
+ },
62
+ toolActive: {
63
+ // active
64
+ color: '#fff',
65
+ backgroundColor: '#6c757d',
66
+ borderColor: '#6c757d',
67
+ boxShadow: '0 0 0 3px rgba(108, 117, 125, 0.5)',
68
+ },
69
+ }));
70
+
71
+ export function IconTool(props) {
72
+ const {
73
+ alt, onClick, isActive, children,
74
+ } = props;
75
+ const classes = useStyles();
76
+ return (
77
+ <button
78
+ className={clsx(classes.icon, { [classes.toolActive]: isActive })}
79
+ onClick={onClick}
80
+ type="button"
81
+ title={alt}
82
+ >
83
+ {children}
84
+ </button>
85
+ );
86
+ }
87
+
88
+ export function IconButton(props) {
89
+ const {
90
+ alt, onClick, children,
91
+ } = props;
92
+ const classes = useStyles();
93
+ return (
94
+ <button
95
+ className={clsx(classes.icon, classes.button)}
96
+ onClick={onClick}
97
+ type="button"
98
+ title={alt}
99
+ >
100
+ {children}
101
+ </button>
102
+ );
103
+ }
104
+
105
+ export default function ToolMenu(props) {
106
+ const {
107
+ setActiveTool,
108
+ activeTool,
109
+ visibleTools = { pan: true, selectLasso: true },
110
+ recenterOnClick = () => {},
111
+ } = props;
112
+ const classes = useStyles();
113
+
114
+ const onRecenterButtonCLick = () => {
115
+ recenterOnClick();
116
+ };
117
+
118
+ return (
119
+ <div className={classes.tool}>
120
+ {visibleTools.pan && (
121
+ <IconTool
122
+ alt="pointer tool"
123
+ onClick={() => setActiveTool(null)}
124
+ isActive={activeTool === null}
125
+ ><PointerIconSVG />
126
+ </IconTool>
127
+ )}
128
+ {visibleTools.selectLasso ? (
129
+ <IconTool
130
+ alt="select lasso"
131
+ onClick={() => setActiveTool(SELECTION_TYPE.POLYGON)}
132
+ isActive={activeTool === SELECTION_TYPE.POLYGON}
133
+ ><SelectLassoIconSVG />
134
+ </IconTool>
135
+ ) : null}
136
+ <IconButton
137
+ alt="click to recenter"
138
+ onClick={() => onRecenterButtonCLick()}
139
+ ><CenterFocusStrong />
140
+ </IconButton>
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,26 @@
1
+ import '@testing-library/jest-dom';
2
+ import { cleanup, render } from '@testing-library/react';
3
+ import { afterEach, expect } from 'vitest';
4
+ import React from 'react';
5
+
6
+ import { IconTool, IconButton } from './ToolMenu.js';
7
+
8
+ afterEach(() => {
9
+ cleanup();
10
+ });
11
+
12
+ describe('ToolMenu.js', () => {
13
+ describe('<IconTool />', () => {
14
+ it('renders with title attribute', () => {
15
+ const { container } = render(<IconTool isActive alt="Lasso" />);
16
+ expect(container.querySelectorAll('[title="Lasso"]').length).toEqual(1);
17
+ });
18
+ });
19
+
20
+ describe('<IconButton />', () => {
21
+ it('renders with title attribute', () => {
22
+ const { container } = render(<IconButton alt="click to recenter" />);
23
+ expect(container.querySelectorAll('[title="click to recenter"]').length).toEqual(1);
24
+ });
25
+ });
26
+ });
@@ -0,0 +1,23 @@
1
+ export const getCursorWithTool = () => 'crosshair';
2
+ export const getCursor = interactionState => (interactionState.isDragging
3
+ ? 'grabbing' : 'default'
4
+ );
5
+
6
+ export function getOnHoverCallback(obsIndex, setObsHighlight, setComponentHover) {
7
+ return (info) => {
8
+ // Notify the parent component that its child component is
9
+ // the "hover source".
10
+ if (setComponentHover) {
11
+ setComponentHover();
12
+ }
13
+ if (info.index) {
14
+ const obsId = obsIndex[info.index];
15
+ if (setObsHighlight) {
16
+ setObsHighlight(obsId);
17
+ }
18
+ } else if (setObsHighlight) {
19
+ // Clear the currently-hovered cell info by passing null.
20
+ setObsHighlight(null);
21
+ }
22
+ };
23
+ }
@@ -0,0 +1,58 @@
1
+ import { deck } from '@vitessce/gl';
2
+ import { clamp } from 'lodash-es';
3
+
4
+ // Reference: https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
5
+ // Reference: https://observablehq.com/@bmschmidt/dot-density-election-maps-with-webgl
6
+ export function getPointSizeDevicePixels(devicePixelRatio, zoom, xRange, yRange, width, height) {
7
+ // Size of a point, in units of the diagonal axis.
8
+ const pointSize = 0.0005;
9
+ // Point size maximum, in screen pixels.
10
+ const pointScreenSizeMax = 10;
11
+
12
+ // Point size minimum, in screen pixels.
13
+ const pointScreenSizeMin = 1 / devicePixelRatio;
14
+
15
+ const scaleFactor = 2 ** zoom;
16
+ const xAxisRange = 2.0 / ((xRange * scaleFactor) / width);
17
+ const yAxisRange = 2.0 / ((yRange * scaleFactor) / height);
18
+
19
+ // The diagonal screen size as a fraction of the current diagonal axis range,
20
+ // then converted to device pixels.
21
+ const diagonalScreenSize = Math.sqrt((width ** 2) + (height ** 2));
22
+ const diagonalAxisRange = Math.sqrt((xAxisRange ** 2) + (yAxisRange ** 2));
23
+ const diagonalFraction = pointSize / diagonalAxisRange;
24
+ const deviceSize = diagonalFraction * diagonalScreenSize;
25
+
26
+ const pointSizeDevicePixels = clamp(
27
+ deviceSize,
28
+ pointScreenSizeMin,
29
+ pointScreenSizeMax,
30
+ );
31
+ return pointSizeDevicePixels;
32
+ }
33
+
34
+ // Reference: https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
35
+ export function getPointOpacity(zoom, xRange, yRange, width, height, numCells, avgFillDensity) {
36
+ const N = numCells;
37
+ const [minX, minY, maxX, maxY] = new deck.OrthographicView({ zoom }).makeViewport({
38
+ height,
39
+ width,
40
+ viewState: { zoom, target: [0, 0, 0] },
41
+ }).getBounds();
42
+ const X = maxY - minY;
43
+ const Y = maxX - minX;
44
+ const X0 = xRange;
45
+ const Y0 = yRange;
46
+ const W = width;
47
+ const H = height;
48
+
49
+ let rho = avgFillDensity;
50
+ if (!rho) {
51
+ rho = Math.min(1, 1 / (10 ** (Math.log10(N) - 3)));
52
+ }
53
+ // p in the calculation is the pixel length/width of a given point, which for us is 1
54
+ // so it does not factor into our calculation here.
55
+ const alpha = ((rho * W * H) / N) * (Y0 / Y) * (X0 / X);
56
+ const pointOpacity = clamp(alpha, 1.01 / 255, 1.0);
57
+ return pointOpacity;
58
+ }
@@ -0,0 +1,33 @@
1
+ import { getPointSizeDevicePixels, getPointOpacity } from './dynamic-opacity.js';
2
+
3
+ describe('dynamic-opacity.js', () => {
4
+ describe('getPointSizeDevicePixels', () => {
5
+ it('calculates point size', () => {
6
+ const devicePixelRatio = 2.0;
7
+ const zoom = null;
8
+ const xRange = 20;
9
+ const yRange = 18;
10
+ const width = 1000;
11
+ const height = 650;
12
+ const pointSize = getPointSizeDevicePixels(
13
+ devicePixelRatio, zoom, xRange, yRange, width, height,
14
+ );
15
+ expect(pointSize).toBeCloseTo(0.5);
16
+ });
17
+ });
18
+ describe('getPointOpacity', () => {
19
+ it('calculates point opacity', () => {
20
+ const zoom = null;
21
+ const width = 1000;
22
+ const height = 650;
23
+ const xRange = 20;
24
+ const yRange = 18;
25
+ const numCells = 500000;
26
+ const avgFillDensity = undefined;
27
+ const pointOpacity = getPointOpacity(
28
+ zoom, xRange, yRange, width, height, numCells, avgFillDensity,
29
+ );
30
+ expect(pointOpacity).toBeCloseTo(0.005);
31
+ });
32
+ });
33
+ });
@@ -0,0 +1,189 @@
1
+ /* eslint-disable no-plusplus */
2
+ /* eslint-disable no-param-reassign */
3
+ import { quadtree } from 'd3-quadtree';
4
+
5
+ /**
6
+ * Returns a closure that returns a constant value.
7
+ */
8
+ function constant(v) {
9
+ return (() => v);
10
+ }
11
+
12
+ /**
13
+ * Adds a tiny bit of randomness to a number.
14
+ */
15
+ function jiggle(v) {
16
+ return v + (Math.random() - 0.5) * 1e-6;
17
+ }
18
+
19
+ /**
20
+ * A force function to be used with d3.forceSimulation.
21
+ * This has been adapted for use here, with comments explaining each part.
22
+ * Reference: https://bl.ocks.org/cmgiven/547658968d365bcc324f3e62e175709b
23
+ */
24
+ export function forceCollideRects() {
25
+ // D3 implements things with function prototypes rather than classes.
26
+ // Pretend these variables are the "instance members" of a class.
27
+ // Note that this function actually returns the internal force() function,
28
+ // but that the force() function is a closure with access to these instance members.
29
+
30
+ let nodes;
31
+ let masses;
32
+ let strength = 1;
33
+ let iterations = 1;
34
+
35
+ let sizes;
36
+ let size = constant([0, 0]);
37
+
38
+ // Given a node, return the center point along the x-axis.
39
+ function xCenter(d) {
40
+ return d.x + d.vx + sizes[d.index][0] / 2;
41
+ }
42
+
43
+ // Given a node, return the center point along the y-axis.
44
+ function yCenter(d) {
45
+ return d.y + d.vy + sizes[d.index][1] / 2;
46
+ }
47
+
48
+ // Given a quadtree node, initialize its .size property.
49
+ function prepare(quad) {
50
+ if (quad.data) {
51
+ // This is a leaf node, so we set quad.size to the node's size.
52
+ // (No need to compute the max of internal nodes,
53
+ // since leaf nodes do not have any internal nodes).
54
+ quad.size = sizes[quad.data.index];
55
+ } else {
56
+ quad.size = [0, 0];
57
+ // Internal nodes of the quadtree are represented
58
+ // as four-element arrays in left-to-right, top-to-bottom order.
59
+ // Here, we are setting quad.size to [maxWidth, maxHeight]
60
+ // among the internal nodes of this current `quad` node.
61
+ for (let i = 0; i < 4; i++) {
62
+ if (quad[i] && quad[i].size) {
63
+ quad.size[0] = Math.max(quad.size[0], quad[i].size[0]);
64
+ quad.size[1] = Math.max(quad.size[1], quad[i].size[1]);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ function force() {
71
+ let node;
72
+ let nodeSize;
73
+ let nodeMass;
74
+ let xi;
75
+ let yi;
76
+
77
+ // Create a quadtree based on node center points.
78
+ // Initialize each quadtree node's .size property by calling
79
+ // the prepare() function on each quadtree node.
80
+ const tree = quadtree(nodes, xCenter, yCenter).visitAfter(prepare);
81
+
82
+ // Update the .vx and .vy properties of both `node` and `data`
83
+ // (the current node pair).
84
+ function apply(quad, x0, y0, x1, y1) {
85
+ // `quad` is a quadtree node.
86
+ const { data } = quad;
87
+ const xSize = (nodeSize[0] + quad.size[0]) / 2;
88
+ const ySize = (nodeSize[1] + quad.size[1]) / 2;
89
+
90
+ if (data && data.index > node.index) {
91
+ // This is a leaf node because `data` is defined.
92
+ // `x` is the difference in x centers between `node` and `data`.
93
+ // `y` is the difference in y centers between `node` and `data`.
94
+ let x = jiggle(xi - xCenter(data));
95
+ let y = jiggle(yi - yCenter(data));
96
+ const xd = Math.abs(x) - xSize;
97
+ const yd = Math.abs(y) - ySize;
98
+
99
+ // If `xd` and `yd` is less than zero,
100
+ // then there is an overlap between the two nodes.
101
+ if (xd < 0 && yd < 0) {
102
+ const l = Math.sqrt(x * x + y * y);
103
+ const m = masses[data.index] / (nodeMass + masses[data.index]);
104
+
105
+ // We move the nodes either in the x or y direction.
106
+ // Nodes are moved proportionally to:
107
+ // their distance apart (`l`), their amount of overlap (`xd` or `yd`), their masses (`m`),
108
+ // and the strength parameter (`strength`).
109
+ if (Math.abs(xd) < Math.abs(yd)) {
110
+ node.vx -= (x *= xd / l * strength) * m;
111
+ data.vx += x * (1 - m);
112
+ } else {
113
+ node.vy -= (y *= yd / l * strength) * m;
114
+ data.vy += y * (1 - m);
115
+ }
116
+ }
117
+ // When the quadtree.visit callback returns _true_ for a node,
118
+ // then the node's children will _not_ be visited.
119
+ return x0 > xi + xSize || x1 < xi - xSize || y0 > yi + ySize || y1 < yi - ySize;
120
+ }
121
+ return false;
122
+ }
123
+
124
+ function iterate() {
125
+ // On every iteration, use the `apply` function to visit every node
126
+ // which has an index greater than the current node's index,
127
+ // (visiting every node pair).
128
+ for (let j = 0; j < nodes.length; j++) {
129
+ node = nodes[j];
130
+ nodeSize = sizes[j];
131
+ nodeMass = masses[j];
132
+ xi = xCenter(node);
133
+ yi = yCenter(node);
134
+
135
+ tree.visit(apply);
136
+ }
137
+ }
138
+
139
+ // Do the specified number of iterations.
140
+ for (let i = 0; i < iterations; i++) {
141
+ iterate();
142
+ }
143
+ }
144
+
145
+ // The "constructor".
146
+ // Takes a list of nodes as input.
147
+ force.initialize = (v) => {
148
+ nodes = v;
149
+ // Get the size [w, h] of each node using the size getter function.
150
+ sizes = nodes.map(size);
151
+ // Get the mass of each node,
152
+ // which is the sum of its horizontal and vertical edge lengths.
153
+ masses = sizes.map(d => d[0] + d[1]);
154
+ };
155
+
156
+ // Set the number of iterations.
157
+ // If no value is provided as a parameter, this acts as a getter function.
158
+ force.iterations = (...v) => {
159
+ if (v.length) {
160
+ iterations = +v[0];
161
+ return force;
162
+ }
163
+ return iterations;
164
+ };
165
+
166
+ // Set the strength value.
167
+ // If no value is provided as a parameter, this acts as a getter function.
168
+ force.strength = (...v) => {
169
+ if (v.length) {
170
+ strength = +v[0];
171
+ return force;
172
+ }
173
+ return strength;
174
+ };
175
+
176
+ // Set the size function.
177
+ // The size function takes a node as a parameter and returns its size.
178
+ // If no size function is provided as a parameter, this acts as a getter function.
179
+ force.size = (...v) => {
180
+ if (v.length) {
181
+ size = (typeof v[0] === 'function' ? v[0] : constant(v[0]));
182
+ return force;
183
+ }
184
+ return size;
185
+ };
186
+
187
+ // Returns the force closure.
188
+ return force;
189
+ }
@@ -0,0 +1,72 @@
1
+ import { forceSimulation } from 'd3-force';
2
+ import { forceCollideRects } from './force-collide-rects.js';
3
+
4
+ describe('force-collide-rects.js', () => {
5
+ describe('forceCollideRects', () => {
6
+ it('can be initialized with a size function', () => {
7
+ const collisionForce = forceCollideRects()
8
+ .size(d => ([d.width, d.height]));
9
+
10
+ const size = collisionForce.size();
11
+ const [w, h] = size({ width: 2, height: 3 });
12
+ expect(w).toEqual(2);
13
+ expect(h).toEqual(3);
14
+ });
15
+
16
+ it('cannot prevent a collision of rects after few iterations', () => {
17
+ const collisionForce = forceCollideRects()
18
+ .size(d => ([d.width, d.height]));
19
+
20
+ const nodes = [
21
+ {
22
+ label: 'A', width: 100, height: 100, x: 2, y: 2,
23
+ },
24
+ {
25
+ label: 'B', width: 100, height: 100, x: 3, y: 3,
26
+ },
27
+ {
28
+ label: 'C', width: 100, height: 100, x: 3, y: 2,
29
+ },
30
+ ];
31
+
32
+ forceSimulation()
33
+ .nodes(nodes)
34
+ .force('collision', collisionForce)
35
+ .tick(2);
36
+
37
+ const collisionAB = (
38
+ Math.abs(nodes[0].x - nodes[1].x) < 100
39
+ && Math.abs(nodes[0].y - nodes[1].y) < 100
40
+ );
41
+ expect(collisionAB).toEqual(true);
42
+ });
43
+
44
+ it('can prevent a collision of rects after many iterations', () => {
45
+ const collisionForce = forceCollideRects()
46
+ .size(d => ([d.width, d.height]));
47
+
48
+ const nodes = [
49
+ {
50
+ label: 'A', width: 100, height: 100, x: 2, y: 2,
51
+ },
52
+ {
53
+ label: 'B', width: 100, height: 100, x: 3, y: 3,
54
+ },
55
+ {
56
+ label: 'C', width: 100, height: 100, x: 3, y: 2,
57
+ },
58
+ ];
59
+
60
+ forceSimulation()
61
+ .nodes(nodes)
62
+ .force('collision', collisionForce)
63
+ .tick(50);
64
+
65
+ const collisionAB = (
66
+ Math.abs(nodes[0].x - nodes[1].x) < 100
67
+ && Math.abs(nodes[0].y - nodes[1].y) < 100
68
+ );
69
+ expect(collisionAB).toEqual(false);
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,8 @@
1
+ export { default as AbstractSpatialOrScatterplot } from './AbstractSpatialOrScatterplot.js';
2
+ export { createQuadTree } from './quadtree.js';
3
+ export { forceCollideRects } from './force-collide-rects.js';
4
+ export { getOnHoverCallback } from './cursor.js';
5
+ export {
6
+ getPointSizeDevicePixels,
7
+ getPointOpacity,
8
+ } from './dynamic-opacity.js';
@@ -0,0 +1,27 @@
1
+ import { quadtree } from 'd3-quadtree';
2
+ import { range } from 'lodash-es';
3
+
4
+ /**
5
+ * Create a d3-quadtree object for cells data points.
6
+ * @param {array} cellsEntries Array of [cellId, cell] tuples,
7
+ * resulting from running Object.entries on the cells object.
8
+ * @param {function} getCellCoords Given a cell object, return the
9
+ * spatial/scatterplot coordinates [x, y].
10
+ * @returns {object} Quadtree instance.
11
+ */
12
+ export function createQuadTree(obsEmbedding, getCellCoords) {
13
+ // Use the cellsEntries variable since it is already
14
+ // an array, converted by Object.entries().
15
+ // Only use cellsEntries in quadtree calculation if there is
16
+ // centroid data in the cells (i.e not just ids).
17
+ // eslint-disable-next-line no-unused-vars
18
+ if (!obsEmbedding) {
19
+ // Abort if the cells data is not yet available.
20
+ return null;
21
+ }
22
+ const tree = quadtree()
23
+ .x(i => getCellCoords(i)[0])
24
+ .y(i => getCellCoords(i)[1])
25
+ .addAll(range(obsEmbedding.shape[1]));
26
+ return tree;
27
+ }
@@ -1,16 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import '@testing-library/jest-dom';
3
- import { cleanup, render } from '@testing-library/react';
4
- import { afterEach, expect } from 'vitest';
5
- import { IconButton } from './ToolMenu';
6
- afterEach(() => {
7
- cleanup();
8
- });
9
- describe('ToolMenu.js', () => {
10
- describe('<IconButton />', () => {
11
- it('renders with title attribute', () => {
12
- const { container } = render(_jsx(IconButton, { isActive: true, alt: "Lasso" }));
13
- expect(container.querySelectorAll('[title="Lasso"]').length).toEqual(1);
14
- });
15
- });
16
- });
@@ -1,5 +0,0 @@
1
- export { default as AbstractSpatialOrScatterplot } from './AbstractSpatialOrScatterplot';
2
- export { createQuadTree } from './quadtree';
3
- export { forceCollideRects } from './force-collide-rects';
4
- export { getOnHoverCallback } from './cursor';
5
- export { getPointSizeDevicePixels, getPointOpacity, } from './dynamic-opacity';
File without changes