npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@biohub/scatterplot

v0.3.0

Published

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

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

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

Link for local development

# In this directory
npm link

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

Unlink when done

# In your consuming project
npm unlink @biohub/scatterplot

# In 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'.

Basic Example

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 (
    <Scatterplot
      points={points}
      width={800}
      height={600}
    />
  );
}

With Selection Handling

import { Scatterplot } from '@biohub/scatterplot';

function InteractiveChart() {
  const points = generateYourData(); // Array of {x, y, color?}

  return (
    <Scatterplot
      points={points}
      width={800}
      height={600}
      enableLasso={true}
      onSelectionChange={(indices) => {
        console.log('Selected points:', indices);
      }}
    />
  );
}

Controlled Camera

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<Camera>(DEFAULT_CAMERA);

  return (
    <div>
      <p>Zoom: {camera.zoom.toFixed(2)}</p>
      <button onClick={() => setCamera(DEFAULT_CAMERA)}>Reset</button>

      <Scatterplot
        points={points}
        width={800}
        height={600}
        controlled={{ camera, onCameraChange: setCamera }}
        maxZoom={50}
        minZoom={0.5}
      />
    </div>
  );
}

Syncing with External Viewers

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:

import { useState, useCallback } from 'react';
import { Scatterplot, type Camera, DEFAULT_CAMERA } from '@biohub/scatterplot';

function SyncedChart({ externalViewer, points }) {
  const [camera, setCamera] = useState<Camera>(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 (
    <Scatterplot
      points={points}
      controlled={{ camera, onCameraChange: handleCameraChange }}
    />
  );
}

Responsive Sizing

Omit width/height to auto-fill the container, or use the built-in useContainerSize hook:

import { useRef } from 'react';
import { Scatterplot, useContainerSize } from '@biohub/scatterplot';

function ResponsiveChart() {
  const ref = useRef<HTMLDivElement>(null);
  const { width, height } = useContainerSize(ref);

  return (
    <div ref={ref} style={{ width: '100%', height: '100vh' }}>
      <Scatterplot
        points={points}
        width={width}
        height={height}
      />
    </div>
  );
}

API Reference

<Scatterplot>

High-level component with built-in state management for camera, selection, and lasso. For spatial mode features (pointSize, dataBounds), use <ScatterplotGL> 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<number>) => void | - | 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 |

<ScatterplotGL>

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<number>) => void | - | Lasso completion callback | | onLassoUpdate | (indices: Set<number>) => void | - | 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 |

Types

Point

interface Point {
  x: number;
  y: number;
  color?: string; // Hex color (e.g., '#ff0000'), defaults to '#3498db'
}

Camera

interface Camera {
  zoom: number;           // 1.0 = no zoom
  pan: { x: number; y: number }; // NDC offset (-1 to 1)
}

DataBounds

interface DataBounds {
  xMin: number;
  xMax: number;
  yMin: number;
  yMax: number;
}

Hooks

useSelection()

Hook for managing point selection state.

const {
  selectedIndices,   // Set<number> — current selection
  handlePointClick,  // (index: number | null) => void — click handler
  setSelection,      // (indices: Set<number>) => void — set selection
  clearSelection,    // () => void — clear all
  isSelected,        // (index: number) => boolean — check membership
} = useSelection();

useContainerSize()

Hook for responsive container sizing.

const containerRef = useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);

Utilities

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

Built-in Presets

import { Scatterplot, lightTheme, darkTheme, highContrastTheme } from '@biohub/scatterplot';

<Scatterplot points={points} 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';

const myTheme = createTheme({
  canvas: { background: '#1a1a2e', dataPadding: 0 },
  points: { size: 8, opacity: 0.8 },
});

<Scatterplot points={points} theme={myTheme} />

Extend any base theme:

import { createTheme, darkTheme } from '@biohub/scatterplot';

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 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')
  };
}

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);
}
<ScatterplotGL positions={positions} colors={colors} className="my-chart" />

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 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.

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)

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:4173

Contributing

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.

  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
  3. When ready to release, merge the Release PR
  4. Merging triggers:
    • GitHub Release creation
    • npm publish with quality checks

License

MIT

Support

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