See the Pen Responsive Pivot Table Prototype by Dudley Storey (@dudleystorey) on CodePen.

Recently my article on creating elegant table data with OpenType gained some attention via a much-appreciated tweet from Smashing Magazine. In response to that, a commenter quite correctly pointed out that the table was not responsive: as the browser window narrowed, data was cut off.

I couldn’t change the table as I have in previous examples: the order and direction of the data matters, and must be retained. However, there is another possible solution.

Microsoft Excel and other spreadsheet applications have a feature called “pivot tables”, which translate rows into columns. For certain kinds of tables, such as the one shown at the top of this article, the same effect can be achieved on web pages using JavaScript.

Starting with Markup

The table starts with similar markup to the OpenType example, but accessorized with an id and class (optional closing tags have been removed to simplify and clarify the code).

<table id="gg" class="large">
    <caption>2014 Greenhouse gas emissions, per country</caption>
    <thead>
        <tr>
            <td>&nbsp;
            <th scope="col">China
            <th scope="col"><abbr title="United States of America">USA</abbr>
            <th scope="col"><abbr title="European Union">EU</abbr>
            <th scope="col">India
            <th scope="col">Russia
    </thead>
    <tbody>
        <tr>
            <th scope="row">CO<sub>2</sub> (mt)
            <td>10,540
            <td>5,334
            <td>3,415
            <td>2,341
            <td>1,766
        <tr>
            <th scope="row">Emission per capita (t)
            <td>7.6
            <td>16.5
            <td>6.7
            <td>1.8
            <td>12.4
    </tbody>
</table>

The CSS is similar too. I’ve written this example in Sass:

table {
    margin: 2rem auto;
    font-size: 1.6rem;
    border-collapse: collapse;
    @media all and (max-width: 636px) {
        font-size: 1.4rem; 
    }
@media all and (max-width: 522px) {
        font-size: 1.2rem; 
    }
table td {
    text-align: right;
    padding: .5rem;
    width: 5rem;
}
table thead th {
    border-bottom: 1px solid #777;
    font-weight: 400;
}
table tbody th {
    font-weight: 400;
    text-align: right;
    padding-right: 1rem;
}
table caption {
  font-weight: 600;
  margin-bottom: 1rem;
}
table tbody td {
  color: #444;
}

Pivotting

The current table structure is read JavaScript as a variable called fulltable; without JavaScript, the table will still be shown; it just won’t pivot.

var fullTable = document.querySelector(".large").innerHTML,
caption = gg.getElementsByTagName("caption"),
ggHead = gg.querySelector("thead tr"),
ggBody = gg.querySelector("tbody"),
ggColHeaders = ggHead.querySelectorAll("th"),
ggDataHeaders = ggBody.querySelectorAll("tr th"),
ggDataRows = ggBody.querySelectorAll("tr"),
matchPoint = window.matchMedia("(max-width: 600px)"),
slimTable;

Each variable contains a part of the table.

function pivotTable() {
  if (matchPoint.matches && gg.matches(".large")) {
  if (typeof slimTable === 'undefined') {
    ggHead.innerHTML = "<td>&nbsp;</td>";
    for (var i = 0; i < ggDataHeaders.length; i++) {
      var scoped = ggDataHeaders[i];
      scoped.scope = "col";
      ggHead.appendChild(scoped);
    }

    var totalCols = ggDataRows[0].getElementsByTagName("td").length;
    ggBody.innerHTML = "";       

    for (var j = 0; j < totalCols; j++) { 
      var newRow = document.createElement("tr");
      var newRowHeader = document.createElement("th");
      newRowHeader.scope = "row";
      newRowHeader.innerHTML = ggColHeaders[j].innerText;
      newRow.appendChild(newRowHeader);
      for (var k = 0; k < ggDataRows.length; k++) { 
        var currentRowData = ggDataRows[k].getElementsByTagName("td");
        var newCell = document.createElement("td");
        newCell.innerHTML = currentRowData[j].innerText;
        newRow.appendChild(newCell);
      }
      ggBody.appendChild(newRow);
    }
  } else {
    gg.innerHTML = slimTable;
  }
    gg.classList.remove("large");
    gg.classList.add("small");
    slimTable = document.querySelector(".small").innerHTML;
  } 
  if (gg.matches(".small") && !matchPoint.matches) {
    console.log("Upsize");
    gg.innerHTML = fullTable;
    gg.classList.remove("small");
    gg.classList.add("large");
  } 
}

The pivotTable function swaps rows into columns, placing the header cells that were originally on the left of each row at the top of each column. Semantically (and from an accessibility perspective) the data remains the same, just presented in a different view.

When the data is pivoted, the table class is switched from large to small; the pivoted version of the table structure is recorded into the slimTable vaaible so that the pivot process doesn’t have to repeat itself if the viewport is made large and small again.

The pivotTable function is called once on page load:

pivotTable();

We don’t want to have to check if we need to call the pivotTable function every time the browser viewport is resized - that would drag down performance of the page - so I’ve throttled it to check every 200 milliseconds during a resize event:

window.addEventListener("resize", throttle( pivotTable, 200 )); 

That throttle process is a function in itself:

function throttle (callback, limit) {
  var wait = false; 
  return function () {
    if (!wait) {  
      callback.call(); 
      wait = true; 
      setTimeout(function () { 
        wait = false;   
        }, limit);
     }
   }
}

Conclusion

While not suitable for every table - those with just as many rows as there are columns would be an unsuitable candidate for this technique - but it could be very useful for tables like those above.

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/ZBPraK