緣起:
接續這篇,這邊要介紹的是,怎麼在 ForceDirected 的 LinkedHierarchyNode
裡面放圖片,並讓圖片的大小可以跟著圓圈的大小來變動,這也是我碰到的最大難題,因為我沒在
api 文件裡找到任何跟這個有關的功能。
官方雖然有教如何在圓圈裡面放圖片,但放完圖片後,圖片的 size
不會隨著圓圈的大小來變動,所以只能自己再另尋方法。
我把自己隨意畫的圓圈 png 圖傳到我作為 blogger CDN 的 github
專案,為了讓圖片更完美的切合 ForceDirected
的圓圈,所以圖片的長跟寬都是跟圓圈的直徑一樣。
在 LinkedHierarchyNode 放圖片:
如果按照官方寫的,把圖片加入 Node 裡,我的程式會是這樣
如果我之後想要再接著設定圖片大小符合圓的大小時,需要取得
Circle,但在 dataitemchanged 發生的當下,LinkedHierarchyNode 的 children
裡面還沒有加入 Circle,所以也沒辧法取得 Circle 的 radius。
後來發現,比較優的解法是設定在 root 的 frameended 事件中,因為事件發生的時候,元件都有加上去了,所以整個 index.js
程式會寫成
document.addEventListener("DOMContentLoaded", function () {
let myAm5Root = am5.Root.new("myAmchartDiv");
myAm5Root.setThemes([
am5themes_Animated.new(myAm5Root)
]);
let myAm5Container = myAm5Root.container.children.push(
am5.Container.new(myAm5Root, {
width: am5.percent(100),
height: am5.percent(100),
layout: myAm5Root.verticalLayout
})
);
let myAm5ForceDirected = myAm5Container.children.push(
am5hierarchy.ForceDirected.new(myAm5Root, {
downDepth: 1,
initialDepth: 3,
topDepth: 1,
valueField: "value",
categoryField: "name",
childDataField: "children"
})
);
let myObjData = [{
name: "Root",
value: 0,
children: [{
name: "A0",
value: 100,
image:"https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle1.png",
children: [{
name: "A0A1",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle2.png",
value: 80,
children: [{
name: "A0A0A2",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle3.png",
value: 71
}, {
name: "A0A0C2",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle4.png",
value: 48
}]
}, {
name: "A0B1",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle5.png",
value: 27
}, {
name: "A0C1",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle6.png",
value: 70,
children: [{
name: "A0C2A2",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle7.png",
value: 40
}, {
name: "A0C2B2",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle8.png",
value: 40,
children: [{
name: "A0C2B1A3",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle9.png",
value: 54
}]
}]
}, {
name: "A0D1",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle10.png",
value: 89
}]
}]
}];
myAm5ForceDirected.nodes.template.set("draggable", false);
myAm5ForceDirected.nodes.template.set("toggleKey", "none");
myAm5ForceDirected.outerCircles.template.set("forceHidden", true);
myAm5ForceDirected.data.setAll(myObjData);
myAm5ForceDirected.set("selectedDataItem", myAm5ForceDirected.dataItems[0]);
myAm5Root.events.on('frameended', frameendedfunction);
function frameendedfunction(ev) {
myAm5Root.events.off('frameended', frameendedfunction);
myAm5ForceDirected.nodes.each(function (node, index) {
let am5Picture = node.children.push(am5.Picture.new(myAm5Root, {
centerX: am5.percent(50),
centerY: am5.percent(50),
width: 70,
height: 70,
src: node.dataItem.dataContext.image
}));
})
}
});
28~76 行 : 原本的 Object data,每個 Object 都再加入 image 的 key,value
對應的是圖片網址
84 行 : 監聽 root 的 frameended
事件,由於之後還需要解除,所以 function 不能寫成匿名的,這邊是新增一個叫
frameendedfunction 來註冊。
88 行 : 執行的當下,用 nodes 的 each
方法來逐一為每個 node 加入 image,把圖片放在中心,長跟寬都先固定
結果會是像這樣
由於圖片大小是設固定的,所以擺在大小不一的圓上,看起來很怪,所以接下來要依每個圓的大小來設定圖片。
從 node 的 children 裡把 Circle 給取出來後,可以透過 get
來取得 radius 屬性
所以 frameendedfunction 就會改寫成這樣
這邊要注意的是第 17 行,LinkedHierarchyNode 的
children 裡會有兩個 Circle,一個是外層的,就是邊框,我們有把它給隱藏起來 (設定
forceHidden 屬性),另一個 Circle 才是我們要的,所以這裡用
nodeChildren.get('forceHidden') === undefined 來判斷。
結果會是這樣.....
看起來怪怪的,我在猜,大概又是執行的當下,Circle 的 radius
還沒被設定好,所以才會這樣。如果用 setTimeout 把設定長寬的動作延後個 300
毫秒,就可以得到想要的結果了
結果
這樣就能在載入圖表後,讓圖片的大小盡量貼齊所屬 Circle
的大小,但是之後 Circle 如果有變化,圖片是不會再變動的。
使用 Proxy 來監控 Circle 的變化:
我自己在測試時,在列印 Circle 物件有發現到,它有一個叫
_settings 的屬性,然後 _settings 裡又有一個型態為 Number 的 radius
屬性
我有試著用全域變數來把某個 Circle 的 _settings 給記下來,然後在調整完視窗、圓的大小有變化後,再把那變數給印出來,發現,每次調整完之後,它的
radius 確實都會有變化。這代表 ForceDirected
的程式在動態調整圓圈時,會去動到那個 _settings 的
radius,所以,只要可以監聽到那個 _settings 在針對 radius 上的操作,當
radius 有變化時,我就把 Picture 的長跟寬設定成 radius X 2
,這麼一來,那些圖片也就能跟著圓的大小來動態調整了。
我在 Proxy 的使用上,是直接參考別人寫好的範例,然後再改成我要的版本,最後寫出來的創造 Proxy 物件的 function
長這樣。我把它給宣告在 index.js
的 document.addEventListener("DOMContentLoaded") 外面。
3~9 行 : get
方法,我一開始看不懂它為啥會寫得那麼麻煩,後來才發覺,它這個很像遞迴的寫法,其實是為了處理複雜物件,如果確定
targetObject 就只是簡單物件的話,那其實直接寫成第 7 行那樣就行。get
的部份,我們就不多做什麼處理,就讓它是正常的取得屬性。
第 11 行 : 在 set
部份,如果它變動的是 radius 這個屬性,那我們就再執行傳入的 callback
function,並把 value 給傳入。
接著是改寫 frameendedfunction 裡的設定 Picture 大小部份,不要用 setTimeout
了
最後,完整的 index.js 程式碼會是這樣
function MyProxy(targetObject, callback) {
const handler = {
get(target, property, receiver) {
try {
return new Proxy(target[property], handler);
} catch (err) {
return Reflect.get(target, property, receiver);
}
},
set(target, property, value) {
if (property == 'radius') {
callback(value)
}
target[property] = value;
return true;
}
};
return new Proxy(targetObject, handler);
}
document.addEventListener("DOMContentLoaded", function () {
let myAm5Root = am5.Root.new("myAmchartDiv");
myAm5Root.setThemes([
am5themes_Animated.new(myAm5Root)
]);
let myAm5Container = myAm5Root.container.children.push(
am5.Container.new(myAm5Root, {
width: am5.percent(100),
height: am5.percent(100),
layout: myAm5Root.verticalLayout
})
);
let myAm5ForceDirected = myAm5Container.children.push(
am5hierarchy.ForceDirected.new(myAm5Root, {
downDepth: 1,
initialDepth: 3,
topDepth: 1,
valueField: "value",
categoryField: "name",
childDataField: "children"
})
);
let myObjData = [{
name: "Root",
value: 0,
children: [{
name: "A0",
value: 100,
image:"https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle1.png",
children: [{
name: "A0A1",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle2.png",
value: 80,
children: [{
name: "A0A0A2",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle3.png",
value: 71
}, {
name: "A0A0C2",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle4.png",
value: 48
}]
}, {
name: "A0B1",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle5.png",
value: 27
}, {
name: "A0C1",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle6.png",
value: 70,
children: [{
name: "A0C2A2",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle7.png",
value: 40
}, {
name: "A0C2B2",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle8.png",
value: 40,
children: [{
name: "A0C2B1A3",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle9.png",
value: 54
}]
}]
}, {
name: "A0D1",
image: "https://cdn.jsdelivr.net/gh/birdshiu/static-blogger-resource/image/amchart-forcedirected/ForceDirected-Circle10.png",
value: 89
}]
}]
}];
myAm5ForceDirected.nodes.template.set("draggable", false);
myAm5ForceDirected.nodes.template.set("toggleKey", "none");
myAm5ForceDirected.outerCircles.template.set("forceHidden", true);
myAm5ForceDirected.data.setAll(myObjData);
myAm5ForceDirected.set("selectedDataItem", myAm5ForceDirected.dataItems[0]);
myAm5Root.events.on('frameended', frameendedfunction);
function frameendedfunction(ev) {
myAm5Root.events.off('frameended', frameendedfunction);
myAm5ForceDirected.nodes.each(function (node, index) {
let am5Picture = node.children.push(am5.Picture.new(myAm5Root, {
centerX: am5.percent(50),
centerY: am5.percent(50),
width: 70,
height: 70,
src: node.dataItem.dataContext.image
}));
let am5Circle = null;
node.children.each(function (nodeChildren, nodeChildrenIndex) {
if (nodeChildren.className === 'Circle') {
if (nodeChildren.get('forceHidden') === undefined) am5Circle = nodeChildren;
}
});
am5Circle._settings = MyProxy(am5Circle._settings, function (iRadius) {
am5Picture.setAll({
width: iRadius * 2,
height: iRadius * 2
});
})
})
}
});
這樣,就能讓 Picture 隨著 Circle 的大小而變化了
沒有留言:
張貼留言