瀏覽器端圖表繪制技術多樣(VML、SVG、HTML5 Canvas),各類 JS 圖表庫(如 draw2d、D3、ECharts)API 差異巨大。若將佈局邏輯耦合在具體繪制庫中,將難以復用與維護。
核心思想:將「佈局計算」與「具體繪制」解耦。維護一個純 JS 對象模型(Graph)記錄節點、邊的拓撲信息與坐標,繪制庫僅負責根據該模型渲染。
var Graph = function() {
this.nodeSet = {}; // id -> Node 映射,便於 O(1) 查找
this.nodes = []; // 節點列表
this.edges = []; // 邊列表
this.adjacency = {}; // 鄰接表:sourceId -> {targetId -> [Edge]}
this.nextNodeId = 0; // 自增 ID 生成器
this.nextEdgeId = 0;
this.eventListeners = []; // 變更通知機制
};
// 節點:id + 業務數據 + 可選 shape(繪制庫具體圖形引用)
var Node = function(id, data) {
this.id = id;
this.data = data || {};
};
// 邊:id + source/target 節點引用 + 業務數據
var Edge = function(id, source, target, data) {
this.id = id;
this.source = source;
this.target = target;
this.data = data || {};
};
// 添加節點(去重)
Graph.prototype.addNode = function(node) {
if (!this.nodeSet[node.id]) {
this.nodes.push(node);
}
this.nodeSet[node.id] = node;
this.notify();
return node;
};
// 添加邊(去重)
Graph.prototype.addEdge = function(edge) {
var exists = this.edges.some(function(e) { return e.id === edge.id; });
if (!exists) this.edges.push(edge);
// 維護鄰接表
var adj = this.adjacency[edge.source.id] = this.adjacency[edge.source.id] || {};
var list = adj[edge.target.id] = adj[edge.target.id] || [];
if (!list.some(function(e) { return e.id === edge.id; })) {
list.push(edge);
}
this.notify();
return edge;
};
// 工廠方法
Graph.prototype.newNode = function(data) {
return this.addNode(new Node(this.nextNodeId++, data));
};
Graph.prototype.newEdge = function(source, target, data) {
return this.addEdge(new Edge(this.nextEdgeId++, source, target, data));
};
// 獲取兩節點間的所有邊
Graph.prototype.getEdges = function(node1, node2) {
return (this.adjacency[node1.id] && this.adjacency[node1.id][node2.id]) || [];
};
// 刪除節點及關聯邊
Graph.prototype.removeNode = function(node) {
delete this.nodeSet[node.id];
this.nodes = this.nodes.filter(function(n) { return n.id !== node.id; });
this.detachNode(node);
};
// 刪除邊
Graph.prototype.removeEdge = function(edge) {
this.edges = this.edges.filter(function(e) { return e.id !== edge.id; });
// 清理鄰接表
for (var x in this.adjacency) {
for (var y in this.adjacency[x]) {
this.adjacency[x][y] = this.adjacency[x][y].filter(function(e) { return e.id !== edge.id; });
}
}
this.notify();
};
// 合併外部圖數據(含節點 id 與邊 from/to 索引)
Graph.prototype.merge = function(data) {
var nodes = [];
data.nodes.forEach(function(n) {
nodes.push(this.addNode(new Node(n.id, n.data)));
}, this);
data.edges.forEach(function(e) {
var from = nodes[e.from];
var to = nodes[e.to];
var id = e.directed ? (e.type + "-" + from.id + "-" + to.id)
: (from.id < to.id ? e.type + "-" + from.id + "-" + to.id
: e.type + "-" + to.id + "-" + from.id);
var edge = this.addEdge(new Edge(id, from, to, e.data));
edge.data.type = e.type;
}, this);
};
// 過濾節點/邊
Graph.prototype.filterNodes = function(fn) {
this.nodes.slice().forEach(function(n) { if (!fn(n)) this.removeNode(n); }, this);
};
Graph.prototype.filterEdges = function(fn) {
this.edges.slice().forEach(function(e) { if (!fn(e)) this.removeEdge(e); }, this);
};
// 觀察者模式:圖變更通知
Graph.prototype.addGraphListener = function(obj) { this.eventListeners.push(obj); };
Graph.prototype.notify = function() {
this.eventListeners.forEach(function(obj) { obj.graphChanged(); });
};
notify() 機制便於 UI 同步更新流程圖編輯器、拓撲圖可視化、知識圖譜探索、組織架構圖、工作流設計器等所有「節點+連線」類應用。