D3 and Knockout

D3 and Knockout

By Tim Pollock, OCI Principal Software Engineer

February 2014


Introduction

This article demonstrates how to use the D3 and Knockout Javascript libraries to create a dynamic website. Both libraries will be introduced with simple examples that show some of the features we'll be using. The use of the libraries together will be demonstrated by building an example application for managing software development projects that use Agile development methods. In Agile development projects work is broken down into small parts called stories. This example application could be used to define those stories.

It is assumed that the reader has some knowledge of D3 and Scalable Vector Graphics (SVG) in order to understand these examples. There are references at the end of this article that you can use to gain a basic knowledge of each.

The name D3 stands for Data-Driven Documents, and is widely used to create graphs and visualizations. We'll be using D3 to draw rectangles representing stories.

Knockout is a Javascript library that lets you define a view model and bind it to controls on a page. We'll use Knockout to bind view model data to the controls used by our example application.

The examples shown in this article are available for download here.

Browser Support

The Javascript libraries used in this article only work on modern browsers. The links below show the browsers that support each of those libraries.

Knockout: http://knockoutjs.com/documentation/browser-support.html
D3: https://github.com/mbostock/d3/wiki
SVG: http://caniuse.com/svg

Simple Examples

Let's begin by looking at some simple examples of using the D3 and Knockout libraries. The examples will help show how those libraries are used and will make it easier to understand the more comprehensive example at the end of this article.

Knockout Observables

To start, let's demonstrate using the ko.observable type to bind data to controls.

The ko.observable is an object that contains data and can be bound to HTML elements. The relationship between an observable and the elements it is bound to depends on the type of each element. For instance, binding an observable to an input element makes the observable dependent on that element. The observable watches the element for change and updates its value to match the inputted value. Other elements subscribe to an observable when bound to it, and update their values when the observable changes.

The code for this first example is shown below. We define two HTML elements and bind both to a Knockout observable. The input element on line 10 is an input element where you can type in characters that are then shown in the span element on line 11. Both are bound to an observable that we defined on line 16 and named entry. The observable is dependent on the input control, and is subscribed to by the span element. The entry observable is updated when a character is entered, and the observable notifies the span element to update it's value.

  1. <!DOCTYPE html>
  2. <html lang="en">
  3.  
  4. <head>
  5. <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/knockout/2.3.0/knockout-min.js"
  6. charset="utf-8"></script>
  7. </head>
  8.  
  9. <body>
  10. <input data-bind="value: entry, valueUpdate: 'afterkeydown'" />
  11. <span data-bind="text: entry">
  12. </span>
  13.  
  14. <script type='text/javascript'>
  15. var ViewModel = function () {
  16. this.entry = ko.observable();
  17. };
  18.  
  19. ko.applyBindings(new ViewModel());
  20. </script>
  21. </body>
  22. </html>

The applyBindings call on line 19 tells Knockout to use an object of type ViewModel as the data source when binding data to controls.

To see this in action, enter something in the field below and notice that the <span> element is updated.

See the Pen wazWKg by OCI (@OCI) on CodePen.

(For the remaining examples we won't show all of the code, but you can always click the HTML tab to see the code behind each example, or download the code to examine it in detail.)

Knockout Computed Observables

We'll extend the previous example by adding a ko.computed type to format the value shown in the  element. Computed observables can subscribe to other observables, read their value, then modify it and send it to it's subscribers. In this example, the value of the entry observable is reversed and used to update the span element that subscribes to it.

The observable, shown on line 2, iterates over the characters of the entry observable (line 5) to build up a return value. The span element is then bound to the computed observable instead of to the entry observable, so it displays the reversed value.

  1. self.entry = ko.observable();
  2. self.computed = ko.computed(function() {
  3. var display = "";
  4. if (self.entry() !== undefined) {
  5. for (i=self.entry().length; i>0; i--) {
  6. display += self.entry()[i - 1];
  7. }
  8. }
  9. return display;
  10. });

Enter something in the field below to see the <span> element updated with the entry reversed.

See the Pen zGKyar by OCI (@OCI) on CodePen.

Knockout Observable Arrays

For the next example we'll add a button that, when clicked, adds what you type into a ko.observableArray. An observableArray contains an array of objects that you want Knockout to keep track of. We'll add the following code to the <script> from our previous example:

  1. self.array = ko.observableArray();
  2. self.addEntry = function () {
  3. if (self.entry() != undefined && self.entry().length > 0) {
  4. self.array.push(self.entry());
  5. self.entry("");
  6. }
  7. }

The ko.observableArray is defined on line 1 above. The value you enter is added to the array in line 4, then the input field is cleared.

We'll also add this HTML, which has a button (on line 1) whose click event is handled by our addEntry function, as well as a <span> (line 6) which we'll bind to the ko.observableArray. The span is only visible when our array contains elements. This is controlled by the Knockout visible binding which controls the visibility of a DOM element. Each element is displayed in the inner span by using the Knockout foreach binding (line 6). The $dataidentifier contains the array item to display.

  1. <button data-bind="click: addEntry">Add</button>
  2. <br />
  3. <span data-bind="visible: array().length > 0">
  4. Array:
  5. <span data-bind="foreach: array">
  6. [<mark data-bind="text: $data"></mark>]
  7. </span>
  8. </span>

Clicking the Add button after entering a value in the input field will add the value to the ko.observableArray, which causes the  section to be updated with the array's values.

[functionality removed since original publication]

Writable Computed Observables

Our previous computed observable example bound an element to the observable and displayed it's value. The value of that observable was read, but you can also write to observables. These are called writable observables and the next example shows how they work.

For this example we'll define two input fields where you can enter a temperature in Fahrenheit or Celsius:

  1. <input data-bind="value: fahrenheit, valueUpdate: 'afterkeydown'" style="width: 120px" />°F
  2. <input data-bind="value: celsius, valueUpdate: 'afterkeydown'" style="width: 120px" />°C

Next, we'll write some Javascript that defines our observables:

  1. var ViewModel = function () {
  2. var self = this;
  3. self.fahrenheit = ko.observable().extend({ throttle: 500 });
  4. self.celsius = ko.computed({
  5. read: function () {
  6. if (self.fahrenheit() != undefined) {
  7. return (self.fahrenheit() - 32) * 5 / 9;
  8. }
  9. },
  10. write: function (value) {
  11. self.fahrenheit(value * 9 / 5 + 32);
  12. }
  13. }).extend({ throttle: 500 });
  14. };
  15.  
  16. ko.applyBindings(new ViewModel());

Line 3 above defines an observable that is bound to the Fahrenheit input, and line 4 defines a writable computed observable that is bound to the Celsius input. The write function of the Celsius observable is what makes it a writable observable. Observables implicitly apply their logic to their read function. With writable observables you explicitly define the read function (line 5) as well as a write function (line 10). When a value is entered into the Fahrenheit field the read function of the Celsius observable, which is bound to the Fahrenheit observable, computes the Celsius value and returns it to the input control that it is bound to (the Celsius input). When a value is entered into the Celsius control, the write function of the Celsius observable converts that value to Fahrenheit and writes it to the Fahrenheit observable, which results in the Fahrenheit control being updated.

Both observables don't do anything until 500 ms after input has ceased, as defined by extend({ throttle: 500 }) on lines 3 and 13. In that code, throttle extends the functionality of the observable. You can add your own extenders by defining a function and passing it to the extend function, as in extend({ myFunction: myArgument }), where myFunction is the name of a function you've defined and myArgument is an option to pass to that function.

To see how this works, enter a numeric value in either of the input fields below and see how it calculates and displays the other temperature type.
[functionality removed since original publication] 

Subscribing to Observables

You can be notified of changes in observables by subscribing to them. For simple implementations this might not be necessary, but the example we'll be building in this article uses subscriptions, so we'll demonstrate them here.

To subscribe to an observable you simply chain the subscribe function to the observable and define a function that executes when the observable changes. For instance:

  1. myViewModel.myObservable.subscribe(function(newValue) { /* do something here */ });

In the example below we define an input field (line 4) where you can enter a value, and an Add button (line 5) that we'll bind to a function that adds the value to an array. We also have a Delete button (line 6) that is only shown when our array of values is not empty.

  1. <tr>
  2. <td>Input:</td>
  3. <td>
  4. <input data-bind="value: entry" />
  5. <button data-bind="click: addEntry">Add</button>
  6. <button data-bind="visible: array().length > 0, click: deleteEntry">Delete</button>
  7. </td>
  8. </tr>

We define an observable named entry to bind to the input field and an observableArray named array to hold the inputted values:

  1. self.entry = ko.observable();
  2. self.array = ko.observableArray();

We then define a function to populate the observableArray:

  1. self.addEntry = function () {
  2. if (self.entry().length > 0) {
  3. if (isNaN(self.entry())) {
  4. alert("Numbers only, please");
  5. }
  6. else {
  7. self.array.push(self.entry());
  8. }
  9. self.entry("");
  10. }
  11. }

In the addEntry function we check if the inputted value is not an empty string (line 2) and that it is a number (line 3). We then push it into our array (line 7) and clear the input (line 9). The code (not shown) for the Delete button simply removes the value you entered in the input field from the array.

Next, we display the contents of our array (if it is not empty) by binding to it and iterating through the array (line 4 below) and display each value (line 5).

  1. <tr>
  2. <td>Array:</td>
  3. <td data-bind="visible: array().length > 0">
  4. <span data-bind="foreach: array">
  5. [<mark data-bind="text: $data"></mark>]
  6. </span>
  7. </td>
  8. </tr>

We also define an observable array to sort the values into, as well as observables for the sum and average of the values:

  1. self.sorted = ko.observableArray();
  2. self.sum = ko.observable();
  3. self.average = ko.observable();

Finally, we subscribe to the observable array of inputted values:

  1. function sortNumber(a, b) {
  2. return a - b;
  3. }
  4.  
  5. viewModel.array.subscribe(function (newValue) {
  6. viewModel.sorted(newValue);
  7. viewModel.sorted.sort(sortNumber);
  8. viewModel.sum(newValue.reduce(function (a, b) { return (+a) + (+b) }));
  9. viewModel.average(viewModel.sum() / newValue.length);
  10. });

First, we replace the sorted array with the contents of our observable array (line 6). Then we sort that array (line 7) so the values are ordered. The default would be to sort alphabetically, so we define a sortNumber function (line 1) and pass it to our call to sort. Finally, we calculate the sum (line 8) and average (line 9) values.

To see this working, enter a number in the input field, then click Add. Do that with several numbers and you will see the array of values, as well as the sorted array and the sum and average of the values.

[functionality removed since original publication]

Scalable Vector Graphics (SVG)

Now let's create a simple page that demonstrates how to draw something with SVG. SVG is a markup language for describing two-dimensional vector graphics, and later we'll be using D3 and SVG to draw shapes. Let's start by drawing some circles directly with SVG.

The code for this example is shown below. We define an <svg> area, then draw circles at horizontal positions 15, 45 and 75.

  1. <!DOCTYPE html>
  2. <html>
  3. <body>
  4. <svg height="80" width="410" xmlns="http://www.w3.org/2000/svg" version="1.1">
  5. <circle cx="15" cy="50" r="10" stroke="black" fill="aqua" />
  6. <circle cx="45" cy="50" r="10" stroke="black" fill="aqua" />
  7. <circle cx="75" cy="50" r="10" stroke="black" fill="aqua" />
  8. </svg>
  9. </body>
  10. </html>

[functionality removed since original publication]

SVG Example using D3

In the previous example we explicitly defined each circle, but a dynamic web site might show objects that represent variable data. We can use D3.js to draw circles corresponding to a set of data. Code demonstrating that is shown below.

  1. var yPos = 50;
  2. var radius = 10;
  3. var positions = [15, 45, 75];
  4. var svg = d3.select("body").append("svg")
  5. .attr("height", 80)
  6. .attr("width", 410);
  7. var circles = svg.selectAll("circle")
  8. .data(positions)
  9. .enter()
  10. .append("circle");
  11. circles.attr("r", radius)
  12. .attr("cy", yPos)
  13. .attr("cx", function (d) { return d; })
  14. .style("stroke", "red")
  15. .style("fill", "aqua");

The data representing the x-position of each circle is defined on line 3. Anarea is appended to the body on line 4, then a D3 selection (line 7) is defined to contain the circles and the position data is bound to it on line 8. A circle element is added for each position on line 10, and the attributes of each circle is defined on line 11.

[functionality removed since original publication]

SVG Example using D3 and Knockout

Our last preliminary example extends the previous one by using Knockout to subscribe to an array of circle positions, then updates those circles using D3.

First, lets add some controls, similar to what we did in the Subscribing to Observables example. Values are entered on line 4 and added to an array by clicking the Add button. The values in the array are displayed on line 13, and circles, whose horizontal positions are defined by those values, are drawn in the node shown on line 19.

  1. <tr>
  2. <td>Input:</td>
  3. <td>
  4. <input data-bind="value: entry" />
  5. <button data-bind="click: addEntry">Add</button>
  6. <button data-bind="visible: array().length > 0, click: deleteEntry">Delete</button>
  7. </td>
  8. </tr>
  9. <tr>
  10. <td>Array:</td>
  11. <td data-bind="visible: array().length > 0">
  12. <span data-bind="foreach: array">
  13. [<mark data-bind="text: $data"></mark>]
  14. </span>
  15. </td>
  16. </tr>
  17. <tr>
  18. <td>Circles:</td>
  19. <td id="svg"></td>
  20. </tr>

An <svg> area is then appended to the <td> node with an id of svg (line 2, below), a function is defined for updating circles (line 7) and a ViewModel defined (line 23) that has functions for adding and deleting values from the position array. The updateCircles function selects all circle elements (line 8) in the svg selection, then binds the passed-in position data to that selection.

The enter selection (line 11) contains data that does not map to existing circles, and new circle elements are appended for each. The exit selection (line 14) contains circle elements that do not have corresponding data, and those elements are removed. Finally, the attributes of the remaining circles are set (line 16).

  1. var svg = d3.select("#svg")
  2. .append("svg")
  3. .attr("height", svgHeight)
  4. .attr("width", svgWidth);
  5.  
  6. window.onload = function () {
  7. function updateCircles(positions) {
  8. var circles = svg.selectAll("circle")
  9. .data(positions);
  10.  
  11. circles.enter()
  12. .append("circle");
  13.  
  14. circles.exit().remove();
  15.  
  16. circles.attr("r", radius)
  17. .attr("cy", yPos)
  18. .attr("cx", function (d) { return d; })
  19. .style("stroke", "red")
  20. .style("fill", "aqua").style("fill-opacity", "0.1");
  21. }
  22.  
  23. var ViewModel = function () {
  24. var self = this;
  25. self.entry = ko.observable();
  26. self.array = ko.observableArray();
  27. self.addEntry = function () {
  28. if (self.entry().length > 0) {
  29. if (isNaN(self.entry())) {
  30. alert("Numbers only, please");
  31. }
  32. if (self.entry() < minPos || self.entry() > maxPos) {
  33. alert("Enter a position between " + minPos + " and " + maxPos);
  34. }
  35. else {
  36. self.array.push(self.entry());
  37. }
  38. self.entry("");
  39. }
  40. }
  41. self.deleteEntry = function () {
  42. if (self.entry().length > 0) {
  43. if (self.array.indexOf(self.entry()) == -1) {
  44. alert("Not in array");
  45. }
  46. else {
  47. self.array.remove(self.entry());
  48. }
  49. self.entry("");
  50. }
  51. }
  52. };
  53. }

Finally, we create an instance of the ViewModel and bind it to the document (line 2, below). The array subscription on line 4 causes the updateCircles function to be called whenever the array of circle positions is changed, so when you add or delete array values you will see circles displayed with x-positions corresponding to those values in the array.

  1. var viewModel = new ViewModel();
  2. ko.applyBindings(viewModel);
  3.  
  4. viewModel.array.subscribe(function (newValue) {
  5. updateCircles(newValue);
  6. });

To test this, add values (between 10 and 400) to the array by entering them in the input field and clicking theAdd button. You'll see circles with their x-positions set to those values.

[functionality removed since original publication]

A Useful Example

Up to this point we've demonstrated the use of SVG, Knockout and D3 in preparation for a more thorough example. The remainder of this article will gradually put together a simple application that could be extended to create a tool for scheduling tasks in an Agile development project. Agile development breaks functionality down into reasonably-sized stories that are given values that represent their relative sizes. In Agile development these values are named story points and are an indication of how much work is involved in each story. For example, a story with a small amount of work, such as making a minor text change might have a point value of 1, while a more complex story, such as retrieving and displaying data, might have a point value of 10 or 20. These are just arbitrary values that represent the relative sizes of stories. The application we'll create will allow the user to easily modify story attributes and could be used in the planning stage of an Agile project.

1. Create a Drawing Area

To begin, lets create an area to draw in. Previously we defined an SVG area with the <svg> tags, but here we'll do it by using D3 to append an <svg> area to the body of the document, then set it's width and height. The code for that is shown below:

  1. var svg = d3.select("body").append("svg")
  2. .attr("width", canvas_width + (2 * padding))
  3. .attr("height", canvas_height + (2 * padding));

[functionality removed since original publication]

2. Draw a Horizontal Months Axis

Next, lets draw a horizontal axis with tick marks indicating months. First, we'll define project begin and end dates, with the begin date being now and the end date five months from now.

  1. var project_beg = new Date();
  2. var project_end = new Date();
  3. project_end.setMonth(project_end.getMonth() + 5);

Next we'll define a D3 time scale that maps from a domain, defined by the project begin and end dates, to a range, defined by the width of the canvas.

  1. var time_scale = d3.time.scale()
  2. .domain([project_beg, project_end])
  3. .range([0, canvas_width]);

We will create an SVG group (<g>) to contain the rules we'll be drawing. SVG <g> elements group elements together so you can apply a transformation to all of them at once. SVG translations allow you to rotate, scale or translate elements. In this example we'll be translating our rules by moving them down and to the right in order to provide some padding.

  1. var rules = svg.append("g")
  2. .attr("transform", "translate(" + padding + ", " + padding + ")")
  3. .classed("rules", true);

Next we'll define a function that will draw our horizontal months axis. In line 2 we declare an axis, then apply attributes to it. We apply the time scale that we defined earlier on line 3 and set the orientation to be a horizontal axis with tick marks above it by setting orient to top. We then set the ticks to indicate months, with one tick per month. We set the tick size to 5 pixels and format the date which will be displayed above each tick. Time formatting is defined in the D3 API Reference, with %b indicating abbreviated month and %Y indicating a 4-digit year.

  1. function make_month_axis() {
  2. return d3.svg.axis()
  3. .scale(time_scale) // Use the time scale defined above
  4. .orient("top") // Add ticks to the top of the axis
  5. .ticks(d3.time.month, 1) // One tick per month
  6. .tickSize(5) // Make ticks 5 pixels high
  7. .tickFormat(d3.time.format("%b %Y")) // Draw dates above ticks
  8. }

Finally, we append an SVG group to contain the horizontal months axis and set its CSS class. We then translate the axis down by the canvas height and call our function to draw the axis.

  1. rules.append("g").classed("months", true)
  2. .attr("transform", "translate(0," + canvas_height + ")") // Position at the bottom of the canvas
  3. .call(make_month_axis()) // Draw the axis defined above

This results in the horizontal months axis shown below.

[functionality removed since original publication]

3. Draw a Horizontal Weeks Axis

A second horizontal axis will overlay the months axis with red tick marks indicating weeks. First we'll define a function to draw the weeks axis. This will be similar to the function for the months axis, but we'll align our tick marks to one per week with no text above each tick.

  1. function make_week_axis() {
  2. return d3.svg.axis()
  3. .scale(time_scale)
  4. .orient("top")
  5. .ticks(d3.time.week, 1) // One tick per week
  6. .tickSize(3)
  7. .tickFormat("")
  8. }

As with the months axis, we'll draw it in a group element where it will be translated to the bottom of the canvas and styled with the D3 classed function.

  1. rules.append("g").classed("weeks", true)
  2. .attr("transform", "translate(0," + canvas_height + ")")
  3. .call(make_week_axis())

This results in the horizontal axis shown below, with green ticks for months and red ticks for weeks.

[functionality removed since original publication]

4. Draw a Horizontal Iterations Axis

A final horizontal axis will have tick marks indicating iterations. Agile projects are typically divided into short, even length sprints of about three weeks . The iteration tick marks will extend vertically from the bottom to the top of the drawing area, and will originate from a third horizontal axis aligned with the bottom of the canvas.

This axis will use a linear scale instead of the time scale used by the previous two axes. The scale will map a domain spanning the number of iterations to a range extending across the canvas width.

  1. var iteration_scale = d3.scale.linear()
  2. .domain([0, num_iterations])
  3. .range([0, canvas_width]);

We then define a function to draw our horizontal iterations axis. We want iteration numbers to be below the axis, so we set its orientation to bottom and set the tick size to be negative in order to have them extend from the bottom to the top of the canvas.

  1. function make_iteration_axis() {
  2. return d3.svg.axis()
  3. .scale(iteration_scale)
  4. .orient("bottom")
  5. .ticks(num_iterations)
  6. .tickSize(-canvas_height, 0, 0)
  7. }

As with the other axes, we'll style, draw and translate it, as shown below.

  1. rules.append("g").classed("iterations", true)
  2. .attr("transform", "translate(0," + canvas_height + ")")
  3. .call(make_iteration_axis())

This results in the axes shown below, with green ticks for months, red ticks for weeks and vertical blue lines for iterations.

[functionality removed since original publication]

5. Draw Vertical Priorities Axis

The final axis we'll draw will be vertical and will indicate story priority. It will have tick marks that extend horizontally across the drawing area to create a grid with the iteration axis tick marks. Agile stories are typically given a priority in order to determine which ones to work on first. Our stories will be represented by rectangles whose size indicates points and whose vertical position indicates priority. Horizontal position will indicate when work on the story should be started.

First, we'll define a linear scale with its domain defined by the number of priorities and its range defined by the canvas height.

  1. var priority_scale = d3.scale.linear()
  2. .domain([num_priorities, 0])
  3. .range([0, canvas_height]);

Next, we'll create a function to draw the priority axis. We'll align this axis with the left side of the canvas and draw five tick marks, then set the tick size to a negative number in order to make them extend across the width of the canvas.

  1. function make_priority_axis() {
  2. return d3.svg.axis()
  3. .scale(priority_scale)
  4. .orient("left")
  5. .ticks(num_priorities / 2)
  6. .tickSize(-canvas_width, 0, 0)
  7. }

Finally, we'll draw the vertical priority axis.

  1. rules.append("g").classed("priorities", true)
  2. .call(make_priority_axis())

This results in our final set of axes.

[functionality removed since original publication]

6. Define a Story

Now that we've set up our drawing area we can move on to defining a story. In a project that uses Agile development principles, work is broken down into chunks called stories which can be completed during a single iteration. An iteration might be defined as a three-week period, so a story is some piece of work that can be completed in that time frame. Stories are given point values to define how much work is involved in completing them.

We'll represent our stories by drawing rectangles whose width indicates story points. Wider rectangles are stories that are more complex than those represented by narrower rectangles. First, let's define a story as shown in the code below. Each story will have an identifier and a name, as well as a start date, priority and number of points. The start date determines the horizontal placement of a rectangle. The priority determines its vertical placement and the number of points determines its width. We'll create observables for each, as well as a computed observable that will be watched for change in order to redraw our rectangles.

  1. function Story() {
  2. var self = this;
  3.  
  4. // Set defaults for a new story.
  5. self.name = ko.observable("story" + num_stories++); // An identifier for this story
  6. self.story_name = ko.observable("New Story"); // The name of the story
  7. self.start_on = ko.observable(padding); // When a story starts
  8. self.priority = ko.observable(padding); // The priority of a story
  9. self.points = ko.observable(40); // The story points
  10.  
  11. // Create a computed observable that watches the members of a Story for change.
  12. // A subscription will be added for the story observable so that when any items change
  13. // an updateStory function is called, which results in the rectangles representing a story to be redrawn.
  14. self.story = ko.computed(function () {
  15. return { story_name: self.story_name(), start_on: self.start_on(),
  16. priority: self.priority(), points: self.points() };
  17. });
  18. };

We'll define a ViewModel class to contain an observableArray of Story objects, as well as a function for adding stories.

  1. function ViewModel() {
  2. var self = this;
  3.  
  4. self.stories = ko.observableArray([]);
  5. self.addStory = function () {
  6. self.stories.push(new Story(self));
  7. };
  8. };

In our windows.onload function we'll create a ViewModel and pass it to the Knockout applyBindings function in order to creating bindings between data and our user interface.

  1. var vm = new ViewModel();
  2. ko.applyBindings(vm);

Next, we'll define an updateStories function that will be called whenever a story is added, deleted or modified. Line 3 selects rect elements and binds them to the data that represents Stories. Line 8 adds rect elements to new data members. Line 17 sets the rectangle attributes and line 24 removes rect elements that no longer have corresponding data.

  1. function updateStories(data) {
  2. // Select all svg rect objects and join them with data.
  3. var rects = group
  4. .selectAll("rect")
  5. .data(data, function (d) { return d.id(); });
  6.  
  7. // Create new svg rect objects when new stories are added.
  8. rects.enter()
  9. .append("rect")
  10. .attr("id", function (d) { return d.id(); })
  11. .attr("opacity", 0.0)
  12. .transition()
  13. .duration(1000)
  14. .attr("opacity", 0.5);
  15.  
  16. // Update existing svg rect objects with story attributes.
  17. rects
  18. .attr("x", function (d) { return d.start_on(); })
  19. .attr("y", function (d) { return d.priority(); })
  20. .attr("width", function (d) { return d.points(); })
  21. .attr("height", function (d) { return rect_height; });
  22.  
  23. // Remove svg rect objects that have no corresponding data.
  24. rects.exit().remove();
  25. }

Finally, we'll create an array of subscriptions (line 1 below), then subscribe to our ViewModel's storiesobservableArray (line 4). Whenever that observableArray changes (a story is added or removed) the new values will be passed to the updateStories function (line 6), which will update our rectangles. The subscription function will then dispose of the subscriptions held in the subs array (line 10), then add a new subscription to our subscriptions array for each story (line 16). Whenever the attributes of any particular story is changed theupdateStories function is called (line 17), resulting in the rectangle representing that story being modified to reflect the change. The last step is to push one new Story into the stories observableArray when the window is first loaded (line 23).

  1. var subs = [];
  2.  
  3. // Register subscription to observableArray.
  4. vm.stories.subscribe(function (newValue) {
  5. // Call the updateStories function, which uses D3 to draw objects representing stories.
  6. updateStories(newValue);
  7.  
  8. // Dispose of any existing subscriptions.
  9. ko.utils.arrayForEach(subs, function (sub) {
  10. sub.dispose();
  11. });
  12.  
  13. // Register subscriptions for each story.
  14. ko.utils.arrayForEach(newValue, function (item) {
  15. // Register to call the updateStories function whenever a stories 'story' computed observable changes.
  16. subs.push(item.story.subscribe(function () {
  17. updateStories(newValue);
  18. }));
  19. });
  20. });
  21.  
  22. // Add one rectangle when the page is loaded.
  23. vm.stories.push(new Story());

This results in the rectangle shown in the upper-left corner of the canvas shown below.

[functionality removed since original publication]

7. Show the Story in a Table

Next, we'll display Story attributes in a table. We'll only show the table if our stories observableArray is not empty (line 1). If stories exist, we'll loop through them (line 10) and display their name, start date, priority and point value.

  1. <table data-bind="visible: stories().length > 0">
  2. <thead>
  3. <tr>
  4. <th>Name</th>
  5. <th>Starts on</th>
  6. <th>Priority</th>
  7. <th>Points</th>
  8. </tr>
  9. </thead>
  10. <tbody data-bind="foreach: stories">
  11. <tr>
  12. <td data-bind="text: story_name"></td>
  13. <td data-bind="text: start_on"></td>
  14. <td data-bind="text: priority"></td>
  15. <td data-bind="text: points"></td>
  16. </tr>
  17. </tbody>
  18. </table>

Now we have a table that displays the attributes of our stories. Right now we have just the one story that we've hard-coded, but soon we'll add the ability to modify and add stories.

[functionality removed since original publication]

8. Add Controls to the Story Table

Now let's add a jQuery UI DatePicker to the table to control a stories start date, and add Spinner widgets to change it's priority and point value. We'll change thedata-bind for our start date cell to define the attributes of a date picker. To do that we'll use another Javascript library (line 2) that provides Knockout bindings for jQuery UI widgets. This makes it easy to set the date format, as well as the minimum and maximum dates.

  1. <td>
  2. <input name="'date' + $data.id()" style="width: 120px" data-bind="datepicker: {
  3. dateFormat: 'D M dd yy',
  4. minDate: project_beg,
  5. maxDate: project_end
  6. },
  7. value: start_on_disp" readonly="true" />
  8. </td>

The value that we display in the start date field comes from a computed observable named start_on_disp (on line 7 above). The read function (line 3 below) takes the start_on value, which is a number between 0 and the canvas width, and passes it to the D3 time scale invert function. The invert function maps the range (0 to canvas width) to the domain (project start to project end). The result is a date specified by the horizontal position of the rectangle.

The write function (line 6) takes a value (a date) and passes it to the time scale, which maps it to a horizontal rectangle position, then sets the start_on observable to that value.

  1. self.start_on_disp = ko.computed({
  2. read: function () {
  3. return time_scale.invert(self.start_on() - padding).toDateString();
  4. },
  5. write: function (value) {
  6. self.start_on(time_scale(Date.parse(value)) + padding);
  7. }
  8. }).extend({ throttle: 500 });

The priority and point value of each story will be displayed in spinner controls. We'll use the Knockout bindings for jQuery UI widgets library again to make it easier to work with the spinner controls. We declare the spinners (lines 2 and 10) along with their minimum and maximum values. The spinstop valueUpdate value indicates that we should not update the bound observable until the user is done changing the spinner control.

  1. <td>
  2. <input name="'priority' + $data.id()" style="width: 50px" data-bind="spinner: {
  3. min: 1,
  4. max: num_priorities
  5. },
  6. value: priority_disp,
  7. valueUpdate: 'spinstop'" readonly="true" />
  8. </td>
  9. <td>
  10. <input name="'points' + $data.id()" style="width: 50px" data-bind="spinner: {
  11. min: min_points,
  12. max: max_points
  13. },
  14. value: points,
  15. valueUpdate: 'spinstop'" readonly="true" />
  16. </td>

As with the datepicker control we discussed earlier, the value we display for the priority spinner control (line 6, above) comes from a computed observable. The code for that is shown below. The read function (line 2) calculates a priority value between 1 and num_priorities from the priority observable (which holds the vertical position of a rectangle). The write function (line 6) does the opposite and sets the priority observable based on the displayable priority value that it receives.

  1. self.priority_disp = ko.computed({
  2. read: function () {
  3. return Math.round(
  4. (canvas_height + padding - self.priority()) / canvas_height * num_priorities);
  5. },
  6. write: function (value) {
  7. self.priority(Math.round(
  8. canvas_height + padding - ((canvas_height * value) / num_priorities)));
  9. }
  10. }).extend({ throttle: 500 });

This results in the table shown below. Now when you change the start-on date, the priority or the point value of the story the updateStories function is triggered, since each story has been subscribed to and the subscription calls updateStories. This results in the horizontal position of the rectangle changing when you change the start-on date, the vertical position of the rectangle changing when you change a stories priority, and the width of the rectangle changing when you change its point value.

[functionality removed since original publication]

9. Add Story Dragging

Now we'll modify the code to allow the user to drag stories in order to modify their start date and priority. First, we'll define a function to handle the drag event for a rectangle. Line 3 registers a function to be executed when the drag event occurs. The start date is modified by the amount of horizontal change (line 5) and the priority is modified by the amount of vertical change (line 6). We then check that the rectangle is still within bounds and modify its position accordingly (lines 9 and 17).

  1. var dragStory = d3.behavior.drag()
  2. .origin(Object)
  3. .on("drag", function (d) {
  4. // Update the view model
  5. d.start_on(d.start_on() + d3.event.dx);
  6. d.priority(d.priority() + d3.event.dy);
  7.  
  8. // Keep rectangle within horizontal bounds
  9. if (d.start_on() < padding) {
  10. d.start_on(padding);
  11. }
  12. else if (d.start_on() + parseInt(d.points()) > canvas_width + padding) {
  13. d.start_on(canvas_width - parseInt(d.points()) + padding);
  14. }
  15.  
  16. // Keep rectangle within vertical bounds
  17. if (d.priority() < padding) {
  18. d.priority(padding);
  19. }
  20. else if (d.priority() + rect_height > canvas_height + padding) {
  21. d.priority(canvas_height - rect_height + padding);
  22. }
  23. });

We then modify the updateStories function to call our dragStory function (line 6 below) when it sets the attributes for each story, which results in each story being registered to update its location when the drag event occurs.

  1. rects
  2. .attr("x", function (d) { return d.start_on(); })
  3. .attr("y", function (d) { return d.priority(); })
  4. .attr("width", function (d) { return d.points(); })
  5. .attr("height", function (d) { return rect_height; })
  6. .call(dragStory); // see https://github.com/mbostock/d3/wiki/Selections

This results in our being able to reposition the story rectangle by dragging it. Notice that as it is dragged the start date and priority is updated in the table. This is because we're modifying the observables that are bound to those table cells.

[functionality removed since original publication]

10. Add Story Points Dragging

Now we'll add a line on the right side of the story that the user can grab and drag right or left to modify story points. Like our drag event handler for dragging rectangles, we'll add a function that will handle dragging the right side of a rectangle. That function will modify the points observable (line 7) with the amount of horizontal change.

  1. var changePoints = d3.behavior.drag()
  2. .origin(Object)
  3. .on("drag", function (d) {
  4. // Update the view model
  5. var newValue = +d.points() + +d3.event.dx;
  6. if (min_points <= newValue && newValue <= max_points) {
  7. d.points(newValue);
  8. }
  9. });

Next we'll modify the updateStories function to draw a horizontal line along the right side of each rectangle. Like our code for binding rectangles to the data, we'll select all line elements (line 3), then append line elements to unbound data (line 8). We'll handle mouseover (line 9) and mouseout (line 13) events by changing the mouse cursor. Next, we'll set the line attributes (line 19) and register for drag events (line 30). Finally, we'll remove anyline elements that are no longer bound to data.

  1. // Select all svg line objects and join them with data.
  2. var lines = group
  3. .selectAll("line")
  4. .data(data, function (d) { return d.id(); });
  5.  
  6. // Create new svg line objects when stories are added.
  7. lines.enter()
  8. .append("line")
  9. .on("mouseover", function (d) {
  10. // Set cursor to the double-arrowhead resize cursor.
  11. d3.select("body").style("cursor", "ew-resize");
  12. })
  13. .on("mouseout", function (d) {
  14. // Set cursor to the default cursor.
  15. d3.select("body").style("cursor", "default");
  16. });
  17.  
  18. // Update existing svg line objects with story attributes.
  19. lines
  20. .attr("x1", function (d) {
  21. return +d.points() + +d.start_on() + +1;
  22. })
  23. .attr("x2", function (d) {
  24. return +d.points() + +d.start_on() + +1;
  25. })
  26. .attr("y1", function (d) { return d.priority() + rect_height; })
  27. .attr("y2", function (d) { return d.priority(); })
  28. .attr("stroke", "red")
  29. .attr("stroke-width", "2")
  30. .call(changePoints);
  31.  
  32. // Remove svg line objects that have no corresponding data.
  33. lines.exit().remove();

Now when you move the mouse over the red line on the right side of a rectangle you'll see the cursor change to have double-arrows. You can then drag the line to change the rectangle width. As it changes you'll see the point value change in the table.

[functionality removed since original publication]

11. Add a Story Tooltip

The next step is to add a tooltip to each story so we can see it's name when we hover the mouse over it. First, we'll define a CSS class to style our tooltips.

 
  1. div.tooltip
  2. {
  3. position: absolute;
  4. padding: 2px;
  5. font: 10px sans-serif;
  6. background: lightsteelblue;
  7. border: 0px;
  8. border-radius: 8px;
  9. }

Next we'll append a <div> to the document, style it as a tooltip and set it's opacity to 0 so it is invisible.

  1. var div = d3.select("body").append("div")
  2. .attr("class", "tooltip")
  3. .style("opacity", 0);

Finally, we'll modify the D3 code that appends rectangles to have each rectangle handle mouseover (line 3) and mouseout (line 14) events. On mouseover we'll make the tooltip visible (line 7) and set the text within it to the story name (line 10). On mouseout we'll hide the tooltip div (line 18).

  1. rects.enter()
  2. .append("rect")
  3. .on("mouseover", function (d) {
  4. // Make the tooltip visible.
  5. div.transition()
  6. .duration(200)
  7. .style("opacity", .9);
  8.  
  9. // Set the tooltip text and position
  10. div.html(d.story_name())
  11. .style("left", (d3.event.pageX) + "px")
  12. .style("top", (d3.event.pageY) + "px");
  13. })
  14. .on("mouseout", function (d) {
  15. // Hide the tooltip
  16. div.transition()
  17. .duration(500)
  18. .style("opacity", 0);
  19. })
  20. .attr("id", function (d) { return d.id(); })
  21. .attr("opacity", 0.0)
  22. .transition()
  23. .duration(1000)
  24. .attr("opacity", 0.5);

Now when you put the mouse pointer over the rectangle you'll see a tooltip showing the rectangle name.

[functionality removed since original publication]

12. Modify the Story Name

Now we'll change the code so that we can modify the story name and have it's tooltip updated. To accomplish this we'll add an input control to allow the name to be changed. The input will be bound to the story_name observable.

  1. <td>
  2. <input name="'story_name' + $data.story_name()" style="width: 120px"
  3. data-bind="value: story_name, valueUpdate: 'afterkeydown'" />
  4. </td>

Now when you change the story name you'll see the name in the tooltip updated, too.

[functionality removed since original publication]

13. Add Stories

Next we'll now add a button that, when clicked, will add a story.

  1. <button data-bind="click:addStory">Add</button>

When the button is clicked a new function, named addStory will be executed. We'll add that function to our view model (line 5 below). When executed it will add a new Story object to our stories observableArray (line 6).

  1. function ViewModel() {
  2. var self = this;
  3.  
  4. self.stories = ko.observableArray([]);
  5. self.addStory = function () {
  6. self.stories.push(new Story(self));
  7. };
  8. };

Since a change in the stories observableArray results in the updateStories function being called, adding the new Story will result in D3 adding a rect element to the DOM. New stories are added at the top-left corner of the canvas, so the workflow would be to add a Story with the Add button, then drag it somewhere on the canvas and set its attributes.

[functionality removed since original publication]

14. Select a Story

Let's track when the mouse enters a rectangle representing a story and change the color of the rectangle, along with the color of the table row representing it. First, we'll add an observable to our Story class (line 9, below).

  1. function Story() {
  2. var self = this;
  3.  
  4. self.id = ko.observable("story" + num_stories++);
  5. self.story_name = ko.observable("New Story");
  6. self.start_on = ko.observable(padding);
  7. self.priority = ko.observable(padding);
  8. self.points = ko.observable(40);
  9. self.selected = ko.observable(false); // Whether or not this story is selected

We'll then set that observable to true or false when the mouse enters (line 10) and leaves (line 27) the rectangle. We'll also set the fill color of the selected rectangle when we enter it (line 8) and leave it (line 25).

  1. rects.enter()
  2. .append("rect")
  3. .on("mouseover", function (d) {
  4. d3.select(this)
  5. .attr("opacity", 0.5)
  6. .transition()
  7. .duration(300)
  8. .style("fill", "LightSkyBlue")
  9. .attr("opacity", 1.0);
  10. d.selected(true);
  11.  
  12. div.transition()
  13. .duration(200)
  14. .style("opacity", .9);
  15.  
  16. div.html(d.story_name())
  17. .style("left", (d3.event.pageX) + "px")
  18. .style("top", (d3.event.pageY) + "px");
  19. })
  20. .on("mouseout", function (d) {
  21. d3.select(this)
  22. .attr("opacity", 1.0)
  23. .transition()
  24. .duration(300)
  25. .style("fill", "Black")
  26. .attr("opacity", 0.5);
  27. d.selected(false);

We'll also define a CSS class that sets a background color indicating an element is selected.

  1. .selected
  2. {
  3. background-color: LightSkyBlue;
  4. }

And we'll style the table row that corresponds to the selected rectangle.

  1. <tr data-bind="css: {'selected': selected}">

Now when the mouse pointer passes over a rectangle both the rectangle and the table row representing it are highlighted.

[functionality removed since original publication]

15. Delete a Story

As a last step, let's add a button for deleting stories. When the button is clicked a function named deleteStory is executed.

  1. <button data-bind="click: $parent.deleteStory">Delete</button>

The deleteStory function (line 8 below) removes the story from the stories observableArray (line 9).

  1. function ViewModel() {
  2. var self = this;
  3.  
  4. self.stories = ko.observableArray([]);
  5. self.addStory = function () {
  6. self.stories.push(new Story(self));
  7. };
  8. self.deleteStory = function (story) {
  9. self.stories.remove(story);
  10. };
  11. };

Now when you click the delete button on a table row the rectangle associated with that row is deleted.

[functionality removed since original publication]

Summary

This article has described using the D3 and Knockout Javascript libraries. We began by covering some basics of those libraries with simple examples, then worked through the development of a more complete application.

The example shown in this article could be extended to be a useful application by modifying it to save and restore its state, and by adding controls to allow for additional story attributes. For example, you could include a story description or provide the ability to assign developers to particular stories.

Hopefully this article has provided some insight into how D3 and Knockout can be used together to create useful applications. It only touched the surface of D3's capabilities. The references below go into much greater detail and have links to many examples and tutorials.

References

Tutorials and details about some of the tools used in this article are listed below.