var pages = ["graphs", "records", "reports", "about"]; // 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(/\/$/, ""); var 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 = 0; belchertown_debug("Debug: skin.conf belchertown_debug enabled"); } var moment_locale = "en-US"; moment.locale(moment_locale); var graphgroups_raw = {"homepage": ["chart1", "chart2", "chart3", "chart4"], "day": ["chart1", "chart2", "chart3", "chart4"], "week": ["chart1", "chart2", "chart3", "chart4"], "month": ["chart1", "chart2", "chart3", "chart4"], "year": ["chart1", "chart2", "chart3", "chart4"]}; var graphgroups_titles = {"homepage": "Homepage", "day": "Today", "week": "This Week", "month": "This Month", "year": "This Year"}; var 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') autoTheme(20, 33, 05, 41) } } // 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 } // 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); } }); }); // Run this on every page for dark mode if skin theme is auto ajaxweewx(); // 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; } // https://stackoverflow.com/a/53387532/1177153 // Archived: https://stackoverflow.com/a/52059759/1177153 (also https://helloacm.com/the-javascript-function-to-compare-version-number-strings/) function check_for_updates(oldVer, newVer) { // Treat non-numerical characters as lower version // Replacing them with a negative number based on charcode of each character function fix(s) { return "." + (s.toLowerCase().charCodeAt(0) - 99999) + "."; } oldVer = ("" + oldVer).replace(/[^0-9\.]/g, fix).split('.'); newVer = ("" + newVer).replace(/[^0-9\.]/g, fix).split('.'); var c = Math.max(newVer.length, oldVer.length); for (var i = 0; i < c; i++) { //convert to integer the most efficient way oldVer[i] = ~~oldVer[i]; newVer[i] = ~~newVer[i]; if (oldVer[i] < newVer[i]) return true; else if (oldVer[i] > newVer[i]) return false; } return false; } // http://stackoverflow.com/a/14887961/1177153 var weatherdirection = { 0: "N", 90: "E", 180: "S", 270: "W", 360: "N" }; // 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 ); } } 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 = "N"; // N } else if (obsvalue >= 11.26 && obsvalue <= 33.75) { ordinal = "NNE"; // NNE } else if (obsvalue >= 33.76 && obsvalue <= 56.25) { ordinal = "NE"; // NE } else if (obsvalue >= 56.26 && obsvalue <= 78.75) { ordinal = "ENE"; // ENE } else if (obsvalue >= 78.76 && obsvalue <= 101.25) { ordinal = "E"; // E } else if (obsvalue >= 101.26 && obsvalue <= 123.75) { ordinal = "ESE"; // ESE } else if (obsvalue >= 123.76 && obsvalue <= 146.25) { ordinal = "SE"; // SE } else if (obsvalue >= 146.26 && obsvalue <= 168.75) { ordinal = "SSE"; // SSE } else if (obsvalue >= 168.76 && obsvalue <= 191.25) { ordinal = "S"; // S } else if (obsvalue >= 191.26 && obsvalue <= 213.75) { ordinal = "SSW"; // SSW } else if (obsvalue >= 213.76 && obsvalue <= 236.25) { ordinal = "SW"; // SW } else if (obsvalue >= 236.26 && obsvalue <= 258.75) { ordinal = "WSW"; // WSW } else if (obsvalue >= 258.76 && obsvalue <= 281.25) { ordinal = "W"; // W } else if (obsvalue >= 281.26 && obsvalue <= 303.75) { ordinal = "WNW"; // WNW } else if (obsvalue >= 303.76 && obsvalue <= 326.25) { ordinal = "NW"; // NW } else if (obsvalue >= 326.26 && obsvalue <= 348.75) { ordinal = "NNW"; // NNW } else if (obsvalue >= 348.76 && obsvalue <= 360) { ordinal = "N"; // 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 = "."; } // If thousandsSep is undefined, use the auto detect from the skin since this comes from the skin. if ( typeof thousandsSep === "undefined" ) { thousandsSep = ","; } 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 spin back around // https://stackoverflow.com/a/19872672/1177153 function rotateThis(newRotation) { if ( newRotation == "N/A") { return; } 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"); jQuery("#themeSwitch").prop( "checked", true ); sessionStorage.setItem('currentTheme', 'dark'); } else if ( themeName == "light" ) { // Apply light theme jQuery('body').addClass("light"); jQuery('body').removeClass("dark"); jQuery("#themeSwitch").prop( "checked", false ); 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"); // Check for updates var update_available = check_for_updates("1.1", data["version"]["github_version"]); belchertown_debug("Checking Update: update available: " + update_available + " (installed: 1.1 - new version: "+data["version"]["github_version"]+")"); if ( update_available ) { jQuery(".updateavailable").show(); } // Auto theme if enabled autoTheme(data["almanac"]["sunset_hour"], data["almanac"]["sunset_minute"], data["almanac"]["sunrise_hour"], data["almanac"]["sunrise_minute"], ); //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(-240.0).format( 'dddd, LL' ) ); jQuery(".snapshot-records-month-header").html( moment.unix( data["current"]["epoch"] ).utcOffset(-240.0).format( 'MMMM YYYY' ) ); 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(-240.0).format( "LT" ) ); jQuery(".sunset-value").html( moment.unix( parseFloat(data["almanac"]["sunset_epoch"]).toFixed(0) ).utcOffset(-240.0).format( "LT" ) ); // Moon icon, phase and illumination percent switch ( data["almanac"]["moon"]["moon_index"] ) { case "0": jQuery(".moon-icon").html( "
" ); break; case "1": jQuery(".moon-icon").html( "
" ); break; case "2": jQuery(".moon-icon").html( "
" ); break; case "3": jQuery(".moon-icon").html( "
" ); break; case "4": jQuery(".moon-icon").html( "
" ); break; case "5": jQuery(".moon-icon").html( "
" ); break; case "6": jQuery(".moon-icon").html( "
" ); break; case "7": jQuery(".moon-icon").html( "
" ); break; } jQuery(".moon-phase").html( titleCase( data["almanac"]["moon"]["moon_phase"] ) ); // Javascript function above jQuery(".moon-visible").html( "" + data["almanac"]["moon"]["moon_fullness"] + "% visible" ); } Highcharts.setOptions({ global: { //useUTC: false timezoneOffset: 240.0 }, lang: { decimalPoint: '.', thousandsSep: ',' } }); 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 "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: true }, 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(-240.0).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(-240.0).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") { var categories = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N/A']; 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 } //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 = "Frequency (%)"; options.yAxis[0].gridLineWidth = 0; 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 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(-240.0).format( tooltip_date_format ) + "
\u25CF High: " + highcharts_tooltip_factory( point.point.high, observation_type, true, rounding, mirrored, numberFormat ) + "
\u25CF Low: " + highcharts_tooltip_factory( point.point.low, observation_type, true, rounding, mirrored, numberFormat ) + "
\u25CF Average: " + 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 ); } 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) + ");"); }); }); };