highcharts不仅可以绘制官网上面指定的报表,同时,还可以利用其封装好的绘图库来绘制自定义图形。
有朋友在工作中刚好有这样的需求,当时看了两个类似的js绘图库:
highcharts重在报表,但是同时提供了封装好的绘图库,屏蔽了浏览器间差异(IE8及以下不支持SVG,使用VML绘图)
Raphaël仅仅提供了各式各样的绘图的API,也非常的好用。
仅仅实现当前功能的话,Raphaël应该是首选,但是,我们做web应用的时候,项目中难免用到其他的报表样式,所以权衡之下,还是选择highcharts比较划算。
先把效果图贴上,比较好描述问题:
1. 首先,我们定义一组这样的树形结构,大家学过数据结构的,应该很容易使用类似链表的结构进行描述。
2. 关于怎么添加节点之类的就不多说了,主要说一下,画图需要知道绘制图形的位置,而这个位置需要根据节点来进行计算。
3. 计算位置的时候,采用递归的方式来进行计算,当前节点的位置始终位于,其子节点的的中间偏上的位置,所以需要一个获取子节点左边位置和一个获取右边位置的函数,此函数会递归调用。和子节点之间的高度则由一个常数来进行定义。
4. 绘制图形,通过第三步可以计算出每个节点对应的位置,那么就可以进行绘图了,首先是节点图形,这个相对简单。其次是父子节点之间的连线。每条线均使用3次贝泽尔曲线来绘制。需要提供3个点,共6个参数。刚好这6个参数可以由父节点的位置和子节点的位置通过组合得到。
具体代码如下所示:(也可以通过http://jsfiddle.net/lloydzhou/JY59J/5/ 在线查看)
1 var Node = { 2 height: 20, 3 width: 60, 4 padding: 30, 5 paddingTop: 50, 6 paddingText: 5, 7 paddingLeft: 20, 8 arrowLength: 4, 9 arrowWidth: 3, 10 create: function(textVal){ 11 return { 12 text: textVal, 13 childNodes: [], 14 parentNode: null, 15 index: null, 16 x: null, 17 y: null, 18 add: function(node) { 19 this.childNodes[node.index = this.childNodes.length] = node; 20 return node.parentNode = this; 21 }, 22 leftNode: function() { 23 return this.parentNode ? (this.index > 0 ? this.parentNode.childNodes[this.index - 1] : this.parentNode.leftNode()) : null; 24 }, 25 top: function() { 26 if (this.y) return this.y; 27 else return this.y = (this.parentNode ? this.parentNode.top() + Node.height + Node.padding : Node.paddingTop); 28 }, 29 left: function() { 30 if (this.x) { 31 return this.x; 32 }else{ 33 return this.x = this.leftNode() ? this.leftNode().left() + Node.padding + this.leftNode().childWidth() : Node.paddingLeft; 34 } 35 }, 36 childHeight: function() { 37 var h = 0;if (this.childNodes.length > 0) {for(var i=0;i<this.childNodes.length;i++) h = Math.max(h,this.childNodes[i].childHeight() + Node.padding);} 38 return (h === 0) ? Node.height : h ; 39 }, 40 childWidth: function() { 41 var w = 0;if (this.childNodes.length > 0) {for(var i=0;i<this.childNodes.length;i++) w = w + this.childNodes[i].childWidth() + Node.padding;} 42 return (w === 0) ? Node.width : w - Node.padding; 43 }, 44 toString: function() { 45 return this.text + \'[\' + this.left() + \',\' + this.top() + \']\' + (this.childNodes.length > 0 ? \':{\' + this.childNodes + \'}\' : \'\'); 46 }, 47 arrowX: function () { 48 return this.left() + this.childWidth() / 2 - (this.parentNode.left() + this.parentNode.childWidth() / 2 - Node.width / 2) - Node.width / 2; 49 }, 50 arrowY: function () { 51 return Node.padding - 2 * Node.paddingText; 52 }, 53 arrow: function(renderer, colors) { 54 var a = Node.arrowLength, b = Node.arrowWidth, x2 = this.arrowX(), y2 = this.arrowY(), 55 x = this.parentNode.left() + this.parentNode.childWidth() / 2 - Node.width / 2+Node.width/2 + Node.paddingText, 56 y = this.parentNode.top() + Node.height + 2 * Node.paddingText 57 renderer.path([\'M\', 0, 0, \'C\', 0, y2/1.8, x2, 0, x2, y2]).attr({\'stroke-width\': 2, stroke: colors[1]}).translate(x,y).add() 58 }, 59 drow: function(renderer, colors) { 60 var x = this.left() + this.childWidth() / 2 - Node.width / 2, y = this.top(); 61 renderer.label(this.text, x, y).attr({fill: colors[0], stroke: \'white\',\'stroke-width\': 3,padding: Node.paddingText, r: 2 * Node.paddingText, width: Node.width, height: Node.height}).css({color: \'white\', textAlign:\'center\',fontWeight: \'bold\',fontSize: \'10px\'}).add().shadow(true); 62 if (this.childNodes.length > 0) { 63 for(var i=0;i<this.childNodes.length;i++) { 64 this.childNodes[i].drow(renderer, colors); 65 this.childNodes[i].arrow(renderer, colors); 66 } 67 } 68 } 69 }; 70 } 71 }
1 var n = Node.create 2 3 var datas = n(1) 4 .add(n(2)) 5 .add(n(3).add(n(7)).add(n(8))) 6 .add(n(4)) 7 .add(n(5).add(n(6)).add(n(9)))
1 var chart = new Highcharts.Chart({ 2 chart: { 3 renderTo: \'container\', 4 events: {load: function () { 5 datas.drow(this.renderer, Highcharts.getOptions().colors); 6 }} 7 }, 8 title: {text: \'demo\'} 9 });