百度的echarts(现在被Apache收编了)所包含的可视化样式非常的丰富多彩,是众多可视化项目必选的框架之一。
除了直方图等各种图标之外,echarts也有一些基于地图(当然是百度地图)的可视化功能,还有大神将OpenLayers和echarts结合起来做成了现成的组件供调用。但是在实际使用中,我发现echarts地图应用的交互体验其实并不是很好,图形化数据在地图层之上像是“挂”上去的,在拖动地图的时候,会出现错位:
于是我萌生了一个自己实现这个散点图动画效果的想法。通过分析,最终大致上实现了这个散点图(没有做交互功能),并且性能还不错(gif图帧率有点低,实际还要流畅一些):
首先准备数据,可以在echarts网站上拷贝出来,这个就不说了。
然后把基本的地图搭出来,数据读取出来并做一下初步的处理。
import { Map, View } from 'ol';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import Point from 'ol/geom/Point';
import Feature from 'ol/Feature';
import { fromLonLat } from 'ol/proj';
import { getVectorContext } from 'ol/render';
import {Style, Stroke, Fill, Circle as CircleStyle} from 'ol/style';
import data from './data/scatter.json'
import { easeOut } from 'ol/easing'
let tileLayer = new TileLayer({
source: new OSM()
})
let map = new Map({
target: 'map',
layers: [
tileLayer
],
view: new View({
center: [11936406.337013, 3786384.633134],
zoom: 5
})
});
var poi = []
data.data.forEach((item, index) => {
item.coord = data.coord[item.name]
poi.push(new Feature(new Point(fromLonLat(item.coord))))
poi[index].set('name', item.name)
poi[index].set('value', item.value)
var bdStyle = new Style({
image: new CircleStyle({
fill: new Fill({
color: [128, 0, 128]
}),
radius: item.value / 20
}),
})
poi[index].setStyle(bdStyle)
})
poi.sort(function (a, b) {
return b.get('value') - a.get('value');
})
这个扩散圆圈的动画不妨来分析一下:如图所示,每一组动画都有3-4个不同半径和透明度的圆环组成,随着时间动态改变半径和透明度这两个属性,形成了“波动”的动画。所以要实现这种效果,需要在同一个点位渲染3-4个这样的圆环,并通过render控制实现关键帧(每一次渲染确定的半径和透明度圆环暂时叫做一个关键帧)的不断变化,最终形成动画效果。
接着又要祭出render大法了。
首先定义几个全局变量,用于控制动画:
var duration = 2000;
var n=3
var flashGeom=new Array(5*n);
每次动画周期设置为2秒,然后扩散圆的数量为3个一组,声明一个5*n大小的数组,准备用于存放渲染扩散圆的要素。虽然很明显同一组的3个扩散圆是同一个要素,但是为了方便记录关键帧每一轮的开始时间,每一个扩散圆都用一个要素来表示,通过要素的自定义属性来记录关键帧每一轮开始渲染的时间。
tileLayer.on('postrender', evt => {
var vc = getVectorContext(evt);
var frameState = evt.frameState;
poi.forEach((item, index) => {
vc.drawFeature(item, item.getStyle())
})
监听tileLayer的postrender事件,获取VectorContex对象,获取到当前帧的状态;然后迭代要素数组的元素,将数据中的静态城市点渲染上去。
for (var i = 0; i < 5; i++) {
for (var j = 0; j < n; j++) {
if(flashGeom[j+i*n] ==undefined)flashGeom[j+i*n] = poi[i].clone()
if (flashGeom[j+i*n].get('start')==undefined) flashGeom[j+i*n].set('start',(new Date().getTime())+600*j) ;
var elapsed = frameState.time - flashGeom[j+i*n].get('start')
if(elapsed >= duration){
flashGeom[j+i*n].set('start',flashGeom[j+i*n].get('start')+duration);
elapsed=0
}
对Top5的城市开始动态渲染过程:
首先克隆n个要素,作为扩散圆的点要素,然后设置每个扩散圆的一次循环(循环一次指扩散圆半径从0向外扩散到消失)的开始时间,然后计算已逝时间;如果此时的已逝时间超过了单次循环的时间duration,则将循环起始时间更新,向后平移已逝时间的长度,同时已逝时间设置为0。
接下来的事情就顺理成章了:
var elapsedRatio = elapsed / duration ;
elapsedRatio = elapsedRatio > 0 ? elapsedRatio : 0
elapsedRatio= elapsedRatio > 1 ? elapsedRatio-1 : elapsedRatio;
var radius = easeOut(elapsedRatio) * flashGeom[j+i*n].get('value') / 7;
radius = radius > 0 ? radius : 0;
var opacity = easeOut(1-elapsedRatio*1.3);
var style = new Style({
image: new CircleStyle({
radius: radius,
stroke: new Stroke({
color: 'rgba(128, 0, 128, ' + opacity + ')',
width: 0.1 + opacity
})
})
});
vc.drawFeature(flashGeom[j+i*n],style);
}
}
map.render()
})
计算已逝比率,根据这个比率和要素的value,也就是污染值,计算得到圆环的大小。此处的参数都是可以调整的,怎样美观怎样来。然后计算透明度,最后根据这个半径和透明度制作样式,并使用这个样式将要素画到canvas上。
最后的最后,显式调用一下render(),进行下一帧的绘制。
完整代码:
import { Map, View } from 'ol';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import Point from 'ol/geom/Point';
import Feature from 'ol/Feature';
import { fromLonLat } from 'ol/proj';
import { getVectorContext } from 'ol/render';
import {Style, Stroke, Fill, Circle as CircleStyle} from 'ol/style';
import data from './data/scatter.json'
import { easeOut } from 'ol/easing'
let tileLayer = new TileLayer({
source: new OSM()
})
let map = new Map({
target: 'map',
layers: [
tileLayer
],
view: new View({
center: [11936406.337013, 3786384.633134],
zoom: 5
})
});
var poi = []
data.data.forEach((item, index) => {
item.coord = data.coord[item.name]
poi.push(new Feature(new Point(fromLonLat(item.coord))))
poi[index].set('name', item.name)
poi[index].set('value', item.value)
var bdStyle = new Style({
image: new CircleStyle({
fill: new Fill({
color: [128, 0, 128]
}),
radius: item.value / 20
}),
})
poi[index].setStyle(bdStyle)
})
poi.sort(function (a, b) {
return b.get('value') - a.get('value');
})
var duration = 2000;
var n=3
var flashGeom=new Array(5*n);
tileLayer.on('postrender', evt => {
var vc = getVectorContext(evt);
var frameState = evt.frameState;
poi.forEach((item, index) => {
vc.drawFeature(item, item.getStyle())
})
for (var i = 0; i < 5; i++) {
for (var j = 0; j < n; j++) {
if(flashGeom[j+i*n] ==undefined)flashGeom[j+i*n] = poi[i].clone()
if (flashGeom[j+i*n].get('start')==undefined) flashGeom[j+i*n].set('start',(new Date().getTime())+600*j) ;
var elapsed = frameState.time - flashGeom[j+i*n].get('start')
if(elapsed >= duration){
flashGeom[j+i*n].set('start',flashGeom[j+i*n].get('start')+duration);
elapsed=0
}
var elapsedRatio = elapsed / duration ;
elapsedRatio = elapsedRatio > 0 ? elapsedRatio : 0
elapsedRatio= elapsedRatio > 1 ? elapsedRatio-1 : elapsedRatio;
var radius = easeOut(elapsedRatio) * flashGeom[j+i*n].get('value') / 7;
radius = radius > 0 ? radius : 0;
var opacity = easeOut(1-elapsedRatio*1.3);
var style = new Style({
image: new CircleStyle({
radius: radius,
stroke: new Stroke({
color: 'rgba(128, 0, 128, ' + opacity + ')',
width: 0.1 + opacity
})
})
});
vc.drawFeature(flashGeom[j+i*n],style);
}
}
map.render()
})
render机制多用于制作动画,项目中常用到动画的朋友有必要学习一下,便于制作一些深度定制的动画效果。毕竟人家造好的轮子不一定适合自己。
我在企鹅家的课堂和CSDN学院都开通了《OpenLayers实例详解》课程,欢迎报名学习。搜索关键字OpenLayers就能看到。