Skip to main content

Laying Out A table: Eloquent JavaScript

While reading the 2nd edition of Eloquent JavaScript by Marijn Haverbeke I go through the chapter 1-4 without having any trouble but at chapter 5 I have struggled a little bit to understand what's going on but with the help of the Annotated version of Eloquent JavaScript by Gordon Zhu (the ex-Google developer) I understand it properly. At chapter 6 I understand most of the topic pretty much well except  the "Laying Out A table" section. I put much attention and give as much time as I can to understand the problem but it makes me crazy. Even the Annotated version of Eloquent JavaScript didn't help me much.

I did some search on google and got a lot of people who have faced the same problem and I am not alone who was struggling with it.

while searching for the problem in google I found a blog where a guy named Tomio Mizoroki who has explained the problem much better. But he didn't explain the rest of it. he just explains the first checkerboard problem.

You can find the problem from the online version of eloquent javaScript. Tomio Mizoroki has explained the checkerboard problem very well so I will explain the main problem of this topic - Nicely laid out a table from a given data. But I recommend you to read the Tomio Mizoroki's explanation first.

Remember I am an advanced beginner and I have never tried to explain code before. English is also not my native language, So some of my explanation may look wired for the readers.

Before reading this article be sure that you have a clear understanding on javaScript array method's such as map and reduce. You also must have a strong understanding on javaScript prototypes and contractors.

The Problem:

The input will be this -

var MOUNTAINS = [
  {name: "Kilimanjaro", height: 5895, country: "Tanzania"},
  {name: "Everest", height: 8848, country: "Nepal"},
  {name: "Mount Fuji", height: 3776, country: "Japan"},
  {name: "Mont Blanc", height: 4808, country: "Italy/France"},
  {name: "Vaalserberg", height: 323, country: "Netherlands"},
  {name: "Denali", height: 6168, country: "United States"},
  {name: "Popocatepetl", height: 5465, country: "Mexico"}
];

and the output will look like this in the browser console

name           height    country
------------   ------    -------------
Kilimanjaro    5895      Tanzania
Everest        8848      Nepal
Mount Fuji     3776      Japan
Mont Blanc     4808      Italy/France
Vaalserberg    323       Netherlands
Denali         6168      United States
Popocatepetl   5465      Mexico

Let's Get Into The Program:

Our input data is very massive. We will make it short so that we can track what is happening with them. so we will work with this input -

var MOUNTAINS = [
  {name: "Everest", height: 8848},
  {name: "vaalserberg", height: 323}
]

If our program works for this it will also work for large data set. So finally our output will look like this -

name         height
-----------  ------
Everest      8848  
Vaalserberg  323

Build The Rows Array:

The drawTable is the final function that makes this magic happen. But it only accepts a grid of cells. So at first we have to make our data a grid of cells. which we will call the rows. Our dataTable function will do the job. Let's take a look at dataTable function.

function dataTable(data) {
 var keys = Object.keys(data[0]);
 var headers = keys.map(function(name){
  return new UnderlinedCell(new TextCell(name));
 });
 var body = data.map(function(row){
  return keys.map(function(name){
   return new TextCell(String(row[name]));
  });
 });
 return [headers].concat(body);
}
console.log(drawTable(dataTable(MOUNTAINS)));

Let's break it down. At the last line our data "MOUNTAINS" have passed through the dataTable function and it accepts it as data.

The dataTable function creates 3 variable -  keys, headers, and body. Let's find out what those variable will contain.

The keys variable:

The first variable keys contain -

var keys = Object.keys(data[0]);

The standard Object.keys function returns an array of property names in an object. by data[0] it will look only in the first element in our data. which is {name: "Everest", height: 8848} and the Object.keys will pull out their properties. So our keys variable will contain  -

var keys = ["name", "height"];

The headers variable:

Step 1:

The second variable is headers.

var headers = keys.map(function(name){
  return new UnderlinedCell(new TextCell(name));
});

The map method has been applied on our keys. The callback function will transform the two inner array and return them in a new array. The callback function will return this -

return new UnderlinedCell(new TextCell(name));

The each element of keys has been sent to the TextCell contractor and the result of it has been sent to the UnderlinedCell constructor. At first, the map method will look at "name" and this will be passed to the TextCell contractor. Like this - new TextCell("name");
Let's take a look at new TextCell contractor.

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

The TextCell contractor will create an Object and the split("\n") method with turns it into an array. So finally our object will look like this -

{text: ["name"]}

Step 2:

This result from the Step 1 will be sent to the UnderlinedCell constructor. Like this -

new UnderlinedCall({text: ["name"]});

Let's take a look at the new UnderlinedCell constructor.

function UnderlinedCell(inner) {
 this.inner = inner;
};

The UnderlinedCell will create an object and it will look like this -

{inner: {text: ["name"]}}

Step 3:

The callback function of map will return {inner: {text: ["name"]}} on the first call and {inner: {text: ["height"]}} on the second call. So finally our header variable will contain -

var headers = [{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]

The Body variable:

the body is the third variable and the hardest part of the dataTable function.

var body = data.map(function(row){
  return keys.map(function(name){
   return new TextCell(String(row[name]));
  });
 });

The map method has been applied to our data and  the callback function  return another 'map' method that has been applied to keys. It's like a loop into another loop.

Remember what is our data and keys -

// data
var MOUNTAINS = [
  {name: "Everest", height: 8848},
  {name: "vaalserberg", height: 323}
]
// keys
var keys = ["name", "height"];

Step 1:

at first, the outer map method will look at  {name: "Everest", height: 5895} and the inner map will look at "name". So the inner map method's callback function will return this -

return new TextCell(String({name: "Everest", height: 5895}["name"]))
// Or
return new TextCell("Everest")

// and we will get from The TextCell constructor  -
{text: ["Everest"]}

The inner map method will go over the keys twice. Second time the inner map method's callback function will return this -

return new TextCell(String({name: "Everest", height: 5895}["height"]))
// or
return new TextCell("5895")

// And we will get from The TextCell constructor  -
{text: ["5895"]}

Step 2:

On the first call, the outer map will receive -

[{text: ["Everest"]},{text: ["5895"]}]

similarly, on the second call, the outer map will receive -

[{text: ["Everest"]},{text: ["5895"]}]

Finally, the body variable will contain -

var body = [[{text: ["Everest"]},{text: ["5895"]}], [{text: ["Vaalserberg"]},{text: ["323"]}]

Step 3 - Concat Headers And Body:

The dataTable function returns the headers and body by concatenating  it.

return [headers].concat(body);
// our headers is -
var headers = [{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]
// and body is -
var body = [[{text: ["Everest"]},{text: ["5895"]}], [{text: ["Vaalserberg"]},{text: ["323"]}]]

So finally dataTable function will return -

[[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}], [{text: ["Everest"]},{text: ["5895"]}], [{text: ["Vaalserberg"]},{text: ["323"]}]]

This is our rows. Finally, this rows has been passed to the drawTable function.

Get The Heights:

If we look at the drawTable function we can see that at first, it defines variable heights, our rows have been passed through the rowHeights function and the heights will contain the result. Let's take a look at rowHeights function -

Step 1:

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

In the rowHeights function map method has been applied to our rows and in the callback function reduce method has been applied to the each of the arrays of rows. So at first the map method will take a look at this as row -

[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]

and the reduce method will take a look at this as cell -

{inner: {text: ["name"]}}

Make sense?

Step 2:

reduce method's callback function will return the maximum value of height. cell.minHeight() method will be applied to each of our cells.

Let's take a look at cell.minHeight(), you have to keep in mind that "inner" object is being created by the UnderlinedCell constructor so cell.minHeight() refers to the UnderlinedCell prototypes.

UnderlinedCell.prototype.minHeight = function(){
 return this.inner.minHeight() + 1;
};
// This will return  -
{text: ["name"]}.minHeight() + 1

Step 3:

Again the "text" object is being created by the TextCell constructor so minHeight() will refer to the TextCell prototypes.

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

So finally it will return 1. because ["name"].length will give you 1. Don't make mistakes by thinking that this will return the length of the "name". The colWidths function will do this.

Step 4:

Get back to the Step 2. minHeight() function of UnderlinedCell prototype will return 2.

UnderlinedCell.prototype.minHeight = function(){
 return 1 + 1;
};

similarly {inner: {text: ["height"]}}] will also return 2.

Step 5:

Again our rows are -

[[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}], [{text: ["Everest"]},{text: ["5895"]}], [{text: ["Vaalserberg"]},{text: ["323"]}]]

it's an array of 3 arrays, so we can write it like this -

[ Array[2], Array[2], Array[2] ]

For the first array, Our map method will get - 2 . Similarly, for 2nd and 3rd array, our map method will get 1. So finally it will return [2,1,1].

But why the 2nd and the 3rd array will get 1 ?

well, if we take a closer look at our rows we can see that into the first array it has "inner" object (constructed by UnderlineCell) But the next 2 array don't have the "inner" object. So the next two array will not get through the UnderlineCell prototypes. We can see that minHeight() of UnderlineCell prototype adding 1 with the result.

Finally, our heights will be -

var heights = [2,1,1]

Get The Widths:

Step 1:

'widths' is the second variable that drawTable function has defined. it has passed the rows into the colWidths function. Let's take a look at 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);
 });
}

The colWidths function applied map method to the first row of rows. which is -

[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]

and the callback function of the map has applied reduce method to rows. We should be careful that the reduce method use the entire rows, not rows[0].  And the reduce method's callback function will return the maximum width. Let's see how.

Step 2:

On the first call, the map will look at the first index of rows[0] which is 0 (zero). and the second index is 1.

we can see from the arguments of the callback function that the value of rows[0] is not needed. And the reduce method will loop over the entire rows.

So on the first call, the reduce method will look up for -

[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}]
// and it will return -
Math.max(max, [{inner: {text: ["name"]}}, {inner: {text: ["height"]}}][0].minWidth())
// or
Math.max(max, {inner: {text: ["name"]}}.minWidth())

Let's take a look at minWidth() and find out what it returns.

Step 3:

Again first it will refer to the UnderlineCell prototypes and then TextCell prototypes.

UnderlinedCell.prototype.minWidth = function(){
 return this.inner.minWidth();
};
// So it will look at this -
{text: ["name"]}}.minWidth()

Step 4:

And then the minWidth() of TextCell prototype will be applied -

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

minWidth() of TextCell will apply a reduce method to the each of the element in that array. Currently, we have only one element in our array ["name"]. But in some situation, it may have multiple elements. The reduce method will return widest characterized word from this array. In this case, it will return 4. Because ["name"].length = 4.

Step 5:

Get back to the Step 2. For the first call to rows the reduce method receive 4 similarly for the 2nd and 3rd call it will receive 7 and 11. And it will return the maximum number to the map which is 11.

Similarly, for the second call of the map, it will look at the second index of row[0], which is 1. And through the process, it will receive the maximum number which is 6. So finally our widths variable will contain [11,6].

var widths = [11,6]

Draw The Rows:

If we take a close look at what the drawTable function returns we can see this -

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

The map method has been applied to the rows and the callback function is drawRow which is defined into the drawTable function. Let's take a look at drawRow.

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 drawRow function receives two arguments from rows. First is the value and second are the index number. At first, the drawRow has defined a variable blocks.

The blocks variable:

In the 'blocks' map method has been applied to the row. Here map method also receives two parameter - value and index number.

var blocks = row.map(function(cell, colNum){
   return cell.draw(widths[colNum], heights[rowNum]);
});

Step 1:

So for the first call, the row will be -

[{inner: {text: ["name"]}}, {inner: {text: ["height"]}}] // and rowNum = 0

And for the first call, the cell will be -

{inner: {text: ["name"]}}

this will return from the first call -

return cell.draw(widths[colNum], heights[rowNum]);
// or
return cell.draw([11,6][0], [2,1,1][0]);
// or
return cell.draw(11,2);
// or
return {inner: {text: ["name"]}}.draw(11,2);

Step 2:

Let's take a look at draw function. Again the object "inner" has been constructed by the UnderlineCell constructor so the draw function will refer to the UnderlineCell's prototype.

UnderlinedCell.prototype.draw = function(width, height){
 return this.inner.draw(width, height - 1)
  .concat([repeat("-", width)]);
};
// or
UnderlinedCell.prototype.draw = function(width, height){
 return {text: ["name"]}.draw(11, 1)    // height - 1 = 2 -1 =1
  .concat([repeat("-", width)]);
};

Again the draw function has been applied to the object but this time the draw function will refer to the TextCell's prototypes. The result that return from the draw function (by TextCell Prototype) it's concatenated ([repeat("-", width)]) with it. We will get to this point a little bit later. Let's take a look at TextCell's draw function.

Step 3:

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;
}

We can see that there is a for loop into the function. As our height is 1 this for loop will execute only once. And there is a variable line which will contain either the text object content or "". In this case, the line variable will contain -

var line = this.text[0]
// or
var line = "name";

before pushing the line into result it will call the helper function repeat. Let's take a look at repeat function.

Step 4:

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

The repeat function will receive two parameter

repeat(" ", width - line.length)
// or
repeat(" ", 11 - 4)  //   width = 11, "name".length = 4
// or
repeat(" ", 7)

from the repeat function's result, we can see that it will return 7 empty space.

Step 5:

Back to the step 3. It will add 7 empty space to the line and then pushed it to the result. so on step 3 draw function will return ["name       "].

On step 2 there is a 'concat' method has been applied to the result that comes from step 3 like this -

["name       "].concat([repeat("-", width)]);

similarly, the repeat will create 11 "-" character and it will add to the ["name       "] like this -

["name       ", "-----------"]

This was the first call to row in map method. On the second call similarly it will receive this -

["height      ", "------"]

So the blocks variable will contain -

var blocks = [["name       ", "-----------"], ["height      ", "------"]]

Similarly, on the 2nd and 3rd call of the rows in map, the blocks variable will contain something like this -

var blocks = [["Everest        "],["8848"]]
var blocks = [["Vaalserberg"],["323  "]]

The empty spaces may not be accurate that I have written to explain the code

Draw The Line:

Step 1:

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

We can see map method has been applied to the first element of blocks. On the first call, our blocks variable will contain -

var blocks = [["name       ", "-----------"], ["height      ", "------"]]
// and block[0] will be -
["name       ", "-----------"]

The callback function will return drawLine function and pass two arguments - the entire blocks and the lineNo which is 0 for now. It's looks like this -

drawLine( [["name       ", "-----------"], ["height      ", "------"]], 0)

Let's take a look at drawLine function.

Step 2:

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

map method has been applied to the blocks. The blocks containing 2 arrays. so at first the map will look at -

["name       ", "-----------"]

// and the callback function will return -
["name       ", "-----------"][0]
// or
["name       "]

similarly, for the second array, it will return ["height      "]. So finally the drawLine function will return -

[["name       "], ["height      "]].join(" ")
// or
"name        height      "

Step 3:

get back to the step 1. On the 2nd call of the map, the line number will be 1. So from the drawLine function, we will receive -

"----------- ------" // we will recieve this pattern

// So finally Step 1 will return -
["name        height      ","----------- ------"]
// And finally, the drawTable function will return -
["name        height      ","----------- ------"].join("\n")
//or
name        height      
----------- ------

Amazing ! we get our first nicely laid out table header.

Previously we saw that our drawTable function return this -

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

For the first row we will receive our first nicely laid out table header and for the 2nd and 3rd row similarly we will receive the rest of our nicely laid out table content.

Conclusion

This problem is very difficult for the beginners. I am also a beginner in programming and javaScript. I know that most of my explanation may look irrelevant to the readers. Your feedback will help me to improve it. So please don't hesitate to make comments and ask questions.

Comments

  1. This comment has been removed by the author.

    ReplyDelete
  2. Dear Parish,

    Thank you very much for your explanation. It has helped me a lot in understanding chapter 6 of eloquent javascript, as I am new to javascript as well.

    Best Regards,

    Ioannis

    ReplyDelete
  3. Hi Parish,

    Thank You for the guidance on this example! I'm relatively new to programming and without this to get through the #6Ch. in the Eloquent JS book, would have been really difficult and time-consuming. It was also good to hear I was not the only one who experienced problems with this task.

    Norbert

    ReplyDelete
    Replies
    1. Thanks for your comment. Eloquent JS is one of the best book to learn programming and javascript. But some chapter is so much complicated. I am still reading this book and i had to skip some project chapter because it was very difficult and time-consuming.

      Delete
  4. Hi Parish,

    I wondered if JS was indeed for me when all my attempts at trying to understand this example failed; you reinstated my confidence with your wonderful exposition. Please do more of your 'simplification' posts - you'll make learners like me really happy

    Best
    Srini

    ReplyDelete
  5. you are my hero .
    you saved my time

    ReplyDelete
  6. Thanks a lot Parish , for such a wonderful and step by step explanation .

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. Hi Parish,

    Very helpful information . Thanks . I think on the body variable section 2 there is a correction

    "similarly, on the second call, the outer map will receive -
    [{text: ["Everest"]},{text: ["5895"]}]

    Actually,
    //i think [{text: ["Vaalserberg"]},{text: ["323"]} object should be mentioned instead of {text: ["Everest"]},{text: ["5895"]} object .

    ReplyDelete

Post a Comment

Popular posts from this blog

Play video while downloading it

You have decided to watch a movie. You go to your favorite movie download site and started downloading it. It will take 2 hours to complete the download. Probably, You will wait 2 hours to watch the movie. Right? Not anymore.You can download and watch the movie simultaneously.  Here's how. Essential software: Internet download manager VLC media player This trick should work on any downloader but I am using IDM here because it's the most popular download manager. I recommended VLC player. It works great in this situation. Other players may or may not work. Software settings: Open IDM and navigate to Download > Options . Now click on the "Connection" tab. Under the max connections number select "Default max conn. number" to "1" .  Its very important because by default IDM use 8 connection to download files from the server. It means IDM download your files on 8 part and when download finished IDM combine the 8 part. But it's im

Top Video Tutorials, Sites And Resources To Learn React

ReactJS was a trading technology of 2016 and 2017 is also a very good time to learn React. On a very short time, I have seen a lot of tech giant companies to move their web application on React. Facebook , Instagram , Dropbox , New York Times , Yahoo Mail and so many big companies are using React right now on production.

Essential Visual Studio Code Extension For Web Designer

Visual studio code is on of the most popular code editor for web designers and developers. It’s simple interface and variety of language support makes it so awesome. In visual studio code, you can use extensions to extend its functionality. There are thousand of extensions are available on visual studio marketplace. But I want to highlight 5 most useful extensions for web designer and developer that will increase productivity.