Web Charts and Maps
Visualize data with interactive charts and maps to communicate spatial and temporal patterns, starting from lesson_3.zip
Overview
This workshop covers:
- Plotting time-series charts with chart.js
- Embedding web maps with Leaflet
- Using NOAA web services for spatial and forecast data
TimeSeries Plots
To show the forcasted temperature over time, lets plot it with the chart.js plugin.
First, include chart.js (and the date adapter) in your HTML (before the index.js):
You'll also need a canvas element in your HTML to hold the chart:
Now you can use the JavaScript below to draw a line chart for the high and low forcasted values over time.
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" }
}
}
}
})
}
In order for chart to be drawn, you must call the drawChart function with the forecast data. Let's modify the update_forcast function from the previous lesson by adding the following function call at the end:
drawChart(_forecastData.properties.periods);
You should now see a chart of the forecasted temperatures over time when the page is loaded.
Interactive web maps
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 Leaflet plugins available to extend its functionality.
To add Leaflet to your webpage, start with loading both its CSS and JS files (before the index.js):
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.
// leaflet-map.js
const map = L.map('map').setView([39.5, -98.35], 4); // centered on US
Be sure to also set the map height in your CSS, otherwise it won't be visible:
#map {
height: 400px;
}
In testing the above you'll notice the map is grey. To fix this, add the following code:
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
See Leaflet Quick Start Guide for reference.
Mini Exercise (3–5 min)
Choose a different base map
- Navigate to https://leaflet-extras.github.io/leaflet-providers/preview/
- Choose a different base map provider
- Update the tileLayer in your JavaScript code to use the new provider
Adding Web Services to the Map
The last lesson we were able to drill into the Current Weather and Wind Station Data from the ArcGIS Living Atlas of the World, to reveal the REST Endpoint for the Stations feature service layer
https://services9.arcgis.com/RHVPKKiFTONKtxq3/arcgis/rest/services/NOAA_METAR_current_wind_speed_direction_v1/FeatureServer/0/query?where=1=1&outFields=*&f=geojson
The code below shows how to load the Current Weather and Wind Station Data to our map.
Note: these file must be loaded before the map initialization code.
️Now we can add the following code to load and display the data:
Note: The code should be placed after the map is initialized.
// 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;
}
The following CSS should also be added to the style.css file:
.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;
}
Adding a Legend to the Map
The following code demonstrates how to add a legend to the map:
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);
And add the following CSS rules to the style.css file:
.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;
}
Customizing the Map Cluster
The default cluster styling can be customized by modifying the clusterGroups iconCreateFunction and updating the associated CSS class.
Replace const clusterGroup = L.markerClusterGroup(); from the earlier code with:
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>
`
});
}
And add the following CSS rules to the style.css file:
.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);
}
Gradient Markers
To make the markers display a gradient color based on temperature, add the following JavaScript:
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;
}
The complete code for this example can be seen at lesson 4, and can be download from lesson_4.zip.
Final Touches
There are lots more aesthetic tweaks and fixes you could add to your web app
Below are some ideas
- Improve color palette. Most websites utilize colors that match their brand for consistency
- Spacing of elements on a web page can help with legitibility
- Labeling can help ensure information is accurately presented
- Testing with different browser window sizes is important too. In our case it reveals that the map disappears in mobile view.
#map-container{
min-height: 400px;
}
Mini Exercise (5–7 min)
Add a label below the tab buttons that updates to correspond with the weather forcast
- Create a new HTML element to house your label
- Add JavaScript code to set the text of the new element
Next Steps
We hope this tutorial has helped you understand how to build interactive web applications using HTML, CSS, and JavaScript.
You can now build upon this foundation to create more complex and feature-rich web applications.