@biohub/scatterplot 0.1.4

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chan Zuckerberg Biohub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,501 @@
1
+ # @biohub/scatterplot
2
+
3
+ [![CI](https://github.com/chanzuckerberg/scatterplot/actions/workflows/ci.yml/badge.svg)](https://github.com/chanzuckerberg/scatterplot/actions/workflows/ci.yml)
4
+ [![Coverage](https://github.com/chanzuckerberg/scatterplot/raw/badges/coverage-badge.svg)](https://github.com/chanzuckerberg/scatterplot/actions/workflows/ci.yml)
5
+ [![Size](https://github.com/chanzuckerberg/scatterplot/raw/badges/size-badge.svg)](https://github.com/chanzuckerberg/scatterplot/actions/workflows/ci.yml)
6
+
7
+ High-performance WebGL scatterplot component for React with support for datasets up to 10M+ points.
8
+
9
+ ## Features
10
+
11
+ - **GPU-accelerated rendering** - WebGL2-based for smooth 60fps performance
12
+ - **Interactive pan & zoom** - Intuitive mouse/touch controls
13
+ - **Lasso selection** - Select multiple points with custom polygons
14
+ - **Customizable styling** - Point colors, sizes, and states
15
+ - **Responsive** - Auto-adapts to container size
16
+ - **React hooks** - Composable selection and interaction hooks
17
+ - **Zero heavy dependencies** - No D3, no regl, just React and WebGL
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @biohub/scatterplot
23
+ ```
24
+
25
+ ## Local Development
26
+
27
+ ### Build the library
28
+
29
+ ```bash
30
+ npm run build
31
+ ```
32
+
33
+ This creates a `dist/` directory with:
34
+ - `scatterplot.js` (ESM)
35
+ - `scatterplot.umd.js` (UMD)
36
+ - `index.d.ts` (TypeScript types)
37
+ - Source maps
38
+
39
+ ### Link for local development
40
+
41
+ ```bash
42
+ # In this directory
43
+ npm link
44
+
45
+ # In your consuming project directory
46
+ npm link @biohub/scatterplot
47
+ ```
48
+
49
+ ### Unlink when done
50
+
51
+ ```bash
52
+ # In your consuming project
53
+ npm unlink @biohub/scatterplot
54
+
55
+ # In this directory
56
+ npm unlink
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ### Basic Example
62
+
63
+ ```tsx
64
+ import { Scatterplot } from '@biohub/scatterplot';
65
+
66
+ function MyChart() {
67
+ const data = [
68
+ { x: 10, y: 20, color: '#ff0000' },
69
+ { x: 30, y: 40, color: '#00ff00' },
70
+ { x: 50, y: 60, color: '#0000ff' },
71
+ ];
72
+
73
+ return (
74
+ <Scatterplot
75
+ data={data}
76
+ width={800}
77
+ height={600}
78
+ />
79
+ );
80
+ }
81
+ ```
82
+
83
+ ### With Selection Handling
84
+
85
+ ```tsx
86
+ import { Scatterplot } from '@biohub/scatterplot';
87
+
88
+ function InteractiveChart() {
89
+ const [selectedIndices, setSelectedIndices] = useState<number[]>([]);
90
+
91
+ const data = generateYourData(); // Array of {x, y, color?}
92
+
93
+ return (
94
+ <Scatterplot
95
+ data={data}
96
+ width={800}
97
+ height={600}
98
+ onSelectionChange={(indices) => {
99
+ console.log('Selected points:', indices);
100
+ setSelectedIndices(indices);
101
+ }}
102
+ highlightedIndices={selectedIndices}
103
+ />
104
+ );
105
+ }
106
+ ```
107
+
108
+ ### With Lasso Selection
109
+
110
+ ```tsx
111
+ import { Scatterplot } from '@biohub/scatterplot';
112
+
113
+ function LassoChart() {
114
+ const [isLassoMode, setIsLassoMode] = useState(false);
115
+ const [selectedIndices, setSelectedIndices] = useState<number[]>([]);
116
+
117
+ return (
118
+ <div>
119
+ <button onClick={() => setIsLassoMode(!isLassoMode)}>
120
+ {isLassoMode ? 'Disable' : 'Enable'} Lasso
121
+ </button>
122
+
123
+ <Scatterplot
124
+ data={data}
125
+ width={800}
126
+ height={600}
127
+ lassoMode={isLassoMode}
128
+ onSelectionChange={setSelectedIndices}
129
+ selectedIndices={selectedIndices}
130
+ />
131
+ </div>
132
+ );
133
+ }
134
+ ```
135
+
136
+ ### Responsive Sizing
137
+
138
+ ```tsx
139
+ import { Scatterplot } from '@biohub/scatterplot';
140
+
141
+ function ResponsiveChart() {
142
+ const containerRef = useRef<HTMLDivElement>(null);
143
+ const [size, setSize] = useState({ width: 800, height: 600 });
144
+
145
+ useEffect(() => {
146
+ const observer = new ResizeObserver((entries) => {
147
+ const { width, height } = entries[0].contentRect;
148
+ setSize({ width, height });
149
+ });
150
+
151
+ if (containerRef.current) {
152
+ observer.observe(containerRef.current);
153
+ }
154
+
155
+ return () => observer.disconnect();
156
+ }, []);
157
+
158
+ return (
159
+ <div ref={containerRef} style={{ width: '100%', height: '100vh' }}>
160
+ <Scatterplot
161
+ data={data}
162
+ width={size.width}
163
+ height={size.height}
164
+ />
165
+ </div>
166
+ );
167
+ }
168
+ ```
169
+
170
+ Or use the built-in `useContainerSize` hook:
171
+
172
+ ```tsx
173
+ import { Scatterplot, useContainerSize } from '@biohub/scatterplot';
174
+
175
+ function ResponsiveChart() {
176
+ const { ref, width, height } = useContainerSize();
177
+
178
+ return (
179
+ <div ref={ref} style={{ width: '100%', height: '100vh' }}>
180
+ <Scatterplot
181
+ data={data}
182
+ width={width}
183
+ height={height}
184
+ />
185
+ </div>
186
+ );
187
+ }
188
+ ```
189
+
190
+ ## API Reference
191
+
192
+ ### `<Scatterplot>`
193
+
194
+ Main component for rendering interactive scatterplots.
195
+
196
+ #### Props
197
+
198
+ | Prop | Type | Default | Description |
199
+ |------|------|---------|-------------|
200
+ | `data` | `Point[]` | **required** | Array of data points with `{x, y, color?}` |
201
+ | `width` | `number` | **required** | Canvas width in pixels |
202
+ | `height` | `number` | **required** | Canvas height in pixels |
203
+ | `selectedIndices` | `number[]` | `[]` | Indices of selected points |
204
+ | `highlightedIndices` | `number[]` | `[]` | Indices of highlighted points |
205
+ | `onSelectionChange` | `(indices: number[]) => void` | - | Callback when selection changes |
206
+ | `lassoMode` | `boolean` | `false` | Enable lasso selection mode |
207
+ | `enableZoom` | `boolean` | `true` | Enable zoom interaction |
208
+ | `enablePan` | `boolean` | `true` | Enable pan interaction |
209
+
210
+ ### Types
211
+
212
+ #### `Point`
213
+
214
+ ```typescript
215
+ interface Point {
216
+ x: number;
217
+ y: number;
218
+ color?: string; // Hex color (e.g., '#ff0000'), defaults to '#999999'
219
+ }
220
+ ```
221
+
222
+ ### Hooks
223
+
224
+ #### `useSelection(initialSelection?: number[])`
225
+
226
+ Hook for managing point selection state.
227
+
228
+ ```typescript
229
+ const { selectedIndices, setSelectedIndices, clearSelection } = useSelection();
230
+ ```
231
+
232
+ Returns:
233
+ - `selectedIndices: number[]` - Current selection
234
+ - `setSelectedIndices: (indices: number[]) => void` - Update selection
235
+ - `clearSelection: () => void` - Clear all selections
236
+
237
+ #### `useContainerSize()`
238
+
239
+ Hook for responsive container sizing.
240
+
241
+ ```typescript
242
+ const { ref, width, height } = useContainerSize();
243
+ ```
244
+
245
+ Returns:
246
+ - `ref: RefObject<HTMLDivElement>` - Attach to container element
247
+ - `width: number` - Container width
248
+ - `height: number` - Container height
249
+
250
+ ### Utilities
251
+
252
+ #### `findClosestPoint(mouseX, mouseY, data, bounds, width, height)`
253
+
254
+ Find the closest point to mouse coordinates.
255
+
256
+ #### `findPointsInLasso(points, lassoPath, bounds, width, height)`
257
+
258
+ Find all points within a lasso polygon.
259
+
260
+ #### `createFlagBuffer(data.length, selectedIndices, highlightedIndices)`
261
+
262
+ Create WebGL flag buffer for point states.
263
+
264
+ ## Advanced Usage
265
+
266
+ ### Custom Renderer
267
+
268
+ For advanced use cases, you can use the low-level `ScatterplotGL` component:
269
+
270
+ ```tsx
271
+ import { ScatterplotGL, useSelection } from '@biohub/scatterplot';
272
+
273
+ function CustomScatterplot() {
274
+ const { selectedIndices, setSelectedIndices } = useSelection();
275
+
276
+ return (
277
+ <ScatterplotGL
278
+ data={data}
279
+ width={800}
280
+ height={600}
281
+ selectedIndices={selectedIndices}
282
+ onPointClick={(index) => {
283
+ setSelectedIndices([index]);
284
+ }}
285
+ // More control over rendering...
286
+ />
287
+ );
288
+ }
289
+ ```
290
+
291
+ ## Theming
292
+
293
+ The scatterplot supports theming through a `theme` prop.
294
+
295
+ ### Built-in Presets
296
+
297
+ ```tsx
298
+ import { Scatterplot, lightTheme, darkTheme, highContrastTheme } from '@biohub/scatterplot';
299
+
300
+ // Use a preset
301
+ <Scatterplot data={data} theme={darkTheme} />
302
+ ```
303
+
304
+ Available presets:
305
+ - `lightTheme` - Light background, blue points (default)
306
+ - `darkTheme` - Dark background, lighter blue points
307
+ - `highContrastTheme` - Black background, white points, yellow lasso
308
+
309
+ ### Custom Themes
310
+
311
+ Use `createTheme()` to customize specific properties:
312
+
313
+ ```tsx
314
+ import { Scatterplot, createTheme } from '@biohub/scatterplot';
315
+
316
+ // Override specific properties (inherits from lightTheme by default)
317
+ const myTheme = createTheme({
318
+ canvas: { background: '#1a1a2e' },
319
+ points: { size: 8, opacity: 0.8 },
320
+ });
321
+
322
+ <Scatterplot data={data} theme={myTheme} />
323
+ ```
324
+
325
+ Extend any base theme:
326
+
327
+ ```tsx
328
+ import { createTheme, darkTheme } from '@biohub/scatterplot';
329
+
330
+ // Extend darkTheme with custom point size
331
+ const myDarkTheme = createTheme({ points: { size: 10 } }, darkTheme);
332
+ ```
333
+
334
+ ### Theme Structure
335
+
336
+ ```typescript
337
+ interface ScatterplotTheme {
338
+ canvas: {
339
+ background: string; // Canvas background color (default: '#ffffff')
340
+ dataPadding: number; // Padding around data in pixels (default: 20)
341
+ };
342
+ points: {
343
+ defaultColor: string; // Fallback point color (default: '#3498db')
344
+ size: number; // Point diameter in pixels (default: 5)
345
+ opacity: number; // Base opacity 0-1 (default: 1.0)
346
+ backgroundOpacity: number; // Opacity for non-selected points (default: 0.5)
347
+ highlightBrightness: number; // Brightness multiplier for highlights (default: 1.4)
348
+ highlightSizeScale: number; // Size multiplier for highlights (default: 1.3)
349
+ unselectedSizeScale: number; // Size multiplier for unselected points (default: 0.2)
350
+ };
351
+ lasso: {
352
+ fill: string; // Lasso fill color (default: 'rgba(59, 130, 246, 0.1)')
353
+ stroke: string; // Lasso stroke color (default: 'rgb(59, 130, 246)')
354
+ strokeWidth: number; // Stroke width in pixels (default: 2)
355
+ strokeDasharray: string; // SVG dash pattern (default: '5,5')
356
+ };
357
+ debug: {
358
+ background: string; // Debug panel background (default: 'rgba(0, 0, 0, 0.8)')
359
+ color: string; // Debug panel text color (default: '#00ff00')
360
+ fontFamily: string; // Debug panel font (default: 'monospace')
361
+ fontSize: string; // Debug panel font size (default: '12px')
362
+ };
363
+ }
364
+ ```
365
+
366
+ ### CSS Custom Properties
367
+
368
+ DOM elements (lasso overlay, debug panel) expose CSS custom properties for external styling.
369
+
370
+ **Naming convention:** `--scatterplot-{section}-{kebab-case-property}`
371
+
372
+ | Section | Properties |
373
+ |---------|------------|
374
+ | `lasso` | `--scatterplot-lasso-fill`, `--scatterplot-lasso-stroke`, `--scatterplot-lasso-stroke-width`, `--scatterplot-lasso-stroke-dasharray` |
375
+ | `debug` | `--scatterplot-debug-background`, `--scatterplot-debug-color`, `--scatterplot-debug-font-family`, `--scatterplot-debug-font-size` |
376
+
377
+ Example:
378
+
379
+ ```css
380
+ .my-chart {
381
+ --scatterplot-lasso-stroke: red;
382
+ --scatterplot-lasso-fill: rgba(255, 0, 0, 0.1);
383
+ --scatterplot-debug-background: navy;
384
+ }
385
+ ```
386
+
387
+ ```tsx
388
+ <Scatterplot data={data} className="my-chart" />
389
+ ```
390
+
391
+ > **Note:** Canvas background and point properties are WebGL-only and must be configured via the `theme` prop, not CSS.
392
+
393
+ ### Using className
394
+
395
+ The `className` prop is applied to the wrapper div. You can use it to:
396
+
397
+ 1. **Override CSS custom properties** (as shown above)
398
+ 2. **Target internal elements** with CSS specificity
399
+
400
+ Internal class names:
401
+ - `.scatterplot-lasso-polygon` - The lasso SVG polygon
402
+ - `.scatterplot-debug-panel` - The debug panel container
403
+ - `.scatterplot-debug-panel-title` - The debug panel title
404
+
405
+ Example - custom lasso styling:
406
+
407
+ ```css
408
+ .my-chart .scatterplot-lasso-polygon {
409
+ stroke: hotpink;
410
+ stroke-width: 3px;
411
+ stroke-dasharray: none;
412
+ }
413
+ ```
414
+
415
+ ```tsx
416
+ <Scatterplot data={data} className="my-chart" />
417
+ ```
418
+
419
+ ## Performance Tips
420
+
421
+ - **Large datasets (>100K points)**: Rendering is optimized for WebGL, should maintain 60fps
422
+ - **Colors**: Pre-calculate colors instead of computing on each render
423
+ - **Selection**: Use `useMemo` to avoid re-creating selection arrays
424
+ - **Responsive**: Debounce resize events for better performance
425
+
426
+ ## Browser Requirements
427
+
428
+ - WebGL2 support required (all modern browsers since ~2017)
429
+ - Chrome 56+, Firefox 51+, Safari 15+, Edge 79+
430
+
431
+ ## Troubleshooting
432
+
433
+ ### "WebGL context lost" error
434
+
435
+ This can happen with very large datasets or after leaving tab inactive for long periods. The component will attempt to recover automatically.
436
+
437
+ ### Selection not working
438
+
439
+ Make sure you're passing `onSelectionChange` callback and updating the `selectedIndices` prop with the new selection.
440
+
441
+ ### Types not found
442
+
443
+ Ensure your `tsconfig.json` includes:
444
+ ```json
445
+ {
446
+ "compilerOptions": {
447
+ "moduleResolution": "node",
448
+ "types": ["node"]
449
+ }
450
+ }
451
+ ```
452
+
453
+ ## Development
454
+
455
+ ```bash
456
+ # Build library
457
+ npm run build
458
+
459
+ # Run tests
460
+ npm test
461
+
462
+ # Run tests in watch mode
463
+ npm run test:watch
464
+
465
+ # Run tests with coverage
466
+ npm run test:coverage
467
+ ```
468
+
469
+ ### Running the Demo
470
+
471
+ The demo app is in the `demo/` directory and links to the local library build.
472
+
473
+ ```bash
474
+ # First, build and link the library
475
+ npm run build
476
+ npm link
477
+
478
+ # Then run the demo
479
+ cd demo
480
+ npm install
481
+ npm link @biohub/scatterplot
482
+ npm run dev # Development server (http://localhost:5173)
483
+ ```
484
+
485
+ #### Production Build (Recommended for Performance Testing)
486
+
487
+ For accurate performance testing, use the production build:
488
+
489
+ ```bash
490
+ cd demo
491
+ npm run build # Build production bundle
492
+ npm run preview # Serve at http://localhost:4173
493
+ ```
494
+
495
+ ## License
496
+
497
+ MIT
498
+
499
+ ## Support
500
+
501
+ For issues and questions, please open an issue on GitHub.