xref: /webtrees/resources/js/statistics.js (revision 5bfc689774bb9a6401271c4ed15a6d50652c991b)
1/**
2 * webtrees: online genealogy
3 * Copyright (C) 2022 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