Package Exports
- @biohub/scatterplot
- @biohub/scatterplot/styles.css
Readme
@biohub/scatterplot
High-performance WebGL scatterplot component for React with support for datasets up to 10M+ points.
Features
- GPU-accelerated rendering - WebGL2-based for smooth 60fps performance
- Interactive pan & zoom - Intuitive mouse/touch controls
- Lasso selection - Select multiple points with custom polygons
- Customizable styling - Point colors, sizes, and states
- Responsive - Auto-adapts to container size
- React hooks - Composable selection and interaction hooks
- Zero heavy dependencies - No D3, no regl, just React and WebGL
Installation
npm install @biohub/scatterplotLocal Development
Build the library
npm run buildThis creates a dist/ directory with:
scatterplot.js(ESM)scatterplot.umd.js(UMD)index.d.ts(TypeScript types)- Source maps
Link for local development
# In this directory
npm link
# In your consuming project directory
npm link @biohub/scatterplotUnlink when done
# In your consuming project
npm unlink @biohub/scatterplot
# In this directory
npm unlinkUsage
Basic Example
import { Scatterplot } from '@biohub/scatterplot';
function MyChart() {
const data = [
{ x: 10, y: 20, color: '#ff0000' },
{ x: 30, y: 40, color: '#00ff00' },
{ x: 50, y: 60, color: '#0000ff' },
];
return (
<Scatterplot
data={data}
width={800}
height={600}
/>
);
}With Selection Handling
import { Scatterplot } from '@biohub/scatterplot';
function InteractiveChart() {
const [selectedIndices, setSelectedIndices] = useState<number[]>([]);
const data = generateYourData(); // Array of {x, y, color?}
return (
<Scatterplot
data={data}
width={800}
height={600}
onSelectionChange={(indices) => {
console.log('Selected points:', indices);
setSelectedIndices(indices);
}}
highlightedIndices={selectedIndices}
/>
);
}With Lasso Selection
import { Scatterplot } from '@biohub/scatterplot';
function LassoChart() {
const [isLassoMode, setIsLassoMode] = useState(false);
const [selectedIndices, setSelectedIndices] = useState<number[]>([]);
return (
<div>
<button onClick={() => setIsLassoMode(!isLassoMode)}>
{isLassoMode ? 'Disable' : 'Enable'} Lasso
</button>
<Scatterplot
data={data}
width={800}
height={600}
lassoMode={isLassoMode}
onSelectionChange={setSelectedIndices}
selectedIndices={selectedIndices}
/>
</div>
);
}Responsive Sizing
import { Scatterplot } from '@biohub/scatterplot';
function ResponsiveChart() {
const containerRef = useRef<HTMLDivElement>(null);
const [size, setSize] = useState({ width: 800, height: 600 });
useEffect(() => {
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
setSize({ width, height });
});
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={containerRef} style={{ width: '100%', height: '100vh' }}>
<Scatterplot
data={data}
width={size.width}
height={size.height}
/>
</div>
);
}Or use the built-in useContainerSize hook:
import { Scatterplot, useContainerSize } from '@biohub/scatterplot';
function ResponsiveChart() {
const { ref, width, height } = useContainerSize();
return (
<div ref={ref} style={{ width: '100%', height: '100vh' }}>
<Scatterplot
data={data}
width={width}
height={height}
/>
</div>
);
}API Reference
<Scatterplot>
Main component for rendering interactive scatterplots.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data |
Point[] |
required | Array of data points with {x, y, color?} |
width |
number |
required | Canvas width in pixels |
height |
number |
required | Canvas height in pixels |
selectedIndices |
number[] |
[] |
Indices of selected points |
highlightedIndices |
number[] |
[] |
Indices of highlighted points |
onSelectionChange |
(indices: number[]) => void |
- | Callback when selection changes |
lassoMode |
boolean |
false |
Enable lasso selection mode |
enableZoom |
boolean |
true |
Enable zoom interaction |
enablePan |
boolean |
true |
Enable pan interaction |
Types
Point
interface Point {
x: number;
y: number;
color?: string; // Hex color (e.g., '#ff0000'), defaults to '#999999'
}Hooks
useSelection(initialSelection?: number[])
Hook for managing point selection state.
const { selectedIndices, setSelectedIndices, clearSelection } = useSelection();Returns:
selectedIndices: number[]- Current selectionsetSelectedIndices: (indices: number[]) => void- Update selectionclearSelection: () => void- Clear all selections
useContainerSize()
Hook for responsive container sizing.
const { ref, width, height } = useContainerSize();Returns:
ref: RefObject<HTMLDivElement>- Attach to container elementwidth: number- Container widthheight: number- Container height
Utilities
findClosestPoint(mouseX, mouseY, data, bounds, width, height)
Find the closest point to mouse coordinates.
findPointsInLasso(points, lassoPath, bounds, width, height)
Find all points within a lasso polygon.
createFlagBuffer(data.length, selectedIndices, highlightedIndices)
Create WebGL flag buffer for point states.
Advanced Usage
Custom Renderer
For advanced use cases, you can use the low-level ScatterplotGL component:
import { ScatterplotGL, useSelection } from '@biohub/scatterplot';
function CustomScatterplot() {
const { selectedIndices, setSelectedIndices } = useSelection();
return (
<ScatterplotGL
data={data}
width={800}
height={600}
selectedIndices={selectedIndices}
onPointClick={(index) => {
setSelectedIndices([index]);
}}
// More control over rendering...
/>
);
}Theming
The scatterplot supports theming through a theme prop.
Built-in Presets
import { Scatterplot, lightTheme, darkTheme, highContrastTheme } from '@biohub/scatterplot';
// Use a preset
<Scatterplot data={data} theme={darkTheme} />Available presets:
lightTheme- Light background, blue points (default)darkTheme- Dark background, lighter blue pointshighContrastTheme- Black background, white points, yellow lasso
Custom Themes
Use createTheme() to customize specific properties:
import { Scatterplot, createTheme } from '@biohub/scatterplot';
// Override specific properties (inherits from lightTheme by default)
const myTheme = createTheme({
canvas: { background: '#1a1a2e' },
points: { size: 8, opacity: 0.8 },
});
<Scatterplot data={data} theme={myTheme} />Extend any base theme:
import { createTheme, darkTheme } from '@biohub/scatterplot';
// Extend darkTheme with custom point size
const myDarkTheme = createTheme({ points: { size: 10 } }, darkTheme);Theme Structure
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 non-selected 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')
};
}CSS Custom Properties
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:
.my-chart {
--scatterplot-lasso-stroke: red;
--scatterplot-lasso-fill: rgba(255, 0, 0, 0.1);
--scatterplot-debug-background: navy;
}<Scatterplot data={data} className="my-chart" />Note: Canvas background and point properties are WebGL-only and must be configured via the
themeprop, not CSS.
Using className
The className prop is applied to the wrapper div. You can use it to:
- Override CSS custom properties (as shown above)
- Target internal elements with CSS specificity
Internal class names:
.scatterplot-lasso-polygon- The lasso SVG polygon.scatterplot-debug-panel- The debug panel container.scatterplot-debug-panel-title- The debug panel title
Example - custom lasso styling:
.my-chart .scatterplot-lasso-polygon {
stroke: hotpink;
stroke-width: 3px;
stroke-dasharray: none;
}<Scatterplot data={data} className="my-chart" />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
useMemoto avoid re-creating selection arrays - Responsive: Debounce resize events for better performance
Browser Requirements
- WebGL2 support required (all modern browsers since ~2017)
- Chrome 56+, Firefox 51+, Safari 15+, Edge 79+
Troubleshooting
"WebGL context lost" error
This can happen with very large datasets or after leaving tab inactive for long periods. The component will attempt to recover automatically.
Selection not working
Make sure you're passing onSelectionChange callback and updating the selectedIndices prop with the new selection.
Types not found
Ensure your tsconfig.json includes:
{
"compilerOptions": {
"moduleResolution": "node",
"types": ["node"]
}
}Development
# Build library
npm run build
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverageRunning the Demo
The demo app is in the demo/ directory and links to the local library build.
# First, build and link the library
npm run build
npm link
# Then 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:
cd demo
npm run build # Build production bundle
npm run preview # Serve at http://localhost:4173Contributing
This project uses Conventional Commits and release-please for automated versioning.
Commit Format
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).
feat!: redesign API
# or
feat: redesign API
BREAKING CHANGE: The `data` prop now requires typed arrays instead of Point[].Release Flow
This project uses release-please for automated releases.
- Merge PR to
mainwith conventional commits - release-please automatically creates/updates a "Release PR" with:
- Updated
package.jsonversion - Updated
CHANGELOG.md
- Updated
- When ready to release, merge the Release PR
- Merging triggers:
- GitHub Release creation
- npm publish with quality checks
License
MIT
Support
For issues and questions, please open an issue on GitHub.