前一篇谈及到了ECharts整合HT for Web的网络拓扑图应用,后来在ECharts的Demo中看到了有关空气质量的相关报表应用,就想将百度地图、ECharts和HT for Web三者结合起来也做一个类似空气质量报告的报表+拓扑图应用,于是有了下面的Demo:http://www.hightopo.com/demo/blog_baidu_20150928/ht-baidu.html

在这个Demo中,将GraphView拓扑图组件添加到百度地图组件中,覆盖在百度地图组件之上,并且在百度地图组件上和GraphView拓扑图组件上分别添加事件监听,相互同步经纬度和屏幕位置信息,从而来控制拓扑图上的组件位置固定在地图上,并在节点和节点之间的连线上加上了流动属性。右下角的图标框是采用HT for Web的Panel面板组件结合ECharts图表组件完成的。
接下来我们来看看具体的代码实现:
1. 百度地图是如何与HT for Web组件结合的;
1map = new BMap.Map("map"); 2var view = graphView.getView(); 3view.className = 'graphView'; 4var mapDiv = document.getElementById('map'); 5mapDiv.firstChild.firstChild.appendChild(view);
首先需要在body中存在id为map的div,再通过百度地图的api来创建一个map地图对象,然后创建GraphView拓扑图组件,并获取GraphView组件中的view,最后将view添加到id为map的div的第二代孩子节点中。这时候问题就来了,为什么要将view添加到map的第二代孩子节点中呢,当你审查元素时你会发现这个div是百度地图的遮罩层,将view添加到上面,会使view会是在地图的顶层可见,不会被地图所遮挡。
2. 百度地图和GraphView的事件监听;
1map.addEventListener('moveend', function(e){ 2 resetPosition(); 3}); 4map.addEventListener('dragend', function(e){ 5 resetPosition(); 6}); 7map.addEventListener('zoomend', function(e){ 8 resetPosition(); 9}); 10 11graphView.handleScroll = function(){}; 12graphView.handlePinch = function(){}; 13 14function resetPosition(e){ 15 graphView.tx(0); 16 graphView.ty(0); 17 dataModel.each(function(data){ 18 var lonLat, position; 19 if(data instanceof ht.HtmlNode){ 20 if(data.getId() != 'chartTotal') { 21 position = data.getHost().getPosition(); 22 position = {x: position.x + 168, y: position.y + 158}; 23 data.setPosition(position.x, position.y); 24 } 25 } else if(data instanceof ht.Node){ 26 lonLat = data.lonLat; 27 position = map.pointToPixel(lonLat); 28 data.setPosition(position.x,position.y); 29 } 30 }); 31}
首先监听map的三个事件:moveend、 dragend、 zoomend,这三个事件做了同一件事--修改DataModel中所有data的position属性,让其在屏幕上的坐标与地图同步,然后将GraphView的Scroll和Pinch两个事件的执行函数设置为空函数,就是当监听到Scroll或者Pinch事件时不做任何的处理,将这两个事件交给map来处理。
在resetPosition函数中,做的事情很简单:遍历DataModel中的data,根据它们各自在地图上的经纬度来换算成屏幕坐标,并将坐标设置到相应的data中,从而达到GraphView中的节点能够固定在地图上的效果。


3. 创建右下角的图表组件:
1ht.Chart = function(option){ 2 var self = this, 3 view = self._view = document.createElement('div'); 4 view.style.position = 'absolute'; 5 view.style.setProperty('box-sizing', 'border-box', null); 6 self._option = option; 7 self._chart = echarts.init(self.getView()); 8 if(option) 9 self._chart.setOption(option); 10 self._FIRST = true; 11}; 12ht.Default.def('ht.Chart', Object, { 13 ms_v: 1, 14 ms_fire: 1, 15 ms_ac: ['chart', 'option', 'isFirst', 'view'], 16 validateImpl: function(){ 17 var self = this, 18 chart = self._chart; 19 chart.resize(); 20 if(self._FIRST){ 21 self._FIRST = false; 22 chart.restore(); 23 } 24 }, 25 setSize: function(w, h){ 26 var view = this._view; 27 view.style.width = w + 'px'; 28 view.style.height = h + 'px'; 29 } 30}); 31 32function createPanel(title, width, height){ 33 chart = new ht.Chart(option); 34 var c = chart.getChart(); 35 c.on(echarts.config.EVENT.LEGEND_SELECTED, legendSelectedFun); 36 var chartPanel = new ht.widget.Panel({ 37 title: title, 38 restoreToolTip: "Overview", 39 width: width, 40 contentHeight: height, 41 narrowWhenCollapse: true, 42 content: chart, 43 expanded: true 44 }); 45 chartPanel.setPositionRelativeTo("rightBottom"); 46 chartPanel.setPosition(0, 0); 47 chartPanel.getView().style.margin = '10px'; 48 49 document.body.appendChild(chartPanel.getView()); 50}
首先定义了ht.Chart类,并实现了validateImpl方法,方法中处理的逻辑也很简单:在每次方法执行的时候调用图表的reset方法重新设定图标的展示大小,如果该方法是第一次执行的话,就调用图表的restore方法将图表还原为最原始的状态。会有这样的设计是因为ht.Chart类中的view是动态创建的,在没有添加到dom之前将一直存在于内存中,在内存中因为并没有浏览器宽高信息,所以div的实际宽高均为0,因此chart将option内容绘制在宽高为0的div中,即使你resize了chart,如果没用重置图表状态的话,图表状态将无法在图表上正常显示。
接下来就是创建panel图表组件了,这是HT for Web的Panel组件的基本用法,其中content属性的值可以是HT for Web的任何组件或div元素,如果是HT fro Web组件的话,该组件必须实现了validateImpl方法,因为在panel的属性变化后将会调用content对应组件的validateImpl方法来重新布局组件内容。
4. ECharts和GraphView拓扑图组件的交互:
1legendSelectedFun = function(param) { 2 if(chart._legendSelect){ 3 delete chart._legendSelect; 4 return; 5 } 6 console.info(param); 7 var id = nodeMap[param.target], 8 dm = graphView.dm(), 9 data = dm.getDataById(id), 10 sm = dm.sm(), 11 selection = sm.getSelection(); 12 13 if(param.selected[param.target]) { 14 sm.appendSelection([data]); 15 if(selectionData.indexOf(param.target) < 0){ 16 selectionData.push(param.target); 17 } 18 }else { 19 sm.removeSelection([data]); 20 var index = selectionData.indexOf(param.target); 21 if(index >= 0){ 22 selectionData.splice(index, 1); 23 } 24 } 25 sm.setSelection(selection.toArray()); 26}; 27 28graphView.mi(function(e){ 29 console.info(e.kind, e.data); 30 var c = chart.getChart(), 31 legend = c.component.legend, 32 selectedMap = legend.getSelectedMap(); 33 34 if(e.kind === 'endRectSelect'){ 35 chart._legendSelect = true; 36 for(var name in notes){ 37 legend.setSelected(name, false); 38 } 39 notes = {}; 40 graphView.dm().sm().each(function(data){ 41 var note = data.s('note'); 42 if(note) 43 notes[note] = 1; 44 }); 45 for(var name in notes){ 46 legend.setSelected(name, true); 47 } 48 } else if(e.kind === 'clickData'){ 49 chart._legendSelect = true; 50 var data = e.data; 51 if(data instanceof ht.Node){ 52 var note = data.s('note'); 53 54 if(note){ 55 var selected = legend.isSelected(note); 56 if(selected){ 57 graphView.dm().sm().removeSelection([data]); 58 } 59 legend.setSelected(note, !selected); 60 } 61 } 62 } 63});
legendSelectedFun函数是EChart图表的legend插件选中事件监听,其中处理的逻辑是:当legend插件中的某个节点被选中了,也选中在GraphView拓扑图中对应的节点,当取消选中是,也取消选中GraphView拓扑图中对应的节点。
在GraphView中添加交互监听,如果在GraphView中做了框选操作,在框选结束后,将原本legend插件上被选中的节点取消选中,然后再获取被选中节点,并在legend插件上选中对应节点;当GraphView上的节点被选中,则根据legend插件中对应节点选中情况来决定legend插件中的节点和graphView上的节点是否选中。
在GraphView交互中,我往chart实例中添加了_legendSelect变量,该变量的设定是为了阻止在GraphView交互中修改legend插件的节点属性后回调legendSelectedFun回调函数做修改GraphView中节点属性操作。

今天就写到这吧,希望这篇文章能够帮到那些有地图、拓扑图、图表相结合需求的朋友,在设计上可能想法还不够成熟,希望大家不吝赐教。