家譜(特殊的層級人物關係)資料結構與自動排版演算法的一種實現
家譜的資料結構並不複雜,邏輯上可以抽象成一種圖,節點為人物,邊為人物關係,關係粗略分為兩類,一類是跨層級的親子關係(如父子,父女,母子,母女),另一類為同層級的夫妻關係(其實如果要加上更多的也可以)。有了這兩類關係,就可以完全地描述一個家譜人物關係。那麼在資料庫中表示只需要兩張表就夠了,一個person表,一個relation表
person表的形式可以為(id, name, sex ...), relation表的形式可以為(id, from_person_id, to_person_id, relation_name)
這種儲存方式可以很方便地查詢到一個完整地家譜,當然也有關係型資料庫固有的缺點,就是不好做單人的連續的層級遍歷,例如找一個人祖上十八代,那一定是對應大量的table join, 不過我這裡不考慮這個問題,只專注於如何表徵一個完整的家譜,並能自動排版在前端展示,最終要達到的一個效果如圖
這張圖在資料庫層面就是按照如上描述儲存的,然而前端要繪製成這樣的樹形結構則需要花一點點小功夫。
我這裡使用d3的force directed graph進行繪製。d3的example圖是這樣的:
看起來是不是亂得一塌糊塗?如果你只按照上面的表關係建立好資料,然後直接用d3畫圖,結果也必然是這個樣子。那如何把它變成看起來比較乾淨整潔的類似樹形圖呢?d3是沒辦法按照我們的要求自動排版的,原因很簡單,我們的要求有三個,第一要分層級(父母在上子女在下),第二,線條交叉要儘量少,不要太雜亂無章,第三,樹看起來比較平衡(例如不要很右邊的父節點連到圖最左邊的子節點,難看的很),很顯然,這種要求屬於高度定製的要求,d3是不可能自動給你排的,那怎麼做呢?我的思路是通過某種演算法,確定圖中每一個人物的座標(x,y),使得滿足上面的3個要求,則自然結果圖能夠整潔。是不是廢話?待我細細說來。。。
第一步,計算層級。
思路如下,先定一個記錄標準:最上層為1層,其子所在層為2,再往下一層為3,以此類推。那麼在給定一個圖之後,只要這個圖是連通圖,那麼從一個節點沿著關係一定能走到任意其他節點,基於這個前提,我用一種想象中的染色法,想象圖中所有節點一開始都是白色,然後選定任意一個節點開始,隨意標記一個層級,例如10,染成紅色,然後從該紅色節點出發,沿著其所有關係遞迴遍歷其他節點,遍歷時,如果是向上走,則走到的節點層級減1,向下走,則走到的節點層級加1,同層走,則走到的節點層級相同,直到所有節點都變成紅色。這樣遞迴完成後,所有的層級都定下來了,但是由於初始節點的層級是隨便取的,最終得到的結果可能是10,11,12,13。。這樣的層次,只要再做個“歸一”,即把最小的層級變成1(例如如果層級列表為(10,11,12),那麼只要統一減去9,即可"歸一"為(1,2,3))。
第二部:減少線條交織,自動調整層高
第一步做完以後,所有的節點都被分到了對應的層級,但僅僅這樣畫出來的圖一定還是不好看,例如一對夫妻在同層級,但是如果一個放在圖最左,一個放在圖最右,中間還放了很多其他兄弟節點,那麼這就很難看,亦或是A放在B的左邊,然後A的後代卻放在B的後代的右邊,那麼可想而知這裡又會有很多不必要的線條交織,影響美觀,所以要做到幾件事,包括:1、把夫妻要並在一起放置,如果A和B是夫妻,C和D是夫妻,那麼應該是ABCD這樣的排布ok,但如果是ACDB這樣就不行。2、如果甲和乙是親兄弟,甲在乙的左邊,那麼甲的後代必須也在乙的後代的左邊,遞迴傳下去。3,每層的層間距也不應固定,例如古代皇帝,有些有一百多個兒子,有些就一兩個兒子,那麼稍微想象,也能知道,畫前者的層間距應該大於後者的層間距才好看,否則兒女多的人發散出去的線條會畫得非常扁平。
第三步,樹的平衡
這一步是基於前兩步來做的,最終能夠確定每個節點的列位置,如果說第一步確定了層位置,第二步粗粒度確定了列位置(排好了層級內每個節點的位序),那麼這一步則是細粒度最終確定了列位置。根據實測,最終定了3個原則來唯一確定一個節點的列位置,第一,位序靠後的節點列座標一定要大於位序靠前的列座標,例如某層內根據第二步確定好的位序為(A,B,C,D,E)則,B的列座標一定大於A的列座標,C的一定大於B的,以此類推。第二,一個節點的後代葉子節點數越多,它佔領的該層的列空間就要越大,同時與下層的距離也要越大,每層和下一層的最終距離為該層所有節點的這種距離中的最大者。第三,一個節點的位置還受到其父節點的影響,父節點若有n個後代葉子節點,則本節點的列座標不應該小於父節點的列座標-n/2。根據這三個原則,就能確定唯一的節點位置。
具體的演算法程式碼用到了大量的記賬式遞迴(recursion+memoization),例如計算層級,計算某節點的後代葉子節點數,拆開來看都不算複雜,拼在一起會有點繞
歡迎大家體驗和使用