The weather forecast widget displays up to a 15-day weather forecast in a horizontal, scrollable layout. The forecast widget uses data from the Weather API along with HTML, Javascript and CSS technologies to create the widget.
Source code
Full source code for the widget can be found on our Github.
Technologies – Weather API, HTML, CSS, JavaScript & D3 visualization library
We will use JavaScript, HTML, SVG and CSS technologies to display the forecast within the page. We will also use the D3 Javascript library to provide help in manipulating the HTML and SVG structure. As we are using the D3 library for the visualization features, we will also use the D3 utility functions for features such as data loading, date formatting etc.
Retrieving the weather forecast data using the weather API
We will be using the Visual Crossing Weather API to retrieve the weather forecast data. The API allows retrieval of weather forecast data as well as historical weather records and it has been designed with web based applications and web pages such as this widget in mind. To see full details on how to construct Weather API queries, head over to How do I add the weather forecast to my web page?.
Constructing the widget
From the widget install guide, we can see that the basic structure to include the widget is as follows:
<div class="weatherWidget" style="background-color: #ffffff99;height: 175px;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<link rel="stylesheet" href="/widgets/forecast/weather-forecast-widget-d3.css">
<script>
window.weatherWidgetConfig = window.weatherWidgetConfig || [];
window.weatherWidgetConfig.push({
selector:".weatherWidget",
apiKey:"YOUR_API_KEY",
location:"_auto_",
unitGroup:"us"
});
(function() {
var d = document, s = d.createElement('script');
s.src = 'https://www.visualcrossing.com/widgets/forecast/weather-forecast-widget-d3.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
There are four main elements to code. The first is the div element that will contain the weather forecast widget. This can be placed anywhere on the page.
<!-- the DIV that will contain the widget -->
<div class="weatherForecastWidget"></div>
The second section of code includes two files. The first is the d3 library and the second is the CSS code for arranging and formatting the widget. The latter file can be customized along with the Javascript code that we will discuss below.
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<link rel="stylesheet" href="/widgets/forecast/weather-forecast-widget-d3.css">
The third section is the configuration. Note that the configuration is contained in a page level scope array called ‘weatherWidgetConfig’. This allows for multiple widgets on the same page.
window.weatherWidgetConfig = window.weatherWidgetConfig || [];
window.weatherWidgetConfig.push({
selector:".weatherForecastWidget",
apiKey:"YOUR_API_KEY",
location:null,
unitGroup:"us"
});
The final section includes the widget Javascript file itself. It is loaded like this to avoid blocking the initial page load.
(function() {
var d = document, s = d.createElement("script");
s.src = "https://www.visualcrossing.com/widgets/forecast/weather-forecast-widget-d3.js";
s.setAttribute("data-timestamp", +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
The widget libary code
We will now look at the implementation of the weather-forecast-widget-d3.js file.
The widget implements a WeatherForecastWidget class which accepts the configuration parameters from above as an input. The skeleton of the class is as follows. The initConfig instance is passed from the boostrap code to set the initial parameters. These are used to set the widget config instance. Note that code checks the browser localstorage to see if the widget location has been set before – it will use this browser saved location in preference to other. If the location is not set or is set to ‘_auto_’ then no explicit location is sent to the API. In this case the server will attempt to geolocate the user of the web page based upon their IP address.
<pre>
function WeatherForecastWidget(initConfig) {
//the root HTML tag selector
this.rawSelector=initConfig.selector;
if ((!initConfig.location || initConfig.location==="_auto_") && localStorage) {
initConfig.location=localStorage.getItem("loc");
}
if (!initConfig.location) initConfig.location="_auto_";
//Initialize the widget using the container parameters
this.config={
"location":initConfig.location, //initial location
"unitGroup":initConfig.unitGroup || "us", //initial location
"key":initConfig.apiKey, //api key
"hourly":false, //whether data should be shown hourly (if not then show daily)
"showInstallLink":initConfig.showInstallLink
}
this.dailydata=null;
this.hourlydata=null;
this.error=null;
var me=this;
//setLocation - updates the location and triggers a data reload
this.setLocation=function(location) {
me.config.location=location;
me.dailydata=null;
me.hourlydata=null;
me.loadForecastData();
}
//constructs Weather API request and then loads the weather forecast data from the Weather API
this.loadForecastData=function() {
...
}
//displays the weather data inside the container tag
this.refresh=function() {
...
}
}
</pre>
Two additional methods are included in this skeleton. The loadForecastData method loads weather forecast data using the Weather API techniques we discussed above. The refresh method constructs the HTML, SVG and CSS styling necessary to create the display.
The loadForecastData method
The loadForecastData method is responsible for loading the actual forecast data. This method is called when the location is changed and after the users switches between hourly and daily view. The method checks if the required data is already loaded and so will exit if nothing has changed. For example if the user switches between daily, hourly and then back to daily view. When a user changes the locatoin, the data is cleared and so this will require a reload of the data.
<pre>
this.loadForecastData=function() {
//abandon loading data if an error has been recorded
if ( me.error) return;
if ((me.config.hourly && me.hourlydata) || (!me.config.hourly && me.dailydata)) return;
//endpoint
var uri=(initConfig.root || "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/weatherdata/");
uri+='forecast?';
//parameters
uri+="unitGroup="+me.config.unitGroup+"&contentType=json&shortColumnNames=true&location="+me.config.location+"&key="+me.config.key;
uri+="&aggregateHours="+(me.config.hourly?"1":"24")
d3.json(uri, function(err, rawResult) {
//simple error handling for various error cases
if (err) {
console.error("Error loading weather forecast data: "+err);
me.error="Error loading data";
} else if (!rawResult) {
console.error("Error loading weather forecast data (no data)");
me.error="Error loading data";
} else if (rawResult.errorCode) {
console.error("Error loading weather forecast data: "+rawResult.message)
me.error="Error loading data";
} else {
//process and populate the data
me.processValues(rawResult);
if (me.config.hourly) {
me.hourlydata=rawResult;
} else {
me.dailydata=rawResult;
}
}
//refresh the display
me.refresh();
});
}
</pre>
The method uses the d3.json function to submit the data and call the callback function. After performing a number of error checks then method processes the forecast values and calls refresh to update the display.
The error handling is worth reviewing. The initial ‘err’ variable is passed by d3 when the network call returns anything but a HTTP success. A failure at this level indicates a bad server name, network configuration or something that prevents the server responding cleanly. If the rawResult from the Weather API, then this suggest the call completed but returned an error. In that case the result JSON object (rawResult) will likely include additional information in the message property.
Processing the weather forecast result
Two methods are provided to process the returned weather forecast data. getForecastValues extracts the array of forecast periods from the return value. The weather forecast data is a JSON structure (it’s possible to also request the data in CSV format). See Weather API JSON result structure for more information. The structure allows for multiple locations however in our simple widget we support a single location so the getForecastValues method simple returns the first location in the list. The code filters out any null forecast values from the list and returns the array of forecast data.
//checks the forecast values and create Date instances for the data time values
this.processValues=function(data) {
var forecastValues=me.getForecastValues(data);
if (!forecastValues) return;
var current=new Date();
var offset=current.getTimezoneOffset()*60000;
forecastValues.forEach(function(d) {
d.datetime=new Date(d.datetime+offset );
});
}
//extracts the array of forecast values representing each time period in the data
this.getForecastValues=function(data) {
if (!data) {
throw "No data available for "+me.config.location;
}
var locations=data.locations;
if (!locations) {
throw "No locations found in data for "+me.config.location;
}
var locationsIds=Object.keys(locations);
var locationData=locations[locationsIds[0]];
if (locationData && locationData.address) {
if (localStorage) localStorage.setItem("loc", locationData.address);
me.config.location=locationData.address;
}
var forecastValues=locationData.values;
forecastValues=forecastValues.filter(function(d) {
return d && (d.temp || d.temp===0);
});
return forecastValues;
}
Generating the widget display
The refresh method is responsible for updating the display.
This is constructed from HTML, SVG and static images.
We’ve added comments on the GitHub to help guide through the sample so we won’t go line by line.
Here are some of the highlights.
Main template structure
The main template structure shows that the widget is div element with a set of vertically arranged elements via a flexbox. The first row displays the location and time period chooser links. The second row contains the individual time period elements. Each of the periods are built as children div elements under the ‘.days’ element (they may be hours or days). The SVG chart is built under the .chart element and finally the footer displays the option install link and credits link.
<div class="location"><span class="value">-</span><input type="text" class="editor" value=""></input>"+
"<div class="viewchooser day" title="View by day">Daily</div>"+
"<div class="viewchooser hour" title="View by hour">Hourly</div>"+
"</div>"+
"<div class="days noselect">"+
"</div>"+
"<svg class="chart"></svg>"+
"<div class="footer">"+
(showInstallLink?"<a href="/weather-widgets" title="Install this weather widget" target="_blank">Install</a>":"")+
"<a href="/?page_id=5455" title="Powered by the Visual Crossing Weather API" target="_blank">Credit</a>"+
"</div>"
Day and time display
Our previous article, How do I add the weather forecast to my web page?, discussed building the HTML for the days and hours display. We will not go into detail here. The only real difference is that now we use images rather than fonts for the weather icons.
D3 Chart display
The d3 chart consists of two elements. An area fill chart that represents the temperature for the day and a precipitation bar chart showing the expected rainfall for each time period. The day HTML display plus the two charts are scaled on the same axis so that they align. As the user pans from left to right, all three elements move together.
The area chart is creating by creating an area function based upon the temperature value of each of the time periods. The x (horizontal) value represents the time of the weather. For most periods, we want the chart to plot the temperature at the center of the period so we add 0.5 hours for hourly view and 12 hours for daily. We special case the first and last period so that the chart plots from the start of the first day to the end of the last day.
The vertical value of the temperature plot is the temperature itself. For daily view, we plot the maximum temperature of the day. For hourly view we view the expected temperature for that hour.
//area creation function for the temperature
var temparea = d3.area()
.x(function(d, i) {
if (i===0) { //first period
return x(d.datetime)
} else if (i===forecastValues.length-1) { //last period
return x(me.config.hourly?d3.timeHour.offset( d.datetime,1):d3.timeHour.offset( d.datetime,24));
} else {
return x(me.config.hourly?d3.timeHour.offset( d.datetime,0.5):d3.timeHour.offset( d.datetime,12));
}
})
.y0(height)
.y1(function(d) { return y(me.config.hourly?d.temp:d.maxt); })
.curve(d3.curveCatmullRom)
The other feature of the temperature area chart is the gradient color. This color highlights the temperature by showing warmer (orange and red) colors for warmer temperatures and cooler colors (green and blue) for cooler temperatures. To do this we use a color function called turbo from the d3 chromatic library. This gives us a cool to warm temperature gradient. The turbo method returns a color between cool and warm given a number between 0 and 1. We want the temperatures of 0 to 100F to represent this color range (colors outside of the this range will be locked to 0 or 1). By creating a gradient like this we will be consistent in choosing the color for a given temperature.
The gradient is created by taking the minimum and maximum of the temperature range of the weather forecast. We then create 10 color stops for the gradient from the minimum to maximum temperature. The color is looked up from the turbo color based on the position between 0 and 100F.
//create a gradient that maps the colors to particular temperature
//in this case we will use the turbo colors from d3 and interpolate them between 255 and 311K (approx 0-100F)
var colorStopCount=10, minTempStop=255, maxTempStop=311;
var minTempK=toK(y.domain()[0]),
maxTempK=toK(y.domain()[1]);
var tempInterval= (maxTempK-minTempK)/(colorStopCount+1);
var colors=[];
for (var i=0;i<=colorStopCount;i++) {
//for each gradient stop, find the color in the the turbo palette based on the min and max temp stops
var t=((minTempK+tempInterval*i)-minTempStop)/(maxTempStop-minTempStop);
t=Math.max(t,0);
t=Math.min(t,1);
colors.push({offset: i*(100/colorStopCount)+"%", color:turbo(t)});
}