@vitessce/scatterplot 2.0.3 → 3.0.1

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 (67) hide show
  1. package/dist/{deflate.65a17097.mjs → deflate-162fa772.js} +2 -2
  2. package/dist/{index.8206952d.mjs → index-578beee3.js} +61450 -51757
  3. package/dist/{index.mjs → index.js} +5 -5
  4. package/dist/{jpeg.4221d32f.mjs → jpeg-00fef901.js} +1 -1
  5. package/dist/{lerc.8d649494.mjs → lerc-55d38607.js} +76 -5
  6. package/dist/{lzw.89350f4e.mjs → lzw-c3bdd9f6.js} +1 -1
  7. package/dist/{packbits.986f9d9f.mjs → packbits-c1c71d64.js} +1 -1
  8. package/dist/{pako.esm.4b234125.mjs → pako.esm-68f84e2a.js} +97 -15
  9. package/dist/{raw.1cc73933.mjs → raw-e1c8b3d7.js} +1 -1
  10. package/dist/{webimage.be69a2d5.mjs → webimage-c7fc297e.js} +1 -1
  11. package/dist-tsc/EmptyMessage.d.ts +2 -0
  12. package/dist-tsc/EmptyMessage.d.ts.map +1 -0
  13. package/dist-tsc/EmptyMessage.js +6 -0
  14. package/dist-tsc/Scatterplot.d.ts +10 -0
  15. package/dist-tsc/Scatterplot.d.ts.map +1 -0
  16. package/dist-tsc/Scatterplot.js +314 -0
  17. package/dist-tsc/ScatterplotOptions.d.ts +2 -0
  18. package/dist-tsc/ScatterplotOptions.d.ts.map +1 -0
  19. package/dist-tsc/ScatterplotOptions.js +56 -0
  20. package/dist-tsc/ScatterplotTooltipSubscriber.d.ts +2 -0
  21. package/dist-tsc/ScatterplotTooltipSubscriber.d.ts.map +1 -0
  22. package/dist-tsc/ScatterplotTooltipSubscriber.js +14 -0
  23. package/dist-tsc/index.d.ts +6 -0
  24. package/dist-tsc/index.d.ts.map +1 -0
  25. package/dist-tsc/index.js +5 -5
  26. package/dist-tsc/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.d.ts +82 -0
  27. package/dist-tsc/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.d.ts.map +1 -0
  28. package/dist-tsc/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +218 -0
  29. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.d.ts +4 -0
  30. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.d.ts.map +1 -0
  31. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.js +83 -0
  32. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.test.d.ts +2 -0
  33. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.test.d.ts.map +1 -0
  34. package/dist-tsc/shared-spatial-scatterplot/ToolMenu.test.js +23 -0
  35. package/dist-tsc/shared-spatial-scatterplot/cursor.d.ts +4 -0
  36. package/dist-tsc/shared-spatial-scatterplot/cursor.d.ts.map +1 -0
  37. package/dist-tsc/shared-spatial-scatterplot/cursor.js +22 -0
  38. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.d.ts +3 -0
  39. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.d.ts.map +1 -0
  40. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.js +47 -0
  41. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.test.d.ts +2 -0
  42. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.test.d.ts.map +1 -0
  43. package/dist-tsc/shared-spatial-scatterplot/dynamic-opacity.test.js +29 -0
  44. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.d.ts +13 -0
  45. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.d.ts.map +1 -0
  46. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.js +169 -0
  47. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.test.d.ts +2 -0
  48. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.test.d.ts.map +1 -0
  49. package/dist-tsc/shared-spatial-scatterplot/force-collide-rects.test.js +59 -0
  50. package/dist-tsc/shared-spatial-scatterplot/index.d.ts +6 -0
  51. package/dist-tsc/shared-spatial-scatterplot/index.d.ts.map +1 -0
  52. package/dist-tsc/shared-spatial-scatterplot/index.js +5 -0
  53. package/dist-tsc/shared-spatial-scatterplot/quadtree.d.ts +10 -0
  54. package/dist-tsc/shared-spatial-scatterplot/quadtree.d.ts.map +1 -0
  55. package/dist-tsc/shared-spatial-scatterplot/quadtree.js +26 -0
  56. package/package.json +25 -13
  57. package/src/Scatterplot.js +13 -2
  58. package/src/ScatterplotOptions.js +27 -5
  59. package/src/index.js +5 -5
  60. package/src/shared-spatial-scatterplot/AbstractSpatialOrScatterplot.js +10 -4
  61. package/src/shared-spatial-scatterplot/ToolMenu.js +56 -18
  62. package/src/shared-spatial-scatterplot/ToolMenu.test.jsx +14 -6
  63. package/src/shared-spatial-scatterplot/dynamic-opacity.js +1 -1
  64. package/src/shared-spatial-scatterplot/dynamic-opacity.test.js +2 -1
  65. package/src/shared-spatial-scatterplot/force-collide-rects.test.js +2 -1
  66. package/src/shared-spatial-scatterplot/index.js +5 -5
  67. package/src/shared-spatial-scatterplot/quadtree.js +1 -1
@@ -0,0 +1,218 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { PureComponent } from 'react';
3
+ import { deck, DEFAULT_GL_OPTIONS } from '@vitessce/gl';
4
+ import ToolMenu from './ToolMenu.js';
5
+ import { getCursor, getCursorWithTool } from './cursor.js';
6
+ /**
7
+ * Abstract class component intended to be inherited by
8
+ * the Spatial and Scatterplot class components.
9
+ * Contains a common constructor, common DeckGL callbacks,
10
+ * and common render function.
11
+ */
12
+ export default class AbstractSpatialOrScatterplot extends PureComponent {
13
+ constructor(props) {
14
+ super(props);
15
+ this.state = {
16
+ gl: null,
17
+ tool: null,
18
+ };
19
+ this.viewport = null;
20
+ this.onViewStateChange = this.onViewStateChange.bind(this);
21
+ this.onInitializeViewInfo = this.onInitializeViewInfo.bind(this);
22
+ this.onWebGLInitialized = this.onWebGLInitialized.bind(this);
23
+ this.onToolChange = this.onToolChange.bind(this);
24
+ this.onHover = this.onHover.bind(this);
25
+ this.recenter = this.recenter.bind(this);
26
+ }
27
+ /**
28
+ * Called by DeckGL upon a viewState change,
29
+ * for example zoom or pan interaction.
30
+ * Emit the new viewState to the `setViewState`
31
+ * handler prop.
32
+ * @param {object} params
33
+ * @param {object} params.viewState The next deck.gl viewState.
34
+ */
35
+ onViewStateChange({ viewState: nextViewState }) {
36
+ const { setViewState, viewState, layers, spatialAxisFixed, } = this.props;
37
+ const use3d = layers?.some(l => l.use3d);
38
+ setViewState({
39
+ ...nextViewState,
40
+ // If the axis is fixed, just use the current target in state i.e don't change target.
41
+ target: spatialAxisFixed && use3d ? viewState.target : nextViewState.target,
42
+ });
43
+ }
44
+ /**
45
+ * Called by DeckGL upon viewport
46
+ * initialization.
47
+ * @param {object} viewState
48
+ * @param {object} viewState.viewport
49
+ */
50
+ onInitializeViewInfo({ viewport }) {
51
+ this.viewport = viewport;
52
+ }
53
+ /**
54
+ * Called by DeckGL upon initialization,
55
+ * helps to understand when to pass layers
56
+ * to the DeckGL component.
57
+ * @param {object} gl The WebGL context object.
58
+ */
59
+ onWebGLInitialized(gl) {
60
+ this.setState({ gl });
61
+ }
62
+ /**
63
+ * Called by the ToolMenu buttons.
64
+ * Emits the new tool value to the
65
+ * `onToolChange` prop.
66
+ * @param {string} tool Name of tool.
67
+ */
68
+ onToolChange(tool) {
69
+ const { onToolChange: onToolChangeProp } = this.props;
70
+ this.setState({ tool });
71
+ if (onToolChangeProp) {
72
+ onToolChangeProp(tool);
73
+ }
74
+ }
75
+ /**
76
+ * Create the DeckGL layers.
77
+ * @returns {object[]} Array of
78
+ * DeckGL layer objects.
79
+ * Intended to be overriden by descendants.
80
+ */
81
+ // eslint-disable-next-line class-methods-use-this
82
+ getLayers() {
83
+ return [];
84
+ }
85
+ // eslint-disable-next-line consistent-return
86
+ onHover(info) {
87
+ const { coordinate, sourceLayer: layer, tile, } = info;
88
+ const { setCellHighlight, cellHighlight, setComponentHover, layers, } = this.props;
89
+ const hasBitmask = (layers || []).some(l => l.type === 'bitmask');
90
+ if (!setCellHighlight || !tile) {
91
+ return null;
92
+ }
93
+ if (!layer || !coordinate) {
94
+ if (cellHighlight && hasBitmask) {
95
+ setCellHighlight(null);
96
+ }
97
+ return null;
98
+ }
99
+ const { content, bbox, index: { z }, } = tile;
100
+ if (!content) {
101
+ if (cellHighlight && hasBitmask) {
102
+ setCellHighlight(null);
103
+ }
104
+ return null;
105
+ }
106
+ const { data, width, height } = content;
107
+ const { left, right, top, bottom, } = bbox;
108
+ const bounds = [
109
+ left,
110
+ data.height < layer.tileSize ? height : bottom,
111
+ data.width < layer.tileSize ? width : right,
112
+ top,
113
+ ];
114
+ if (!data) {
115
+ if (cellHighlight && hasBitmask) {
116
+ setCellHighlight(null);
117
+ }
118
+ return null;
119
+ }
120
+ // Tiled layer needs a custom layerZoomScale.
121
+ if (layer.id.includes('bitmask')) {
122
+ // The zoomed out layer needs to use the fixed zoom at which it is rendered.
123
+ const layerZoomScale = Math.max(1, 2 ** Math.round(-z));
124
+ const dataCoords = [
125
+ Math.floor((coordinate[0] - bounds[0]) / layerZoomScale),
126
+ Math.floor((coordinate[1] - bounds[3]) / layerZoomScale),
127
+ ];
128
+ const coords = dataCoords[1] * width + dataCoords[0];
129
+ const hoverData = data.map(d => d[coords]);
130
+ const cellId = hoverData.find(i => i > 0);
131
+ if (cellId !== Number(cellHighlight)) {
132
+ if (setComponentHover) {
133
+ setComponentHover();
134
+ }
135
+ // eslint-disable-next-line no-unused-expressions
136
+ setCellHighlight(cellId ? String(cellId) : null);
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * Emits a function to project from the
142
+ * cell ID space to the scatterplot or
143
+ * spatial coordinate space, via the
144
+ * `updateViewInfo` prop.
145
+ */
146
+ viewInfoDidUpdate(obsIndex, obsLocations, makeGetObsCoords) {
147
+ const { updateViewInfo, uuid } = this.props;
148
+ const { viewport } = this;
149
+ if (updateViewInfo && viewport) {
150
+ updateViewInfo({
151
+ uuid,
152
+ project: (obsId) => {
153
+ try {
154
+ if (obsIndex && obsLocations) {
155
+ const getObsCoords = makeGetObsCoords(obsLocations);
156
+ const obsIdx = obsIndex.indexOf(obsId);
157
+ const obsCoord = getObsCoords(obsIdx);
158
+ return viewport.project(obsCoord);
159
+ }
160
+ return [null, null];
161
+ }
162
+ catch (e) {
163
+ return [null, null];
164
+ }
165
+ },
166
+ });
167
+ }
168
+ }
169
+ /**
170
+ * Intended to be overridden by descendants.
171
+ */
172
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
173
+ componentDidUpdate() {
174
+ }
175
+ /** Intended to be overridden by descendants.
176
+ * Resets the view type to its original position.
177
+ */
178
+ // eslint-disable-next-line class-methods-use-this
179
+ recenter() { }
180
+ /**
181
+ * Intended to be overridden by descendants.
182
+ * @returns {boolean} Whether or not any layers are 3D.
183
+ */
184
+ // eslint-disable-next-line class-methods-use-this
185
+ use3d() {
186
+ return false;
187
+ }
188
+ /**
189
+ * A common render function for both Spatial
190
+ * and Scatterplot components.
191
+ */
192
+ render() {
193
+ const { deckRef, viewState, uuid, hideTools, } = this.props;
194
+ const { gl, tool } = this.state;
195
+ const layers = this.getLayers();
196
+ const use3d = this.use3d();
197
+ const showCellSelectionTools = this.obsSegmentationsData !== null;
198
+ const showPanTool = layers.length > 0;
199
+ // For large datasets or ray casting, the visual quality takes only a small
200
+ // hit in exchange for much better performance by setting this to false:
201
+ // https://deck.gl/docs/api-reference/core/deck#usedevicepixels
202
+ const useDevicePixels = (!use3d
203
+ && (this.obsSegmentationsData?.shape?.[0] < 100000
204
+ || this.obsLocationsData?.shape?.[1] < 100000));
205
+ return (_jsxs(_Fragment, { children: [_jsx(ToolMenu, { activeTool: tool, setActiveTool: this.onToolChange, visibleTools: {
206
+ pan: showPanTool && !hideTools,
207
+ selectLasso: showCellSelectionTools && !hideTools,
208
+ }, recenterOnClick: this.recenter }), _jsx(deck.DeckGL, { id: `deckgl-overlay-${uuid}`, ref: deckRef, views: [
209
+ use3d
210
+ ? new deck.OrbitView({ id: 'orbit', controller: true, orbitAxis: 'Y' })
211
+ : new deck.OrthographicView({
212
+ id: 'ortho',
213
+ }),
214
+ ], layers: gl && viewState.target.slice(0, 2).every(i => typeof i === 'number')
215
+ ? layers
216
+ : [], glOptions: DEFAULT_GL_OPTIONS, onWebGLInitialized: this.onWebGLInitialized, onViewStateChange: this.onViewStateChange, viewState: viewState, useDevicePixels: useDevicePixels, controller: tool ? { dragPan: false } : true, getCursor: tool ? getCursorWithTool : getCursor, onHover: this.onHover, children: this.onInitializeViewInfo })] }));
217
+ }
218
+ }
@@ -0,0 +1,4 @@
1
+ export function IconTool(props: any): JSX.Element;
2
+ export function IconButton(props: any): JSX.Element;
3
+ export default function ToolMenu(props: any): JSX.Element;
4
+ //# sourceMappingURL=ToolMenu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToolMenu.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/ToolMenu.js"],"names":[],"mappings":"AAsEA,kDAeC;AAED,oDAeC;AAED,0DAsCC"}
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import clsx from 'clsx';
4
+ import { SELECTION_TYPE } from '@vitessce/gl';
5
+ import { PointerIconSVG, SelectLassoIconSVG } from '@vitessce/icons';
6
+ import { makeStyles } from '@material-ui/core';
7
+ import { CenterFocusStrong } from '@material-ui/icons';
8
+ const useStyles = makeStyles(() => ({
9
+ toolButton: {
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
+ toolIcon: {
32
+ // btn btn-outline-secondary mr-2 icon
33
+ padding: '0',
34
+ height: '2em',
35
+ width: '2em',
36
+ backgroundColor: 'white',
37
+ display: 'inline-block',
38
+ fontWeight: '400',
39
+ textAlign: 'center',
40
+ verticalAlign: 'middle',
41
+ cursor: 'pointer',
42
+ userSelect: 'none',
43
+ border: '1px solid #6c757d',
44
+ fontSize: '16px',
45
+ lineHeight: '1.5',
46
+ borderRadius: '4px',
47
+ 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',
48
+ color: '#6c757d',
49
+ marginRight: '8px',
50
+ '& > svg': {
51
+ verticalAlign: 'middle',
52
+ color: 'black',
53
+ },
54
+ '&:active': {
55
+ extend: 'iconClicked',
56
+ },
57
+ },
58
+ toolActive: {
59
+ // active
60
+ color: '#fff',
61
+ backgroundColor: '#6c757d',
62
+ borderColor: '#6c757d',
63
+ boxShadow: '0 0 0 3px rgba(108, 117, 125, 0.5)',
64
+ },
65
+ }));
66
+ export function IconTool(props) {
67
+ const { alt, onClick, isActive, children, } = props;
68
+ const classes = useStyles();
69
+ return (_jsx("button", { className: clsx(classes.toolIcon, { [classes.toolActive]: isActive }), onClick: onClick, type: "button", title: alt, children: children }));
70
+ }
71
+ export function IconButton(props) {
72
+ const { alt, onClick, children, } = props;
73
+ const classes = useStyles();
74
+ return (_jsx("button", { className: clsx(classes.toolIcon, classes.toolButton), onClick: onClick, type: "button", title: alt, children: children }));
75
+ }
76
+ export default function ToolMenu(props) {
77
+ const { setActiveTool, activeTool, visibleTools = { pan: true, selectLasso: true }, recenterOnClick = () => { }, } = props;
78
+ const classes = useStyles();
79
+ const onRecenterButtonCLick = () => {
80
+ recenterOnClick();
81
+ };
82
+ return (_jsxs("div", { className: classes.tool, children: [visibleTools.pan && (_jsx(IconTool, { alt: "pointer tool", onClick: () => setActiveTool(null), isActive: activeTool === null, children: _jsx(PointerIconSVG, {}) })), visibleTools.selectLasso ? (_jsx(IconTool, { alt: "select lasso", onClick: () => setActiveTool(SELECTION_TYPE.POLYGON), isActive: activeTool === SELECTION_TYPE.POLYGON, children: _jsx(SelectLassoIconSVG, {}) })) : null, _jsx(IconButton, { alt: "click to recenter", onClick: () => onRecenterButtonCLick(), children: _jsx(CenterFocusStrong, {}) })] }));
83
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ToolMenu.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToolMenu.test.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/ToolMenu.test.jsx"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, afterEach } from 'vitest';
3
+ import '@testing-library/jest-dom';
4
+ import { cleanup, render } from '@testing-library/react';
5
+ import React from 'react';
6
+ import { IconTool, IconButton } from './ToolMenu.js';
7
+ afterEach(() => {
8
+ cleanup();
9
+ });
10
+ describe('ToolMenu.js', () => {
11
+ describe('<IconTool />', () => {
12
+ it('renders with title attribute', () => {
13
+ const { container } = render(_jsx(IconTool, { isActive: true, alt: "Lasso" }));
14
+ expect(container.querySelectorAll('[title="Lasso"]').length).toEqual(1);
15
+ });
16
+ });
17
+ describe('<IconButton />', () => {
18
+ it('renders with title attribute', () => {
19
+ const { container } = render(_jsx(IconButton, { alt: "click to recenter" }));
20
+ expect(container.querySelectorAll('[title="click to recenter"]').length).toEqual(1);
21
+ });
22
+ });
23
+ });
@@ -0,0 +1,4 @@
1
+ export function getOnHoverCallback(obsIndex: any, setObsHighlight: any, setComponentHover: any): (info: any) => void;
2
+ export function getCursorWithTool(): string;
3
+ export function getCursor(interactionState: any): "default" | "grabbing";
4
+ //# sourceMappingURL=cursor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/cursor.js"],"names":[],"mappings":"AAKA,qHAiBC;AAtBM,4CAA2C;AAC3C,yEAEN"}
@@ -0,0 +1,22 @@
1
+ export const getCursorWithTool = () => 'crosshair';
2
+ export const getCursor = interactionState => (interactionState.isDragging
3
+ ? 'grabbing' : 'default');
4
+ export function getOnHoverCallback(obsIndex, setObsHighlight, setComponentHover) {
5
+ return (info) => {
6
+ // Notify the parent component that its child component is
7
+ // the "hover source".
8
+ if (setComponentHover) {
9
+ setComponentHover();
10
+ }
11
+ if (info.index) {
12
+ const obsId = obsIndex[info.index];
13
+ if (setObsHighlight) {
14
+ setObsHighlight(obsId);
15
+ }
16
+ }
17
+ else if (setObsHighlight) {
18
+ // Clear the currently-hovered cell info by passing null.
19
+ setObsHighlight(null);
20
+ }
21
+ };
22
+ }
@@ -0,0 +1,3 @@
1
+ export function getPointSizeDevicePixels(devicePixelRatio: any, zoom: any, xRange: any, yRange: any, width: any, height: any): any;
2
+ export function getPointOpacity(zoom: any, xRange: any, yRange: any, width: any, height: any, numCells: any, avgFillDensity: any): any;
3
+ //# sourceMappingURL=dynamic-opacity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-opacity.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/dynamic-opacity.js"],"names":[],"mappings":"AAKA,mIA0BC;AAGD,uIAuBC"}
@@ -0,0 +1,47 @@
1
+ import { deck } from '@vitessce/gl';
2
+ import { clamp } from 'lodash-es';
3
+ // Reference: https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
4
+ // Reference: https://observablehq.com/@bmschmidt/dot-density-election-maps-with-webgl
5
+ export function getPointSizeDevicePixels(devicePixelRatio, zoom, xRange, yRange, width, height) {
6
+ // Size of a point, in units of the diagonal axis.
7
+ const pointSize = 0.0005;
8
+ // Point size maximum, in screen pixels.
9
+ const pointScreenSizeMax = 10;
10
+ // Point size minimum, in screen pixels.
11
+ const pointScreenSizeMin = 1 / devicePixelRatio;
12
+ const scaleFactor = 2 ** zoom;
13
+ const xAxisRange = 2.0 / ((xRange * scaleFactor) / width);
14
+ const yAxisRange = 2.0 / ((yRange * scaleFactor) / height);
15
+ // The diagonal screen size as a fraction of the current diagonal axis range,
16
+ // then converted to device pixels.
17
+ const diagonalScreenSize = Math.sqrt((width ** 2) + (height ** 2));
18
+ const diagonalAxisRange = Math.sqrt((xAxisRange ** 2) + (yAxisRange ** 2));
19
+ const diagonalFraction = pointSize / diagonalAxisRange;
20
+ const deviceSize = diagonalFraction * diagonalScreenSize;
21
+ const pointSizeDevicePixels = clamp(deviceSize, pointScreenSizeMin, pointScreenSizeMax);
22
+ return pointSizeDevicePixels;
23
+ }
24
+ // Reference: https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
25
+ export function getPointOpacity(zoom, xRange, yRange, width, height, numCells, avgFillDensity) {
26
+ const N = numCells;
27
+ const [minX, minY, maxX, maxY] = new deck.OrthographicView({ zoom }).makeViewport({
28
+ height,
29
+ width,
30
+ viewState: { zoom, target: [0, 0, 0] },
31
+ }).getBounds();
32
+ const X = maxY - minY;
33
+ const Y = maxX - minX;
34
+ const X0 = xRange;
35
+ const Y0 = yRange;
36
+ const W = width;
37
+ const H = height;
38
+ let rho = avgFillDensity;
39
+ if (!rho) {
40
+ rho = Math.min(1, 1 / (10 ** (Math.log10(N) - 3)));
41
+ }
42
+ // p in the calculation is the pixel length/width of a given point, which for us is 1
43
+ // so it does not factor into our calculation here.
44
+ const alpha = ((rho * W * H) / N) * (Y0 / Y) * (X0 / X);
45
+ const pointOpacity = clamp(alpha, 1.01 / 255, 1.0);
46
+ return pointOpacity;
47
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dynamic-opacity.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-opacity.test.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/dynamic-opacity.test.js"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getPointSizeDevicePixels, getPointOpacity } from './dynamic-opacity.js';
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(devicePixelRatio, zoom, xRange, yRange, width, height);
13
+ expect(pointSize).toBeCloseTo(0.5);
14
+ });
15
+ });
16
+ describe('getPointOpacity', () => {
17
+ it('calculates point opacity', () => {
18
+ const zoom = null;
19
+ const width = 1000;
20
+ const height = 650;
21
+ const xRange = 20;
22
+ const yRange = 18;
23
+ const numCells = 500000;
24
+ const avgFillDensity = undefined;
25
+ const pointOpacity = getPointOpacity(zoom, xRange, yRange, width, height, numCells, avgFillDensity);
26
+ expect(pointOpacity).toBeCloseTo(0.005);
27
+ });
28
+ });
29
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A force function to be used with d3.forceSimulation.
3
+ * This has been adapted for use here, with comments explaining each part.
4
+ * Reference: https://bl.ocks.org/cmgiven/547658968d365bcc324f3e62e175709b
5
+ */
6
+ export function forceCollideRects(): {
7
+ (): void;
8
+ initialize(v: any): void;
9
+ iterations(...v: any[]): number | any;
10
+ strength(...v: any[]): number | any;
11
+ size(...v: any[]): () => any;
12
+ };
13
+ //# sourceMappingURL=force-collide-rects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"force-collide-rects.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/force-collide-rects.js"],"names":[],"mappings":"AAkBA;;;;GAIG;AACH;;;;;;EAqKC"}
@@ -0,0 +1,169 @@
1
+ /* eslint-disable no-plusplus */
2
+ /* eslint-disable no-param-reassign */
3
+ import { quadtree } from 'd3-quadtree';
4
+ /**
5
+ * Returns a closure that returns a constant value.
6
+ */
7
+ function constant(v) {
8
+ return (() => v);
9
+ }
10
+ /**
11
+ * Adds a tiny bit of randomness to a number.
12
+ */
13
+ function jiggle(v) {
14
+ return v + (Math.random() - 0.5) * 1e-6;
15
+ }
16
+ /**
17
+ * A force function to be used with d3.forceSimulation.
18
+ * This has been adapted for use here, with comments explaining each part.
19
+ * Reference: https://bl.ocks.org/cmgiven/547658968d365bcc324f3e62e175709b
20
+ */
21
+ export function forceCollideRects() {
22
+ // D3 implements things with function prototypes rather than classes.
23
+ // Pretend these variables are the "instance members" of a class.
24
+ // Note that this function actually returns the internal force() function,
25
+ // but that the force() function is a closure with access to these instance members.
26
+ let nodes;
27
+ let masses;
28
+ let strength = 1;
29
+ let iterations = 1;
30
+ let sizes;
31
+ let size = constant([0, 0]);
32
+ // Given a node, return the center point along the x-axis.
33
+ function xCenter(d) {
34
+ return d.x + d.vx + sizes[d.index][0] / 2;
35
+ }
36
+ // Given a node, return the center point along the y-axis.
37
+ function yCenter(d) {
38
+ return d.y + d.vy + sizes[d.index][1] / 2;
39
+ }
40
+ // Given a quadtree node, initialize its .size property.
41
+ function prepare(quad) {
42
+ if (quad.data) {
43
+ // This is a leaf node, so we set quad.size to the node's size.
44
+ // (No need to compute the max of internal nodes,
45
+ // since leaf nodes do not have any internal nodes).
46
+ quad.size = sizes[quad.data.index];
47
+ }
48
+ else {
49
+ quad.size = [0, 0];
50
+ // Internal nodes of the quadtree are represented
51
+ // as four-element arrays in left-to-right, top-to-bottom order.
52
+ // Here, we are setting quad.size to [maxWidth, maxHeight]
53
+ // among the internal nodes of this current `quad` node.
54
+ for (let i = 0; i < 4; i++) {
55
+ if (quad[i] && quad[i].size) {
56
+ quad.size[0] = Math.max(quad.size[0], quad[i].size[0]);
57
+ quad.size[1] = Math.max(quad.size[1], quad[i].size[1]);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ function force() {
63
+ let node;
64
+ let nodeSize;
65
+ let nodeMass;
66
+ let xi;
67
+ let yi;
68
+ // Create a quadtree based on node center points.
69
+ // Initialize each quadtree node's .size property by calling
70
+ // the prepare() function on each quadtree node.
71
+ const tree = quadtree(nodes, xCenter, yCenter).visitAfter(prepare);
72
+ // Update the .vx and .vy properties of both `node` and `data`
73
+ // (the current node pair).
74
+ function apply(quad, x0, y0, x1, y1) {
75
+ // `quad` is a quadtree node.
76
+ const { data } = quad;
77
+ const xSize = (nodeSize[0] + quad.size[0]) / 2;
78
+ const ySize = (nodeSize[1] + quad.size[1]) / 2;
79
+ if (data && data.index > node.index) {
80
+ // This is a leaf node because `data` is defined.
81
+ // `x` is the difference in x centers between `node` and `data`.
82
+ // `y` is the difference in y centers between `node` and `data`.
83
+ let x = jiggle(xi - xCenter(data));
84
+ let y = jiggle(yi - yCenter(data));
85
+ const xd = Math.abs(x) - xSize;
86
+ const yd = Math.abs(y) - ySize;
87
+ // If `xd` and `yd` is less than zero,
88
+ // then there is an overlap between the two nodes.
89
+ if (xd < 0 && yd < 0) {
90
+ const l = Math.sqrt(x * x + y * y);
91
+ const m = masses[data.index] / (nodeMass + masses[data.index]);
92
+ // We move the nodes either in the x or y direction.
93
+ // Nodes are moved proportionally to:
94
+ // their distance apart (`l`), their amount of overlap (`xd` or `yd`), their masses (`m`),
95
+ // and the strength parameter (`strength`).
96
+ if (Math.abs(xd) < Math.abs(yd)) {
97
+ node.vx -= (x *= xd / l * strength) * m;
98
+ data.vx += x * (1 - m);
99
+ }
100
+ else {
101
+ node.vy -= (y *= yd / l * strength) * m;
102
+ data.vy += y * (1 - m);
103
+ }
104
+ }
105
+ // When the quadtree.visit callback returns _true_ for a node,
106
+ // then the node's children will _not_ be visited.
107
+ return x0 > xi + xSize || x1 < xi - xSize || y0 > yi + ySize || y1 < yi - ySize;
108
+ }
109
+ return false;
110
+ }
111
+ function iterate() {
112
+ // On every iteration, use the `apply` function to visit every node
113
+ // which has an index greater than the current node's index,
114
+ // (visiting every node pair).
115
+ for (let j = 0; j < nodes.length; j++) {
116
+ node = nodes[j];
117
+ nodeSize = sizes[j];
118
+ nodeMass = masses[j];
119
+ xi = xCenter(node);
120
+ yi = yCenter(node);
121
+ tree.visit(apply);
122
+ }
123
+ }
124
+ // Do the specified number of iterations.
125
+ for (let i = 0; i < iterations; i++) {
126
+ iterate();
127
+ }
128
+ }
129
+ // The "constructor".
130
+ // Takes a list of nodes as input.
131
+ force.initialize = (v) => {
132
+ nodes = v;
133
+ // Get the size [w, h] of each node using the size getter function.
134
+ sizes = nodes.map(size);
135
+ // Get the mass of each node,
136
+ // which is the sum of its horizontal and vertical edge lengths.
137
+ masses = sizes.map(d => d[0] + d[1]);
138
+ };
139
+ // Set the number of iterations.
140
+ // If no value is provided as a parameter, this acts as a getter function.
141
+ force.iterations = (...v) => {
142
+ if (v.length) {
143
+ iterations = +v[0];
144
+ return force;
145
+ }
146
+ return iterations;
147
+ };
148
+ // Set the strength value.
149
+ // If no value is provided as a parameter, this acts as a getter function.
150
+ force.strength = (...v) => {
151
+ if (v.length) {
152
+ strength = +v[0];
153
+ return force;
154
+ }
155
+ return strength;
156
+ };
157
+ // Set the size function.
158
+ // The size function takes a node as a parameter and returns its size.
159
+ // If no size function is provided as a parameter, this acts as a getter function.
160
+ force.size = (...v) => {
161
+ if (v.length) {
162
+ size = (typeof v[0] === 'function' ? v[0] : constant(v[0]));
163
+ return force;
164
+ }
165
+ return size;
166
+ };
167
+ // Returns the force closure.
168
+ return force;
169
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=force-collide-rects.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"force-collide-rects.test.d.ts","sourceRoot":"","sources":["../../src/shared-spatial-scatterplot/force-collide-rects.test.js"],"names":[],"mappings":""}