// Copyright 2012 Google Inc. All Rights Reserved. /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @author * Shan Aminzadeh, shan.aminzadeh@gmail.com * Arya Bondarian, aryabond@gmail.com * Albert Gau, agau@uci.edu * Travis Lai, travisrlai@gmail.com * Daniel Nguyen, danielnuwin@gmail.com * Nick Mihailovski, api.nickm@gmail.com * * @fileoverview * This library is designed to create an easier way to build a custom * Google Analytics Dashboard by visualizing data from Google Analytics * API with the Google Chart Tools. */ // Loads the core chart and table from the Google Visualization. google.load('visualization', '1', {'packages': ['corechart', 'table']}); // Create namespace for this library if not already created. var gadash = gadash || {}; // Namespace for util object. Contains lots of library utilities. gadash.util = gadash.util || {}; // Boolean that checks to see if gapi client is loaded. gadash.isLoaded = false; /** * Refers to the Google Analytics API scope that the user will need * authentication for. * @const {String} */ gadash.SCOPE = 'https://www.googleapis.com/auth/analytics.readonly'; /** * List of functions that are queued for execution. This is only used * until all the libraries have fully loaded. * @type {Array} */ gadash.commandQueue = []; /** * Callback executed once the Google APIs Javascript client library has loaded. * The function name is specified in the onload query parameter of URL to load * this library. After 1 millisecond, checkAuth is called. */ window.gadashInit = function() { gapi.client.setApiKey(gadash.apiKey); window.setTimeout(gadash.checkAuth, 1); }; /** * Sets the API key and Client ID passed by the user. * This information can be found in your Google API Console. * @param {Object} settings - Contains the API Key and Client ID variables. */ gadash.configKeys = function(settings) { gadash.apiKey = settings.apiKey; gadash.clientId = settings.clientId; }; /** * Uses the OAuth2.0 clientId to query the Google Accounts service * to see if the user has authorized. Once complete, handleAuthResults is * called. */ gadash.checkAuth = function() { gapi.auth.authorize({ client_id: gadash.clientId, scope: gadash.SCOPE, immediate: true}, gadash.handleAuthResult); }; /** * Handler that is called once the script has checked to see if the user has * authorized access to their Google Analytics data. If the user has authorized * access, the analytics api library is loaded and the handleAuthorized * function is executed. If the user has not authorized access to their data, * the handleUnauthorized function is executed. * @param {Object} authResult The result object returned form the authorization * service that determine whether the user has currently authorized access * to their data. If it exists, the user has authorized access. */ gadash.handleAuthResult = function(authResult) { if (authResult) { gapi.client.setApiVersions({'analytics': 'v3'}); gapi.client.load('analytics', 'v3', gadash.handleAuthorized); } else { gadash.handleUnAuthorized(); } }; /** * Updates the UI once the user has authorized this script to access their * data by hiding the authorize button. Also, runs executeCommandQueue * function to render all charts in the commandQueue. The execution of the * command queue only happens once. */ gadash.handleAuthorized = function() { var authorizeButton = document.getElementById('authorize-button'); authorizeButton.style.visibility = 'hidden'; gadash.isLoaded = true; gadash.executeCommandQueue(); }; /** * Updates the UI if a user has not yet authorized this script to access * their Google Analytics data. This function changes the visibility of * some elements on the screen. It also adds the handleAuthClick * click handler to the authorize-button. */ gadash.handleUnAuthorized = function() { var authorizeButton = document.getElementById('authorize-button'); authorizeButton.style.visibility = ''; authorizeButton.onclick = gadash.handleAuthClick; }; /** * Checks to see if user is authenticated, calls handleAuthResult * @return {boolean} false. * @param {Object} event - event when button is clicked. */ gadash.handleAuthClick = function(event) { gapi.auth.authorize({ client_id: gadash.clientId, scope: gadash.SCOPE, immediate: false}, gadash.handleAuthResult); return false; }; /** * Iterates through all commands on the commandQueue and executes them. */ gadash.executeCommandQueue = function() { for (var i = 0, command; command = gadash.commandQueue[i]; ++i) { command(); } }; /** * A Chart object is the primary object in this library. * A Chart accepts an optional configuration object that contains all the * parameters of the chart. Also changes start and end date of * the query, if last-n-days is set in the config. * @param {?Object} config - Contains all configuration variables * of a Chart object. This parameter is passed by value, and a deep * copy is made. Once set, the original object can be modified and * it will not affect this object. * @return {Object} this Returns a reference to the newly instantiated * Chart instance. Useful for chaining methods together. * @constructor */ gadash.Chart = function(opt_config) { /** * The main configuration object. * @type {Object} */ this.config = {}; if (opt_config) { gadash.util.extend(opt_config, this.config); } return this; }; /** * Extends the values in the chart's config object with the keys in * the config parameters. If a key in config already exists in the chart, * and the value is not an object, the new value overwrites the old. * @param {Object} config The config object to set inside this object. * @return {Object} The current instance of the Chart object. Useful * for chaining methods. */ gadash.Chart.prototype.set = function(config) { gadash.util.extend(config, this.config); return this; }; /** * First checks to see if the GA library is loaded. If it is then the * chart can be rendered right away. Otherwise, other operations are queued, * so the render command is pushed to the command queue to be executed in * the same order as originally called. * @this Points to the current chart instance. * @return {Object} The current instance of this chart object. Useful for * chaining methods. */ gadash.Chart.prototype.render = function() { // If the client library has loaded. if (gadash.isLoaded) { this.renderFunction(); } else { var renderFunction = gadash.util.bindMethod(this, this.renderFunction); gadash.commandQueue.push(renderFunction); } return this; }; /** * Makes a request to the Google Analytics API. * Updates the start and end date if last-n-days * has been set. The function also creates and executes a Google Analytics * API request using the Chart objects callback method. The callback * is bound to the Chart instance so a reference back to this chart is * maintained within the callback. */ gadash.Chart.prototype.renderFunction = function() { // Update the start and end dates based on last n days. if (this.config['last-n-days']) { this.config.query['end-date'] = gadash.util.lastNdays(0); this.config.query['start-date'] = gadash.util.lastNdays(this.config['last-n-days']); } var request = gapi.client.analytics.data.ga.get(this.config.query); request.execute(gadash.util.bindMethod(this, this.callback)); } /** * Callback function that is called after a GA query is executed. * First, the function checks to see if there are any errors on the * response. Then check to see if a onSuccess function was declared * in the config. If present, call onSuccess by first binding it to * this (ie this chart object instance). If not defined, just use * the default callback. The entire JSON response from the API * is passed to either defined or default callback. * @param {Object} response - Google Analytics API JSON response. */ gadash.Chart.prototype.callback = function(response) { if (response.error) { this.defaultOnError(response.error.code + ' ' + response.error.message); } else { if (this.config.onSuccess) { gadash.util.bindMethod(this, this.config.onSuccess)(response); } else { this.defaultOnSuccess(response); } } }; /** * Checks to see if onError parameter is set in config. If it is, * use the user defined error function else check to see if an error * div is created. If not, create an error div. Print error message * to the error div. * @param {String} message - error message to print. */ gadash.Chart.prototype.defaultOnError = function(message) { // If onError param exists, use that as error handling function. if (this.config.onError) { this.config.onError(message); } else { var errorDiv = document.getElementById('errors'); // Create error div if not already made. if (!errorDiv) { errorDiv = document.createElement('div'); errorDiv.style.color = 'red'; errorDiv.setAttribute('id', 'errors'); errorDiv.innerHTML = 'ERRORS:' + '
'; document.body.appendChild(errorDiv); } // Prints chart divContainer and message to error div. errorDiv.innerHTML += this.config.divContainer + ' error: ' + message + '
'; } }; /** * Default callback for creating Google Charts with a response. First, the * response is put into a DataTable object Second, the corresponding chart * is returned. The two are then combined to draw a chart that is populated * with the GA data. * @param {Object} resp - A Google Analytics API JSON response. */ gadash.Chart.prototype.defaultOnSuccess = function(resp) { var dataTable = gadash.util.getDataTable(resp, this.config.type); var chart = gadash.util.getChart(this.config.divContainer, this.config.type); gadash.util.draw(chart, dataTable, this.config.chartOptions); }; /** * Creates a DataTable object using a GA response. * @param {Object} resp - A Google Analytics response. * @param {?String} opt_chartType - The chart type. Provides a hint on * how to parse the API results into a data table. * @return {Object} data - A Google DataTable object populated * with the GA response data. */ gadash.util.getDataTable = function(resp, opt_chartType) { var chartType = opt_chartType || false; var data = new google.visualization.DataTable(); var numOfColumns = resp.columnHeaders.length; var numOfRows; // Throw an error if there are no rows returned. if (resp.rows && resp.rows.length) { numOfRows = resp.rows.length; } else { this.defaultOnError('No rows returned for that query.'); } /* * Looks at the resp column headers to set names and types for each column. * Since bar and column chart don't support date object, set type as string * rather than a Date. */ for (var i = 0; i < numOfColumns; i++) { var dataType = resp.columnHeaders[i].dataType; var name = resp.columnHeaders[i].name; if (name == 'ga:date' && !(chartType == 'ColumnChart' || chartType == 'BarChart')) { dataType = 'date'; } else if (dataType == 'STRING') { dataType = 'string'; } else { dataType = 'number'; } data.addColumn(dataType, gadash.util.formatGAString(name)); } /* * Populates the rows by using the resp.rows array. If the type * is an int then parse the INT. If it is a percent, then round * to last two decimal places and store as INT. */ for (var i = 0; i < numOfRows; i++) { var arrayMetrics = []; for (var j = 0; j < numOfColumns; j++) { var name = resp.columnHeaders[j].name; var dataType = resp.columnHeaders[j].dataType; if (name == 'ga:date' && !(chartType == 'ColumnChart' || chartType == 'BarChart')) { arrayMetrics.push(gadash.util.stringToDate(resp.rows[i][j])); } else if (dataType == 'INTEGER') { arrayMetrics.push(parseInt(resp.rows[i][j])); } else if (dataType == 'CURRENCY' ) { arrayMetrics.push(parseFloat(resp.rows[i][j])); } else if (dataType == 'PERCENT' || dataType == 'TIME' || dataType == 'FLOAT') { arrayMetrics.push(Math.round((resp.rows[i][j]) * 100) / 100); } else { arrayMetrics.push(resp.rows[i][j]); } } data.addRow(arrayMetrics); } /* * Iterates through each column in the data table and formats * any column that has a CURRENCY datatype to two decimal places * and a '$' before the amount. */ for (var i = 0; i < numOfColumns; i++) { var dataType = resp.columnHeaders[i].dataType; if (dataType == 'CURRENCY') { var formatter = new google.visualization.NumberFormat( {fractionDigits: 2}); formatter.format(data, i); } } return data; }; /** * Checks to see if the type of chart in the config is valid. * If it is, get its chart instance, else return a Table instance. * @param {String} id The ID of the HTML element in which to render * the chart. * @param {String} chartType The type of the Chart to render. * @return {Object} visualization - returns the Chart instance. */ gadash.util.getChart = function(id, chartType) { var elem = document.getElementById(id); if (google.visualization[chartType]) { return new google.visualization[chartType](elem); } return new google.visualization.Table(elem); }; /** * Draws a chart to its declared div using a DataTable. * @param {Object} chart - The Chart instance you wish to draw the data into. * @param {Object} dataTable - The Google DataTable object holding * the response data. * @param {Object} options - The optional configuration parameters to pass * into the chart. */ gadash.util.draw = function(chart, dataTable, chartOptions) { chart.draw(dataTable, chartOptions); }; /** * Binds a method to its object. * @param {Object} object The main object to bind to. * @param {Object} method The method to bind to the object. * @return {function} the function passed in boound to the object parameter. */ gadash.util.bindMethod = function(object, method) { return function() { return method.apply(object, arguments); }; }; /** * Utility method to return the lastNdays from today in the format yyyy-MM-dd. * @param {Number} n The number of days in the past from tpday that we should * return a date. Value of 0 returns today. * @return {String} date - The adjusted date value represented as a String. */ gadash.util.lastNdays = function(n) { var today = new Date(); var before = new Date(); before.setDate(today.getDate() - n); var year = before.getFullYear(); var month = before.getMonth() + 1; if (month < 10) { month = '0' + month; } var day = before.getDate(); if (day < 10) { day = '0' + day; } return [year, month, day].join('-'); }; /** * Utility method to return Date from a String in the format yyyy-MM-dd. * This function is used for a Chart that has a Time Series. * @param {String} date - The String representation of the date. * @return {Date} date - Corresponding JS Date object. */ gadash.util.stringToDate = function(date) { var year = date.substring(0, 4); var month = date.substring(4, 6); var day = date.substring(6, 8); if (month < 10) { month = month.substring(1, 2); } month = month - 1; if (day < 10) { day = day.substring(1, 2); } var dateObj = new Date(year, month, day); return dateObj; }; /** * Formats the Google Metrics and Dimensions into readable strings * Strips away the 'ga' and capitalizes first letter. Also puts a space * between any lowercase and capital letters. * ie: "ga:percentNewVisits" ---> "Percent New Visits" * @param {String} gaString - the String name of Metric/Dimension from GA. * @return {String} newString - Metric/Dimension formatted nicely. */ gadash.util.formatGAString = function(gaString) { var newString = gaString.substring(3); newString = newString.charAt(0).toUpperCase() + newString.slice(1); // Check for a capital letter in the string. If found, // put a space between that char and the char before it. for (var i = 1; i < newString.length; i++) { if (newString.charAt(i) == newString.charAt(i).toUpperCase()) { var left = newString.substring(0, i); var right = newString.substring(i, newString.length); newString = [left, right].join(' '); i++; } } return newString; }; /** * Recursively copies the values in the from object into the to object. * If a key in from object already exists, and has child values, * the child values are copied over. So: * extend({'a': {'b': 2}}, {'a': {'c': 1}}) will result in: * {'a': {'b': 2, 'c': 1}} * Once run, modifying the from object will not impact the to object. * NOTE: Arrays will write over each other. * NOTE: This is unsafe in that circular references are not checked. Calling * this method with a circular reference could cause an infinite loop. * @param {Object} from The object to copy values from. * @param {Object} to The object to copy values into. */ gadash.util.extend = function(from, to) { for (var key in from) { var type = gadash.util.getType(from[key]); if (type == 'object') { to[key] = to[key] || {}; gadash.util.extend(from[key], to[key]); } else { to[key] = from[key]; } } } /** * Returns the native type (class property) of this object. * General idea grabbed from here: http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/ * Per ECMA-262: * * 15.2.4.2 Object.prototype.toString ( ) * When the toString method is called, the following steps are taken: * 1. Get the [[Class]] property of this object. * 2. Compute a string value by concatenating the three * strings "[object ", | Result(1), and "]". * 3. Return Result(2). * * @param {Object} value Any type. * @returns {String} The lower class property of the object. Undefined if value * is undefined or null. */ gadash.util.getType = function(value) { var classStringName = Object.prototype.toString.call(value); return ({ '[object Boolean]': 'boolean', '[object Number]': 'number', '[object String]': 'string', '[object Array]': 'array', '[object Date]': 'date', '[object RegExp]': 'regex', '[object Object]' : 'object' })[classStringName]; }