Eloquent JavaScript: Laying out a Table

Introduction

When reading the 2nd Edition of Eloquent JavaScript, I noticed that the "Laying out a Table" section in Chapter 6 seemed like it could be a sticking point for many for beginners trying to learn Javascript. After poking around a bit online, I saw a couple comments that confirmed this assumption.

I think the only way to show what is going on is to trace the drawTable function from begining to end. I used the rows array that was built with the checkerboard code to go trace the function. I documented all of the steps in hopes that it helps anyone learning from this book get unstuck.

I am assuming you already have a firm grasp of the map and reduce methods. Those are necessary to understanding much of the drawTable function.

Hopefully this guide is not too confusing and helps clear some things up!

Step 1: Build the rows array

For the sake of simplicity, lets go through the checkerboard problem but only make it a 2 x 2 board.

var rows = [];
for (var i = 0; i < 2; i++) {
    var row = [];
    for (var j = 0; j < 2; j++) {
        if ((j + i) % 2 == 0)
           row.push(new TextCell("##"));
        else
            row.push(new TextCell("  "));
    }
    rows.push(row);
}
console.log(drawTable(rows));

This code should be relatively easy to follow. Basically it is creating an array of arrays where the outer array (rows) is a container for all of the rows in the array. Each inner array represents one row of the checkerboard.

The line:

if ((j + i) % 2 == 0)

is the part of the code that creates the checker pattern. It takes advantage of the fact that if we fill in (with “##”) every cell where the sum of the row number and column number is even, a checkerboard pattern will appear. Keep in mind to start counting from zero, not one, when checking this for yourself.

If the “if statement” we just described above is true, then the code will push an object that is built with the TextCell constructor to the end of the current row array. Let’s take a look at the TextCell constructor:

function TextCell(text) {
  this.text = text.split("\n");
}

The TextCell constructor only has one parameter, text. We can see that the parameter is meant to be a string that will be the content for one of the cells in the table we are building. The string has the split method applied to it, which will turn it into an array. If there are any newlines present in the string that was used as an argument, then that is where the string will be separated into array elements when the array is being formed. For example:

var testObj = new TextCell(“This is a\nTEST”);

Will create an object called testObj that looks like:

{text: [“This is a”,”TEST”]}

Getting back to the checkerboard. When the conditional is true, the TextCell constructed object that is pushed to the row array will look like:

{text: [“##”]}

and when the conditional is false, the object that is pushed will look like:

{text: [“ ”]}

As we see in the checkerboard code, when the inner for loop completes itself, the array that has just been built, which represents a single row, will then be pushed to the outer rows array. With our 2 x 2 checkerboard, our complete rows array should look like:

[[{text: [“##”]},{text: [“ ”]}],[{text: [“ ”]},{text: [“##”]}]]

It is an array that contains two inner arrays. Each inner array contains two objects. Each object represents one cell or “box” in our checkerboard.

It is important to realize that the rows array is our input which the drawTable function will use to build a formatted checkerboard.

Step 2: Pass the rows array into the drawTable function

Now that we understand what rows looks like, we can move on to the hard part. We can see in the final line of the checkerboard code above, that rows is passed into the function, drawTable. When it is logged to the console, our checkerboard should look like:

// → ##   
//      ##

a 2 x 2 checkerboard where the upper left and bottom right boxes are filled in with ##.

Let’s look at the complete drawTable function to see how this is created:

function drawTable(rows) {
  var heights = rowHeights(rows);
  var widths = colWidths(rows);

  function drawLine(blocks, lineNo) {
    return blocks.map(function(block) {
      return block[lineNo];
    }).join(" ");
  }

  function drawRow(row, rowNum) {
    var blocks = row.map(function(cell, colNum) {
      return cell.draw(widths[colNum], heights[rowNum]);
    });
    return blocks[0].map(function(_, lineNo) {
      return drawLine(blocks, lineNo);
    }).join("\n");
  }

  return rows.map(drawRow).join("\n");
}

Step 3: Find the heights value with the rowHeights function

First, the drawTable function creates two variables, heights and widths, using the rowHeights and colWidths functions. We can see above that rowHeights and colWidths each takes in the argument, rows, which is the array that we just built and passed into drawTable. Lets take a look at rowHeights:

function rowHeights(rows) {
  return rows.map(function(row) {
    return row.reduce(function(max, cell) {
      return Math.max(max, cell.minHeight());
    }, 0);
  });
}

Lets break it down:

Line 1: rowHeights takes in the rows array as an argument. As a reminder, this is our rows array:

[[{text: [“##”]},{text: [“ ”]}],[{text: [“ ”]},{text: [“##”]}]]

Line 2: An array that is created with the Array.prototype.map method is being returned. The map method is being used on our rows array that we passed in. The callback function will be transforming the two inner row arrays and returning them in a new array. To clarify, the map method will first look at the first inner array:

[{text: [“##”]},{text: [“ ”]}]

followed by the second inner array:

[{text: [“ ”]},{text: [“##”]}]

They will be transformed by the body of the callback function we will see on line 3 and 4 and returned as a new array.

Line 3 & 4: We can see the callback function’s body is a reduce method what will go through each of the objects in the inner array and return a the height (in lines) of the tallest cell in the row we are looking at. It does this by checking each cell.minHeight and returning the largest one.

cell.minHeight comes from the TextCell prototype object. Lets take a look:

TextCell.prototype.minHeight = function() {
  return this.text.length;
};

By looking at the reduce function, we can see that cell represents each object in the inner arrays. So first, the reduce method is going to look at the top row (the first inner array) and see there are two objects in it. Its going to take the first object which is:

{text: [“##”]}

We see that the minHeight function simply returns the length of the array that is paired with the text property. It is an array with only one element so the length is 1. Don’t make the mistake of thinking it is asking for the length of the string in the array. That’s what the colWidths function is supposed to do as we will see in a moment.

Since both of the objects in the first row have arrays of length one, the map method is going to return 1. The exact same thing happens for the second row. Now we can see that the rowHeights function returns an array with the two elements we just found and it will look like this.

heights == [1,1]

Step 4: Find the widths value with the colWidths function

Now lets looks at the colWidths function:

function colWidths(rows) {
  return rows[0].map(function(_, i) {
    return rows.reduce(function(max, row) {
      return Math.max(max, row[i].minWidth());
    }, 0);
  });
}

Like rowHeights, the rows array is passed into function as an argument. Once again, this is what the entire rows array looks like:

[[{text: [“##”]},{text: [“ ”]}],[{text: [“ ”]},{text: [“##”]}]]

We see on line two of the colWidths function, it is returning an array that is generated by the map method on rows[0]. Looking at the rows array, rows[0] is this part:

[{text: [“##”]},{text: [“ ”]}]

It is an array containing two objects. We can see that the callback function that is being used as an argument in map is only concerned with the index and not the element value. The underscore signifies that the element value is not needed. We can see that map’s callback function returns a reduce method on the entire rows array (not just rows[0] like the map method that contains it) and returning the maximum width (in characters) for each column. We see that it specifically does this by looking at row[i].minWidth. This works because the first time the map method iterates, i will be 0. The reduce method will first look at the first row and then specifically look at row[0], which contains:

{text: [“##”]}

It will use the minWidth() method (which is located in the TextCell prototype object) to return the length of the string in the array which is the “##”. Let’s take a look at minWidth:

TextCell.prototype.minWidth = function() {
  return this.text.reduce(function(width, line) {
    return Math.max(width, line.length);
  }, 0);
};

We see that there is a reduce method being used because in certain cases, the array value that is being evaluated will have more than one string contained in it. That is not what is happening in this case. Since it is only the “##” contained in the row[0].text array, it will only return the length of that which is 2.

Next the reduce method looks at the second row[0] which we can see is:

{text: [“ ”]}

Following the same logic we see that this also returns 2 which is the same as what was in the previous row. And since it was the same, the reduce method will return a 2 to the first element in the array that is created by the map method.

Next the value of i iterates to 1. Since we can also see that both objects in the second column also only contain strings that have length two, we know that the second element in the array returned by the map method is going to be 2 as well. From this we conclude that colWidths returns the following array and sends it to the widths variable:

widths == [2,2]

Step 5: Find the blocks value

Now the the values for heights and widths are set, the next thing that the drawTable function does is looks at the last line which is:

return rows.map(drawRow).join("\n");

We see that the map method is being used on the rows array and it is taking in the drawRow function as a callback. Lets take a look at drawRow to see what it is doing.

function drawRow(row, rowNum) {
    var blocks = row.map(function(cell, colNum) {
      return cell.draw(widths[colNum], heights[rowNum]);
    });
    return blocks[0].map(function(_, lineNo) {
      return drawLine(blocks, lineNo);
    }).join("\n");
}

The first thing that is happening is that it is creating "blocks". Each row is being mapped by the draw function, and takes in widths[colNum] and heights[rowNum]. This is looking at the heights and widths variables (that contain arrays) that we went through in Step 3 and 4. Since drawRow is the callback function of a map method, the second parameter, rowNum refers to the index number that is currently being iterated. The same thing goes for colNum. Since cell.draw is in a map within a map (much like a loop within a loop) it is going to draw all of the cells in the first row, before moving on to the next row. Let’s look at the draw function.

TextCell.prototype.draw = function(width, height) {
  var result = [];
  for (var i = 0; i < height; i++) {
    var line = this.text[i] || "";
    result.push(line + repeat(" ", width - line.length));
  }
  return result;
};

Let’s look at what is happening step by step. The rows array

[[{text: [“##”]},{text: [“ ”]}],[{text: [“ ”]},{text: [“##”]}]]

is being mapped with the drawRow function. drawRow is going to take the first row

[{text: [“##”]},{text: [“ ”]}]

and the first rowNum index will start at 0. The blocks variable is being defined by mapping the first row. colNum will start at 0. We know by understanding the map method that cell is referring to the object inside of row. The first cell in the first row is

{text: [“##”]}

so we know that the draw method is going to be working on this object like this:

{text: [“##”]}.draw(widths[0], heights[0])

If we look at the widths variable we see that it is referencing the value 2, and the heights variable is referencing the value 1.

{text: [“##”]}.draw(2, 1)

Now, looking at TextCell.prototype.draw, we see that there is a for loop that loops through the height of each cell and assigns the content of each line in a cell to a variable called line. If the cell is empty, it will assign an empty string. Before line is pushed to the result array, it is concatenated with spaces until it is the length of width. Since in our case, all of the cells contain lines that are the same length, the repeat function will never be used. Here is the repeat function so you can see how it works.

function repeat(string, times) {
  var result = "";
  for (var i = 0; i < times; i++)
    result += string;
  return result;
}

After the for loop terminates, the result array is returned and eventually returned again as part of the map that is saved in the blocks variable. In our case, the first time drawRow iterates, the variable blocks will look like:

[[“##”],[“  ”]]

with each of the inner arrays being what was returned by the draw method.

The second, and final time it iterates, blocks will look like:

[["  "],["##"]]

Step 6: Create each line of the table/checkerboard with the drawLine function

Next we see that drawRows returns:

return blocks[0].map(function(_, lineNo) {
  return drawLine(blocks, lineNo);
}).join("\n");

blocks[0] is the first inner array in blocks and only contains one element since the cell (along with all of the other cells in this example) is only one line in height. It uses the drawLine function that along with drawRow, that was defined in drawTable.

  function drawLine(blocks, lineNo) {
    return blocks.map(function(block) {
      return block[lineNo];
    }).join(" ");
  }

If we look at our first iteration of blocks we see that drawLine has mapped

[[“##”],[“ ”]]

and joining them with a space. So it will look like:

“##___”

where the underscores represent the spaces. Note that the .join(\n) at the end of drawRow will not be used because the row has only one line.

Step 7: Draw the entire checkerboard

Finally, looking at the last line of the drawTable function, we should have all of the rows mapped out looking like:

[“##___”,”___##”]

with the underscores representing spaces. We see that the last thing drawTable does before returning this is it joins it together with a newline character so it will actually return this:

“##___\n___##”

When this is logged to the console, we will get our 2 x 2 checkerboard!

// → ##   
//      ##

Conclusion

One last thing to note is that the checkerboard doesn't show what the drawFunction table is fully capable of because all of the cells are identical in that they all have a height of 1 and a length of 2. Because of this the repeat function isn't utilized and the drawLine function doesn't have to deal with cells that contain multiple lines. Despite this, I hope you were able to follow along.

If you are still confused by this example, you may want to checkout Gordon Zhu's Eloquent JavaScript: The Annotated Version. Also, if you have any requests for future blog posts or clarifications to this post, let me know in the comments.

Good Luck!