目录javascript
示例代码托管在:http://www.github.com/dashnowords/blogshtml
博客园地址:《大史住在大前端》原创博文目录前端
华为云社区地址:【你要的前端打怪升级指南】java
使用原生canvasAPI
绘制散点图。(截图以及数据来自于百度Echarts官方示例库【查看示例连接】)。git
学习过折线图的绘制后,若是数据点只有坐标数据,则经过基本的坐标转换在对应的点上绘制出散点并不难实现。而在气泡图中,当咱们直接将百度Echarts
示例中的数据拿来通过必定的线性缩小后做为半径直接绘制散点时,就会出现一些问题,数据集的范围跨度较大,致使大部分点呈现后都很是小,这个时候就须要使用某种方法从真实数据值映射到散点圆半径进行映射,来缩小它们之间的差别,不然一旦数据集中有一个偏离度较大的点,就会形成其余点所对应的散点半径都很大或者都很小,对数据呈现来讲是不可取的。例如在下面的示例中,当使用几种不一样的映射方法来处理数据后,能够看到绘制的散点图是不同的。github
//求散点半径时所使用的公式 //1.直接数值 r = value * 5 / 100000000; //2.求对数 r = Math.log(value); //3.求指数 r = Math.pow(value,0.4) / 100;
所绘制出的散点图以下所示:算法
坐标映射
的实现思路其实并不算复杂,它的概念能够参考算法的时间复杂度来进行理解,挑选一个增加更快的映射函数来区分相近的点,或者挑选一个增加更慢的映射函数来减少大跨度数据之间的差别,在数据可视化中是很是实用的技巧。本文示例中的效果是笔者本身手动调的,若是要实现根据数据集自动挑选适当的映射函数,还须要设计一些计算方法,感兴趣的读者能够自行研究。canvas
气泡散点图绘制示例代码(坐标轴的绘制过程在前述博文中已经出现过不少次,故再也不赘述,有须要的小伙伴能够直接翻看这个系列以前的博文或者查看本篇的demo):浏览器
/*数据点来自于百度Echarts官方示例库,每一个数值分别表示[横坐标,纵坐标,数值,国家,年份] *[28604,77,17096869,'Australia',1990] */ /** * 绘制数据 */ function drawData(options) { let data = options.data;//获取数据集 let xLength = (options.chartZone[2] - options.chartZone[0]); let yLength = (options.chartZone[3] - options.chartZone[1]); let gap = xLength / options.xAxisLabel.length; //遍历两个年份 for(let i = 0; i < data.length ;i++){ let x,y,r,c; context.fillStyle = options.colorPool[i];//从颜色池中选取颜色 context.globalAlpha = 0.8;//为避免点覆盖,采起半透明绘制 //遍历各个数据点 for(let j = 0; j < data[i].length ; j++){ //计算坐标 x = options.chartZone[0] + xLength * data[i][j][0] / 70000; y = options.chartZone[3] - yLength * (data[i][j][1] - 55) / (85 - 55); //直接数值 r = data[i][j][2] * 5 / 100000000; //求对数 r = Math.log(data[i][j][2]); //开根号 r = Math.pow(data[i][j][2],0.4) / 100; //绘制散点 context.beginPath(); context.arc(x, y , r , 0 , 2*Math.PI,false); context.fill(); context.closePath(); } } }
浏览器中可查看效果:缓存
在散点图上实现hover交互效果的基本算法以下:
canvas
元素上监听鼠标移动事件,将鼠标坐标转换为canvas坐标系的坐标值。resetHover( )
方法清除以前的hover状态。drawImage( )
方法将对应区域恢复到hover前的状态。hover效果的关键代码以下,完整示例代码请在demo中获取,或访问【个人github仓库】
/*简单hover效果*/ canvas.onmousemove = function (event) { //转换鼠标坐标为相对canvas let pos = { x: event.clientX - rect.left, y: event.clientY - rect.top } //获取当前hover点坐标 let hoverPoint = checkHover(options, pos); /** * 若是当前有聚焦点 */ if (hoverPoint) { //若是当前点和上一次记录的hover点是不一样的点,则先调一次reset方法,而后把hover点更改成当前的点 let samePoint = options.hoverData === hoverPoint ? true : false; if (!samePoint) { resetHover(); options.hoverData = hoverPoint; } //绘制当前点的hover状态 paintHover(); } else{ //第一次尝试手动恢复 // resetHover(); //使用离屏canvas恢复 resetHoverWithOffScreen(); } } /*检测是否hover在散点之上*/ function checkHover(options,pos) { let data = options.paintingData; let found = false; for(let i = 0; i < data.length; i++){ found = false; for(let j = 0; j < data[i].length; j++){ if (Math.sqrt(Math.pow(pos.x - data[i][j].x , 2) + Math.pow(pos.y - data[i][j].y , 2)) < data[i][j].r) { found = data[i][j]; break; } } if (found) break; } return found; } /*绘制hover状态*/ function paintHover() { let {x,y,r,c} = options.hoverData; let step = 0.5; context.globalAlpha = 1; context.fillStyle = c; //逐帧增长hover点的绘图半径,从新绘制hover状态的散点 for(let i = 0 ; i < 30; i++){ context.beginPath(); context.arc(x,y,r + i * step, 0 , 2*Math.PI,false); context.fill(); context.closePath(); } } /*首次尝试的取消高亮状态的函数*/ function resetHover() { if (!options.hoverData) return; let {x,y,r,c} = options.hoverData; let step = 0.5; context.globalAlpha = 1; for(let i = 29; i>0; i--){ context.save(); //绘制外圆范围 context.beginPath(); context.arc(x,y,r + 30 * step, 0 , 2*Math.PI,false); context.closePath(); //设置剪裁区域 context.clip(); //用全局背景色绘制剪裁区背景 context.globalAlpha = 1; context.fillStyle = options.globalGradient; context.fill(); //绘制内圆 context.beginPath(); context.arc(x,y,r + i * step, 0 , 2*Math.PI,false); context.closePath(); context.fillStyle = c; context.globalAlpha = 0.8; //填充内圆 context.fill(); context.restore(); } options.hoverData = null; console.log('清除hover效果'); } //利用离屏canvas恢复hover前的状态 function resetHoverWithOffScreen() { if (!options.hoverData) return; let {x,y,r,c} = options.hoverData; let step = 0.5; context.globalAlpha = 1; for(let i = 29; i>0; i--){ context.save(); //将hover状态下数据点圆所在的正方形范围恢复为hover前的状态 context.drawImage(canvas2, x - r - 30 * step, y - r - 30 * step , 2 * (r + 30 * step),2*(r + 30 * step),x - r - 30 * step, y - r - 30 * step , 2*(r + 30 * step),2*(r + 30 * step)); //绘制内圆 context.beginPath(); context.arc(x,y,r + i * step, 0 , 2*Math.PI,false); context.closePath(); context.fillStyle = c; context.globalAlpha = 0.8; //填充内圆 context.fill(); context.restore(); } options.hoverData = null; console.log('清除hover效果'); }
为了简化代码,demo中的一些绘图数据并无参数化,而是采起直接写死的形式放在代码里,尤为是逐帧绘图的代码,通常开发中此处都会配合动画来进行实现。
为了重置某个数据点的hover状态,笔者最初的实现思路是在每一帧中,使用context.clip( )
方法裁切出绘图区域,先用全局背景绘制出背景图,缩小数据点半径,而后再绘制数据点,直到半径缩小至hover前的值。但在实现后发现这种方式存在一个问题,那就是数据点之间出现重叠时,若是只是简单地背景重绘,就会将部分重叠区域清除掉,形成其余数据点没法复原,以下图所示:
因此最终采用离屏canvas的方法,将初次绘制后的数据点先暂存下来,而后在清除hover状态时,使用context.drawImage( )
方法将有关区域的数据复制粘贴过来,以替代原来的使用背景图填充该区域的作法,这样就能够在数据点之间有重叠时重现hover前的状态。