@mmdalipour/graphie
v0.1.0
Published
A powerful and lightweight React graph visualization library with interactive force-directed layouts
Maintainers
Readme
Graphie
A powerful and lightweight React graph visualization library with interactive force-directed layouts built with a custom physics simulation engine and Canvas 2D rendering.
Features
- 🚀 High Performance: Canvas 2D rendering for smooth interactions with large graphs
- ⚡ Force-Directed Layout: Physics-based layout using custom simulation engine
- 🎨 Fully Configurable: Comprehensive configuration for colors, sizes, styling, and behavior - customize nodes, edges, containers, overlays, and camera settings
- 🖱️ Interactive: Pan, zoom, click, and hover interactions out of the box with visual feedback
- 📦 Lightweight: Zero dependencies, only requires React
- 🎯 TypeScript: Full TypeScript support with type definitions
- 🔍 Neighborhood Exploration: Click nodes to explore their connected subgraphs
- ⚙️ Custom Physics: Built-in force simulation with link, charge, center, and collision forces
- 🔀 Multiple Separated Graphs: Automatically detects and displays disconnected graph components in a single canvas
Installation
npm install graphie
# or
yarn add graphie
# or
pnpm add graphieQuick Start
import { Graphie, generateSampleGraph } from 'graphie';
function App() {
const graphData = generateSampleGraph();
return (
<Graphie
data={graphData}
height={600}
onNodeClick={(node) => {
console.log('Node clicked:', node);
}}
/>
);
}API Reference
Graphie Component
The main component for rendering graph visualizations.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| data | GraphData | Required | The graph data containing nodes and edges |
| width | string \| number | "100%" | Width of the graph container |
| height | number | 600 | Height of the graph container in pixels |
| onNodeClick | (node: Node) => void | undefined | Callback fired when a node is clicked |
| config | GraphConfig | {} | Visual configuration options for styling nodes, edges, and UI elements |
| options | GraphOptions | {} | Additional configuration options |
| className | string | "" | Additional CSS classes for the container |
Types
GraphData
type GraphData = {
nodes: Node[];
edges: Edge[];
groups?: GraphGroup[]; // Optional: explicitly define separate graph groups
};Node
type Node = {
id: string; // Unique identifier for the node
label?: string; // Display label for the node
meta?: Record<string, any>; // Additional metadata
x?: number; // Initial x position
y?: number; // Initial y position
color?: string; // Custom node color (hex or CSS color)
};Edge
type Edge = {
source: string; // ID of the source node
target: string; // ID of the target node
weight?: number; // Optional edge weight
meta?: Record<string, any>; // Additional metadata
};GraphGroup
type GraphGroup = {
id: string; // Unique identifier for the group
nodeIds: string[]; // IDs of nodes in this group
position?: { x: number; y: number }; // Optional fixed position for the group center
label?: string; // Optional label for the group
};GraphConfig
Complete visual configuration for customizing the graph appearance and behavior.
type GraphConfig = {
// Node styling
node?: {
defaultColor?: string; // Default node color (default: "#6366f1")
selectedColor?: string; // Color when node is selected (default: "#10b981")
hoveredColor?: string; // Color when node is hovered (default: "#f59e0b")
connectedColor?: string; // Color for nodes connected to hovered node (default: "#fcd34d")
radius?: number; // Node radius in pixels (default: 8)
borderColor?: string; // Default border color (default: "#e2e8f0")
hoveredBorderColor?: string; // Border color when hovered (default: "#fbbf24")
selectedBorderColor?: string; // Border color when selected (default: "#34d399")
connectedBorderColor?: string; // Border color for connected nodes (default: "#fde68a")
borderWidth?: number; // Border width in pixels (default: 1.5)
fontSize?: number; // Label font size in pixels (default: 12)
labelColor?: string; // Label text color (default: "#1e293b")
render?: (props: NodeRenderProps) => void; // Custom node render function
};
// Edge styling
edge?: {
defaultColor?: string; // Default edge color (default: "#94a3b8")
highlightedColor?: string; // Color for edges connected to hovered node (default: "#fb923c")
defaultWidth?: number; // Default edge width in pixels (default: 1.5)
highlightedWidth?: number; // Width for highlighted edges (default: 2.5)
};
// Container styling
container?: {
background?: string; // Background color (default: "#ffffff")
border?: string; // Border style (default: "1px solid #e2e8f0")
};
// Stats overlay styling
stats?: {
show?: boolean; // Show/hide stats overlay (default: true)
background?: string; // Background color (default: "rgba(255, 255, 255, 0.95)")
textColor?: string; // Primary text color (default: "#1e293b")
secondaryTextColor?: string; // Secondary text color (default: "#64748b")
position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'; // Position (default: "bottom-left")
};
// Back button styling
backButton?: {
background?: string; // Background color (default: "rgba(255, 255, 255, 0.95)")
textColor?: string; // Text color (default: "#334155")
border?: string; // Border style (default: "1px solid #e2e8f0")
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; // Position (default: "top-right")
};
// Camera and animation
camera?: {
initialZoom?: number; // Initial zoom level (default: 1)
minZoom?: number; // Minimum zoom level (default: 0.1)
maxZoom?: number; // Maximum zoom level (default: 5)
fitPadding?: number; // Padding when fitting graph to screen in pixels (default: 60)
animationDuration?: number; // Camera animation duration in milliseconds (default: 300)
};
};GraphOptions
type GraphOptions = {
maxVisibleNodes?: number;
animation?: {
durationMs?: number;
easing?: string;
};
};Utility Functions
generateSampleGraph()
Generates a sample hierarchical graph data for testing and demonstration purposes.
import { generateSampleGraph } from 'graphie';
const sampleData = generateSampleGraph();generateMultipleGraphs()
Generates multiple separated graphs for demonstration of the multi-graph feature.
import { generateMultipleGraphs } from 'graphie';
const multiGraphData = generateMultipleGraphs();getNeighborhood(nodeId, data, depth)
Gets the neighborhood subgraph of a specific node up to a certain depth.
import { getNeighborhood } from 'graphie';
const neighborhood = getNeighborhood('node-1', graphData, 2);Parameters:
nodeId(string): The ID of the node to start fromdata(GraphData): The complete graph datadepth(number): How many levels deep to traverse (default: 1)
detectGraphComponents(data)
Automatically detects disconnected graph components using a breadth-first search algorithm.
import { detectGraphComponents } from 'graphie';
const components = detectGraphComponents(graphData);
console.log(`Found ${components.length} separated graphs`);Parameters:
data(GraphData): The graph data to analyze
Returns: Array of GraphGroup objects representing disconnected components
calculateBoundingBox(nodes)
Calculates the bounding box for a set of nodes.
import { calculateBoundingBox } from 'graphie';
const bbox = calculateBoundingBox(nodes);
console.log(`Width: ${bbox.width}, Height: ${bbox.height}`);Parameters:
nodes(Node[]): Array of nodes with x and y positions
Returns: Object containing minX, minY, maxX, maxY, width, height, centerX, centerY
Examples
Basic Usage
import { Graphie } from 'graphie';
function BasicExample() {
const data = {
nodes: [
{ id: '1', label: 'Node 1' },
{ id: '2', label: 'Node 2' },
{ id: '3', label: 'Node 3' },
],
edges: [
{ source: '1', target: '2' },
{ source: '2', target: '3' },
],
};
return <Graphie data={data} height={500} />;
}Custom Node Colors
Individual node colors can be set using the color property on each node:
import { Graphie } from 'graphie';
function ColoredGraph() {
const data = {
nodes: [
{ id: '1', label: 'Red', color: '#ff6b6b' },
{ id: '2', label: 'Blue', color: '#4ecdc4' },
{ id: '3', label: 'Green', color: '#45b7d1' },
],
edges: [
{ source: '1', target: '2' },
{ source: '2', target: '3' },
],
};
return <Graphie data={data} height={500} />;
}Configuring Graph Appearance
Use the config prop to customize the entire graph's visual appearance:
import { Graphie, GraphConfig } from 'graphie';
function CustomStyledGraph() {
const data = {
nodes: [
{ id: '1', label: 'Node 1' },
{ id: '2', label: 'Node 2' },
{ id: '3', label: 'Node 3' },
],
edges: [
{ source: '1', target: '2' },
{ source: '2', target: '3' },
],
};
const config: GraphConfig = {
node: {
defaultColor: "#8b5cf6", // Purple nodes
hoveredColor: "#ec4899", // Pink on hover
selectedColor: "#10b981", // Green when selected
connectedColor: "#f59e0b", // Orange for connected nodes
radius: 12, // Larger nodes
fontSize: 14, // Larger labels
},
edge: {
defaultColor: "#94a3b8", // Gray edges
highlightedColor: "#ec4899", // Pink highlighted edges
defaultWidth: 2, // Thicker edges
},
container: {
background: "#0f172a", // Dark background
border: "2px solid #334155", // Custom border
},
stats: {
show: true,
position: "top-left", // Move stats to top-left
background: "rgba(15, 23, 42, 0.95)",
textColor: "#f1f5f9",
secondaryTextColor: "#94a3b8",
},
backButton: {
position: "top-right",
background: "rgba(15, 23, 42, 0.95)",
textColor: "#f1f5f9",
border: "1px solid #334155",
},
camera: {
minZoom: 0.5,
maxZoom: 3,
fitPadding: 80,
animationDuration: 500, // Slower animations
},
};
return (
<Graphie
data={data}
config={config}
height={600}
onNodeClick={(node) => console.log('Clicked:', node)}
/>
);
}Dark Theme Example
import { Graphie, GraphConfig } from 'graphie';
function DarkThemeGraph() {
const darkConfig: GraphConfig = {
node: {
defaultColor: "#60a5fa",
hoveredColor: "#fbbf24",
selectedColor: "#34d399",
labelColor: "#f1f5f9",
},
edge: {
defaultColor: "#475569",
highlightedColor: "#fbbf24",
},
container: {
background: "#0f172a",
border: "1px solid #1e293b",
},
stats: {
background: "rgba(15, 23, 42, 0.9)",
textColor: "#f1f5f9",
secondaryTextColor: "#94a3b8",
},
backButton: {
background: "rgba(15, 23, 42, 0.9)",
textColor: "#f1f5f9",
border: "1px solid #334155",
},
};
const data = generateSampleGraph();
return <Graphie data={data} config={darkConfig} height={600} />;
}Custom Node Rendering
You can completely customize how nodes are rendered by providing a custom render function. Since Graphie uses Canvas 2D rendering, your custom function receives the canvas context and node data:
import { Graphie, GraphConfig, NodeRenderProps } from 'graphie';
function CustomNodesGraph() {
const config: GraphConfig = {
node: {
radius: 12,
render: ({ ctx, node, state, config, colors }: NodeRenderProps) => {
const { isHovered, isSelected, isConnected } = state;
// Choose color based on state
let fillColor = node.color || colors.defaultColor;
if (isHovered) fillColor = colors.hoveredColor;
else if (isSelected) fillColor = colors.selectedColor;
else if (isConnected) fillColor = colors.connectedColor;
// Draw custom hexagon shape
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (Math.PI / 3) * i;
const x = node.x + config.radius * Math.cos(angle);
const y = node.y + config.radius * Math.sin(angle);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.fillStyle = fillColor;
ctx.fill();
// Optional: Add a border for selected nodes
if (isSelected) {
ctx.strokeStyle = colors.selectedBorderColor;
ctx.lineWidth = 2 / config.scale;
ctx.stroke();
}
// Draw label
if (node.label) {
ctx.font = `${config.fontSize}px Inter, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillStyle = colors.labelColor;
ctx.fillText(node.label, node.x, node.y + config.radius + 4);
}
}
}
};
const data = generateSampleGraph();
return <Graphie data={data} config={config} height={600} />;
}NodeRenderProps Interface:
interface NodeRenderProps {
ctx: CanvasRenderingContext2D; // Canvas 2D context for drawing
node: Node & { x: number; y: number }; // Node data with position
state: {
isHovered: boolean; // True if mouse is over this node
isSelected: boolean; // True if node is selected (clicked)
isConnected: boolean; // True if node is connected to hovered node
};
config: {
radius: number; // Configured node radius
fontSize: number; // Configured font size (already scaled)
scale: number; // Current zoom scale factor
};
colors: {
defaultColor: string;
selectedColor: string;
hoveredColor: string;
connectedColor: string;
borderColor: string;
hoveredBorderColor: string;
selectedBorderColor: string;
connectedBorderColor: string;
labelColor: string;
};
}With custom rendering, you can create:
- Different shapes (squares, hexagons, stars, etc.)
- Icons or images rendered on nodes
- Complex visual effects
- Data-driven visualizations based on node metadata
- Animated nodes
Interactive Graph with Callbacks
import { Graphie, Node } from 'graphie';
import { useState } from 'react';
function InteractiveGraph() {
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
const data = {
nodes: [
{ id: '1', label: 'Node 1' },
{ id: '2', label: 'Node 2' },
{ id: '3', label: 'Node 3' },
],
edges: [
{ source: '1', target: '2' },
{ source: '2', target: '3' },
],
};
return (
<div>
<Graphie
data={data}
height={500}
onNodeClick={(node) => setSelectedNode(node)}
/>
{selectedNode && (
<div>
<h3>Selected Node</h3>
<p>ID: {selectedNode.id}</p>
<p>Label: {selectedNode.label}</p>
</div>
)}
</div>
);
}Dynamic Graph Updates
import { Graphie, GraphData } from 'graphie';
import { useState } from 'react';
function DynamicGraph() {
const [data, setData] = useState<GraphData>({
nodes: [{ id: '1', label: 'Node 1' }],
edges: [],
});
const addNode = () => {
const newId = String(data.nodes.length + 1);
setData({
nodes: [...data.nodes, { id: newId, label: `Node ${newId}` }],
edges: data.edges,
});
};
return (
<div>
<button onClick={addNode}>Add Node</button>
<Graphie data={data} height={500} />
</div>
);
}Multiple Separated Graphs
Graphie automatically detects and displays disconnected graph components in a single canvas. Each component is positioned in its own region and maintains independent physics simulation.
import { Graphie, generateMultipleGraphs, detectGraphComponents } from 'graphie';
function MultiGraphExample() {
const graphData = generateMultipleGraphs();
const components = detectGraphComponents(graphData);
return (
<div>
<p>Detected {components.length} separated graphs</p>
<Graphie data={graphData} height={700} />
</div>
);
}You can also manually define graph groups with custom positions:
import { Graphie, GraphData } from 'graphie';
function CustomGroupedGraphs() {
const data: GraphData = {
nodes: [
// Graph 1
{ id: '1', label: 'A', color: '#ff6b6b' },
{ id: '2', label: 'B', color: '#ff6b6b' },
// Graph 2
{ id: '3', label: 'C', color: '#4ecdc4' },
{ id: '4', label: 'D', color: '#4ecdc4' },
],
edges: [
{ source: '1', target: '2' },
{ source: '3', target: '4' },
],
groups: [
{
id: 'group1',
nodeIds: ['1', '2'],
label: 'Team A',
position: { x: 200, y: 300 } // Optional fixed position
},
{
id: 'group2',
nodeIds: ['3', '4'],
label: 'Team B',
position: { x: 600, y: 300 }
}
]
};
return <Graphie data={data} height={600} />;
}Features in Detail
Pan and Zoom
Users can:
- Pan: Click and drag the canvas to move around
- Zoom: Use mouse wheel to zoom in/out
- The zoom is centered around the cursor position for intuitive navigation
Node Interaction
- Click: Click on nodes to trigger the
onNodeClickcallback and explore neighborhoods - Hover: Visual feedback on hover with color changes and cursor updates
Force-Directed Layout
The graph uses a custom physics simulation engine with:
- Link force: Keeps connected nodes at a comfortable distance using spring-like forces
- Charge force: Applies repulsion between nodes to prevent overlap
- Center force: Pulls nodes toward the center of the canvas
- Collision force: Prevents nodes from overlapping with configurable collision radius
Neighborhood Exploration
When a node is clicked, the graph automatically:
- Saves the current state to history
- Finds all nodes within 2 degrees of separation
- Smoothly transitions to show only the neighborhood
- Shows a "Back" button to return to the previous view
Multiple Separated Graphs
Graphie intelligently handles disconnected graph components:
Automatic Detection
- Uses breadth-first search to identify disconnected components
- No configuration needed - works automatically with any graph data
- Components are detected when there are no edges connecting different groups of nodes
Smart Layout
- Multiple graphs are arranged in a grid layout for optimal viewing
- Grid dimensions automatically calculated based on number of components
- Each component gets its own space with configurable padding
Independent Physics
- Each graph component has its own force simulation
- Nodes within a component interact with each other
- Components are kept separated by repulsion forces
- Maintains the hexagonal layout pattern within each component
Manual Control (Optional)
- Optionally specify graph groups explicitly via the
groupsproperty - Define custom positions for each component
- Add labels to identify different graph groups
Browser Support
Graphie works in all modern browsers that support:
- Canvas 2D
- ES6+
- React 18+
Performance
- Optimized Canvas 2D rendering
- Efficient custom force simulation with optimized physics calculations
- Handles graphs with hundreds of nodes smoothly
- RequestAnimationFrame for smooth 60fps animations
- No external dependencies for maximum performance
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT © Graphie
Acknowledgments
Built with:
- React
- Canvas 2D API
- Custom physics simulation engine
