数据可视化之风向图

您所在的位置:网站首页 风向f图 数据可视化之风向图

数据可视化之风向图

2023-03-21 12:08| 来源: 网络整理| 查看: 265

      很多人都见过风向图,直观形象,也是地图数据和现实数据在可视化上很好的结合。

0

 

       这是我见的第一个风向图,记得是2012年吧,当时觉得很有意思,作为一名技术人员,自然好奇它是如何做到的,是Canvas还是SVG?但当时没深究。最近正好有人(大哥)提到了这个,不妨深入了解,一探究竟。于是乎,发现原来还有这么多玩法,大同小异,比如说这个,来自earth.nullschool.net:

 

1

 

      当然还有来自度娘开源的echarts-x的:

2

 

      基本上,这三个效果图基本涵盖了目前风向图的技术点和功能点(我自己的看法,因为windyty是基于earth.nullschool写的,前者多了一个worker线程处理数据,而后者在github上开源)。不知道哪一个最对你的胃口?对我而言,图1简单易懂,可以快速掌握风向图的实现;图2是实时的全球风向数据,而且是二进制格式,是大数据传输的一个方案;图3则采用WebGL实时渲染,算是大数据渲染的一个方案,所以各有千秋。正好本文就结合这三个例子说一下其中处理好的地方,也是一个由易到难的过程。

原理

      乍看上去,多少会觉得无从下手。这是怎么做到的?其实吧,懂与不懂就是那一层纸,就看你愿不愿意戳破而已。我们先从数据说起。

      首先介绍一下向量场(Vector Field)的概念。在维基百科的解释是:在向量分析中,向量场是把空间中的每一点指派到一个向量的映射。物理学中的向量场有风场、引力场、电磁场、水流场等等。如图,下面是一个二维的向量场,每一个点都是一个向量。

 

3

 

      当然这是一个抽象的数学概念和表达,物理中的电磁场经常会用到它,在现实中其实也随处可见,比如下面这个有意思的磁场的向量表达,密集恐惧症的人请略过~ 

   

4

 

      同理,风场的抽象模型也是一个向量场:每一个点都有一个风速和方向,可以分解为在该点分别在XY方向上的向量(我们简化为XY两个方向,不考虑Z,所以可惜不能听《龙卷风》了),则该向量则代表该点X方向和Y方向的速度。

 

5

 

      如上图,是一个真实的风向图数据。简单来说,timestamp代表当前数据的采集时间,(x0,y0,x1,y1)分别是经纬度的范围,而grid是该向量场的行列数,field就是向量场中每一个点的速度值,如果是(0,0)则表示此点风平浪静。可能不同平台的风向图数据有一定差别,但都大同小异。

      向量场和数据格式,直觉上,我们可以知道,就是把这些向量拟合成平滑线,可以形成如下一个真实的风向。

 

6

 

      如何形成线,而且看上去全球范围内有总不能只有一阵风吧(让我想起了木星的大红斑,这场风在木星已经吹了至少200年从来没停过),这揭露了两个问题,1向量场是离散点,而线是平滑,这里面有一个插值问题;2更麻烦的是,这些线有好多好多连接的方式,都可以连接成线,有点类似等高线的算法,怎么连,看上去无从下手啊。

      这是我看完数据后,自己觉得要实现风向效果时觉得需要解决的问题,感觉好难啊。怀着这个疑问进入梦乡,第二天format了一下js脚本,本地调试后,发现我的问题是对的,可是思路是错的。不要一上来就考虑这么多因素,而是基于当前的状态来解决当前的问题,就好比一道非常复杂的代数问题,或许通过几何方式反而可以很简单的解决。

      不多废话了,尽管我觉得这些废话才是提高能力的最有价值的,解决问题不过是一个感悟过程的必然而已。好了,有了数据,看看“神诸葛”如何起风的吧。

      举个例子,给你一个围棋棋盘(向量场),每一个格子就是一个向量,你随手拿一个棋子,随手(随机)放在一个格子上,这就是风的起点。下一回合(下一帧或下一秒),你根据当前格子的向量值(X值和Y值)移动棋子,就是风在当前的风速下拖着常常的尾巴跳到下一个格子上的效果。这样,这个棋子会根据所在格子的向量值不停的移动,直到格子的向量值为零(风停)。

      也就是说只要给一个起点,我就能刮起一股风来。那给你5000个棋子(起点),你就能刮起5000股风了。当然可能两股气流重叠,这时可能不太符合物理规律了,因为我们的思路下是各吹各的,不过谁关心呢。于是,基于每一帧状态的管理,我们可以很简单的模拟出风向图的效果。很简单巧妙吧。

如何实现

      好了,理论上我们知道该怎么做了,看看如何代码实现。我们也整理一下这个流程,把它们模块化。

      今天就和围棋干上了,还是这个例子,首先呢就是数据,也就是棋盘和格子,也就是Vector和Vector Field这两个对象来方便数据的读取、管理等;其次,当然是棋子了,记录每一个棋子的生命周期,当前的位置,下一步的位置,也就是风上对应的每一个帧的位置信息,这个是Particle类来记录这些信息;最后,有了棋盘和棋子,还需要一个推手来落子,这里称作MotionDisplay把,负责管理每一回合(帧)下棋子对应棋盘的位置,这个类要做的事情很多:有多少个棋子、哪一个还收回、需要新增几个棋子(风粒子的管理),怎么在棋盘上放置(渲染);等等,最后还少了一个,就是时钟啊,每一回合可是要读秒的哦,也就是Animation。

      还是得上代码,不然显得不专业。下面先把上面提到的这些对象中一些关键的属性和方法说明一下,可以知道哪些关键的属性是由哪些类来管理,而一些关键的方法进行一个说明,大家可以先专注类和函数本身的内容,了解这个拼图的部分内容。最终会有一个初始化的函数来一个整体流程的介绍,这时大家会了解整个拼图的面貌。

      向量比较简单,就是X和Y两个分量,其他的比如长度,角度这些方法就不在此赘述:

var Vector = function(x, y) { this.x = x; this.y = y; }

      下面是向量场类读取JSON数据并解析:

VectorField.read= function(data, correctForSphere) { var field = []; var w = data.gridWidth; var h = data.gridHeight; for (var x = 0; x < w; x++) { field[x] = []; for (var y = 0; y < h; y++) { var vx = data.field[i++]; var vy = data.field[i++]; var v = new Vector(vx,vy); …… field[x][y] = v; } } var result = newVectorField(field,data.x0,data.y0,data.x1,data.y1); return result; };

      如此,向量场已经布置完善,当然,对照JSON数据仔细看一下代码,有保存了经纬度的范围,行和列等信息,当然,该类中有其他几个函数没有在此列出,比如判断一个点是否在棋盘内,另外还有插值,因为每一个网格位置都是离散的,行和列都是整数,而现实中风的走向是连续的,可能在当前时刻的位置是分数,则需要根据临近的整数点的值插值获取当前点的一个近似值,这里采用的是双线性插值,取的周围四个点:

VectorField.prototype.bilinear= function(coord, a, b) { var na = Math.floor(a); var nb = Math.floor(b); var ma = Math.ceil(a); var mb = Math.ceil(b); var fa = a - na; var fb = b - nb; return this.field[na][nb][coord] * (1 - fa)* (1 - fb) + this.field[ma][nb][coord] * fa * (1 - fb) + this.field[na][mb][coord] * (1 - fa) * fb + this.field[ma][mb][coord] * fa * fb; };

      如上是向量和向量场的一些关键函数和属性。实现了读取数据,通过getValue函数获取任意一个位置(可以使小数)的速度的X和Y分量。

      下面就是棋子了,每一回合棋子的位置也就是风在每一帧的位置:

var Particle =function(x, y, age) { this.x = x; this.y = y; this.oldX = -1; this.oldY = -1; this.age = age; }

      如上,XY是当前的位置,而old则是上一帧的位置,age是它的生命周期,有的时候棋子会被吃,起风了也有风停的那一刻,都是通过age来记录它还能活多久(每一帧减一)。

      现在就开始介绍这只下棋的手了,看如何起风如何刮。

varMotionDisplay = function(canvas, imageCanvas, field, numParticles,opt_projection) { this.field = field; this.numParticles = numParticles; this.x0 = this.field.x0; this.x1 = this.field.x1; this.y0 = this.field.y0; this.y1 = this.field.y1; this.makeNewParticles(null, true); };

      这是它的构造函数,用来记录向量场的信息(范围和速度向量),同时numParticles表示粒子数,即同时有多少条风线在地图上显示。projection用于经纬度和向量场之间的映射换算。最后makeNewParticles则会构建numParticles个风,并随机赋给它们一个起点和生命周期,代码如下:

MotionDisplay.prototype.makeNewParticles= function(animator) { this.particles = []; for (var i = 0; i < this.numParticles;i++) { this.particles.push(this.makeParticle(animator)); } }; MotionDisplay.prototype.makeParticle= function(animator) { var a = Math.random(); var b = Math.random(); var x = a * this.x0 + (1 - a) *this.x1; var y = b * this.y0 + (1 - b) * this.y1; return new Particle(x,y,1 + 40 * Math.random()); };

     如上是一个简单的创建粒子的过程:随机在经纬度(x,y)创建一个能够存活1 + 40 *Math.random()帧的风,一共创建numParticles个这样的随机风。当然这里为了简单示意。并没有考虑随机数是否会超出范围等特殊情况。

     对象都构建完成了,那每一帧这只手如何主持大局呢?两件事情:Update和Render。

MotionDisplay.prototype.animate= function(animator) { this.moveThings(animator);//update this.draw(animator); // render }

      先看看如何更新:

MotionDisplay.prototype.moveThings= function(animator) { var speed = .01 * this.speedScale /animator.scale; for (var i = 0; i 0 &&this.field.inBounds(p.x, p.y)) { var a = this.field.getValue(p.x,p.y); p.x += speed * a.x; p.y += speed * a.y; p.age--; } else { this.particles[i] = this.makeParticle(animator); } } };

      如上,每一帧都根据速度*时间(帧)=距离来更新所有风粒子位置,同时检测如果age为负时,则重新创建一个来替换。

MotionDisplay.prototype.draw= function(animator) { var g = this.canvas.getContext('2d'); var w = this.canvas.width; var h = this.canvas.height; if (this.first) { g.fillStyle = this.background; this.first = false; } else { g.fillStyle = this.backgroundAlpha; } g.fillRect(dx, dy, w , h ); for (var i = 0; i 0.0) { vec4 vTex = texture2D(velocityTexture,p.xy); vec2 v = vTex.xy; v = (v - 0.5) * 2.0; p.z = length(v); p.xy += v * deltaTime / 50.0 *speedScaling; // Make the particle surface seamless p.xy = fract(p.xy); p.w -= deltaTime; } gl_FragColor = p;

      你会看到除了语法和JS的不同,里面的思路是一样的,首先从'velocityTexture'里面得到xy,该纹理就是向量场中的信息,每一个点则对应的是速度向量,而w则表示生命周期。经过计算后把值赋给了particleTexture

      然后呢,如果你看懂了,就是如梦初醒的时候了,原来每一帧中,particleTexture里面每一个点对应了当前风的位置,在particle.fragment中更新每一个点的位置,然后最终在场景中渲染出来。

voidmain() { vec4 p = texture2D(particleTexture,texcoord); gl_Position = worldViewProjection *vec4(p.xy * 2.0 - 1.0, 0.0, 1.0); }

      一个WebGL渲染风向图的大致思路,说的很不详细,关键是思路。技术的钻研,只要精益求精,总会有所收获。在这个过程中,我先想到风向图怎么实现的,等看明白了又想看看其他的脚本有何不同处,发现了数据实时性,也看到了百度的WebGL渲染的方式,可能也会有疏漏的地方,但总体感觉收获很大,面纱揭开后,也不再神秘。或者换句话说,风场,水流,重力场都可以按照这种方式来实现,只是计算公式上稍微调整一下就可以。

      看到这的人也不容易,希望对你也有所收获.



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3