據(jù)可視化建模與自動(dòng)布局:從數(shù)據(jù)結(jié)構(gòu)到可復(fù)用圖編輯器)
“diagram-design”這個(gè)項(xiàng)目名第一眼容易讓人覺(jué)得只是個(gè)畫圖Demo但實(shí)際上它要做的事是“把一張圖變成一套可描述、可計(jì)算、可復(fù)用的數(shù)據(jù)模型”。我在拿到這個(gè)項(xiàng)目時(shí)很快就意識(shí)到它的重點(diǎn)不是“畫得好看”而是“讓圖自己會(huì)排、會(huì)算、會(huì)適配”。簡(jiǎn)單來(lái)說(shuō)它解決的是手工拖拽畫圖效率低、后期維護(hù)成本高、圖表表達(dá)不一致這三類老問(wèn)題。無(wú)論是做架構(gòu)圖、知識(shí)圖譜、依賴關(guān)系圖還是項(xiàng)目匯報(bào)里的數(shù)據(jù)流向圖只要是需要把復(fù)雜關(guān)系可視化的場(chǎng)景這個(gè)工具鏈都值得參考。適合想自己實(shí)現(xiàn)一個(gè)可視化編輯器的前端開(kāi)發(fā)、需要搭建低代碼圖表平臺(tái)的技術(shù)負(fù)責(zé)人以及準(zhǔn)備做圖形化表達(dá)但不知道如何拆解實(shí)體、關(guān)系、布局、渲染四層關(guān)系的初學(xué)者。1. 項(xiàng)目到底解決了什么問(wèn)題1.1 手工畫圖的三個(gè)老大難我在很多項(xiàng)目里看到團(tuán)隊(duì)用白板工具或者繪圖軟件畫各種圖剛開(kāi)始很爽畫完發(fā)出去也很有面子。但最多兩個(gè)月問(wèn)題就集中爆發(fā)業(yè)務(wù)變了要改字段、加模塊可原圖的作者已經(jīng)不在這個(gè)組了剩下的人只能重新畫一遍又或者同一套數(shù)據(jù)在不同人手里畫出了三種風(fēng)格匯報(bào)時(shí)老板問(wèn)“到底哪個(gè)是準(zhǔn)確的”沒(méi)人敢拍板。這類問(wèn)題的本質(zhì)是“圖的語(yǔ)義沒(méi)有和數(shù)據(jù)綁定”。圖只是一張圖片而不是一組結(jié)構(gòu)化對(duì)象。diagram-design的項(xiàng)目思路不同它把圖拆成了節(jié)點(diǎn)Node、連線Edge、分組Group和標(biāo)注Annotation并把這些元素映射到一份配置文件里。這樣當(dāng)數(shù)據(jù)源變化時(shí)圖可以自動(dòng)刷新而不是讓人去手動(dòng)移動(dòng)箭頭。另外一個(gè)我特別在意的問(wèn)題是“改圖成本”。很多團(tuán)隊(duì)會(huì)把系統(tǒng)架構(gòu)圖畫進(jìn)文檔里架構(gòu)一變文檔就過(guò)期翻新文檔又沒(méi)人愿意干。如果圖本身是由代碼或配置生成的那么每次提交代碼時(shí)順帶更新配置文檔圖就永遠(yuǎn)是最新的。這是我始終認(rèn)為“配置即圖表”比“拖拽即圖表”更值得投入的原因。1.2 從“畫圖”到“描述圖”的思路轉(zhuǎn)變傳統(tǒng)繪圖工具的交互邏輯是“選中-拖拽-對(duì)齊-微調(diào)”它更偏向藝術(shù)家視角而diagram-design更傾向于程序員視角你描述圖里有哪些實(shí)體、它們之間有什么關(guān)系、方向是什么、層級(jí)怎么定義剩下的位置計(jì)算交給布局算法。我舉個(gè)最簡(jiǎn)單的例子一個(gè)依賴關(guān)系圖A依賴BB依賴CC又依賴A。如果用手工畫連線的交點(diǎn)會(huì)讓你頭大。但如果用布局算法先把三者的依賴關(guān)系抽象成一張有向圖再用拓?fù)渑判蚺袛鄬蛹?jí)然后用同層對(duì)齊算法計(jì)算每層的上下順序最后用正交連線算法把邊轉(zhuǎn)化成橫平豎直的折線那整個(gè)過(guò)程就是“數(shù)據(jù)進(jìn)圖形出”完全不需要人干預(yù)。這個(gè)思路非常核心它帶來(lái)的好處有幾點(diǎn)數(shù)據(jù)驅(qū)動(dòng)圖中的每個(gè)節(jié)點(diǎn)可以直接對(duì)應(yīng)數(shù)據(jù)庫(kù)里的一張表、接口里的一個(gè)字段、運(yùn)維平臺(tái)里的一臺(tái)機(jī)器。確定性強(qiáng)同一份配置永遠(yuǎn)生成同一張圖不會(huì)因?yàn)楫媹D的人不同而風(fēng)格迥異??蓴U(kuò)展要加新的節(jié)點(diǎn)類型、連線樣式、布局策略都只是增加一個(gè)類或一段配置的問(wèn)題。1.3 項(xiàng)目邊界與適用場(chǎng)景認(rèn)清邊界比急著寫代碼更重要。diagram-design不適合做成Photoshop那種自由畫布工具它更適合做“半結(jié)構(gòu)化”的可視化場(chǎng)景。什么意思就是用戶能調(diào)整節(jié)點(diǎn)位置和分組區(qū)域但圖的總體結(jié)構(gòu)仍然由數(shù)據(jù)模型和布局算法決定。判斷一個(gè)場(chǎng)景適不適合這類項(xiàng)目我一般看三個(gè)條件數(shù)據(jù)是否有明確的實(shí)體關(guān)系圖是否需要頻繁更新多人協(xié)作時(shí)是否需要保證表達(dá)一致性。如果三個(gè)條件至少滿足兩個(gè)那diagram-design這類方案就比通用繪圖軟件合適。典型場(chǎng)景包括業(yè)務(wù)流程圖、系統(tǒng)架構(gòu)圖、微服務(wù)調(diào)用圖、組織架構(gòu)圖、知識(shí)圖譜預(yù)覽以及數(shù)據(jù)血緣分析面板這類需要嵌入到后臺(tái)產(chǎn)品里的局部功能。2. 核心模塊拆解與方案選型2.1 數(shù)據(jù)模型圖的抽象層級(jí)diagram-design的底層不是面、線、顏色這些視覺(jué)概念而是數(shù)據(jù)模型。我設(shè)計(jì)這個(gè)項(xiàng)目時(shí)把模型分成四層第一層是Graph它是最頂層容器保存全局屬性比如畫布尺寸、縮放比例、布局類型。第二層是Node節(jié)點(diǎn)必須包含id、type、position、size、data五類信息其中data是業(yè)務(wù)數(shù)據(jù)體和圖形展示解耦。第三層是Edge保存source、target、direction、points它描述兩節(jié)點(diǎn)之間的關(guān)系但不關(guān)心箭頭長(zhǎng)什么樣。第四層是ViewState記錄用戶縮放水平、已展開(kāi)的分支、被選中的節(jié)點(diǎn)等交互狀態(tài)。這個(gè)分層最大的好處是“數(shù)據(jù)層和渲染層可以獨(dú)立測(cè)試”。我經(jīng)常先只寫一堆JSON跑一遍布局算法確認(rèn)坐標(biāo)都正確了再去做視覺(jué)美化。如果一上來(lái)就綁定了DOM操作調(diào)試會(huì)痛苦得多。實(shí)際建模時(shí)我建議Node的data字段不要直接塞對(duì)象而是存一個(gè)引用或者key通過(guò)store取出。這樣當(dāng)幾百個(gè)節(jié)點(diǎn)需要序列化保存時(shí)圖描述文件不會(huì)無(wú)限膨脹而且還方便做增量更新。2.2 布局引擎為什么算法是靈魂布局是diagram-design里最容易被低估的部分同時(shí)也是我認(rèn)為最值得細(xì)細(xì)打磨的部分。如果節(jié)點(diǎn)位置全靠用戶手動(dòng)拖那再好的圖形引擎也只是一塊大白板。而要自動(dòng)布局就需要一套可插拔的布局策略。我初期實(shí)現(xiàn)時(shí)只做了三種足夠覆蓋80%的業(yè)務(wù)場(chǎng)景層級(jí)布局適合依賴關(guān)系圖、流程圖核心是用最長(zhǎng)路徑分層再按層內(nèi)規(guī)模做垂直對(duì)齊。力導(dǎo)向布局適合知識(shí)圖譜、社交網(wǎng)絡(luò)圖用彈簧模型讓關(guān)聯(lián)緊密的節(jié)點(diǎn)自然靠近。網(wǎng)格布局適合工單看板、卡片大盤純粹按照行列排布簡(jiǎn)單直接。布局算法必須保證“無(wú)重疊”和“連通性可見(jiàn)”。很多初級(jí)實(shí)現(xiàn)容易踩的坑是算完坐標(biāo)后節(jié)點(diǎn)疊成一團(tuán)連線全部穿過(guò)節(jié)點(diǎn)。這個(gè)問(wèn)題沒(méi)有銀彈只能通過(guò)兩個(gè)手段規(guī)避布局前對(duì)圖做連通分量劃分把不相關(guān)的子圖拆開(kāi)單獨(dú)排列布局后對(duì)每一個(gè)節(jié)點(diǎn)做AABB碰撞檢測(cè)發(fā)現(xiàn)重疊就局部迭代外擴(kuò)。2.3 渲染方案SVG還是Canvas我在這件事上糾結(jié)過(guò)很長(zhǎng)時(shí)間最終結(jié)論是“按場(chǎng)景混用”而非二選一。diagram-design的默認(rèn)渲染層用SVG原因是節(jié)點(diǎn)數(shù)量一般不超過(guò)上千SVG的DOM語(yǔ)義讓事件綁定和樣式定制都太方便了——比如節(jié)點(diǎn)hover變色、點(diǎn)擊彈出詳情面板都是天然支持。而且SVG導(dǎo)出為矢量圖在匯報(bào)場(chǎng)景里很實(shí)用。但是當(dāng)節(jié)點(diǎn)數(shù)量上萬(wàn)時(shí)SVG的DOM節(jié)點(diǎn)壓力會(huì)非常明顯滾動(dòng)和縮放都會(huì)卡頓。所以我額外實(shí)現(xiàn)了一個(gè)Canvas渲染通道專門處理大規(guī)模只讀預(yù)覽場(chǎng)景。這個(gè)通道不處理點(diǎn)擊交互只負(fù)責(zé)把布局引擎算好的坐標(biāo)批量畫成圖像。簡(jiǎn)單說(shuō)SVG通道負(fù)責(zé)“編輯態(tài)”Canvas通道負(fù)責(zé)“展示態(tài)”。這里分享一個(gè)細(xì)節(jié)兩套渲染通道對(duì)應(yīng)同一份數(shù)據(jù)模型用戶在編輯態(tài)拖拽了節(jié)點(diǎn)位置模型里更新position展示態(tài)重新繪制時(shí)直接用新坐標(biāo)。兩個(gè)通道之間沒(méi)有任何同步問(wèn)題因?yàn)楫嫴加肋h(yuǎn)是模型的影子絕不能反過(guò)來(lái)讓DOM成為數(shù)據(jù)源。2.4 交互設(shè)計(jì)拖拽、縮放、折疊交互層容易做得很“重”很多編輯器把拖拽、縮放、框選、連線全部堆在一起。diagram-design我做得相對(duì)克制拖拽是移動(dòng)單個(gè)節(jié)點(diǎn)縮放是按住畫布平移和滾輪縮放分組折疊則通過(guò)點(diǎn)擊分組標(biāo)題欄切換。這三個(gè)交互覆蓋了大多數(shù)查看和微調(diào)需求。值得強(qiáng)調(diào)的是“拖拽過(guò)程中要實(shí)時(shí)觸發(fā)布局重算嗎”這個(gè)問(wèn)題的選擇。我的經(jīng)驗(yàn)是不重算拖拽只是改變VieweState里的覆蓋坐標(biāo)全部拖完點(diǎn)擊“自動(dòng)整理”按鈕才回到布局算法重新排列。這么做有幾個(gè)好處操作響應(yīng)不卡用戶手動(dòng)調(diào)整的結(jié)果不會(huì)被算法搶走真正需要重排時(shí)用戶有明確預(yù)期。另外交互層的所有操作都要進(jìn)入操作歷史棧也就是支持撤銷重做。這看起來(lái)是編輯器標(biāo)配但實(shí)現(xiàn)起來(lái)涉及節(jié)點(diǎn)增刪、位置更新、分組變更等多類操作的類型判定如果不提前抽象成一個(gè)command模式后期填坑會(huì)非常痛苦。3. 實(shí)操過(guò)程從零搭起一個(gè)可用版本3.1 定義數(shù)據(jù)模型的JSON Schema一個(gè)新項(xiàng)目最容易犯的錯(cuò)誤是一上來(lái)就畫界面。我現(xiàn)在習(xí)慣從一開(kāi)始就用一份JSON Schema把數(shù)據(jù)結(jié)構(gòu)釘死界面寫得再快也不如數(shù)據(jù)模型穩(wěn)定帶來(lái)的節(jié)省多。下面是我在diagram-design里用的一份示例配置為了演示做了精簡(jiǎn)但字段結(jié)構(gòu)和高保真版本一致。{ graph: { type: hierarchy, width: 1400, height: 900, nodeDefaultWidth: 160, nodeDefaultHeight: 60 }, nodes: [ { id: svc-payment, type: box, data: { title: 支付服務(wù), status: ready, version: v2.3.0 } }, { id: svc-order, type: box, data: { title: 訂單服務(wù), status: warning, version: v1.9.0 } }, { id: svcpool-mysql, type: cylinder, data: { title: 核心庫(kù)主實(shí)例, role: primary } } ], edges: [ { id: e1, source: svc-order, target: svc-payment, kind: sync-call }, { id: e2, source: svc-payment, target: svcpool-mysql, kind: read-write } ], groups: [ { id: grp-order-line, title: 訂單域, nodeIds: [svc-order, svc-payment] } ], viewState: { zoom: 1, unfoldGroups: [grp-order-line], selectedIds: [] } }這份Schema雖然簡(jiǎn)單但骨架已經(jīng)很完整。注意我在Node里沒(méi)有直接存坐標(biāo)而是交給布局引擎去計(jì)算這樣可以避免手工寫布局造成的牽一發(fā)動(dòng)全身。真正的坐標(biāo)會(huì)保存在運(yùn)行時(shí)的一個(gè)Map里序列化時(shí)也不會(huì)寫進(jìn)配置除非用戶主動(dòng)拖拽形成了覆蓋坐標(biāo)。3.2 編寫一個(gè)輕量的層級(jí)布局算法以依賴關(guān)系圖為例我實(shí)現(xiàn)過(guò)一個(gè)經(jīng)典的分層布局算法核心邏輯分三步拓?fù)浞謱?、層?nèi)排序、坐標(biāo)換算。拓?fù)浞謱拥拇a邏輯如下def assign_layers(graph): indegree {n: 0 for n in graph.nodes} out_edges {n: [] for n in graph.nodes} for edge in graph.edges: out_edges[edge.source].append(edge.target) indegree[edge.target] 1 queue [n for n in graph.nodes if indegree[n] 0] layer {} depth 0 while queue: for n in queue: layer[n] depth next_queue [] for n in queue: for t in out_edges[n]: indegree[t] - 1 if indegree[t] 0: next_queue.append(t) queue next_queue depth 1 # 簡(jiǎn)單處理環(huán)剩余未分層節(jié)點(diǎn)放到最后一層 return layer層內(nèi)排序我通常用的是“減少交叉邊”的啟發(fā)式算法也就是同層節(jié)點(diǎn)按與其前驅(qū)節(jié)點(diǎn)連接數(shù)量的加權(quán)和排序這樣可以大幅降低連線交叉。坐標(biāo)換算則更簡(jiǎn)單同一層節(jié)點(diǎn)按順序放到一條水平線上層與層之間用固定間距縱向排開(kāi)。這個(gè)算法的輸出是一份坐標(biāo)映射表nodeId - (x, y)。整個(gè)過(guò)程不依賴任何渲染代碼所以我可以用單測(cè)驗(yàn)證任意復(fù)雜的依賴圖是否會(huì)出現(xiàn)重疊。這是整個(gè)項(xiàng)目里收益最高的測(cè)試強(qiáng)烈建議把這個(gè)單測(cè)寫起來(lái)。3.3 渲染與樣式系統(tǒng)實(shí)現(xiàn)渲染層我做了一套比較靈活的模板方案每種節(jié)點(diǎn)類型對(duì)應(yīng)一個(gè)渲染函數(shù)傳入數(shù)據(jù)模型返回帶樣式的SVG節(jié)點(diǎn)組件。比如普通服務(wù)節(jié)點(diǎn)渲染為圓角矩形內(nèi)部有標(biāo)題和狀態(tài)圓點(diǎn)數(shù)據(jù)庫(kù)節(jié)點(diǎn)渲染為圓柱形干系人節(jié)點(diǎn)渲染為圓形頭像。這樣后續(xù)增加新節(jié)點(diǎn)類型時(shí)不用改動(dòng)主流程只要新增一個(gè)渲染函數(shù)并注冊(cè)進(jìn)去。樣式系統(tǒng)要與主題分離。我定義了三層主題基礎(chǔ)主題負(fù)責(zé)字體、畫布底色、網(wǎng)格線節(jié)點(diǎn)主題負(fù)責(zé)節(jié)點(diǎn)顏色、描邊、陰影連線主題負(fù)責(zé)直線還是折線、顏色和粗細(xì)。切換深色模式時(shí)只替換主題配置不需要重繪模型。寫渲染代碼時(shí)我踩過(guò)一個(gè)坑SVG的auto換行支持很差text標(biāo)簽不會(huì)自動(dòng)換行。我的解決辦法是在渲染前用Canvas的measureText估算文本寬度如果超出節(jié)點(diǎn)寬度就提前拆行再逐行渲染。以下是一段核心的換行邏輯function splitText(text, maxWidth, fontSize) { const ctx measureContext; // 預(yù)先創(chuàng)建的離屏canvas上下文 const chars Array.from(text); const lines []; let current ; for (const ch of chars) { const testLine current ch; if (ctx.measureText(testLine).width maxWidth current) { lines.push(current); current ch; } else { current testLine; } } if (current) lines.push(current); return lines; }這段代碼看起來(lái)普通但它在實(shí)際項(xiàng)目中價(jià)值巨大。因?yàn)樽詣?dòng)布局算出來(lái)的節(jié)點(diǎn)寬度都是統(tǒng)一的如果標(biāo)題超長(zhǎng)不換行要么溢出到節(jié)點(diǎn)外面要么被裁剪掉兩種情況都會(huì)讓使用者覺(jué)得這個(gè)工具“不太聰明”。做了拆行之后節(jié)點(diǎn)高度會(huì)自適應(yīng)觀感會(huì)提升一個(gè)檔次。3.4 規(guī)?;秩九c性能優(yōu)化當(dāng)圖里節(jié)點(diǎn)數(shù)超過(guò)兩千時(shí)優(yōu)化點(diǎn)的順序就變得很重要。我的實(shí)測(cè)結(jié)論是最先優(yōu)化的不是算法而是渲染調(diào)度和事件委托。先說(shuō)渲染調(diào)度。SVG的appendChild操作過(guò)多會(huì)阻塞主線程所以我把初始化渲染改成了分幀渲染每幀只處理200個(gè)節(jié)點(diǎn)加上requestIdleCallback的權(quán)重區(qū)分高優(yōu)先級(jí)節(jié)點(diǎn)先渲染低優(yōu)先級(jí)區(qū)域后補(bǔ)。這樣即使整圖有五千節(jié)點(diǎn)首屏也不會(huì)白屏。再說(shuō)事件委托。拖拽、點(diǎn)擊這類交互如果給每個(gè)節(jié)點(diǎn)都綁定監(jiān)聽(tīng)器內(nèi)存壓力大且不好清理。我用了事件委托在SVG根元素上監(jiān)聽(tīng)pointermove、pointerup等事件再用data-node-id屬性反查目標(biāo)節(jié)點(diǎn)。這個(gè)方案不僅性能好還能自然支持新增節(jié)點(diǎn)無(wú)需重新綁定事件。第三個(gè)優(yōu)化是視口裁剪。我計(jì)算當(dāng)前視口在模型坐標(biāo)系里的可見(jiàn)范圍只渲染落在范圍內(nèi)的節(jié)點(diǎn)和邊其余節(jié)點(diǎn)全部跳過(guò)。等于說(shuō)無(wú)論數(shù)據(jù)量多大單幀渲染量維持在千級(jí)以內(nèi)滾動(dòng)切換視口時(shí)才增量渲染。三檔優(yōu)化下來(lái)一萬(wàn)節(jié)點(diǎn)在普通辦公電腦上也能流暢平移和縮放這就夠用了。4. 常見(jiàn)問(wèn)題與排查實(shí)錄4.1 文本溢出與節(jié)點(diǎn)尺寸失配癥狀節(jié)點(diǎn)標(biāo)題長(zhǎng)一點(diǎn)就頂出圓角矩形邊界或者兩個(gè)節(jié)點(diǎn)之間重疊。排查思路先看字體度量。SVG里同一字號(hào)在不同操作系統(tǒng)下的實(shí)際渲染寬度不同直接按CSS像素字體估算容易不準(zhǔn)。最佳方案是用離屏Canvas的measureText獲取真實(shí)像素寬度再乘以1.2的系數(shù)做安全余量。另一個(gè)隱蔽因素是邊距。節(jié)點(diǎn)文本的padding如果沒(méi)納入尺寸計(jì)算布局引擎按160寬度排布文本卻需要180那必然溢出。所以我建議預(yù)留8~12像素的padding并且layout引擎計(jì)算碰撞時(shí)要用完整的外部包圍盒也就是sizepadding而不是只用內(nèi)層文本區(qū)域。4.2 連線交叉嚴(yán)重癥狀自動(dòng)布局后一半以上的連線交叉圖根本沒(méi)法看。這是布局算法的問(wèn)題不是渲染bug。交叉的根源往往是層內(nèi)節(jié)點(diǎn)順序不匹配比如source層的A、B對(duì)應(yīng)target層的X、Y如果target層順序是Y、X兩條線必然交叉。我的改進(jìn)方案是給層內(nèi)排序增加“祖先節(jié)點(diǎn)加權(quán)”先統(tǒng)計(jì)每個(gè)節(jié)點(diǎn)到上一層所有連接目標(biāo)的節(jié)點(diǎn)索引計(jì)算中位索引再按中位索引排序。這個(gè)思路能立刻減少大部分交叉。再配合“分段線性插值”折線連接方式讓邊在預(yù)測(cè)空間內(nèi)走正交路徑視覺(jué)上比直線連貫穿插干凈很多。4.3 拖拽后坐標(biāo)漂移癥狀鼠標(biāo)按住節(jié)點(diǎn)拖拽節(jié)點(diǎn)比鼠標(biāo)慢半拍或停手后節(jié)點(diǎn)自動(dòng)跳回原位。多半是坐標(biāo)空間不一致。鼠標(biāo)事件的clientX和clientY是屏幕坐標(biāo)而節(jié)點(diǎn)position是模型坐標(biāo)兩者中間隔著畫布縮放比例和畫布平移偏移量。正確的換算公式是const rect svgRoot.getBoundingClientRect(); const modelX (event.clientX - rect.left - viewState.panX) / viewState.zoom; const modelY (event.clientY - rect.top - viewState.panY) / viewState.zoom;我遇到這個(gè)問(wèn)題的次數(shù)比預(yù)想多而且每次都是在小功能改動(dòng)后復(fù)發(fā)。所以現(xiàn)在會(huì)把這一小段坐標(biāo)換算封裝成一個(gè)函數(shù)并在拖拽啟動(dòng)時(shí)強(qiáng)制讀取一次畫布當(dāng)前的pan和zoom值不依賴內(nèi)存里可能存在延遲的舊狀態(tài)。4.4 導(dǎo)出圖片時(shí)背景和透明問(wèn)題很多編輯器都提供“導(dǎo)出PNG”功能diagram-design也不能少。但默認(rèn)導(dǎo)出經(jīng)常遇到兩個(gè)坑導(dǎo)出背景是黑色導(dǎo)出內(nèi)容只有當(dāng)前視口不是全圖。黑色背景通常是因?yàn)镾VG沒(méi)有顯式聲明fill導(dǎo)出時(shí)被默認(rèn)值覆蓋。解決方案是在導(dǎo)出前給根節(jié)點(diǎn)加一個(gè)白底的rect。全圖導(dǎo)出則需要先讀取模型里所有節(jié)點(diǎn)的包圍盒計(jì)算畫布尺寸然后臨時(shí)把viewBox改為全圖范圍等導(dǎo)出完成后再恢復(fù)原來(lái)的縮放狀態(tài)。這樣導(dǎo)出的圖片在任何平臺(tái)上看都是完整且干凈的。4.5 分組折疊后連線懸空折疊分組本身只是隱藏子節(jié)點(diǎn)但子節(jié)點(diǎn)與其他組的連線如果不處理就會(huì)出現(xiàn)“線從空地牽出來(lái)”的懸空效果。我給分組模型增加了一個(gè)entry節(jié)點(diǎn)和exit節(jié)點(diǎn)分組折疊時(shí)所有進(jìn)出邊先匯聚到分組邊界的錨點(diǎn)再由錨點(diǎn)連接到外部其他分組或其他孤立節(jié)點(diǎn)。本質(zhì)上是在抽象層面做了一次“邊的重連”視覺(jué)層面則靠給折線加垂直邊角來(lái)呈現(xiàn)。這個(gè)細(xì)節(jié)雖然復(fù)雜但做好之后分組折疊功能才能真正可用而不是單純隱藏圖層。5. 關(guān)于這個(gè)項(xiàng)目我最后還想說(shuō)的幾件事diagram-design做到后面我發(fā)現(xiàn)難點(diǎn)已經(jīng)不在視覺(jué)表現(xiàn)而在“如何讓數(shù)據(jù)與結(jié)構(gòu)保持同步”。一份配置對(duì)應(yīng)一張圖這句話說(shuō)起來(lái)輕松但真正常態(tài)化運(yùn)行需要把數(shù)據(jù)校驗(yàn)、布局算法、渲染調(diào)度、交互狀態(tài)管理全部打通。很多人在第一步就被困住了因?yàn)樗麄兛傁胫劝旬嫴甲銎猎倩靥顢?shù)據(jù)結(jié)果每一步都在補(bǔ)洞。根據(jù)我的經(jīng)驗(yàn)最值得提前花時(shí)間的部分是JSON Schema設(shè)計(jì)和布局引擎的單測(cè)這兩塊做好之后后面的渲染和交互都會(huì)順理成章。最后再分享一個(gè)實(shí)用習(xí)慣項(xiàng)目中所有坐標(biāo)輸出都加一份“圖形預(yù)覽調(diào)試面板”每一步布局后能直觀看到坐標(biāo)分布而不是靠console.log猜測(cè)問(wèn)題在哪。這個(gè)面板在后期的性能優(yōu)化和維護(hù)中會(huì)幫你省下大量時(shí)間。