1/** 2 * webtrees: online genealogy 3 * Copyright (C) 2019 webtrees development team 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * This program is distributed in the hope that it will be useful, 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * GNU General Public License for more details. 12 * You should have received a copy of the GNU General Public License 13 * along with this program. If not, see <http://www.gnu.org/licenses/>. 14 */ 15 16'use strict'; 17 18const GOOGLE_CHARTS_LIB = 'https://www.gstatic.com/charts/loader.js'; 19 20/** 21 * Statistics class. 22 */ 23class Statistics { 24 /** 25 * Constructor. 26 * 27 * @returns {Statistics} 28 */ 29 constructor () { 30 // Create singleton instance 31 if (!Statistics.instance) { 32 Statistics.instance = this; 33 34 this.callbacks = []; 35 this.initialized = false; 36 this.loading = false; 37 } 38 39 return Statistics.instance; 40 } 41 42 /** 43 * Initializes the google chart engine. Loads the chart lib only once. 44 * 45 * @param {String} locale - Locale, e.g. en, de, ... 46 */ 47 init (locale) { 48 if (this.loading || this.initialized) { 49 return; 50 } 51 52 var that = this; 53 54 Promise.all([ 55 this.load(GOOGLE_CHARTS_LIB) 56 ]).then(() => { 57 google.charts.load( 58 'current', 59 { 60 packages: [ 61 'corechart', 62 'geochart', 63 'bar' 64 ], 65 language: locale, 66 // Note: you will need to get a mapsApiKey for your project. 67 // See: https://developers.google.com/chart/interactive/docs/basic_load_libs#load-settings 68 mapsApiKey: '' 69 } 70 ); 71 72 google.charts.setOnLoadCallback(function () { 73 that.callbacks.forEach((element) => { 74 element(); 75 }); 76 }); 77 78 that.initialized = true; 79 }).catch((error) => { 80 console.log(error); 81 }); 82 } 83 84 /** 85 * Dynamically loads a script by the given URL. 86 * 87 * @param {String} url 88 * @returns {Promise} 89 */ 90 load (url) { 91 if (this.loading) { 92 return; 93 } 94 95 this.loading = true; 96 97 return new Promise(function (resolve, reject) { 98 const script = document.createElement('script'); 99 100 script.async = true; 101 script.onload = function () { 102 resolve(url); 103 }; 104 script.onerror = function () { 105 reject(url); 106 }; 107 108 script.src = url; 109 document.body.appendChild(script); 110 }); 111 } 112 113 /** 114 * Adds the given callback method to the callback stack or add it directly to 115 * the google charts interface once the chart engine is up and running. 116 * 117 * @param {Function} callback 118 */ 119 addCallback (callback) { 120 if (this.initialized) { 121 google.charts.setOnLoadCallback(callback); 122 } else { 123 this.callbacks.push(callback); 124 } 125 126 $(window).resize(function () { 127 callback(); 128 }); 129 } 130 131 /** 132 * Draws a google chart. 133 * 134 * @param {String} containerId 135 * @param {String} chartType 136 * @param {Array} data 137 * @param {Object} options 138 */ 139 drawChart (containerId, chartType, data, options) { 140 const dataTable = google.visualization.arrayToDataTable(data); 141 142 const wrapper = new google.visualization.ChartWrapper({ 143 chartType: chartType, 144 dataTable: dataTable, 145 options: options, 146 containerId: containerId 147 }); 148 149 wrapper.draw(); 150 } 151 152 /** 153 * Draws a pie chart. 154 * 155 * @param {String} elementId - The element id of the HTML element the chart is rendered too 156 * @param {Array} data - The chart data array 157 * @param {Object} options - The chart specific options to overwrite the default ones 158 */ 159 drawPieChart (elementId, data, options) { 160 // Default chart options 161 const defaults = { 162 title: '', 163 height: '100%', 164 width: '100%', 165 pieStartAngle: 0, 166 pieSliceText: 'none', 167 pieSliceTextStyle: { 168 color: '#777' 169 }, 170 pieHole: 0.4, // Donut 171 // is3D: true, // 3D (not together with pieHole) 172 legend: { 173 alignment: 'center', 174 // Flickers on mouseover :( 175 labeledValueText: 'value', 176 position: 'labeled' 177 }, 178 chartArea: { 179 left: 0, 180 top: '5%', 181 height: '90%', 182 width: '100%' 183 }, 184 tooltip: { 185 trigger: 'none', 186 text: 'both' 187 }, 188 backgroundColor: 'transparent', 189 colors: [] 190 }; 191 192 // Merge default with provided options 193 options = Object.assign(defaults, options); 194 195 // Create and draw the chart 196 this.drawChart(elementId, 'PieChart', data, options); 197 } 198 199 /** 200 * Draws a column chart. 201 * 202 * @param {String} elementId - The element id of the HTML element the chart is rendered too 203 * @param {Array} data - The chart data array 204 * @param {Object} options - The chart specific options to overwrite the default ones 205 */ 206 drawColumnChart (elementId, data, options) { 207 // Default chart options 208 const defaults = { 209 title: '', 210 subtitle: '', 211 titleTextStyle: { 212 color: '#757575', 213 fontName: 'Roboto', 214 fontSize: '16px', 215 bold: false, 216 italic: false 217 }, 218 height: '100%', 219 width: '100%', 220 vAxis: { 221 title: '' 222 }, 223 hAxis: { 224 title: '' 225 }, 226 legend: { 227 position: 'none' 228 }, 229 backgroundColor: 'transparent' 230 }; 231 232 // Merge default with provided options 233 options = Object.assign(defaults, options); 234 235 // Create and draw the chart 236 this.drawChart(elementId, 'ColumnChart', data, options); 237 } 238 239 /** 240 * Draws a combo chart. 241 * 242 * @param {String} elementId - The element id of the HTML element the chart is rendered too 243 * @param {Array} data - The chart data array 244 * @param {Object} options - The chart specific options to overwrite the default ones 245 */ 246 drawComboChart (elementId, data, options) { 247 // Default chart options 248 const defaults = { 249 title: '', 250 subtitle: '', 251 titleTextStyle: { 252 color: '#757575', 253 fontName: 'Roboto', 254 fontSize: '16px', 255 bold: false, 256 italic: false 257 }, 258 height: '100%', 259 width: '100%', 260 vAxis: { 261 title: '' 262 }, 263 hAxis: { 264 title: '' 265 }, 266 legend: { 267 position: 'none' 268 }, 269 seriesType: 'bars', 270 series: { 271 2: { 272 type: 'line' 273 } 274 }, 275 colors: [], 276 backgroundColor: 'transparent' 277 }; 278 279 // Merge default with provided options 280 options = Object.assign(defaults, options); 281 282 // Create and draw the chart 283 this.drawChart(elementId, 'ComboChart', data, options); 284 } 285 286 /** 287 * Draws a geo chart. 288 * 289 * @param {String} elementId - The element id of the HTML element the chart is rendered too 290 * @param {Array} data - The chart data array 291 * @param {Object} options - The chart specific options to overwrite the default ones 292 */ 293 drawGeoChart (elementId, data, options) { 294 // Default chart options 295 const defaults = { 296 title: '', 297 subtitle: '', 298 height: '100%', 299 width: '100%' 300 }; 301 302 // Merge default with provided options 303 options = Object.assign(defaults, options); 304 305 // Create and draw the chart 306 this.drawChart(elementId, 'GeoChart', data, options); 307 } 308} 309 310// Create singleton instance of class 311const statistics = new Statistics(); 312