-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmaps_and_charts.html
More file actions
552 lines (483 loc) · 18 KB
/
maps_and_charts.html
File metadata and controls
552 lines (483 loc) · 18 KB
1
2
3
4
5
6
7
8
9
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Charts and Maps</title>
<link rel="stylesheet" href="style/style.css" />
<script src="js/index.js"></script>
</head>
<body>
<div id="nav-placeholder"></div>
<div class="layout">
<aside class="sidebar">
<h2>Workshop</h2>
<nav>
<ul>
<li><a href="#overview">Overview</a></li>
<li><a href="#chart">TimeSeries Plots</a></li>
<li><a href="#leaflet">Leaflet Map</a></li>
<li><a href="#exercise_1">Mini Exercise</a></li>
<li><a href="#web_service">Web Services</a></li>
<li><a href="#fetch">Fetching Forecast Data</a></li>
<li><a href="#cluster">Customizing the Map Clusters</a></li>
<li><a href="#gradient_markers">Gradient Markers</a></li>
<li><a href="#final_touches">Final Touches</a></li>
<li><a href="#exercise_2">Mini Exercise</a></li>
<li><a href="#next">Next Steps</a></li>
</ul>
</nav>
</aside>
<main class="content">
<header>
<h1>Web Charts and Maps</h1>
<p class="subtitle">Visualize data with interactive charts and maps to communicate spatial and temporal patterns, starting from <a href="examples/lesson_3.zip">lesson_3.zip</a></p>
</header>
<section id="overview">
<h2>Overview</h2>
<p>This workshop covers:</p>
<ul>
<li>Plotting time-series charts with chart.js</li>
<li>Embedding web maps with Leaflet</li>
<li>Using NOAA web services for spatial and forecast data</li>
</ul>
</section>
<section id="chart">
<h2>TimeSeries Plots</h2>
<p>To show the forcasted temperature over time, lets plot it with the chart.js plugin.</p>
<p>First, include <a href="https://www.chartjs.org/docs/latest/">chart.js</a> (and the date adapter) in your HTML (before the index.js):</p>
<pre><code id="code-container2"></code></pre>
<script>
// The raw HTML code you want to display
var rawHTML = `
<script src="https://cdn.jsdelivr.net/npm/chart.js">< /script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns">< /script>
`;
// Set the textContent property to display the code as text
document.getElementById('code-container2').textContent = rawHTML;
</script>
<p>You'll also need a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/canvas">canvas</a> element in your HTML to hold the chart:</p>
<pre><code id="code-container3"></code></pre>
<script>
// The raw HTML code you want to display
var rawHTML = `
<canvas id="chart"></canvas>
`;
// Set the textContent property to display the code as text
document.getElementById('code-container3').textContent = rawHTML;
</script>
<p>Now you can use the JavaScript below to draw a line chart for the high and low forcasted values over time.
</p>
<pre><code>
let chart;
function drawChart(rawData) {
const labels = rawData.map(d => new Date(d.startTime));
const highs = rawData.map(d => d.isDaytime ? d.temperature : null);
const lows = rawData.map(d => !d.isDaytime ? d.temperature : null);
if (chart) {
chart.destroy();
}
chart = new Chart(document.getElementById("chart"), {
type: "line",
data: {
labels,
datasets: [
{ label: "Highs", data: highs, borderColor: "firebrick",spanGaps: true },
{ label: "Lows", data: lows, borderColor: "steelblue",spanGaps: true }
]
},
options: {
scales: {
x: {
type: "time",
time: { unit: "day" }
}
}
}
})
}
</code></pre>
<p>In order for chart to be drawn, you must call the drawChart function with the forecast data.
Let's modify the <i>update_forcast</i> function from the <a href="apis_and_structured_data.html:#fetch">previous lesson</a>
by adding the following function call at the end:</p>
</p>
<pre><code>
drawChart(_forecastData.properties.periods);
</code></pre>
<p>You should now see a chart of the forecasted temperatures over time when the page is loaded.</p>
</section>
<section id="leaflet">
<h2>Interactive web maps</h2>
<p>There are many popular web map plugins, and Leaflet ranks among them.
This lightweight open-source JavaScript library is relatively easy to work with
and there are lots of <a href="https://leafletjs.com/plugins.html">Leaflet plugins</a> available to extend its functionality.</p>
<p>To add Leaflet to your webpage, start with loading both its CSS and JS files (before the index.js):</p>
<pre><code id="code-container0"></code></pre>
<script>
// The raw HTML code you want to display
var rawHTML = `
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<!-- Make sure you put this AFTER Leaflet's CSS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin="">< /script>
`;
// Set the textContent property to display the code as text
document.getElementById('code-container0').textContent = rawHTML;
</script>
<p>Next, initialize the map in your JavaScript code with the following:
Note: You'll need to ensure the DOM is fully loaded before initializing the map; the 'DOMContentLoaded' event works well for this.</p>
</p>
<pre><code>
// leaflet-map.js
const map = L.map('map').setView([39.5, -98.35], 4); // centered on US
</code></pre>
Be sure to also set the map height in your CSS, otherwise it won't be visible:
<pre><code>
#map {
height: 400px;
}
</code></pre>
<p>In testing the above you'll notice the map is grey. To fix this, add the following code:</p>
<pre><code>
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
</code></pre>
<p>See <a href="https://leafletjs.com/examples/quick-start/">Leaflet Quick Start Guide</a> for reference.</p>
</section>
<section id="exercise_1">
<h2>Mini Exercise (3–5 min)</h2>
<p>Choose a different base map</p>
<ul>
<li>Navigate to <a href="https://leaflet-extras.github.io/leaflet-providers/preview/">https://leaflet-extras.github.io/leaflet-providers/preview/</a></i></li>
<li>Choose a different base map provider</li>
<li>Update the <i>tileLayer</i> in your JavaScript code to use the new provider</li>
</ul>
When you're done, save the file and test it in your web browser.
</section>
<section id ="web_service">
<h2>Adding Web Services to the Map</h2>
<p>The last lesson we were able to drill into the <a href="https://www.arcgis.com/home/item.html?id=cb1886ff0a9d4156ba4d2fadd7e8a139">Current Weather and Wind Station Data</a>
from the <a href="https://livingatlas.arcgis.com/en/browse/">ArcGIS Living Atlas of the World</a>,
to reveal the REST Endpoint for the Stations feature service layer</p>
<pre><code >https://services9.arcgis.com/RHVPKKiFTONKtxq3/arcgis/rest/services/NOAA_METAR_current_wind_speed_direction_v1/FeatureServer/0/query?where=1=1&outFields=*&f=geojson
</code></pre>
<p>The code below shows how to load the Current Weather and Wind Station Data to our map.</p>
<i>Note: these file must be loaded before the map initialization code.</i>
<pre><code id="code-container1"></code></pre>
<script>
// The raw HTML code you want to display
var rawHTML = `
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css"/>
<!-- As there are many points, we will use the Leaflet MarkerCluster plugin to group them. -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css"/>
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js">< /script>
`;
// Set the textContent property to display the code as text
document.getElementById('code-container1').textContent = rawHTML;
</script>
️<p>Now we can add the following code to load and display the data:</p>
<i>Note: The code should be placed after the map is initialized.</i>
<pre><code >
// create a clusterGroup to hold the markers
const clusterGroup = L.markerClusterGroup();
fetch('https://services9.arcgis.com/RHVPKKiFTONKtxq3/arcgis/rest/services/NOAA_METAR_current_wind_speed_direction_v1/FeatureServer/0/query?where=1=1&outFields=*&f=geojson')
.then(res => res.json())
.then(data => {
data.features.forEach(f => {
const p = f.properties;
try{
const lat = f.geometry.coordinates[1];
const lng = f.geometry.coordinates[0];
const marker = L.marker([lat, lng], {
icon: temperatureIcon(p.TEMP)
});
// retain the original data in the marker for later use
marker.properties = p
marker.bindPopup(`
Station:${p.STATION_NAME || "METAR Station"}<br/>
State, Country:${p.COUNTRY}<br/>
Wind: ${p.WIND_SPEED} kt @ ${p.WIND_DIRECT}°<br/>
TEMP: ${p.TEMP}<br/>
<a href="#" onclick="get_weather_forcast(${p.LATITUDE}, ${p.LONGITUDE}).then(data => update_forcast(data)); return false;">Show Forecast </a>
`);
clusterGroup.addLayer(marker);
}catch(err){ console.log("skip",err)
}
});
map.addLayer(clusterGroup);
});
function temperatureIcon(tempC) {
// this is a long ternary operator (fancy if/else)
//read as condition ? valueIfTrue : valueIfFalse
const size =
tempC < 0 ? 18 :
tempC < 10 ? 20 :
tempC < 25 ? 24 :
28;
return L.divIcon({
className: "temp-icon",
iconSize: [size, size],
html: `
<div class="temp-marker"
style="background:${tempColor(tempC)}">
${Math.round(tempC)}°
</div>
`
});
}
function tempColor(_temp) {
let temp=fToC(_temp)
return temp <= -10 ? "#313695" :
temp <= 0 ? "#4575b4" :
temp <= 10 ? "#74add1" :
temp <= 20 ? "#fdae61" :
temp <= 30 ? "#f46d43" :
"#a50026";
}
function fToC(f) {
return ((f - 32) * 5) / 9;
}
</code></pre>
<p>The following CSS should also be added to the style.css file:</p>
<pre><code >
.temp-marker {
width: 100%;
height: 100%;
border-radius: 50%;
color: white;
font-size: 11px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
</code></pre>
</section>
<section id="map_legend">
<h2>Adding a Legend to the Map</h2>
<p>The following code demonstrates how to add a legend to the map:</p>
<pre><code>
const tempLegend = L.control({ position: "bottomright" });
tempLegend.onAdd = function () {
const div = L.DomUtil.create("div", "temp-legend");
div.innerHTML = `
<div class="legend-title">Air Temperature (°F)</div>
<div class="legend-gradient"></div>
<div class="legend-labels">
<span>14</span>
<span>32</span>
<span>50</span>
<span>68</span>
<span>86</span>
<span>104+</span>
</div>
`;
return div;
};
tempLegend.addTo(map);
</code></pre>
<p>And add the following CSS rules to the style.css file:</p>
<pre><code>
.temp-legend {
background: white;
padding: 8px 10px;
font-size: 12px;
line-height: 1.2;
box-shadow: 0 0 6px rgba(0,0,0,0.3);
border-radius: 6px;
width: 180px;
}
.legend-title {
font-weight: bold;
margin-bottom: 6px;
text-align: center;
}
.legend-gradient {
height: 14px;
width: 100%;
background: linear-gradient(
to right,
#313695,
#4575b4,
#74add1,
#fdae61,
#f46d43,
#a50026
);
border-radius: 4px;
margin-bottom: 4px;
}
.legend-labels {
display: flex;
justify-content: space-between;
}
</code></pre>
</section>
<section id="cluster">
<h2>Customizing the Map Cluster</h2>
<p>The default cluster styling can be customized by modifying the clusterGroups <i>iconCreateFunction</i> and updating the associated CSS class.
</p>
<p>Replace <i> const clusterGroup = L.markerClusterGroup();</i> from the earlier code with:</p>
<pre><code>
const clusterGroup = L.markerClusterGroup({
maxClusterRadius: 40,
iconCreateFunction: function (cluster) {
const markers = cluster.getAllChildMarkers();
let sumTemp = 0;
markers.forEach(m => {
sumTemp += m.properties.TEMP;
});
const avgTemp = sumTemp / markers.length;
return temperatureClusterIcon(avgTemp, markers.length);
}
});
function temperatureClusterIcon(avgTemp, count) {
const size =
count < 10 ? 35 :
count < 50 ? 45 :
55;
return L.divIcon({
className: "temp-cluster",
iconSize: [size, size],
html: `
<div class="temp-cluster-wrapper"
style="background:${tempColor(avgTemp)}">
<div class="temp-cluster-value">
${Math.round(avgTemp)}°
</div>
<div class="temp-cluster-count">
${count}
</div>
</div>
`
});
}
</code></pre>
<p>And add the following CSS rules to the style.css file:</p>
<pre><code>
.temp-cluster-wrapper {
width: 100%;
height: 100%;
border-radius: 50%;
color: white;
font-weight: bold;
text-align: center;
position: relative;
}
.temp-cluster-value {
font-size: 14px;
line-height: 1.2;
}
.temp-cluster-count {
font-size: 10px;
opacity: 0.85;
}
.temp-marker,
.temp-cluster-wrapper {
border: 1px solid rgba(255,255,255,0.6);
}
</code></pre>
</section>
<section id="gradient_markers">
<h2>Gradient Markers</h2>
<p>To make the markers display a gradient color based on temperature, add the following JavaScript:</p>
<pre><code>
const TEMP_COLOR_STOPS = [
{ t: -10, color: "#313695" },
{ t: 0, color: "#4575b4" },
{ t: 10, color: "#74add1" },
{ t: 20, color: "#fdae61" },
{ t: 30, color: "#f46d43" },
{ t: 40, color: "#a50026" }
];
function tempColor(_temp) {
let temp=fToC(_temp)
// clamp
if (temp <= TEMP_COLOR_STOPS[0].t) {
return TEMP_COLOR_STOPS[0].color;
}
if (temp >= TEMP_COLOR_STOPS[TEMP_COLOR_STOPS.length - 1].t) {
return TEMP_COLOR_STOPS[TEMP_COLOR_STOPS.length - 1].color;
}
// find surrounding stops
for (let i = 0; i < TEMP_COLOR_STOPS.length - 1; i++) {
const a = TEMP_COLOR_STOPS[i];
const b = TEMP_COLOR_STOPS[i + 1];
if (temp >= a.t && temp <= b.t) {
const t = (temp - a.t) / (b.t - a.t);
const c1 = hexToRgb(a.color);
const c2 = hexToRgb(b.color);
return rgbToHex({
r: lerp(c1.r, c2.r, t),
g: lerp(c1.g, c2.g, t),
b: lerp(c1.b, c2.b, t)
});
}
}
}
function hexToRgb(hex) {
const v = hex.replace("#", "");
return {
r: parseInt(v.substring(0, 2), 16),
g: parseInt(v.substring(2, 4), 16),
b: parseInt(v.substring(4, 6), 16)
};
}
function rgbToHex({ r, g, b }) {
return (
"#" +
[r, g, b]
.map(v => Math.round(v).toString(16).padStart(2, "0"))
.join("")
);
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
</code></pre>
The complete code for this example can be seen at <a href="examples/lesson_4/">lesson 4</a>, and can be download from <a href="examples/lesson_4.zip">lesson_4.zip</a>.
</section>
<section id="final_touches">
<h2>Final Touches</h2>
<p>There are lots more aesthetic tweaks and fixes you could add to your web app</p>
<p>Below are some ideas</p>
<ul>
<li>Improve color palette. Most websites utilize colors that match their brand for consistency</li>
<li>Spacing of elements on a web page can help with legitibility</li>
<li>Labeling can help ensure information is accurately presented</li>
<li>Testing with different browser window sizes is important too. In our case it reveals that the map disappears in mobile view.</li>
</ul>
The following CSS fixes the last bullt:
<pre><code>
#map-container{
min-height: 400px;
}
</code></pre>
</section>
<section id="exercise_2">
<h2>Mini Exercise (5–7 min)</h2>
<p>Add a label below the tab buttons that updates to
correspond with the weather forcast</p>
<ul>
<li>Create a new HTML element to house your label</li>
<li>Add JavaScript code to set the text of the new element</li>
</ul>
When you're done, save the file and test it in your web browser.
</section>
<section id="next">
<h2>Next Steps</h2>
<p>We hope this tutorial has helped you understand how to build interactive web applications using HTML, CSS, and JavaScript.
<br/> You can now build upon this foundation to create more complex and feature-rich web applications.</p>
</section>
<footer>
Homework: From <a href="https://colostate-my.sharepoint.com/:w:/g/personal/kaworth_colostate_edu/IQD-Ek8LDhBqTZWcCkIuDPPCAaYqu3OVDHnDz9MgD64tVCk?e=0RdwIt">Getting Started With Web Design</a>
and complete part: 5) Think in Sections, Not Pages and 6) Only Now: Start Coding.
</footer>
</main>
</div>
</body>
</html>