Fuzzy to Focused

Porting Jquery Plugins with Backbone and Deep Model Merging

I’m currently working on an analytics project at VMware where I was lucky enough to redo the front end with backbone. Though one thing I couldn’t get away from was the jQuery plugins we were using to do the graphs and tables. Much of the data being passed from the back end to the front end was stable and we didn’t want to redo the graphs in D3 or other libraries. I really didn’t want to have a mix of jquery plugin syntax with this clean backbone setup, so I had to come up with some sort of solution with porting these jquery options with backbone. At the same time I wanted a clean way of keeping things modular and reusing code, because the project had started to become ridden with conditional statements for different graphs/reports

So the mission was to port plugins and make it more modular

Solution To Porting

jQuery plugins usually consist of many options, and I have seen examples of people placing these options in Backbone.View’s render method for rendering. This doesn’t seem clean to me, because we use a lot of different options with highcharts, and would clog up the render method.

I basically asked myself what are options in a jQuery plugin?

Options are basically attributes. What has attributes?

Models!

So lets store the highcharts options in a Backbone Model under the default method.

Our Model

Wow thats a lot of options! Good thing we didn’t clog our view up with them.

var HighChartsGraph = Backbone.Model.extend({
    // Deep merge needed.  Backbone only has a shallow merge available, which will remove
    // the nested data.  So had to use Jqueries extend to merge the objects in a deep
    // recursive manner by setting the deep option to true.    
    deepSet: function(data){
        // Deep merge of data
        $.extend(true, this.attributes, data);
        this.trigger('change', this);
    },

    defaults: {
        chart: {
            renderTo: 'graph-2',
            defaultSeriesType: 'column',
            marginBotton: 30,
            reflow: true,
            zoomType: 'xy',
            backgroundColor: 'transparent',
            style: {
                fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
                fontSize: '13px',
                lineHeight: '18px',
                color: '#333333'
            },
            height: null
        },
        colors: [ 
                    '#4572A7', 
                    '#AA4643', 
                    '#89A54E', 
                    '#80699B', 
                    '#3D96AE', 
                    '#DB843D', 
                    '#92A8CD', 
                    '#A47D7C', 
                    '#B5CA92',
                    '#6baed6',
                    '#9ecae1',
                    '#c6dbef',
                    '#e6550d',
                    '#fd8d3c',
                    '#fdae6b',
                    '#fdd0a2',
                    '#31a354',
                    '#74c476',
                    '#a1d99b',
                    '#c7e9c0',
                    '#756bb1',
                    '#9e9ac8',
                    '#bcbddc',
                    '#dadaeb',
                    '#636363',
                    '#969696',
                    '#bdbdbd',
                    '#d9d9d9'
                  ],
        credits: {
            enabled: false
        },
        exporting: {
            buttons: {
                exportButton: {
                    enabled:false
                }
            }
        },        
        plotOptions: {
            column: {
                pointPadding: 0.2,
                borderWidth: 0,
                pointWidth: 13
            },
            pie : {
                allowPointSelect: true,
                cursor: 'pointer',
                point: {
                    events: {
                        click: function() {
                          DrilldownView.graphDrilldown($('#graph'), false)
                        }
                    }
                },
                size: DASHBOARD ? '60%' : '80%',
                dataLabels: {
                    enabled: true,
                    color: '#000000',
                    connectorColor: '#000000',
                    formatter: function() {
                        return ''+ this.point.name +': '+ this.point.y;
                    },
                    verticalAlign:'bottom',
                    distance: 20,
                }
            },
            series: {
                borderWidth: 1,
                borderColor: '#fff',
                cursor: 'pointer',
                point: {
                    events: {
                        click: function(e) {
                            var graphWrapper = $('#'+container).parents('.graph');
                            get_specific_data(this.x, this.y, this.series, graphWrapper);
                            DrilldownView.graphDrilldown(graphWrapper);
                        }
                    }
                }
            },
            line: {
                lineWidth: 4,
                states: {
                    hover: {
                        lineWidth: 5
                    }
                },
                marker: {
                    enabled: false,
                    states: {
                        hover: {
                            enabled: true,
                            symbol: 'circle',
                            radius: 5,
                            lineWidth: 1
                        }
                    }
                }
            }
        },
        subtitle: {
            margin: 20,
            style: {
                fontSize: '13px',
                color: '#999999',
                marginBottom: '20px'
            }
        },
        title: {
            margin: 5,
            style: {
                fontSize: '16px',
                lineHeight: '18px',
                color: '#333333'
            }
        },
        tooltip: {
            formatter: function() {
                return this.series.name + ' has ' + this.y + ' on ' + this.x;
            }
        },
        xAxis: {
            allowDecimals: false,
            tickWidth: 0,
            labels: {
                rotation: -45,
                style: {
                    color:'#333333'
                }
            }
        },
        yAxis: {
            title: {
                style: {
                    color: '#89A54E'
                }
            },
            gridLineWidth: 1,
            gridLineColor: '#EEEEEE',      
            allowDecimals: false
        }
    }

For simplicity sake we are going to just stick with models and not add a collection

Deep Set Function

You may have noticed that I added a deepSet function to the model. Thats because when you use the function model.set(data), it won’t deep merge the attributes. Meaning if the parent attribute changes it will remove all of its nested attributes, whether or not they are different. So I used extend and set the deep copy to true. I also created a trigger, so that we get similar backbone model behavior.

     // Deep merge needed.  Backbone only has a shallow merge available, which will remove
    // the nested data.  So had to use Jqueries extend to merge the objects in a deep
    // recursive manner by setting the deep option to true.    
    deepSet: function(data){
        this._previousAttributes = _.clone(this.attributes);
        // Deep recursive merge of data
        $.extend(true, this.attributes, data);
        this.trigger('change', this);
    }

Note: On the change event we won’t be able to filter by specific attribute, ie. this.model.on(‘change:attribute’, etc….. That would require some more work on my side, for my use I don’t need to use this functionality, I just want the basic model structure.

IMPORTANT!: Merged JSON objects

One very important thing to note is that the json objects that we merge have the same structure and sequence. We use the same options structure that can be found in the plugin’s docs. This keeps it very clean and organized, for both front end and back end.

Our View

Set up a basic view to set the data with our new deepSet function, and have it listen to the model change, where we pass the model.toJSON() into the jQuery plugin, and then append to our el.

var GraphView = Backbone.View.extend({
    el: $('.chart-wrapper'),

    initialize: function(){
        _.bindAll(this, 'render');

        this.model = new HighChartsGraph();

        this.model.on('change', _.bind(function(data){
            this.$el.prepend(this.template(data.toJSON()));
            // Intialize our high charts
            var chart = new Highcharts.Chart(data.toJSON());
        }, this));

    },

    template: function(data){
        var graphSection = ich.reportSection(data.layout);
        return graphSection
    },

    render: function(data){
        this.model.deepSet(data)

        return this
    }
});

Complete Picture

To show you a more complete picture of how I render the highcharts graph:

I setup a report model and collections

I setup a report view, and fire off a fetch on intialize of the view. Then parse the report data model appropriately and render the graph view among other views that I won’t go into detail about.

var Report = Backbone.Model.extend({
    url: '/get-report/'
});

var Reports = Backbone.Collection.extend({
    url: '/get-report/',
    model:ReportData
});

var ReportView = Backbone.View.extend({
    el: $('body'),

    intialize: function(){
        _.bindAll(this);

        this.collection = new Reports();

        this.collection.on('add', function(report){
            this.render(report)
        });
        
        this.collection.fetch();
    },

    render: function(report){
        var d = report.toJSON();

        if(d.graph){
            GraphView.render(d.graph);
        }
        if(d.table){
            TableView.render(d.table);
        }
    }

});

var HighChartsGraph = Backbone.Model.extend({
    // Deep merge needed.  Backbone only has a shallow merge available, which will remove
    // the nested data.  So had to use Jqueries extend to merge the objects in a deep
    // recursive manner by setting the deep option to true.    
    deepSet: function(data){
        // Deep recursive merge of data
        $.extend(true, this.attributes, data);
        this.trigger('change', this);
    },

    defaults: {
        chart: {
            renderTo: 'graph-2',
            defaultSeriesType: 'column',
            marginBotton: 30,
            reflow: true,
            zoomType: 'xy',
            backgroundColor: 'transparent',
            style: {
                fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
                fontSize: '13px',
                lineHeight: '18px',
                color: '#333333'
            },
            height: null
        },
        colors: [ 
                    '#4572A7', 
                    '#AA4643', 
                    '#89A54E', 
                    '#80699B', 
                    '#3D96AE', 
                    '#DB843D', 
                    '#92A8CD', 
                    '#A47D7C', 
                    '#B5CA92',
                    '#6baed6',
                    '#9ecae1',
                    '#c6dbef',
                    '#e6550d',
                    '#fd8d3c',
                    '#fdae6b',
                    '#fdd0a2',
                    '#31a354',
                    '#74c476',
                    '#a1d99b',
                    '#c7e9c0',
                    '#756bb1',
                    '#9e9ac8',
                    '#bcbddc',
                    '#dadaeb',
                    '#636363',
                    '#969696',
                    '#bdbdbd',
                    '#d9d9d9'
                  ],
        credits: {
            enabled: false
        },
        exporting: {
            buttons: {
                exportButton: {
                    enabled:false
                }
            }
        },        
        plotOptions: {
            column: {
                pointPadding: 0.2,
                borderWidth: 0,
                pointWidth: 13
            },
            pie : {
                allowPointSelect: true,
                cursor: 'pointer',
                point: {
                    events: {
                        click: function() {
                          DrilldownView.graphDrilldown($('#graph'), false)
                        }
                    }
                },
                size: DASHBOARD ? '60%' : '80%',
                dataLabels: {
                    enabled: true,
                    color: '#000000',
                    connectorColor: '#000000',
                    formatter: function() {
                        return ''+ this.point.name +': '+ this.point.y;
                    },
                    verticalAlign:'bottom',
                    distance: 20,
                }
            },
            series: {
                borderWidth: 1,
                borderColor: '#fff',
                cursor: 'pointer',
                point: {
                    events: {
                        click: function(e) {
                            var graphWrapper = $('#'+container).parents('.graph');
                            get_specific_data(this.x, this.y, this.series, graphWrapper);
                            DrilldownView.graphDrilldown(graphWrapper);
                        }
                    }
                }
            },
            line: {
                lineWidth: 4,
                states: {
                    hover: {
                        lineWidth: 5
                    }
                },
                marker: {
                    enabled: false,
                    states: {
                        hover: {
                            enabled: true,
                            symbol: 'circle',
                            radius: 5,
                            lineWidth: 1
                        }
                    }
                }
            }
        },
        subtitle: {
            margin: 20,
            style: {
                fontSize: '13px',
                color: '#999999',
                marginBottom: '20px'
            }
        },
        title: {
            margin: 5,
            style: {
                fontSize: '16px',
                lineHeight: '18px',
                color: '#333333'
            }
        },
        tooltip: {
            formatter: function() {
                return this.series.name + ' has ' + this.y + ' on ' + this.x;
            }
        },
        xAxis: {
            allowDecimals: false,
            tickWidth: 0,
            labels: {
                rotation: -45,
                style: {
                    color:'#333333'
                }
            }
        },
        yAxis: {
            title: {
                style: {
                    color: '#89A54E'
                }
            },
            gridLineWidth: 1,
            gridLineColor: '#EEEEEE',      
            allowDecimals: false
        }
    }
});    

var GraphView = Backbone.View.extend({
    el: $('.chart-wrapper'),

    initialize: function(){
        _.bindAll(this, 'render');

        this.model = new HighChartsGraph();

        this.model.on('change', _.bind(function(data){
            this.$el.prepend(this.template(data.toJSON()));
            // Intialize our high charts
            var chart = new Highcharts.Chart(data.toJSON());
        }, this));

    },

    template: function(data){
        var graphSection = ich.reportSection(data.layout);
        return graphSection
    },

    render: function(data){
        this.model.deepSet(data)

        return this
    }
});    

Review

So to review, I set up a model and insert the jQuery plugin options I will be always be using in the default function of the model. I then fetch my saved data options from the backend. Do a deep merge of the default attributes and new attributes. Then pass that model to the jQuery plugin in the view and append the template to our el. 

Boom! Clean and done..

All options changes and new data show up on the front end in a nice clean manner.

Making noodles near the delta

Ho chi Minh view from hotel View high resolution

Ho chi Minh view from hotel

Mercedes Rolls Out Invisible Car [VIDEO]

Mercedes Rolls Out Invisible Car [VIDEO]
When Mercedes wanted to promote its new fuel cell vehicle, instead of placing it squarely in front of everyone in the world, the company decided to make the car invisible. We have video. In this clever publicity stunt, Mercedes wanted to emphasize that its F-Cell vehicle has no omi…

Jason Shah: How Path Gets Clicks and Why Foursquare Flops | UX for User Generated Content Pages

jasonshah:

Path and Foursquare are both geolocation-enabled mobile apps that help you track and share where you go, what do you do, and who you do it with. (That would be a horrible pitch for both apps if my aim was to get you to sign up. We can discuss why in the comments.) I am a fan of both and think they…

coburnhawk:

User Interface for the xfinity control apps on iPad and iPhone
View high resolution

coburnhawk:

User Interface for the xfinity control apps on iPad and iPhone

Ultralite Powered by Tumblr | Designed by:Doinwork