開發環境:
Power BI Desktop 2017
API版本v1.9.0
導入離線版
導入在線版d3.js.(3.0以上的是可以安裝D3的,詳細的可以參考微軟官方demo介紹:https://docs.microsoft.com/zh-cn/power-bi/developer/visuals/custom-visual-develop-tutorial)
以上兩種導入方案在https://blog.csdn.net/Javon_huang/article/details/105192597 有介紹,此不做講解
1.visual.ts
/*
* Power BI Visual CLI
*
* Copyright (c) Microsoft Corporation
* All rights reserved.
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the ""Software""), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
module powerbi.extensibility.visual {
"use strict";
export class Visual implements IVisual {
private target: HTMLElement;
private tips:HTMLElement;
private d3node: HTMLElement;
private settings: VisualSettings;
private textNode: Text;
private d3: any;
private rootData:any;
private rootName:string;
private width:number;
private height:number;
private d3Load:any;
private rootList:Array<any>;
private timer:any;
private valueSources:Array<any>;
private list:Array<any>
constructor(options: VisualConstructorOptions) {
console.log('Visual constructor', options);
this.target = options.element;
this.d3Load=false;
if (typeof document !== "undefined") {
this.tips = document.createElement("p");
this.tips.appendChild(document.createTextNode("正在加載資源..."));
this.target.appendChild(this.tips);
this.d3node= document.createElement("div");
this.d3node.id="product_tree";
this.target.appendChild(this.d3node);
}
this.onloadD3();
}
public update(options: VisualUpdateOptions) {
this.width = options.viewport.width;
this.height= options.viewport.height;
this.settings = Visual.parseSettings(options && options.dataViews && options.dataViews[0]);
let dataTree:any=options.dataViews[0].matrix;
this.valueSources=options.dataViews[0].matrix.valueSources;
this.list=dataTree.rows.root.children;
console.log(options);
clearTimeout(this.timer);
this.timer=setTimeout(()=>{
this.rootList=[{
direction: "downward",
name: "origin",
value:"origin",
children:this.list,
level:-1
}];
this.readTree(this.rootList);
this.mapTree(this.rootList);
this.AddLastNode(this.rootList);
this.target.removeChild(this.d3node);
this.d3node= document.createElement("div");
this.d3node.id="product_tree";
this.target.appendChild(this.d3node);
this.checkJsLoad();
},500);
}
private readTree(list:Array<any>){
let that=this;
// debugger;
list.forEach((item)=>{
// item.name=item.level;
item.name=item.value;
item.amount="100";
item.ratio="55%";
item.hasHumanholding=false;
item.hasChildren=false;
item.isExpand=false;
if(typeof item.children !="undefined"){
item.count=0;
that.readTree(item.children);
}else{
item.count=item.values[0].value;
}
});
}
private mapTree(list:Array<any>){
let that=this;
list.forEach((item)=>{
if(typeof item.children !="undefined"){
item.count=that.countNum(item);
that.mapTree(item.children);
}
});
}
private countNum(node:any){
let that=this;
let result:Array<any>=new Array();
getList(node.children,result);
let count=0;
result.forEach((item)=>{
count+=item.count;
});
return count;
function getList(list:Array<any>,result:Array<any>){
list.forEach((item)=>{
if(typeof item.children =="undefined"){
result.push(item);
}else{
getList(item.children,result);
}
});
return result;
}
}
private AddLastNode(list:Array<any>){
let that=this;
// debugger;
list.forEach((item)=>{
if(typeof item.children !="undefined"){
that.AddLastNode(item.children);
}else{
if(Object.keys(item.values).length>1){
let keyList=Object.keys(item.values);
let nextChildren=new Array();
keyList.forEach((_item:any)=>{
if(_item>0&&item.values[_item].value!=null){
nextChildren.push({
level:item.level+1,
// name:item.level+1,
value:that.valueSources[item.values[_item].valueSourceIndex].displayName,
values:{0:{value:item.values[_item].value}},
count:item.values[_item].value,
name:that.valueSources[item.values[_item].valueSourceIndex].displayName,
amount:"100",
ratio:"55%",
hasHumanholding:false,
hasChildren:false,
isExpand:false
})
}
});
item.children=nextChildren;
}
}
});
}
private onloadD3(){
let script=document.createElement('script');
script.type="text/javascript";
script.src="https://cdn.bootcss.com/d3/3.2.7/d3.min.js";
document.body.appendChild(script);
let that=this;
script.onload=function(){
that.d3=d3;
that.d3Load=true;
that.tips.innerHTML="加載完成";
}
}
private getData(){
this.rootData ={};
this.rootData={
downward:this.rootList[0],
upward:{
direction: "upward",
name: "origin",
children: []
}
}
console.log(this.rootData);
this.rootName = '我是根節我是根節點點';
this.drawing();
}
private drawing(){
let _this=this;
let rootRectWidth = 0; //根節點rect的寬度
let downwardLength = 0,
upwardLength = 0;
let forUpward = true;
let treeChart = function(d3Object) {
this.d3 = d3Object;
this.directions = ['upward', 'downward'];
};
treeChart.prototype.drawChart = function() {
this.treeData = {};
let self = this;
self.directions.forEach(function(direction) {
self.treeData[direction] = _this.rootData[direction];
});
// rootName = '上海冰鑑信息科技有限公司';
rootRectWidth = _this.rootName.length * 15;
//獲得upward第一級節點的個數
upwardLength = _this.rootData.upward.children.length;
//獲得downward第一級節點的個數
downwardLength = _this.rootData.downward.children.length;
self.graphTree(self.getTreeConfig());
};
treeChart.prototype.getTreeConfig = function() {
let treeConfig:any = {
'margin': {
'top': 0,
'right': 0,
'bottom': 0,
'left': 0
}
}
treeConfig.chartWidth = (_this.width - treeConfig.margin.right - treeConfig.margin.left);
treeConfig.chartHeight = (_this.height - treeConfig.margin.top - treeConfig.margin.bottom);
treeConfig.centralHeight = treeConfig.chartHeight / 4;
treeConfig.centralWidth = treeConfig.chartWidth / 2;
treeConfig.linkLength = 120;
treeConfig.duration = 500; //動畫時間
return treeConfig;
};
treeChart.prototype.graphTree = function(config) {
let self = this;
let d3 = _this.d3;
let linkLength = config.linkLength;
let duration = config.duration;
let hasChildNodeArr = [];
let id = 0;
let diagonal;
let treeG:any =null;
//折線
let funLine = function(obj) {
let d=obj.source;
let c=obj.target;
return `M${d.x},${d.y+0} ${d.x},${d.y+80} ${c.x},${d.y+80} ${c.x},${c.y}`;
};
diagonal = funLine;
let zoom = d3.behavior.zoom()
.scaleExtent([0.5, 2])
.on('zoom', redraw);
let svg = d3.select('#product_tree')
.append('svg')
.attr('width', config.chartWidth + config.margin.right + config.margin.left)
.attr('height', config.chartHeight + config.margin.top + config.margin.bottom)
.attr('xmlns','http://www.w3.org/2000/svg')
.on('mousedown', disableRightClick)
.call(zoom)
.on('dblclick.zoom', null);
treeG = svg.append('g')
.attr('class', 'gbox')
.attr('transform', 'translate(' + config.margin.left + ',' + config.margin.top + ')');
//箭頭(下半部分)
let markerDown = svg.append("marker")
.attr("id", "resolvedDown")
.attr("markerUnits", "strokeWidth") //設置爲strokeWidth箭頭會隨着線的粗細發生變化
.attr("markerUnits", "userSpaceOnUse")
.attr("viewBox", "0 -5 10 10") //座標系的區域
.attr("refX", 0) //箭頭座標
.attr("refY", 0)
.attr("markerWidth", 12) //標識的大小
.attr("markerHeight", 12)
.attr("orient", "90") //繪製方向,可設定爲:auto(自動確認方向)和 角度值
.attr("stroke-width", 2) //箭頭寬度
.append("path")
.attr("d", "M0,-5L10,0L0,5") //箭頭的路徑
.attr('fill', _this.settings.dataPoint.arrowColor); //箭頭顏色
// Initialize the tree nodes and update chart.
for(var d in this.directions) {
var direction = this.directions[d];
var data = self.treeData[direction];
data.x0 = config.centralWidth;
data.y0 = config.centralHeight;
data.children.forEach(collapse);
update(data, data, treeG);
}
function update(source, originalData, g) {
var direction = originalData['direction'];
forUpward = direction == 'upward';
var node_class = direction + 'Node';
var link_class = direction + 'Link';
var downwardSign = (forUpward) ? -1 : 1;
var nodeColor = (forUpward) ? '#37592b' : '#8b4513';
var isExpand = false;
var statusUp = true;
var statusDown = true;
var nodeSpace = 130;
var tree = d3.layout.tree().sort(sortByDate).nodeSize([nodeSpace, 0]);
var nodes = tree.nodes(originalData);
var links = tree.links(nodes);
var offsetX = -config.centralWidth;
nodes.forEach(function(d) {
d.y = downwardSign * (d.depth * linkLength) + config.centralHeight;
d.x = d.x - offsetX;
if(d.name == 'origin') {
d.x = config.centralWidth;
d.y += downwardSign * 0+50; // 上下兩樹圖根節點之間的距離
}
});
// Update the node.
var node = g.selectAll('g.' + node_class)
.data(nodes, function(d) {
return d.id || (d.id = ++id);
});
var nodeEnter = node.enter().append('g')
.attr('class', node_class)
.attr('transform', function(d) {
return 'translate(' + source.x0 + ',' + source.y0 + ')';
})
.style('cursor', function(d) {
return(d.name == 'origin') ? '' : (d.children || d._children) ? 'pointer' : '';
});
// .on('click', click);
nodeEnter.append("svg:rect")
.attr("x", function(d) {
return(d.name == 'origin') ? -(rootRectWidth / 2) : -60;
})
.attr("y", function(d) {
return(d.name == 'origin') ? -20 : forUpward ? -52 : 12;
})
.attr("width", function(d) {
return(d.name == 'origin') ? rootRectWidth : 120;
})
.attr("height", 40)
.attr("rx", 10)
.style("stroke", function(d) {
// return(d.name == 'origin') ? "#1078AF" : "#CCC";
return "transparent";
})
.style("fill", function(d) {
//節點背景色
if(d.level%2!=0){
return _this.settings.dataPoint.singularColor;
}else{
return _this.settings.dataPoint.dualColor;
}
});
nodeEnter.append('circle')
.attr('r', 1e-6);
nodeEnter.append("text")
.attr("class", "linkname")
.attr("x", function(d) {
return(d.name == 'origin') ? '0' : "0";
})
.attr('dy', function(d) {
return(d.name == 'origin') ? '.35em' : forUpward ? '-40' : '30';
})
.attr("text-anchor", function(d) {
return(d.name == 'origin') ? 'middle' : "middle";
})
.attr('fill', '#fff')
.text(function(d) {
if(d.name == 'origin') {
// return ((forUpward) ? '根節點TOP' : '根節點Bottom');
return _this.rootName;
}
if(d.repeated) {
return '[Recurring] ' + d.name;
}
return(d.name.length > 10) ? d.name.substr(0, 10) : d.name;
})
.style({
'fill-opacity': 1e-6,
'fill': function(d) {
if(d.level%2!=0){
return _this.settings.dataPoint.singularFontColor;
}else{
return _this.settings.dataPoint.dualFontColor;
}
},
'font-size': function(d) {
if(d.level%2!=0){
return _this.settings.dataPoint.singularFontSize;
}else{
return _this.settings.dataPoint.dualFontSize;
}
},
'cursor': "pointer"
})
.on('click', Change_modal);
nodeEnter.append("text")
.attr("class", "linkname")
.attr("x", "-55")
.attr("dy", function(d) {
return(d.name == 'origin') ? '.35em' : forUpward ? '-29' : '35';
})
.attr("text-anchor", function(d) {
return(d.name == 'origin') ? 'middle' : "start";
})
.text(function(d) {
return d.name.substr(10, d.name.length);
})
.style({
'fill': "#337ab7",
'font-size': function(d) {
return(d.name == 'origin') ? 14 : 11;
},
'cursor': "pointer"
});
// .on('click', Change_modal);
nodeEnter.append("text")
.attr("x", "0")
.attr("dy", function(d) {
return(d.name == 'origin') ? '.35em' : forUpward ? '-16' : '45';
})
.attr("text-anchor", "middle")
.attr("class", "linkname")
.style("fill", _this.settings.dataPoint.numColor)
.style('font-size', _this.settings.dataPoint.numFontSize)
.text(function(d) {
var str = (d.name == 'origin') ? '' : d.count ;
return(str.length > 13) ? str.substr(0, 13) + ".." : str;
});
nodeEnter.append("text")
.attr("x", "10")
.attr("dy", function(d) {
return(d.name == 'origin') ? '.35em' : forUpward ? '0' : '10';
})
.attr("text-anchor", "start")
.attr("class", "linkname")
.style("fill", "green")
.style('font-size', 10)
// .text(function(d) {
// return(d.name == 'origin') ? "" : d.ratio;
// });
// Transition nodes to their new position.原有節點更新到新位置
var nodeUpdate = node.transition()
.duration(duration)
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
nodeUpdate.select('circle')
.attr('r', function(d) {
return(d.name == 'origin') ? 0 : (hasChildNodeArr.indexOf(d) == -1) ? 0 : 6;
})
.attr('cy', function(d) {
return(d.name == 'origin') ? -20 : (forUpward) ? -59 : 59;
})
.style('fill', function(d) {
return hasChildNodeArr.indexOf(d) != -1 ? _this.settings.dataPoint.pointBackGroupColor : "";
// if (d._children || d.children) { return "#fff"; } else { return "rgba(0,0,0,0)"; }
})
.style('stroke', function(d) {
return hasChildNodeArr.indexOf(d) != -1 ? _this.settings.dataPoint.pointColor : "";
// if (d._children || d.children) { return "#8b4513"; } else { return "rgba(0,0,0,0)"; }
})
.style('fill-opacity', function(d) {
if(d.children) {
return 0.35;
}
})
// Setting summary node style as class as mass style setting is
// not compatible to circles.
.style('stroke-width', function(d) {
if(d.repeated) {
return 5;
}
});
//代表是否展開的+-號
nodeEnter.append("svg:text")
.attr("class", "isExpand")
.attr("x", "0")
.attr("dy", function(d) {
return forUpward ? -56 : 63;
})
.attr("text-anchor", "middle")
.style("fill", _this.settings.dataPoint.pointColor)
.text(function(d) {
if(d.name == 'origin') {
return '';
}
return hasChildNodeArr.indexOf(d) != -1 ? "+" : "";
// /* if (d._children || d.children) {
// return "+";
// } */
})
.on('click',click)
nodeUpdate.select('text').style('fill-opacity', 1)
var nodeExit = node.exit().transition()
.duration(duration)
.attr('transform', function(d) {
return 'translate(' + source.x + ',' + source.y + ')';
})
.remove();
nodeExit.select('circle')
.attr('r', 1e-6)
nodeExit.select('text')
.style('fill-opacity', 1e-6);
var link = g.selectAll('path.' + link_class)
.data(links, function(d) {
return d.target.id;
});
link.enter().insert('path', 'g')
.attr('class', link_class)
.attr('stroke',function(d){
return _this.settings.dataPoint.lineColor;
})
.attr('fill',"none")
.attr('stroke-width','1px')
.attr('opacity', 1)
.attr('d', function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
})
.attr("marker-end", function(d) {
return forUpward ? "url(#resolvedUp)" : "url(#resolvedDown)";
}) //根據箭頭標記的id號標記箭頭;
.attr("id", function(d, i) {
return "mypath" + i;
})
link.transition()
.duration(duration)
.attr('d', diagonal);
link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
function Change_modal () {
// _this.Modal = true
}
function click(d) {
if(forUpward) {
} else {
if(d._children) {
console.log('對外投資--ok')
} else {
console.log('對外投資--no')
}
}
isExpand = !isExpand;
if(d.name == 'origin') {
return;
}
if(d.children) {
d._children = d.children;
d.children = null;
d3.select(this).text('+')
} else {
d.children = d._children;
d._children = null;
// expand all if it's the first node
if(d.name == 'origin') {
d.children.forEach(expand);
}
d3.select(this).text('-')
}
update(d, originalData, g);
}
}
function redraw() {
treeG.attr('transform', 'translate(' + d3.event.translate + ')' +' scale(' + d3.event.scale + ')');
};
function disableRightClick() {
if(d3.event.button == 2) {
console.log('No right click allowed');
d3.event.stopImmediatePropagation();
}
};
function collapse(d) {
if(d.children && d.children.length != 0) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
hasChildNodeArr.push(d);
}
};
};
function sortByDate(a, b) {
var aNum = a.name.substr(a.name.lastIndexOf('(') + 1, 4);
var bNum = b.name.substr(b.name.lastIndexOf('(') + 1, 4);
return d3.ascending(aNum, bNum) ||
d3.ascending(a.name, b.name) ||
d3.ascending(a.id, b.id);
};
function expand(d) {
if(d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
};
let d3GenerationChart = new treeChart(d3);
d3GenerationChart.drawChart();
};
//檢查第三方js加載
private checkJsLoad(){
let that=this;
if(that.d3Load){
that.tips.innerHTML="";
that.getData();
}
}
private static parseSettings(dataView: DataView): VisualSettings {
return VisualSettings.parse(dataView) as VisualSettings;
}
/**
* This function gets called for each of the objects defined in the capabilities files and allows you to select which of the
* objects and properties you want to expose to the users in the property pane.
*
*/
public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
return VisualSettings.enumerateObjectInstances(this.settings || VisualSettings.getDefault(), options);
}
}
}
2.capabilities.json
{
"dataRoles": [{
"name": "Category",
"displayName": "Category",
"displayNameKey": "Visual_Category",
"kind": "Grouping"
},
{
"name": "Measure",
"displayName": "Measure",
"displayNameKey": "Visual_Values",
"kind": "Measure"
}
],
"objects": {
"dataPoint": {
"displayName": "樹圖屬性",
"properties": {
"singularColor": {
"displayName": "單節點背景",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"singularFontSize": {
"displayName": "單節點字體",
"type": {
"formatting": {
"fontSize": true
}
}
},
"singularFontColor": {
"displayName": "單節點字體顏色",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"dualColor": {
"displayName": "雙節點背景",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"dualFontSize": {
"displayName": "雙節點字體",
"type": {
"formatting": {
"fontSize": true
}
}
},
"dualFontColor": {
"displayName": "雙節點字體顏色",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"numColor": {
"displayName": "數字顏色",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"numFontSize": {
"displayName": "數字字體",
"type": {
"formatting": {
"fontSize": true
}
}
},
"arrowColor": {
"displayName": "箭頭顏色",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"lineColor": {
"displayName": "折線顏色",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"pointColor": {
"displayName": "折點顏色",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"pointBackGroupColor": {
"displayName": "折點背景顏色",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
}
}
}
},
"dataViewMappings": [{
"matrix": {
"rows": {
"for": {
"in": "Category"
}
},
"values": {
"select": [{
"for": {
"in": "Measure"
}
}]
}
}
}]
}
3.settings.ts
/*
* Power BI Visualizations
*
* Copyright (c) Microsoft Corporation
* All rights reserved.
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the ""Software""), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
module powerbi.extensibility.visual {
"use strict";
import DataViewObjectsParser = powerbi.extensibility.utils.dataview.DataViewObjectsParser;
export class VisualSettings extends DataViewObjectsParser {
public dataPoint: dataPointSettings = new dataPointSettings();
}
export class dataPointSettings {
// Default color
public singularColor: string = "#67a244";
public singularFontSize: number = 12;
public singularFontColor: string = "#b1b1b1";
public dualColor: string = "#b1b1b1";
public dualFontSize: number = 12;
public dualFontColor: string = "#67a244";
public numColor: string = "#E066FF";
public numFontSize: number = 12;
public arrowColor: string = "#fff";
public lineColor: string = "#8b4513";
public pointColor: string = "#b1b1b1";
public pointBackGroupColor: string = "#ffffff";
// Show all
public showAllDataPoints: boolean = true;
// Fill
public fill: string = "";
// Color saturation
public fillRule: string = "";
// Text Size
public fontSize: number = 12;
}
}
4.test.d.ts
declare var d3: any;
5.tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES5",
"sourceMap": true,
"out": "./.tmp/build/visual.js"
},
"files": [
".api/v1.9.0/PowerBI-visuals.d.ts",
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts",
"src/settings.ts",
"src/visual.ts",
"src/test.d.ts"
]
}
6.效果圖