#encoding UTF-8
#import datetime
#if "-" in str($station.stn_info.latitude_f)
#set global $hemisphere = "southern-hemisphere"
#else
#set global $hemisphere = "northern-hemisphere"
#end if
var pages = ["graphs", "records", "reports", "about", "pi"];
var pageName = "";
// If this page we're on now is listed as a subpage, use ".." to get to the relative root
function get_relative_url() {
var sPath = window.location.pathname.replace(/\/$/, "");
pageName = sPath.substring(sPath.lastIndexOf('/') + 1);
if ( pages.includes( pageName ) ) {
var relative_url = "..";
} else {
var relative_url = ".";
}
belchertown_debug("URL: Relative URL is: " + relative_url);
return relative_url;
}
// Determine if debug is on via URL var or config setting
if ( getURLvar("debug") && ( getURLvar("debug") == "true" || getURLvar("debug") == "1" ) ) {
var belchertown_debug_config = true;
belchertown_debug("Debug: URL debug variable enabled");
} else {
var belchertown_debug_config = $belchertown_debug;
belchertown_debug("Debug: skin.conf belchertown_debug enabled");
}
var moment_locale = "$system_locale_js";
moment.locale(moment_locale);
var graphgroups_raw = $charts;
var graphgroups_titles = $graphpage_titles;
var graphpage_content = $graphpage_content;
function belchertown_debug(message) {
if (belchertown_debug_config > 0) {
console.log(message);
}
}
jQuery(document).ready(function() {
// Bootstrap hover tooltips
jQuery(function () {
jQuery('[data-toggle="tooltip"]').tooltip()
})
// If the visitor has overridden the theme, keep that theme going throughout the full site and their visit.
if ( sessionStorage.getItem('theme') == "toggleOverride" ) {
belchertown_debug("Theme: sessionStorage override in place.");
changeTheme( sessionStorage.getItem('currentTheme') );
}
// Change theme if a URL variable is set
if ( window.location.search.indexOf('theme') ) {
if (window.location.search.indexOf('?theme=dark') === 0) {
belchertown_debug("Theme: Setting dark theme because of URL override");
changeTheme("dark", true);
} else if (window.location.search.indexOf('?theme=light') === 0) {
belchertown_debug("Theme: Setting light theme because of URL override");
changeTheme("light", true);
} else if (window.location.search.indexOf('?theme=auto') === 0) {
belchertown_debug("Theme: Setting auto theme because of URL override");
sessionStorage.setItem('theme', 'auto')
#if $almanac.sunrise.raw is not None and $almanac.sunset.raw is not None
autoTheme(#echo datetime.datetime.fromtimestamp($almanac.sunset.raw).strftime('%H')#, #echo datetime.datetime.fromtimestamp($almanac.sunset.raw).strftime('%M')#, #echo datetime.datetime.fromtimestamp($almanac.sunrise.raw).strftime('%H')#, #echo datetime.datetime.fromtimestamp($almanac.sunrise.raw).strftime('%M')#)
#end if
}
}
#if $Extras.has_key('theme_toggle_enabled') and $Extras.theme_toggle_enabled == '1'
// Dark mode checkbox toggle switcher
try {
document.getElementById('themeSwitch').addEventListener('change', function(event) {
belchertown_debug("Theme: Toggle button changed");
(event.target.checked) ? changeTheme("dark", true) : changeTheme("light", true);
});
} catch(err) {
// Silently exit
}
#end if
// After charts are loaded, if an anchor tag is in the URL, let's scroll to it
jQuery(window).on('load', function() {
var anchor_tag = location.hash.replace('#','');
if ( anchor_tag != '' ) {
// Scroll the webpage to the chart. The timeout is to let jQuery finish appending the outer div so the height of the page is completed.
setTimeout(function() {
jQuery('html, body').animate({ scrollTop: jQuery('#'+anchor_tag).offset().top }, 500);
}, 500);
}
});
});
#if $Extras.has_key("theme") and $Extras.theme == 'auto'
// Run this on every page for dark mode if skin theme is auto
ajaxweewx();
#end if
// Disable AJAX caching
jQuery.ajaxSetup({
cache:false
});
// Get the URL variables. Source: https://stackoverflow.com/a/26744533/1177153
function getURLvar(k) {
var p={};
location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){p[k]=v});
return k?p[k]:p;
}
// http://stackoverflow.com/a/14887961/1177153
var weatherdirection = $obs.label.graphs_windDir_ordinals;
// Change the color of the outTemp_F variable
function get_outTemp_color( unit, outTemp, returnColor=false ) {
outTemp = parseFloat( outTemp ).toFixed(0); // Convert back to decimal literal
if ( unit == "degree_F" ) {
if ( outTemp <= 0 ) {
var outTemp_color = "#1278c8";
} else if ( outTemp <= 25 ) {
var outTemp_color = "#30bfef";
} else if ( outTemp <= 32 ) {
var outTemp_color = "#1fafdd";
} else if ( outTemp <= 40 ) {
var outTemp_color = "rgba(0,172,223,1)";
} else if ( outTemp <= 50 ) {
var outTemp_color = "#71bc3c";
} else if ( outTemp <= 55 ) {
var outTemp_color = "rgba(90,179,41,0.8)";
} else if ( outTemp <= 65 ) {
var outTemp_color = "rgba(131,173,45,1)";
} else if ( outTemp <= 70 ) {
var outTemp_color = "rgba(206,184,98,1)";
} else if ( outTemp <= 75 ) {
var outTemp_color = "rgba(255,174,0,0.9)";
} else if ( outTemp <= 80 ) {
var outTemp_color = "rgba(255,153,0,0.9)";
} else if ( outTemp <= 85 ) {
var outTemp_color = "rgba(255,127,0,1)";
} else if ( outTemp <= 90 ) {
var outTemp_color = "rgba(255,79,0,0.9)";
} else if ( outTemp <= 95 ) {
var outTemp_color = "rgba(255,69,69,1)";
} else if ( outTemp <= 110 ) {
var outTemp_color = "rgba(255,104,104,1)";
} else if ( outTemp >= 111 ) {
var outTemp_color = "rgba(218,113,113,1)";
}
} else if ( unit == "degree_C" ) {
if ( outTemp <= 0 ) {
var outTemp_color = "#1278c8";
} else if ( outTemp <= -3.8 ) {
var outTemp_color = "#30bfef";
} else if ( outTemp <= 0 ) {
var outTemp_color = "#1fafdd";
} else if ( outTemp <= 4.4 ) {
var outTemp_color = "rgba(0,172,223,1)";
} else if ( outTemp <= 10 ) {
var outTemp_color = "#71bc3c";
} else if ( outTemp <= 12.7 ) {
var outTemp_color = "rgba(90,179,41,0.8)";
} else if ( outTemp <= 18.3 ) {
var outTemp_color = "rgba(131,173,45,1)";
} else if ( outTemp <= 21.1 ) {
var outTemp_color = "rgba(206,184,98,1)";
} else if ( outTemp <= 23.8 ) {
var outTemp_color = "rgba(255,174,0,0.9)";
} else if ( outTemp <= 26.6 ) {
var outTemp_color = "rgba(255,153,0,0.9)";
} else if ( outTemp <= 29.4 ) {
var outTemp_color = "rgba(255,127,0,1)";
} else if ( outTemp <= 32.2 ) {
var outTemp_color = "rgba(255,79,0,0.9)";
} else if ( outTemp <= 35 ) {
var outTemp_color = "rgba(255,69,69,1)";
} else if ( outTemp <= 43.3 ) {
var outTemp_color = "rgba(255,104,104,1)";
} else if ( outTemp >= 43.4 ) {
var outTemp_color = "rgba(218,113,113,1)";
}
}
// Return the color value if requested, otherwise just set the div color
if ( returnColor ) {
return outTemp_color;
} else {
jQuery(".outtemp_outer").css( "color", outTemp_color );
}
}
// Change the color of the Windspeed variable according to US-EPA standards
// (adjusted to match skin colors better)
function get_windSpeed_color(windSpeed, returnColor = false) {
if (windSpeed <= 0) {
var windSpeed_color = "rgba(0,255,0,1)";
} else if (windSpeed >= 19.9) {
var windSpeed_color = "rgba(126,255,0,1)";
} else if (windSpeed >= 39.9) {
var windSpeed_color = "rgba(0,255,0,1)";
} else if (windSpeed >= 59.9) {
var windSpeed_color = "rgba(255,128,0,1)";
} else if (windSpeed >= 79.9) {
var windSpeed_color = "rgba(255,0,0,1)";
} else if (windSpeed >= 99.9) {
var windSpeed_color = "rgba(255,0,120,1)";
} else if (windSpeed >= 118.0) {
var windSpeed_color = "rgba(255,1,113,1)";
}
// Return the color value if requested, otherwise just set the div color
if (returnColor) {
return windSpeed_color;
} else {
jQuery(".curwindspeed").css("color", windSpeed_color);
}
}
// Change the color of the aqi variable according to US-EPA standards
// (adjusted to match skin colors better)
function get_windGust_color(windGust, returnColor = false) {
if (windGust <= 0) {
var windGust_color = "rgba(0,255,0,1)";
} else if (windGust >= 19.9) {
var windGust_color = "rgba(126,255,0,1)";
} else if (windGust >= 39.9) {
var windGust_color = "rgba(0,255,0,1)";
} else if (windGust >= 59.9) {
var windGust_color = "rgba(255,128,0,1)";
} else if (windGust >= 79.9) {
var windGust_color = "rgba(255,0,0,1)";
} else if (windGust >= 99.9) {
var windGust_color = "rgba(255,0,120,1)";
} else if (windGust >= 118.0) {
var windGust_color = "rgba(255,1,113,1)";
}
// Return the color value if requested, otherwise just set the div color
if (returnColor) {
return windGust_color;
} else {
jQuery(".curwindgust").css("color", windGust_color);
}
}
function highcharts_tooltip_factory(obsvalue, point_obsType, highchartsReturn=false, rounding, mirrored=false, numberFormat) {
// Mirrored values have the negative sign removed
if ( mirrored ) {
obsvalue = Math.abs( obsvalue );
}
if ( point_obsType == "windDir" ) {
if (obsvalue >= 0 && obsvalue <= 11.25) {
ordinal = "$ordinate_names[0]"; // N
} else if (obsvalue >= 11.26 && obsvalue <= 33.75) {
ordinal = "$ordinate_names[1]"; // NNE
} else if (obsvalue >= 33.76 && obsvalue <= 56.25) {
ordinal = "$ordinate_names[2]"; // NE
} else if (obsvalue >= 56.26 && obsvalue <= 78.75) {
ordinal = "$ordinate_names[3]"; // ENE
} else if (obsvalue >= 78.76 && obsvalue <= 101.25) {
ordinal = "$ordinate_names[4]"; // E
} else if (obsvalue >= 101.26 && obsvalue <= 123.75) {
ordinal = "$ordinate_names[5]"; // ESE
} else if (obsvalue >= 123.76 && obsvalue <= 146.25) {
ordinal = "$ordinate_names[6]"; // SE
} else if (obsvalue >= 146.26 && obsvalue <= 168.75) {
ordinal = "$ordinate_names[7]"; // SSE
} else if (obsvalue >= 168.76 && obsvalue <= 191.25) {
ordinal = "$ordinate_names[8]"; // S
} else if (obsvalue >= 191.26 && obsvalue <= 213.75) {
ordinal = "$ordinate_names[9]"; // SSW
} else if (obsvalue >= 213.76 && obsvalue <= 236.25) {
ordinal = "$ordinate_names[10]"; // SW
} else if (obsvalue >= 236.26 && obsvalue <= 258.75) {
ordinal = "$ordinate_names[11]"; // WSW
} else if (obsvalue >= 258.76 && obsvalue <= 281.25) {
ordinal = "$ordinate_names[12]"; // W
} else if (obsvalue >= 281.26 && obsvalue <= 303.75) {
ordinal = "$ordinate_names[13]"; // WNW
} else if (obsvalue >= 303.76 && obsvalue <= 326.25) {
ordinal = "$ordinate_names[14]"; // NW
} else if (obsvalue >= 326.26 && obsvalue <= 348.75) {
ordinal = "$ordinate_names[15]"; // NNW
} else if (obsvalue >= 348.76 && obsvalue <= 360) {
ordinal = "$ordinate_names[0]"; // N
}
// highchartsReturn returns the full wind direction string for highcharts tooltips. e.g "NNW (337)"
if ( highchartsReturn ) {
output = ordinal + " ("+ Math.round(obsvalue) + "\xBA)";
} else {
output = ordinal;
}
} else {
try {
// Setup any graphs.conf overrides on formatting
var { decimals, decimalPoint, thousandsSep } = numberFormat;
// Try to apply the highcharts numberFormat for locale awareness. Use rounding from weewx.conf StringFormats.
// -1 is set from Python to notate no rounding data available and decimals from graphs.conf is undefined.
if ( rounding == "-1" && typeof decimals === "undefined" ) {
output = Highcharts.numberFormat(obsvalue);
} else {
// If the amount of decimal is defined, use that instead since rounding is provided to the function.
if ( typeof decimals !== "undefined" ) {
rounding = decimals;
}
// If decimalPoint is undefined, use the auto detect from the skin since this comes from the skin.
if ( typeof decimalPoint === "undefined" ) {
decimalPoint = "$highcharts_decimal";
}
// If thousandsSep is undefined, use the auto detect from the skin since this comes from the skin.
if ( typeof thousandsSep === "undefined" ) {
thousandsSep = "$highcharts_thousands";
}
output = Highcharts.numberFormat(obsvalue, rounding, decimalPoint, thousandsSep);
}
} catch(err) {
// Fall back to just returning the highcharts point number value, which is a best guess.
output = Highcharts.numberFormat(obsvalue);
}
}
return output;
}
// Handle wind arrow rotation with the ability to "rollover" past 0
// without spinning back around. e.g 350 to 3 would normally spin back around
// https://stackoverflow.com/a/19872672/1177153
function rotateThis(newRotation) {
if ( newRotation == "N/A") { return; }
belchertown_debug("rotateThis: rotating to " + newRotation);
var currentRotation;
finalRotation = finalRotation || 0; // if finalRotation undefined or 0, make 0, else finalRotation
currentRotation = finalRotation % 360;
if ( currentRotation < 0 ) { currentRotation += 360; }
if ( currentRotation < 180 && (newRotation > (currentRotation + 180)) ) { finalRotation -= 360; }
if ( currentRotation >= 180 && (newRotation <= (currentRotation - 180)) ) { finalRotation += 360; }
finalRotation += (newRotation - currentRotation);
jQuery(".wind-arrow").css( "transform", "rotate(" + finalRotation + "deg)" );
jQuery(".arrow").css( "transform", "rotate(" + finalRotation + "deg)" );
}
// Title case strings. https://stackoverflow.com/a/45253072/1177153
function titleCase(str) {
return str.toLowerCase().split(' ').map(function(word) {
return word.replace(word[0], word[0].toUpperCase());
}).join(' ');
}
function autoTheme(sunset_hour, sunset_min, sunrise_hour, sunrise_min) {
// First check if ?theme= is in URL. If so, bail out and do not change anything.
if ( getURLvar("theme") && getURLvar("theme") != "auto" ) {
belchertown_debug("Auto theme: theme override detected in URL. Skipping auto theme");
return true;
}
belchertown_debug("Auto theme: checking to see if theme needs to be switched");
var d = new Date();
var nowHour = d.getHours();
var nowMinutes = d.getMinutes();
nowHour = nowHour;
sunrise_hour = sunrise_hour;
sunset_hour = sunset_hour;
// Determine if it's day time. https://stackoverflow.com/a/14718577/1177153
if ( sunrise_hour <= nowHour && nowHour < sunset_hour ) {
dayTime = true;
} else {
dayTime = false;
}
belchertown_debug("Auto theme: sunrise: " + sunrise_hour);
belchertown_debug("Auto theme: now: " + nowHour);
belchertown_debug("Auto theme: sunset: " + sunset_hour);
belchertown_debug("Auto theme: are we in daylight hours: " + dayTime);
belchertown_debug("Auto theme: sessionStorage.getItem('theme') = " + sessionStorage.getItem('theme'));
if ( dayTime ) {
// Day time, set light if needed
if ( document.body.classList.contains("dark") ) {
if ( sessionStorage.getItem('theme') == "auto" ) {
belchertown_debug("Auto theme: setting light theme since dayTime variable is true (day)");
} else {
belchertown_debug("Auto theme: cannot set light theme since visitor used toggle to override theme. Refresh to reset the override.");
}
// Only change theme if user has not overridden the auto option with the toggle
if ( sessionStorage.getItem('theme') == "auto" ) {
changeTheme("light");
}
}
} else {
// Night time, set dark if needed
if ( document.body.classList.contains("light") ) {
if ( sessionStorage.getItem('theme') == "auto" ) {
belchertown_debug("Auto theme: setting dark theme since dayTime variable is false (night)");
} else {
belchertown_debug("Auto theme: cannot set dark theme since visitor used toggle to override theme. Refresh to reset the override.");
}
// Only change theme if user has not overridden the auto option with the toggle
if ( sessionStorage.getItem('theme') == "auto" ) {
changeTheme("dark");
}
}
}
}
function changeTheme(themeName, toggleOverride=false) {
belchertown_debug("Theme: Changing to " + themeName);
// If the configured theme is auto, but the user toggles light/dark, remove the auto option.
if ( toggleOverride ) {
belchertown_debug("Theme: toggle override clicked.");
belchertown_debug("Theme: sessionStorage.getItem('theme') was previously: " + sessionStorage.getItem('theme') );
// This was applied only to auto theme config, but now it's applied to all themes so visitor has full control on light/dark mode
//if ( sessionStorage.getItem('theme') == "auto" ) { }
sessionStorage.setItem('theme', 'toggleOverride');
belchertown_debug("Theme: sessionStorage.getItem('theme') is now: " + sessionStorage.getItem('theme') );
}
if ( themeName == "dark" ) {
// Apply dark theme
jQuery('body').addClass("dark");
jQuery('body').removeClass("light");
#if $Extras.has_key('theme_toggle_enabled') and $Extras.theme_toggle_enabled == '1'
jQuery("#themeSwitch").prop( "checked", true );
#end if
#if $Extras.has_key('logo_image_dark') and $Extras.logo_image_dark != ""
belchertown_debug("Theme: logo_image_dark is defined.");
jQuery("#logo_image").attr("src","$Extras.logo_image_dark" );
#end if
sessionStorage.setItem('currentTheme', 'dark');
} else if ( themeName == "light" ) {
// Apply light theme
jQuery('body').addClass("light");
jQuery('body').removeClass("dark");
#if $Extras.has_key('theme_toggle_enabled') and $Extras.theme_toggle_enabled == '1'
jQuery("#themeSwitch").prop( "checked", false );
#end if
#if $Extras.has_key('logo_image') and $Extras.logo_image != ""
belchertown_debug("Theme: logo_image is defined.");
jQuery("#logo_image").attr("src", "$Extras.logo_image");
#end if
sessionStorage.setItem('currentTheme', 'light');
}
}
function ajaxweewx() {
// Relative URL
jQuery.getJSON(get_relative_url() + "/json/weewx_data.json", update_weewx_data);
};
// Update weewx data elements
//var station_obs_array = "";
var unit_rounding_array = "";
var unit_label_array = "";
function update_weewx_data( data ) {
belchertown_debug("Updating weewx data");
#if $Extras.has_key("theme") and $Extras.theme == 'auto'
// Auto theme if enabled
autoTheme(data["almanac"]["sunset_hour"], data["almanac"]["sunset_minute"], data["almanac"]["sunrise_hour"], data["almanac"]["sunrise_minute"] );
#end if
//station_obs_array = data["station_observations"];
unit_rounding_array = data["unit_rounding"];
unit_label_array = data["unit_label"];
// Daily High Low
high = data["day"]["outTemp"]["max"];
low = data["day"]["outTemp"]["min"];
jQuery(".high").html( high );
jQuery(".low").html( low );
// Barometer trending by finding a negative number
count = ( data["current"]["barometer_trend"].match(/-/g) || [] ).length
if ( count >= 1 ) {
jQuery(".pressure-trend").html( '' );
} else {
jQuery(".pressure-trend").html( '' );
}
// Daily max gust span
jQuery(".dailymaxgust").html( parseFloat( data["day"]["wind"]["max"] ).toFixed(1) );
// Daily Snapshot Stats Section
jQuery(".snapshot-records-today-header").html( moment.unix( data["current"]["epoch"] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_snapshot_records_today_header' ) );
jQuery(".snapshot-records-month-header").html( moment.unix( data["current"]["epoch"] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_snapshot_records_month_header' ) );
jQuery(".dailystatshigh").html( data["day"]["outTemp"]["max"] );
jQuery(".dailystatslow").html( data["day"]["outTemp"]["min"] );
jQuery(".dailystatswindavg").html( data["day"]["wind"]["average"] );
jQuery(".dailystatswindmax").html( data["day"]["wind"]["max"] );
jQuery(".dailystatsrain").html( data["day"]["rain"]["sum"] );
jQuery(".dailystatsrainrate").html( data["day"]["rain"]["max"] );
// Month Snapshot Stats Section
jQuery(".monthstatshigh").html( data["month"]["outTemp"]["max"] );
jQuery(".monthstatslow").html( data["month"]["outTemp"]["min"] );
jQuery(".monthstatswindavg").html( data["month"]["wind"]["average"] );
jQuery(".monthstatswindmax").html( data["month"]["wind"]["max"] );
jQuery(".monthstatsrain").html( data["month"]["rain"]["sum"] );
jQuery(".monthstatsrainrate").html( data["month"]["rain"]["max"] );
// Sunrise and Sunset
jQuery(".sunrise-value").html( moment.unix( parseFloat(data["almanac"]["sunrise_epoch"]).toFixed(0) ).utcOffset($moment_js_utc_offset).format( "$obs.label.time_sunrise" ) );
jQuery(".sunset-value").html( moment.unix( parseFloat(data["almanac"]["sunset_epoch"]).toFixed(0) ).utcOffset($moment_js_utc_offset).format( "$obs.label.time_sunset" ) );
// Moon icon, phase and illumination percent
switch ( data["almanac"]["moon"]["moon_index"] ) {
case "0":
jQuery(".moon-icon").html( "
";
jQuery(".wx-stn-alert-text").append( forecast_alert_modal );
}
jQuery(".wx-stn-alert").show();
} else {
belchertown_debug("Forecast: There are no forecast alerts");
jQuery(".wx-stn-alert").hide();
}
}
function update_forecast_data( data ) {
forecast_provider = "$Extras.forecast_provider";
belchertown_debug("Forecast: Provider is " + forecast_provider);
belchertown_debug("Forecast: Updating data");
forecast_row = [];
if ( forecast_provider == "N/A" ) {
jQuery(".forecastrow").hide();
belchertown_debug("Forecast: No provider, hiding forecastrow");
return;
} else if ( forecast_provider == "darksky" ) {
var forecast_subtitle = moment.unix( data["currently"]["time"] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_forecast_last_updated' );
// WX icon in temperature box
if ( data['currently']['icon'] == "partly-cloudy-night" ) {
// partly-cloudy-night could also be clear-day
var wxicon = get_relative_url() + "/images/partly-cloudy-night.png";
} else {
var wxicon = get_relative_url() + "/images/" + data['currently']['icon'] + ".png";
}
belchertown_debug("Forecast: icon is " + wxicon);
// Current observation text
jQuery(".current-obs-text").html( data["currently"]["summary"] );
// Visibility text in station observation table
try {
visibility_output = parseFloat(parseFloat(data["currently"]["visibility"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["visibility"], maximumFractionDigits: unit_rounding_array["visibility"]}) + " " + unit_label_array["visibility"];
jQuery(".station-observations .visibility").html( visibility_output );
} catch(err) {
// Visibility not in the station observation table or any of the unit arrays, so silently exit
}
// Force 7 day forecast
//for (i = 0; i < data["daily"]["data"].length; i++) {
for (i = 0; i < 7; i++) {
var weekday = moment.unix( data["daily"]["data"][i]["time"] + 7200 ).utcOffset($moment_js_utc_offset).format( "$obs.label.time_forecast_date" );
if ( data['daily']['data'][i]['icon'] == "partly-cloudy-night" ) {
var image_url = get_relative_url() + "/images/clear-day.png";
} else {
var image_url = get_relative_url() + "/images/"+data['daily']['data'][i]['icon']+".png";
}
var condition_text = "";
switch ( data["daily"]["data"][i]["icon"] ) {
case "clear-day":
condition_text = "$obs.label.forecast_cloud_code_CL"; // Clear
break;
case "clear-night":
condition_text = "$obs.label.forecast_cloud_code_CL"; // Clear
break;
case "rain":
condition_text = "$obs.label.forecast_weather_code_R"; // Rain
break;
case "snow":
condition_text = "$obs.label.forecast_weather_code_S"; // Snow
break;
case "sleet":
condition_text = "$obs.label.forecast_weather_code_IP"; // Sleet
break;
case "wind":
condition_text = "$obs.label.forecast_weather_code_W"; // Windy
break;
case "fog":
condition_text = "$obs.label.forecast_weather_code_F"; // Fog
break;
case "cloudy":
condition_text = "$obs.label.forecast_cloud_code_OV"; // Overcast
break;
case "partly-cloudy-day":
condition_text = "$obs.label.forecast_cloud_code_SC"; // Partly Cloudy
break;
case "partly-cloudy-night":
condition_text = "$obs.label.forecast_cloud_code_CL"; // Clear - https://darksky.net/dev/docs/faq - So you can just treat partly-cloudy-night as an alias for clear-day.
break;
case "hail":
condition_text = "$obs.label.forecast_weather_code_A"; // Hail
break;
case "thunderstorm":
condition_text = "$obs.label.forecast_weather_code_T"; // Thunderstorm
break;
case "tornado":
condition_text = "$obs.label.forecast_weather_code_TO"; // Tornado
break;
}
if ( data["daily"]["data"][i].hasOwnProperty("precipType") && data["daily"]["data"][i]["precipType"] == "snow" ) {
var snow_depth = data["daily"]["data"][i]["precipAccumulation"];
var snow_unit = "in";
} else if ( data["daily"]["data"][i].hasOwnProperty("precipType") && data["daily"]["data"][i]["precipType"] == "rain" ) {
var precip = data["daily"]["data"][i]["precipProbability"] * 100;
} else {
var precip = 0;
var snow_depth = 0;
var snow_unit = "";
}
var darksky_link_units = data["flags"]["units"].toLowerCase().concat("12"); // Currently the DarkSky forecast link uses a "{unit}12" string in the URL.
#if $Extras.has_key("darksky_lang")
var forecast_link = '$obs.label.daily_forecast';
#else
var forecast_link = '$obs.label.daily_forecast';
#end if
forecast_row.push( {
"weekday": weekday,
"image_url": image_url,
"condition_text": condition_text,
"minTemp": data["daily"]["data"][i]["temperatureLow"],
"maxTemp": data["daily"]["data"][i]["temperatureHigh"],
"windSpeed": data["daily"]["data"][i]["windSpeed"],
"windGust": data["daily"]["data"][i]["windGust"],
"snow_depth": snow_depth,
"snow_unit": snow_unit,
"precip": precip,
"forecast_link": forecast_link,
} );
}
}
if ( forecast_provider == "aeris" ) {
var forecast_subtitle = moment.unix( data["timestamp"] ).utcOffset($moment_js_utc_offset).format( '$obs.label.time_forecast_last_updated' );
try {
belchertown_debug("Forecast: icon from Aeris data is " + data["current"][0]["response"]["ob"]["icon"] );
var wxicon = get_relative_url() + "/images/" + aeris_icon( data["current"][0]["response"]["ob"]["icon"] ) + ".png";
belchertown_debug("Forecast: Belchertown icon is " + wxicon);
// Current observation text
jQuery(".current-obs-text").html( aeris_coded_weather( data["current"][0]["response"]["ob"]["weatherPrimaryCoded"], true ) );
// Visibility text in station observation table
if ( ( "$Extras.forecast_units" == "si" ) || ( "$Extras.forecast_units" == "ca" ) ) {
// si and ca = kilometer
visibility = data["current"][0]["response"]["ob"]["visibilityKM"];
} else {
// us and uk2 and default = miles
visibility = data["current"][0]["response"]["ob"]["visibilityMI"];
}
try {
visibility_output = parseFloat(parseFloat( visibility )).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["visibility"], maximumFractionDigits: unit_rounding_array["visibility"]}) + " " + unit_label_array["visibility"];
jQuery(".station-observations .visibility").html( visibility_output );
} catch(err) {
// Visibility not in the station observation table or any of the unit arrays, so silently exit
}
} catch(err) {
// Probably a non-metar current observation, which does not have visibility, icon, conditions, etc. So silently exit.
}
for (i = 0; i < data["forecast"][0]["response"][0]["periods"].length; i++) {
var image_url = get_relative_url() + "/images/" + aeris_icon( data["forecast"][0]["response"][0]["periods"][i]["icon"] ) + ".png";
var condition_text = aeris_coded_weather( data["forecast"][0]["response"][0]["periods"][i]["weatherPrimaryCoded"], false );
var weekday = moment.unix( data["forecast"][0]["response"][0]["periods"][i]["timestamp"] + 7200 ).utcOffset($moment_js_utc_offset).format( "$obs.label.time_forecast_date" );
// Determine temperature units
if ( ( "$Extras.forecast_units" == "ca" ) || ( "$Extras.forecast_units" == "uk2" ) || ( "$Extras.forecast_units" == "si" ) ) {
minTemp = data["forecast"][0]["response"][0]["periods"][i]["minTempC"];
maxTemp = data["forecast"][0]["response"][0]["periods"][i]["maxTempC"];
} else {
// Default
minTemp = data["forecast"][0]["response"][0]["periods"][i]["minTempF"];
maxTemp = data["forecast"][0]["response"][0]["periods"][i]["maxTempF"];
}
// Determine wind units
if ( "$Extras.forecast_units" == "ca" ) {
// ca = kph
windSpeed = data["forecast"][0]["response"][0]["periods"][i]["windSpeedKPH"];
windGust = data["forecast"][0]["response"][0]["periods"][i]["windGustKPH"];
} else if ( "$Extras.forecast_units" == "si" ) {
// si = meters per second. MPS is KPH / 3.6
windSpeed = data["forecast"][0]["response"][0]["periods"][i]["windSpeedKPH"] / 3.6;
windGust = data["forecast"][0]["response"][0]["periods"][i]["windGustKPH"] / 3.6;
} else {
// us and uk2 and default = mph
windSpeed = data["forecast"][0]["response"][0]["periods"][i]["windSpeedMPH"];
windGust = data["forecast"][0]["response"][0]["periods"][i]["windGustMPH"];
}
if ( ( data["forecast"][0]["response"][0]["periods"][i]["snowCM"] > 0 ) || ( data["forecast"][0]["response"][0]["periods"][i]["snowIN"] > 0 ) ) {
// Determine snow unit
if ( ( "$Extras.forecast_units" == "si" ) || ( "$Extras.forecast_units" == "ca" ) || ( "$Extras.forecast_units" == "uk2" ) ) {
snow_depth = data["forecast"][0]["response"][0]["periods"][i]["snowCM"];
snow_unit = "cm";
} else {
snow_depth = data["forecast"][0]["response"][0]["periods"][i]["snowIN"];
snow_unit = "in";
}
} else if ( data["forecast"][0]["response"][0]["periods"][i]["pop"] > 0 ) {
// Rain percent of precip
precip = data["forecast"][0]["response"][0]["periods"][i]["pop"];
} else {
precip = 0;
snow_depth = 0;
snow_unit = "";
}
var forecast_link_setup = "$Extras.forecast_daily_forecast_link".replace("YYYY", moment.unix( data["forecast"][0]["response"][0]["periods"][i]["timestamp"] + 7200 ).utcOffset($moment_js_utc_offset).format( "YYYY" )).replace("MM", moment.unix( data["forecast"][0]["response"][0]["periods"][i]["timestamp"] + 7200 ).utcOffset($moment_js_utc_offset).format( "MM" )).replace("DD", moment.unix( data["forecast"][0]["response"][0]["periods"][i]["timestamp"] + 7200 ).utcOffset($moment_js_utc_offset).format( "DD" ));
var forecast_link = '$obs.label.daily_forecast';
forecast_row.push( {
"weekday": weekday,
"image_url": image_url,
"condition_text": condition_text,
"minTemp": minTemp,
"maxTemp": maxTemp,
"windSpeed": windSpeed,
"windGust": windGust,
"snow_depth": snow_depth,
"snow_unit": snow_unit,
"precip": precip,
"forecast_link": forecast_link
} );
}
}
// WX icon in temperature box
jQuery("#wxicon").attr( "src", wxicon );
belchertown_debug("Forecast: Changing icon to " + wxicon);
// Build daily forecast row
var output_html = "";
for (i = 0; i < forecast_row.length; i++) {
if ( i == 0 ) {
output_html += '
';
} else {
output_html += '
';
}
// Add 7200 (2 hours) to the epoch to get an hour well into the day to avoid any DST issues. This way it'll either be 1am or 2am. Without it, we get 12am or 11pm (the previous day).
output_html += ''+forecast_row[i]["weekday"]+'';
output_html += ' ';
output_html += '
';
#if $Extras.has_key("forecast_show_daily_forecast_link") and $Extras.forecast_show_daily_forecast_link == '1'
output_html += forecast_row[i]["forecast_link"];
#end if
output_html += '
';
}
// Show the forecast row
jQuery(".forecast-subtitle").html( "$obs.label.forecast_last_updated " + forecast_subtitle );
jQuery(".forecasts").html( output_html );
#if $Extras.has_key("forecast_alert_enabled") and $Extras.forecast_alert_enabled == '1'
// Show weather alert
show_forcast_alert( data, forecast_provider );
#end if
}
#end if
#if $Extras.has_key("mqtt_websockets_enabled") and $Extras.mqtt_websockets_enabled == '1'
//============================================//
// Live website using MQTT Websockets enabled //
//============================================//
var mqttConnected = false;
function ajaximages(section=false, reload_timer_interval_seconds=false) {
// This function only runs if the elements have an img src.
// Update images within the specific section
if ( section ) {
if ( section == "radar" ) {
belchertown_debug("Updating radar image");
// Reload images
if ( document.querySelectorAll(".radar-map img").length > 0 ) {
var radar_img = document.querySelectorAll(".radar-map img")[0].src;
var new_radar_img = radar_img + "&t=" + Math.floor(Math.random() * 999999999);
document.querySelectorAll(".radar-map img")[0].src = new_radar_img;
//var radar_html = jQuery('.radar-map').children('img').attr('src').split('?')[0] // Get the img src and remove everything after "?" so we don't stack ?'s onto the image during updates
//jQuery('.radar-map').children('img').attr('src', radar_html + "?" + Math.floor(Math.random() * 999999999));
}
// Reload iframe - https://stackoverflow.com/a/4249946/1177153
if ( document.querySelectorAll(".radar-map iframe").length > 0 ) {
jQuery(".radar-map iframe").each(function(){
jQuery(this).attr( 'src', function ( i, val ) { return val; });
});
}
} else {
belchertown_debug("Updating " + section + " images");
// Reload images
jQuery('.'+section+' img').each(function(){
new_image_url = jQuery(this).attr('src').split('?')[0] + "?" + Math.floor(Math.random() * 999999999);
jQuery(this).attr('src', new_image_url);
});
// Reload iframes
jQuery('.'+section+' iframe').each(function(){
jQuery(this).attr( 'src', function ( i, val ) { return val; });
});
}
// Set the new timer
if ( reload_timer_interval_seconds ) {
var reload_timer_ms = reload_timer_interval_seconds * 1000; // convert to millis
setTimeout(function() { ajaximages(section, reload_timer_interval_seconds); }, reload_timer_ms);
}
}
}
var reconnect_using_inactive_timestamp = false;
var inactive_timestamp = "";
// MQTT connect
function connect() {
#if $Extras.has_key("mqtt_websockets_ssl") and $Extras.mqtt_websockets_ssl == '1'
belchertown_debug( "MQTT: Connecting to MQTT Websockets: $Extras.mqtt_websockets_host $Extras.mqtt_websockets_port (SSL Enabled)" );
#else
belchertown_debug( "MQTT: Connecting to MQTT Websockets: $Extras.mqtt_websockets_host $Extras.mqtt_websockets_port (SSL Disabled)" );
#end if
if ( reconnect_using_inactive_timestamp ) {
updated = moment.unix( inactive_timestamp ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" );
} else {
updated = moment.unix( "$current.dateTime.raw" ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" );
}
reported = "$obs.label.mqtt_websockets_connecting $obs.label.header_last_updated " + updated;
jQuery(".updated").html( reported );
jQuery(".onlineMarker").hide();
jQuery(".offlineMarker").hide();
jQuery(".loadingMarker").show();
client = new Paho.Client("$Extras.mqtt_websockets_host", $Extras.mqtt_websockets_port, mqttclient);
client.onConnectionLost = onConnectionLost;
client.onMessageArrived = onMessageArrived;
var options = {
#if $Extras.has_key("mqtt_websockets_ssl") and $Extras.mqtt_websockets_ssl == '1'
useSSL: true,
#else
useSSL: false,
#end if
reconnect: true,
onSuccess:onConnect,
onFailure:onFailure
}
client.connect( options );
}
// MQTT connect callback
function onConnect() {
mqttConnected = true;
belchertown_debug( "MQTT: MQTT Connected. Subscribing." );
if ( reconnect_using_inactive_timestamp ) {
updated = moment.unix( inactive_timestamp ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" );
} else {
updated = moment.unix( "$current.dateTime.raw" ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" );
}
if ( pageName == "pi" ) {
reported = "$obs.label.mqtt_websockets_waiting_pi $obs.label.header_last_updated " + updated;
} else {
reported = "$obs.label.mqtt_websockets_waiting $obs.label.header_last_updated " + updated;
}
jQuery(".updated").html( reported );
jQuery(".onlineMarker").hide();
jQuery(".offlineMarker").hide();
jQuery(".loadingMarker").show();
client.subscribe( "$Extras.mqtt_websockets_topic" );
#if $Extras.has_key("disconnect_live_website_visitor") and $Extras.disconnect_live_website_visitor != '0'
if ( getURLvar("stayconnected") && ( getURLvar("stayconnected") == "true" || getURLvar("stayconnected") == "1" ) ) {
belchertown_debug("stayconnected URL var found: ignoring disconnect_live_website_visitor value");
} else {
if ( pageName != "pi" ) {
var activityTimeout = setTimeout( inactive, $Extras.disconnect_live_website_visitor ); // Stop automatic ajax refresh
}
}
#end if
}
// MQTT Failure
function onFailure() {
mqttConnected = false;
jQuery(".onlineMarker").hide();
jQuery(".offlineMarker").show();
jQuery(".loadingMarker").hide();
var d = new Date();
epoch = parseFloat( (d / 1000) ).toFixed(0); // Convert millis to seconds
if ( client.isConnected() ) {
updated = moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" );
} else {
updated = moment.unix( "$current.dateTime.raw" ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" );
}
jQuery(".updated").html( "$obs.label.mqtt_websockets_failed $obs.label.header_last_updated " + updated );
console.log( "MQTT: " + moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format() + ": Cannot connect to MQTT broker" );
}
// MQTT connection lost
function onConnectionLost(responseObject) {
mqttConnected = false;
jQuery(".onlineMarker").hide();
jQuery(".offlineMarker").show();
jQuery(".loadingMarker").hide();
var d = new Date();
epoch = parseFloat( (d / 1000) ).toFixed(0); // Convert millis to seconds
if ( client.isConnected() ) {
updated = moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" );
} else {
updated = moment.unix( "$current.dateTime.raw" ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" );
}
jQuery(".updated").html( "$obs.label.mqtt_websockets_lost $obs.label.header_last_updated " + updated );
if (responseObject.errorCode !== 0) {
console.log( "MQTT: " + moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format() + ": mqtt Connection Lost: "+responseObject.errorMessage );
}
}
function inactive() {
client.disconnect(); // Disconnect mqtt
belchertown_debug( "MQTT: Inactive timer expired. MQTT Disconnected" );
jQuery(".onlineMarker").hide(); // Hide online beacon
jQuery(".offlineMarker").show(); // Show offline beacon
jQuery(".loadingMarker").hide(); // Hide loading beacon
var d = new Date();
epoch = parseFloat( (d / 1000) ).toFixed(0); // Convert millis to seconds
updated = moment.unix( epoch ).utcOffset( $moment_js_utc_offset ).format( "$obs.label.time_last_updated" );
jQuery(".updated").html( "$obs.label.mqtt_websockets_stopped $obs.label.header_last_updated " + updated + " " );
reconnect_using_inactive_timestamp = true; // Set a flag to use the inactive timestamp in case we reconnect we have the latest last updated time
inactive_timestamp = epoch; // Store this timestamp in case we reconnect
}
// New message from mqtt, process it
function onMessageArrived(message) {
belchertown_debug( "MQTT: " + message.payloadString );
update_current_wx( message.payloadString );
}
// Handle MQTT message
function update_current_wx(data) {
data = jQuery.parseJSON( data );
#if $Extras.has_key('googleAnalyticsId')
// Send a pageview
gtag('config', '$Extras.googleAnalyticsId');
#end if
// This message is a weewx archive update. Update weewx data, forecast data and highcharts graphs
if ( data.hasOwnProperty("interval_minute") ) {
// Delays are recommended to allow the other skins to complete processing
belchertown_debug( "MQTT: MQTT message indicates this is an archive interval." );
if ( pageName != "pi" ) {
setTimeout( showChart, 30000, homepage_graphgroup ); // Load updated charts.
}
setTimeout( ajaxweewx, 10000 ); // Update weewx data
#if $Extras.has_key("forecast_enabled") and $Extras.forecast_enabled == '1'
setTimeout( ajaxforecast, 10000 ); // Update forecast data
#end if
setTimeout( ajaximages, 10000 ); // Update radar and home page hook "img src" if present
} else {
// Only show the updated time on non-archive packets
epoch = parseFloat( data["dateTime"] ).toFixed(0);
updated = moment.unix(epoch).utcOffset($moment_js_utc_offset).format("$obs.label.time_last_updated");
if ( pageName == "pi" ) {
updated_text = "$obs.label.mqtt_websockets_connected_pi " + updated;
} else {
updated_text = "$obs.label.mqtt_websockets_connected " + updated;
}
jQuery(".updated").html( updated_text );
}
// If we're in this function, show the online beacon and hide the others
jQuery(".onlineMarker").show(); // Show the online beacon
jQuery(".offlineMarker").hide();
jQuery(".loadingMarker").hide();
// Update the station observation box elements
station_mqtt_data = Object.keys(data); // Turn data (mqtt message) into an object we can forEach
// Get all span elements within the table. This is setup by Python initially
jQuery('.station-observations').find("span").each(function () {
// The class name is the same as the Extras.station_observations name (weewx schema)
thisElementClass = jQuery(this).attr("class")
// Loop through each MQTT payload item
station_mqtt_data.forEach( mqttdata => {
if ( thisElementClass == "rainWithRainRate" ) {
// Force dayRain since that's the MQTT payload name
thisElementClass = "dayRain";
}
if ( thisElementClass == "barometer" || thisElementClass == "pressure" || thisElementClass == "altimeter" || thisElementClass == "cloudbase" ) {
// Do not group number into thousands,hundreds format
localeStringUseGrouping = false;
} else {
localeStringUseGrouping = true;
}
// If this MQTT payload key begins with the name of the span class name, update the info. Can also use mqttdata.includes(thisElementClass) if weewx-mqtt changes in future
if ( mqttdata.startsWith(thisElementClass) ) {
html_output = parseFloat(parseFloat(data[mqttdata])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array[thisElementClass], maximumFractionDigits: unit_rounding_array[thisElementClass], useGrouping: localeStringUseGrouping}) + unit_label_array[thisElementClass];
// Finally update the element class
jQuery("." + thisElementClass).html( html_output );
}
});
});
// End dynamic station observation box
// Temperature F
if ( data.hasOwnProperty("outTemp_F") ) {
// Inside parseFloat converts str to int. Outside parseFloat processes the locale string
// Help from: https://stackoverflow.com/a/40152286/1177153 and https://stackoverflow.com/a/31581206/1177153
outTemp = parseFloat(parseFloat(data["outTemp_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["outTemp"], maximumFractionDigits: unit_rounding_array["outTemp"]});
get_outTemp_color( "degree_F", outTemp );
jQuery(".outtemp").html( outTemp );
// Feels like temp as defined by NOAA's "Apparent Temperature" at: http://www.nws.noaa.gov/ndfd/definitions.htm
//if ( data["outTemp_F"] <= 50 ) {
// jQuery(".feelslike").html( "Feels like: " + parseFloat(parseFloat(data["windchill_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: 1, maximumFractionDigits: 1}) + " $unit.label.outTemp" );
//} else if ( data["outTemp_F"] >= 80 ) {
// jQuery(".feelslike").html( "Feels like: " + parseFloat(parseFloat(data["heatindex_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: 1, maximumFractionDigits: 1}) + " $unit.label.outTemp" );
//} else {
// jQuery(".feelslike").html( "Feels like: " + parseFloat(parseFloat(data["outTemp_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: 1, maximumFractionDigits: 1}) + " $unit.label.outTemp" );
//}
}
// Temperature C
if ( data.hasOwnProperty("outTemp_C") ) {
outTemp = parseFloat(parseFloat(data["outTemp_C"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["outTemp"], maximumFractionDigits: unit_rounding_array["outTemp"]});
get_outTemp_color( "degree_C", outTemp );
jQuery(".outtemp").html( outTemp );
}
// Apparent Temperature US
if ( data.hasOwnProperty("appTemp_F") ) {
jQuery(".feelslike").html( "$obs.label.feels_like: " + parseFloat(parseFloat(data["appTemp_F"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["outTemp"], maximumFractionDigits: unit_rounding_array["outTemp"]}) + " $unit.label.outTemp" );
}
// Apparent Temperature Metric
if ( data.hasOwnProperty("appTemp_C") ) {
jQuery(".feelslike").html( "$obs.label.feels_like: " + parseFloat(parseFloat(data["appTemp_C"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["outTemp"], maximumFractionDigits: unit_rounding_array["outTemp"]}) + " $unit.label.outTemp" );
}
// Wind
if ( data.hasOwnProperty("windDir") ) {
// No toLocaleString() here since there is no float decimal needed.
rotateThis( data["windDir"] );
//jQuery(".wind-arrow").css( "transform", "rotate(" + data["windDir"] + "deg)" );
jQuery(".curwinddeg").html( parseFloat( data["windDir"] ).toFixed(0) + "°" );
jQuery(".curwinddir").html( highcharts_tooltip_factory( parseFloat( data["windDir"] ).toFixed(0), "windDir" ) );
}
// Windspeed US
if ( data.hasOwnProperty("windSpeed_mph") ) {
jQuery(".curwindspeed").html( parseFloat(parseFloat(data["windSpeed_mph"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]}) );
}
// Windspeed Metric
if (data.hasOwnProperty("windSpeed_kph")) {
windSpeed = parseFloat(parseFloat(data["windSpeed_kph"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]});
get_windSpeed_color(windSpeed);
jQuery(".curwindspeed").html(windSpeed);
}
// Windspeed METRICWX
if ( data.hasOwnProperty("windSpeed_mps") ) {
jQuery(".curwindspeed").html( parseFloat(parseFloat(data["windSpeed_mps"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windSpeed"], maximumFractionDigits: unit_rounding_array["windSpeed"]}) );
}
// Wind Gust US
// May not be provided in mqtt, but just in case.
if ( data.hasOwnProperty("windGust_mph") ) {
jQuery(".curwindgust").html( parseFloat(parseFloat(data["windGust_mph"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windGust"], maximumFractionDigits: unit_rounding_array["windGust"]}) );
}
// Wind Gust Metric
if (data.hasOwnProperty("windGust_kph")) {
windGust = parseFloat(parseFloat(data["windGust_kph"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windGust"], maximumFractionDigits: unit_rounding_array["windGust"]});
get_windGust_color(windSpeed);
jQuery(".curwindgust").html(windGust);
// Wind Gust METRICWX
if ( data.hasOwnProperty("windGust_mps") ) {
jQuery(".curwindgust").html( parseFloat(parseFloat(data["windGust_mps"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windGust"], maximumFractionDigits: unit_rounding_array["windGust"]}) );
}
// Windchill US
if ( data.hasOwnProperty("windchill") ) {
jQuery(".curwindchill").html( parseFloat(parseFloat(data["windchill"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windchill"], maximumFractionDigits: unit_rounding_array["windchill"]}) + "$unit.label.outTemp" );
}
// Windchill Metric
if ( data.hasOwnProperty("windchill_C") ) {
jQuery(".curwindchill").html( parseFloat(parseFloat(data["windchill_C"])).toLocaleString("$system_locale_js", {minimumFractionDigits: unit_rounding_array["windchill"], maximumFractionDigits: unit_rounding_array["windchill"]}) + "$unit.label.outTemp" );
}
};
#end if
Highcharts.setOptions({
global: {
//useUTC: false
timezoneOffset: $highcharts_timezoneoffset
},
lang: {
decimalPoint: "$highcharts_decimal",
thousandsSep: "$highcharts_thousands"
}
});
function showChart(json_file, prepend_renderTo=false) {
// Relative URL by finding what page we're on currently.
jQuery.getJSON(get_relative_url() + '/json/' + json_file + '.json', function(data) {
// Loop through each chart name (e.g. chart1, chart2, chart3)
jQuery.each(data, function (plotname, obsname) {
var observation_type = undefined;
// Ignore the Belchertown Version since this "plot" has no other options
if ( plotname == "belchertown_version" ) {
return true;
}
// Ignore the generated timestamp since this "plot" has no other options
if ( plotname == "generated_timestamp" ) {
return true;
}
// Ignore the chartgroup_title since this "plot" has no other options
if ( plotname == "chartgroup_title" ) {
return true;
}
// Set the chart's tooltip date time format, then return since this "plot" has no other options
if ( plotname == "tooltip_date_format" ) {
tooltip_date_format = obsname;
return true;
}
// Set the chart colors, then return right away since this "plot" has no other options
if ( plotname == "colors" ) {
colors = obsname.split(",");
return true;
}
// Set the chart credits, then return right away since this "plot" has no other options
if ( plotname == "credits" ) {
credits = obsname.split(",")[0];
return true;
}
// Set the chart credits url, then return right away since this "plot" has no other options
if ( plotname == "credits_url" ) {
credits_url = obsname.split(",")[0];
return true;
}
// Set the chart credits position, then return right away since this "plot" has no other options
if ( plotname == "credits_position" ) {
credits_position = obsname;
return true;
}
// Loop through each chart options
jQuery.each(data[plotname]["options"], function (optionName, optionVal) {
switch(optionName) {
case "type":
type = optionVal;
break;
case "renderTo":
renderTo = optionVal;
break;
case "title":
title = optionVal;
break;
case "subtitle":
subtitle = optionVal;
break;
case "yAxis_label":
yAxis_label = optionVal;
break;
case "chart_group":
chart_group = optionVal;
break;
case "gapsize":
gapsize = optionVal;
break;
case "connectNulls":
connectNulls = optionVal;
break;
case "rounding":
rounding = optionVal;
break;
case "xAxis_categories":
xAxis_categories = optionVal;
break;
case "plot_tooltip_date_format":
plot_tooltip_date_format = optionVal;
break;
case "css_class":
css_class = optionVal;
break;
case "css_height":
css_height = optionVal;
break;
case "css_width":
css_width = optionVal;
break;
case "legend":
legend_enabled = optionVal;
break;
case "exporting":
exporting_enabled = optionVal;
break;
}
});
// Handle any per-chart date time format override
if ( typeof plot_tooltip_date_format !== "undefined" ) {
var tooltip_date_format = plot_tooltip_date_format;
}
var options = {
chart: {
renderTo: '',
spacing: [5, 10, 10, 0],
type: '',
zoomType: 'x'
},
exporting: {
enabled: JSON.parse( String( exporting_enabled ) ) // Convert string to bool
},
title: {
useHTML: true,
text: ''
},
subtitle: {
text: ''
},
legend: {
enabled: JSON.parse( String( legend_enabled ) ) // Convert string to bool
},
xAxis: {
dateTimeLabelFormats: {
day: '%e %b',
week: '%e %b',
month: '%b %y',
},
lineColor: '#555',
minRange: 900000,
minTickInterval: 900000,
title: {
style: {
font: 'bold 12px Lucida Grande, Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif'
}
},
ordinal: false,
type: 'datetime'
},
yAxis: [{
endOnTick: true,
lineColor: '#555',
minorGridLineWidth: 0,
startOnTick: true,
showLastLabel: true,
title: {
},
opposite: false
}],
plotOptions: {
area: {
lineWidth: 2,
gapSize: '',
gapUnit: 'value',
marker: {
enabled: false,
radius: 2
},
threshold: null,
softThreshold: true
},
line: {
lineWidth: 2,
gapSize: '',
gapUnit: 'value',
marker: {
enabled: false,
radius: 2
},
},
spline: {
lineWidth: 2,
gapSize: '',
gapUnit: 'value',
marker: {
enabled: false,
radius: 2
},
},
areaspline: {
lineWidth: 2,
gapSize: '',
gapUnit: 'value',
marker: {
enabled: false,
radius: 2
},
threshold: null,
softThreshold: true
},
scatter: {
gapSize: '',
gapUnit: 'value',
marker: {
radius: 2
},
},
},
// Highstock is needed for gapsize. Disable these 3 to make it look like standard Highcharts
scrollbar: {
enabled: false
},
navigator: {
enabled: false
},
rangeSelector: {
enabled: false
},
tooltip: {
enabled: true,
crosshairs: true,
dateTimeLabelFormats: {
hour: '%e %b %H:%M'
},
// For locale control with moment.js
formatter: function (tooltip) {
try {
// The first returned item is the header, subsequent items are the points.
// Mostly applies to line style charts (line, spline, area)
return [moment.unix( this.x / 1000).utcOffset($moment_js_utc_offset).format( tooltip_date_format )].concat(
this.points.map(function (point) {
// If observation_type is in the series array, use that otherwise use the obsType
var point_obsType = point.series.userOptions.observation_type ? point.series.userOptions.observation_type : point.series.userOptions.obsType;
var rounding = point.series.userOptions.rounding;
var mirrored = point.series.userOptions.mirrored_value;
var numberFormat = point.series.userOptions.numberFormat ? point.series.userOptions.numberFormat : "";
return "\u25CF " + point.series.name + ': ' + highcharts_tooltip_factory( point.y, point_obsType, true, rounding, mirrored, numberFormat );
})
);
} catch(e) {
// There's an error so check if it's windDir to apply wind direction label, or if it's a scatter. If none of those revert back to default tooltip.
if (this.series.userOptions.obsType == "windDir" || this.series.userOptions.observation_type == "windDir") {
// If observation_type is in the series array, use that otherwise use the obsType
var point_obsType = this.series.userOptions.observation_type ? this.series.userOptions.observation_type : this.series.userOptions.obsType;
var rounding = this.series.userOptions.rounding;
var mirrored = this.series.userOptions.mirrored_value;
return moment.unix( this.x / 1000).utcOffset($moment_js_utc_offset).format( tooltip_date_format ) +' ' + highcharts_tooltip_factory( this.point.y, point_obsType, true, rounding, mirrored );
} else if (this.series.userOptions.type == "scatter") {
// Catch anything else that might be a scatter plot. Scatter plots will just show x,y coordinates without this.
return "\u25CF " + this.series.name + ': ' + Highcharts.numberFormat(this.y);
} else {
return tooltip.defaultFormatter.call(this, tooltip);
}
}
},
split: true,
},
credits: {},
series: [{}]
};
// Default options completed, build overrides from JSON and graphs.conf
// Set the chart render div and title
if ( prepend_renderTo ) {
options.chart.renderTo = json_file + "_" + renderTo;
} else {
options.chart.renderTo = renderTo;
}
belchertown_debug( options.chart.renderTo + ": building a " + type + " chart" );
if ( css_class ) {
jQuery( "#" + options.chart.renderTo ).addClass( css_class );
belchertown_debug( options.chart.renderTo + ": div id is " + options.chart.renderTo + " and adding CSS class: " + css_class );
}
options.chart.type = type;
options.title.text = ""+title+""; // Anchor link to chart for direct linking
options.subtitle.text = subtitle;
options.plotOptions.area.gapSize = gapsize;
options.plotOptions.line.gapSize = gapsize;
options.plotOptions.spline.gapSize = gapsize;
options.plotOptions.scatter.gapSize = gapsize;
if ( connectNulls == "true" ) {
options.plotOptions.series = { connectNulls: connectNulls };
}
options.colors = colors;
// If we have xAxis categories, reset xAxis and populate it from these options. Also need to reset tooltip since there's no datetime for moment.js to use.
if ( xAxis_categories.length >= 1 ) {
belchertown_debug( options.chart.renderTo + ": has " + xAxis_categories.length + " xAxis categories. Resetting xAxis and tooltips for grouping" );
options.xAxis = {}
options.xAxis.categories = xAxis_categories;
options.tooltip = {}
options.tooltip = {
enabled: true,
crosshairs: true,
split: true,
formatter: function () {
// The first returned item is the header, subsequent items are the points
return [this.x].concat(
this.points.map(function (point) {
// If observation_type is in the series array, use that otherwise use the obsType
var point_obsType = point.series.userOptions.observation_type ? point.series.userOptions.observation_type : point.series.userOptions.obsType;
var rounding = point.series.userOptions.rounding;
var mirrored = point.series.userOptions.mirrored_value;
return "\u25CF " + point.series.name + ': ' + highcharts_tooltip_factory( point.y, point_obsType, true, rounding, mirrored );
})
);
},
}
}
// Reset the series everytime we loop.
options.series = [];
// Build the series
var i = 0;
jQuery.each(data[plotname]["series"], function (seriesName, seriesVal) {
observation_type = data[plotname]["series"][seriesName]["obsType"];
options.series[i] = data[plotname]["series"][seriesName];
i++;
});
/* yAxis customization handler and label handling
Take the following example.
yAxis is in observation 0 (rainTotal), so that label is caught and set by yAxis1_active.
If you move yAxis to observation 1 (rainRate), then the label is caught and set by yAxis_index.
There may be a more efficient way to do this. If so, please submit a pull request :)
[[[chart3]]]
title = Rain
[[[[rainTotal]]]]
name = Rain Total
yAxis = 1
[[[[rainRate]]]]
*/
var yAxis1_active = undefined;
// Find if any series have yAxis = 1. If so, save the array number so we can set labels correctly.
// We really care if yAxis is in array 1+, so we can go back and set yAxis 0 to the right label.
var yAxis_index = options.series.findIndex( function(item){ return item.yAxis == 1 } )
// Handle series specific data, overrides and non-Highcharts options that we passed through
options.series.forEach(s => {
if (s.yAxis == "1") {
// If yAxis = 1 is set for the observation, add a new yAxis and associate that observation to the right side of the chart
yAxis1_active = true;
options.yAxis.push({ // Secondary yAxis
opposite: true,
title: {
text: s.yAxis_label,
},
}),
// Associate this series to the new yAxis 1
s.yAxis = 1
// We may have already passed through array 0 in the series without setting the "multi axis label", go back and explicitly define it.
if ( yAxis_index >= 1 ) {
options.yAxis[0].title.text = options.series[0].yAxis_label;
}
} else {
if ( yAxis1_active ) {
// This yAxis is first in the data series, so we can set labels without needing to double back
options.yAxis[0].title.text = s.yAxis_label;
} else {
// Apply the normal yAxis 0's label without observation name
options.yAxis[0].title.text = s.yAxis_label;
}
// Associate this series to yAxis 1
s.yAxis = 0;
}
// Run yAxis customizations
this_yAxis = s.yAxis;
belchertown_debug( options.chart.renderTo + ": " + s.obsType + " is on yAxis " + this_yAxis );
// Some charts may require a defined min/max on the yAxis
options.yAxis[this_yAxis].min = s.yAxis_min !== "undefined" ? s.yAxis_min : null;
options.yAxis[this_yAxis].max = s.yAxis_max !== "undefined" ? s.yAxis_max : null;
// Some charts may require a defined soft min/max on the yAxis
options.yAxis[this_yAxis].softMin = s.yAxis_softMin !== "undefined" ? parseInt(s.yAxis_softMin) : null;
options.yAxis[this_yAxis].softMax = s.yAxis_softMax !== "undefined" ? parseInt(s.yAxis_softMax) : null;
// Set the yAxis tick interval. Mostly used for barometer.
if ( s.yAxis_tickInterval ) {
options.yAxis[this_yAxis].tickInterval = s.yAxis_tickInterval;
}
// Set yAxis minorTicks. This is a graph-wide setting so setting it for any of the yAxis will set it for the graph itself
if ( s.yAxis_minorTicks ) {
options.yAxis[this_yAxis].minorTicks = true;
}
// Barometer chart plots get a higher precision yAxis tick
if (s.obsType == "barometer") {
if ( typeof s.yAxis_tickInterval === "undefined" ) {
// If no tick interval override, set 0.01 as default tick interval to satisfy an old request for this level of precision.
options.yAxis[this_yAxis].tickInterval = 0.01;
}
// Define yAxis label float format if rounding is defined. Default to 2 decimals if nothing defined
if ( typeof s.rounding !== "undefined" ) {
options.yAxis[this_yAxis].labels = { format: '{value:.'+s.rounding+'f}' }
} else {
options.yAxis[this_yAxis].labels = { format: '{value:.2f}' }
}
}
// Rain, RainRate and rainTotal (special Belchertown skin observation) get yAxis precision
if (s.obsType == "rain" || s.obsType == "rainRate" || s.obsType == "rainTotal") {
options.yAxis[this_yAxis].min = 0;
options.yAxis[this_yAxis].minRange = 0.01;
options.yAxis[this_yAxis].minorGridLineWidth = 1;
}
if (s.obsType == "windDir") {
options.yAxis[this_yAxis].tickInterval = 90;
options.yAxis[this_yAxis].labels = { formatter: function() { var value = weatherdirection[this.value]; return value !== 'undefined' ? value : this.value; } }
}
// Check if this series has a gapsize override
if (s.gapsize) {
options.plotOptions.area.gapSize = s.gapsize;
options.plotOptions.line.gapSize = s.gapsize;
options.plotOptions.spline.gapSize = s.gapsize;
options.plotOptions.scatter.gapSize = s.gapsize;
}
// If this chart is a mirrored chart, make the yAxis labels non-negative
if ( s.mirrored_value ) {
belchertown_debug( options.chart.renderTo + ": mirrored chart due to mirrored_value = true" );
options.yAxis[s.yAxis].labels = { formatter: function() { return Math.abs(this.value); } }
}
// Lastly, apply any numberFormat label overrides
if ( typeof s.numberFormat !== "undefined" && Object.keys(s.numberFormat).length >= 1 ) {
var { decimals, decimalPoint, thousandsSep } = s.numberFormat
options.yAxis[this_yAxis].labels = { formatter: function () { return Highcharts.numberFormat(this.value, decimals, decimalPoint, thousandsSep); } }
}
});
// If windRose is present, configure a special chart to show that data
if (observation_type == "windRose") {
#if isinstance($ordinate_names[1], str) is True:
## Python 3 unicode string, don't change anything
var categories = $ordinate_names;
#else
## Python 2 hack to convert backslash unicode to html char
var categories = #echo [x.encode('ascii', 'xmlcharrefreplace') for x in $ordinate_names] #;
#end if
options.chart.className = "highcharts-windRose"; // Used for dark mode
options.chart.type = "column";
options.chart.polar = true;
options.chart.alignTicks = false;
options.pane = { size: '80%' }
// Reset xAxis and rebuild
options.xAxis = {}
options.xAxis.min = 0;
options.xAxis.max = 16;
options.xAxis.crosshair = true;
options.xAxis.categories = categories;
options.xAxis.tickmarkPlacement = 'on';
//options.xAxis.labels = { useHTML: true } // Option disabled in highcharts 8
//options.legend.align = "right";
options.legend.verticalAlign = "top";
options.legend.x = 210;
options.legend.y = 119;
options.legend.layout = "vertical";
options.legend.floating = true;
options.yAxis[0].min = 0;
options.yAxis[0].endOnTick = false;
options.yAxis[0].reversedStacks = false;
options.yAxis[0].title.text = "$obs.label.graphs_windrose_frequency (%)";
options.yAxis[0].gridLineWidth = 0;
options.yAxis[0].labels = { enabled: false }
options.yAxis[0].zIndex = 800;
options.plotOptions = { column: {
stacking: 'normal',
shadow: false,
groupPadding: 0,
pointPlacement: 'on',
}
}
// Reset the tooltip
options.tooltip = {}
options.tooltip.shared = true;
options.tooltip.valueSuffix = '%';
options.tooltip.followPointer = true;
options.tooltip.useHTML = true;
// Since wind rose is a special observation, I did not re-do the JSON arrays to accomodate it as a separate array.
// So we need to grab the data array within the series and save it to a temporary array, delete the entire chart series,
// and reapply the windrose data back to the series.
var newSeries = options.series[0].data;
options.series = [];
newSeries.forEach( ns => {
options.series.push(ns);
});
}
// If Hays chart is present, configure a special chart to show that data
if (observation_type == "haysChart") {
options.chart.type = "arearange"
options.chart.polar = true;
options.plotOptions = {
turboThreshold: 0,
series: {
marker: {
enabled: false
}
}
};
// Find min and max of the series data for the yAxis min and max
var maximum_flattened = [];
options.series[0].data.forEach( seriesData => {
maximum_flattened.push(seriesData[2]);
});
var range_max = Math.max(...maximum_flattened);
if (options.series[0].yAxis_softMax) {
var range_max = options.series[0].yAxis_softMax;
}
options.legend = { "enabled": false }
options.yAxis = {
showFirstLabel: false,
tickInterval: 2,
tickmarkPlacement: 'on',
min: -1,
softMax: range_max,
title: {
text: options.series[0].yAxis_label,
},
labels: {
align: 'center',
x: 0,
y: 0
},
}
options.tooltip = {
split: false,
shared: true,
followPointer: true,
useHTML: true,
formatter: function (tooltip) {
return this.points.map(function (point) {
var rounding = point.series.userOptions.rounding;
var mirrored = point.series.userOptions.mirrored_value;
var numberFormat = point.series.userOptions.numberFormat ? point.series.userOptions.numberFormat : "";
return "" + moment.unix( point.x / 1000).utcOffset($moment_js_utc_offset).format( tooltip_date_format ) + " \u25CF $obs.label.highest_temperature: " + highcharts_tooltip_factory( point.point.high, observation_type, true, rounding, mirrored, numberFormat ) + " \u25CF $obs.label.lowest_temperature: " + highcharts_tooltip_factory( point.point.low, observation_type, true, rounding, mirrored, numberFormat );
});
}
}
var currentSeries = options.series;
var currentSeriesData = options.series[0].data;
var range_unit = options.series[0].range_unit;
var newSeriesData = [];
var currentSeriesColor = options.series[0].color;
currentSeriesData.forEach( seriesData => {
newSeriesData.push({
x: seriesData[0],
low: seriesData[1],
high: seriesData[2],
});
});
options.series = [];
options.series.push({
data: newSeriesData,
obsType: "haysChart",
obsUnit: range_unit,
color: currentSeriesColor,
fillColor: currentSeriesColor,
connectEnds: false,
});
}
// If weather range is present, configure a special chart to show that data
// https://www.highcharts.com/blog/tutorials/209-the-art-of-the-chart-weather-radials/
if (observation_type == "weatherRange") {
options.chart.type = "columnrange";
// If polar is defined, use it and add a special dark mode CSS class
if ( JSON.parse( String( options.series[0].polar.toLowerCase() ) ) ) {
options.chart.polar = true; // Make sure the option is a string, then convert to bool
options.chart.className = "highcharts-weatherRange belchertown-polar"; // Used for dark mode
} else {
options.chart.className = "highcharts-weatherRange"; // Used for dark mode
}
options.legend = { "enabled": false }
// Find min and max of the series data for the yAxis min and max
var minimum_flattened = [];
var maximum_flattened = [];
options.series[0].data.forEach( seriesData => {
minimum_flattened.push(seriesData[1]);
maximum_flattened.push(seriesData[2]);
});
var range_min = Math.min(...minimum_flattened);
var range_max = Math.max(...maximum_flattened);
var yAxis_tickInterval = Math.ceil( Math.round(range_max / 5) / 5 ) * 5; // Divide max outTemp by 5 and round it, then round that value up to the nearest 5th multiple. This gives clean yAxis tick lines.
options.yAxis = {
showFirstLabel: true,
tickInterval: yAxis_tickInterval,
min: range_min,
max: range_max,
title: {
text: options.series[0].yAxis_label,
},
}
options.xAxis = {}
options.xAxis = {
labels: {
format: "{value: %b}"
},
tickInterval: 2592000000, // 30 days
showLastLabel: true,
crosshair: true,
type: "datetime"
}
options.plotOptions = {}
options.plotOptions = {
series: {
turboThreshold: 0,
stacking: "normal",
showInLegend: false,
borderWidth: 0,
}
}
options.tooltip = {
split: false,
shared: true,
followPointer: true,
useHTML: true,
formatter: function (tooltip) {
return this.points.map(function (point) {
var rounding = point.series.userOptions.rounding;
var mirrored = point.series.userOptions.mirrored_value;
var numberFormat = point.series.userOptions.numberFormat ? point.series.userOptions.numberFormat : "";
return "" + moment.unix( point.x / 1000).utcOffset($moment_js_utc_offset).format( tooltip_date_format ) + " \u25CF $obs.label.highest_temperature: " + highcharts_tooltip_factory( point.point.high, observation_type, true, rounding, mirrored, numberFormat ) + " \u25CF $obs.label.lowest_temperature: " + highcharts_tooltip_factory( point.point.low, observation_type, true, rounding, mirrored, numberFormat ) + " \u25CF $obs.label.average_temperature: " + highcharts_tooltip_factory( point.point.average, observation_type, true, rounding, mirrored, numberFormat );
});
}
}
// Update data
var currentSeries = options.series;
var currentSeriesData = options.series[0].data;
var range_unit = options.series[0].range_unit;
var newSeriesData = [];
currentSeriesData.forEach( seriesData => {
if ( options.series[0].color ) {
var color = options.series[0].color;
} else {
// Set color of the column based on the average temperature, or return default if not temperature
var color = get_outTemp_color( range_unit, seriesData[3], true );
// Set color of the column based on the Windspeed, or return default if not temperature
var color = get_windspeed_color( range_unit, seriesData[3], true );
}
newSeriesData.push({
x: seriesData[0],
low: seriesData[1],
high: seriesData[2],
average: seriesData[3],
color: color
});
});
options.series = [];
options.series.push({
data: newSeriesData,
obsType: "weatherRange",
obsUnit: range_unit
});
}
// Apply any width, height CSS overrides to the parent div of the chart
if ( css_height != "" ) {
jQuery("#"+options.chart.renderTo).parent().css({
'height' : css_height,
'padding' : '0px 15px',
'margin-bottom' : '20px'
});
}
if ( css_width != "" ) {
jQuery("#"+options.chart.renderTo).parent().css('width', css_width);
}
if ( credits != "highcharts_default" ) {
options.credits.text = credits;
}
if ( credits_url != "highcharts_default" ) {
options.credits.href = credits_url;
}
if ( credits_position != "highcharts_default" ) {
options.credits.position = JSON.parse(credits_position);
}
// Finally all options are done, now show the chart
var chart = new Highcharts.chart(options);
// If using debug, show a copy paste debug for use with jsfiddle
belchertown_debug("Highcharts.chart('container', " + JSON.stringify(options) + ");");
});
});
};