緣起:
接續這篇,這邊要介紹的是,怎麼在 ForceDirected 的 LinkedHierarchyNode
裡面放圖片,並讓圖片的大小可以跟著圓圈的大小來變動,這也是我碰到的最大難題,因為我沒在
api 文件裡找到任何跟這個有關的功能。
官方雖然有教如何在圓圈裡面放圖片,但放完圖片後,圖片的 size
不會隨著圓圈的大小來變動,所以只能自己再另尋方法。
我把自己隨意畫的圓圈 png 圖傳到我作為 blogger CDN 的 github
專案,為了讓圖片更完美的切合 ForceDirected
的圓圈,所以圖片的長跟寬都是跟圓圈的直徑一樣。
在 LinkedHierarchyNode 放圖片:
如果按照官方寫的,把圖片加入 Node 裡,我的程式會是這樣
myAm5ForceDirected.nodes.template.setup = function (target) {
target.events.on("dataitemchanged", function (ev) {
var icon = target.children.push(am5.Picture.new(root, {
width: 70,
height: 70,
centerX: am5.percent(50),
centerY: am5.percent(50),
src: ev.target.dataItem.dataContext.image
}));
});
}
如果我之後想要再接著設定圖片大小符合圓的大小時,需要取得
Circle,但在 dataitemchanged 發生的當下,LinkedHierarchyNode 的 children
裡面還沒有加入 Circle,所以也沒辧法取得 Circle 的 radius。
後來發現,比較優的解法是設定在 root 的 frameended 事件中,因為事件發生的時候,元件都有加上去了,所以整個 index.js
程式會寫成
document.addEventListener("DOMContentLoaded", function () {
let myAm5Root = am5.Root.new("myAmchartDiv"); //傳入 div 的 id
myAm5Root.setThemes([
am5themes_Animated.new(myAm5Root) //設定 root 的動畫效果,預設即可
]);
let myAm5Container = myAm5Root.container.children.push(//root 裡放入一個 container
am5.Container.new(myAm5Root, {
width: am5.percent(100),
height: am5.percent(100),
layout: myAm5Root.verticalLayout
})
);
let myAm5ForceDirected = myAm5Container.children.push( //container 裡面放入 ForceDirected 圖表
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); //設定 ForceDirected 的資料
myAm5ForceDirected.set("selectedDataItem", myAm5ForceDirected.dataItems[0]);//採用第一個 Object
myAm5Root.events.on('frameended', frameendedfunction); //root 的 frameended
function frameendedfunction(ev) {
//由於圖片只需要設定一次,所以執行時要順便解除 frameended 事件
myAm5Root.events.off('frameended', frameendedfunction);
myAm5ForceDirected.nodes.each(function (node, index) {
let am5Picture = node.children.push(am5.Picture.new(myAm5Root, { //設定 node 的圖片
centerX: am5.percent(50),
centerY: am5.percent(50),
width: 70, //長
height: 70, //寬
src: node.dataItem.dataContext.image //可以透過 dataItem.dataContext 來取得對應的 Object 資料
}));
})
}
});
28~76 行 : 原本的 Object data,每個 Object 都再加入 image 的 key,value
對應的是圖片網址
84 行 : 監聽 root 的 frameended
事件,由於之後還需要解除,所以 function 不能寫成匿名的,這邊是新增一個叫
frameendedfunction 來註冊。
88 行 : 執行的當下,用 nodes 的 each
方法來逐一為每個 node 加入 image,把圖片放在中心,長跟寬都先固定
結果會是像這樣
See the Pen ForceDirected-4 by 鳥旭 (@lbdjszpl-the-lessful) on CodePen.
由於圖片大小是設固定的,所以擺在大小不一的圓上,看起來很怪,所以接下來要依每個圓的大小來設定圖片。
從 node 的 children 裡把 Circle 給取出來後,可以透過 get
來取得 radius 屬性
function frameendedfunction(ev) {
//由於圖片只需要設定一次,所以執行時要順便解除 frameended 事件
myAm5Root.events.off('frameended', frameendedfunction);
myAm5ForceDirected.nodes.each(function (node, index) {
let am5Picture = node.children.push(am5.Picture.new(myAm5Root, { //設定 node 的圖片
centerX: am5.percent(50),
centerY: am5.percent(50),
width: 70, //長
height: 70, //寬
src: node.dataItem.dataContext.image //可以透過 dataItem.dataContext 來取得對應的 Object 資料
}));
let am5Circle = null;
node.children.each(function (nodeChildren, nodeChildrenIndex) {
if (nodeChildren.className === 'Circle') {
//這邊會有兩個 Circle,外層跟內層,外層的 forceHidden 有被設定成 true,所以要取 forceHidden 為 undefined 的 Circle
if (nodeChildren.get('forceHidden') === undefined) am5Circle = nodeChildren;
}
});
am5Picture.setAll({ //依圓的大小來設定圖片的高跟寬
width: am5Circle.get('radius') * 2,
height: am5Circle.get('radius') * 2
});
})
}
這邊要注意的是第 17 行,LinkedHierarchyNode 的
children 裡會有兩個 Circle,一個是外層的,就是邊框,我們有把它給隱藏起來 (設定
forceHidden 屬性),另一個 Circle 才是我們要的,所以這裡用
nodeChildren.get('forceHidden') === undefined 來判斷。
結果會是這樣.....
See the Pen ForceDirected-4 by 鳥旭 (@lbdjszpl-the-lessful) on CodePen.
看起來怪怪的,我在猜,大概又是執行的當下,Circle 的 radius
還沒被設定好,所以才會這樣。如果用 setTimeout 把設定長寬的動作延後個 300
毫秒,就可以得到想要的結果了
function frameendedfunction(ev) {
//由於圖片只需要設定一次,所以執行時要順便解除 frameended 事件
myAm5Root.events.off('frameended', frameendedfunction);
myAm5ForceDirected.nodes.each(function (node, index) {
let am5Picture = node.children.push(am5.Picture.new(myAm5Root, { //設定 node 的圖片
centerX: am5.percent(50),
centerY: am5.percent(50),
width: 70, //長
height: 70, //寬
src: node.dataItem.dataContext.image //可以透過 dataItem.dataContext 來取得對應的 Object 資料
}));
let am5Circle = null;
node.children.each(function (nodeChildren, nodeChildrenIndex) {
if (nodeChildren.className === 'Circle') {
//這邊會有兩個 Circle,外層跟內層,外層的 forceHidden 有被設定成 true,所以要取 forceHidden 為 undefined 的 Circle
if (nodeChildren.get('forceHidden') === undefined) am5Circle = nodeChildren;
}
});
setTimeout(() => {
am5Picture.setAll({ //依圓的大小來設定圖片的高跟寬
width: am5Circle.get('radius') * 2,
height: am5Circle.get('radius') * 2
});
}, 300);//延後 300 毫秒
})
}
結果
See the Pen ForceDirected-5 by 鳥旭 (@lbdjszpl-the-lessful) on CodePen.
這樣就能在載入圖表後,讓圖片的大小盡量貼齊所屬 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") 外面。
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);
}
3~9 行 : get
方法,我一開始看不懂它為啥會寫得那麼麻煩,後來才發覺,它這個很像遞迴的寫法,其實是為了處理複雜物件,如果確定
targetObject 就只是簡單物件的話,那其實直接寫成第 7 行那樣就行。get
的部份,我們就不多做什麼處理,就讓它是正常的取得屬性。
第 11 行 : 在 set
部份,如果它變動的是 radius 這個屬性,那我們就再執行傳入的 callback
function,並把 value 給傳入。
接著是改寫 frameendedfunction 裡的設定 Picture 大小部份,不要用 setTimeout
了
function frameendedfunction(ev) {
//由於圖片只需要設定一次,所以執行時要順便解除 frameended 事件
myAm5Root.events.off('frameended', frameendedfunction);
myAm5ForceDirected.nodes.each(function (node, index) {
let am5Picture = node.children.push(am5.Picture.new(myAm5Root, { //設定 node 的圖片
centerX: am5.percent(50),
centerY: am5.percent(50),
width: 70, //長
height: 70, //寬
src: node.dataItem.dataContext.image //可以透過 dataItem.dataContext 來取得對應的 Object 資料
}));
let am5Circle = null;
node.children.each(function (nodeChildren, nodeChildrenIndex) {
if (nodeChildren.className === 'Circle') {
//這邊會有兩個 Circle,外層跟內層,外層的 forceHidden 有被設定成 true,所以要取 forceHidden 為 undefined 的 Circle
if (nodeChildren.get('forceHidden') === undefined) am5Circle = nodeChildren;
}
});
//把 Circle 的 _settings 給掉包成 Proxy 版本的 _setting
am5Circle._settings = MyProxy(am5Circle._settings, function (iRadius) {
am5Picture.setAll({ //依傳入的半徑來設定圖片的長跟寬
width: iRadius * 2,
height: iRadius * 2
});
})
})
}
最後,完整的 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"); //傳入 div 的 id
myAm5Root.setThemes([
am5themes_Animated.new(myAm5Root) //設定 root 的動畫效果,預設即可
]);
let myAm5Container = myAm5Root.container.children.push(//root 裡放入一個 container
am5.Container.new(myAm5Root, {
width: am5.percent(100),
height: am5.percent(100),
layout: myAm5Root.verticalLayout
})
);
let myAm5ForceDirected = myAm5Container.children.push( //container 裡面放入 ForceDirected 圖表
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); //設定 ForceDirected 的資料
myAm5ForceDirected.set("selectedDataItem", myAm5ForceDirected.dataItems[0]);//採用第一個 Object
myAm5Root.events.on('frameended', frameendedfunction); //root 的 frameended
function frameendedfunction(ev) {
//由於圖片只需要設定一次,所以執行時要順便解除 frameended 事件
myAm5Root.events.off('frameended', frameendedfunction);
myAm5ForceDirected.nodes.each(function (node, index) {
let am5Picture = node.children.push(am5.Picture.new(myAm5Root, { //設定 node 的圖片
centerX: am5.percent(50),
centerY: am5.percent(50),
width: 70, //長
height: 70, //寬
src: node.dataItem.dataContext.image //可以透過 dataItem.dataContext 來取得對應的 Object 資料
}));
let am5Circle = null;
node.children.each(function (nodeChildren, nodeChildrenIndex) {
if (nodeChildren.className === 'Circle') {
//這邊會有兩個 Circle,外層跟內層,外層的 forceHidden 有被設定成 true,所以要取 forceHidden 為 undefined 的 Circle
if (nodeChildren.get('forceHidden') === undefined) am5Circle = nodeChildren;
}
});
//把 Circle 的 _settings 給掉包成 Proxy 版本的 _setting
am5Circle._settings = MyProxy(am5Circle._settings, function (iRadius) {
am5Picture.setAll({ //依傳入的半徑來設定圖片的長跟寬
width: iRadius * 2,
height: iRadius * 2
});
})
})
}
});
這樣,就能讓 Picture 隨著 Circle 的大小而變化了
See the Pen ForceDirected-6 by 鳥旭 (@lbdjszpl-the-lessful) on CodePen.
沒有留言:
張貼留言