OpenLayers 6 實現(xiàn)百度echarts風格的“空氣質(zhì)量”散點圖
百度的echarts(現(xiàn)在被Apache收編了)所包含的可視化樣式非常的豐富多彩,是眾多可視化項目必選的框架之一。
除了直方圖等各種圖標之外,echarts也有一些基于地圖(當然是百度地圖)的可視化功能,還有大神將OpenLayers和echarts結(jié)合起來做成了現(xiàn)成的組件供調(diào)用。但是在實際使用中,我發(fā)現(xiàn)echarts地圖應用的交互體驗其實并不是很好,圖形化數(shù)據(jù)在地圖層之上像是“掛”上去的,在拖動地圖的時候,會出現(xiàn)錯位:
于是我萌生了一個自己實現(xiàn)這個散點圖動畫效果的想法。通過分析,最終大致上實現(xiàn)了這個散點圖(沒有做交互功能),并且性能還不錯(gif圖幀率有點低,實際還要流暢一些):
分析:
OpenLayers渲染點要素是很容易完成的,但是echarts這個動畫的效果不大容易做。
通過觀察,發(fā)現(xiàn)每個帶動畫效果的點向外擴散的圈只有三個,總共有5*3=15個圓形渲染的點要素,所以感覺OpenLayers的性能應該跟得上。
OpenLayers官方實例有一個類似的動畫效果,是利用render機制實現(xiàn)的,可以拿來借鑒。
實現(xiàn):
首先準備數(shù)據(jù),可以在echarts網(wǎng)站上拷貝出來,這個就不說了。
然后把基本的地圖搭出來,數(shù)據(jù)讀取出來并做一下初步的處理。
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個不同半徑和透明度的圓環(huán)組成,隨著時間動態(tài)改變半徑和透明度這兩個屬性,形成了“波動”的動畫。所以要實現(xiàn)這種效果,需要在同一個點位渲染3-4個這樣的圓環(huán),并通過render控制實現(xiàn)關(guān)鍵幀(每一次渲染確定的半徑和透明度圓環(huán)暫時叫做一個關(guān)鍵幀)的不斷變化,最終形成動畫效果。
接著又要祭出render大法了。
首先定義幾個全局變量,用于控制動畫:
var duration = 2000;
var n=3
var flashGeom=new Array(5*n);
每次動畫周期設(shè)置為2秒,然后擴散圓的數(shù)量為3個一組,聲明一個5*n大小的數(shù)組,準備用于存放渲染擴散圓的要素。雖然很明顯同一組的3個擴散圓是同一個要素,但是為了方便記錄關(guān)鍵幀每一輪的開始時間,每一個擴散圓都用一個要素來表示,通過要素的自定義屬性來記錄關(guān)鍵幀每一輪開始渲染的時間。
tileLayer.on('postrender', evt => {
var vc = getVectorContext(evt);
var frameState = evt.frameState;
poi.forEach((item, index) => {
vc.drawFeature(item, item.getStyle())
})
監(jiān)聽tileLayer的postrender事件,獲取VectorContex對象,獲取到當前幀的狀態(tài);然后迭代要素數(shù)組的元素,將數(shù)據(jù)中的靜態(tài)城市點渲染上去。
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的城市開始動態(tài)渲染過程:
首先克隆n個要素,作為擴散圓的點要素,然后設(shè)置每個擴散圓的一次循環(huán)(循環(huán)一次指擴散圓半徑從0向外擴散到消失)的開始時間,然后計算已逝時間;如果此時的已逝時間超過了單次循環(huán)的時間duration,則將循環(huán)起始時間更新,向后平移已逝時間的長度,同時已逝時間設(shè)置為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()
})
計算已逝比率,根據(jù)這個比率和要素的value,也就是污染值,計算得到圓環(huán)的大小。此處的參數(shù)都是可以調(diào)整的,怎樣美觀怎樣來。然后計算透明度,最后根據(jù)這個半徑和透明度制作樣式,并使用這個樣式將要素畫到canvas上。
最后的最后,顯式調(diào)用一下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機制多用于制作動畫,項目中常用到動畫的朋友有必要學習一下,便于制作一些深度定制的動畫效果。畢竟人家造好的輪子不一定適合自己。