JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 188
  • Score
    100M100P100Q97264F
  • License MIT

High-performance WebGL scatterplot component for React with pan/zoom and lasso selection

Package Exports

  • @biohub/scatterplot

Readme

@biohub/scatterplot

CI Coverage Size

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/scatterplot

Local Development

Build the library

npm run build

This creates a dist/ directory with:

  • scatterplot.js (ESM)
  • scatterplot.umd.js (UMD)
  • index.d.ts (TypeScript types)
  • Source maps
# In this directory
npm link

# In your consuming project directory
npm link @biohub/scatterplot
# In your consuming project
npm unlink @biohub/scatterplot

# In this directory
npm unlink

Usage

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 selection
  • setSelectedIndices: (indices: number[]) => void - Update selection
  • clearSelection: () => void - Clear all selections

useContainerSize()

Hook for responsive container sizing.

const { ref, width, height } = useContainerSize();

Returns:

  • ref: RefObject<HTMLDivElement> - Attach to container element
  • width: number - Container width
  • height: 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 points
  • highContrastTheme - 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 theme prop, not CSS.

Using className

The className prop is applied to the wrapper div. You can use it to:

  1. Override CSS custom properties (as shown above)
  2. 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 useMemo to 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:coverage

Running 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)

For accurate performance testing, use the production build:

cd demo
npm run build        # Build production bundle
npm run preview      # Serve at http://localhost:4173

License

MIT

Support

For issues and questions, please open an issue on GitHub.