High-performance WebGL scatterplot component for React with pan/zoom and lasso selection
npm install @biohub/scatterplot


High-performance WebGL scatterplot component for React with support for datasets up to 10M+ points.
- GPU-accelerated rendering - WebGL2-based for smooth 60fps performance
- Interactive pan & zoom - Mouse wheel zoom-to-cursor, drag panning, configurable zoom limits
- Lasso selection - Select multiple points with custom polygons
- Controlled camera - External camera control for syncing with OpenSeadragon or similar viewers
- Customizable styling - Theming system with point colors, sizes, states, and presets
- Responsive - Auto-adapts to container size
- React hooks - Composable selection and interaction hooks
- Zero heavy dependencies - No D3, no regl, just React and WebGL
``bash`
npm install @biohub/scatterplot
`bash`
npm run build
This creates a dist/ directory with:scatterplot.js
- (ESM)scatterplot.umd.js
- (UMD)index.d.ts
- (TypeScript types)
- Source maps
`bashIn this directory
npm link
$3
`bash
In your consuming project
npm unlink @biohub/scatterplotIn this directory
npm unlink
`Usage
Styles are bundled with the library: the package uses vite-plugin-lib-inject-css so the main JS bundle imports the CSS at build time. When you
import { Scatterplot } from '@biohub/scatterplot', your bundler will process the CSS import (emit a link tag, inline it, etc.)—no extra configuration needed, and SSR-friendly. To load the CSS explicitly, use import '@biohub/scatterplot/styles.css'.$3
`tsx
import { Scatterplot } from '@biohub/scatterplot';function MyChart() {
const points = [
{ x: 10, y: 20, color: '#ff0000' },
{ x: 30, y: 40, color: '#00ff00' },
{ x: 50, y: 60, color: '#0000ff' },
];
return (
points={points}
width={800}
height={600}
/>
);
}
`$3
`tsx
import { Scatterplot } from '@biohub/scatterplot';function InteractiveChart() {
const points = generateYourData(); // Array of {x, y, color?}
return (
points={points}
width={800}
height={600}
enableLasso={true}
onSelectionChange={(indices) => {
console.log('Selected points:', indices);
}}
/>
);
}
`$3
`tsx
import { useState } from 'react';
import { Scatterplot, type Camera, DEFAULT_CAMERA } from '@biohub/scatterplot';function ControlledChart() {
const points = generateYourData(); // Array of {x, y, color?}
const [camera, setCamera] = useState(DEFAULT_CAMERA);
return (
Zoom: {camera.zoom.toFixed(2)}
points={points}
width={800}
height={600}
controlled={{ camera, onCameraChange: setCamera }}
maxZoom={50}
minZoom={0.5}
/>
);
}
`$3
Use the
controlled prop (or camera + onCameraChange on ScatterplotGL) to keep an external viewer in sync. Derive the external viewer's viewport from the scatterplot camera state inside your onCameraChange handler — this avoids drift that can occur with per-event forwarding:`tsx
import { useState, useCallback } from 'react';
import { Scatterplot, type Camera, DEFAULT_CAMERA } from '@biohub/scatterplot';function SyncedChart({ externalViewer, points }) {
const [camera, setCamera] = useState(DEFAULT_CAMERA);
const handleCameraChange = useCallback(
(update: Camera | ((prev: Camera) => Camera)) => {
setCamera((prev) => {
const next = typeof update === 'function' ? update(prev) : update;
// Derive external viewer state from scatterplot camera
externalViewer.syncToCamera(next.zoom, next.pan);
return next;
});
},
[externalViewer],
);
return (
points={points}
controlled={{ camera, onCameraChange: handleCameraChange }}
/>
);
}
`$3
Omit
width/height to auto-fill the container, or use the built-in useContainerSize hook:`tsx
import { useRef } from 'react';
import { Scatterplot, useContainerSize } from '@biohub/scatterplot';function ResponsiveChart() {
const ref = useRef(null);
const { width, height } = useContainerSize(ref);
return (
points={points}
width={width}
height={height}
/>
);
}
`API Reference
$3
High-level component with built-in state management for camera, selection, and lasso. For spatial mode features (
pointSize, dataBounds), use directly.#### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
|
points | Point[] | required | Array of {x, y, color?} data points |
| width | number | container width | Canvas width in CSS pixels |
| height | number | container height | Canvas height in CSS pixels |
| initialCamera | Camera | {zoom: 1, pan: {x:0, y:0}} | Initial camera state |
| theme | ScatterplotTheme | lightTheme | Theme configuration |
| pixelRatio | number | devicePixelRatio | Device pixel ratio for high-DPI |
| enableLasso | boolean | true | Enable lasso selection mode |
| enablePanZoom | boolean | true | Enable pan and zoom interactions |
| debug | boolean | false | Show debug panel with performance metrics |
| onSelectionChange | (indices: Set | - | Callback when selection changes |
| lassoRealtimeThreshold | number | 1_000_000 | Point count above which realtime lasso highlighting is disabled |
| controlled | {camera, onCameraChange?} | - | External camera control (see Controlled Camera) |
| maxZoom | number | Infinity | Maximum zoom level |
| minZoom | number | 0.5 | Minimum zoom level |$3
Low-level rendering component for advanced use cases. Accepts raw
Float32Array buffers instead of Point[].#### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
|
positions | Float32Array | required | Interleaved [x, y, x, y, ...] in data space |
| colors | Uint8Array | required | Interleaved [r, g, b, a, ...] in 0-255 range |
| width | number | container width | Canvas width in CSS pixels |
| height | number | container height | Canvas height in CSS pixels |
| flags | Uint8Array | - | Per-point selection/highlight state (use createFlagBuffer()) |
| dataBounds | DataBounds | auto-computed | Override normalization bounds (e.g., {xMin: -1, xMax: 1, yMin: -1, yMax: 1}) |
| camera | Camera | internal | Controlled camera state |
| onCameraChange | (camera \| updater) => void | - | Camera change callback (supports functional updates) |
| panZoomEnabled | boolean | true | Enable pan and zoom interactions |
| lassoEnabled | boolean | false | Enable lasso selection mode |
| onLassoComplete | (indices: Set | - | Lasso completion callback |
| onLassoUpdate | (indices: Set | - | Real-time lasso highlight callback |
| onPointClick | (index: number \| null) => void | - | Point click callback |
| pointSize | number | - | Override point size in CSS pixels |
| maxZoom | number | Infinity | Maximum zoom level |
| minZoom | number | 0.5 | Minimum zoom level |
| theme | ScatterplotTheme | lightTheme | Theme configuration |
| pixelRatio | number | devicePixelRatio | Device pixel ratio |
| debug | boolean | false | Show debug panel |
| className | string | - | CSS class for the wrapper div |$3
####
Point`typescript
interface Point {
x: number;
y: number;
color?: string; // Hex color (e.g., '#ff0000'), defaults to '#3498db'
}
`####
Camera`typescript
interface Camera {
zoom: number; // 1.0 = no zoom
pan: { x: number; y: number }; // NDC offset (-1 to 1)
}
`####
DataBounds`typescript
interface DataBounds {
xMin: number;
xMax: number;
yMin: number;
yMax: number;
}
`$3
####
useSelection()Hook for managing point selection state.
`typescript
const {
selectedIndices, // Set — current selection
handlePointClick, // (index: number | null) => void — click handler
setSelection, // (indices: Set) => void — set selection
clearSelection, // () => void — clear all
isSelected, // (index: number) => boolean — check membership
} = useSelection();
`####
useContainerSize()Hook for responsive container sizing.
`typescript
const containerRef = useRef(null);
const { width, height } = useContainerSize(containerRef);
`$3
####
createFlagBuffer(count, selectedIndices?, highlightedIndices?, backgroundIndices?)Create a per-point flag buffer for selection/highlight/background state.
####
createTheme(overrides, baseTheme?)Create a theme by merging partial overrides into a base theme (defaults to
lightTheme).####
findClosestPointRaw(mouseX, mouseY, positions, count, width, height, camera, maxDistance, dataBounds, dataPadding)Find the closest point to mouse coordinates using raw buffers.
####
findPointsInLassoRaw(polygon, positions, count, width, height, camera, dataBounds, dataPadding)Find all points within a lasso polygon using raw buffers.
Theming
$3
`tsx
import { Scatterplot, lightTheme, darkTheme, highContrastTheme } from '@biohub/scatterplot';
`Available presets:
-
lightTheme - Light background, blue points (default)
- darkTheme - Dark background, lighter blue points
- highContrastTheme - Black background, white points, yellow lasso$3
Use
createTheme() to customize specific properties:`tsx
import { Scatterplot, createTheme } from '@biohub/scatterplot';const myTheme = createTheme({
canvas: { background: '#1a1a2e', dataPadding: 0 },
points: { size: 8, opacity: 0.8 },
});
`Extend any base theme:
`tsx
import { createTheme, darkTheme } from '@biohub/scatterplot';const myDarkTheme = createTheme({ points: { size: 10 } }, darkTheme);
`$3
`typescript
interface ScatterplotTheme {
canvas: {
background: string; // Canvas background color (default: '#ffffff')
dataPadding: number; // Padding around data in pixels (default: 20)
};
points: {
defaultColor: string; // Fallback point color (default: '#3498db')
size: number; // Point diameter in pixels (default: 5)
opacity: number; // Base opacity 0-1 (default: 1.0)
backgroundOpacity: number; // Opacity for background points (default: 0.5)
highlightBrightness: number; // Brightness multiplier for highlights (default: 1.4)
highlightSizeScale: number; // Size multiplier for highlights (default: 1.3)
unselectedSizeScale: number; // Size multiplier for unselected points (default: 0.2)
};
lasso: {
fill: string; // Lasso fill color (default: 'rgba(59, 130, 246, 0.1)')
stroke: string; // Lasso stroke color (default: 'rgb(59, 130, 246)')
strokeWidth: number; // Stroke width in pixels (default: 2)
strokeDasharray: string; // SVG dash pattern (default: '5,5')
};
debug: {
background: string; // Debug panel background (default: 'rgba(0, 0, 0, 0.8)')
color: string; // Debug panel text color (default: '#00ff00')
fontFamily: string; // Debug panel font (default: 'monospace')
fontSize: string; // Debug panel font size (default: '12px')
};
}
`$3
DOM elements (lasso overlay, debug panel) expose CSS custom properties for external styling.
Naming convention:
--scatterplot-{section}-{kebab-case-property}| Section | Properties |
|---------|------------|
|
lasso | --scatterplot-lasso-fill, --scatterplot-lasso-stroke, --scatterplot-lasso-stroke-width, --scatterplot-lasso-stroke-dasharray |
| debug | --scatterplot-debug-background, --scatterplot-debug-color, --scatterplot-debug-font-family, --scatterplot-debug-font-size |Example:
`css
.my-chart {
--scatterplot-lasso-stroke: red;
--scatterplot-lasso-fill: rgba(255, 0, 0, 0.1);
}
``tsx
`> Note: Canvas background and point properties are WebGL-only and must be configured via the
theme prop, not CSS.Performance Tips
- Large datasets (>100K points): Rendering is optimized for WebGL, should maintain 60fps
- Colors: Pre-calculate colors instead of computing on each render
- Selection: Use
useMemo to avoid re-creating selection arrays
- Responsive: Debounce resize events for better performanceBrowser Requirements
- WebGL2 support required (all modern browsers since ~2017)
- Chrome 56+, Firefox 51+, Safari 15+, Edge 79+
Troubleshooting
$3
This can happen with very large datasets or after leaving tab inactive for long periods. The component will attempt to recover automatically.
$3
Ensure your
tsconfig.json includes:
`json
{
"compilerOptions": {
"moduleResolution": "node",
"types": ["node"]
}
}
`Development
`bash
Build library
npm run buildRun tests
npm testRun tests in watch mode
npm run test:watchRun tests with coverage
npm run test:coverage
`$3
The demo app is in the
demo/ directory and links to the local library build.`bash
First, build and link the library
npm run build
npm linkThen run the demo
cd demo
npm install
npm link @biohub/scatterplot
npm run dev # Development server (http://localhost:5173)
`#### Production Build (Recommended for Performance Testing)
For accurate performance testing, use the production build:
`bash
cd demo
npm run build # Build production bundle
npm run preview # Serve at http://localhost:4173
`Contributing
This project uses Conventional Commits and release-please for automated versioning.
$3
`
type(scope): description
`| Type | Description | Release |
|------|-------------|---------|
|
feat | New feature | Minor (0.1.0 → 0.2.0) |
| fix | Bug fix | Patch (0.1.0 → 0.1.1) |
| docs | Documentation only | No release |
| style | Code style (formatting) | No release |
| refactor | Code refactoring | No release |
| perf | Performance improvement | Patch |
| test | Adding tests | No release |
| chore | Maintenance tasks | No release |Breaking changes: Add
! after type or include BREAKING CHANGE: in footer for major release (0.1.0 → 1.0.0).`bash
feat!: redesign API
or
feat: redesign APIBREAKING CHANGE: The
data prop now requires typed arrays instead of Point[].
`$3
This project uses release-please for automated releases.
1. Merge PR to
main with conventional commits
2. release-please automatically creates/updates a "Release PR" with:
- Updated package.json version
- Updated CHANGELOG.md`MIT
For issues and questions, please open an issue on GitHub.