npm package: @mmdalipour/graphie
Description: A powerful and lightweight React graph visualization library with interactive force-directed layouts
Author: Mohammad Alipour
Latest version: 0.1.0

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

[![npm version](https://img.shields.io/npm/v/graphie.svg)](https://www.npmjs.com/package/graphie)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

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

```bash
npm install graphie
# or
yarn add graphie
# or
pnpm add graphie
```

## Quick Start

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

```typescript
type GraphData = {
  nodes: Node[];
  edges: Edge[];
  groups?: GraphGroup[];  // Optional: explicitly define separate graph groups
};
```

#### Node

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

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

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

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

```typescript
type GraphOptions = {
  maxVisibleNodes?: number;
  animation?: {
    durationMs?: number;
    easing?: string;
  };
};
```

### Utility Functions

#### generateSampleGraph()

Generates a sample hierarchical graph data for testing and demonstration purposes.

```typescript
import { generateSampleGraph } from 'graphie';

const sampleData = generateSampleGraph();
```

#### generateMultipleGraphs()

Generates multiple separated graphs for demonstration of the multi-graph feature.

```typescript
import { generateMultipleGraphs } from 'graphie';

const multiGraphData = generateMultipleGraphs();
```

#### getNeighborhood(nodeId, data, depth)

Gets the neighborhood subgraph of a specific node up to a certain depth.

```typescript
import { getNeighborhood } from 'graphie';

const neighborhood = getNeighborhood('node-1', graphData, 2);
```

**Parameters:**
- `nodeId` (string): The ID of the node to start from
- `data` (GraphData): The complete graph data
- `depth` (number): How many levels deep to traverse (default: 1)

#### detectGraphComponents(data)

Automatically detects disconnected graph components using a breadth-first search algorithm.

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

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

```tsx
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:

```tsx
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:

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

```tsx
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:

```tsx
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:**

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

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

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

```tsx
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:

```tsx
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 `onNodeClick` callback 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:
1. Saves the current state to history
2. Finds all nodes within 2 degrees of separation
3. Smoothly transitions to show only the neighborhood
4. 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 `groups` property
- 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](https://reactjs.org/)
- Canvas 2D API
- Custom physics simulation engine