The stock explorer application

In this section, we will use D3 and Backbone to create a single page application to display a time series of stock prices. The user will be able to select different stocks and a period of time in order to get a detail view. The page will have several components:

  • Context chart: This chart will display the complete series of prices that are available for the stock. It will have a brush component that selects a time interval.
  • Detail chart: This will be a bigger area chart that will show you the stock prices for the time interval selected in the context chart.
  • Control view: This view will show you a control that selects the stock.
  • Stock title: This will display the name of the company.

We will display the control and title view on top of the page, a big area with the detail view, and a context area chart at the bottom of the page.

The stock explorer application

A diagram of the application components

As you can see, there are several components that should be in sync. If we change the stock, the area chart should be updated. If we change the time interval in the context chart, the detail chart must be updated to show you only the selected period. We will use Backbone to structure this application.

The state of our application can be described by the stock and the time interval that we want to examine. We will create a model to store the application state and one view for each component.

Note

The general strategy is that each view will contain an instance of a D3-based chart, which is created following the reusable chart pattern. In the initialize method, we will tell the view to listen for changes in the model and invoke the render method when one of the model attributes is changed. In the render method of the view, we will create a selection for the container element of the view, bind the data corresponding to the selected stock, and invoke the charting function using the selection.call method. We will begin by creating reusable charts with D3.

Creating the stock charts

In this section, we will implement the charts that will be used by the application. This time, the code of the charts will be in a separated JavaScript file. To follow the examples in this section, open the chapter06/stocks/js/lib/stockcharts.js and chapter06/01-charts.html files.

We will begin by creating the stock title chart and then implement the stock area chart, which we will be using in the context and detail views. Note that the title chart is not really necessary, but it will be helpful to introduce the pattern that integrates reusable charts and Backbone.

The stock title chart

The stock title chart is a reusable chart that creates a paragraph with the title of the stock. As we mentioned previously, it's probably not a good idea to create a chart just to write a string, but it shows you how to integrate a reusable chart that doesn't involve SVG with Backbone. It has a configurable title accessor function, so the user can define the content of the paragraph using the data that is bound to the container selection. The chart is structured using the reusable chart pattern, as shown in the following code:

function stockTitleChart() {
    'use strict';

    // Default title accessor
    var title = function(d) { return d.title; };

    // Charting function
    function chart(selection) {
        selection.each(function(data) {
            // Creation and update of the paragraph...
        });
    }

    // Title function accessor
    chart.title = function(titleAccessor) {
        if (!arguments.length) { return title; }
        title = titleAccessor;
        return chart;
    };

    return chart;
}

In the charting function, we select the div element and create a selection for the paragraph. We add the stock-title class to the paragraph in order to allow the user to modify its style, as shown in the following code:

    // Charting function
    function chart(selection) {
        selection.each(function(data) {

            // Create the selection for the title
            var div = d3.select(this),
                par = div.selectAll('p.stock-title').data([data]);

            // Create the paragraph element on enter
            par.enter().append('p')
                .attr('class', 'stock-title'));

            // Update the paragraph content
            par.text(title);
        });
    }

As usual, we can use the chart by creating and configuring a chart instance, selecting the container element, binding the data, and invoking the chart using the selection.call method, as shown in the following code:

// Create and configure the title chart
var titleChart = stockTitleChart()
    .title(function(d) { return d.name; });

// Select the container element, bind the data and invoke
// the charting function on the selection
d3.select('div#chart')
    .data([{name: 'Apple Inc.'}])
    .call(titleChart);

The stock area chart

The stock area chart will display the time series for the stock price as an area chart. In Chapter 5, Creating User Interface Elements, we implemented an area chart that uses the brush behavior to select a time interval and annotate the chart with additional information about the price variation in the period. We will create an improved version of this chart and use it in the stock explorer application.

Besides having the usual width, height, and margin attributes and accessors methods, this chart will have an optional axis, brush behavior, and a configurable brush listener function so that the user can define actions to be performed on the brush. The time extent can be also be configured, allowing the user to show only part of the chart.

We have added all these methods so that we can use two chart instances for different purposes: one to allow the user to select a time interval and another to display the selected time interval in more detail. In the chapter06/01-charts.html file, we created one instance in order to select the time interval:

    var contextAreaChart = stockAreaChart()
        .height(60)
        .value(function(d) { return d.price; })
        .yaxis(false)
        .onBrushListener(function(extent) {
            console.log(extent);
        });

We will use the chart accessor methods to set the height, disable the y axis, and set the value accessor and the brush listener functions. In this case, the brush listener function will display the time extent in the browser console on brush.

The stock area chart

A stock area chart with the brush behavior enabled

We will use a second instance of the same chart to display a specific time interval. In this instance, we will disable the brush control and set the initial time extent, the value, and the date accessors. This chart will display the stock prices between the from and to dates:

// Set the time extent
var from = new Date('2002/01/01'),
    to = new Date('2004/12/31'),

// Create and configure the detail area chart
var detailAreaChart = stockAreaChart()
    .value(function(d) { return d.price; })
    .date(function(d) { return new Date(d.date); })
    .timeExtent([from, to])
    .brush(false);

As you have probably guessed, the first instance is intended to control the time extent of the second chart instance. We will get to that soon; in the meantime, we will discuss some implications of controlling the time extent of the chart.

The stock area chart

A stock area chart with the y-axis enabled and the brush behavior disabled

If we change the time extent of the chart, we will want the chart to reflect its new state. If the brush is dragged left in the first chart, we will want the area of the second chart to move to the right-hand side until it matches the interval selected in the first chart, and if we shorten the time interval in the first chart, we will want the area of the second chart to compress itself to display the selected interval in the same horizontal space.

The stock area chart will be implemented as a reusable chart. As most of the chart structure is similar to the chart presented in the previous section, we will skip some parts for brevity:

function stockAreaChart() {
    'use strict';

    // Chart Attributes
    var width = 700,
        height = 300,
        margin = {top: 20, right: 20, bottom: 20, left: 20};

    // Time Extent
    var timeExtent;

    // The axis and brush are enabled by default
    var yaxis = true,
        xaxis = true,
        brush = true;

    // Default accessor functions
    var date = function(d) { return new Date(d.date); };
    var value = function(d) { return +d.value; };

    // Default brush listener
    var onBrush = function(extent) {};

    function chart(selection) {
        selection.each(function(data) {
            // Charting function contents...
        });
    }

    var svgInit = function(selection) { ... };

    // Accessor Methods...

    return chart;
}

In order to have the detail chart moving in sync with the context chart, we will need to draw the complete series in the detail chart but only displaying the interval selected with the brush in the context chart. To prevent the area chart from being visible outside the charting area, we will define a clip path, and only the content inside the clipping path will be visible:

var svgInit = function(selection) {
    // Define the clipping path
    selection.append('defs')
        .append('clipPath')
        .attr('id', 'clip')
        .append('rect')
        .attr('width', width - margin.left - margin.right)
        .attr('height', height - margin.top - margin.bottom);

    // Create the chart and axis groups...
};

The element that will be clipped should reference the clipping path using the clip-path attribute. In the charting function, we select the container element and create the SVG element on enter. We also set the SVG element's width and height and translate the axis, chart, and brush groups. We create the scales and axis (if they are enabled) and create and configure the area generator to draw the chart area path:

            // Charting function...
            // Add the axes
            if (xaxis) { svg.select('g.xaxis').call(xAxis); }
            if (yaxis) { svg.select('g.yaxis').call(yAxis); }

            // Area Generator
            var area = d3.svg.area()
                .x(function(d) { return xScale(date(d)); })
                .y0(yScale(0))
                .y1(function(d) { return yScale(value(d)); });

We create a selection for the path and bind the time series array to the selection. We append the path on enter and set its class to stock-area. We set the path data using the area generator and set the clip-path attribute using the clipPath variable defined previously:

            // Create the path selection
            var path = svg.select('g.chart').selectAll('path')
                .data([data]);

            // Append the path element on enter
            path.enter().append('path')
                .attr('class', 'stock-area'),

            // Set the path data string and clip the area
            path.attr('d', area)
                .attr('clip-path', 'url(#clip)'),

We create an envelope brush listener function. In this function, we retrieve the brush extent and invoke the user-configurable onBrush function, passing the extent as an argument. We initialize the brush behavior and bind the brushListener function to the brush event:

            // Brush Listener Function
            function brushListener() {
                timeExtent = d3.event.target.extent();
                onBrush(timeExtent);
            }

            // Brush Behavior
            var brushBehavior = d3.svg.brush()
                .x(xScale)
                .on('brush', brushListener);

The initial time extent of the chart can be configured. If that's the case, we update the brush behavior extent, so the brush overlay fits the configured time extent:

            // Set the brush extent to the time extent
            if (timeExtent) { 
                brushBehavior.extent(timeExtent); 
            }

We call the brush behavior using the selection.call method on the brush group and set the overlay height:

            if (brush) {
                svg.select('g.brush').call(brushBehavior);

                // Change the height of the brushing rectangle
                svg.select('g.brush').selectAll('rect')
                    .attr('height', h);
            }

The preceding charts are implemented following the reusable chart pattern and were created with D3 only. We will use the charts in a Backbone application, but they can be used in other applications as standalone charts.

Preparing the application structure

In Backbone projects, it is a common practice to create directories for the models, views, collections, and routers. In the Chapter06 directory, we created the stocks directory to hold the files for this application:

stocks/
    css/
    js/
        views/
        models/
        collections/
        routers/
        lib/
        app.js
    data/
    index.html

The models, views, collections, and routers folders contain JavaScript files that contain the Backbone models, views, collections, and routers. We add the D3 charts to the js/lib directory; additional JavaScript libraries would be there too. There is also a data folder with JSON files for the stock data. The index.html file contains the application markup.

The index page

In the header of the page, we include style sheets and JavaScript libraries that we need for our application. To create the page more quickly, we will use a CSS library that will add styles to enable uniform fonts, sizes, and default colors among browsers and define the grid system. A grid system is a set of styles that allows us to define rows and columns of standard column sizes without having to define the styles for each size ourselves. We will use Yahoo's Pure CSS modules to use the grid system, which is a pretty minimal set of CSS modules. These modules are used only in this page; if you are more comfortable with Bootstrap or other libraries, you are free to replace the div classes or define the sizes and behaviors of each container yourself.

We will create a container for the application and add the pure-g-r class, which is a container with responsive behavior enabled. If the viewport is wide, the columns will be shown side by side; if the user has a small screen, the columns will be shown stacked. We will also create two child containers, one for the stock control and title and a second container for the stock area chart, both classed pure-u-1, that is, containers with full width. The pure container uses fractional sizes to define the div width; in order to have a div that covers 80 percent of the parent container width, we can set its class to pure-u-4-5:

<div class="pure-g-r" id="stock-app">
  <!-- Stock Selector and Title -->
  <div class="pure-u-1">
    <div id="stock-control"></div>
    <div id="stock-title"></div>
  </div>
  <div class="pure-u-1 charts">
    <div id="stock-detail"></div>
    <div id="stock-context"></div>
  </div>
</div>

We include the application files at the end of the page so that the markup is rendered while the remaining assets are loaded:

<!-- Application Components -->
<script src="/chapter06/stocks/js/models/app.js"></script>
<script src="/chapter06/stocks/js/models/stock.js"></script>
<script src="/chapter06/stocks/js/collections/stocks.js"></script>
<script src="/chapter06/stocks/js/views/stocks.js"></script>
<script src="/chapter06/stocks/js/views/app.js"></script>
<script src="/chapter06/stocks/js/routers/router.js"></script>
<script src="/chapter06/stocks/js/app.js"></script>

Creating the models and collections

Models contain application data and the logic related to this data. For our application, we will need a model to represent the stock information and the application model, which will store the visualization state. We will also create a collection that holds the available stock instances. To avoid polluting the global namespace, we will encapsulate the application components in the app variable:

var app = app || {};

Adding this line to all the files in the application will allow us to extend the object with models, collections, and views.

The stock model

The stock model will contain basic information about each stock. It will contain the stock name (Apple Inc.), the symbol (AAPL), and the URL where the time series of prices can be retrieved (aapl.json). Models are created by extending Backbone.Model:

// Stock Information Model
app.Stock = Backbone.Model.extend({

    // Default stock symbol, name and url
    defaults: {symbol: null, name: null, url: null},

    // The stock symbol is unique, it can be used as ID
    idAttribute: 'symbol'
});

Here, we defined the default values for the model to null. This is not really necessary, but it might be useful to know which properties are expected. We will use the stock symbol as ID. Besides this, we don't need any further initialization code. As stock symbols are unique, we will use the symbol as the ID for easier retrieval later. We can create stock instances by using the constructor and setting the attributes that pass an object:

var appl = new app.Stock({
        symbol: 'AAPL', 
        name: 'Apple', 
        url: 'aapl.json'
    });

We can set or get its attributes using the accessor methods:

aapl.set('name', 'Apple Inc.'),
aapl.get('name'),  // Apple Inc.

In this application, we will create and access stock instances using a collection rather than creating individual instances.

The stock collection

To define a collection, we need to specify the model. When defining the collection, we can set the URL of an endpoint where the collection records can be retrieved, which is usually the URL of a REST endpoint. In our case, the URL points towards a static JSON file that contains the stocks records:

// Stock Collection
app.StockList = Backbone.Collection.extend({
    model: app.Stock,
    url: '/chapter06/stocks/data/stocks.json'
});

Individual stocks can be added to the collection one by one, or they can be fetched from the server using the collection URL. We can also specify the URL when creating the collection instance:

// Create a StockList instance
var stockList = new app.StockList({});

// Add one element to the collection
stockList.add({
    symbol: 'AAPL',
    name: 'Apple Inc.',
    url: 'aapl.json'
});
stockList.length; // 1

As we defined the stock symbol as idAttribute, individual stock instances can be retrieved using the stock's ID. In this case, the stock symbol is the ID of the stock model, so we can retrieve stock instances using the symbol:

var aapl = stockList.get('AAPL'),

Models use the URL of the collection to construct their own URL. The default URL will have the form collectionUrl/modelId. If the server provides a RESTful API, this URL can be used to create, update, and delete records.

The application model

We will create an application model to store and manage the application state.

To define the application model, we extend the Backbone.Model object, adding the corresponding default values. The stock attribute will contain the current stock symbol (AAPL), and the data will contain the time series for the current stock:

// Application Model
app.StockAppModel = Backbone.Model.extend({

    // Model default values
    defaults: {
        stock: null,
        from: null,
        to: null,
        data: []
    },

    initialize: function() {
        this.on('change:stock', this.fetchData);
        this.listenTo(app.Stocks, 'reset', this.fetchData);
    },

    // Additional methods...
    getStock: function() {...},
    fetchData: function() {...}
});

We will also set a template for the stock collection data. In this case, the base URL is chapter06/stocks/data/. As we mentioned previously, there is a JSON file in the data directory with the data of the available stocks:

    // Compiled template for the stock data url
   urlTemplate: _.template('/chapter06/stocks/data/<%= url %>'),

We have also added a fetchData method in order to retrieve the time series for the corresponding stock. We invoke the template that passes the current stock data and use the parsed URL to retrieve the stock time series. We use the d3.json method to get the stock data and set the model data attribute to notify the views that the data is ready:

    fetchData: function() {
     // Fetch the current stock data
        var that = this,
            stock = this.getStock(),
            url = this.urlTpl(stock.toJSON());

        d3.json(url, function(error, data) {
            if (error) { return error; }
            that.set('data', data.values);
        });
    }

Implementing the views

To integrate the D3-based charts with Backbone Views, we will use the following strategy:

  1. We will create and configure a chart instance as an attribute of the view.
  2. In the initialization method, we tell the view to listen for changes on the model application and render the view on model updates.

The views for the page components are in the chapter06/stocks/js/views/stocks.js file, and the application view code is in the chapter06/stocks/js/views/app.js file.

The title view

This view will simply display the stock symbol and name. It's intended to be used as a title of the visualization. We create and configure an instance of the underlying chart and store a reference to the chart in the chart attribute. In the initialize method, we tell the view to invoke the render method when the model's stock attribute is updated.

In the render method, we create a selection that will hold the container element of the view, bind this element to a dataset that contains the current stock, and invoke the chart using selection.chart:

app.StockTitleView = Backbone.View.extend({

    chart: stockTitleChart()
        .title(function(d) {
            return _.template('<%= symbol %><%= name %>', d);
        }),

    initialize: function() {
        this.listenTo(this.model, 'change:stock', this.render);
        this.render();
    },

    render: function() {
        d3.select(this.el)
            .data([this.model.getStock().toJSON()])
            .call(this.chart);

        return this;
    }
});

Changes to the stock attribute of the application model will trigger the change:stock event, causing the view to invoke its render method, updating the D3 chart. In this particular view, using a reusable chart is overkill; for a real-life problem, we could have used a small Backbone View with a template. We did this to have a minimal example of reusable charts working with Backbone Views.

The title view

Rendered stock title view

The stock selector view

To add some diversity, we will create the selector without using D3. This view will show you the available stocks as a selection menu, updating the application model's stock attribute when the user selects a value. In this view, we will use a template. To create a template, we create a script element of type text/template in the index.html file and assign it an ID, in our case, stock-selector-tpl:

<script type="text/template" id="stock-selector-tpl">
<select id="stock-selector">
        ...
</select>
</script>

Underscore templates can render variables using <%= name %> and execute JavaScript code using <% var a = 1; %>. Here, for instance, we evaluate the callback function on each element of the stocks array:

<!-- Create the stocks selector and add its options -->
<% _.each(stocks, function(s) { %>
  <option value="<%= s.symbol %>"><%= s.symbol %></option>
<% }); %>

For each one of the elements of the stocks array, we add an option with the stock symbol attribute as the value and content of the option element. After rendering the template with the application data, the HTML markup will be as follows:

<select id="stock-selector">
  <option value="AAPL">AAPL</option>
  <option value="MSFT">MSFT</option>
  <option value="IBM">IBM</option>
  <option value="AMZN">AMZN</option>
</select>

In the Backbone View, we select the content of the script with the stock-selector-tpl ID, compile the template to use it later, and store a reference to the compiled template in the template attribute:

// Stock Selector View
app.StockSelectorView = Backbone.View.extend({

    // Compiles the view template
    template: _.template($('#stock-selector-tpl').html()),

    // DOM Event Listeners
    events: {
        'change #stock-selector': 'stockSelected'
    },

    // Initialization and render methods...
});

We set the events attribute, with maps' DOM events of the inner elements of the view, to methods of the view. In this case, we bind changes to the stock-selector select element (the user changing the stock) with the stockSelected method.

In the initialize method, we tell the view to render when the app.Stocks collection emits the reset event. This event is triggered when new data is fetched (with the {reset: true} option) and when an explicit reset is triggered. If a new set of stocks is retrieved, we will want to update the available options. We also listen for changes to the application model's stock attribute. The current stock should always be the selected option in the select element:

    initialize: function() {
        // Listen for changes to the collection and the model
        this.listenTo(app.Stocks, 'reset', this.render);
        this.listenTo(this.model, 'change:stock', this.render);
        this.render();
    }

In the render method, we select the container element and set the element content to the rendered template, writing the necessary markup to display the drop-down control. We pass a JavaScript object with the stock attribute set to an array that contains the app.Stocks model's data. Finally, we iterate through the options in order to mark the option that matches the current stock symbol as selected:

    render: function() {
        // Stores a reference to the 'this context'
        var self = this;

        // Render the select element
        this.$el.html(this.template({stocks: app.Stocks.toJSON()});

        // Update the selected option
        $('#stock-selector option').each(function() {
            this.selected = (this.value === self.model.get('stock'));
        });
    }

Backbone models and collection instances have the JSON method, which transforms the model or collection attributes to a JavaScript object. This method can be overloaded if we need to add computed properties besides the existing attributes. Note that in the each callback, the this context is set to the current DOM element, that is, the option element. We store a reference to the this context in the render function (the self variable) in order to reference it later.

The stock selector view

Stock selector view allows selecting a stock by symbol

The stock context view

The context view contains a small area chart that allows the user to select a time interval that can be displayed in the detail view:

We will use the same strategy as the one used in the previous views to create and configure an instance of stockAreaChart, and store a reference to it in the chart attribute of the view:


app.StockContextView = Backbone.View.extend({

    // Initialize the stock area chart
    chart: stockAreaChart()
        .height(60)
        .margin({top: 5, right: 5, bottom: 20, left: 30})
        .date(function(d) { return new Date(d.date); })
        .value(function(d) { return +d.price; })
        .yaxis(false),

    // Render the view on model changes
    initialize: function() { ... },

    render: function(e) { ... }
});

In the initialize method, we tell the view to listen for changes to the application model and set the chart brush listener to update the from and to attributes of the model:

    initialize: function() {

        // Get the width of the container element
        var width = parseInt(d3.select(this.el).style('width'), 10);

        // Bind the brush listener function. The listener will update
        // the model time interval
        var self = this;

        this.chart
            .width(width)
            .brushListener(function(extent) {
                self.model.set({from: extent[0], to: extent[1]});
            });

        // The view will render on changes to the model
        this.listenTo(this.model, 'change', this.render);
    },

We get the width of the this.el container element using D3 and set the chart width. This will make the chart use the full width of the user's viewport.

The stock context view

The chart fills the container width in Safari Mobile (iPhone Simulator)

The render method will update the chart's time extent, so it reflects the current state of the model, creates a selection that holds the container element of the view, binds the stock data, and invokes the chart using selection.call:

    render: function() {
        // Update the time extent
        this.chart
            .timeExtent([
                this.model.get('from'), 
                this.model.get('to')
            ]);

        // Select the container element and call the chart
        d3.select(this.el)
            .data([this.model.get('data')])
            .call(this.chart);

        return this;
    }

This view is the only component that can change the from and to attributes of the model.

The stock context view

The stock context view uses the brush behavior to set the time interval

The stock detail view

The stock detail view will contain a stock area chart, showing only a given time interval. It's designed to follow the time interval selected in the stock context view.

We create and configure a stockAreaChart instance, setting the margin, value, and date accessors and disabling the brushing behavior:

// Stock Detail Chart
app.StockDetailView = Backbone.View.extend({

    // Initialize the stock area chart
    chart: stockAreaChart()
        .margin({top: 5, right: 5, bottom: 30, left: 30})
        .value(function(d) { return +d.price; })
        .date(function(d) { return new Date(d.date); })
        .brush(false),

    // Render the view on model changes
    initialize: function() { ... },
    render: function() { ... }
});

As we did in the context view, we tell the view to invoke the render method on model changes in the initialize method:

    initialize: function() {

        // Get the width of the container element
        var width = parseInt(d3.select(this.el).style('width'), 10);

        // Set the chart width to fill the container
        this.chart.width(width);

        // The view will listen the application model for changes
        this.listenTo(this.model, 'change', this.render);
    },

In the render method, we update the chart time extent, so the visible section of the area chart matches the time interval specified by the application model s from and to attribute:

    render: function() {

        // Update the chart time extent
        var from = this.model.get('from'),
            to = this.model.get('to'),

        this.chart.timeExtent([from, to]);

        // Select the container element and create the chart
        d3.select(this.el)
            .data([this.model.get('data')])
            .call(this.chart);
    }
The stock detail view

The detail view shows you the stock prices for the selected time interval

Note that when using object.listenTo(other, 'event', callback), the this context in the callback function will be the object that listens for the events (object).

The application view

The application view will be in charge of creating instances of the views for each component of the application.

The initialize method binds the reset event of the app.Stocks collection and then invokes the collection's fetch method, passing the {reset: true} option. The collection will request the data to the server using its url attribute. When the data is completely loaded, it will trigger the reset event, and the application view will invoke its render method:

// Application View
app.StockAppView = Backbone.View.extend({

    // Listen to the collection reset event
    initialize: function() {
        this.listenTo(app.Stocks, 'reset', this.render);
        app.Stocks.fetch({reset: true});
    },

    render: function() { ... }
});

In the render method, we create instances of the views that we just created for each component. At this point, the current symbol of the application can be undefined, so we get the first stock in the app.Stocks collection and set the stock attribute of the model if is not already set.

We proceed to initialize the views for the title, the selector, the context chart, and the detail chart, passing along a reference to the model instance and the DOM element where the views will be rendered:

    render: function() {

        // Get the first stock in the collection
        var first = app.Stocks.first();

        // Set the stock to the first item in the collection
        if (!this.model.get('stock')) {
            this.model.set('stock', first.get('symbol'));
        }

        // Create and initialize the title view
        var titleView = new app.StockTitleView({
            model: this.model,
            el: 'div#stock-title'
        });

        // Create and initialize the selector view
        var controlView = new app.StockSelectorView({
            model: this.model,
            el: 'div#stock-control'
        });

        // Create and initialize the context view
        var contextView = new app.StockContextView({
            model: this.model,
            el: 'div#stock-context'
        });

        // Create and initialize the detail view
        var detailView = new app.StockDetailView({
            model: this.model,
            el: 'div#stock-detail'
        });

        // Fetch the stock data.
        this.model.fetchData();
        return this;
    }

Finally, we tell the model to fetch the stock data to allow the context and detail chart to be rendered. Remember that when the data is ready, the model will set its data attribute, notifying the charts to update its contents.

The application view

Components of the rendered application

Defining the routes

In our application, the state of the visualization can be described by the stock symbol and the time interval selected in the context chart. In this section, we will connect the URL with the application state, allowing the user to navigate (using the back button of the browser) the bookmark and share a particular state of the application.

We define the routes for our application by assigning callbacks for each hash URL (Backbone provides support for real URLs too). Here, we define two routes, one to set the stock and one to set the complete state of the application. If the user types the #stock/AAPL hash fragment, the setStock method will be invoked, passing the 'AAPL' string as the argument. The second route allows you to navigate to a specific state of the application using a URL fragment of the #stock/AAPL/from/Mon Dec 01 2003/to/Tue Mar 02 2010 form; this will invoke the setState method of the router:

app.StockRouter = Backbone.Router.extend({

    // Define the application routes
    routes: {
        'stock/:stock': 'setStock',
        'stock/:stock/from/:from/to/:to': 'setState'
    },

    // Initialize and route callbacks...
});

The router also has an initialize method, which will be in charge of synchronizing changes in the application URL with changes in the application model. We will set the model for the router and configure the router to listen for change events of the model. At the beginning, the data might not have been loaded yet (and the from and to attributes might be undefined at this point); in this case, we set the stock symbol only. When the data finishes the loading, the from and to attributes will change and the router will invoke its setState method:

    // Listen to model changes to update the url route
    initialize: function(attributes) {
        this.model = attributes.model;
        this.listenTo(this.model, 'change', function(m) {
            if (m.get('from') && m.get('to')) {
                this.setState(m.get('stock'), m.get('from'), m.get('to'));
            } else {
                this.setStock(m.get('stock'));
            }
        });
    },

The setStock method updates the symbol attribute of the model. The navigate method updates the browser URL to reflect the change of stock. Note that we are using the time interval as a variable of the application state. If we select an interval, the back button of the browser will get us to the previously selected time intervals. This might not be desirable in some cases. The choice of which variables should be included in the URL will depend on the application and the behavior that most users will expect. In this case for instance, an alternative approach could be to update the application state on drag start and drag end, not on every change in the interval:

        // Set the application stock and updates the url
        setStock: function(symbol) {
            var urlTpl = _.template('stock/<%= stock %>'),

            this.model.set({stock: symbol});
            this.navigate(urlTpl({stock: symbol}), {trigger: true});
        },

The setState method parses the from and to parameters from the URL as dates and sets the model's stock, from, and to attributes. As we cast the strings to the date, we can use any format recognizable by the date constructor (YYYY-MM-DD, for instance), but this will imply that we format the from and to attributes to this format when the model changes in order to update the URL. We will use the toDateString method to keep things simple. After setting the model state, we construct the URL and invoke the navigate method to update the browser URL:

        // Set the application state and updates the url
        setState: function(symbol, from, to) {

            from = new Date(from),
            to = new Date(to);

            this.model.set({stock: symbol, from: from, to: to});

            var urlTpl = _.template('stock/<%= stock %>/from/<%= from %>/to/<%= to %>'),
                fromString = from.toDateString(),
                toString = to.toDateString();

            this.navigate(urlTpl({stock: symbol, from: fromString, 
to: toString}), {trigger: true});
        }

The simple addition of a router can make an application way more useful, allowing users to bookmark and share a particular state of the page and navigate back to previous states of the application.

Defining the routes

The application state is displayed in the browser URL

Initializing the application

Once we have created the application models, collections, views, and router, we can create the instances for the application model and view. The application initialization code is in the chapter06/stocks/js/app.js file.

We begin by creating an instance of the app.StockList collection:

// Create an instance of the stocks collection
app.Stocks = new app.StockList();

The collection instances will be retrieved later.

We create an instance of the application model and an instance of the application view. We initialize the application model by indicating the model of the view and the container element ID:

// Create the application model instance
app.appModel = new app.StockAppModel();

// Create the application view
app.appView = new app.StockAppView({
    model: app.appModel, 
    el: 'div#stock-app'
});

Finally, we initialize the router, passing the application model as the first argument, and then we tell Backbone to begin monitoring changes to hashchange events:

// Initializes the router
var router = new app.StockRouter({model: app.appModel});
Backbone.history.start();
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset