初始化创建画布
我目前正在为Three.js编写下一本书,其中一章涉及可视化开放数据。 在寻找可以使用的数据时,我遇到了来自NOAA的一组数据。 通过此站点,您可以以网格格式下载一组全世界的每月降水报告。 因此,我下载了它们,然后开始处理数据以查看其外观和使用方式。 在本文中,我不会向您展示基于Three.js的结果,但是我将为您提供一个快速概述,如何获得最初用于调试目的的格式:
在此图像中,您可以看到2012年7月全球对月降水量的对数。我还创建了一个简单的站点来显示此动画以及正在运行的动画。
因此,您需要做什么才能将可以从NOAA站点下载的集转换为可视的内容。
- 下载并转换NetCDF格式。
- 加载生成的CSV文件
- 将CSV数据处理到世界网格中
- 动画化两个月之间的过渡
- 作为奖励:还可以创建图例以显示什么颜色表示什么
但是,首先,我们需要获取数据。
下载并转换NetCDF格式
我们需要做的第一件事就是获取数据。 我使用了以下链接:您可以在其中定义要下载的数据范围。 在此示例中,我使用了2012年1月至2012年12月的范围,并选择了创建子集而不绘制图的选项。
但是,下载它的格式不能直接用作我们基于HTML5画布的可视化的输入。 您可以使用ncdump-json创建一个JSON文件,但是仍然需要能够解释它,因此我选择了另一种方法。 我刚刚编写了一个简单的Java程序,将NetCDF格式转换为简单的CSV文件。
我使用了以下Maven依赖项:
<dependencies>
<dependency>
<groupId>edu.ucar</groupId>
<artifactId>netcdf</artifactId>
<version>4.2.20</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
</dependencies>
并使用以下一段Java代码:
public class NetCDFDump {
public static void main(String[] args) throws IOException, InvalidRangeException {
String year = "2012";
NetcdfFile nc = NetcdfFile.open("src/main/resources/X84.31.143.145.44.1.47.49.nc");
Variable precip = nc.findVariable("precip");
// use the shapes to create an array
int[] shapes = precip.getShape();
// month, lat, lon
float[][][] data = new float[shapes[0]][shapes[1]][shapes[2]];
// iterate over 12 (or 11) months
int[] pos = new int[3];
int[] shape = {1,1,1};
for (int i = 0 ; i < shapes[0] ; i++) {
pos[0]=i;
for (int lat = 0 ; lat < shapes[1]; lat++) {
pos[1] = lat;
for (int lon = 0 ; lon < shapes[2]; lon++) {
pos[2] = lon;
Array result = precip.read(pos, shape);
data[pos[0]][pos[1]][pos[2]] = result.getFloat(0);
}
}
}
// output data like this
// month, lat, lon, humidity
float[][] combined = new float[data[0].length][data[0][0].length];
for (int m = 0 ; m < data.length ; m++) {
File outputM = new File(year + "-out-" + m + ".csv");
for (int lat = 0 ; lat < data[m].length ; lat++) {
for (int lon = 0 ; lon < data[m][lat].length; lon++) {
float value = data[m][lat][lon];
if (value > -1000) {
combined[lat][lon]+=value;
} else {
combined[lat][lon]+=-1000;
}
// write the string for outputfile
StringBuffer bOut = new StringBuffer();
bOut.append(m);
bOut.append(',');
bOut.append(lat);
bOut.append(',');
bOut.append(lon);
bOut.append(',');
bOut.append(value);
bOut.append('\n');
// write to month file
FileUtils.write(outputM,bOut,true);
}
}
}
// now process the combined
File outputM = new File(year + "-gem.csv");
for (int i = 0; i < combined.length; i++) {
for (int j = 0; j < combined[0].length; j++) {
StringBuffer bOut = new StringBuffer();
bOut.append(i);
bOut.append(',');
bOut.append(j);
bOut.append(',');
bOut.append(combined[i][j]/data.length);
bOut.append('\n');
FileUtils.write(outputM, bOut, true);
}
}
}
}
我不会详细介绍正在发生的事情,但是这段代码会生成许多文件,每个文件一个月,其中一个包含平均值。
每月以以下格式显示
...
0,65,78,32.65
0,65,79,35.09
0,65,80,31.14
0,65,81,42.7
0,65,82,49.57
...
这些值分别表示:月份,纬度,经度和降水。 对于平均值,除了省略第一个条目外,它看起来几乎相同。
...
59,94,59.874165
59,95,65.954994
59,96,57.805836
...
现在,我们已经获得了易于使用的格式的数据,可以使用它来创建可视化。
加载生成的CSV文件
要加载文件,我们只使用一个简单的XMLHttpRequest,如下所示:
// create an XMLHttpRequest to get the data
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
var coords = CSVToArray(xmlhttp.responseText,",");
// and process each of the coordinates
...
}
}
// make the call and use the callback to process the result
xmlhttp.open("GET", "location/of/the/file", true);
xmlhttp.send();
现在,coords变量包含所有坐标,并为每个坐标显示值。 实际上,将其转换为画布非常容易。
将CSV数据处理到世界网格中
在XMLHttpRequest的回调中,我们检查是否已接收到数据并将其转换为一组坐标。 我们唯一需要做的就是将这些坐标转换为画布上的可视化图像。
var coords = CSVToArray(xmlhttp.responseText,",");
coords.forEach(function(point) {
var offset = 0;
if (point.length > 3) {
offset = 1;
}
if (parseFloat(point[2+offset]) >= 0) {
var lat = parseInt(point[0+offset]);
var lon = parseInt(point[1+offset]);
var value = parseFloat(point[2+offset]);
if (value > max) max = value;
// lat is from 0 to 180
// lon is from 0 to 360
var x = canvas.width/360*((lon)-180);
if (x<=0) {
x=canvas.width-(x*-1);
}
var y = canvas.height/180*lat;
if (value >= 0) {
context.beginPath();
context.rect(x,y,4,4);
context.fillStyle = scale(value).hex();
context.fill();
}
}
});
如您所见,非常简单的代码就是我们将位置取下来,将它们转换为画布上的X和Y坐标,并创建具有特定颜色的小方块。 为了生成颜色,我们使用Chroma.js比例尺。
var scale = chroma.scale(['red' , 'yellow', 'green', 'blue']).domain([1,1700], 100, 'log');
此调用创建从红色到黄色到绿色到蓝色的色标。 值的范围是1到1700,分为100步,并使用对数刻度。 这将产生以下图像(这次是2012年1月的降水:
由于我们拥有所有月份的数据,因此我们现在可以轻松创建简单的动画。
动画化两个月之间的过渡
对于动画,我们将创建类似于以下电影中所示的内容,其中我们在各个月份之间缓慢过渡:
只需将图像彼此叠加显示并更改不透明度,即可轻松创建此动画。 因此,首先设置一些css,它将大部分图像隐藏起来,然后将它们全部放在另一个顶部。
#cf {
position:relative;
margin:0 auto;
height: 700px;
}
#cf img {
position:absolute;
left:0;
width: 1600px;
}
现在我们可以添加图像,并使用“ bottom”类仅显示第一个图像:
<div id="cf">
<img id="img-1" class="top" src="./assets/images/2012-01-perc.png" />
<img id="img-2" class="bottom" src="./assets/images/2012-02-perc.png" />
<img id="img-3" class="bottom" src="./assets/images/2012-03-perc.png" />
<img id="img-4" class="bottom" src="./assets/images/2012-04-perc.png" />
<img id="img-5" class="bottom" src="./assets/images/2012-05-perc.png" />
<img id="img-6" class="bottom" src="./assets/images/2012-06-perc.png" />
<img id="img-7" class="bottom" src="./assets/images/2012-07-perc.png" />
<img id="img-8" class="bottom" src="./assets/images/2012-08-perc.png" />
<img id="img-9" class="bottom" src="./assets/images/2012-09-perc.png" />
<img id="img-10" class="bottom" src="./assets/images/2012-10-perc.png" />
<img id="img-11" class="bottom" src="./assets/images/2012-11-perc.png" />
<img id="img-12" class="bottom" src="./assets/images/2012-12-perc.png" />
</div>
现在,我们只需要一些JavaScript即可将所有内容捆绑在一起:
var month=[];
month[0]="January";
month[1]="February";
month[2]="March";
month[3]="April";
month[4]="May";
month[5]="June";
month[6]="July";
month[7]="August";
month[8]="September";
month[9]="October";
month[10]="November";
month[11]="December";
var allTweens;
init();
animate();
function init() {
// create a chain of tweens
allTweens = setupTweens(12);
allTweens[0].start();
}
function setupTweens(imageCount) {
var tweens = [];
for (var i = 0 ; i < imageCount ; i++) {
var tween = new TWEEN.Tween( { opac: 0, image: i, max: imageCount } )
.to( { opac: 100 }, 2500 )
.easing( TWEEN.Easing.Linear.None )
.onUpdate( function () {
// on update, lower the opacity of image i and update the opacity of
// image i+1;
var currentImage = document.getElementById('img-'+(this.image+1));
if (this.image == imageCount -1) {
var nextImage = document.getElementById('img-'+1);
} else {
var nextImage = document.getElementById('img-'+(this.image+2));
}
currentImage.style.opacity = 1- this.opac / 100;
nextImage.style.opacity = this.opac / 100;
} );
tween.onComplete(function() {
document.getElementById('title-2012').textContent = "Showing precipitation: " + month[this.image] + " " + 2012;
// Set the inner variable to 0.
this.opac = 0;
// we're done, restart
if (this.max-1 == this.image) {
allTweens[0].start();
}
});
// connect to each another
if (i > 0) {
tweens[i-1].chain(tween);
}
tweens.push(tween);
tweens[0].repeat();
}
return tweens;
}
function animate() {
requestAnimationFrame(animate);
TWEEN.update();
}
在这里,我们使用tween.js设置图像之间的过渡。
作为奖励:还可以创建图例以显示什么颜色表示什么
在动画中,您可以在底部看到图例。 此图例创建为简单的画布,另存为图像。 为了完整起见,此处显示执行此操作的代码:
var canvas = document.createElement("canvas");
canvas.width = 435;
canvas.height = 30;
var context = canvas.getContext('2d');
var domains = scale.domain();
document.body.appendChild(canvas);
// from 1 to 1700
for (var i = 0 ; i < domains.length ; i++) {
context.beginPath();
context.rect(10+i*4,0,4,20);
console.log(domains[i]);
context.fillStyle = scale(domains[i]).hex();
context.fill();
}
context.fillStyle = 'black';
context.fillText("0 mm", 0, 30);
context.fillText(Math.round(domains[25]) + " mm", 100, 30);
context.fillText(Math.round(domains[50]) + " mm", 200, 30);
context.fillText(Math.round(domains[75]) + " mm", 300, 30);
context.fillText("1700 mm", 390, 30);
在这里,我们只使用我们更容易看到的比例,并遍历各个域以创建彩色图例。
初始化创建画布