This is my study of conditional formatting for HTML table cells without jQuery. After looking for a simple visual solution to table cell conditioning with vanilla JavaScript failed, I decided to create my own. While this will forever be a draft, I hope it can be the seed for a community initiative to bring better data representation to HTML tables.
While working for a financial company, I spent a great deal of time staring at HTML tables filled with numbers. There was no way around this, as the main product for this company revolved around comparative performance of their products against its benchmark – all represented in numbers. However, being the visual person that I am, I often found it hard to compare hard numbers against each other. I needed to have the comparison visually represented, ideally through bold colors.
After spending some time searching for a simple solution to conditional formatting of table cells, I found that all of the solutions I came across, though simple, relied heavily upon jQuery. Furthermore, the solutions were not very flexible and required multiple instantiations when the same code could be reused. This led me to explore my own solution to this problem without relying on jQuery.
My first draft of this code relied much too heavily on if statements. It required me to take a step back and look at what the core functionality required. After some thought, I realized that I needed to rely more on scalable parsers. My ultimate goal is to provide a fully customizable solution to conditional formatting of HTML tables; the current solution is a step in that direction.
| Data Group | Sorted Numbers | Numbers with Ties | Some Numbers, Some Not |
|---|---|---|---|
| Set 1 | -6.73 | -7.82 | -12.16 |
| Set 1 | 7.22 | -7.82 | text |
| Set 2 | 99.89 | -4.75 | 8.33 |
| Set 2 | 85.88 | -4.75 | 4.79 |
| Set 2 | 63.24 | -4.75 | text |
| Set 2 | 9.12 | -4.75 | 2.30 |
| Set 2 | -8.22 | -4.75 | text |
| Set 3 | -6.59 | -10.92 | 9.32 |
| Set 3 | -5.67 | -10.92 | -2.99 |
| Set 3 | -4.68 | 5.06 | text |
| Set 3 | -3.87 | 5.00 | 4.21 |
| Set 3 | 2.22 | 5.06 | -3.89 |
| Set 3 | 5.22 | -4.38 | text |
| Set 3 | 9.323 | 5.00 | 100.241 |
| Set 3 | 11.54 | 5.06 | 0.11 |
| Set 3 | 14.229 | -4.38 | 4.5 |
One of my goals was to modify the HTML as little as possible. To achieve this, I used the HTML5 ‘data’ attribute. This attribute allows the flexibility of grouping the elements while also not adding too much weight or obscure classes and identifiers. The developed component uses just one ‘data’ attribute with multiple associations to group together sets of comparable data. This allows the JavaScript to focus on core functionality and not worry about grouping together the rows of a table.
<table cellpadding="0" cellspacing="0">
<thead>
<tr>
<th>Data Group</th>
<th>Sorted Numbers</th>
<th>Numbers with Ties</th>
<th>Some Numbers,<br >Some Not</th>
</tr>
</thead>
<tbody>
<tr data-compare-group="group1">
<td>Set 1</td>
<td class="center">-6.73</td>
<td class="center">-7.82</td>
<td class="center">-12.16</td>
</tr>
<tr data-compare-group="group1">
<td>Set 1</td>
<td class="center">7.22</td>
<td class="center">-7.82</td>
<td class="center">text</td>
</tr>
<tr data-compare-group="group2">
<td>Set 2</td>
<td class="center">99.89</td>
<td class="center">-4.75</td>
<td class="center">8.33</td>
</tr>
<tr data-compare-group="group2">
<td>Set 2</td>
<td class="center">85.88</td>
<td class="center">-4.75</td>
<td class="center">4.79</td>
</tr>
<tr data-compare-group="group2">
<td>Set 2</td>
<td class="center">63.24</td>
<td class="center">-4.75</td>
<td class="center">text</td>
</tr>
<tr data-compare-group="group2">
<td>Set 2</td>
<td class="center">9.12</td>
<td class="center">-4.75</td>
<td class="center">2.30</td>
</tr>
<tr data-compare-group="group2">
<td>Set 2</td>
<td class="center">-8.22</td>
<td class="center">-4.75</td>
<td class="center">text</td>
</tr>
<tr data-compare-group="group3">
<td>Set 3</td>
<td class="center">-6.59</td>
<td class="center">-10.92</td>
<td class="center">9.32</td>
</tr>
<tr data-compare-group="group3">
<td>Set 3</td>
<td class="center">-5.67</td>
<td class="center">-10.92</td>
<td class="center">-2.99</td>
</tr>
<tr data-compare-group="group3">
<td>Set 3</td>
<td class="center">-4.68</td>
<td class="center">5.06</td>
<td class="center">text</td>
</tr>
<tr data-compare-group="group3">
<td>Set 3</td>
<td class="center">-3.87</td>
<td class="center">5.00</td>
<td class="center">4.21</td>
</tr>
<tr data-compare-group="group3">
<td>Set 3</td>
<td class="center">2.22</td>
<td class="center">5.06</td>
<td class="center">-3.89</td>
</tr>
<tr data-compare-group="group3">
<td>Set 3</td>
<td class="center">5.22</td>
<td class="center">-4.38</td>
<td class="center">text</td>
</tr>
<tr data-compare-group="group3">
<td>Set 3</td>
<td class="center">9.323</td>
<td class="center">5.00</td>
<td class="center">100.241</td>
</tr>
<tr data-compare-group="group3">
<td>Set 3</td>
<td class="center">11.54</td>
<td class="center">5.06</td>
<td class="center">0.11</td>
</tr>
<tr data-compare-group="group3">
<td>Set 3</td>
<td class="center">14.229</td>
<td class="center">-4.38</td>
<td class="center">4.5</td>
</tr>
</tbody>
</table>
While the CSS for the output above appears simple, the challenge here was determining how the JavaScript would use CSS to implement the changes. For certain results where blending of colors was needed and the number of rows was unknown, creating classes for each table cell would not be possible. With cell color blending as my basis for this project, I did not originally develop this to handle custom classes for certain attributes. This is an area where I hope the community can add value.
body table {
width: 100%;
}
body table tr td,
body table tr th {
padding: 8px;
text-align: center;
border-right: 1px solid #aaaaaa;
}
body table tr td.left,
body table tr th.left {
text-align: left;
}
body table tr td.right,
body table tr th.right {
text-align: right;
}
body table tr td:last-child,
body table tr th:last-child {
border-right: none;
}
body table tr td:first-child,
body table tr th:first-child {
text-align: left;
}
body table tr th {
font-weight: bold;
border-bottom: 3px solid #444444;
}
body table tr td {
border-bottom: 1px solid #aaaaaa;
}
body table tr:last-child td,
body table tr.last-group td {
border-bottom: 3px solid #444444;
}
The JavaScript is the backbone of the whole component. My desired pattern was to mimic a typical jQuery plugin, without the jQuery. This required some workarounds, although the final result is a lighter weight than including the jQuery library in its entirety.
I began with creating the default settings for the component. This helped to create a foundation from which I could build functions without worrying about variables changing or being overwritten. Once the core functions were written, I started pulling them apart to create functions that were reused within each of the more general functions to create a simpler appearance and more modularity. Lastly, I focused on customizing the experience for the user, with jQuery in mind. This required me to define certain features directly in the object passed through the function declaration. The challenge was to then make sure these customizations were passed to the settings and, more importantly, were overwriting the defaults.
;function compareTableData(options){
"use strict";
/*
Default settings and variables, can be overriden by user
*/
var defaults = {
// The HTML5 data attribute which groups the elements
attribute : 'data-compare-group',
// The default cell indexes for which to compare, 0 is the first cell
cellIndex : [0],
colors : {
low : 'rgb(255, 0, 0, 0.5)', // Any RGBA or HEX color
high : 'rgb(0, 128, 0, 0.5)' // Any RGBA or HEX color
},
// A class added to each cell for easy removal of css styles
comparedClass : 'compared',
// The default parser uses numeric
parser : function(data) {
var arr = [];
for (var i = 0; i < data.length; i++) {
if (Number(data[i])) {
arr.push(data[i])
}
};
function sorter(a,b) {
return Number(a) > Number(b) ? true : false;
}
return arr.sort(sorter);
},
debug : false
}
/*
Commonly used functions
*/
var utilities = {
getGroupRows : function(group){
if (!group) return document.querySelectorAll('table tbody tr[' + settings.attribute + ']')
else return document.querySelectorAll('table tbody tr[' + settings.attribute + '=' + group + ']')
},
getGroups : function(){
var arr = [],
tableRows = this.getGroupRows();
for (var i = 0; i < tableRows.length; i++) {
if (arr.indexOf(tableRows[i].getAttribute(settings.attribute)) == -1 ) {
arr.push(tableRows[i].getAttribute(settings.attribute))
}
};
return arr;
},
colorStyles : function(percentage){
var colorLow = settings.colors.low,
colorHigh = settings.colors.high;
/*
All credits for the below function go to Pimp Trizkit of Stack Overflow for spending years writing a function to mathematically blend HEX and RGBA colors
http://stackoverflow.com/users/693927/pimp-trizkit
*/
function shadeBlendConvert(p, from, to) {
if(typeof(p)!="number"||p<-1||p>1||typeof(from)!="string"||(from[0]!='r'&&from[0]!='#')||(typeof(to)!="string"&&typeof(to)!="undefined"))return null; //ErrorCheck
var sbcRip=function(d){
var l=d.length,RGB=new Object();
if(l>9){
d=d.split(",");
if(d.length<3||d.length>4)return null;//ErrorCheck
RGB[0]=i(d[0].slice(4)),RGB[1]=i(d[1]),RGB[2]=i(d[2]),RGB[3]=d[3]?parseFloat(d[3]):-1;
}else{
switch(l){case 8:case 6:case 3:case 2:case 1:return null;} //ErrorCheck
if(l<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(l>4?d[4]+""+d[4]:""); //3 digit
d=i(d.slice(1),16),RGB[0]=d>>16&255,RGB[1]=d>>8&255,RGB[2]=d&255,RGB[3]=l==9||l==5?r(((d>>24&255)/255)*10000)/10000:-1;
}
return RGB;
},
i=parseInt,r=Math.round,h=from.length>9,h=typeof(to)=="string"?to.length>9?true:to=="c"?!h:false:h,b=p<0,p=b?p*-1:p,to=to&&to!="c"?to:b?"#000000":"#FFFFFF",f=sbcRip(from),t=sbcRip(to);
if(!f||!t)return null; //ErrorCheck
if(h)return "rgba("+r((t[0]-f[0])*p+f[0])+","+r((t[1]-f[1])*p+f[1])+","+r((t[2]-f[2])*p+f[2])+(f[3]<0&&t[3]<0?")":","+(f[3]>-1&&t[3]>-1?r(((t[3]-f[3])*p+f[3])*10000)/10000:t[3]<0?f[3]:t[3])+")");
else return "#"+(0x100000000+(f[3]>-1&&t[3]>-1?r(((t[3]-f[3])*p+f[3])*255):t[3]>-1?r(t[3]*255):f[3]>-1?r(f[3]*255):255)*0x1000000+r((t[0]-f[0])*p+f[0])*0x10000+r((t[1]-f[1])*p+f[1])*0x100+r((t[2]-f[2])*p+f[2])).toString(16).slice(f[3]>-1||t[3]>-1?1:3);
};
return shadeBlendConvert(percentage, colorLow, colorHigh)
},
removeDuplicates : function(arr) {
var temp = {};
for (var i = 0; i < arr.length; i++) {
temp[arr[i]] = true;
}
var r = [];
for (var k in temp) {
r.push(k);
}
return r;
}
}
/*
Merges the defaults object with the options object given by the user
*/
var extend = function(out) {
out = out || {};
for (var i = 1; i < arguments.length; i++) {
var obj = arguments[i];
if (!obj) continue;
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object') extend(out[key], obj[key]);
else out[key] = obj[key];
}
}
}
return out;
};
/*
Declares the settings which should be used by the program
*/
var settings = extend(defaults, options)
/*
Object used to compare each set of data
*/
var compareDataSet = {
// Defined with each new object creation
group : undefined,
// Defined with each new object creation
cell : undefined,
// Returns the rows within the defined group
groupRows : function(){
return utilities.getGroupRows(this.group)
},
// Returns the original cell data for the set to compare
cellData : function(){
var arr = [];
var rows = this.groupRows();
for (var i = 0; i < rows.length; i++) {
arr.push(rows[i].children[this.cell].innerHTML)
};
return arr;
},
// Returns the cleaned cell data for the set to compare and makes sure it only has unique values
cellDataCleaned : function() {
return utilities.removeDuplicates(settings.parser(this.cellData()))
},
// Compares the original data to the sorted data to determine the sort order of the original data
compare : function(){
var origData = this.cellData(),
cleanData = this.cellDataCleaned(),
arr = [];
for (var i = 0; i < origData.length; i++) {
for (var j = 0; j < cleanData.length; j++) {
// The original data matches an instance in the new data
if (origData[i] == cleanData[j]) {
arr.push(j)
break;
}
// The original data does not match an instance in the new data because it was stripped out
else if (j+1 == cleanData.length){
arr.push(undefined)
}
};
};
return arr;
},
// Sets the corresponding color for each cell
getColor : function() {
var comparedData = this.compare(),
comparedLength = this.cellDataCleaned(),
arr = [];
for (var i = 0; i < comparedData.length; i++) {
var percentage;
if (!isNaN(comparedData[i])){
if (comparedLength.length > 1) {
percentage = comparedData[i]/(comparedLength.length-1)
}
else {
percentage = undefined;
}
}
else {
percentage = undefined;
}
arr.push(utilities.colorStyles(percentage))
};
return arr;
},
// Applies the color and classname to each cell
updateHTML : function(){
var coloredCells = this.getColor();
var rows = this.groupRows();
for (var i = 0; i < rows.length; i++) {
var cell = rows[i].children[this.cell]
cell.style.backgroundColor = coloredCells[i]
cell.className += " " + settings.comparedClass
};
}
}
/*
Base function that iterates through each of the compare sets
*/
function compare(){
var groups = utilities.getGroups(),
cells = settings.cellIndex;
if (settings.debug) console.log(settings); console.log(utilities); console.log(compareDataSet)
for (var i = 0; i < groups.length; i++) {
loopThroughGroup(cells)
};
function loopThroughGroup(cells){
for (var j = 0; j < cells.length; j++) {
var compareSet = Object.create(compareDataSet);
compareSet.group = groups[i];
compareSet.cell = cells[j];
compareSet.updateHTML();
if(settings.debug) {
console.log('Group = '+ groups[i] + '\n' +
'Cell Index = '+ cells[j] + '\n' +
'Group Cell Data = '+ compareSet.cellData() + '\n' +
'Group Cell Data Cleaned = '+ compareSet.cellDataCleaned() + '\n' +
'Group Cell Data Compare = '+ compareSet.compare() + '\n' +
'Group Cell Data Colors = '+ compareSet.getColor() + '\n'
);
}
};
}
}
/*
Initializes the module
*/
compare();
}
/*
Initializes the component
*/
compareTableData({
// The HTML5 data attribute which groups the elements
attribute : 'data-compare-group',
// Points to which columns to compare
cellIndex : [1,2,3],
colors : {
low : 'rgb(255, 0, 0, 0.7)', // Any RGBA or HEX color
high : 'rgb(0, 128, 0, 0.7)' // Any RGBA or HEX color
},
// A class added to each cell for easy removal of css styles
comparedClass : 'compared',
// Helps to fix those pesky bugs
debug : false
});
While this code is still a working draft, I do have some changes I would ultimately like to make to see this code be even further customizable.