Google Analytics with Visualization Graphs (Javascript)

In the PHP page of this project, I expand on the project scenario, challenges, directives, and PHP code structure. The summary is that it originated in a company that hosted hundreds of company sites, and it was intended to give those company administrators a high level view of site visits, devices, and time of day visits. The server side code is built on OOP PHP and integrates the Google Analytics API with the Google Visualization API to output the graphs. What I’ll talk about on this page is what happens on the front end, both in requests and rendering.

If you haven’t yet, first have a look at the demo. This is not live data, it displays from a semi-static data set I built that uses no secure keys or data. Play around with it for a bit and see what it does. Resize your window and note there is some animation (see notes below.)

The Logic

  • A custom dimension exists in each of the companies Analytics Javascript, dimension3, that holds a reference to the company’s ID. Compose a front end request of post parameters to request dimensions and metrics for the current company from the Analytics API. Normally these parameters are the company ID, the start date, and graph type. Start date is not available in the demo due to the static nature of the mock response script.
  • The PHP page for this project discusses this at length, and how the server-side scripting accepts user input and retrieves this data.
  • Once the data is retrieved,
    • Modify the data to the appropriate format and data type for the requested graphs.
    • Modify the data for compatibility for the tooltips (details below.)
    • On change of DOM elements – start date change or graph type – determine if a new request is required or the graph can be redrawn with existing data. Animate the graph when it supports it.
  • Render the graphs in their appropriate containers.

The Challenges

  • Many of the data formats returned by Analytics don’t play nicely with the formats required by the Visualization API for the graphs and associated tooltips. There are multiple objects and methods to correctly map Anaytics data to the graphs.
  • For the sessions/users graphs, the way you are **supposed** to combine two data sets is to do a table join on the data, see documentation. That didn’t work for us, so there is a significant portion of the client side code dedicated to merging results so we could superimpose two data sets in one graph.

The Code and Structure

There are a number of Javascript objects at play in the application:

  • initialize-google-charts.js: This is the only non-object in the app. It contains two events, $(‘document’).ready, which calls function initializeCharts(), and $(window).resize which calls GaRenderCharts.queueChartReload(), the function that redraws the charts to fit the containers.
    
    /**
     * Attach event handlers and load up any included JS files. If we don't use
     * this approach, some scripts won't load properly and throws errors.
     *
     * @return void
     */
    function initializeCharts()
    {
        $.when(
            $.getScript(analytics_js_path + 'ga-chart-config.min.js'),
            $.getScript(analytics_js_path + 'ga-data-formatting.min.js'),
            $.getScript(analytics_js_path + 'ga-vis-chart-options.min.js'),
            $.getScript(analytics_js_path + 'ga-chart-gui.min.js'),
            $.getScript(analytics_js_path + 'ga-load-analytics.js'),
            $.getScript(analytics_js_path + 'ga-render-charts.min.js'),
            $.getScript(analytics_js_path + 'ga-multi-query-render.min.js'),
            $.Deferred(function (deferred)
        {
            $(deferred.resolve);
        })
        ).fail(function ()
        {
            console.log('Failed to load required scripts, check the paths');
        }).done(function ()
        {
    
            GaChartGui
            .setSelectObjects()
            .setDatePickers();
    
            GaChartConfig.setChartConfigs();
            // Once the scripts are fully loaded, send the entire config so all charts are queried.
            GaLoadAnalytics.loadAnalyticsData(ga_configs);
        });
    }
    
    Two items are of note in the initialization. The first is that I discovered early on if the various objects are not ready (files not loaded,) the graphs fail. This is the reason for the use of $.when() combined with getScript() – it does not attempt to load the graphs until we are sure all required objects are available. The second is why I love using objects, note we can chain multiple methods within an object with the dot separator.
  • GaChartConfig: This object contains all the static configurations for the initial post request to the server-side "controller". It contains all the methods and objects required to compose the initial request, and is updated as the user selects different graph types or a different start date. A sampling of GaChartConfig:
    
    var GaChartConfig =
    {
        /** @var array
         * default configuration values, some overwritten by DOM events.
         * sessions-seven-back is a second query pulled so we can
         * overlay a second set of lines for seven days ago.
         */
        ga_chart_defaults: [
            {
                'chart-name': 'sessions-users',
                'colcount': 3,
                'type': 'LineChart',
                'description': 'Overview of your Visitors'
            },
            {
                'chart-name': 'time-of-day',
                'colcount': 3,
                'type': 'HeatMap',
                'description': 'Visits by Time of Day'
            },
            {
                'chart-name': 'device-type',
                'colcount': 2,
                'type': 'Doughnut',
                'description': 'Computing Devices Visitors Use'
            },
            {
                'for': 'sessions-users',
                'chart-name': 'sessions-seven-back',
                'colcount': 4,
                'type': 'LineChart',
                'description': 'Visitors Seven Days Ago'
            }
        ],
    
        // Continued, other methods related to configuration
    };
    
  • GaDataFormat: This object is responsible for properly formatting data for display in all the charts on both initial load or when a new chart type is selected. As mentioned one of the challenges was that the date format returned by Analytics is sometimes incompatible with the chart itself or the tooltips in the chart (longer story, required tweaks to be compatible with the Data Formatters.) This object manages all the data to be compatible with the various charts.
    
    var GaDataFormat =
    {
        // Other methods and properties above . . .
    
        /**
         * Create a google visualization object for number or date format if it applies.
         * Creates/executes new google.visualization.NumberFormat([format pattern]) or
         * new google.visualization.DateFormat([format pattern])
         * https://developers.google.com/chart/interactive/docs/reference#dateformat
         *
         * @param configs object
         * @param col_index int
         * @return object| empty string
         */
        googleFormatObjects: function (configs, col_index)
        {
            var format_class  = GaDataFormat.setFormatterClass(configs.columns[col_index].name);
            var format        = GaDataFormat.setFormatPattern(configs.columns[col_index]);
    
            if ((format_class === '') || (format === '')) {
                return '';
            }
    
            return new google.visualization[format_class](format);
        },
    
        // Continued methods below ...
    };
    
  • VisualizationOptions: This object manages all the various options for the charts. Some are common to all charts, others are specific to a given chart, and some support animations from one state to the next. When the user changes chart types, this object references the data and sets the correct options. Among the attributes are line colors, opacity, line color/style/thickness, curveType, animation rates, and hole sizes for pie charts. A bit of this object that demonstrates the use of method chaining:
    
    var VisualizationOptions =
    {
        /** var object, store locally so we can chain and reduce code */
        opts: {},
    
        /**
         * Main access point to set chart configurations. Build options for
         * chart, which varies by chart type and even chart name. Note that
         * any formatting applies to only the H axis, to format the
         * mouseover tooltips see
         * GaDataFormat.addToolTipFormatting().
         *
         * @param configs configuration object
         * @param data array
         * @return opts array
         */
        buildChartOptions: function (configs, data)
        {
            VisualizationOptions
                .setCommonChartOptions(configs, data)
                .addLineChartOptions(configs)
                .addPieAndDoughnutOptions(configs)
                .addAnimationOptions(configs);
    
            return VisualizationOptions.opts;
        },
    
        // More methods follow ...
    };
    
  • GaChartGui: This object manages all GUI events and actions. The code is fairly typical, for example on load event handlers are attached and any messages or errors are dispatched to the DOM.
  • GaLoadAnalytics: This is the core controller of the charts which assembles user input, updates the config mentioned earlier, and acts as a controller to determine which rendering object to send the request to. The entry point accepts an instance of the GaChartConfig object to compose the correct params. If it is a single query and the promise from the AJAX query is complete, it passes the data to the GaRenderCharts. In a multi-query request it passes the config params to the GaMultiQueryRender object for query and rendering.
    
    var GaLoadAnalytics =
    {
        // Properties and methods preceding . . .
    
        /**
         * Main entry point: query Analytics for data. This will return multiple
         * reports in one query or a single report on change of a form element.
         *
         * @param configs object
         * @return this
         */
        loadAnalyticsData: function (configs)
        {
            GaChartGui.setPreloader(configs);
    
            if (GaMultiQueryRender.isMultipleQueriesInRequest(configs)) {
                GaMultiQueryRender.loadMultipleGaQueries(configs);
                return this;
            }
    
            GaLoadAnalytics.loadSingleQueryConfig(configs);
    
            return this;
        },
    
        // More methods follow ...
    };
    
    This bit of code demonstrates the rule I mention in I am a Recovered Else Addict, Don’t Use Else. Notice that if it’s a multi-render chart, call the multi render object’s render method and return. No "else" is needed, if it’s not a multi-render chart, load a single render result in this object.
  • GaRenderCharts: Most charts need only a single request and response to manage the data. This controller references all the objects mentioned to this point to set all chart attributes for the request and eventually calls the Google library charts.load() method to load the charts.
    
    var GaRenderCharts =
    {
        // Methods and properties precede . . .
    
        /**
         * Render a single chart.
         *
         * @param configs object - this is the sub array of parent ga_configs
         * @param data object, dataset (or subset if one) returned from GA query.
         * @return this
         */
        renderChart: function (configs, data)
        {
            var load_library = GaRenderCharts.setChartLibrary(configs['chart-name']);
    
            google.charts.load('current', {'packages': [load_library]});
            google.charts.setOnLoadCallback(function ()
            {
                GaRenderCharts.drawChart(configs, data);
            });
    
            return this;
        },
    
        // More methods follow ...
    };
    
  • GaMultiQueryRender: Other charts require multiple queries to overlay multiple data sets on a single chart. In our case it is only one – sessions/users, last week and the week prior – but the code is structured to have multiple queries assigned to a single chart. Note the "for" param in the GaChartConfig object. This tells the app that this query is a second query to be superimposed over the data that it is "for." GaMultiQueryRender joins the data into a single data set, combines the column and data rows, then outputs the chart with it’s own render method.
    
    var GaMultiQueryRender =
    {
        // properties and methods preceding ...
        /**
         * Consolidate the reports and tag each with a chart name and for
         * property (if it exists.) This will tell us when we need to do
         * joins on multiple data sets.
         *
         * @param primary array
         * @param secondary array
         * @return this
         */
        consolidateReports: function (primary, secondary)
        {
            GaLoadAnalytics.ga_results['reports'] =
                primary[0]['reports'].concat(secondary[0]['reports']);
    
            $.each(GaLoadAnalytics.ga_results['reports'], function (ind, el)
            {
                GaMultiQueryRender.setForChartValue(ind, el);
            });
    
            return this;
        },
    
        // More method follow . . .
    };
    
    As mentioned in the opening challenges, this is not the recommended approach to the problem, but joining tables did not give us the results we were looking for.

Notes and Comments

  • You will notice I specifically use the object identifiers even within the object, for example GaLoadAnalytics.methodName() instead of this.methodName(). The two reasons I do this is that in Javascript, (almost) everything is global and the value of "this" can sometimes change, although in the context of an object it is not likely. The second reason is the specificity of using the identifier. There is no doubt to anyone reading the code (or myself) which object the method is being called on.
  • The demo does not query live data, reload/play with it all you like. I have created a MockAnalyticsResponse object that dynamically sets the dates from a configuration file. The metrics are all static, it will show the same numbers each day you return.
  • See the HTML/CSS version for details on the Time of Day Map and the PHP discussion of this project for details on the server side code structure and logic.
  • Many of the graphs can animate from one state to the next, but is mostly hidden in this demo because selecting start dates is disabled due to the static data. Resize the window, the top graph will demonstrate some animation.
  • "Show me the codez!" This code is not open source, however serious inquiries from potential clients can request samples.