Dashboard

A dashboard app displaying dynamic line and pie charts.



Launch App


Description


The dashboard app contains charts and summary statistics on a hypothetical website. At the top is a line chart that displays users/month, visits/month, and sales/month. You change the charts by hovering the mouse cursor over one of the three large buttons. The buttons themselves display the current monthly values.

Next is a pie chart which shows the distribution of the website's demographics. Most visitors are coming from the US, then UK, India, Germany, etc. Hover the mouse over a slice to get the country and percentages for that slice.

At the bottom you find a real-time chart showing the number of concurrent sessions. They are updated in real-time. Of course, these figures are simulated.

To display the charts we use flot which an open source JavaScript charting package written by Google. It requires HTML5. In case your browser does not support the canvas element a message will suggest that you upgrade to a more recent browser version. All recent browsers today do support canvas.

The JavaScript code is listed below (the code is fairly lengthy; it may be best to open the actual source code in a separate editor and read along).

var Patterns = {
    // ** namespace pattern
    namespace: function (name) {

        // ** single var pattern
        var parts = name.split(".");
        var ns = this;

        // ** iterator pattern
        for (var i = 0, len = parts.length; i < len; i++) {
            // ** || idiom
            ns[parts[i]] = ns[parts[i]] || {};
            ns = ns[parts[i]];
        }

        return ns;
    }
};

// ** namespace pattern 
// ** revealing module pattern
// ** singleton pattern
Patterns.namespace("InAction").Dashboard = (function () {

    // ** immediate function idiom 
    var users = (function () {
        // ** lazy load pattern (using closure)
        var data;
        var show = function () {
            // ** truthy/falsy idiom
            // ** || idiom
            data = data || get("users");
            showPlot(data);
        }
        return { show: show };
    })();

    // ** immediate function idiom 
    var visits = (function () {
        // ** lazy load pattern (using closure)
        var data;
        var show = function () {
            // ** truthy/falsy idiom
            // ** || idiom
            data = data || get("visits");
            showPlot(data);
        }
        return { show: show }; 
    })();

    // ** immediate function idiom 
    var sales = (function () {
        // ** lazy load pattern (using closure)
        var data;
        var show = function () {
            // ** truthy/falsy idiom
            // ** || idiom
            data = data || get("sales");
            showPlot(data);
        }
        return { show: show }; 
    })();

    // ** strategy pattern
    var strategy = users;  // default strategy

    var showPlot = function (data) {

        // ** option hash idiom
        var options = {
            legend: { position: "nw" },
            lines: { show: true },
            points: { show: true },
            xaxis: { ticks: [[1, "jan"], [2, "feb"], [3, "mar"], [4, "apr"], [5, "may"], [6, "jun"], [7, "jul"], [8, "aug"], [9, "sep"], [10, "oct"], [11, "nov"], [12, "dec"]] },
            grid: { backgroundColor: { colors: ["#fff", "#f5f6f7"] } }
        };

        $.plot($("#plotarea"), data, options);
    }

    var pieHover = function (event, pos, obj) {

        // ** truthy/falsy idiom
        // ** && idiom
        if (obj && obj.series) {
            percent = parseFloat(obj.series.percent).toFixed(2);
            $("#hover").html('' + obj.series.label + ' (' + percent + '%)');
        }
    }

    var pieClick = function (event, pos, obj) {

        // ** truthy/falsy idiom
        // ** && idiom
        if (obj && obj.series) {
            percent = parseFloat(obj.series.percent).toFixed(2);
            alert('' + obj.series.label + ': ' + percent + '%');
        }
    }

    var initPie = function () {
        var piedata = [
                { label: "USA", data: 110 },
                { label: "UK", data: 60 },
                { label: "India", data: 50 },
                { label: "Germany", data: 30 },
                { label: "China", data: 24 },
                { label: "Canada", data: 20 }
        ];

        var $piearea = $("#piearea");

        // ** option hash idiom
        $.plot($piearea, piedata,
            {
                series: {
                    pie: {
                        show: true
                    }
                },
                grid: {
                    hoverable: true,
                    clickable: true
                }
            });

        // ** chaining pattern
        $piearea.bind("plothover", pieHover)
                .bind("plotclick", pieClick)
                .hover(function () { $("#hover").css("visibility", "visible"); },
                        function () { $("#hover").css("visibility", "hidden"); }
        );
    };

    var showLineChart = function () {
        strategy.show();
    }

    var initPlots = function () {

        // ** observer pattern
        $("#link-users").on('hover', function () {
            $('[id^="link-"]').removeClass("active");
            $("#link-users").addClass("active");
            // ** strategy pattern
            strategy = users;
            showLineChart();
        });
        $("#link-visits").on('hover', function () {
            $('[id^="link-"]').removeClass("active");
            $("#link-visits").addClass("active");
            // ** strategy pattern
            strategy = visits;
            showLineChart();
        });
        $("#link-sales").on('hover', function () {
            $('[id^="link-"]').removeClass("active");
            $("#link-sales").addClass("active");
            // ** strategy pattern
            strategy = sales;
            showLineChart();
        });

        showLineChart();
    };

    var initRealtime = function () {

        var data = [];

        function getData() {
            if (data.length > 0) data = data.slice(1);

            // generate random data
            while (data.length < 300) {

                var prev = data.length > 0 ? data[data.length - 1] : 50;
                var y = Math.round(prev + Math.random() * 5 - 2.5);

                y = Math.max(Math.min(y, 100), 0); // range is 0 - 100;
                data.push(y);
            }

            // return x and y coordinate pairs
            var coordinates = [];
            for (var x = 0; x < data.length; ++x) {
                coordinates.push([x, data[x]])
            }
            return coordinates;
        }

        // ** option hash idiom
        var options = {
            series: { shadowSize: 0 },
            yaxis: { min: 0, max: 100 },
            xaxis: { show: false },
            colors: ["#cc0000"]
        };
        var plot = $.plot($("#realtime"), [getData()], options);

        function updateRealtime() {

            plot.setData([getData()]);
            plot.draw();   // call draw because axis did not change

            // ** zero timeout pattern  (30 milliseconds)
            setTimeout(updateRealtime, 30);
        }

        updateRealtime();
    };

    // mock server call
    var get = function (what) {
        switch(what) {
            case "users": return [{ "label": "Users", "data": [[1, 344], [2, 578], [3, 460], [4, 902], [5, 1933], [6, 2303], [7, 3281], [8, 3590], [9, 6830], [10, 8429]] }];
            case "visits": return [{ "label": "Visits", "data": [[1, 12965], [2, 16935], [3, 19993], [4, 21983], [5, 76801], [6, 67372], [7, 87922], [8, 100399], [9, 140332], [10, 188022]], "color": 2 }];
            case "sales": return [{ "label": "Sales", "data": [[1, 266], [2, 1009], [3, 6754], [4, 6570], [5, 7489], [6, 8888], [7, 10821], [8, 14099], [9, 12222], [10, 23390]], "color": 3 }];
            default: return [];
        }
    }

    var start = function () {
        initPlots();
        initPie();
        initRealtime();
    };

    return { start: start };
})();
        

$(function () {

    // First check for HTML5 canvas support

    // ** double !! idiom
    var supportsCanvas = !!document.createElement("canvas").getContext;
    if (!supportsCanvas) {
        // ** chained pattern
        $("#message").css("color", "red").html("It seems that your browser does not support HTML canvas. Please upgrade to a more recent version.");
        return;
    }
        
    // ** facade pattern
    var dashboard = Patterns.InAction.Dashboard;
    dashboard.start();
});

The following patterns and idioms are present in this code:

  • Immediate function idiom
  • Double !! idiom
  • Truthy/falsy idiom
  • || and && idiom
  • Option Hash idiom
  • Namespace pattern
  • Single var pattern
  • Module pattern
  • Chaining pattern
  • Lazy Load pattern
  • Zero Timeout pattern
  • Iterator pattern
  • Singleton pattern
  • Strategy pattern
  • Observer pattern
  • Façade pattern

It is interesting to note the large number of patterns and idioms in only 250 lines of code. As mentioned earlier we did not set out to get as many patterns as possible into the script. However, what this demonstrates is how pervasive patterns and idioms are in today's JavaScript.

The Namespace pattern allows us to keep the entire code base in the Patterns.InAction namespace. The Dashboard module was built with the Module pattern, more specifically the Revealing Module pattern which exposes only a single method: the start method. Everything else in Dashboard is hidden from the outside world. Only a single Dashboard instance can exist which is the idea behind the Singleton pattern.

The Dashboard module contains 3 'subsystems': line charts, pie chart, and real-time chart. Each one uses numerous private variables and functions. This represents the Façade pattern which exposes a simple API and hides the complexity of a set of subsystems. This API is extremely simple because the only way to access the Façade is through the start method.

Each one of the three line-charts at the top has its own function object; they are named users, visits, and sales and are constructed using the Immediate Function idiom. Each one only exposes a single method named show. The data is lazy loaded and kept inside of the function objects using the function's closures. This is the Lazy Load pattern in action – Lazy Initialization to be exact.

The line data = data || get("users"); is built around two idioms: the truthy/falsy idiom and the || and && idiom. First it checks whether the data variable is truthy: if so, we shortcut the || and assign data to itself. If not, the data is retrieved using get which is a mocked server call. This statement also gets to the heart of the Lazy Load pattern because data is only loaded when absolutely necessary. Once loaded we will not load it again.

The users, visits, and sales function objects are used in a Strategy Pattern. The strategy variable is bound to the line-chart that is currently displayed. If a different chart needs be displayed (because the user moves the mouse cursor over a different button), the strategy gets reassigned with a different strategy function object. The showLineChart function invokes the strategy's show method without really knowing which strategy is used.

The Option Hash idiom is used to configure the chart options. The flot package makes extensive use of this idiom.

The Chaining pattern lets us chain multiple jQuery event bindings to the Pie Chart. This is a very effective and efficient way of hooking up multiple event handlers in a single statement.

In JavaScript whenever you are attaching an event handler or callback to an event you're implementing the Observer pattern. JavaScript being an event-driven language, you simply cannot escape the Observer pattern. It is always present.

The real-time Chart is updated at a very high frequency: every 30 milliseconds. We could have written a tight loop without setTimeout but that would freeze the browser because it wouldn't have any time to catch up with other pending events in the event queue. The setTimeout call gives the browser breathing room and allows it to respond to all other events (including the repainting of the real-time chart). It is not quite 0 timeout (Zero Timeout pattern), but the idea and the effects are the same.

The Iterator pattern allows you to loop over an array or list of elements. The built-in for and while are iterators. Perhaps a more 'pure' Iterator implementation is the jQuery each method which is used in other Patterns in Action samples.

The script checks if the HTML5 canvas element is supported. It uses the Double !! idiom which ensures that supportsCanvas is a true Boolean.


Launch App