D3 and CSS Grid with expanding content

Andy Barefoot
20 min readJun 11, 2017

--

Combining D3 and CSS to create a flexible content grid with expandable content and sorting. Spoiler alert: Completed project on Codepen.

It’s 2002 again

Back in the heady days of Netscape 4, and all the lovely extra edge-case coding it required, I was a HTML developer. And as I hand-coded my artisan HTML, using only the freshest natural ingredients, I loved nothing more than building a table based layout. I carefully constructed huge complex tables with cells for everything. Cells to hold images in place, cells to arrange and align text, cells spanning several columns, cells containing tables with even more cells. Cells. Cells. Cells. Delicious!

But then along came miserable kill-joys like Jeffrey Zeldman and Ethan Marcote with their precious web standards and suddenly the poor old table was only supposed to be used for tabulated data, not for layout. Yes, sure, I could put everything in divs and float them here, there and everywhere or even use masonry.js but it just wasn’t the same.

But now the table is back! Sure, we aren’t calling it a table. We are calling it CSS Grid. But I’m not fooled. There are things that look suspiciously like cells. There are rows and columns. We can make the cells span the rows and columns. It’s like a table, but exactly for layout. Oh benevolent Gods, we are not worthy!

So immediately I decided I must do something with this delicious new tool. I shall make a table! And I shall fill it with cells! And the cells will span columns! And rows! And whatever else they like! Yes, its 2002 again and just like those glorious Netscape 4 days there is even a significant proportion of browsers that won’t support my code!

A table for 2017

The idea is this:

  • Take some content/data that could be displayed in a grid.
  • Use D3.js and CSS Grid to create that grid.
  • Add some functionality to open/expand clicked content whilst maintaining grid.
  • Add some functionality to order content based on values.
  • Learn something along the way

Now, this is going to be quite a long read but I’ve broken it up into sections and at the end of each section is a link to all the code for the progress so far on Codepen. I also add the functionality incrementally, so if you just want to know how to create a basic grid layout from D3 you’ll be free to bail out with all the code you need pretty early on. If you want to hang around for more functionality like expanding content and D3 sorting then grab yourself a coffee and some energy sustaining biscuits before we start.

And if you just want to play around with the code here is a list of links to the Codepen examples at the various stages:

Grid of Thrones

First we need some content, and I just happen to have some that I have scraped from the excellent Game of Thrones Wiki. After a bit of cleaning up and manual enhancement I have the following information about the characters in the show (*not available for all characters):

  • Name
  • Photo
  • First appearance episode
  • Number of episodes
  • Allegiance*
  • Culture*
  • Religion*
  • Origin*

After a bit of manual fiddling with the “Allegiance” I managed to also define a suitable sigil or badge for each character. Putting all this into JSON I get something like this for each character:

{
"characters": [
{
"charName": "Arya Stark",
"charURL": "http:\/\/gameofthrones.wikia.com\/wiki\/Arya_Stark",
"charFirst": "1",
"charAllegiance": "House Stark, Faceless Men",
"charSigil": "Stark.jpg",
"charCulture": "Northmen",
"charReligion": "Old Gods of the Forest, Faith of the Seven, Many-Faced God",
"charOrigin": "Winterfell",
"totalAppearances": "47"
},...

The full JSON file is on GitHub.

Now we’re ready to start. We will display each character as an element in the grid. The element will display the name of the character and the sigil. The size of the element will represent the number of episodes the character has appeared in, and therefore roughly their importance.

On clicking a character the element will expand to a larger size and the additional content (photo, allegiance, religion, etc) will be displayed as well as a link to the full character page on the Game of Thrones Wiki.

Setting up the Code

Firstly I will include Mike Bostock’s (@mbostock) D3.js library which we will use to turn the character data into HTML elements.

<!-- HTML -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>

The only HTML we will define initially is the <body>.

<!-- HTML -->
<body></body>

And we’ll add some basic CSS.

/* CSS */
body {
background-color: #323232;
font-family: 'Trirong', georgia, times, serif;
font-weight: 200;
font-size: 18px;
}
body, a {
color: #808080;
}

The CSS uses a Google font we also need to include in the header

<!-- HTML -->
<link href="https://fonts.googleapis.com/css?family=Trirong:100,200" rel="stylesheet">

Reading in the Data

Now onwards it will mainly be JavaScript and CSS, starting with the JavaScript that uses D3 to read in the JSON.

// JAVASCRIPT
<script>
//Load in character data
d3.json("chars.json", function(data) {
/////////////////////////////////////////////
//////// Here we will put the code
//////// concerned with drawing the grid.
/////////////////////////////////////////////
})
</script>

Basic Grid

Once the data is loaded in we can generate the grid. Within the function defined above we first use D3 to append a div for the whole grid.

// JAVASCRIPT
grid = d3.select("body")
.append("div")
.attr("id", "grid")
.attr("class", "grid")
;

And we can set some CSS Grid parameters to the new grid:

/* CSS */
.grid {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(60px,1fr));
grid-auto-rows: 60px;
}

Firstly, this is defining that the div should be displayed as a grid and then sets some properties for the grid:

  • grid-gap: 10px; This defines the spacing between rows and columns. It can be set separately but we will have the same gap between columns as between rows.
  • grid-template-columns This defines the number and size of columns. Instead of defining each column individually repeat tells the grid to define multiple similar columns. Instead of setting an absolute number of repeated columns auto-fill defines that we want as many as will fit in the available space. Finally, instead of setting a fixed width for each column we will define minmax(60px, 1fr) which means that the minimum width of each column must be 60px but as the grid space gets larger they should all expand equally until there is enough space for another 60px column.
  • grid-auto-rows: 60px; As many rows as needed with a height of 60px each.

Now we have the grid we can add a div for each character. Putting these within the div we have already defined as a grid means these divs will be our div items. We add the char class for normal formatting.

// JAVASCRIPT
chars = grid
.selectAll("div")
.data(data.characters)
.enter()
.append("div")
.attr("class", "char")
;

And add some CSS to define the appearance of each div. None of this CSS is grid specific.

/* CSS */
.char {
border-radius: 3px;
padding: 0.5em;
background-color: #808080;
}

Already we have a simple grid with each character represented by a simple block:

Displaying Content

Next, we will start to make this useful by adding the name and sigil of each character to the blocks.

Firstly by attaching a background image to each div:

// JAVASCRIPT
chars
.style("background-image", function(d){
return 'url("images/sigil/'+d.charSigil+'")';
})
;

And then by adding a text element to hold the name.

// JAVASCRIPT
content = chars
.append("div")
.attr("class", "charContent")
;
content
.append("h2")
.text(function(d,i){
return d.charName;
})
;

Then we add some CSS to style these new elements:

/* CSS */
.char {
border-radius: 3px;
padding: 0.5em;
background-color: #808080;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
position: relative;
}
.charContent {
text-align: center;
position: absolute;
left: 0;
right: 0;
bottom: 0.5em;
margin: auto;
}
.char h2 {
display: inline-block;
background-color:rgba(28,28,28,1);
font-size: 60%;
line-height: auto;
color:rgba(210,210,210,1);
padding: 3px 8px 3px 8px;
}

Each character block now has the name displayed in front of that character’s sigil:

Varying grid item size

Next, we want to make the blocks for the more important characters larger. We create five new classes for the larger blocks, meaning there will be 6 sizes in total. (The five classes plus the 1x1 default currently applied to all blocks).

Here we use the CSS Grid to define the size of the blocks using the grid-column-end and grid-row-end values.

grid-column-end specifies at which column the grid item ends and can be an absolute value. However we want to define the size without defining the start or end columns. By using a value such as span x we define that the item should end 2 columns after it starts, i.e. have a width of x columns. We can use a similar syntax for grid-row-end to set a height of x rows. Therefore if we want our grid item to have a dimension of 3 columns wide and 2 columns high we would use

grid-column-end: span 3;
grid-row-end: span 2;

In this way we can define 5 new sizes for the 5 new classes. At the same time we are defining a larger font size for the character name.

/* CSS */
.size1 {
grid-column-end: span 2;
grid-row-end: span 1;
}
.size1 h2{
font-size: 80%;
}
.size2 {
grid-column-end: span 2;
grid-row-end: span 2;
}
.size2 h2{
font-size: 100%;
}
.size3 {
grid-column-end: span 3;
grid-row-end: span 2;
}
.size3 h2{
font-size: 120%;
}
.size4 {
grid-column-end: span 3;
grid-row-end: span 3;
}
.size4 h2{
font-size: 130%;
}
.size5 {
grid-column-end: span 4;
grid-row-end: span 4;
}
.size5 h2{
font-size: 180%;
}

Now using D3 we can add the appropriate class to each character depending on the totalAppearances value. I’ve decided that any character that appears in more than 1 episode should get the next largest size, then over 5 episodes for the next largest, over 10 for the next, over 30 for the next and over 50 for the largest size. I chose these thresholds after a bit of experimenting to see what looked right.

The code below is how I applied the different classes as the totalAppearances increases, I’d be interested to see a neater way of doing this.

// JAVASCRIPT
chars
.filter(function(d){ return d.totalAppearances > 1; })
.classed("size1", true)
.filter(function(d){ return d.totalAppearances > 5; })
.classed("size1", false)
.classed("size2", true)
.filter(function(d){ return d.totalAppearances > 10; })
.classed("size2", false)
.classed("size3", true)
.filter(function(d){ return d.totalAppearances > 30; })
.classed("size3", false)
.classed("size4", true)
.filter(function(d){ return d.totalAppearances > 50; })
.classed("size4", false)
.classed("size5", true)
;

We now have a grid with different size items for the characters:

Dense Grid Packing

At this stage there are gaps in the grid where the next item to be placed has been too large for the gap and therefore placed in the next row. This maintains the order of the grid items but in this case I want a neater looking grid. To do this we go back to the CSS for the grid and add a grid-auto-flow value.

/* CSS */
.grid {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(60px,1fr));
grid-auto-rows: 60px;
grid-auto-flow: dense;
}

This tells the grid to pack the items in as efficiently as possible, changing the order if necessary to fill gaps with smaller items from later in the grid.

The resulting grid looks like this:

Grid Interaction

So far everything is looking good and we have a nice grid. However I wanted to push it a bit more and make it interactive allowing the user to click a grid item and enlarge it to see more details of the character.

To enable the enlargement of the grid items we define a new class open that specifies a column span of 5 and a row span of 4.

/* CSS */
.open {
grid-column-end: span 5;
grid-row-end: span 4;
}

Now we’ll add an onclick action to the grid items. First it looks to see if the open class is already applied to this grid item. If it is it removes it. This means that clicking again on an already enlarged grid item will return it to its original size.
If the grid item isn’t already enlarged then it selects all grid items and ensures the openclass is not applied, this returns any other previously clicked items to their original size. It then applies the openclass to the clicked grid item.

// JAVASCRIPT
chars
.on("click", function(d, i) {
if(this.className.split(' ').indexOf('open') > -1 ){
d3.select(this).classed("open", false);
}else{
d3.selectAll(".char").classed("open", false);
d3.select(this).classed("open", true);
}
})
;

We now have the enlarge functionality in place. Clicking a grid item makes it bigger and the dense packing means the other grid items are rearranged around it:

More Content

Now we are going to add the new content to the existing grid items that will be displayed when the item is enlarged. To to do this we create a new div within our existing item .

// JAVASCRIPT
details = content
.append("div")
.attr("class", "details")
;

We will then add a new div to hold our character image. Within this div we will place the image.

// JAVASCRIPT
imageHolder = details
.filter(function(d){ return d.charThumb != ""; })
.append("div")
.attr("class", "imageHolder")
;
imageHolder
.append("img")
.attr("class","portrait")
.attr("data-src", function(d,i){
return "images/char"+d.charID+".png";
})
;

We are not setting the src of the image at this point because even though this content will not be displayed when the page is loaded it would still cause the page to download all the images. As we only want to display images as the user clicks on a character we have no need to download them all and will only download them as we need to display them.

Next we will add another div that will contain further details of the character. These will include the name of the character and a link to the Game of Thrones Wiki, but will also contain Allegiance, Culture, Origin and Religion. For each of these four additional values it is possible that nothing is specified for a character so before adding the field we check to ensure there is a value in the data.

// JAVASCRIPT
bio = details
.append("div")
.attr("class", "bio")
;
bio
.append("h3")
.text(function(d,i){
return d.charName;
})
;
bio
.filter(function(d){ return d.charAllegiance != ""; })
.append("h4")
.text("Allegiance:")
;
bio
.filter(function(d){ return d.charAllegiance != ""; })
.append("span")
.text(function(d,i){
return d.charAllegiance;
})
;
bio
.filter(function(d){ return d.charCulture != ""; })
.append("h4")
.text("Culture:")
;
bio
.filter(function(d){ return d.charCulture != ""; })
.append("span")
.text(function(d,i){
return d.charCulture;
})
;
bio
.filter(function(d){ return d.charOrigin != ""; })
.append("h4")
.text("Origin:")
;
bio
.filter(function(d){ return d.charOrigin != ""; })
.append("span")
.text(function(d,i){
return d.charOrigin;
})
;
bio
.filter(function(d){ return d.charReligion != ""; })
.append("h4")
.text("Religion:")
;
bio
.filter(function(d){ return d.charReligion != ""; })
.append("span")
.text(function(d,i){
return d.charReligion;
})
;
bio
.append("div")
.attr("class", "bioLink")
.append("a")
.attr("href", function(d){ return d.charURL })
.attr("target", "_blank")
.text("More Details >>")
;

We can now add a whole chunk of CSS to style these new elements. However within this CSS we will set display: none; for the whole details div meaning that for now all this content will be invisible to the user and our grid does not look any different.

/* CSS */
.char h3 {
font-size: 130%;
color:rgba(210,210,210,1);
margin-top: 2px;
margin-bottom: 6px;
}
.char h4 {
font-size: 80%;
color:rgba(202,173,45,1);
margin-top: 2px;
margin-bottom: 2px;
}
.details {
display: none;
line-height: 80%;
}
.details .bio{
font-size: 80%;
padding: 10px;
text-align: left;
}
.details span{
font-size: 100%;
color:rgba(210,210,210,1);
}
.portrait{
max-height: 100%;
margin: 0 -200%;
}
.bioLink{
margin-top: 20px;
}
.bioLink a{
color:rgba(202,173,45,1);
}
.imageHolder{
position: relative;
float: right;
height: 100%;
max-width: 150px;
overflow: hidden;
}

We also add CSS so that when the open class is applied to our grid item the details div is displayed. Also, we will hide the original character name.

/* CSS */
.open .details {
display: block;
height: 100%;
}
.open .charContent{
position: relative;
width: 100%;
height: 100%;
background-color:rgba(28,28,28,0.8);
}
.open .charContent h2{
display: none;
}

Now when the grid item is expanded the previously hidden content is displayed with the exception of the character photo. This is because previously we had specified data-src for the img rather than src to avoid pre-loading hundreds of images we may never display. Now we need to add code to the onclick to load the image.

// JAVASCRIPT
chars
.on("click", function(d, i) {
if(this.className.split(' ').indexOf('open') > -1 ){
d3.select(this).classed("open", false);
}else{
thisPortrait = this.getElementsByClassName("portrait")[0];
thisPortrait.setAttribute("src",thisPortrait.getAttribute("data-src"));
d3.selectAll(".char").classed("open", false);
d3.select(this).classed("open", true);
}
})
;

That’s everything we need to display the extra content. Clicking on a grid item enlarges the item and displays the detailed content:

Positioning Problem

So far everything is working pretty well but if you’ve clicked on a few characters you will have seen a problem, especially with the smaller grid items.

The images below show what happens when the user clicks on Gared in the top right corner.

Keep an eye on Gared in the top right corner.

When clicked the grid item expands as expected but moves lower down the grid. In some cases the item can move completely beyond the bottom of the view port which is extremely confusing for the user.

What is happening? If you look back at the grid layout when every item was the same size you can see that Gared is the 10th item in the grid. Even when we apply the different sizes it remains the 10th item in the grid. (Count from left to right based on the position of the top left corner of the item in respect to the top left corner of the grid).

However if you look at the image above after we apply dense packing the grid has moved Gared to the 6th position in the grid, earlier than its natural position, to fill a gap left by larger grid items.

Once enlarged Gared no longer fits in the small gap and so the dense packing moves the grid item back to its “natural” position. This means it appears after the first 9 items and also after some other small grid items that have been promoted to fill the gaps. Depending on how far up the page the small item had been “promoted” to start with the jump back down can be quite large and disorientating for the user.

If that was all a bit confusing I have made a codepen with the natural order number of each character shown next to their name. By resizing the view port and enlarging grid items you can get a feel for how the items are generally ordered to maintain the correct order before “promoting” later items to fill the gaps.

Locking Position

Ideally I would like the enlarged grid item to stay in the same position when enlarged and the other items to rearrange around it. But this isn’t straightforward and at this point things need to get a little bit hacky. I have a solution but if anyone knows of a more elegant approach I would be very keen to hear about it.

The issue is that when some grid items are enlarged the automatic positioning of the grid moves them from their previous position, in some cases a confusingly large distance. The plan is to override the automatic repositioning and define a fixed position for the enlarged item. The other items will be left to auto position around the enlarged item in order to keep our densely packed grid. We will try and fix the item in the same position it was when clicked.

However finding out the current position of the grid item isn’t straight forward. I was hoping to use window.getComputedStyle(this) which returns an object containing all the applied styles for an element. Sure enough there is a gridColumnStart and gridRowStart property in this object but unfortunately in our auto displayed grid the value for each is simply auto.

Instead the best I think we can do is to use this.getBoundingClientRect(this) which returns an object representing the bounding box of the element. From this we can get the x and y position of the grid item relative to the view port. Obviously this changes as the page is scrolled so we need to compare it to the same values for the full grid, which is the grid item’s parent.

This means the x and y positions of the grid item relative to the top left corner of the grid can be found like this:

// JAVASCRIPT
xPosInGrid = this.getBoundingClientRect().left - this.parentElement.getBoundingClientRect().left;
yPosInGrid = this.getBoundingClientRect().top - this.parentElement.getBoundingClientRect().top;

We now need to work out how many columns and rows these values represent, and for this we can use the applied styles of the grid using window.getComputedStyle(this.parentElement)

In particular we can return values representing the layout of columns and rows.

window.getComputedStyle(this.parentElement).gridTemplateColumns gives a value of 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px 60.7031px

We can see this is a string showing all the columns, in this case 14 columns, each of width 60.7031 pixels. We can convert the string to an array using split(" “) and therefore determine the number of columns from the length of this array.

We have set our grid up to have equal size columns so we can find the width of a column by using parseFloat on any value from the array.

We can also retrieve the column gap directly using window.getComputedStyle(this.parentElement.gridColumnGap .

We are now in a position to find the starting position of our grid item by dividing it’s x position by the width of a column plus the column gap.

This Column position = 1 + (X Position / (Column Width + Gap Width));
(We add 1 because column numbering starts at 1, not 0)

We can use exactly the same approach to find the starting row position.

We can now use these values to define a position for the selected grid item, meaning it will be held in place and will not be moved automatically to a new place in the grid.

// JAVASCRIPT
d3.select(this).style(“grid-row-start”, thisRow)
d3.select(this).style(“grid-column-start”, thisColumn)

The full code looks like this:

// JAVASCRIPT
chars
.on("click", function(d, i) {
if(this.className.split(' ').indexOf('open') > -1 ){
d3.select(this).classed("open", false);
}else{
gridColumns = window.getComputedStyle(this.parentElement).gridTemplateColumns.split(" ");
gridRows = window.getComputedStyle(this.parentElement).gridTemplateRows.split(" ");
numColumns = gridColumns.length;
numRows = gridRows.length;
xPosInGrid = this.getBoundingClientRect().left - this.parentElement.getBoundingClientRect().left;
yPosInGrid = this.getBoundingClientRect().top - this.parentElement.getBoundingClientRect().top;
gridRowHeight = parseFloat(gridRows[0]) + parseFloat(window.getComputedStyle(this.parentElement).gridRowGap);
gridColumnWidth = parseFloat(gridColumns[0]) + parseFloat(window.getComputedStyle(this.parentElement).gridColumnGap);
thisRow = Math.round(yPosInGrid/gridRowHeight) +1;
thisColumn = Math.round(xPosInGrid/gridColumnWidth) +1;
thisPortrait = this.getElementsByClassName("portrait")[0];
if(thisPortrait)thisPortrait.setAttribute("src",thisPortrait.getAttribute("data-src"));
d3.selectAll(".char").classed("open", false);
d3.selectAll(".char").style("grid-row-start", "auto");
d3.selectAll(".char").style("grid-column-start", "auto");
d3.select(this).classed("open", true);
d3.select(this).style("grid-row-start", thisRow)
d3.select(this).style("grid-column-start", thisColumn)
}
})
;

You can see that before setting the row and column start positions of the selected item to a specific number we return the positions for all items back to auto to reset any previously selected items back into the grid.

So now we have ensured that our selected items stay in the same place when they are extended:

But we still have a problem. If that position is too close to the right edge of the grid the expanded item doesn’t fit. In the image below Gared (at the far top right) has been selected but there isn’t room to show the full expanded item and the grid is distorted.

The solution for this is simple. Instead of constraining the extended grid item to exactly the same grid position when it expands, instead we will check to see if it is too close to the right edge and move it to the left so that it does fit.

We can return the new width and height of the expanded grid item (after the open class has been applied) from the gridColumnEnd and gridRowEnd property of getComputedStyle .

As we have previously calculated the total number of row and columns in the grid the calculation to adjust the position if its too close to the edge is straightforward.

However it is important to place this code after applying the open class to the selected grid item (we need to return the new, bigger item width and height) but before setting the new column and row starts.

// JAVASCRIPT
d3.select(this).classed("open", true);
divWidth = parseFloat(window.getComputedStyle(this).gridColumnEnd.split(" ")[1]);
divHeight = parseFloat(window.getComputedStyle(this).gridRowEnd.split(" ")[1]);
if(thisRow+divHeight>numRows)thisRow = 1 + numRows-divHeight;
if(thisColumn+divWidth>numColumns)thisColumn = 1 + numColumns-divWidth;

d3.select(this).style("grid-row-start", thisRow)
d3.select(this).style("grid-column-start", thisColumn)

So that was the section that included the ugliest code, and I’d be interested in suggestions on a cleaner approach but we have solved the issue of grid items leaping around the grid when their size changes:

Reordering the grid

We now have what we set out to create, a good looking table of elements than can be extended to show more content and our work with the CSS Grid framework is complete. However as we are using D3 to place our elements we do have the opportunity to easily add some basic sorting functionality.

Our sorting will rearrange the order of our grid items in the grid. But it is important to remember that this won’t give a strict reordering. As discussed earlier, the dense packing we are using means the items are only shown in their order as far as the packing allows. Items later in the order are shown earlier in the grid when they are needed to fill small spaces left between larger items. However as the purpose of our grid is an attractive display and not to accurately display information I think this kind of rough sorting is acceptable.

First we’ll add some links to the page with our reordering options. We will be able to sort on the number of appearances a character has made, a character’s allegiance, their first appearance (also the default ordering) and alphabetically by name.

We haven’t added any href to the links, we will attach onclick functions to them later based on their ids.

<!-- HTML -->
<div id="filters">
Order by:
<a id="orderAppearances" >No. of Appearances</a> |
<a id="orderSigil" >Allegiance</a> |
<a id="orderFirst" >First Appearance</a> |
<a id="orderName" >Name</a>
</div>

Now we’ll add functions to each of the links to reorder our data. The functions are slightly different depending on whether we are sorting an integer (number of appearances and first appearance) or a string (allegiance and name) and whether we wish the order to be ascending (first appearance) or descending (number of appearances).

To each function we will also add the code to ‘close’ any expanded grid items by removing the open class and resetting the grid start positions to auto .

/// JAVASCRIPT
d3
.select("#orderAppearances")
.on("click", function () {
chars.sort(function(a, b) {
return b["totalAppearances"] - a["totalAppearances"];
});
chars.classed("open", false);
chars.style("grid-row-start", "auto");
chars.style("grid-column-start", "auto");
})
;

d3
.select("#orderSigil")
.on("click", function () {
chars.sort(function(a, b) {
return d3.ascending(b["charSigil"], a["charSigil"]);
});
chars.classed("open", false);
chars.style("grid-row-start", "auto");
chars.style("grid-column-start", "auto");
})
;

d3
.select("#orderFirst")
.on("click", function () {
chars.sort(function(a, b) {
return a["charFirst"] - b["charFirst"];
});
chars.classed("open", false);
chars.style("grid-row-start", "auto");
chars.style("grid-column-start", "auto");
})
;

d3
.select("#orderName")
.on("click", function () {
chars.sort(function(a, b) {
return d3.ascending(a["charName"], b["charName"]);
});
chars.classed("open", false);
chars.style("grid-row-start", "auto");
chars.style("grid-column-start", "auto");
})
;

At this point we have our reordering functionality:

As mentioned above the ordering won’t be strict due to our dense packing and how important this is depends on the value we are sorting on.

When sorting on character name it looks pretty good. The smaller items are dispersed throughout the order and so there are no obviously mis-ordered elements.

Alphabetic sorting on character name.

Ordering by first appearance also works well. This has the advantage of several characters all having the same value, especially at the start with many characters making their first appearance in episode 1. It also has the advantage that the first appearance isn’t displayed so the user won’t notice a mis-ordering.

Ordered by first appearance.

Ordering by allegiance is the most visually obvious to the user. The allegiance corresponds to the background image of each item so the ordering is clear. There is some mis-ordering where small gas are backfilled and this happens more noticeably when there are only a few items sharing a particular allegiance.

Ordered by allegiance

The ordering by number of appearances is the least successful. As the sizes of the items are defined by the number of appearances this is effectively ordering by item size. This will mean that any small gaps left at the top of the grid are going to be filled by items that should be at the bottom of the sorting creating an obvious and jarring mis-sort. I would argue that this sorting is not a successful way of displaying the content and as the number of appearances is obviously represented by the size of the item anyway I would suggest that sorting by appearances is an unnecessary functionality.

Finished

That is our grid complete. Hopefully it has recaptured for you all the heady excitement of those early 21st century table based layouts but with the additional zesty tang of CSS and D3.

Please let me know if any of it was incomprehensible or if you disagree with any of the approaches or coding in the examples.

--

--