@super-linear/supertopo 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,60 @@
1
+ import { NodeSingular, EdgeDataDefinition, ElementDefinition, NodeDataDefinition, Position } from 'cytoscape';
2
+
3
+ type NodeData = NodeDataDefinition;
4
+ type EdgeData = EdgeDataDefinition;
5
+ type CytoNode = NodeSingular;
6
+ type EventCallback = (id: string, data: any) => void;
7
+ type Events = Partial<Record<string, EventCallback>>;
8
+ /**
9
+ * Topology 节点元素定义 - group 为可选,方便使用
10
+ */
11
+ interface TopoNode extends Omit<ElementDefinition, 'group'> {
12
+ group?: "nodes";
13
+ data: NodeData;
14
+ position?: Position;
15
+ }
16
+ /**
17
+ * Topology 边元素定义 - group 为可选,方便使用
18
+ */
19
+ interface TopoEdge extends Omit<ElementDefinition, 'group'> {
20
+ group?: "edges";
21
+ data: EdgeData;
22
+ }
23
+ /**
24
+ * 图数据
25
+ */
26
+ interface GraphData {
27
+ nodes: TopoNode[];
28
+ edges: TopoEdge[];
29
+ }
30
+ /**
31
+ * SU 范围配置 - 用于控制 SU 分组时使用哪些服务器
32
+ * include/exclude 回调函数接收 Cytoscape 节点对象(CytoNode)
33
+ *
34
+ * @example
35
+ * suScope: {
36
+ * // Cytoscape 节点对象,可以使用 node.id() 或 node.data() 访问数据
37
+ * include: (node) => String(node.data("id") || node.id()).toLowerCase().includes("gpu"),
38
+ *
39
+ * // 根据类型过滤
40
+ * exclude: (node) => node.data("type") === "switch"
41
+ * }
42
+ */
43
+ interface SUScope {
44
+ /** 只考虑满足条件的服务器参与 SU 分组判断 */
45
+ include?: (node: CytoNode) => boolean;
46
+ /** 排除满足条件的服务器后参与 SU 分组判断 */
47
+ exclude?: (node: CytoNode) => boolean;
48
+ }
49
+ /**
50
+ * 用于过滤回调的简化节点数据结构
51
+ * 这是扁平结构,直接包含 id, type, label, size 等属性
52
+ */
53
+ interface NodeDataForFilter {
54
+ id: string;
55
+ label: string;
56
+ type?: string;
57
+ size: number;
58
+ }
59
+
60
+ export type { CytoNode, EdgeData, EventCallback, Events, GraphData, NodeData, NodeDataForFilter, SUScope, TopoEdge, TopoNode };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@super-linear/supertopo",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "main": "dist/cjs/index.js",
9
+ "module": "dist/esm/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "devDependencies": {
15
+ "@rollup/plugin-commonjs": "^29.0.0",
16
+ "@rollup/plugin-node-resolve": "^16.0.3",
17
+ "@rollup/plugin-terser": "^0.4.4",
18
+ "@rollup/plugin-typescript": "^12.3.0",
19
+ "@types/react": "^19.2.14",
20
+ "react": "^19.2.4",
21
+ "rollup": "^4.59.0",
22
+ "rollup-plugin-dts": "^6.3.0",
23
+ "rollup-plugin-peer-deps-external": "^2.2.4",
24
+ "tslib": "^2.8.1",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "dependencies": {
28
+ "cytoscape": "^3.33.1"
29
+ },
30
+ "peerDependencies": {
31
+ "react": "^19.2.4"
32
+ },
33
+ "scripts": {
34
+ "rollup": "rollup -c --bundleConfigAsCjs",
35
+ "test": "echo \"Error: no test specified\" && exit 1"
36
+ }
37
+ }
@@ -0,0 +1,39 @@
1
+ import resolve from "@rollup/plugin-node-resolve";
2
+ import commonjs from "@rollup/plugin-commonjs";
3
+ import typescript from "@rollup/plugin-typescript";
4
+ import dts from "rollup-plugin-dts";
5
+ import terser from "@rollup/plugin-terser";
6
+ import peerDepsExternal from "rollup-plugin-peer-deps-external";
7
+
8
+ const packageJson = require("./package.json");
9
+
10
+ export default [
11
+ {
12
+ input: "src/index.ts",
13
+ output: [
14
+ {
15
+ file: packageJson.main,
16
+ format: "cjs",
17
+ sourcemap: true,
18
+ },
19
+ {
20
+ file: packageJson.module,
21
+ format: "esm",
22
+ sourcemap: true,
23
+ },
24
+ ],
25
+ plugins: [
26
+ peerDepsExternal(),
27
+ resolve(),
28
+ commonjs(),
29
+ typescript({ tsconfig: "./tsconfig.json", declaration: false, declarationMap: false }),
30
+ terser(),
31
+ ],
32
+ external: ["react", "react-dom"],
33
+ },
34
+ {
35
+ input: "src/index.ts",
36
+ output: [{ file: "dist/types.d.ts", format: "es" }],
37
+ plugins: [dts.default()],
38
+ },
39
+ ];
@@ -0,0 +1,44 @@
1
+ // Graph 配置
2
+ // 这里定义了节点的默认颜色、大小等样式
3
+
4
+ /**
5
+ * 节点默认配置
6
+ */
7
+ export const NODE_DEFAULTS = {
8
+ /** 节点默认大小统一值(与 SuperGraph 保持一致) */
9
+ DEFAULT_SIZE: 45,
10
+
11
+ /** switch 节点默认颜色 */
12
+ SWITCH_COLOR: "#3b82f6",
13
+
14
+ /** server 节点默认颜色 */
15
+ SERVER_COLOR: "#999999",
16
+
17
+ /** 节点字体最小大小 */
18
+ MIN_FONT_SIZE: 4,
19
+
20
+ /** 计算字体大小的字符宽度系数(SuperGraph 使用 0.6) */
21
+ FONT_SIZE_COEFFICIENT: 0.6,
22
+ } as const
23
+
24
+ /**
25
+ * 图形默认配置
26
+ */
27
+ export const GRAPH_DEFAULTS = {
28
+ /** 最小缩放级别 */
29
+ MIN_ZOOM: 0.1,
30
+
31
+ /** 最大缩放级别 */
32
+ MAX_ZOOM: 10,
33
+
34
+ /** 边 ID 分隔符(用于自动生成边 ID) */
35
+ EDGE_ID_SEPARATOR: "<->",
36
+ } as const
37
+
38
+ /**
39
+ * 视图默认配置
40
+ */
41
+ export const VIEW_DEFAULTS = {
42
+ /** fit padding 默认值 */
43
+ FIT_PADDING: 10,
44
+ } as const
@@ -0,0 +1,298 @@
1
+ // Graph 核心类
2
+
3
+ import cytoscape from 'cytoscape';
4
+ import type { StylesheetJson } from 'cytoscape';
5
+ import { GraphData, NodeData, EdgeData } from '../types';
6
+ import { defaultGraphStyles } from '../styles/graph-style';
7
+ import { register } from '../layouts/clos';
8
+ import { NODE_DEFAULTS, GRAPH_DEFAULTS, VIEW_DEFAULTS } from './config';
9
+
10
+ /**
11
+ * Graph 初始化选项
12
+ */
13
+ export interface GraphOptions {
14
+ /** 自定义样式(会与默认样式合并,用户样式优先级更高) */
15
+ style?: StylesheetJson;
16
+ /** 最小缩放级别 */
17
+ minZoom?: number;
18
+ /** 最大缩放级别 */
19
+ maxZoom?: number;
20
+ /** 图元池大小(优化性能) */
21
+ textureOnViewport?: boolean;
22
+ /** 视口移动时隐藏边缘 */
23
+ hideEdgesOnViewport?: boolean;
24
+ /** 视口移动时隐藏标签 */
25
+ hideLabelsOnViewport?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Graph 核心类
30
+ * 管理节点和边的数据,提供 CRUD 操作和事件系统
31
+ */
32
+ export class Graph {
33
+ private cy: cytoscape.Core;
34
+
35
+ constructor(options?: GraphOptions) {
36
+ // 合并样式:默认样式在前,用户样式在后(用户样式会覆盖默认样式)
37
+ const style = options?.style
38
+ ? [...defaultGraphStyles, ...options.style]
39
+ : (defaultGraphStyles as StylesheetJson);
40
+
41
+ // 构造配置对象,只包含必要的选项
42
+ const config = {
43
+ minZoom: options?.minZoom ?? GRAPH_DEFAULTS.MIN_ZOOM,
44
+ maxZoom: options?.maxZoom ?? GRAPH_DEFAULTS.MAX_ZOOM,
45
+ style,
46
+ textureOnViewport: options?.textureOnViewport ?? false,
47
+ hideEdgesOnViewport: options?.hideEdgesOnViewport ?? false,
48
+ hideLabelsOnViewport: options?.hideLabelsOnViewport ?? false,
49
+ };
50
+
51
+ this.cy = cytoscape(config);
52
+
53
+ // 注册 clos 布局(只在浏览器环境中执行)
54
+ if (typeof window !== 'undefined') {
55
+ register(cytoscape as any);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * 计算节点字体大小
61
+ * 确保标签文本能完整显示在节点圆圈内
62
+ */
63
+ private calculateFontSize(label: string, nodeSize: number): number {
64
+ if (label.length === 0) return 12;
65
+
66
+ const charWidth = nodeSize / label.length;
67
+ const fontSize = Math.floor(charWidth / NODE_DEFAULTS.FONT_SIZE_COEFFICIENT);
68
+ return Math.max(NODE_DEFAULTS.MIN_FONT_SIZE, fontSize);
69
+ }
70
+
71
+ /**
72
+ * 挂载到容器
73
+ * 可以重复调用以重新挂载到不同的容器
74
+ * @param container - DOM 容器元素
75
+ * @param fit - 是否自动适配视图,默认为 false
76
+ * @param padding - 适配时的内边距(像素)
77
+ */
78
+ render(container: HTMLDivElement, fit = false, padding?: number): void {
79
+ this.cy.mount(container);
80
+ if (fit) {
81
+ // 使用 requestAnimationFrame 确保 DOM 完全渲染后再 fit
82
+ requestAnimationFrame(() => {
83
+ this.cy.fit(undefined, padding ?? VIEW_DEFAULTS.FIT_PADDING);
84
+ });
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 加载数据
90
+ * 直接使用 Cytoscape 的 add 方法,并设置默认值
91
+ * 注意:只设置 switch 和 server 的默认值
92
+ * spine、leaf 等其他类型的节点,颜色应通过 nodeAttributes 或 renderFunction 由用户自定义
93
+ */
94
+ load(data: GraphData): void {
95
+ // 先清空旧数据
96
+ this.cy.elements().remove();
97
+
98
+ // 为没有 id 的边生成默认 id
99
+ const edgesWithId = data.edges.map((edge) => {
100
+ if (!edge.data.id) {
101
+ const source = edge.data.source;
102
+ const target = edge.data.target;
103
+ // 使用配置文件中的分隔符
104
+ const defaultId = [String(source), String(target)]
105
+ .sort()
106
+ .join(GRAPH_DEFAULTS.EDGE_ID_SEPARATOR);
107
+ return {
108
+ ...edge,
109
+ data: {
110
+ ...edge.data,
111
+ id: defaultId,
112
+ },
113
+ };
114
+ }
115
+ return edge;
116
+ });
117
+
118
+ this.cy.add([...data.nodes, ...edgesWithId]);
119
+
120
+ // 遍历所有节点,设置默认值并计算 fontSize
121
+ this.cy.nodes().forEach((n: cytoscape.NodeSingular) => {
122
+ // 设置默认颜色和大小(只处理 switch 和 server)
123
+ const type = String(n.data('type') || '').toLowerCase();
124
+
125
+ if (!n.data('size')) {
126
+ // 所有节点都使用统一的默认大小
127
+ n.data('size', NODE_DEFAULTS.DEFAULT_SIZE);
128
+ }
129
+
130
+ if (!n.data('color')) {
131
+ if (type === 'switch') {
132
+ // switch 节点默认颜色(蓝色)
133
+ n.data('color', NODE_DEFAULTS.SWITCH_COLOR);
134
+ } else if (type === 'server') {
135
+ // server 节点默认颜色(灰色)
136
+ n.data('color', NODE_DEFAULTS.SERVER_COLOR);
137
+ }
138
+ // 其他类型(spine、leaf 等)不设置默认颜色,由用户通过 nodeAttributes 或 renderFunction 自定义
139
+ }
140
+
141
+ // 计算并设置 fontSize(根据原始 label)
142
+ const size = n.data('size');
143
+ const label = n.data('label') || n.id();
144
+ const fontSize = this.calculateFontSize(label, size);
145
+ n.data('fontSize', fontSize);
146
+ });
147
+ }
148
+
149
+ /**
150
+ * 获取节点数据
151
+ */
152
+ getNode(id: string): NodeData | null {
153
+ const node = this.cy.getElementById(id);
154
+ return node.length > 0 ? (node.data() as NodeData) : null;
155
+ }
156
+
157
+ /**
158
+ * 设置节点属性
159
+ */
160
+ setNodeAttribute(id: string, key: string, value: unknown): void {
161
+ const node = this.cy.getElementById(id);
162
+ if (node.length > 0) {
163
+ node.data(key, value);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * 获取边数据
169
+ */
170
+ getEdge(id: string): EdgeData | null {
171
+ const edge = this.cy.getElementById(id);
172
+ return edge.length > 0 ? (edge.data() as EdgeData) : null;
173
+ }
174
+
175
+ /**
176
+ * 设置边属性
177
+ */
178
+ setEdgeAttribute(id: string, key: string, value: unknown): void {
179
+ const edge = this.cy.getElementById(id);
180
+ if (edge.length > 0) {
181
+ edge.data(key, value);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * 应用布局
187
+ * @param name - 布局名称(如 "clos", "random", "grid" 等)
188
+ * @param options - 布局选项
189
+ */
190
+ applyLayout(name: string, options?: Record<string, unknown>): void {
191
+ try {
192
+ const elements = this.cy.elements();
193
+ if (!elements || elements.length === 0) {
194
+ console.error('[Graph] 错误:没有可布局的元素!');
195
+ return;
196
+ }
197
+
198
+ const layout = elements.layout({
199
+ name,
200
+ ...options,
201
+ });
202
+
203
+ layout.run();
204
+ } catch (error) {
205
+ console.error(`[Graph] 布局执行失败 (${name}):`, error);
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * 调整视图以容纳所有内容
212
+ * @param padding - 周围内边距(像素),默认使用配置文件中的值
213
+ */
214
+ fit(padding?: number): void {
215
+ this.cy.fit(undefined, padding ?? VIEW_DEFAULTS.FIT_PADDING);
216
+ }
217
+
218
+ /**
219
+ * 设置元素的样式
220
+ * @param selector - Cytoscape selector 语法(如 "node", "node[type='server']", "edge", ".selected" 等)
221
+ * @param style - 样式属性(使用 Cytoscape style 属性名)
222
+ * @example
223
+ * // 设置所有节点的背景色
224
+ * graph.setStyles('node', { 'background-color': '#ff0000' })
225
+ *
226
+ * // 设置 spine 节点的样式(由用户自定义)
227
+ * graph.setStyles("node[type='spine']", { 'background-color': '#3b82f6' })
228
+ *
229
+ * // 设置 leaf 节点的样式(由用户自定义)
230
+ * graph.setStyles("node[type='leaf']", { 'background-color': '#0d9488' })
231
+ */
232
+ setStyles(selector: string, style: Record<string, unknown>): void {
233
+ this.cy
234
+ .style()
235
+ .selector(selector)
236
+ .style(style as any)
237
+ .update();
238
+ }
239
+
240
+ /**
241
+ * 批量设置样式
242
+ * @param styles - 样式配置,key 是 selector,value 是样式对象
243
+ * 样式值可以是静态值,也可以是函数(Cytoscape 会为每个匹配元素调用该函数)
244
+ * 注意:由于 Cytoscape 限制,renderFunction 中通过函数修改的 label 无法正确计算 fontSize
245
+ * 建议修改原始 data.label(通过 nodeAttributes)而非在 renderFunction 中修改
246
+ * @example
247
+ * graph.batchSetStyles({
248
+ * 'node[type="spine"]': { 'background-color': "#3b82f6" },
249
+ * 'node[type="leaf"]': { 'background-color': "#0d9488" },
250
+ * 'node[type="server"]': { 'background-color': (ele) => ele.data('color') || '#999999' }
251
+ * })
252
+ */
253
+ batchSetStyles(styles: Record<string, Record<string, unknown>>): void {
254
+ const cyStyle = this.cy.style();
255
+ Object.entries(styles).forEach(([selector, style]) => {
256
+ cyStyle.selector(selector).style(style as any);
257
+ });
258
+ cyStyle.update();
259
+ }
260
+
261
+ /**
262
+ * 绑定事件处理器
263
+ * @param eventTypes - 事件类型(如 "tap", "click" 等)
264
+ * @param selectorOrCallback - 选择器或回调函数(支持 Cytoscape selector 语法,如 "node", "node[type='server']", "node[type^='gpu']" 等)
265
+ * @param callback - 回调函数(当第二个参数是 selector 时使用)
266
+ * @example
267
+ * // 不使用 selector(向后兼容)
268
+ * graph.on('click', (evt) => console.log(evt.target.id()))
269
+ *
270
+ * // 使用 selector
271
+ * graph.on('click', 'node', (evt) => console.log('节点', evt.target.id()))
272
+ * graph.on('click', "node[type='server']", (evt) => console.log('服务器', evt.target.id()))
273
+ * graph.on('click', "node[type^='gpu']", (evt) => console.log('GPU节点', evt.target.id()))
274
+ * graph.on('click', '.selected', (evt) => console.log('选中元素', evt.target.id()))
275
+ */
276
+ on(
277
+ eventTypes: string,
278
+ selectorOrCallback: string | ((evt: cytoscape.EventObject) => void),
279
+ callback?: (event: cytoscape.EventObject) => void
280
+ ): void {
281
+ if (typeof selectorOrCallback === 'function') {
282
+ // 如果第二个参数是函数,则是最早的API(向后兼容)
283
+ this.cy.on(eventTypes, selectorOrCallback);
284
+ } else {
285
+ // 如果第二个参数是字符串,则是 selector
286
+ this.cy.on(eventTypes, selectorOrCallback, callback!);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * 销毁 Graph 实例
292
+ */
293
+ destroy(): void {
294
+ this.cy.destroy();
295
+ }
296
+ }
297
+
298
+ export default Graph;
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './react/supertopo'
2
+ export * from './types'