Hello everyone! :)
Here are some notes I typed up to help you guys understand the beginning of the Table Layout example in chapter 6 of eloquent javascript.
This isn't the best worded thing in the world, but I believe if you work through it things will click and you will understand what is going on.
This is a reaaaally long-winded explanation of it all, but hopefully it is helpful :)
Enjoy!
NOTE: THIS CODE WAS DESIGNED TO BE READ IN A CODE EDITOR LIKE TEXTWRANGLER OR IN THE INTERACTIVE PROMPT AT http://eloquentjavascript.net/06_object.html
THE TABLES I PRINTED OUT FOR REFERENCE DO LINE UP PROPERLY ON BLOGGER
/*
var exampleData = [ //layed out in rows
[ //row one (really row 0, etc)
TextCell{text: ["##", "__"]},//the rows contain TextCell objects which contain the text
TextCell{text: [" "]},
TextCell{text: ["##", "__"]},//if there are two text chunks that represents a height of 2
TextCell{text: [" "]},
TextCell{text: ["##", "__"]}],
[ //row two
TextCell{text: [" "]},
TextCell{text: ["##", "__"]},
TextCell{text: [" "]},
TextCell{text: ["##", "__"]},
TextCell{text: [" "]}],
[ //row three
TextCell{text: ["##", "__"]},
TextCell{text: [" "]},
TextCell{text: ["##", "__"]},
TextCell{text: [" "]},
TextCell{text: ["##", "__"]}],
[
TextCell{text: [" "]},
TextCell{text: ["##", "__"]},
TextCell{text: [" "]},
TextCell{text: ["##", "__"]},
TextCell{text: [" "]}],
[
TextCell{text: ["##", "__"]},
TextCell{text: [" "]},
TextCell{text: ["##", "__"]},
TextCell{text: [" "]},
TextCell{text: ["##", "__"]}]
];
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
*/
var exampleData = [
[
new TextCell("##\n__"),
new TextCell(" "),
new TextCell("#u#\n_o_"),
new TextCell(" "),
new TextCell("##\n__")],
[
new TextCell(" "),
new TextCell("##\n__"),
new TextCell(".."),
new TextCell("##\n__"),
new TextCell(" ")],
[
new TextCell("##\n__"),
new TextCell(" "),
new TextCell("#u#\n_o_"),
new TextCell(" "),
new TextCell("##\n__")],
[
new TextCell(" "),
new TextCell("##\n__"),
new TextCell(".."),
new TextCell("##\n__"),
new TextCell(" ")],
[
new TextCell("##\n__"),
new TextCell(" "),
new TextCell("#u#\n_o_"),
new TextCell(" "),
new TextCell("##\n__")]
];
/*
The idea behind this is that it returns an array of RowHeights
So it reduces each row of TextCells to 1 value: the minimum RowHeight
(which really means the largest row height: the maximum of the rows,
but the minimum you need to make the row the right size when drawing)
(i.e. if you have a row of objects of different sizes, the minimum size(height)
of the row is the largest size of any of those objects, right?)
So let's break it down
It starts off with rows.map()
This means it is turning each row into some piece of info about the row
(i.e. it is mapping each row to the "minimumHeight" that row can be to
represent TextCell correctly (i.e. a two line hight textcell can't be
represented with a one cell high row))
Then it uses row.reduce()
It is RETURNing row.reduce()'s result.
This is because it is mapping the row itself to the result of the reduce
Reduce takes two arguments (max, cell).
Now reduce usually takes the first element, and the next element in the
array as arguments.
However, if you look at the code, it has
return Math.max(max, cell.minHeight());
Hmm you think, 'max' is a TextCell object.
Thus it should fail in Math.max, since max() is a numeric function
However, if it was coded as
return Math.max(max.minHeight(), cell.minHeight());
Then on the second attempt at reducing we would have
return Math.max( (previous numeric result).minHeight(), cell.minHeight());
"WAIT!" you think, the previous result is a number!
Thus the next call to "max.minHeight()", using the prior result, would fail.
Numbers don't have a minHeight method.
What we really want to do is compare that prior max number with the next
cell.minHeight.
So what do we do?
That is where the second argument to reduce() comes in.
If you noticed, the Eloquent code has the following structure
row.reduce(function(max,cell){ //code }, 0);
See that zero?
That is the default first 'max', or current element.
What this means is that the reduce uses that as the first 'max'
and uses the first element of the row array as the current 'cell'.
I.e.
The first time it runs it will do:
return Math.max( 0, cell.minHeight());
//cell is row[0], NOT row[1] like it normally would be.
This fixes both issues we had with our earlier attempts!
Because the next time it will still use return Math.max(max, cell.minHeight)
only now max will always be a number and never a TextCell object!
Success!
*/
function rowHeights(rows) {
return rows.map(function(row) {
return row.reduce(function(max, cell) {
return Math.max(max, cell.minHeight());
}, 0);
});
}
//The example below prints out all 2 because the minimum height is 2 "##\n__" => "##", "__" (two lines!)
console.log(rowHeights(exampleData));
/* Alright, let's check out this one.
For reference, if we printed out drawTable(exampleData) we would get:
## #u# ##
__ _o_ __
## .. ##
__ __
## #u# ##
__ _o_ __
## .. ##
__ __
## #u# ##
__ _o_ __
carefully notice the 3rd column, see how it has a width of 3 but the ".." element
has a width of 2? (in our actually code definition of it
new TextCell(".."), //<- br="" not="" of="" three="" two="" width=""> This is where the rest of this explanation comes in.
This is what cellWidths is all for, the padding and alignment of the columns
with their subcolumns.
The idea behind this one is a bit trickier.
I am just going to phrase it bluntly, which will make little sense at first,
and then I am going to break it down so it makes sense.
It will help to know what the output of this is.
It will return an array of values, just like rowHeights, which represent the
maximum column widths of a given column
So, in my example, I purposely made the third column alternate between "#u#\n_o_"
and ".."
In the first one we would get
#u#
_o_
Upon printing it out. This column has a width of 3 characters, so it has a width of 3.
But upon printing the one out just below it we would get
".."
This column has a width of 2, not 3.
This means that at least two layers of the same overall column have different widths!
//3rd column
#u#
_o_
..//width of 2, doesn't match the width of the other pieces
#u#
_o_
..//width of 2, doesn't match the width of the other pieces
#u#
_o_
Thus, to find out what the minimum width a column needs to be to draw it all correctly
colWidths maps the place of this column to the "minimum" width needed.
By place I mean, the output of colWidths is
[col1's minimum width, col2's minimum width, col3's min width, etc]
As you can probably imagine in your mind's eye,
This array is like smooshing the entire table into a flat disk,
where each slot in the disk corresponds to the largest width of that corresponding
column.
(Note that the minimum width needed to draw a column is the maximum of the subcolumns
themselves. Minimum needed to draw right, maximum of the subpieces)
So, what this means is, if we ran
console.log(colWidths(exampleData));
//-> [2,2,3,2,2]
The output is saying column 1 has a minimum needed width of 2
The largest object in the 3rd column is 3 wide, so the minimum for that column is 3
This means smaller pieces in that column need padding.
(this is so that ".." will line up with "#u#". You just add a space ".. " taa daa lol)
Phew! That wasn't so hard was it?
Now we have to delve into the nasty code lol.
This is where it gets a bit trickier, but with the above concepts in mind,
it shouldn't be that bad ;)
So here is the blunt explanation first, and then I'll break it down.
Alright, so the basic structure of "rows" ("exampleData" in my example)
is
[ row1, row2, row3, row4, row5]
and the structure of any row is
[ itemFromColumn1, itemFromColumn2, itemFromColumn3, itemFromColumn5, itemFromColumn5]
Let's represent the first row as ordered pairs, where we have
TextCell(m,n) is the TextCell of row N(y) at position M(x)
This just makes sense though, doesn't it? rows are the y values, columns the X values
If you think about it like a grid then we have each row is
//rowN, where n is the row number 1, 2, 3, 4, or 5 etc
[ TextCell(1,n), TextCell(2,n), TextCell(3,n), TextCell(4,n), TextCell(5,n)]
Thus the whole array is really like the following diagram, which is basically a grid
[ TextCell(1,1), TextCell(2,1), TextCell(3,1), TextCell(4,1), TextCell(5,1)]//row1
[ TextCell(1,2), TextCell(2,2), TextCell(3,2), TextCell(4,2), TextCell(5,2)]//row2
[ TextCell(1,3), TextCell(2,3), TextCell(3,3), TextCell(4,3), TextCell(5,3)]//row3
[ TextCell(1,4), TextCell(2,4), TextCell(3,4), TextCell(4,4), TextCell(5,4)]//row4
[ TextCell(1,5), TextCell(2,5), TextCell(3,5), TextCell(4,5), TextCell(5,5)]//row5
In my model I start the numbers at 1, but they really start at 0 because arrays
start at 0.
But as you should be able to tell, the row number is clearly the "Y" value in the grid
And the index into any given "row" is like the "X" value into the grid.
Now how does this apply to understanding the code?
Well, the code returns just the first array of the above:
[ TextCell(1,1), TextCell(1,2), TextCell(1,3), TextCell(1,4), TextCell(1,5)]//row1
mapping each slot to a reduction of all the rows containing elements of that column.
First, let's understand the outer code, to understand what role 'i' has in all this,
but then let's dive straight into the inner code first, so we can understand the outer
code in relation to it.
Alright, so we first have
return rows[0].map(function(_,i){ //inner code blah blah});
The map() function takes two parameters (or more, but don't worry about that)
They are as follows
map(element, index_into_array)
We aren't using the element itself, because we are doing some super confusing
sneaky stuff and don't need it.
(basically if we just have map(element) it takes each element in the array and uses it
to determine what to map that slot to.
We are just using the index into the array to figure out what to map the slot to
This means we are just using the X values, or column numbers, to determine
what to map each slot to.
Each slot represents a column, so slot 0 represents column 0(the first one, col 1)
And we are only using that information, not the 'row' element stored in that slot,
to figure out what to put in that slot.
This is a highly unusual way to use map, (based on the prior examples) but it is nifty.
(tho confusing as all hell)
)
All we need is the index into the array itself.
This is equivalent to the column number, since the index into rows[0]
is the index into
[ TextCell(1,1), TextCell(1,2), TextCell(1,3), TextCell(1,4), TextCell(1,5)]//row1
In our model that would seem to be 1, 2, 3, 4, 5, the x values, but it is really 0 - 4, since
arrays start at 0. This is clearly the X value of our grid, or column number.
Okay, so i represents the column number, or 'x'.
It would have been better if the Eloquent code had named it as such, but sadly it
didn't.
Now that we know i represents the column number, let's dive into the inner code.
return rows.reduce(function(max, row) { //blah blah}, 0);
Notice that second parameter again? The zero?
That means the first time through the reduce's 'max' is 0 and 'row' is rows[0]
(the variables 'max' and 'row' take on those values the first time reduce does its thing)
This is the same idea as what we saw with rowHeights() earlier.
We are reducing all of the rows based on max, and the individual row.
Let's look at the inner code to figure out what is going on.
return Math.max(max, row[i].minWidth());
Woah! There is the 'i' again! Wait, what is i really?
return Math.max(max, row[columnNumber].minWidth());
Oh! okay.. sooo.. this is saying.. what exactly?
You have to remember that this reduction is embedded inside of a map.
This means that 'reduction', reduce(), is called 5 times, one for each time map maps an element
of row[0]. (row[0][0] - row[0][4]
This means each time reduce() is called 'i', columnNumber, is fixed, constant.
Thus, reduce compares each _>rows<_ against="" another.="" br="" column="" element="" i="" one="" th=""> (i.e. it compares each X slot of the grid with the ones right below it, incrementing the Y value)
Basically, in other words, putting all the pieces together, we get:
function colWidths(rows) {
//rows[0] = [ TextCell(1,1), TextCell(1,2), TextCell(1,3), TextCell(1,4), TextCell(1,5)]
return rows[0].map(function(_, column) { //column is columnNumber or the x value of the grid
return rows.reduce(function(max, row) {
return Math.max(max, row[column].minWidth());//using the X value as the fixed
// Y value
/*
i.e. for each row compare row[column] against the prior row's row[column]
i.e.
1st time map does what it does
map(row[0][0]), column = 0, to the result of reduce()
reduce() max = 0, row = rows[0] //[y][x(column)] (x,y)
return Math.max( 0, rows[0][0].minWidth()); //Max(0, TextCell(0,1))
return Math.max(priorResult, rows[1][0].minWidth()); //Max(0, TextCell(0,2))
return Math.max(priorResult, rows[2][0].minWidth()); //Max(0, TextCell(0,3))
return Math.max(priorResult, rows[3][0].minWidth()); //Max(0, TextCell(0,4))
return Math.max(priorResult, rows[4][0].minWidth()); //Max(0, TextCell(0,5))
//result[0] is now full, and contains the min
2cnd time map does what it does
map(row[0][1]), column = 1, to the result of reduce()
reduce() max = 0, row = rows[0] //[y][x] //comparing to model start @ 1
return Math.max( 0, rows[0][1].minWidth()); //Max(0, TextCell(1,1))
return Math.max(priorResult, rows[1][1].minWidth()); //Max(0, TextCell(1,2))
return Math.max(priorResult, rows[2][1].minWidth()); //Max(0, TextCell(1,3))
return Math.max(priorResult, rows[3][1].minWidth()); //Max(0, TextCell(1,4))
return Math.max(priorResult, rows[4][1].minWidth()); //Max(0, TextCell(1,5))
//result[1] is now full, and contains the min
3rd time map does what it does
map(row[0][2]), column = 2, to the result of reduce()
reduce() max = 0, row = rows[0] //[y][x] //comparing to model start @ 1
return Math.max( 0, rows[0][2].minWidth()); //Max(0, TextCell(3,1))
return Math.max(priorResult, rows[1][2].minWidth()); //Max(0, TextCell(3,2))
return Math.max(priorResult, rows[2][2].minWidth()); //Max(0, TextCell(3,3))
return Math.max(priorResult, rows[3][2].minWidth()); //Max(0, TextCell(3,4))
return Math.max(priorResult, rows[4][2].minWidth()); //Max(0, TextCell(3,5))
//result[2] is now full, and contains the min
4th time map does what it does
map(row[0][3]), column = 3, to the result of reduce()
//once again, let me reiterate, 3 is the X value, but due to the way 'rows' is set up
//compared to the grid, x and y are swapped
//rows[y][x] is the TextCell at point (x,y)
reduce() max = 0, row = rows[0] //[y][x] //comparing to model start @ 1
return Math.max( 0, rows[0][3].minWidth()); //Max(0, TextCell(4,1))
return Math.max(priorResult, rows[1][3].minWidth()); //Max(0, TextCell(4,2))
return Math.max(priorResult, rows[2][3].minWidth()); //Max(0, TextCell(4,3))
return Math.max(priorResult, rows[3][3].minWidth()); //Max(0, TextCell(4,4))
return Math.max(priorResult, rows[4][3].minWidth()); //Max(0, TextCell(4,5))
//result[3] is now full, and contains the min
5th and last time map does what it does
map(row[0][4]), column = 4, to the result of reduce()
reduce() max = 0, row = rows[0] //[y][x] //comparing to model start @ 1
return Math.max( 0, rows[0][4].minWidth()); //Max(0, TextCell(5,1))
return Math.max(priorResult, rows[1][4].minWidth()); //Max(0, TextCell(5,2))
return Math.max(priorResult, rows[2][4].minWidth()); //Max(0, TextCell(5,3))
return Math.max(priorResult, rows[3][4].minWidth()); //Max(0, TextCell(5,4))
return Math.max(priorResult, rows[4][4].minWidth()); //Max(0, TextCell(5,5))
//result[3] is now full, and contains the min
*/
}, 0);
});
}
So basically, this uses the first row as an anchor, and reduces each column based on
using the index into that row (i) as a fixed index into each row.
i.e.
row[0] is mapped using 0-4 as keys
0: is mapped to a reduction of rows[all][0] based on which has the maximum minwidth
1: is mapped to a reduction of rows[all][1] based on max minwidth
2: mapped to reduction of rows[all][2]
etc
The whole "reduce" thing is like basically doing this
var result = [];
//this part is analogous to the outer Map loop
//X is equal to column number, etc
for(var x = 0; x < rows[0].length; x++){
result[x] = 0; //the default for Max as the second parameter to reduce()
//This part is analogous to the inner reduce loop
//Stay at a fixed X value and loop through each item in that column
//until you find the largest minimum width needed to draw that column
for(var y = 0; y < rows.length; y++ ){
if(rows[y][x].minWidth() > result[x]) //get the maximum minWidth
result[x] = rows[y][x].minWidth();
}
}
I hope this all makes sense?
It was kinda harder to explain than I had imagined, but hopefully it made sense.
I was a bit rambly, but if you look at and read everything I wrote
things should eventually click.
:)
*/
function colWidths(rows) {
return rows[0].map(function(_, i) {
return rows.reduce(function(max, row) {
return Math.max(max, row[i].minWidth());
}, 0);
});
}->
No comments:
Post a Comment