前言:echarts本身是有气泡图的,官方气泡图的特点是每个气泡的位置是基于坐标轴进行定位,如图1和2所示。但是本文所介绍的气泡图并不是官方所介绍的气泡图,而是一类区别于官方的图表类型,这种图表类型通常采用d3.js插件实现,如图3所示。从图1、图2、图3可以看出两种气泡图的展示,各有其特点。图1和图2,虽然是基于坐标轴,但气泡图之间容易叠加在一起。图3所示的气泡图则没有这个缺点,气泡之间互相相邻,彼此之间各不侵犯。众多周知,d3.js主要是svg技术实现的图表插件,而Echarts则主要是基于canvas画布技术实现的。两者之间虽然互相没有关联,但实现的算法确是可以参考和借鉴的。
实现思路:通过研究d3.js气泡图的代码构建过程,我发现其实现的核心主要在于计算每一个气泡的x、y、r,这三个跟气泡生成相关的属性值,只要把这三个属性值求出来了,那么问题自然就可以解决了。然后采用Echarts插件配置项中的graphic接口,添加circle和text类型元素,就可以渲染成一个个气泡。
具体实现:
(1)求解每一个气泡的x、y、r属性。这个工作很麻烦,涉及的算法,也挺复杂的。通过研究d3.js,我发现其求解每一个气泡的属性代码就有500多行。主要是思路是,首先构建一个所有气泡的根气泡(这个根气泡会包含所有的子气泡,但不会在页面上渲染出来,它主要作用是定位和布局),接着求解每一个气泡的x、y、r的值,特别指出的是每一个气泡的半径r,是通过其值得开根号求出的,最后根据根气泡的信息和渲染区域大小再判断是否要缩放气泡(即缩放x、y、半径r)。具体的求解代码如下所示,其中有两个重要的函数echarts.util.pack和echarts.util.hierarchy,这两个接口是求解气泡属性的关键,具体的实现源码,我已从d3.js插件里面将其独立出来了。如果大家想了解,可以到此处下载。压缩出来的文件中,bubbleUtil.js有详细源码。下载地址:http://download.csdn.net/download/mulumeng981/10023337
/**
* 获取气泡图的根气泡和各气泡的属性:颜色、坐标位置(x,y)、半径(r)
* @param {Function} data: 初始化数据
* @param {Function} pack: 布局函数
* @return {Array} 气泡信息集合
*/
function processBubbleData(data, container) {
var width = container.style.width.replace('px', '');
var height = container.style.height.replace('px', '');
//初始化气泡图显示的布局(主要是宽度和高度)
var pack = echarts.util.pack()
.size([width, height])
.padding(1);
var index = 0;
var color = ["#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5",
"#8c564b", "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"];
var root = echarts.util.hierarchy({children: data})
.sum(function(d) { return d.value; })
.each(function(d){
d.color = color[index % 20];
index++;
return d.color;
});
return pack(root).leaves();
}
(2)使用Echarts插件渲染气泡图。
众所周知,Echarts插件的配置主要是option的配置,而气泡图的配置主要是option中graphic接口的配置。而本文在设计中将一个气泡和其相关的文本看成一个集合。如下所示是具体实现源码。
/**
* 绘制Echarts图表
* @param {Object} container: dom实例
*/
function drawChart(chart, bubbleData) {
var option = {
tooltip: {
show: true,
triggerOn: 'none',
formatter: function (params) {
return '<span style="border-radius: 6px;width: 12px;height: 12px;display: inline-block;background-color:' + params.color +'"></span>' + ' ' + params.data.name + ': ' + params.data.value;
}
},
graphic: getCircleOption(bubbleData, chart),
series: []
};
chart.setOption(option);
}
/**
* 获取气泡的graphic配置项
* @param {Array} processData: 气泡的属性信息集合
* @param {Object} chart: echarts实例
* @return {Array} 配置项集合
*/
function getCircleOption(processData, chart) {
return echarts.util.map(processData, function (item, dataIndex) {
return {
type: 'group',
id: 'circleGroup' + dataIndex,
bounding: 'raw',
color: item.color,
data: item.data,
children: getChildOption(item, dataIndex, chart),
onmousemove: function (event) {
showTooltip.call(this, event, chart);
},
onmouseout: function (event) {
hideTooltip.call(this, event, chart);
}
};
});
}
/**
* 得到各个气泡和文本节点配置信息
* @param {Object} item: 气泡的属性信息
* @dataIndex {Number} dataIndex: 气泡的序号
* @param {Object} chart: echarts实例
* @return {Array} children: 配置项集合
*/
function getChildOption(item, dataIndex, chart) {
var children = [];
children.push({
id: 'circle' + dataIndex,
type: 'circle',
shape: {
cx: item.x,
cy: item.y,
r: item.r
},
name: 'mainElem',
style: {
fill: item.color
},
eventData: {
id: 'circle' + dataIndex
},
onclick: bindClickEvent
});
children.push({
type: 'text',
style: {
fill: '#fff',
text: item.data.name,
x: item.x - item.r / 4,
y: item.y
},
value: item.value
});
return children;
/**
* 气泡绑定click事件
*/
function bindClickEvent() {
var circle = new echarts.graphic.Circle({
shape: this.shape,
style: {
stroke: 'green',
fill: null,
lineWidth: 2
}
});
this.parent.add(circle);
chart.addClickElem(circle);
}
}
(3)配置每一个气泡的提示框
Echarts是有提示框组件的,叫tooltip,显示和隐藏的时候调用的接口都是dispatchAction,只是传递的参数不一样而已。
**显示提示框。**
有下面两种使用方式。
1 指定在相对容器的位置处显示提示框,如果指定的位置无法显示则无效。
dispatchAction({
type: 'showTip',
// 屏幕上的 x 坐标
x: number,
// 屏幕上的 y 坐标
y: number,
// 本次显示 tooltip 的位置。只在本次 action 中生效。
// 缺省则使用 option 中定义的 tooltip 位置。
position: Array.<number>|string|Function
})
2 指定数据图形,根据 tooltip 的配置项显示提示框。
dispatchAction({
type: 'showTip',
// 系列的 index,在 tooltip 的 trigger 为 axis 的时候可选。
seriesIndex?: number,
// 数据的 index,如果不指定也可以通过 name 属性根据名称指定数据
dataIndex?: number,
// 可选,数据名称,在有 dataIndex 的时候忽略
name?: string,
// 本次显示 tooltip 的位置。只在本次 action 中生效。
// 缺省则使用 option 中定义的 tooltip 位置。
position: Array.<number>|string|Function,
})
**隐藏提示框。**
dispatchAction({
type: 'hideTip'
})
针对本文的情况,主要采用的是显示的第一个接口,由于本文没有采用配置项中的series属性,故采用显示框的第一种方法,如下代码所示。特别注意的是formatterParams参数的配置,一定要有,不然就无法显示出提示框。
/**
* 利用dispatchAction函数显示每个气泡信息
*@param {Object} event
*@param {Object} chart: echarts instance
*/
function showTooltip(event, chart) {
chart.dispatchAction({
type: 'showTip',
// 屏幕上的 x 坐标
x: event.offsetX,
// 屏幕上的 y 坐标
y: event.offsetY,
tooltip: {
formatterParams: {
color: this.color,
data: this.data
}
}
});
//显示气泡的边界条纹
this.childOfName('mainElem').setStyle({
stroke: 'green',
lineWidth: 2
});
}
/**
* 隐藏提示框
*@param {Object} event
*@param {Object} chart: echarts instance
*/
function hideTooltip(event, chart) {
chart.dispatchAction({
type: 'hideTip'
});
this.childOfName('mainElem').setStyle({
stroke: null
});
}
(4)绑定有利于用户操作的事件,如点击气泡图,气泡边缘变绿色等。
/**
* 图表添加事件处理函数
* @param {Object} chart: echarts实例
*/
function bindChartEvent(chart) {
//点击气泡后的回调
chart.addClickElem = function (elem) {
if (this.currentElem !== elem && this.currentElem != null) {
this.currentElem.parent.remove(this.currentElem);
}
this.currentElem = elem;
};
//点击canvas中非气泡部分
chart._zr.on('click', function (params) {
if(params.target == null) {
if (chart.currentElem != null) {
chart.currentElem.parent.remove(chart.currentElem);
chart.currentElem = null;
}
}
});
//点击页面中非canvas部分
document.onclick = function (event) {
if (event.toElement.tagName != 'CANVAS') {
if (chart.currentElem != null) {
chart.currentElem.parent.remove(chart.currentElem);
chart.currentElem = null;
}
}
};
}
总结: 以上就是气泡图实现的主要代码,难点主要是气泡属性信息的计算,这一块主要采用的是d3.js的算法,本人主要是作为一个代码的搬运工,将其从d3.js中独立出来,并嵌入到Echarts中。详细的demo,大家可下载此处:http://download.csdn.net/download/mulumeng981/10023337,本人已将其封装成有一个插件,只要按要求传递参数,即可生成气泡图。如图4所示。
require(['bubbleUtil'],function(bubbleUtil){
var data = [
{"name":"黄瓜", "value":12345},
{"name":"猕猴桃", "value":7345},
{"name":"红提", "value":1345},
{"name":"枣子", "value":2345},
{"name":"火龙果", "value":345},
{"name":"葡萄", "value":14534},
{"name":"橘子", "value":9435} ,
{"name":"柚子", "value":134} ,
{"name":"桃子", "value":6346} ,
{"name":"西红柿", "value":634} ,
{"name":"梨子", "value":6346} ,
{"name":"苹果", "value":3644} ,
{"name":"香蕉", "value":2346} ,
{"name":"小明", "value":3869} ,
{"name":"小米", "value":6534} ,
{"name":"大米", "value":45} ,
{"name":"玉米", "value":8345}
];
bubbleUtil(data, document.getElementById('container'));
});