OpenLayers 6 實(shí)現(xiàn)百度echarts風(fēng)格的“空氣質(zhì)量”散點(diǎn)圖

百度的echarts(現(xiàn)在被Apache收編了)所包含的可視化樣式非常的豐富多彩,是眾多可視化項(xiàng)目必選的框架之一。

除了直方圖等各種圖標(biāo)之外,echarts也有一些基于地圖(當(dāng)然是百度地圖)的可視化功能,還有大神將OpenLayers和echarts結(jié)合起來(lái)做成了現(xiàn)成的組件供調(diào)用。但是在實(shí)際使用中,我發(fā)現(xiàn)echarts地圖應(yīng)用的交互體驗(yàn)其實(shí)并不是很好,圖形化數(shù)據(jù)在地圖層之上像是“掛”上去的,在拖動(dòng)地圖的時(shí)候,會(huì)出現(xiàn)錯(cuò)位:


















于是我萌生了一個(gè)自己實(shí)現(xiàn)這個(gè)散點(diǎn)圖動(dòng)畫(huà)效果的想法。通過(guò)分析,最終大致上實(shí)現(xiàn)了這個(gè)散點(diǎn)圖(沒(méi)有做交互功能),并且性能還不錯(cuò)(gif圖幀率有點(diǎn)低,實(shí)際還要流暢一些):
在這里插入圖片描述





















在這里插入圖片描述






















分析:

OpenLayers渲染點(diǎn)要素是很容易完成的,但是echarts這個(gè)動(dòng)畫(huà)的效果不大容易做。
通過(guò)觀(guān)察,發(fā)現(xiàn)每個(gè)帶動(dòng)畫(huà)效果的點(diǎn)向外擴(kuò)散的圈只有三個(gè),總共有5*3=15個(gè)圓形渲染的點(diǎn)要素,所以感覺(jué)OpenLayers的性能應(yīng)該跟得上。
OpenLayers官方實(shí)例有一個(gè)類(lèi)似的動(dòng)畫(huà)效果,是利用render機(jī)制實(shí)現(xiàn)的,可以拿來(lái)借鑒。

實(shí)現(xiàn):

首先準(zhǔn)備數(shù)據(jù),可以在echarts網(wǎng)站上拷貝出來(lái),這個(gè)就不說(shuō)了。
在這里插入圖片描述































然后把基本的地圖搭出來(lái),數(shù)據(jù)讀取出來(lái)并做一下初步的處理。

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');
})

這個(gè)擴(kuò)散圓圈的動(dòng)畫(huà)不妨來(lái)分析一下:如圖所示,每一組動(dòng)畫(huà)都有3-4個(gè)不同半徑和透明度的圓環(huán)組成,隨著時(shí)間動(dòng)態(tài)改變半徑和透明度這兩個(gè)屬性,形成了“波動(dòng)”的動(dòng)畫(huà)。所以要實(shí)現(xiàn)這種效果,需要在同一個(gè)點(diǎn)位渲染3-4個(gè)這樣的圓環(huán),并通過(guò)render控制實(shí)現(xiàn)關(guān)鍵幀(每一次渲染確定的半徑和透明度圓環(huán)暫時(shí)叫做一個(gè)關(guān)鍵幀)的不斷變化,最終形成動(dòng)畫(huà)效果。
在這里插入圖片描述


















接著又要祭出render大法了。

首先定義幾個(gè)全局變量,用于控制動(dòng)畫(huà):

var duration = 2000;
var n=3
var flashGeom=new Array(5*n);

每次動(dòng)畫(huà)周期設(shè)置為2秒,然后擴(kuò)散圓的數(shù)量為3個(gè)一組,聲明一個(gè)5*n大小的數(shù)組,準(zhǔn)備用于存放渲染擴(kuò)散圓的要素。雖然很明顯同一組的3個(gè)擴(kuò)散圓是同一個(gè)要素,但是為了方便記錄關(guān)鍵幀每一輪的開(kāi)始時(shí)間,每一個(gè)擴(kuò)散圓都用一個(gè)要素來(lái)表示,通過(guò)要素的自定義屬性來(lái)記錄關(guān)鍵幀每一輪開(kāi)始渲染的時(shí)間。

tileLayer.on('postrender', evt => {
  var vc = getVectorContext(evt);
  var frameState = evt.frameState;
  poi.forEach((item, index) => {
    vc.drawFeature(item, item.getStyle())
  })

監(jiān)聽(tīng)tileLayer的postrender事件,獲取VectorContex對(duì)象,獲取到當(dāng)前幀的狀態(tài);然后迭代要素?cái)?shù)組的元素,將數(shù)據(jù)中的靜態(tài)城市點(diǎn)渲染上去。

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
      }

對(duì)Top5的城市開(kāi)始動(dòng)態(tài)渲染過(guò)程:

首先克隆n個(gè)要素,作為擴(kuò)散圓的點(diǎn)要素,然后設(shè)置每個(gè)擴(kuò)散圓的一次循環(huán)(循環(huán)一次指擴(kuò)散圓半徑從0向外擴(kuò)散到消失)的開(kāi)始時(shí)間,然后計(jì)算已逝時(shí)間;如果此時(shí)的已逝時(shí)間超過(guò)了單次循環(huán)的時(shí)間duration,則將循環(huán)起始時(shí)間更新,向后平移已逝時(shí)間的長(zhǎng)度,同時(shí)已逝時(shí)間設(shè)置為0。

接下來(lái)的事情就順理成章了:

      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ì)算已逝比率,根據(jù)這個(gè)比率和要素的value,也就是污染值,計(jì)算得到圓環(huán)的大小。此處的參數(shù)都是可以調(diào)整的,怎樣美觀(guān)怎樣來(lái)。然后計(jì)算透明度,最后根據(jù)這個(gè)半徑和透明度制作樣式,并使用這個(gè)樣式將要素畫(huà)到canvas上。

最后的最后,顯式調(diào)用一下render(),進(jìn)行下一幀的繪制。

完整代碼:

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機(jī)制多用于制作動(dòng)畫(huà),項(xiàng)目中常用到動(dòng)畫(huà)的朋友有必要學(xué)習(xí)一下,便于制作一些深度定制的動(dòng)畫(huà)效果。畢竟人家造好的輪子不一定適合自己。