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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@hively/living-hive

v2.0.0

Published

A React library for visualizing interconnected stories using UMAP and hex grid layouts

Readme

Living Hive React

A React library for visualizing interconnected stories using UMAP dimensionality reduction and interactive hex grid layouts.

Features

  • 🎨 Interactive hex grid visualization with zoom and pan
  • 🤖 OpenAI integration for story embeddings and theme generation
  • 🎯 Automatic theme extraction from stories
  • 🎨 Customizable color palettes (defaults to warm palette)
  • ♿ Full keyboard navigation and accessibility support
  • ⚡ Web worker-based UMAP computation for smooth performance
  • 🎭 Customizable story rendering
  • 🔄 Support for both client-side and server-side embedding generation
  • 💾 Pre-generated embeddings and themes support for faster loading
  • 🚀 Optimized batch API calls with retry logic and rate limiting
  • 📊 Theme legend with story counts

Installation

npm install @hively/living-hive

Peer Dependencies

This library requires the following peer dependencies:

npm install react react-dom tailwindcss class-variance-authority clsx tailwind-merge

Quick Start

Basic Usage

You need to generate embeddings and themes before passing them to the LivingHive component. Living Hive does not include network helpers in the public API, so you can source embeddings and themes from your own data pipeline or reuse the mock data shipped in examples/.

import { LivingHive, DEFAULT_WORKER_URL, type BaseStory, type Theme } from '@hively/living-hive'
import { useMemo, useState } from 'react'

function App() {
  const stories = [
    { id: '1', text: 'My first story about teamwork...' },
    { id: '2', text: 'Another story about collaboration...' },
  ]

  // Provide embeddings and themes from your own data pipeline.
  const [embeddings] = useState(
    () =>
      new Map<string, number[]>([
        ['1', [0.1, 0.2, 0.3]],
        ['2', [0.3, 0.1, 0.4]],
      ]),
  )
  const [themes] = useState<Theme[]>([
    { id: 'teamwork', label: 'Teamwork' },
    { id: 'collaboration', label: 'Collaboration' },
  ])

  return (
    <LivingHive
      stories={stories}
      embeddings={embeddings}
      themes={themes}
      workerUrl={DEFAULT_WORKER_URL}
    />
  )
}

With Pre-generated Embeddings and Themes

For faster loading and to avoid API calls, you can pre-generate embeddings and themes:

import { LivingHive, type BaseStory, type Theme } from '@hively/living-hive'

const stories: BaseStory[] = [
  { id: '1', text: 'Story about teamwork' },
  { id: '2', text: 'Story about collaboration' },
]

const embeddings = new Map<string, number[]>(
  stories.map((story, index) => [story.id, [index / 10, index / 5, index / 2]]),
)

const themes: Theme[] = [
  { id: 'teamwork', label: 'Teamwork' },
  { id: 'collaboration', label: 'Collaboration' },
]

export function App() {
  return <LivingHive stories={stories} embeddings={embeddings} themes={themes} />
}

Note: When using pre-generated embeddings and themes, no API calls are made. This is ideal for deployed examples or when you want to avoid API costs.

Configuring the UMAP worker asset

The UMAP algorithm runs in a standalone worker that ships with the package at dist/workers/umap-worker.js.

Recommended approach (works with any bundler):

Pass the worker URL explicitly using your bundler's worker import syntax:

Vite:

import { LivingHive } from '@hively/living-hive'
// Option 1: Use the source worker path (recommended for Vite)
import workerUrl from '@hively/living-hive/workers/umap-placement.worker?worker&url'

// Option 2: Use the built worker file directly
// import workerUrl from '@hively/living-hive/workers/umap-worker.js?worker&url'
;<LivingHive stories={stories} embeddings={embeddings} themes={themes} workerUrl={workerUrl} />

Note: The library's package.json exports support both umap-placement.worker and umap-worker.js paths. Vite's ?worker&url query string will work with either.

Webpack 5+ (native worker support):

import { LivingHive } from '@hively/living-hive'
import Worker from '@hively/living-hive/workers/umap-placement.worker?worker'

// Create worker URL using the Worker constructor
const workerUrl = new URL(Worker, import.meta.url).href

<LivingHive
  stories={stories}
  embeddings={embeddings}
  themes={themes}
  workerUrl={workerUrl}
/>

Webpack 4 or older (with worker-loader):

import { LivingHive } from '@hively/living-hive'
// Configure worker-loader in your webpack config
;<LivingHive
  stories={stories}
  embeddings={embeddings}
  themes={themes}
  workerUrl={require('@hively/living-hive/workers/umap-worker.js')}
/>

Manual setup (copy worker to public directory):

If your bundler doesn't support worker imports, you can manually copy the worker file:

  1. Copy node_modules/@hively/living-hive/dist/workers/umap-worker.js to your public/workers/ directory
  2. Pass the URL:
<LivingHive
  stories={stories}
  embeddings={embeddings}
  themes={themes}
  workerUrl="/workers/umap-worker.js"
/>

Automatic resolution (fallback):

If you don't provide workerUrl, the library will attempt to auto-resolve it using import.meta.url. This may work with some modern bundlers, but results vary. It's recommended to pass workerUrl explicitly for reliable behavior across all bundlers.

Environment variables (alternative):

You can also set the worker URL via environment variables that your bundler exposes:

  • VITE_LIVING_HIVE_WORKER_URL (Vite)
  • NEXT_PUBLIC_LIVING_HIVE_WORKER_URL (Next.js)
  • LIVING_HIVE_WORKER_URL (generic)

With Custom Themes

import { LivingHive, StoryDataGenerator, type Theme } from '@hively/living-hive'
import { useState, useEffect } from 'react'

function App() {
  const [embeddings, setEmbeddings] = useState<Map<string, number[]>>(new Map())
  const themes: Theme[] = [
    { id: 'teamwork', label: 'Teamwork' },
    { id: 'collaboration', label: 'Collaboration' },
  ]

  useEffect(() => {
    const generator = new StoryDataGenerator(stories, 'your-api-key-here')
    generator.generateEmbeddings().then(setEmbeddings)
  }, [stories])

  return <LivingHive stories={stories} embeddings={embeddings} themes={themes} />
}

With Custom Color Palette

const customPalette = ['#FF6E7F', '#4F81B0', '#CDB15E', '#DAA5AD', '#AEBEC5']

// Assuming `stories`, `embeddings`, and `themes` are defined as in the basic example
<LivingHive
  stories={stories}
  embeddings={embeddings}
  themes={themes}
  colorPalette={customPalette}
/>

With Custom Story Rendering

<LivingHive
  stories={stories}
  embeddings={embeddings}
  themes={themes}
  renderStory={story => (
    <div>
      <h3>{story.title}</h3>
      <p>{story.text}</p>
      {story.metadata && <span>{story.metadata}</span>}
    </div>
  )}
/>

With Extended Story Types

interface MyStory extends BaseStory {
  title: string
  author: string
  date: string
}

const stories: MyStory[] = [
  {
    id: '1',
    text: 'Story content...',
    title: 'My Story',
    author: 'John Doe',
    date: '2024-01-01',
  },
]

const embeddings = new Map<string, number[]>([['1', [0.1, 0.2, 0.3]]])

const themes: Theme[] = [{ id: 'teamwork', label: 'Teamwork' }]

<LivingHive<MyStory>
  stories={stories}
  embeddings={embeddings}
  themes={themes}
  renderStory={(story) => (
    <div>
      <h3>{story.title}</h3>
      <p>{story.text}</p>
      <small>By {story.author} on {story.date}</small>
    </div>
  )}
/>

Understanding Embeddings and Themes

Embeddings are numerical vectors that capture the meaning of each story. Living Hive typically generates them with OpenAI's text-embedding-3-small model, which converts every story's text into a fixed-length array of numbers. Stories that discuss similar ideas end up with vectors that are close together in multidimensional space. The visualization uses those distances to place related stories near one another when UMAP projects the vectors down to the hex grid.

Themes are concise narrative labels that explain why clusters of stories belong together. After embeddings are computed, Living Hive prompts a language model (defaults to gpt-4-turbo-preview) to summarize the clusters and produce human-readable theme names. You can think of themes as the legend for the map: they describe the neighborhoods formed by the embeddings and act as the connective tissue between the qualitative stories and the quantitative placement.

Both embeddings and themes can be generated anywhere—client, server, or build scripts—and then shipped with your app. The component simply needs a Map of story IDs to embedding vectors plus an array of theme objects; how you produce them is up to your data pipeline.

Custom Styling

LivingHive ships with a set of CSS custom properties so you can restyle the experience without forking the component. The library injects the styles automatically; opt into the default charcoal palette by wrapping the component in the provided .living-hive-theme class:

export function App() {
  return (
    <div className="living-hive-theme">
      <LivingHive stories={stories} embeddings={embeddings} themes={themes} />
    </div>
  )
}

Override any variable by scoping new values to your own class or element:

.my-product-hive {
  --living-hive-canvas-background: #ffffff;
  --living-hive-canvas-outline: rgba(28, 28, 28, 0.12);
  --living-hive-toggle-bg: rgba(255, 255, 255, 0.85);
  --living-hive-legend-background: rgba(28, 28, 28, 0.85);
  --living-hive-dialog-color: #1c1c1c;
}
<div className="my-product-hive">
  <LivingHive stories={stories} embeddings={embeddings} themes={themes} canvasHeight={480} />
</div>

Available variables include:

| Variable | Purpose | | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | --living-hive-width | Canvas width (default 100%) | | --living-hive-height | Canvas height (default calc(100vh - 312px)) | | --living-hive-canvas-background | Canvas background color shown behind hexes | | --living-hive-canvas-outline | Canvas border color | | --living-hive-canvas-border-radius | Canvas corner radius | | --living-hive-focus-ring / --living-hive-focus-ring-offset | Focus ring colors for keyboard users | | --living-hive-toggle-bg / --living-hive-toggle-bg-hover / --living-hive-toggle-color / --living-hive-toggle-border | Fullscreen toggle palette | | --living-hive-legend-background / --living-hive-legend-border / --living-hive-legend-color / --living-hive-legend-opacity | Legend chip visuals |

You can also supply explicit dimensions via the new canvasWidth and canvasHeight props. Numbers are treated as pixel values (e.g. canvasHeight={480}), while strings accept any CSS length (e.g. canvasHeight="70vh"). When these props are omitted, the component falls back to the CSS custom properties so host applications can manage sizing through styles alone.

Handling Hex Clicks

The LivingHive component provides an onHexClick callback that fires when a hex is clicked or activated via keyboard navigation. You can use this to implement your own dialog, popover, or modal solution.

Important: As of version 1.0.0, the component no longer includes built-in dialog/popover components. You must implement your own UI for displaying story details.

Basic Example

import { LivingHive } from '@hively/living-hive'
import { useState, useCallback } from 'react'
// Import your preferred dialog/popover component
import { Dialog, DialogContent } from './components/Dialog'

function App() {
  const [selectedStory, setSelectedStory] = useState(null)
  const [selectedTheme, setSelectedTheme] = useState(null)
  const [isDialogOpen, setIsDialogOpen] = useState(false)

  // Wrap in useCallback to prevent unnecessary re-renders
  const handleHexClick = useCallback((story, theme) => {
    setSelectedStory(story)
    setSelectedTheme(theme)
    setIsDialogOpen(true)
  }, [])

  return (
    <>
      <LivingHive
        stories={stories}
        embeddings={embeddings}
        themes={themes}
        onHexClick={handleHexClick}
      />
      {selectedStory && (
        <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
          <DialogContent>
            {selectedTheme && <h2>{selectedTheme.label}</h2>}
            <p>{selectedStory.text}</p>
          </DialogContent>
        </Dialog>
      )}
    </>
  )
}

Complete Example

See examples/src/examples/BasicExample.tsx for a complete example including a side panel dialog implementation.

Generating Embeddings and Themes

Living Hive ships with the StoryDataGenerator class for producing embeddings and themes. Use it client-side, server-side, or in build scripts—whichever best fits your data pipeline.

Using StoryDataGenerator

The StoryDataGenerator class stores stories and API key once, making it easy to generate embeddings and themes:

import { StoryDataGenerator } from '@hively/living-hive'

// Create generator with stories and API key
const generator = new StoryDataGenerator(stories, apiKey)

// Generate embeddings
const embeddings = await generator.generateEmbeddings({
  model: 'text-embedding-3-small',
  dimensions: 384,
  batchSize: 100,
})

// Generate themes using the embeddings
const themes = await generator.generateThemes(embeddings, {
  model: 'gpt-4-turbo-preview',
  minThemes: 5,
  maxThemes: 10,
})

// Assign stories to themes (synchronous)
const assignments = generator.assignStoriesToThemes(embeddings, themes)

Client-Side Usage (React)

import { StoryDataGenerator } from '@hively/living-hive'
import { useState, useEffect } from 'react'

function MyComponent() {
  const [embeddings, setEmbeddings] = useState<Map<string, number[]>>(new Map())
  const [themes, setThemes] = useState<Theme[]>([])
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    const generateData = async () => {
      setLoading(true)
      try {
        const generator = new StoryDataGenerator(stories, apiKey)
        const embeddings = await generator.generateEmbeddings()
        const themes = await generator.generateThemes(embeddings)
        setEmbeddings(embeddings)
        setThemes(themes)
      } finally {
        setLoading(false)
      }
    }
    generateData()
  }, [stories])

  return <LivingHive stories={stories} embeddings={embeddings} themes={themes} loading={loading} />
}

Server-Side Usage (API Routes)

// In Next.js API route or similar
import { StoryDataGenerator } from '@hively/living-hive'

export async function GET(request: Request) {
  const stories = await getStories()
  const generator = new StoryDataGenerator(stories, process.env.OPENAI_API_KEY)

  const embeddings = await generator.generateEmbeddings()
  const themes = await generator.generateThemes(embeddings)

  return Response.json({ embeddings, themes })
}

Script Usage (Pre-computation)

// In build script or pre-computation script
import { StoryDataGenerator } from '@hively/living-hive'
import fs from 'fs'

const stories = [...]
const generator = new StoryDataGenerator(stories, process.env.OPENAI_API_KEY)

const embeddings = await generator.generateEmbeddings({
  model: 'text-embedding-3-small',
  dimensions: 384,
  batchSize: 100
})

const themes = await generator.generateThemes(embeddings, {
  model: 'gpt-4-turbo-preview',
  minThemes: 5,
  maxThemes: 10
})

// Save to JSON files for use in component
fs.writeFileSync('embeddings.json', JSON.stringify(Object.fromEntries(embeddings)))
fs.writeFileSync('themes.json', JSON.stringify(themes))

OpenAI Parameter Configuration

StoryDataGenerator accepts configuration options for both embeddings and themes:

Embedding Options:

  • model?: string - Embedding model (default: "text-embedding-3-small")
  • dimensions?: number - Embedding dimensions (default: 384)
  • batchSize?: number - Batch size for API calls (default: 100)
  • apiEndpoint?: string - Server endpoint URL (for server-side usage)
  • onError?: (error: Error) => void - Error callback

Theme Options:

  • model?: string - Theme generation model (default: "gpt-4-turbo-preview")
  • minThemes?: number - Minimum number of themes (default: 5)
  • maxThemes?: number - Maximum number of themes (default: 10)
  • apiEndpoint?: string - Server endpoint URL (for server-side usage)

Zoom and Pan

The visualization supports interactive zoom and pan:

  • Scroll wheel: Zoom in/out (towards mouse cursor)
  • Click and drag: Pan around the visualization
  • Auto-fit: Automatically centers and zooms to fit all hexes on initial load

The canvas has a visible background and border to show the visualization boundaries.

API Reference

LivingHive Component

Props

| Prop | Type | Required | Default | Description | | ---------------------- | -------------------------- | -------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------- | | stories | Story<T>[] | Yes | - | Array of stories to visualize | | embeddings | Map<string, number[]> | Yes | - | Pre-generated embeddings (Map of storyId to embedding vector). Can be empty Map if not yet loaded. | | themes | Theme[] | Yes | - | Pre-generated themes. Can be empty array if not yet loaded. | | loading | boolean | No | - | Loading state to show shimmer while data is being fetched | | openaiApiKey | string | No | - | Not used by component (only needed when using helper utilities like StoryDataGenerator) | | apiEndpoint | string | No | - | Custom endpoint used by helper utilities in server-side mode | | colorPalette | string[] | No | Warm palette | Array of hex color strings | | onHexClick | (story, theme) => void | No | - | Callback when a hex is clicked or activated via keyboard navigation | | onError | (error) => void | No | - | Error handler callback | | onThemesChange | (themes) => void | No | - | Callback when themes are updated | | onAssignmentsChange | (assignments) => void | No | - | Callback when story-to-theme assignments change | | className | string | No | - | Additional CSS classes | | canvasWidth | number \| string | No | 100% | Optional canvas width. Numbers are treated as pixel values; strings can be any CSS length. | | canvasHeight | number \| string | No | calc(100vh - 312px) | Optional canvas height. Numbers are treated as pixel values; strings can be any CSS length. | | config | Partial<PlacementConfig> | No | - | Canvas/hex configuration (used as internal defaults for placement math). | | workerUrl | string | No | prop → env → "/workers/umap-worker.js" | URL that resolves to the compiled UMAP worker asset. | | throwIfMissingWorker | boolean | No | true | When false, the component surfaces worker issues via error state instead of throwing. |

Types

interface BaseStory {
  id: string
  text: string
}

interface Theme {
  id: string
  label: string
}

interface PlacementConfig {
  canvasWidth: number
  canvasHeight: number
  hexRadius: number
  margin: number
}

StoryDataGenerator Class

const generator = new StoryDataGenerator<T extends BaseStory>(
  stories: T[],
  apiKey: string
)

// Methods:
await generator.generateEmbeddings(options?: GenerateEmbeddingsOptions): Promise<Map<string, number[]>>
await generator.generateThemes(embeddings?: Map<string, number[]>, options?: GenerateThemesOptions): Promise<Theme[]>
generator.assignStoriesToThemes(embeddings?: Map<string, number[]>, themes?: Theme[]): Map<string, string>

useUMAPPlacement Hook

If you prefer a hook for computing coordinates inside your own components, useUMAPPlacement remains available. It accepts the same workerUrl and throwIfMissingWorker options as the component and falls back to the environment variables / default URL when no prop is supplied:

import { useUMAPPlacement, DEFAULT_WORKER_URL } from '@hively/living-hive'

const { computePlacement, loading, error } = useUMAPPlacement({
  workerUrl: DEFAULT_WORKER_URL,
  throwIfMissingWorker: false,
})

Interaction

Keyboard Navigation

  • Arrow Keys: Navigate between hexes
  • Enter/Space: Trigger onHexClick callback for focused hex
  • Escape: Clear selection

Mouse Controls

  • Scroll Wheel: Zoom in/out (towards cursor position)
  • Click and Drag: Pan around the visualization
  • Click Hex: Trigger onHexClick callback with story and theme data

Accessibility

The component includes:

  • ARIA labels and roles
  • Keyboard navigation support
  • Focus management
  • Screen reader friendly

Error Handling

Errors can be handled in two ways:

  1. Callback: Use the onError prop for error handling integration (e.g., Sentry)
  2. Try/Catch: Errors are also thrown and can be caught in try/catch blocks
<LivingHive
  stories={stories}
  embeddings={embeddings}
  themes={themes}
  onError={error => {
    // Send to error tracking service
    Sentry.captureException(error)
  }}
/>

Server-Side Setup

For server-side embedding generation, create an API endpoint that accepts:

{
  "text": "Story text here"
}

And returns:

{
  "embedding": [0.1, 0.2, ...]
}

Note: If you're using server-side embeddings (apiEndpoint), you can also provide a server-side theme generation endpoint. The library will automatically call {apiEndpoint.replace('/embeddings', '/themes')} for theme generation if using server-side mode.

Example Netlify Function:

import { Handler } from '@netlify/functions'

export const handler: Handler = async event => {
  const { text } = JSON.parse(event.body || '{}')

  const response = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      input: text,
      model: 'text-embedding-3-small',
      dimensions: 384,
    }),
  })

  const data = await response.json()

  return {
    statusCode: 200,
    body: JSON.stringify({ embedding: data.data[0].embedding }),
  }
}

Development Scripts

Building the package

# Compile the worker asset and library bundles
npm run build

# Or run each step individually
npm run build:worker
npm run build:library

Quality and Testing

Run these commands before opening a pull request:

# Static analysis
npm run lint
npm run format:check
npm run type-check

# Unit tests
npm test

Data Utilities

The project includes several utility scripts:

# Fetch stories from Reddit and sanitize with OpenAI
npm run fetch-stories

# Generate embeddings for sample stories (saves to mockEmbeddings.json)
npm run regenerate-embeddings

# Generate themes for sample stories (saves to mockThemes.json)
npm run regenerate-themes

These scripts require an OpenAI API key in .env.local:

VITE_OPENAI_API_KEY=sk-your-key-here

Examples

See the examples/ directory for a complete working example:

  • Basic Example: Features stories from r/work Reddit, theme legend, zoom/pan controls, and support for pre-generated embeddings/themes

To run the example:

# Install dependencies (from project root)
npm install

# Set up environment variables (see .env.local.example)
cp .env.local.example .env.local
# Edit .env.local and add your OpenAI API key

# Run the example app
npm run dev

The examples app will start at http://localhost:5173.

Using Mock Mode (No API Calls)

Deployment to Netlify

The examples directory includes a netlify.toml configuration file. To deploy:

  1. Push your code to a Git repository
  2. Connect the repository to Netlify
  3. Set build command: npm install && npm run build:examples
  4. Set publish directory: examples/dist

Default Color Palette

The default warm color palette includes:

  • #4F81B0 - Blue
  • #AEBEC5 - Light Gray
  • #DAA5AD - Pink
  • #FF6E7F - Coral
  • #CDB15E - Gold

Performance Considerations

  • UMAP computation runs in a web worker to avoid blocking the UI
  • Embeddings use batch API calls (up to 100 stories per request) for efficiency
  • Built-in caching prevents duplicate embedding generation
  • Retry logic with exponential backoff handles rate limiting gracefully
  • Pre-generated embeddings/themes eliminate API calls entirely
  • Canvas rendering is optimized for smooth interactions
  • Zoom and pan are hardware-accelerated

Version History

See CHANGELOG.md for a complete list of changes.

Breaking Changes in 1.0.0

Version 1.0.0 includes breaking changes to simplify the API and give you more control over dialog/popover implementations:

  • Removed internal Dialog/Popover: The component no longer manages dialogs internally
  • Removed Popover exports: Popover, PopoverTrigger, PopoverContent are no longer available
  • Removed props: dialogConfig and renderStory props have been removed

Migration required: If you're upgrading from 0.4.0, see MIGRATION.md for detailed migration instructions.

License

MIT

Contributing

Contributions are welcome! See CONTRIBUTING.md for detailed guidelines on development workflow, testing, and pull request expectations.