В настоящее время я работаю над своей следующей книгой для Three.js, и одна из глав посвящена визуализации открытых данных. Когда я искал данные, которые мог бы использовать, я наткнулся на набор данных из NOAA. Через этот сайт вы можете скачать набор ежемесячных отчетов об осадках для всего мира в сеточном формате. Поэтому я скачал их и начал играть с данными, чтобы посмотреть, как они выглядят и как их можно использовать. В этой статье я не собираюсь показывать вам результат на основе three.js, но я дам вам краткий обзор того, как добраться до формата, который я изначально использовал для целей отладки:
На этом изображении вы можете увидеть месячные осадки для всего мира в логарифмическом масштабе за июль 2012 года. Я также создал простой сайт, который показывает это и анимацию в действии.
Итак, что вам нужно сделать, чтобы конвертировать набор, который вы можете загрузить с сайта NOAA, во что-то визуальное.
- Загрузите и конвертируйте формат NetCDF.
- Загрузите полученный CSV-файл
- Обработка данных CSV в мировой сетке
- Анимировать переходы между месяцами
- В качестве бонуса: также создайте легенду, чтобы показать, что цвет означает, что
Однако сначала нам нужно получить данные.
Скачать и конвертировать формат NetCDF
Первое, что нам нужно сделать, это получить данные. Я использовал следующую ссылку: где вы можете определить диапазон данных, которые вы хотите загрузить. В этом примере я использовал диапазон с января 2012 года по декабрь 2012 года и выбрал опцию для создания подмножества без построения графика.
Однако формат, в котором он загружается, не может напрямую использоваться в качестве входных данных для нашей визуализации на основе холста HTML5. Вы можете использовать ncdump-json для создания файла JSON, но вам все еще нужно уметь его интерпретировать, поэтому я выбрал альтернативный способ. Я только что написал простую Java-программу для преобразования формата NetCDF в простой CSV-файл.
Я использовал следующие maven-зависимости:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
<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:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
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); } } }} |
Я не буду вдаваться в подробности того, что происходит, но этот фрагмент кода приводит к нескольким файлам, по одному на каждый месяц и один, содержащий среднее значение.
Каждый месяц отображается в следующем формате
|
1
2
3
4
5
6
7
|
...0,65,78,32.650,65,79,35.090,65,80,31.140,65,81,42.70,65,82,49.57... |
Соответственно значения означают: месяц, широту, долготу и количество осадков. Для среднего это выглядит почти так же, за исключением того, что первая запись опущена.
|
1
2
3
4
5
|
...59,94,59.87416559,95,65.95499459,96,57.805836... |
Теперь, когда у нас есть данные в удобном для использования формате, мы можем использовать их для создания визуализаций.
Загрузите полученный CSV-файл
Чтобы загрузить файл, мы просто используем простой XMLHttpRequest, например:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
// 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(); |
Переменная ords теперь содержит все координаты, а для каждой координаты — значение, которое нужно показать. Преобразовать это в холст на самом деле очень легко.
Обработка данных CSV в мировой сетке
В обратном вызове из XMLHttpRequest мы проверяем, получили ли мы данные, и конвертируем их в набор координат. Единственное, что нам нужно сделать, это преобразовать эти координаты в визуализацию на холсте.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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 .
|
1
|
var scale = chroma.scale(['red' , 'yellow', 'green', 'blue']).domain([1,1700], 100, 'log'); |
Этот вызов создает цветовую шкалу от красного до желтого, от зеленого до синего. Значения находятся в диапазоне от 1 до 1700, делятся на 100 шагов и используют логарифмическую шкалу. Это приводит к следующему изображению (на этот раз для осадков в январе 2012 года:
Поскольку у нас есть цифры за все месяцы, теперь мы можем легко создать простую анимацию.
Анимировать переходы между месяцами
Для анимации мы собираемся создать что-то вроде показанного в следующем фильме, где мы медленно переключаемся между различными месяцами:
Создание этой анимации может быть сделано довольно просто, просто показывая изображения друг над другом и изменяя непрозрачность. Поэтому сначала настройте несколько CSS, которые скрывают большинство изображений и размещают их все друг на друге.
|
01
02
03
04
05
06
07
08
09
10
11
|
#cf { position:relative; margin:0 auto; height: 700px; } #cf img { position:absolute; left:0; width: 1600px; } |
Теперь мы можем просто добавить изображения и использовать класс ‘bottom’, чтобы показать только первое изображение:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
<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, чтобы связать все вместе:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
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 для настройки переходов между изображениями.
В качестве бонуса: также создайте легенду, чтобы показать, что цвет означает, что
В анимации вы можете увидеть легенду внизу. Эта легенда была создана как простой холст, сохраненный как изображение. Для полноты кода, код для этого показан здесь:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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); |
Здесь мы просто используем масштаб, который мы видели проще, и проходим домены, чтобы создать цветную легенду.

