使用js實現思維導圖
本文主要闡述使用js實現思維導圖的關鍵技術點,如果還不知道什么是思維導圖的同學,請自行度娘。以下是demo和源碼的傳送門:
在源碼中我使用了svg繪制思維導圖。與canvas相比,svg將圖像當成對象,我們可將思維導圖中節點和線等圖形表現為對象,而且svg更適合用于動態交互的應用
下面介紹幾個關鍵技術點:
子節點位置的重繪
一個基本的思維導圖工具應該擁有增加節點和刪除節點的功能。在某個節點上增刪節點時,為了使得所有子節點的高度相對于該節點垂直居中,都會重新渲染子節點的垂直位置。
如圖1所示,首先求得父節點的中心點F的坐標為(hfx, hfy),設父節點與子節點的水平距離為interval,父節點的寬為parentWidth。作水平線段FC,C點的橫坐標即為子節點的橫坐標childX。如下圖所示:
為了讓子節點間垂直隔開,每一個子節點上下都有補白,所以一個子節點所占的區域高度為該子節點的節點高度加上兩個補白高度。迭代所有子節點,求取 所有子節點的區域高度areaHeight,然后在線段FC的C點上作一條長度為areaHeight的垂直平分線AB,所有子節點的垂直區域都在垂直平 分線AB內,這樣可以保證所有子節點的高度相對于該節點垂直居中。如下圖所示:
我們需要求得每一個子節點的垂直坐標childY。首先求得A點的垂直坐標startY = hfy – areaHeight / 2,第一個子節點的垂直坐標由startY加padding可得。求第二個子節點的垂直坐標時,startY累加上一個子節點的區域高度,則第二個子節點 的垂直坐標等于當前startY加上padding。之后的子節點通過迭代相同的操作可得。在每一輪迭代中,根據求得的子節點坐標(childX, childY)渲染節點的位置。如下圖所示:
實現代碼如下:
// 以下變量請自行求得 var hfx , // 父節點的中心x軸坐標 hfy , // 父節點的中心y軸坐標 parentWidth , // 父節點的寬度 children , // 子節點列表 padding , // 子節點垂直間距 interval ; // 節點間水平間距 var childX , // 子節點的x軸坐標 startY , // 子節點區域的起始坐標 childrenAreaHeight = 0 ; // 子節點總區域高度 childX = hfx + parentWidth / 2 + interval ; // 迭代子節點,求得子節點總區域高度 children . forEach ( function ( child ) { var curAreaHeight = getNodeHeight ( child ) + padding * 2 ; childrenAreaHeight += curAreaHeight ; } ) ; startY = hfy - childrenAreaHeight / 2 ; // 迭代子節點,求得每個子節點的垂直坐標 children . forEach ( function ( child ) { var childY = startY + padding ; // 已經求得當前子節點坐標(childX, childY),在這里作渲染操作 var curAreaHeight = getNodeHeight ( child ) + padding * 2 ; startY += curAreaHeight ; // 其實高度累加 } ) ; /** * 獲取節點的高度 */ function getNodeHeight ( ) { // ... } |
祖先節點的同級節點的垂直位置調整
如下圖所示,當增加一個節點時,該節點父節點的同級節點需要被“撐開”:設該節點的1/2區域高度為moveY,在父節點的同級節點中,比父節 點高的向上偏移一個moveY,比父節點低的向下偏移一個moveY。父節點的父節點的同級節點也做相同的處理,一直遞歸到根節點為止。當刪除一個節點 時,節點的父節點的同級節點會被“壓低”,“壓低”操作和上述操作相似。注意,當增加第一個子節點和刪除最后一個子節點時,不會進行“撐開”和“壓低”操 作。
實現源碼如下:
/** * 調整當前的父節點的同級節點的位置 * @param node 當前的父節點, 以下為該節點需要用到的屬性 * node.father: 節點的父節點,為null時表示父節點為根節點 * node.children: 節點的子節點列表 * node.x: 節點的x軸坐標 * node.y: 節點的y軸坐標 * * @oaram areaHeight 被操作節點的區域高度 */ function resetBrotherPosition ( node , areaHeight ) { var brother , // 同級節點 moveY = areaHeight / 2 ; // 需要移動的高度 if ( node . father ) { node . father . children . forEach ( function ( curNode ) { // 遍歷同級節點 if ( curNode != node ) { if ( brother . y < node . y ) { // 向上移動brother節點的代碼寫在這 } else { // 向下移動brother節點的代碼寫在這 } } } ) ; } // 遞歸父節點 if ( node . father ) { resetBrotherPosition ( node . father , areaHeight ) ; } } |
拖動節點
當拖動根節點時,通過改變svg的視口坐標來實現拖動整個思維導圖的效果。當拖動
非根節點時,會按順序觸發mouseup、mousemove、mousedown三個事件,分別對應按下鼠標、鼠標移動和放下鼠標三個狀態。 在按下鼠標狀態下,會以當前節點為原型克隆一個節點用于占位。在拖動鼠標狀態下,通過改變節點的坐標實現節點位置的改變。在放下鼠標狀態下,會判斷當前節 點是否與其他節點重疊,如果重疊則使重疊節點變為當前節點的父節點,否則,當前節點返回原來的位置。
其他技術點我就不一一列出來了,有興趣的同學可以到上面的傳送門看看源碼。