npm package: @superlinearspace/supertopo
Description: > 基于 Cytoscape.js 的拓扑图渲染组件
Latest version: 0.0.1

# SuperTopo

> 基于 Cytoscape.js 的拓扑图渲染组件

一个轻量级的拓扑图库，同时支持 React/Next.js 和原生 HTML/JS。

## 功能特性

- ✅ **Clos 布局** - 自动为 Clos 拓扑网络计算最佳布局位置
- ✅ **Cytoscape.js 渲染** - 利用 Cytoscape.js 强大的可视化能力
- ✅ **TypeScript 支持** - 完整的类型定义
- ✅ **性能优化** - 支持大规模拓扑图渲染
- ✅ **双模式支持** - React 组件 + Vanilla JS（独立文件）
  - **React 版本**: 用于 Next.js/React 项目
  - **Vanilla 版本**: 用于原生 HTML/JS 项目（包含所有依赖，无需构建工具）

## 安装

```bash
pnpm add @repo/supertopo
```

## 使用方式

SuperTopo 提供两种使用方式：

### 方式 1: React/Next.js（推荐用于 React 项目）

```tsx
import { SuperTopo } from "@repo/supertopo"

export default function Page() {
  const graphData = {
    nodes: [
      { data: { id: "spine0", label: "Spine 0", type: "spine", size: 40, color: "#3B82F6" } },
      { data: { id: "leaf0", label: "Leaf 0", type: "leaf", size: 35, color: "#0D9488" } },
      { data: { id: "server0", label: "Server 0", type: "server", size: 25, color: "#F97316" } },
    ],
    edges: [
      { data: { source: "spine0", target: "leaf0" } },
      { data: { source: "leaf0", target: "server0" } },
    ],
  }

  return (
    <div>
      <h1>Network Topology</h1>
      <SuperTopo 
        data={graphData}
        layout="clos"
        height={600}
      />
    </div>
  )
}
```

### 方式 2: Vanilla JS（用于原生 HTML/JS 项目）

#### 步骤 1: 构建 Vanilla 版本

```bash
cd packages/supertopo
pnpm build
```

#### 步骤 2: 复制生成的文件

```bash
cp dist/supertopo-vanilla.js /path/to/your/project/static/
```

#### 步骤 3: 在 HTML 中使用

```html
<!DOCTYPE html>
<html>
<head>
  <title>Network Topology</title>
  <script src="/static/supertopo-vanilla.js"></script>
  <style>
    #topology { width: 100%; height: 600px; border: 1px solid #ccc; }
  </style>
</head>
<body>
  <div id="topology"></div>
  <script>
    const topo = new SuperTopo({
      container: '#topology',
      nodes: [
        { id: 'spine0', label: 'Spine 0', type: 'spine', size: 40, color: '#3B82F6' },
        { id: 'leaf0', label: 'Leaf 0', type: 'leaf', size: 35, color: '#0D9488' },
        { id: 'server0', label: 'Server 0', type: 'server', size: 25, color: '#F97316' }
      ],
      edges: [
        { source: 'spine0', target: 'leaf0' },
        { source: 'leaf0', target: 'server0' }
      ],
      layout: 'clos',
      fit: true
    });
  </script>
</body>
</html>
```

**Vanilla API 详情：**
- `container` - 容器选择器（如 `#topology`）或 DOM 元素
- `nodes` - 节点数组（简化格式或完整格式）
- `edges` - 边数组（简化格式或完整格式）
- `layout` - 布局算法名称（`clos`, `random`, `grid`）
- `layoutOptions` - 布局选项
- `fit` - 是否自动适应视图（默认 true）
- `events` - 事件处理器

#### Vanilla JS 示例（在 FastAPI 中）

```html
<!-- templates/topology.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Network Topology</title>
  <script src="/static/supertopo-vanilla.js"></script>
  <style>
    #topology { width: 100%; height: 600px; border: 1px solid #ddd; }
  </style>
</head>
<body>
  <h1>NUNet Topology</h1>
  <div id="topology"></div>
  <script>
    const topo = new SuperTopo({
      container: '#topology',
      nodes: {{ nodes|tojson|safe }},
      edges: {{ edges|tojson|safe }},
      layout: 'clos',
      layoutOptions: {
        suScope: {
          include: (node) => String(node.id).toLowerCase().includes('gpu')
        }
      },
      events: {
        click: (evt) => console.log('Clicked:', evt.target.id())
      },
      fit: true
    });
  </script>
</body>
</html>
```

```python
# FastAPI router
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

router = APIRouter()
templates = Jinja2Templates(directory="app/templates")

@router.get("/", response_class=HTMLResponse)
async def topology_page(request: Request):
    # 假设这是从数据库获取的拓扑数据
    nodes = [
        {'id': 'spine0', 'label': 'Spine 0', 'type': 'spine', 'size': 40, 'color': '#3B82F6'},
        {'id': 'leaf0', 'label': 'Leaf 0', 'type': 'leaf', 'size': 35, 'color': '#0D9488'},
        {'id': 'server0', 'label': 'Server 0', 'type': 'server', 'size': 25, 'color': '#F97316'},
    ]
    edges = [
        {'source': 'spine0', 'target': 'leaf0'},
        {'source': 'leaf0', 'target': 'server0'},
    ]
    
    return templates.TemplateResponse("topology.html", {
        "request": request,
        "nodes": nodes,
        "edges": edges
    })
```

## API 参考

### React 版本 API

#### SuperTopo Props

| Prop | 类型 | 默认值 | 描述 |
|------|------|---------|------|
| `data` | `GraphData` | - | 图数据（节点和边） |
| `layout` | `string` | `"clos"` | 布局算法名称 |
| `height` | `string \| number` | `"600px"` | 画布高度 |
| `width` | `string \| number` | `"100%"` | 画布宽度 |
| `layoutOptions` | `Record<string, unknown>` | - | 布局选项 |

### Vanilla 版本 API

#### 构造函数选项

| 选项 | 类型 | 默认值 | 描述 |
|------|------|---------|------|
| `container` | `string \| HTMLElement` | - | 容器（必需） |
| `nodes` | `Array` | `[]` | 节点数组（简化格式或完整格式） |
| `edges` | `Array` | `[]` | 边数组（简化格式或完整格式） |
| `layout` | `string` | - | 布局算法名称 |
| `layoutOptions` | `Object` | - | 布局选项 |
| `fit` | `boolean` | `true` | 是否自动适应视图 |
| `padding` | `number` | `10` | 适应视图的内边距 |
| `events` | `Object` | - | 事件处理器 |

#### 实例方法

```javascript
// 应用布局
topo.applyLayout('clos', options);

// 适应视图
topo.fit(padding);

// 获取节点数据
const nodeData = topo.getNode('spine0');

// 获取边数据
const edgeData = topo.getEdge('edge0');

// 设置节点属性
topo.setNodeAttribute('spine0', 'label', 'New Label');

// 销毁实例
topo.destroy();
```

### 图数据格式

```typescript
interface NodeData {
  id: string | number
  label?: string
  type?: string
  size?: number
  color?: string
}

interface EdgeData {
  source: string | number
  target: string | number
  id?: string
  label?: string
}

interface TopoNode {
  group?: "nodes"
  data: NodeData
  position?: { x: number; y: number }
}

interface TopoEdge {
  group?: "edges"
  data: EdgeData
}

interface GraphData {
  nodes: TopoNode[]
  edges: TopoEdge[]
}
```

**注意**：在实际使用中，可以只传入 `data` 对象，Cytoscape 会自动将其转换为完整的元素定义。因此，为了简化使用，示例代码中直接使用了简化的格式。

**简化格式（Vanilla JS 推荐）：**
```javascript
{
  nodes: [
    { id: 'spine0', label: 'Spine 0', type: 'spine' },
    { id: 'leaf0', label: 'Leaf 0', type: 'leaf' },
  ],
  edges: [
    { source: 'spine0', target: 'leaf0' }
  ]
}
```

**完整格式：**
```javascript
{
  nodes: [
    { data: { id: 'spine0', label: 'Spine 0', type: 'spine' } },
  ],
  edges: [
    { data: { source: 'spine0', target: 'leaf0' } }
  ]
}
```

### 布局选项

Clos 布局支持的选项：

| 选项 | 类型 | 默认值 | 描述 |
|------|------|---------|------|
| `devicesPerRow` | `number` | `8` | 每行最大设备数 |
| `horizontalSpacingFactor` | `number` | `2.0` | SU 内横向间距倍数 |
| `spineToLeafSpacingFactor` | `number` | `8.0` | Spine 到 Leaf 的间距倍数 |
| `suVerticalSpacingFactor` | `number` | `4.0` | SU 内纵向间距倍数 |
| `suSpacingFactor` | `number` | `2.0` | SU 之间间距倍数 |
| `spineCoverageFactor` | `number` | `0.8` | Spine 覆盖宽度比例 |
| `scaleFactor` | `number` | `0.5` | 整体缩放因子 |
| `suScope` | `SUScope` | - | SU 范围配置 |

#### SU 范围配置 (suScope)

`suScope` 用于控制哪些服务器参与 SU 分组判断。回调函数接收 Cytoscape 节点对象（Vanilla 版本通过 `node.id()` 等方法访问）。

**Vanilla JS 示例：**
```javascript
layoutOptions: {
  suScope: {
    // 只考虑满足条件的服务器参与分组
    include: (node) => String(node.id()).toLowerCase().includes('gpu'),
    
    // 排除满足条件的服务器
    exclude: (node) => node.data('type') === 'switch'
  }
}
```

**常用方法：**
- `node.id()` - 获取节点ID
- `node.data('label')` - 获取节点标签
- `node.data('type')` - 获取节点类型
- `node.data('size')` - 获取节点大小

## 节点类型

SuperTopo 通过节点的 `type` 属性自动识别节点类型：

- **spine** - 脊柱交换机
- **leaf** - 叶子交换机  
- **其他** - 服务器节点

## 示例

### 快速体验（独立 HTML 示例）

1. 构建 Vanilla 版本：
   ```bash
   cd packages/supertopo
   pnpm build
   ```

2. 在浏览器中打开独立示例：
   ```bash
   open examples/simple-standalone.html
   # 或在 Windows/Linux 中使用浏览器打开
   ```

这个示例包含：
- 2 个 Spine 节点
- 2 个 Leaf 节点
- 4 个 Server 节点
- 完整的 Clos 布局
- 点击事件交互

### FastAPI 集成示例

完整的 FastAPI 模板示例位于 `examples/fastapi-topology.html`，包含：
- 完整的页面样式
- 节点统计显示
- 图例说明
- 错误处理
- 事件处理

## 开发

### 构建

```bash
# 开发模式（监听文件变化）
pnpm dev

# 生产构建
pnpm build
```

### 输出文件

构建后会在 `dist/` 目录生成：
- `supertopo-vanilla.js` - Vanilla 版本（包含所有依赖，可直接在浏览器中使用）
- `supertopo-vanilla.js.map` - Source Map（用于调试）

## 许可证

MIT