14 |
15 | HadCRUT3 Climate Data
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/demos/climate-sim.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 | HadCRUT3 Climate Data
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/demos/temp-anomaly.json:
--------------------------------------------------------------------------------
1 | {
2 | "1850": -0.431,
3 | "1851": -0.020,
4 | "1852": -0.320,
5 | "1853": -0.388,
6 | "1854": 0.065,
7 | "1855": -0.188,
8 | "1856": -0.352,
9 | "1857": -0.289,
10 | "1858": -0.375,
11 | "1859": -0.363,
12 | "1860": -0.326,
13 | "1861": -0.266,
14 | "1862": -0.591,
15 | "1863": -0.307,
16 | "1864": -0.639,
17 | "1865": -0.306,
18 | "1866": -0.358,
19 | "1867": -0.371,
20 | "1868": -0.291,
21 | "1869": -0.260,
22 | "1870": -0.266,
23 | "1871": -0.413,
24 | "1872": -0.164,
25 | "1873": -0.204,
26 | "1874": -0.343,
27 | "1875": -0.584,
28 | "1876": -0.245,
29 | "1877": -0.040,
30 | "1878": 0.109,
31 | "1879": -0.409,
32 | "1880": -0.169,
33 | "1881": -0.303,
34 | "1882": -0.138,
35 | "1883": -0.412,
36 | "1884": -0.541,
37 | "1885": -0.500,
38 | "1886": -0.423,
39 | "1887": -0.522,
40 | "1888": -0.488,
41 | "1889": -0.228,
42 | "1890": -0.467,
43 | "1891": -0.599,
44 | "1892": -0.603,
45 | "1893": -0.689,
46 | "1894": -0.551,
47 | "1895": -0.551,
48 | "1896": -0.380,
49 | "1897": -0.301,
50 | "1898": -0.405,
51 | "1899": -0.330,
52 | "1900": -0.176,
53 | "1901": -0.187,
54 | "1902": -0.352,
55 | "1903": -0.435,
56 | "1904": -0.570,
57 | "1905": -0.434,
58 | "1906": -0.224,
59 | "1907": -0.619,
60 | "1908": -0.493,
61 | "1909": -0.471,
62 | "1910": -0.346,
63 | "1911": -0.469,
64 | "1912": -0.392,
65 | "1913": -0.326,
66 | "1914": -0.063,
67 | "1915": -0.082,
68 | "1916": -0.370,
69 | "1917": -0.694,
70 | "1918": -0.489,
71 | "1919": -0.277,
72 | "1920": -0.306,
73 | "1921": -0.168,
74 | "1922": -0.261,
75 | "1923": -0.295,
76 | "1924": -0.370,
77 | "1925": -0.280,
78 | "1926": -0.046,
79 | "1927": -0.231,
80 | "1928": -0.164,
81 | "1929": -0.444,
82 | "1930": -0.137,
83 | "1931": -0.125,
84 | "1932": -0.056,
85 | "1933": -0.296,
86 | "1934": -0.071,
87 | "1935": -0.167,
88 | "1936": -0.111,
89 | "1937": -0.071,
90 | "1938": 0.109,
91 | "1939": -0.059,
92 | "1940": -0.030,
93 | "1941": -0.013,
94 | "1942": -0.028,
95 | "1943": -0.064,
96 | "1944": 0.073,
97 | "1945": -0.100,
98 | "1946": -0.078,
99 | "1947": -0.009,
100 | "1948": -0.059,
101 | "1949": -0.150,
102 | "1950": -0.318,
103 | "1951": -0.125,
104 | "1952": -0.037,
105 | "1953": 0.058,
106 | "1954": -0.186,
107 | "1955": -0.204,
108 | "1956": -0.433,
109 | "1957": -0.052,
110 | "1958": 0.081,
111 | "1959": -0.011,
112 | "1960": -0.100,
113 | "1961": 0.040,
114 | "1962": -0.000,
115 | "1963": 0.007,
116 | "1964": -0.285,
117 | "1965": -0.185,
118 | "1966": -0.116,
119 | "1967": -0.120,
120 | "1968": -0.210,
121 | "1969": -0.059,
122 | "1970": -0.022,
123 | "1971": -0.205,
124 | "1972": -0.176,
125 | "1973": 0.156,
126 | "1974": -0.306,
127 | "1975": -0.114,
128 | "1976": -0.368,
129 | "1977": 0.072,
130 | "1978": -0.046,
131 | "1979": 0.062,
132 | "1980": 0.141,
133 | "1981": 0.248,
134 | "1982": 0.021,
135 | "1983": 0.320,
136 | "1984": -0.058,
137 | "1985": -0.010,
138 | "1986": 0.117,
139 | "1987": 0.290,
140 | "1988": 0.342,
141 | "1989": 0.195,
142 | "1990": 0.428,
143 | "1991": 0.339,
144 | "1992": 0.103,
145 | "1993": 0.183,
146 | "1994": 0.326,
147 | "1995": 0.477,
148 | "1996": 0.215,
149 | "1997": 0.464,
150 | "1998": 0.821,
151 | "1999": 0.493,
152 | "2000": 0.363,
153 | "2001": 0.559,
154 | "2002": 0.666,
155 | "2003": 0.645,
156 | "2004": 0.622,
157 | "2005": 0.760,
158 | "2006": 0.674,
159 | "2007": 0.680,
160 | "2008": 0.527,
161 | "2009": 0.672
162 | }
163 |
--------------------------------------------------------------------------------
/demos/temp-anomaly.js:
--------------------------------------------------------------------------------
1 | var tempAnomaly =
2 | {
3 | "1850": -0.431,
4 | "1851": -0.020,
5 | "1852": -0.320,
6 | "1853": -0.388,
7 | "1854": 0.065,
8 | "1855": -0.188,
9 | "1856": -0.352,
10 | "1857": -0.289,
11 | "1858": -0.375,
12 | "1859": -0.363,
13 | "1860": -0.326,
14 | "1861": -0.266,
15 | "1862": -0.591,
16 | "1863": -0.307,
17 | "1864": -0.639,
18 | "1865": -0.306,
19 | "1866": -0.358,
20 | "1867": -0.371,
21 | "1868": -0.291,
22 | "1869": -0.260,
23 | "1870": -0.266,
24 | "1871": -0.413,
25 | "1872": -0.164,
26 | "1873": -0.204,
27 | "1874": -0.343,
28 | "1875": -0.584,
29 | "1876": -0.245,
30 | "1877": -0.040,
31 | "1878": 0.109,
32 | "1879": -0.409,
33 | "1880": -0.169,
34 | "1881": -0.303,
35 | "1882": -0.138,
36 | "1883": -0.412,
37 | "1884": -0.541,
38 | "1885": -0.500,
39 | "1886": -0.423,
40 | "1887": -0.522,
41 | "1888": -0.488,
42 | "1889": -0.228,
43 | "1890": -0.467,
44 | "1891": -0.599,
45 | "1892": -0.603,
46 | "1893": -0.689,
47 | "1894": -0.551,
48 | "1895": -0.551,
49 | "1896": -0.380,
50 | "1897": -0.301,
51 | "1898": -0.405,
52 | "1899": -0.330,
53 | "1900": -0.176,
54 | "1901": -0.187,
55 | "1902": -0.352,
56 | "1903": -0.435,
57 | "1904": -0.570,
58 | "1905": -0.434,
59 | "1906": -0.224,
60 | "1907": -0.619,
61 | "1908": -0.493,
62 | "1909": -0.471,
63 | "1910": -0.346,
64 | "1911": -0.469,
65 | "1912": -0.392,
66 | "1913": -0.326,
67 | "1914": -0.063,
68 | "1915": -0.082,
69 | "1916": -0.370,
70 | "1917": -0.694,
71 | "1918": -0.489,
72 | "1919": -0.277,
73 | "1920": -0.306,
74 | "1921": -0.168,
75 | "1922": -0.261,
76 | "1923": -0.295,
77 | "1924": -0.370,
78 | "1925": -0.280,
79 | "1926": -0.046,
80 | "1927": -0.231,
81 | "1928": -0.164,
82 | "1929": -0.444,
83 | "1930": -0.137,
84 | "1931": -0.125,
85 | "1932": -0.056,
86 | "1933": -0.296,
87 | "1934": -0.071,
88 | "1935": -0.167,
89 | "1936": -0.111,
90 | "1937": -0.071,
91 | "1938": 0.109,
92 | "1939": -0.059,
93 | "1940": -0.030,
94 | "1941": -0.013,
95 | "1942": -0.028,
96 | "1943": -0.064,
97 | "1944": 0.073,
98 | "1945": -0.100,
99 | "1946": -0.078,
100 | "1947": -0.009,
101 | "1948": -0.059,
102 | "1949": -0.150,
103 | "1950": -0.318,
104 | "1951": -0.125,
105 | "1952": -0.037,
106 | "1953": 0.058,
107 | "1954": -0.186,
108 | "1955": -0.204,
109 | "1956": -0.433,
110 | "1957": -0.052,
111 | "1958": 0.081,
112 | "1959": -0.011,
113 | "1960": -0.100,
114 | "1961": 0.040,
115 | "1962": -0.000,
116 | "1963": 0.007,
117 | "1964": -0.285,
118 | "1965": -0.185,
119 | "1966": -0.116,
120 | "1967": -0.120,
121 | "1968": -0.210,
122 | "1969": -0.059,
123 | "1970": -0.022,
124 | "1971": -0.205,
125 | "1972": -0.176,
126 | "1973": 0.156,
127 | "1974": -0.306,
128 | "1975": -0.114,
129 | "1976": -0.368,
130 | "1977": 0.072,
131 | "1978": -0.046,
132 | "1979": 0.062,
133 | "1980": 0.141,
134 | "1981": 0.248,
135 | "1982": 0.021,
136 | "1983": 0.320,
137 | "1984": -0.058,
138 | "1985": -0.010,
139 | "1986": 0.117,
140 | "1987": 0.290,
141 | "1988": 0.342,
142 | "1989": 0.195,
143 | "1990": 0.428,
144 | "1991": 0.339,
145 | "1992": 0.103,
146 | "1993": 0.183,
147 | "1994": 0.326,
148 | "1995": 0.477,
149 | "1996": 0.215,
150 | "1997": 0.464,
151 | "1998": 0.821,
152 | "1999": 0.493,
153 | "2000": 0.363,
154 | "2001": 0.559,
155 | "2002": 0.666,
156 | "2003": 0.645,
157 | "2004": 0.622,
158 | "2005": 0.760,
159 | "2006": 0.674,
160 | "2007": 0.680,
161 | "2008": 0.527,
162 | "2009": 0.672
163 | }
164 |
--------------------------------------------------------------------------------
/demos/climate-sim.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | climate-sim.css
4 | CSS styles for HADCrut3 Climate Data using MapboxGL.js
5 |
6 | AUTHOR: Teoman (Ted) Yavuzkurt
7 | www.github.com/teomandavid
8 | www.teomandavid.com
9 |
10 | */
11 |
12 | body {
13 | margin:0;
14 | padding:0;
15 | }
16 |
17 | #map {
18 | position:absolute;
19 | top:0;
20 | bottom: 80px;
21 | width:100%;
22 | }
23 |
24 | .m5{
25 | margin: 5px;
26 | }
27 |
28 | .m5-top{
29 | margin-top: 5px !important;
30 | }
31 |
32 | .m6-top{
33 | margin-top: 6px !important;
34 | }
35 |
36 | .m8-top{
37 | margin-top: 8px !important;
38 | }
39 |
40 | .m2{
41 | margin: 2px;
42 | }
43 |
44 |
45 | .mapboxgl-popup {
46 | max-width: 400px;
47 | font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
48 | }
49 |
50 | .mapboxgl-popup-content {
51 | pointer-events: none !important;
52 | text-align: center;
53 | padding: 5px;
54 | background-color: #55a;
55 | opacity: .75;
56 | }
57 |
58 | .mapboxgl-popup-content h4 {
59 | font-size: 120%;
60 | font-weight: bold;
61 | margin: 0px;
62 | }
63 |
64 | footer{
65 | height: 50px;
66 | width: 100%;
67 | display: block;
68 | background-color: #555;
69 | position: fixed;
70 | bottom: 0px;
71 | padding: 10px;
72 | font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
73 | border-top: 10px solid #080808;
74 | }
75 |
76 | footer div:first-child{
77 | min-width: 400px;
78 | max-width: 900px;
79 | margin: auto;
80 | padding-right: 35px;
81 | }
82 |
83 | .map-text{
84 | display: inline-block;
85 | width: 80px;
86 | font-size: 150%;
87 | margin: 2px;
88 | margin-top: 3px;
89 | }
90 |
91 | .map-text-title{
92 | display: inline-block;
93 | width: 80px;
94 | font-size: 150%;
95 | margin: 2px;
96 | font-weight: bold;
97 | text-align: right;
98 | }
99 |
100 | .mini{
101 | font-size: inherit;
102 | margin-left: 5px;
103 | margin-right: 0px;
104 | font-weight: bold;
105 | display: flex;
106 | }
107 |
108 | input[type="checkbox"]{
109 | margin-right: 10px;
110 | flex-shrink: 0;
111 | }
112 |
113 | select{
114 | margin-left: 5px;
115 | margin-right: 5px;
116 | display: inline-block;
117 | text-transform: capitalize;
118 | font-size: 120%;
119 | width: auto;
120 | min-width: 50px;
121 | }
122 |
123 | .map-controls{
124 | display: flex;
125 | flex-direction: row;
126 | }
127 |
128 | button{
129 | height: 20px;
130 | max-width: 75px;
131 | flex-basis: 1;
132 | overflow: hidden;
133 | }
134 |
135 | #map-slider{
136 | max-width: 600px;
137 | min-width: 100px;
138 | height: 15px;
139 | margin-left: 9px;
140 | flex-grow: 2.0;
141 | flex-basis: 0.2;
142 | }
143 |
144 | .gradient-container{
145 | max-width: 470px;
146 | min-width: 100px;
147 | display: inline-block;
148 | height: 22px;
149 | margin-left: 10px;
150 | margin-right: 10px;
151 | flex-grow: 0.5;
152 | display: flex;
153 | flex-direction: row;
154 | }
155 |
156 | .gradient-container span{
157 | display: flex;
158 | vertical-align: top;
159 | margin-top: 6px;
160 | flex-basis: 1;
161 | flex-shrink: 3;
162 | max-width: 30px;
163 | min-width: 10px;
164 | overflow: hidden;
165 | }
166 |
167 | .gradient{
168 | text-align: center;
169 | font-weight: bold;
170 | flex-grow: 1;
171 | overflow: hidden;
172 | }
173 |
174 | #toggle-anomaly{
175 | margin-right: 0px;
176 | }
177 |
178 | #author-info{
179 | position: fixed;
180 | right: 0px;
181 | bottom: 0px;
182 | width: 50;
183 | height: auto;
184 | }
185 |
186 | #temp-gradient{
187 | max-width: 350px;
188 | height: 20px;
189 | margin: 5px;
190 | margin-left: 10px;
191 | border: 1px solid black;
192 | display: inline-block;
193 | /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#0000ff+0,ff0000+100 */
194 | background: #0000ff; /* Old browsers */
195 | background: -moz-linear-gradient(left, #0000ff 0%, #ff0000 100%); /* FF3.6-15 */
196 | background: -webkit-linear-gradient(left, #0000ff 0%,#ff0000 100%); /* Chrome10-25,Safari5.1-6 */
197 | background: linear-gradient(to right, #0000ff 0%,#ff0000 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
198 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#0000ff', endColorstr='#ff0000',GradientType=1 ); /* IE6-9 */
199 | }
200 |
201 | #anomaly-gradient{
202 | max-width: 350px;
203 | height: 20px;
204 | margin: 5px;
205 | margin-left: 10px;
206 | border: 1px solid black;
207 | display: inline-block;
208 | /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#000077+0,ff6100+100 */
209 | background: #000077; /* Old browsers */
210 | background: -moz-linear-gradient(left, #000077 0%, #ff6100 100%); /* FF3.6-15 */
211 | background: -webkit-linear-gradient(left, #000077 0%,#ff6100 100%); /* Chrome10-25,Safari5.1-6 */
212 | background: linear-gradient(to right, #000077 0%,#ff6100 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
213 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#000077', endColorstr='#ff6100',GradientType=1 ); /* IE6-9 */
214 | }
--------------------------------------------------------------------------------
/parse.py:
--------------------------------------------------------------------------------
1 | ##### parse.py
2 | ##### Parsing Script converting HADCrut3 Climate Data to GEOJSON
3 |
4 | ##### AUTHOR: Teoman (Ted) Yavuzkurt
5 | ##### www.github.com/teomandavid
6 | ##### www.teomandavid.com
7 |
8 | ##### IMPORTS
9 | import os, re, sys, time, geojson, argparse, subprocess
10 |
11 | ##### CONSTANTS
12 |
13 | # NOTE: these can be set via command line arguments
14 | # But these are good defaults (so you can just run python3 parse.py)
15 |
16 | # NOTE: climate anomaly data is available from 1850 to 2009
17 | # So this is a good range.
18 | START_YEAR = 1850 # year to start outputting data
19 | END_YEAR = 2010 # year to end data output (non-inclusive)
20 | VERBOSE = False # print extra info to command line
21 | PRETTY_PRINT = True # make output pretty
22 | PATH = "./climate-data" # directory containing climate data set
23 | OUTPATH = "./output/" # directory to store output, will be created
24 | OUTPREFIX = "" # filename prefix. Will append .json if needed
25 | PRETTY_PRINT = True # pretty print JSON Output
26 | LIMIT = 25000 # maximum # of stations to parse (>6000 == all of them)
27 | AVERAGE_TEMPS = True # averages missing data points
28 | MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
29 |
30 | ##### REGEX -- precompiled for speed
31 | # Matches fields we want to capture
32 | reHeaders = re.compile("(Number|Name|Country|Lat|Long|Height|Start\syear|End\syear)=\s*(.*)")
33 | # Matches numbers
34 | reNum = re.compile("\-*\d+.*")
35 | # Matches years
36 | reYear = re.compile("(\d{4})\s+(.*)")
37 | # Matches temperature readings
38 | reTemp = re.compile("(\-*\d+.\d+)\s*(.*)")
39 | # Matches the word "Obs:" (placed before temp readings)
40 | reObs = re.compile("Obs:")
41 | # Matches positive numbers (i.e. names of files) -- prevents matching DS_STORE etc
42 | reInclude = re.compile("[0-9]+")
43 |
44 | ##### MAIN CODE
45 |
46 | ##### I've segmented these into functions
47 | ##### So it's clearer what each part is doing
48 |
49 | ##### Not adding a lot of comments in this file as it's pretty self explanatory
50 |
51 | ##### Could've done this with Pandas.
52 |
53 | # init():
54 | # Arguments: None
55 | # Description: Parses script arguments and starts parsing
56 | # Called on startup
57 | START_YEAR = 1850 # year to start outputting data
58 | END_YEAR = 2010 # year to end data output (non-inclusive)
59 | VERBOSE = False # print extra info to command line
60 | PRETTY_PRINT = True # make output pretty
61 | PATH = "./climate-data/" # directory containing climate data set
62 | OUTPATH = "./output/" # directory to store output, will be created
63 | OUTPREFIX = "" # filename prefix. Will append .json if needed
64 | PRETTY_PRINT = True # pretty print JSON Output
65 | LIMIT = 25000 # maximum # of stations to parse (>6000 == all of them)
66 | AVERAGE_TEMPS = True # averages missing data points
67 | MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
68 |
69 | def init():
70 | global LIMIT, VERBOSE, START_YEAR, END_YEAR, PRETTY_PRINT, OUTPATH, PATH, AVERAGE_TEMPS
71 | parser = argparse.ArgumentParser(description='Parse climate data set into GEOJson.')
72 | parser.add_argument('-l', help='Max # of stations to process. (>6000 == all) Default: ' + str(LIMIT), type=int, nargs=1, default=[LIMIT])
73 | parser.add_argument('-v', action="store_true", help='Print extra info during parsing. Default: ' + str(VERBOSE), default=VERBOSE)
74 | parser.add_argument('-p', help="Period for output. Default: " + str(START_YEAR) + ", " + str(END_YEAR), type=int, nargs=2, default=[START_YEAR, END_YEAR])
75 | parser.add_argument('-a', action="store_true", help="Average missing data points or not. Default: " + str(AVERAGE_TEMPS), default=AVERAGE_TEMPS)
76 | parser.add_argument('-f', action="store_true", help="Format JSON output nicely. Default: " + str(PRETTY_PRINT), default=PRETTY_PRINT)
77 | parser.add_argument('-o', help="Directory for output. Default: " + OUTPATH, type=str, default=[OUTPATH], nargs=1)
78 | parser.add_argument('-i', help="Directory containing climate data. Default: " + PATH, type=str, default=[PATH], nargs=1)
79 |
80 | args = parser.parse_args()
81 | LIMIT = vars(args)['l'][0]
82 | VERBOSE = vars(args)['v']
83 | START_YEAR = vars(args)['p'][0]
84 | END_YEAR = vars(args)['p'][1]
85 | AVERAGE_TEMPS = vars(args)['a']
86 | PRETTY_PRINT = vars(args)['f']
87 | OUTPATH = vars(args)['o'][0]
88 | PATH = vars(args)['i'][0]
89 |
90 |
91 |
92 | parseData()
93 |
94 |
95 | # parseData():
96 | # Arguments: None
97 | # Description: Calls other parsing functions
98 | def parseData():
99 | stations = parseFiles()
100 |
101 | for month in range(0, 12):
102 | print("Parsing month: " + MONTHS[month])
103 | features = stationsToFeatures(stations, month)
104 | outputJson(features, MONTHS[month])
105 |
106 | features = stationsToFeatures(stations, None, True)
107 | outputJson(features, "headers")
108 |
109 | def parseFiles():
110 | filenames = getAllFilenames()
111 | stations = []
112 | start = time.clock()
113 |
114 | for filename in filenames:
115 | station = parseFile(filename)
116 | if(station):
117 | stations.append(station)
118 | printv("PARSE COMPLETE, file: %s\t station %20s\t start %i\t end %i" % (filename[-6:], station['Name'], station['Start year'], station['End year']))
119 | print("%i files parsed in %i seconds" % (len(stations), time.clock()-start))
120 | return stations
121 |
122 | def getAllFilenames():
123 | filenames = []
124 | count = 1
125 | for root, dirs, files in os.walk(PATH, topdown = False):
126 | for name in files:
127 | if reInclude.match(name):
128 | filenames.append(os.path.join(root, name))
129 | if count > LIMIT:
130 | break
131 | count = count + 1
132 | if count > LIMIT:
133 | break
134 | return filenames
135 |
136 | def parseFile(filename):
137 | station = {}
138 | temperatures = []
139 | station['temperatures'] = temperatures
140 | file = open(filename)
141 | # are we capturing the actual data yet
142 | capturing = False
143 | for line in file:
144 | if reObs.match(line):
145 | capturing = True
146 | padTemperatures(station)
147 | continue
148 | if capturing:
149 | temperatures.extend(parseTemp(line))
150 | elif reHeaders.match(line):
151 | header = reHeaders.search(line)
152 | station[header.group(1)] = parseHeader(header.group(2))
153 | padTemperatures(station, True)
154 | return station if validStation(station) else []
155 |
156 | # filter out bad station data
157 | def validStation(station):
158 | return abs(station['Lat']) <= 90 and abs(station['Long']) <= 180
159 |
160 | def parseHeader(string):
161 | return num(string) if reNum.match(string) else string.title()
162 |
163 | def num(s):
164 | try:
165 | return int(s)
166 | except ValueError:
167 | return float(s)
168 |
169 | def padTemperatures(station, padEnd = False):
170 | if(padEnd):
171 | finishYear = END_YEAR
172 | startYear = station['End year']
173 | else:
174 | finishYear = station['Start year']
175 | startYear = START_YEAR
176 | if(finishYear > startYear):
177 | for i in range(startYear, finishYear):
178 | for j in range(0, 12):
179 | station['temperatures'].append(-99.0);
180 |
181 | def parseTemp(line):
182 | temps = []
183 | yearMatch = reYear.search(line)
184 | year = int(yearMatch.group(1))
185 | line = yearMatch.group(2)
186 | if year >= START_YEAR:
187 | for i in range(0, 12):
188 | temp = reTemp.search(line)
189 | temps.append(float(temp.group(1)))
190 | line = temp.group(2)
191 | return temps
192 |
193 | def ifelse(ddict, key, default):
194 | return ddict[key] if key in ddict.keys() else default
195 |
196 | def interpolate(temps, index):
197 | # verifies that the indices separated by rng are valid
198 | def valid(rng):
199 | return index - rng > 0 and \
200 | index + rng < len(temps) and \
201 | temps[index - rng] != -99 and \
202 | temps[index + rng] != -99
203 | def avg(rng):
204 | return round((temps[index - rng] + temps[index + rng]) / 2, 1)
205 |
206 | # average first by surrounding months
207 | # if not possible, average by last year's reading and next year's
208 | if(valid(1) or valid(12)):
209 | return avg(1) if valid(1) else avg(12)
210 | else:
211 | # data points missing for both surrounding months and same month in surrounding years
212 | return -99
213 |
214 | def stationToGeojson(station, month, headersOnly = False):
215 | # negative Longitude in data is East, in MapBox it's West
216 | point = geojson.Point((-1 * station['Long'],station['Lat']))
217 | stationProperties = generateProperties(station, month, headersOnly)
218 | feature = geojson.Feature(geometry = point, properties=stationProperties)
219 | printv("FEATURE ENCODING COMPLETE, id: %s\t station: %20s\t" % (station['Number'], station['Name']))
220 | return feature
221 |
222 | def generateProperties(station, month, headersOnly = False):
223 | if(headersOnly):
224 | return {
225 | "id" : ifelse(station, 'Number', time.clock()),
226 | "name" : ifelse(station, 'Name', ""),
227 | "country" : ifelse(station, 'Country', ""),
228 | "elevation" : ifelse(station, 'Height', 0.0),
229 | "start_year": ifelse(station, 'Start year', START_YEAR),
230 | "end_year" : ifelse(station, 'End year', END_YEAR)
231 | }
232 |
233 | properties = {}
234 |
235 | # month == offset
236 | for i in range(0, len(station['temperatures']), 12):
237 | tempIndex = i + month
238 | if(tempIndex > len(station['temperatures'])):
239 | break
240 | tempstr = str(i//12)
241 | if(station['temperatures'][tempIndex] == -99):
242 | if(AVERAGE_TEMPS):
243 | station['temperatures'][tempIndex] = interpolate(station['temperatures'], tempIndex)
244 |
245 | properties[tempstr] = station['temperatures'][tempIndex]
246 | return properties
247 |
248 | def stationsToFeatures(stations, month, headersOnly = False):
249 | features = []
250 | for station in stations:
251 | geoStation = stationToGeojson(station, month, headersOnly)
252 | features.append(geoStation)
253 | return features
254 |
255 | def outputJson(features, suffix = ""):
256 | print("OUTPUTING JSON DATA")
257 |
258 | geoData = geojson.FeatureCollection(features);
259 | printv("GeoJSON FeatureCollection Created")
260 |
261 | filename = OUTPATH + OUTPREFIX + str(suffix) + ".json"
262 | os.makedirs(os.path.dirname(filename), exist_ok = True)
263 | printv("Created Directory for " + filename)
264 |
265 | extraArgs = {"indent":4, "separators":(',', ': ')} if PRETTY_PRINT else {}
266 | rawJson = geojson.dumps(geoData, sort_keys = True, **extraArgs)
267 |
268 | printv("Raw JSON Generated. Writing to file " + filename)
269 |
270 | with open(filename, 'w') as outfile:
271 | outfile.write(rawJson)
272 | print("SUCCESSFULLY WROTE JSON FILE: " + filename)
273 |
274 | # printv():
275 | # Arguments: Any number of string parameters
276 | # Description: Prints the arguments if VERBOSE is enabled
277 | def printv(*args):
278 | if(VERBOSE):
279 | print(*args)
280 |
281 | init()
282 |
--------------------------------------------------------------------------------
/demos/climate-sim.js:
--------------------------------------------------------------------------------
1 | // climate-sim.js
2 | // Main Javascript to display HADCrut3 Climate Data using MapboxGL.js
3 |
4 | // AUTHOR: Teoman (Ted) Yavuzkurt
5 | // www.github.com/teomandavid
6 | // www.teomandavid.com
7 |
8 | // Note:
9 | // This code has been refactored SIGNIFICANTLY to cut down on the number of functions and closures.
10 | // This makes it easier to follow the flow of the code and to see the MapBoxGL API in use.
11 | // However, this is not a very robust way to code (just putting everything in one script)
12 | // Some "boring" functions (i.e. JQuery DOM, color etc stuff) are minified at the top. Use jsbeautifier.org
13 | // if you want to see their code nicely.
14 |
15 | // API access code to get the custom style for this project
16 | mapboxgl.accessToken = 'pk.eyJ1IjoidGVvbWFuZGF2aWQiLCJhIjoiY2lwaHBrNnp4MDE2Z3RsbmpxeWVkbXhxMSJ9.rhKrjQ0Eb8iH0inNPQ7W8Q';
17 |
18 | // Actual map style we're going to use. Change this if you want to use a custom style.
19 | // just ensure layers are named for months in lowercase, with headers in a separate layer called 'headers'
20 | const mapStyle = 'mapbox://styles/teomandavid/ciqhsdrro002qcfnn41ofo4f2';
21 |
22 | // need to declare this before using it, or JQuery will throw an error
23 | var map;
24 |
25 | // ###### CONSTANTS -- CONFIGURATION ######
26 |
27 | // months and Temperature Anomaly data.
28 | const months = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'];
29 | const tempAnomaly = {1850:-0.431,1851:-0.020,1852:-0.320,1853:-0.388,1854:0.065,1855:-0.188,1856:-0.352,1857:-0.289,1858:-0.375,1859:-0.363,1860:-0.326,1861:-0.266,1862:-0.591,1863:-0.307,1864:-0.639,1865:-0.306,1866:-0.358,1867:-0.371,1868:-0.291,1869:-0.260,1870:-0.266,1871:-0.413,1872:-0.164,1873:-0.204,1874:-0.343,1875:-0.584,1876:-0.245,1877:-0.040,1878:0.109,1879:-0.409,1880:-0.169,1881:-0.303,1882:-0.138,1883:-0.412,1884:-0.541,1885:-0.500,1886:-0.423,1887:-0.522,1888:-0.488,1889:-0.228,1890:-0.467,1891:-0.599,1892:-0.603,1893:-0.689,1894:-0.551,1895:-0.551,1896:-0.380,1897:-0.301,1898:-0.405,1899:-0.330,1900:-0.176,1901:-0.187,1902:-0.352,1903:-0.435,1904:-0.570,1905:-0.434,1906:-0.224,1907:-0.619,1908:-0.493,1909:-0.471,1910:-0.346,1911:-0.469,1912:-0.392,1913:-0.326,1914:-0.063,1915:-0.082,1916:-0.370,1917:-0.694,1918:-0.489,1919:-0.277,1920:-0.306,1921:-0.168,1922:-0.261,1923:-0.295,1924:-0.370,1925:-0.280,1926:-0.046,1927:-0.231,1928:-0.164,1929:-0.444,1930:-0.137,1931:-0.125,1932:-0.056,1933:-0.296,1934:-0.071,1935:-0.167,1936:-0.111,1937:-0.071,1938:0.109,1939:-0.059,1940:-0.030,1941:-0.013,1942:-0.028,1943:-0.064,1944:0.073,1945:-0.100,1946:-0.078,1947:-0.009,1948:-0.059,1949:-0.150,1950:-0.318,1951:-0.125,1952:-0.037,1953:0.058,1954:-0.186,1955:-0.204,1956:-0.433,1957:-0.052,1958:0.081,1959:-0.011,1960:-0.100,1961:0.040,1962:-0.000,1963:0.007,1964:-0.285,1965:-0.185,1966:-0.116,1967:-0.120,1968:-0.210,1969:-0.059,1970:-0.022,1971:-0.205,1972:-0.176,1973:0.156,1974:-0.306,1975:-0.114,1976:-0.368,1977:0.072,1978:-0.046,1979:0.062,1980:0.141,1981:0.248,1982:0.021,1983:0.320,1984:-0.058,1985:-0.010,1986:0.117,1987:0.290,1988:0.342,1989:0.195,1990:0.428,1991:0.339,1992:0.103,1993:0.183,1994:0.326,1995:0.477,1996:0.215,1997:0.464,1998:0.821,1999:0.493,2000:0.363,2001:0.559,2002:0.666,2003:0.645,2004:0.622,2005:0.760,2006:0.674,2007:0.680,2008:0.527,2009:0.672};
30 |
31 | const dataStartYear = 1850; // year the data set starts, inclusive
32 | const dataEndYear = 2010; // year the data set ends, not inclusive
33 | const defaultStyle = 'solid'; // default display mode for the map ('solid' or 'heatmap')
34 | const defaultStartYear = 2000; // year to start the display
35 | const defaultStartMonth = months[0]; // month to start the display
36 | const animationSpeed = 1000; // how fast to change years in milliseconds
37 | const tempRange = [-20, 40]; // temperature range for raw temperatures
38 | const tempColors = [[0,0,255], [255,0,0]]; // colors for Raw Temperature Gradient (color 1: [Red, Green, Blue] color2: [Red, Green, Blue])
39 | const anomalyRange = [-1, 1]; // temperature range for anomaly temperatures
40 | const anomalyColors = [[0,0,119], [255,97,0]]; // colors for Anomaly Temp Gradient (color 1: [Red, Green, Blue] color2: [Red, Green, Blue])
41 |
42 | const layerNames = months; // by default, our layers are just named after months
43 |
44 | // NOTE: if you change the gradient colors here, you'll need to update them in climate-sim.css as well!
45 |
46 | // ###### GLOBAL VARIABLES FOR SIMULATION #####
47 |
48 | // simulation state
49 | var currentYear = defaultStartYear; // current year for simulation
50 | var currentMonth = defaultStartMonth; // current month for simulation
51 | var currentIndex = currentYear - dataStartYear; // current position in temperatures array (i.e. year offset)
52 | var currentStyle = defaultStyle; // current simulation display style ('solid' or 'heatmap')
53 |
54 | // display variables
55 | var defaultWaterColor; // default color for water -- loaded dynamically from map on init
56 | var showAnomaly = false; // display anomaly data or not
57 |
58 | // animation variables
59 | var intervalID; // JavaScript intervalID for animation, so it can be cancelled
60 | var playing = false; // animation playing or not
61 | var loop = false; // loop animation
62 |
63 | // popup variables
64 | var popup = new mapboxgl.Popup({ // popup object
65 | closeButton: false,
66 | closeOnClick: false
67 | });
68 | var popupActive = false; // TRUE: popup is showing FALSE: popup hidden
69 | var currentFeature = null; // current feature loaded in popup
70 |
71 | // ###### HELPER FUNCTIONS #####
72 |
73 | // these functions are not terribly interesting as far as MapBoxGL goes, so I've minified their code here
74 | // if you're curious about some JQuery go to jsbeautifier.org and paste these in
75 | // converts color array [R,G,B] to string usable by MapBoxGL
76 | function convertColor(colorArray){ return "rgb(" + colorArray[0] + "," + colorArray[1] + "," + colorArray[2] + ")"; }
77 | // calculates color along a gradient, returns [R,G,B] array
78 | function colorFromGradient(percent, gradient){ return gradient[0].map(function(color, index){return Math.round((1-percent)*color + percent*gradient[1][index]);}); }
79 | // initializes select menus
80 | function initSelect(id, source, selected, handler){ source.forEach(function(item){ var option = $("").attr("value",item).text(item); if(item == selected) {option.attr("selected", "selected");} $(id).append(option); }); $(id).on('change', function(){ handler(this.value); }); }
81 | // loads temperature scales
82 | function loadTemperatureScales() {var elements = ['#raw-temp-start', '#raw-temp-end', '#anomaly-start', '#anomaly-end']; var temps = [tempRange[0], tempRange[1], anomalyRange[0], anomalyRange[1]]; elements.forEach(function(ele, index){ $(ele).html(temps[index] + "°C"); }); }
83 | // toggles animation when button is pushed
84 | function toggleAnimation(){ if(playing) { $('#toggle-loop').attr('disabled', true); $('#playpause').text("Play"); window.clearInterval(intervalID);}else { $('#toggle-loop').attr('disabled', false); $('#playpause').text("Stop"); intervalID = window.setInterval(function(){ if(currentYear < dataEndYear - 1){ currentYear++; }else if(loop){ currentYear = dataStartYear;}else{$('#playpause').click();} updateMap(); }, animationSpeed); } playing = !playing;}
85 | // shows an alert w/author information
86 | function showInfo(e){e.preventDefault(); alert("HADCrut3 Climate Simulation by Teoman (Ted) Yavuzkurt.\nhttp://www.github.com/TeomanDavid\nhttp://www.teomandavid.com\n\nSource available on GitHub under MIT license (my portions).\nRaw Data From: http://www.metoffice.gov.uk/research/climate/climate-monitoring/land-and-atmosphere/surface-station-records\n\nPOWERED BY MAPBOXGL: http://www.mapbox.com");}
87 |
88 | // ###### STYLES #####
89 |
90 | // simulation display styles
91 | // NOTE: each of these is a function
92 | // This way we can pass in an argument (prop) which is the name
93 | // of the property containing the values for the styling.
94 | // e.g. styles['heatmap'](20);
95 | var styles = {
96 | heatmap : function(prop){
97 | return{
98 | 'circle-radius' : {
99 | 'type': 'exponential',
100 | 'stops': [[2, 60], [6, 600]]
101 | },
102 | 'circle-color': {
103 | 'property' : "" + prop, // we have to do "" + prop to make it a string
104 | 'type' : 'exponential',
105 | 'stops' : [
106 | [tempRange[0], convertColor(tempColors[0])],
107 | [tempRange[1], convertColor(tempColors[1])]
108 | ]
109 | },
110 | 'circle-opacity': {
111 | 'property' : "" + prop,
112 | 'type': 'exponential',
113 | 'stops': [[-99, 0.0], [-50, 0.125]]
114 | },
115 | 'circle-blur': 1
116 | };
117 | },
118 | solid : function(prop){
119 | return{
120 | 'circle-radius': {
121 | 'type': 'exponential',
122 | 'stops': [[2, 5], [6, 35]]
123 | },
124 | 'circle-opacity': {
125 | 'property' : "" + prop,
126 | 'type': 'exponential',
127 | 'stops': [[-99, 0.0], [-50, 1.0]]
128 | },
129 | 'circle-color': {
130 | 'property' : "" + prop,
131 | 'type' : 'exponential',
132 | 'stops' : [
133 | [tempRange[0], convertColor(tempColors[0])],
134 | [tempRange[1], convertColor(tempColors[1])]
135 | ]
136 | },
137 | 'circle-blur': 0
138 | };
139 | }
140 | };
141 |
142 | // display style for the header layer
143 | // uses circle-radius style from solid display style
144 | // opacity set to 0 so we don't actually see it, but we can still
145 | // interact with it
146 | var headerStyle = {
147 | 'circle-radius' : styles['solid'](0)['circle-radius'],
148 | 'circle-opacity': 0
149 | };
150 |
151 | // ###### DISPLAY FUNCTIONS #####
152 |
153 | // updateMap()
154 | // recalculates currentIndex and updates circle-color and circle-opacity
155 | // properties on the map. Redraws temperature anomaly if it's displayed.
156 | function updateMap(){
157 | currentIndex = currentYear - dataStartYear;
158 | applyStyles([currentMonth],['circle-color', 'circle-opacity']);
159 | if(showAnomaly){
160 | updateAnomaly();
161 | }
162 | updateHTML();
163 | updatePopup();
164 | }
165 |
166 | // applyStyles(layers, props, style)
167 | // layers = layers to apply styles to
168 | // props = properties to apply ("all" === all properties)
169 | // style = style to apply to layers
170 | // applies styles to layers automatically to avoid lots of repeated API calls
171 | function applyStyles(layers, props, style){
172 | if(style == null){ style = styles[currentStyle](currentIndex); } // bug fix: safari and some browsers don't support default assignment in arguments
173 | if(props === "all"){ props = Object.keys(style);} // get all the properties from the style if we didn't specify which
174 | layers.forEach(function(layer){
175 | props.forEach(function(prop){
176 | // set the paint property on each layer
177 | map.setPaintProperty(layer, prop, style[prop]);
178 | });
179 | });
180 | }
181 |
182 | // updateAnomaly()
183 | // redraws temperature anomaly data
184 | // calculates what percentage along the temperature anomaly gradient
185 | // the current temperature anomaly is. Then converts it to a color
186 | // and finally renders on the map. If anomaly isn't showing, resets
187 | // water color to default.
188 | function updateAnomaly(){
189 | var color = defaultWaterColor;
190 | if(showAnomaly){
191 | var anomaly = tempAnomaly[currentYear];
192 | var percent = (anomaly - anomalyRange[0])/(anomalyRange[1] - anomalyRange[0]);
193 | percent = ((percent > 1)? 1 : ((percent < 0)?0 : percent));
194 | color = convertColor(colorFromGradient(percent, anomalyColors));
195 | }
196 | map.setPaintProperty('water', 'fill-color', color);
197 | }
198 |
199 |
200 | // updatePopup()
201 | // a quick function to update the text in our popup
202 | // it first checks that the popup is currently visible (popupActive == true)
203 | // and then renders the HTML
204 | // NOTE: you'd probably want to use a JQuery or other template here,
205 | // but that would complicate this example
206 | function updatePopup(){
207 | if(popupActive){
208 | popup.setHTML("
" + currentFeature['name'] + '
' + "Temperature: " + currentFeature['temperatures']["" + currentIndex] + "°C");
209 | }
210 | }
211 |
212 | // updateHTML()
213 | // updates HTML elements to keep pace with map updates
214 | function updateHTML(){
215 | $('#year').html(currentYear);
216 | $('#anomaly').html(tempAnomaly[currentYear] + "°C");
217 | $('#map-slider').slider("option", "value", currentYear);
218 | }
219 |
220 |
221 | // using $(document).ready() ensures we don't try to render the map
222 | // before the HTML page has completed loading. Otherwise we'll get errors.
223 | $(document).ready(function(){
224 |
225 | // instantiate the map with some basic parameters that work well for this data set
226 | map = new mapboxgl.Map({
227 | container: 'map',
228 | style: mapStyle,
229 | zoom: 2,
230 | minZoom: 2,
231 | maxZoom: 7,
232 | dragRotate: false, // don't allow rotation
233 | center: [5.425411010332567, 51.22556912180988]
234 | });
235 |
236 | // We let the map load, then configure it
237 | map.on('load', function () {
238 |
239 | // ###### LAYOUT AND DISPLAY ACTIONS ######
240 |
241 | // first we grab and store the current color of the water
242 | // using the getPaintProperty function
243 | // this will allow us to put it back later if we change it
244 | defaultWaterColor = map.getPaintProperty('water', 'fill-color');
245 |
246 | // once the map loads, we want to style all the layers
247 | // this will apply the current style to all temperature layers
248 | applyStyles(layerNames, "all");
249 |
250 | // we then apply our style to the header layer as well and make it "visible."
251 | // we could have done this in studio and programmatically changed it here
252 | // to match the 'solid' display style
253 | // NOTE: opacity for this is 0 because we don't want it to show -- just interact with mouse
254 | // if layer isn't visible it won't interact with mouse
255 | applyStyles(['headers'],['circle-radius', 'circle-opacity'], headerStyle)
256 | map.setLayoutProperty('headers', 'visibility', 'visible');
257 |
258 | // show the current month
259 | map.setLayoutProperty(currentMonth, 'visibility', 'visible');
260 |
261 | // now let's draw the slider
262 | // this JQuery code is uninteresting but I've left it here to show how we update the map
263 | // we only change the map on the 'stop' event so that it can scroll smoothly
264 | // we don't use the 'change' event because this leads to an infinite loop if the map
265 | // updates the slider position (i.e. during iteration)
266 | $('#map-slider').slider({
267 | animate: 'fast',
268 | max: dataEndYear - 1,
269 | min: dataStartYear,
270 | value: currentYear,
271 | slide: function(event, ui){
272 | $('#year').html(ui.value);
273 | $('#anomaly').html(tempAnomaly[ui.value] + "°C");
274 | },
275 | stop: function(event, ui){
276 | currentYear = ui.value;
277 | updateMap();
278 | }
279 | });
280 |
281 | // update HTML fields (including slider)
282 | // write year and such
283 | updateHTML();
284 |
285 | // initSelect function itself is not terribly interesting -- just populates select element
286 | // and registers a callback function (code at top if interested).
287 | // interesting part here is the callback -- we just swap layer visibility based
288 | // on what month is selected.
289 | // We call updateMap() because the new layer is probably out of sync with the current year.
290 | initSelect('#map-months', months, currentMonth, handler = function(month){
291 | var prevMonth = currentMonth;
292 | currentMonth = month;
293 | map.setLayoutProperty(prevMonth, 'visibility', 'none');
294 | map.setLayoutProperty(currentMonth, 'visibility', 'visible');
295 | updateMap();
296 | });
297 |
298 | // if we change the style from 'heatmap' to 'solid' or vice versa
299 | // we set the current style, then apply it to all layers using applyStyles()
300 | initSelect('#map-display', Object.keys(styles), currentStyle, handler = function(style){
301 | currentStyle = style;
302 | applyStyles(layerNames, "all");
303 | });
304 |
305 | // boring JQuery function to display temperature scales
306 | // reads tempRange and anomalyRange and outputs HTML
307 | loadTemperatureScales();
308 |
309 | // ###### POPUP EVENT HANDLING ######
310 |
311 | // this function is a little long because there are a lot of cases in which we don't
312 | // want to display the popup. If any of these conditions are met we immediately set
313 | // popupActive to false (so it won't be updated and we don't waste cycles) and we
314 | // remove the popup from the map.
315 |
316 | // any time the mouse moves over the map, we query features at that point
317 | // since the temperature layers do not contain header information, we have to grab
318 | // parts of our data from different layers. Thus we loop through all features at a point
319 | // and grab the respective information we need. The map SHOULD only trigger features at one
320 | // coordinate, thus we don't have to worry about getting the name for one climate station
321 | // and the temperature for another
322 |
323 | // finally, when we have the information, we display it
324 | map.on('mousemove', function(event) {
325 | // heatmap is too diffuse to display popups, so we return
326 | if(currentStyle == 'heatmap') { popupActive = false; return popup.remove(); }
327 |
328 | // query features at the mouse pointer location
329 | var features = map.queryRenderedFeatures(event.point, {
330 | layers: ['headers', currentMonth]
331 | });
332 |
333 | // if we didn't get any features or we only got 1 (i.e. not enough to get our data), return
334 | if(!features.length || features.length == 1) { popupActive = false; return popup.remove(); }
335 |
336 | // we loop through all the features we found and pull out the info we need
337 | var result = [];
338 | for(i in features){
339 | if('name' in features[i].properties){
340 | result['name'] = features[i].properties.name;
341 | result['coordinates'] = features[i].geometry.coordinates;
342 | }
343 | if(("" + currentIndex) in features[i].properties){
344 | result['temperatures'] = features[i].properties;
345 | }
346 | }
347 |
348 | // if the temperature is -99 at this point, it means the station has missing data
349 | // so we don't want to display a popup because there will not be a dot on the map. Return.
350 | // note again we have to use "" + currentIndex to access our temperature data because
351 | // the indices in GEOJSON features are all strings.
352 | if(result['temperatures']["" + currentIndex] == -99) { popupActive = false; return popup.remove(); }
353 |
354 | // if we've passed all of these checks, we are going to display the popup, so we set it active
355 | popupActive = true;
356 |
357 | // store the currentFeature so we can update temperature (if map is animated)
358 | // without querying again (slow)
359 | currentFeature = result;
360 |
361 | // change mouse cursor
362 | map.getCanvas().style.cursor = 'pointer';
363 |
364 | // display the popup
365 | popup.setLngLat(result['coordinates'])
366 | .addTo(map);
367 | updatePopup();
368 | });
369 |
370 | // ###### CONTROL EVENT HANDLING ######
371 |
372 | // most of this code is just JQuery code to update the display
373 | // we toggle the value of playing
374 | // and then set an animation interval and text accordingly
375 | // we call updateMap after each tick
376 | // code not terribly interesting so it's minified at above
377 | $('#playpause').on('click', toggleAnimation);
378 |
379 | // when we change the checkbox, toggle showAnomaly, and redraw
380 | $('#toggle-anomaly').on('change', function(){
381 | showAnomaly = !showAnomaly;
382 | updateAnomaly();
383 | });
384 |
385 | // when we change the checkbox, toggle loop
386 | // has no immediate effect unless animation is paused on last year
387 | $('#toggle-loop').on('change', function() { loop = !loop; });
388 |
389 | // show author information
390 | $('#info-button').on('click', showInfo);
391 |
392 | // and that's it! Not so bad :)
393 |
394 | });
395 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # HadCRUT3 Climate Change Visualization
4 | ##### Ted Yavuzkurt ([TedY.io](http://www.tedy.io))
5 |
6 | This is a visualization of the [HadCRUT3 Global Temperature Record](http://www.metoffice.gov.uk/research/climate/climate-monitoring/land-and-atmosphere/surface-station-records) provided by the [World Meteorological Organization](http://www.wmo.int/pages/index_en.html). This visualization shows both monthly average temperatures and global temperature anomalies (differences from long term mean) from 1850-2010. More than 3000 land temperature stations are visualized.
7 |
8 | The visualization is powered by [Mapbox GL](https://www.mapbox.com/blog/mapbox-gl/)
9 |
10 | ## Motivation
11 | A friend of mine started working at Mapbox recently and has been singing their praises. When I saw the HadCRUT3 data posted on Reddit(?) I thought it would be a great opportunity to experiment with the API. This was pretty fun to make and the visualization code (```climate-sim.js```) is **heavily** commented to explain how it works.
12 |
13 | ## Live Demo
14 | [A live demo is available here.](https://tedyav.com/demos/climate-vis/)
15 |
16 | ## How it Works
17 | The raw temperature data is contained in ```climate-data.tar.gz``` and was downloaded from the [UK Met Office](http://www.metoffice.gov.uk/research/climate/climate-monitoring/land-and-atmosphere/surface-station-records). I wrote a simple python script to parse these files (```parse.py```), turning them into [GeoJSON](http://geojson.org/) Feature Collections that could be uploaded to [Mapbox Studio](https://www.mapbox.com/studio/).
18 |
19 | The parsing script generates 13 output files. Each output file contains all temperature data for a given month of the year, while one contains header information (name of station, elevation, country, etc).
20 |
21 | I divided the data like this because placing all temperature data in one feature collection was too large for Mapbox Studio (and browsers) to display comfortably. Since we're concerned with seeing data trends, segmenting the data by month makes sense as this shows change over time rather than seasonal variations.
22 |
23 | Within the GeoJSON collections, each station is represented by a point feature. It has properties numbered from ```0``` to ```160```, corresponding to obvserved temperature in 1850 until 2010. If a temperature observation is missing, the value is set to ```-99```, so that the visualization code knows to disregard it.
24 |
25 | Once the data was processed, I uploaded all files to [Mapbox Studio](https://www.mapbox.com/studio/) and added them to a new style. Each file went on a different layer, which were placed below country labels / boundaries to prevent the visualization from obscuring the underlying map.
26 |
27 | All layers were initially set to be invisible so that I could enable / disable them programmatically.
28 |
29 | I then exported this style and used it as the basis of the Visualization. The nice thing about this is that I don't have to add layers to the map manually--since they're in the style they will be automatically loaded, though they will be invisible at first.
30 |
31 | The actual visualization code is stored in ```demos/climate-sim.js```. It's fairly straightforward and **thoroughly commented**, but I'll go through it here to explain things I learned along the way.
32 |
33 | ###Breakdown of the code
34 |
35 | The main code for this is stored in `climate-sim.js`, but I'll go over the HTML first. Not going to go over the styles as I've never been strong with CSS.
36 |
37 | ####0. HTML File
38 | The HTML for this is pretty simple:
39 |
40 | ```HTML
41 |
42 |
43 |
44 |
45 | HadCRUT3 Climate Data
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
88 |
89 |
90 |
91 | ```
92 |
93 | We load a few scripts, create a div for our map, and then have some rows at the bottom for controls. That's it. The heavy lifting is in `climate-sim.js`.
94 |
95 | ####1. Constants
96 | These aren't terribly interesting and are commented in the file if you want to change them. They tell the script when the data set starts, the colors to use, overall map style to use, etc.
97 |
98 | ```javascript
99 | mapboxgl.accessToken = 'pk.eyJ1IjoidGVvbWFuZGF2aWQiLCJhIjoiY2lwaHBrNnp4MDE2Z3RsbmpxeWVkbXhxMSJ9.rhKrjQ0Eb8iH0inNPQ7W8Q';
100 | const mapStyle = 'mapbox://styles/teomandavid/ciqhsdrro002qcfnn41ofo4f2';
101 |
102 | // need to declare this before using it, or JQuery will throw an error
103 | var map;
104 |
105 | const months = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'];
106 | const tempAnomaly = // redacted
107 | const dataStartYear = 1850; // year the data set starts, inclusive
108 | const dataEndYear = 2010; // year the data set ends, not inclusive
109 | const defaultStyle = 'solid'; // default display mode for the map ('solid' or 'heatmap')
110 | const defaultStartYear = 2000; // year to start the display
111 | const defaultStartMonth = months[0]; // month to start the display
112 | const animationSpeed = 1000; // how fast to change years in milliseconds
113 | const tempRange = [-20, 40]; // temperature range for raw temperatures
114 | const tempColors = [[0,0,255], [255,0,0]]; // colors for Raw Temperature Gradient (color 1: [Red, Green, Blue] color2: [Red, Green, Blue])
115 | const anomalyRange = [-1, 1]; // temperature range for anomaly temperatures
116 | const anomalyColors = [[0,0,119], [255,97,0]]; // colors for Anomaly Temp Gradient (color 1: [Red, Green, Blue] color2: [Red, Green, Blue])
117 | const layerNames = months;
118 | ```
119 |
120 | ####2. Global State Variables
121 | Next I declare some globals to store values like the current year for the visualization, the current selected temperature display style, whether the animation is playing or not, etc.
122 |
123 | ```javascript
124 | var currentYear = defaultStartYear; // current year for simulation
125 | var currentMonth = defaultStartMonth; // current month for simulation
126 | var currentIndex = currentYear - dataStartYear; // current position in temperatures array (i.e. year offset)
127 | var currentStyle = defaultStyle; // current simulation display style ('solid' or 'heatmap')
128 |
129 | // display variables
130 | var defaultWaterColor; // default color for water -- loaded dynamically from map on init
131 | var showAnomaly = false; // display anomaly data or not
132 |
133 | // animation variables
134 | var intervalID; // JavaScript intervalID for animation, so it can be cancelled
135 | var playing = false; // animation playing or not
136 | var loop = false;
137 | ```
138 |
139 | Most of this is uninteresting and commented thoroughly, though one interesting tidbit is this:
140 |
141 | ```javascript
142 | var popup = new mapboxgl.Popup({ // popup object
143 | closeButton: false,
144 | closeOnClick: false
145 | });
146 | var popupActive = false; // TRUE: popup is showing FALSE: popup hidden
147 | var currentFeature = null; // current feature loaded in popup
148 | ```
149 |
150 | If you've played with the live demo for the map, you'll notice the little information pop-up when you hover over a point. This is actually **a single popup object** that I simply move around. This is a better way to work with popups than creating one for each point and showing/hiding them.
151 |
152 | ####3. Helper Functions
153 | I declare a few functions to calculate gradient colors and populate form fields with JQuery. Again, not terribly interesting.
154 |
155 | ```javascript
156 | function convertColor(colorArray){ return "rgb(" + colorArray[0] + "," + colorArray[1] + "," + colorArray[2] + ")"; }
157 | // calculates color along a gradient, returns [R,G,B] array
158 | function colorFromGradient(percent, gradient){ return gradient[0].map(function(color, index){return Math.round((1-percent)*color + percent*gradient[1][index]);}); }
159 | // initializes select menus
160 | function initSelect(id, source, selected, handler){ source.forEach(function(item){ var option = $("").attr("value",item).text(item); if(item == selected) {option.attr("selected", "selected");} $(id).append(option); }); $(id).on('change', function(){ handler(this.value); }); }
161 | // loads temperature scales
162 | function loadTemperatureScales() {var elements = ['#raw-temp-start', '#raw-temp-end', '#anomaly-start', '#anomaly-end']; var temps = [tempRange[0], tempRange[1], anomalyRange[0], anomalyRange[1]]; elements.forEach(function(ele, index){ $(ele).html(temps[index] + "°C"); }); }
163 | // toggles animation when button is pushed
164 | function toggleAnimation(){ if(playing) { $('#toggle-loop').attr('disabled', true); $('#playpause').text("Play"); window.clearInterval(intervalID);}else { $('#toggle-loop').attr('disabled', false); $('#playpause').text("Stop"); intervalID = window.setInterval(function(){ if(currentYear < dataEndYear - 1){ currentYear++; }else if(loop){ currentYear = dataStartYear;}else{$('#playpause').click();} updateMap(); }, animationSpeed); } playing = !playing;}
165 | // shows an alert w/author information
166 | function showInfo(e){e.preventDefault(); alert("HADCrut3 Climate Simulation by Teoman (Ted) Yavuzkurt.\nhttp://www.github.com/TeomanDavid\nhttp://www.teomandavid.com\n\nSource available on GitHub under MIT license (my portions).\nRaw Data From: http://www.metoffice.gov.uk/research/climate/climate-monitoring/land-and-atmosphere/surface-station-records\n\nPOWERED BY MAPBOXGL: http://www.mapbox.com");}
167 | ```
168 |
169 | ####4. Temperature/Header Display Styles
170 | This is where the code starts to get interesting. I searched for a long time about how to do a good temperature display using Mapbox GL, and I ultimately decided that trying to do a contour map would be too complicated. Instead, I opted to offer two distinct display methods: ```heatmap``` and ```solid```. These are stored in a global object called ```styles``` that contains two functions: ```heatmap``` and ```solid```.
171 |
172 | I use functions instead of static styles *because I need to choose which property contains temperature data dynamically*. Thus, I can call ```styles['heatmap'](150)``` to get a heatmap temperature style corresponding to the 150th year of the visualization.
173 |
174 | Here is the ```heatmap``` style
175 |
176 | ```javascript
177 | var styles = {
178 | heatmap : function(prop){
179 | return{
180 | 'circle-radius' : {
181 | 'type': 'exponential',
182 | 'stops': [[2, 60], [6, 600]]
183 | },
184 | 'circle-color': {
185 | 'property' : "" + prop, // we have to do "" + prop to make it a string
186 | 'type' : 'exponential',
187 | 'stops' : [
188 | [tempRange[0], convertColor(tempColors[0])],
189 | [tempRange[1], convertColor(tempColors[1])]
190 | ]
191 | },
192 | 'circle-opacity': {
193 | 'property' : "" + prop,
194 | 'type': 'exponential',
195 | 'stops': [[-99, 0.0], [-50, 0.125]]
196 | },
197 | 'circle-blur': 1
198 | };
199 | },
200 | ```
201 |
202 | All temperatures are displayed as circles with radii from 60 at base-zoom to 600 at max-zoom. This ensures they will be quite large and overlap. To create the heatmap effect, you'll notice ```circle-opacity``` is set to ```0.125```. This ensures that they will add and blend with each other (it is set to ```0.0``` at ```-99``` so that missing data points do not draw). Lastly, I set ```circle-blur``` to ```1``` so that they display in a very diffuse manner.
203 |
204 | Continuing on, the ```solid``` style is a little more straightforward:
205 |
206 | ```javascript
207 | solid : function(prop){
208 | return{
209 | 'circle-radius': {
210 | 'type': 'exponential',
211 | 'stops': [[2, 5], [6, 35]]
212 | },
213 | 'circle-opacity': {
214 | 'property' : "" + prop,
215 | 'type': 'exponential',
216 | 'stops': [[-99, 0.0], [-50, 1.0]]
217 | },
218 | 'circle-color': {
219 | 'property' : "" + prop,
220 | 'type' : 'exponential',
221 | 'stops' : [
222 | [tempRange[0], convertColor(tempColors[0])],
223 | [tempRange[1], convertColor(tempColors[1])]
224 | ]
225 | },
226 | 'circle-blur': 0
227 | };
228 | }
229 | };
230 | ```
231 |
232 | Here we have no blur, and 100% opacity. This makes each circle small and discrete--ideal for showing the precise location of temperature stations.
233 |
234 | Lastly, we have the style for our header layer:
235 |
236 | ```javascript
237 | var headerStyle = {
238 | 'circle-radius' : styles['solid'](0)['circle-radius'],
239 | 'circle-opacity': 0
240 | };
241 | ```
242 |
243 | Interesting points here. First, I use the ```circle-radius``` property from the ```solid``` style. This means that I don't have to keep the code in sync if I change the radius of the ```solid``` style. Also, `circle-opacity` is set to ```0``` so that the header dots will trigger the popups to display without drawing on the map.
244 |
245 | ####5. Map Update Functions
246 | I wrote a few functions to wrap map updates. **Note: the map has not been initialized at this point, but I'm writing this in terms of the layout of the file. We'll get to the creation of the map soon, I promise!**
247 |
248 | Every time the map changes I call ```updateMap``` which simply updates the current temperature index and then applies new styles to the map.
249 |
250 | ```javascript
251 | function updateMap(){
252 | currentIndex = currentYear - dataStartYear;
253 | applyStyles([currentMonth],['circle-color', 'circle-opacity']);
254 | if(showAnomaly){
255 | updateAnomaly();
256 | }
257 | updateHTML();
258 | updatePopup();
259 | }
260 | ```
261 |
262 | One thing to note is that it calls ```applyStyles```, which only updates the properties passed in. On a given map update, I only need to change the `circle-color` and `circle-opacity` properties, as blur and size do not change within a given display style. This speeds up redraws.
263 |
264 | Here is the `applyStyles` function:
265 |
266 | ```javascript
267 | function applyStyles(layers, props, style){
268 | if(style == null){ style = styles[currentStyle](currentIndex); } // bug fix: safari and some browsers don't support default assignment in arguments
269 | if(props === "all"){ props = Object.keys(style);} // get all the properties from the style if we didn't specify which
270 | layers.forEach(function(layer){
271 | props.forEach(function(prop){
272 | // set the paint property on each layer
273 | map.setPaintProperty(layer, prop, style[prop]);
274 | });
275 | });
276 | }
277 | ```
278 |
279 | This code would be simpler if ES6 default arguments were more commonly supported, but essentially I just iterate over the specified layers and apply the current style to all of them by calling ```map.setPaintProperty```. Writing this wrapper function allows me to change the style for all layers easily, by calling it with the global ```layerNames``` variable as the `layers` argument.
280 |
281 | Next, I have the `updateAnomaly` function. This changes the color of the water on the map if the user has checked the anomaly checkbox.
282 |
283 | ```javascript
284 | function updateAnomaly(){
285 | var color = defaultWaterColor;
286 | if(showAnomaly){
287 | var anomaly = tempAnomaly[currentYear];
288 | var percent = (anomaly - anomalyRange[0])/(anomalyRange[1] - anomalyRange[0]);
289 | percent = ((percent > 1)? 1 : ((percent < 0)?0 : percent));
290 | color = convertColor(colorFromGradient(percent, anomalyColors));
291 | }
292 | map.setPaintProperty('water', 'fill-color', color);
293 | }
294 | ```
295 |
296 | Pretty straightforward. I get the current temperature anomaly and calculate what percentage it is in the anomaly range I specified (in the constants). I then use some helper functions (`convertColor` and `colorFromGradient`) to calculate the color for this and draw it on the map.
297 |
298 | Next, I have `updatePopup`. This just changes the popup's HTML content if the map is animating (so that the temperature updates).
299 |
300 | ```javascript
301 | function updatePopup(){
302 | if(popupActive){
303 | popup.setHTML("
" + currentFeature['name'] + '
' + "Temperature: " + currentFeature['temperatures']["" + currentIndex] + "°C");
304 | }
305 | }
306 | ```
307 |
308 | And finally, a helper function to keep all the HTML fields in the UI up to date:
309 |
310 | ```javascript
311 | function updateHTML(){
312 | $('#year').html(currentYear);
313 | $('#anomaly').html(tempAnomaly[currentYear] + "°C");
314 | $('#map-slider').slider("option", "value", currentYear);
315 | }
316 | ```
317 |
318 | ####6. Drawing the Map
319 | I make sure the document is ready:
320 |
321 | `$(document).ready(function(){ ... }`
322 |
323 | Create the map:
324 |
325 | ```javascript
326 | map = new mapboxgl.Map({
327 | container: 'map',
328 | style: mapStyle,
329 | zoom: 2,
330 | minZoom: 2,
331 | maxZoom: 7,
332 | dragRotate: false, // don't allow rotation
333 | center: [5.425411010332567, 51.22556912180988]
334 | });
335 | ```
336 |
337 | Then wait for the map to load:
338 | ```
339 | map.on('load', function () { ... }
340 | ```
341 |
342 | Display our data and load some values:
343 |
344 | ```javascript
345 | defaultWaterColor = map.getPaintProperty('water', 'fill-color');
346 | applyStyles(layerNames, "all");
347 | applyStyles(['headers'],['circle-radius', 'circle-opacity'], headerStyle)
348 | map.setLayoutProperty('headers', 'visibility', 'visible');
349 | map.setLayoutProperty(currentMonth, 'visibility', 'visible');
350 | ```
351 | I store the water color so it can be restored (this decouples the code from the specified style). I apply the current style to all layers, and then set the current layers as visible so they can interact with the mouse.
352 |
353 | Next I create the slider using JQuery:
354 |
355 | ```javascript
356 | $('#map-slider').slider({
357 | animate: 'fast',
358 | max: dataEndYear - 1,
359 | min: dataStartYear,
360 | value: currentYear,
361 | slide: function(event, ui){
362 | $('#year').html(ui.value);
363 | $('#anomaly').html(tempAnomaly[ui.value] + "°C");
364 | },
365 | stop: function(event, ui){
366 | currentYear = ui.value;
367 | updateMap();
368 | }
369 | });
370 | updateHTML();
371 | ```
372 | Important thing to note here: I do not update the map as the slider is moving--only when it stops. This makes it faster.
373 |
374 | Next I initialize the months selection menu and set an event handler on change (using my `initSelect` helper function):
375 |
376 | ```javascript
377 | initSelect('#map-months', months, currentMonth, handler = function(month){
378 | var prevMonth = currentMonth;
379 | currentMonth = month;
380 | map.setLayoutProperty(prevMonth, 'visibility', 'none');
381 | map.setLayoutProperty(currentMonth, 'visibility', 'visible');
382 | updateMap();
383 | });
384 | ```
385 |
386 | Important point to note here is that to change months, I simply hide the current layer and show the layer corresponding to the month I want. This creates a break in continuity to change layers, but it ensures that animations within a layer (showing temperature trends) are smooth.
387 |
388 | Next, I initialize the styles selector similarly:
389 |
390 | ```javascript
391 | initSelect('#map-display', Object.keys(styles), currentStyle, handler = function(style){
392 | currentStyle = style;
393 | applyStyles(layerNames, "all");
394 | });
395 | ```
396 | Here I use the `applyStyles` helper to reapply the new style to all layers.
397 |
398 | Next major order of business is setting up event handling for the popups:
399 | ```javascript
400 | map.on('mousemove', function(event) {
401 | // heatmap is too diffuse to display popups, so we return
402 | if(currentStyle == 'heatmap') { popupActive = false; return popup.remove(); }
403 |
404 | // query features at the mouse pointer location
405 | var features = map.queryRenderedFeatures(event.point, {
406 | layers: ['headers', currentMonth]
407 | });
408 |
409 | // if we didn't get any features or we only got 1 (i.e. not enough to get our data), return
410 | if(!features.length || features.length == 1) { popupActive = false; return popup.remove(); }
411 |
412 | // we loop through all the features we found and pull out the info we need
413 | var result = [];
414 | for(i in features){
415 | if('name' in features[i].properties){
416 | result['name'] = features[i].properties.name;
417 | result['coordinates'] = features[i].geometry.coordinates;
418 | }
419 | if(("" + currentIndex) in features[i].properties){
420 | result['temperatures'] = features[i].properties;
421 | }
422 | }
423 |
424 | // if the temperature is -99 at this point, it means the station has missing data
425 | // so we don't want to display a popup because there will not be a dot on the map. Return.
426 | // note again we have to use "" + currentIndex to access our temperature data because
427 | // the indices in GEOJSON features are all strings.
428 | if(result['temperatures']["" + currentIndex] == -99) { popupActive = false; return popup.remove(); }
429 |
430 | // if we've passed all of these checks, we are going to display the popup, so we set it active
431 | popupActive = true;
432 |
433 | // store the currentFeature so we can update temperature (if map is animated)
434 | // without querying again (slow)
435 | currentFeature = result;
436 |
437 | // change mouse cursor
438 | map.getCanvas().style.cursor = 'pointer';
439 |
440 | // display the popup
441 | popup.setLngLat(result['coordinates'])
442 | .addTo(map);
443 | updatePopup();
444 | });
445 |
446 | ```
447 |
448 | I've left the comments in here to explain what it's doing. Main thing to note is that we don't display the popup if we're in heatmap mode (as this wouldn't make sense--how would you know which station you're over?) and that we only display the popup by calling `popup.setLngLat(result['coordinates'])` if we've passed all the checks. If we fail any check (ensuring that the station has data at this time, ensuring that we're in the right mode, etc) we remove the popup by calling `popup.remove()`.
449 |
450 | Also note that since the header layer and the temperature layer are different, we must query two layers as follows:
451 |
452 | ```javascript
453 | var features = map.queryRenderedFeatures(event.point, {
454 | layers: ['headers', currentMonth]
455 | });
456 | ```
457 |
458 | and then load the data out of them using a bit of clever logic:
459 |
460 | ```javascript
461 | var result = [];
462 | for(i in features){
463 | if('name' in features[i].properties){
464 | result['name'] = features[i].properties.name;
465 | result['coordinates'] = features[i].geometry.coordinates;
466 | }
467 | if(("" + currentIndex) in features[i].properties){
468 | result['temperatures'] = features[i].properties;
469 | }
470 | }
471 | ```
472 |
473 | This adds complexity, but keeps the map size smaller. Adding header data to every temperature layer would be wasteful.
474 |
475 | Lastly, I set up event handlers for the other controls:
476 |
477 | ```javascript
478 | $('#playpause').on('click', toggleAnimation);
479 | $('#toggle-anomaly').on('change', function(){
480 | showAnomaly = !showAnomaly;
481 | updateAnomaly();
482 | });
483 | $('#toggle-loop').on('change', function() { loop = !loop; });
484 | $('#info-button').on('click', showInfo);
485 | ```
486 |
487 | That's all there is to it! Seems like a lot going on, but it's basically pretty simple. Just a few wrapper functions to update the proper layers. We just translate a given year to a temperature index, and then tell Mapbox GL to render the map color based on that temperature index.
488 |
489 | ## Running/Modifying the Code
490 | If you're interested in tinkering around with the visualization, first clone the repo:
491 |
492 | ```git clone https://github.com/TedYav/HadCRUT3-Visualization && cd ./HadCRUT3-Visualization```
493 |
494 | There are a few things you can do:
495 |
496 | ###Parse the Data Differently
497 | The parsing script has a few options if you want to mess around on your own. If you just want to reparse the data using the parameters I did, then do the following (assuming you are in the repo directory):
498 |
499 | ```tar -zxvf ./climate-data.tar.gz && python3 parse.py```
500 |
501 | The GeoJSON files will then be in the ```output``` directory.
502 |
503 | I've written a few different options to output the data. If you want to see them run:
504 |
505 | ```python3 parse.py --help```
506 |
507 | ####Recalculate Temperature Anomalies####
508 | There is a script [available here that will calculate temperature anomalies](http://www.metoffice.gov.uk/media/zip/8/k/gridding_and_averaging_code.zip). If you want to run it yourself quickly, run the following command:
509 |
510 | ```
511 | mkdir anomaly && cd ./anomaly && curl http://www.metoffice.gov.uk/media/zip/e/0/station_files.20110720.zip > station_files.zip && unzip -x station_files.zip -d ./station_files && curl http://www.metoffice.gov.uk/media/zip/8/k/gridding_and_averaging_code.zip > grid.zip && unzip -x grid.zip && perl station_gridder.perl | perl make_global_average_ts_ascii.perl > anomaly.txt
512 | ```
513 |
514 | Anomaly data is currently stored directly in `climate-sim.js`.
515 |
516 | ###Change the Map Style
517 | If you want to fundamentally change the style of the map, the best way to do it will be in [Mapbox Studio](https://www.mapbox.com/studio/). You will have to run the parsing script again to reupload the data.
518 |
519 | [Here is the original style I made](https://api.mapbox.com/styles/v1/teomandavid/ciqhsdrro002qcfnn41ofo4f2.html?title=true&access_token=pk.eyJ1IjoidGVvbWFuZGF2aWQiLCJhIjoiY2lwaHBrNnp4MDE2Z3RsbmpxeWVkbXhxMSJ9.rhKrjQ0Eb8iH0inNPQ7W8Q#2.0001085731437036/36.39815027614806/-12.706191133281237/0) in case you want to see it by itself and edit it.
520 |
521 | Make sure you set your layer names to lowercase months (i.e. 'january'), or set the names accordingly in `layerNames` in `climate-sim.js`.
522 |
523 | ###Change Visualization Colors or Display###
524 | The best way to do this is to edit `climate-sim.js`. There are a set of constants at the top of the file that control most display options. If you want to dig in further, edit the code and have fun!
525 |
--------------------------------------------------------------------------------