An interactive React component for visualizing data as positioned dots with zoom, pan, and hover interactions. Extracted from the cybird visualization project.
- Interactive Dots: Display data points as SVG circles with customizable size, color, and stroke
- Zoom & Pan: Smooth zoom with mouse wheel + ctrl/cmd or trackpad pinch, pan with mouse wheel or trackpad
- Hover Interactions: Customizable hover callbacks with automatic debouncing during zoom operations
- Click Interactions: Handle dot clicks with custom callbacks
- Collision Detection: Optional D3 force simulation to prevent dot overlap
- Automatic Layout: Calculates optimal viewBox from data bounds with configurable margins
- Performance Optimized: Efficient rendering and interaction handling for large datasets
npm install react-dot-visualizationimportReact,{useState}from'react';import{DotVisualization}from'react-dot-visualization';constMyComponent=()=>{// Just x, y coordinates - that's it!constdata=[{x: 100,y: 150},{x: 200,y: 100},{x: 150,y: 200}];const[hovered,setHovered]=useState(null);return(<divstyle={{width: '100%',height: '400px'}}><DotVisualizationdata={data}onHover={setHovered}/>{hovered&&<div>Hovering: {hovered.name}</div>}</div>);};That's literally all the code you need!
The component automatically provides:
- ✅ Zoom: Ctrl/Cmd + mouse wheel or trackpad pinch
- ✅ Pan: Mouse wheel or trackpad scroll
- ✅ Hover callbacks: Work during pan/zoom
- ✅ Collision detection: Prevents dot overlap
- ✅ Auto-generated IDs: No manual ID management
- ✅ Optimal layout: Calculates viewBox from data bounds
- ✅ Beautiful colors: Generated automatically
# Clone and install dependencies
git clone <your-repo>cd react-dot-visualization
npm install
# Start development server for testing
npm run dev
# Opens http://localhost:3011 with demo# Build library for distribution
npm run build:lib
# Link for local development in other projects
npm run link:localAfter running npm run link:local, you can use the library in other React projects:
# In your other projectcd ../my-other-project
npm link react-dot-visualizationThen import and use normally:
import{DotVisualization}from'react-dot-visualization';- Edit source files in
src/ - Test changes with
npm run dev - Rebuild library with
npm run build:lib - Linked projects automatically get updates
To test the built package:
# Navigate to the package directorycd react-dot-visualization
# Start development server
npm run dev
# Open in browser
open http://localhost:3011Test the interactions:
- Hover: Move mouse over dots to see hover callbacks
- Zoom: Ctrl/Cmd + mouse wheel or trackpad pinch
- Pan: Mouse wheel or trackpad scroll
- Click: Click dots to test click callbacks
| Prop | Type | Default | Description |
|---|---|---|---|
data | Array | [] | Array of data points with {x, y} required, optional {id, size, color, ...customData} |
onHover | Function | - | Callback when hovering over a dot: (item, event) => {} |
onLeave | Function | - | Callback when leaving a dot: (item, event) => {} |
onClick | Function | - | Callback when clicking a dot: (item, event) => {} |
onZoomStart | Function | - | Callback when zoom starts: (event) => {} |
onZoomEnd | Function | - | Callback when zoom ends: (event) => {} |
enableCollisionDetection | Boolean | true | Enable D3 force simulation to prevent dot overlap |
zoomExtent | Array | [0.7, 10] | Min/max zoom levels [min, max] |
scrollZoomModifier | 'meta' | 'alt' | 'meta-or-alt' | 'meta-or-alt' | Modifier accepted for scroll zoom; trackpad pinch is unaffected |
margin | Number | 0.1 | Margin around data bounds as fraction (0.1 = 10% margin) |
dotStroke | String | "#111" | Default stroke color for dots |
dotStrokeWidth | Number | 0.2 | Default stroke width for dots |
defaultColor | String | null | Default color for dots without color property |
defaultSize | Number | 2 | Default size for dots without size property |
useImages | Boolean | false | Enable image patterns inside dots (requires imageUrl or svgContent in data) |
imageProvider | Function | - | Function to provide image URLs: (id) => string | undefined |
hoverImageProvider | Function | - | Function to provide hover image URLs: (id) => string | undefined |
className | String | "" | CSS class name for the SVG element |
style | Object | {} | Inline styles for the SVG element |
Each data point should be an object with these properties:
{id: string|number,// Required: Unique identifierx: number,// Required: X coordinatey: number,// Required: Y coordinatesize?: number,// Optional: Dot radiuscolor?: string,// Optional: Fill color (CSS color value)imageUrl?: string,// Optional: URL to bitmap image (JPG, PNG, etc.)svgContent?: string,// Optional: Raw SVG content for pattern
...customData// Optional: Any additional properties for your callbacks}You can display images inside the circular dots using two approaches:
Use the imageUrl property to display bitmap images (JPG, PNG, etc.):
constdata=[{id: 1,x: 100,y: 150,imageUrl: "/path/to/album-cover.jpg"// Local or remote image},{id: 2,x: 200,y: 100,imageUrl: "https://example.com/photo.png"// Remote image}];Use the svgContent property to embed raw SVG:
import*asjdenticonfrom'jdenticon';constdata=[{id: 1,x: 100,y: 150,svgContent: jdenticon.toSvg('user1',64)// Generated identicon},{id: 2,x: 200,y: 100,svgContent: '<svg xmlns="...">...</svg>'// Custom SVG}];To show images in dots, pass the useImages prop:
<DotVisualizationdata={data}useImages={true}// Enable image patternsdefaultSize={15}// Larger dots show images better/>- Automatic scaling: Images automatically resize to match each dot's size (10px dot = 10px image, 50px dot = 50px image)
- Smart cropping: Images are centered and cropped to fill the entire circle (like CSS
background-size: cover) - Aspect ratio preserved: Images maintain their proportions while filling the circle completely
- Zoom responsive: Images scale smoothly with zoom level - no pixelation or distortion
- Circular masking: Images are automatically clipped to perfect circles
- Preserves all interactions: Hover, click, zoom, collision detection all work normally
- Fallback to colors: Dots without images use normal color fills
- Performance optimized: Uses SVG patterns for efficient rendering
constAlbumViz=()=>{constalbums=[{id: 'album1',x: 100,y: 100,imageUrl: '/covers/dark-side-moon.jpg',title: 'Dark Side of the Moon',artist: 'Pink Floyd'},{id: 'album2',x: 200,y: 150,imageUrl: '/covers/abbey-road.jpg',title: 'Abbey Road',artist: 'The Beatles'}];return(<DotVisualizationdata={albums}useImages={true}defaultSize={20}// Larger dots for album coversonHover={(album)=>console.log(`${album.title} by ${album.artist}`)}/>);};For better performance with large datasets or when images need to be loaded asynchronously, use the imageProvider and hoverImageProvider props instead of adding imageUrl to each data point.
The traditional approach of adding imageUrl to data objects causes performance issues:
- ❌ Images are re-fetched every time positions update
- ❌ Expensive async operations on every render
- ❌ SVG patterns are recreated unnecessarily
- ❌ Poor performance during animations and interactions
Image providers solve this by separating image loading from position updates:
- ✅ Images are loaded once and cached by the parent component
- ✅ Position updates become pure mathematical operations
- ✅ SVG patterns are created once and reused
- ✅ Smooth animations without blocking async calls
import{DotVisualization}from'react-dot-visualization';constMusicVisualization=()=>{consttracks=[{id: 'track1',x: 100,y: 150,title: 'Song One'},{id: 'track2',x: 200,y: 100,title: 'Song Two'}];// Cache images in parent componentconst[artworkCache,setArtworkCache]=useState(newMap());// Preload images when tracks changeuseEffect(()=>{constloadArtwork=async()=>{constcache=newMap();for(consttrackoftracks){try{constimageUrl=awaitfetchArtworkForTrack(track.id);cache.set(track.id,imageUrl);}catch(error){// Handle missing artwork gracefullycache.set(track.id,undefined);}}setArtworkCache(cache);};loadArtwork();},[tracks]);// Simple image provider functionconstimageProvider=(id)=>artworkCache.get(id);return(<DotVisualizationdata={tracks}useImages={true}imageProvider={imageProvider}defaultSize={20}/>);};Use hoverImageProvider to show different images on hover (e.g., high-resolution versions):
constImageVisualization=()=>{const[thumbnailCache,setThumbnailCache]=useState(newMap());const[fullSizeCache,setFullSizeCache]=useState(newMap());constimageProvider=(id)=>thumbnailCache.get(id);consthoverImageProvider=(id)=>fullSizeCache.get(id);return(<DotVisualizationdata={data}useImages={true}imageProvider={imageProvider}hoverImageProvider={hoverImageProvider}hoverSizeMultiplier={1.5}/>);};// Fallback chain providerconstcreateFallbackProvider=(...providers)=>(id)=>{for(constproviderofproviders){constresult=provider(id);if(result)returnresult;}returnundefined;};// Category-based providerconstcreateCategoryProvider=(categoryMap,imageMap)=>(id)=>{constcategory=categoryMap.get(id);returnimageMap.get(category);};// Composed provider exampleconstMyVisualization=()=>{constprimaryProvider=(id)=>primaryCache.get(id);constfallbackProvider=(id)=>`/placeholder/${id}.png`;constimageProvider=createFallbackProvider(primaryProvider,fallbackProvider);return(<DotVisualizationdata={data}useImages={true}imageProvider={imageProvider}/>);};Old approach (slower performance):
constdata=[{id: 1,x: 100,y: 150,imageUrl: '/image1.jpg'},{id: 2,x: 200,y: 100,imageUrl: '/image2.jpg'}];<DotVisualizationdata={data}useImages={true}/>New approach (optimized performance):
constdata=[{id: 1,x: 100,y: 150},{id: 2,x: 200,y: 100}];constimageMap=newMap([[1,'/image1.jpg'],[2,'/image2.jpg']]);constimageProvider=(id)=>imageMap.get(id);<DotVisualizationdata={data}useImages={true}imageProvider={imageProvider}/>The component maintains backward compatibility - imageUrl properties still work, but imageProvider takes precedence when both are present.
import{DotVisualization}from'react-dot-visualization';constAdvancedExample=()=>{const[selectedItem,setSelectedItem]=useState(null);// Generate data with custom propertiesconstdata=Array.from({length: 200},(_,i)=>({id: i,x: Math.random()*1000,y: Math.random()*1000,size: Math.random()*8+2,color: `hsl(${i*137.508}deg, 70%, 50%)`,// Golden angle color distributioncategory: ['A','B','C'][Math.floor(Math.random()*3)],value: Math.random()*100}));return(<DotVisualizationdata={data}onHover={(item)=>console.log(`Hovering: ${item.category} - ${item.value}`)}onClick={(item)=>setSelectedItem(item)}onZoomStart={()=>setSelectedItem(null)}// Clear selection on zoomenableCollisionDetection={true}zoomExtent={[0.5,20]}margin={0.2}dotStroke="#333"dotStrokeWidth={1}style={{border: '2px solid #ddd',borderRadius: '8px'}}/>);};- Modern browsers with SVG and ES6+ support
- Tested with React 18+
react(peer dependency)react-dom(peer dependency)d3- For zoom/pan behavior and force simulation