Okay, I have a table. In this table I have a whole bunch of columns, and I would like to use a Sortable Tables javascript so that the user can sort the table as they wish. There are many such JS scripts available (ie: http://tablesorter.com/docs/)
However, the problem I have is that for each row of my table that I want sorted, there is a colspan="4" row right below it that I dont want sorted. In fact, I want these rows linked directly to the row above them so that when those rows get sorted, the 4-span row below it sticks with it.
Is something like this possible?
table {
border: 1px black solid;
width: 100%;
}
thead {
background-color: lightgrey;
text-align: left;
}
.notes {
text-align: right;
}
<table>
<thead>
<tr>
<th>Command</th>
<th>DMG</th>
<th>EXE</th>
<th>TOT</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jouho Touken</td>
<td>19</td>
<td>17</td>
<td>42</td>
</tr>
<tr>
<td colspan="4" class="notes">Opponent crouching (H: Stagger)</td>
</tr>
<tr>
<td>Chouyoushu</td>
<td>25</td>
<td>18</td>
<td>46</td>
</tr>
<tr>
<td colspan="4" class="notes">Damage varies due to distance (25-40)</td>
</tr>
<tr>
<td>Tetsuzankou</td>
<td>40</td>
<td>19</td>
<td>75</td>
</tr>
<tr>
<td colspan="4" class="notes">Super Replay; Damage varies due to distance: 40-80</td>
</tr>
</tbody>
</table>
Here is an example of how you could do this.
Make an array of all rows in the <tbody>.
Group it into pairs. [{data, note}, ...]
Sort by a given sorting function
Flatten back into an array of table rows.
empty the <tbody> tag
Insert into the <tbody> tag everything in the sorted table rows array.
var tableBody = document.querySelector('tbody')
var tableRows = Array
.from(document.querySelectorAll('tbody > tr'))
var notesAndData = []
/* Group elements into
[
{data: <tr>, note: <tr>},
...
]
*/
for(var i = 1; i < tableRows.length; i += 2) {
notesAndData.push({
data: tableRows[i-1],
note: tableRows[i]
})
}
function flatten(arr) {
return arr.reduce(function(acc, curr) {
acc.push(curr.data)
acc.push(curr.note)
return acc
}, [])
}
function repopulateTable(arr) {
tableBody.innerHTML = ''
arr.forEach(function(element) {
tableBody.appendChild(element)
})
}
function sortTable(sortingFunc) {
/* Spread the notesAndData into a new array in
order to not modify it. This syntax is es6 */
var sorted = [...notesAndData].sort(sortingFunc)
repopulateTable(flatten(sorted))
}
function sortByDmg(ascending) {
return function(a, b) {
var aDmg = parseInt(a.data.children[1].textContent)
var bDmg = parseInt(b.data.children[1].textContent)
if (aDmg < bDmg) return ascending ? 1 : -1
return ascending ? 1 : -1
}
}
document
.querySelector('.dmgSort')
.addEventListener('click', function() {
sortTable(sortByDmg(true))
})
table {
border: 1px black solid;
width: 100%;
}
thead {
background-color: lightgrey;
text-align: left;
}
.notes {
text-align: right;
}
<button class="dmgSort">Sort By DMG Ascending</button>
<table>
<thead>
<tr>
<th>Command</th>
<th>DMG</th>
<th>EXE</th>
<th>TOT</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jouho Touken</td>
<td>19</td>
<td>17</td>
<td>42</td>
</tr>
<tr>
<td colspan="4" class="notes">Opponent crouching (H: Stagger)</td>
</tr>
<tr>
<td>Chouyoushu</td>
<td>25</td>
<td>18</td>
<td>46</td>
</tr>
<tr>
<td colspan="4" class="notes">Damage varies due to distance (25-40)</td>
</tr>
<tr>
<td>Tetsuzankou</td>
<td>40</td>
<td>19</td>
<td>75</td>
</tr>
<tr>
<td colspan="4" class="notes">Super Replay; Damage varies due to distance: 40-80</td>
</tr>
</tbody>
</table>
Related
I have a table like this
In which I have different city where we have demand-supply of different products.
Now what I want as here demand is different for all the products However supply is the same on all of three product, so I want that table looks like in this manner.
What I want to do is I want only to show the supply column once in the last of the table. This has to be done dynamically as in the future we have multiple products
Can anyone help me with this?
What the code below does is:
Identify the positions of the "Supply"'s and store them in ind array, in this case will be [3, 5, 7]
Loops through ind except for the last element 7(as one "Supply" will be left) and hide all td's; $("td:nth-child("3"), $("td:nth-child("5")
The "Demand"s that precede each of these elements will be assigned two spaces.
let ind = [];
$("td:contains('Supply')").each(function (index) {
ind.push($(this).index() + 1);
});
$(".hide").on("click", function () {
for (let i = 0; i < ind.length - 1; i++) {
let el = $("td:nth-child(" + ind[i] + ")");
el.prev().attr("colspan", "2");
el.hide();
}
});
table,
th,
td {
border: 1px solid black;
border-collapse: collapse;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<table>
<tr>
<th>City</th>
<th colspan="2">Product 1</th>
<th colspan="2">Product 2</th>
<th colspan="2">Product 3</th>
</tr>
<tr>
<td></td>
<td>Demand</td>
<td>Supply</td>
<td>Demand</td>
<td>Supply</td>
<td>Demand</td>
<td>Supply</td>
</tr>
<tr>
<td>City 1</td>
<td>50$</td>
<td>60$</td>
<td>90$</td>
<td>60$</td>
<td>100$</td>
<td>60$</td>
</tr>
<tr>
<td>City 2</td>
<td>50$</td>
<td>60$</td>
<td>90$</td>
<td>60$</td>
<td>100$</td>
<td>60$</td>
</tr>
<tr>
<td>City 3</td>
<td>50$</td>
<td>60$</td>
<td>90$</td>
<td>60$</td>
<td>100$</td>
<td>60$</td>
</tr>
<tr>
<td>City 4</td>
<td>50$</td>
<td>60$</td>
<td>90$</td>
<td>60$</td>
<td>100$</td>
<td>60$</td>
</tr>
</table>
<button class="hide">Hide</button>
I developed clicking on the parent node to display its child row. I just need to enable to click on the child data should open its sub child rows as recursive one or table tree. Could anyone add your logic which will help me to understand and help the same to others?
document.getElementById("products").addEventListener("click", function(e) {
if (e.target.tagName === "A") {
e.preventDefault();
var row = e.target.parentNode.parentNode;
while ((row = nextTr(row)) && !/\bparent\b/ .test(row.className))
toggle_it(row);
}
});
function nextTr(row) {
while ((row = row.nextSibling) && row.nodeType != 1);
return row;
}
function toggle_it(item){
if (/\bopen\b/.test(item.className))
item.className = item.className.replace(/\bopen\b/," ");
else
item.className += " open";
}
tbody tr {
display : none;
}
tr.parent {
display : table-row;
}
tr.open {
display : table-row;
}
<!--
Bootstrap docs: https://getbootstrap.com/docs
-->
<div class="container">
<table class="table" id="products">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Destination</th>
<th>Updated on</th>
</tr>
</thead>
<tbody>
<tr class="parent">
<td>+Oranges</td>
<td>100</td>
<td>On Store</td>
<td>22/10</td>
</tr>
<tr>
<td>121</td>
<td>120</td>
<td>City 1</td>
<td>22/10</td>
</tr>
<tr>
<td>212</td>
<td>140</td>
<td>City 2</td>
<td>22/10</td>
</tr>
<tr>
<td>212</td>
<td>140</td>
<td>City 2</td>
<td>22/10</td>
</tr>
<tr class="parent">
<td>+Apples</td>
<td>100</td>
<td>On Store</td>
<td>22/10</td>
</tr>
<tr>
<td>120</td>
<td>120</td>
<td>City 1</td>
<td>22/10</td>
</tr>
<tr>
<td>120</td>
<td>140</td>
<td>City 2</td>
<td>22/10</td>
</tr>
</tbody>
</table>
</div>
Updated answer
I've changed almost everything and simplified the code:
The toggle buttons are added automatically,
+ changes to - when the parent is opened,
The table, the classes for opened and visible elements, and the buttons are passed as parameters,
It could be used on multiple table,
…
I've created a repository with that code on GitHub:
https://github.com/TakitIsy/table-to-tree
/* ---- < MAIN FUNCTION > ---- */
function tableToTree(table_Selector, tr_OpenedClass, tr_VisibleClass, tr_ToggleButton) {
// Table elements variables
var table = document.querySelector(table_Selector);
var trs = document.querySelectorAll(table_Selector + " tr");
// Add the buttons above the table
var buttons = document.createElement('div');
buttons.innerHTML = '<button>[‒] All</button><button>[+] All</button>';
table.insertBefore(buttons, table.childNodes[0]);
buttons = buttons.querySelectorAll('button');
// Add the actions of these buttons
buttons[0].addEventListener("click", function() {
trs.forEach(function(elm) {
elm.classList.remove(tr_OpenedClass);
elm.classList.remove(tr_VisibleClass);
});
});
buttons[1].addEventListener("click", function() {
trs.forEach(function(elm) {
if (elm.innerHTML.includes(tr_ToggleButton))
elm.classList.add(tr_OpenedClass);
elm.classList.add(tr_VisibleClass);
});
});
// Next tr function
function nextTr(row) {
while ((row = row.nextSibling) && row.nodeType != 1);
return row;
}
// On creation, automatically add toggle buttons if the tr has childs elements
trs.forEach(function(tr, index) {
if (index < trs.length - 1) {
if (+tr.getAttribute("level") < +trs[index + 1].getAttribute("level")) {
var elm1 = tr.firstElementChild;
elm1.innerHTML = tr_ToggleButton + elm1.innerHTML;
}
}
});
// Use the buttons added by the function above
table.addEventListener("click", function(e) {
// Event management
if (!e) return;
if (e.target.outerHTML !== tr_ToggleButton) return;
e.preventDefault();
// Get the parent tr and its level
var row = e.target.closest("tr");
row.classList.toggle(tr_OpenedClass);
var lvl = +(row.getAttribute("level"));
// Loop to make childs visible/hidden
while ((row = nextTr(row)) && ((+(row.getAttribute("level")) == (lvl + 1)) || row.className.includes(tr_VisibleClass))) {
row.classList.remove(tr_OpenedClass);
row.classList.toggle(tr_VisibleClass);
}
});
}
/* ---- </ MAIN FUNCTION > ---- */
// Call the above main function to make the table tree-like
tableToTree('#myTable', 'opened', 'visible', '<span class="toggle"></span>');
tbody tr {
display: none;
}
tr[level="0"],
tr.visible {
display: table-row;
}
td {
background: #ccc;
padding: 4px 8px 4px 32px;
text-align: left;
}
tr[level="1"] td {
background: #ddd;
padding-left: 40px;
}
tr[level="2"] td {
background: #eee;
padding-left: 48px;
}
tr .toggle {
position: absolute;
left: 16px;
cursor: pointer;
}
.toggle::after {
content: "[+]";
}
.opened .toggle::after {
content: "[‒]";
}
<table id="myTable">
<tbody>
<tr level="0">
<td>Parent 1</td>
</tr>
<tr level="1">
<td>Match 1</td>
</tr>
<tr level="1">
<td>Match 2</td>
</tr>
<tr level="0">
<td>Parent 2</td>
</tr>
<tr level="1">
<td>Mismatch 1</td>
</tr>
<tr level="1">
<td>Mismatch 2</td>
</tr>
<tr level="2">
<td>Mismatch 2.1</td>
</tr>
</tbody>
</table>
<br>
⋅
⋅
⋅
Old answer
I played a little with your code…
Trying to use as many as possible of existing functions/methods to make the code cleaner and easier to read and understand.
… and ended-up with that snippet:
(See comments in my code for more details)
document.getElementById("products").addEventListener("click", function(e) {
// I think using the not equal is nicer here, just my opinion… Less indenting.
if (!e) return; // Exit if null
if (e.target.tagName !== "A") return; // Exit if not A element
e.preventDefault();
var row = e.target.closest("tr"); // Better than parent parent!
var cls = row.classList[0]; // Get the first class name (the only one in our html)
var lvl = +(cls.slice(-1)) + 1; // Unary operator +1 on the last character
cls = cls.slice(0, -1) + lvl; // Replace the last char with lvl to get the class to be toggled
// Check if the row is of the class to be displayed OR if the row is already opened
// (so that clicking close on parent also closes sub-childs)
while ((row = nextTr(row)) && (row.className.includes(cls) || row.className.includes("open")))
row.classList.toggle("open"); // Better than the function you created, it already exists!
});
function nextTr(row) {
while ((row = row.nextSibling) && row.nodeType != 1);
return row;
}
// Select all the tr childs elements (all levels except 0
var allChilds = document.querySelectorAll("tr[class^=level]:not(.level0)");
// Added the below for buttons after comments
document.getElementById("openAll").addEventListener("click", function() {
allChilds.forEach(function(elm){
elm.classList.add("open");
});
});
document.getElementById("closeAll").addEventListener("click", function() {
allChilds.forEach(function(elm){
elm.classList.remove("open");
});
});
tbody tr {
display: none;
}
/* Simplified */
tr.level0,
tr.open {
display: table-row;
}
/* Added colors for better visibility */
tr.level0 {
background: #ccc;
}
tr.level1 {
background: #ddd;
}
tr.level2 {
background: #eee;
}
/* Added some more styling after comment */
tr td {
padding: 0.2em 0.4em;
}
tr td:first-of-type {
position: relative;
padding: 0.2em 1em;
}
tr td a {
color: inherit;
/* No special color */
text-decoration: inherit;
/* No underline */
position: absolute;
left: 0.25em;
}
tr.level1 td:first-of-type {
padding-left: 1.5em;
}
tr.level2 td:first-of-type {
padding-left: 2em;
}
<button id="openAll">+ All</button>
<button id="closeAll">- All</button>
<table class="table" id="products">
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Destination</th>
<th>Updated on</th>
</tr>
</thead>
<tbody>
<tr class="level0">
<td>+Oranges</td>
<td>100</td>
<td>On Store</td>
<td>22/10</td>
</tr>
<tr class="level1">
<td>121</td>
<td>120</td>
<td>City 1</td>
<td>22/10</td>
</tr>
<tr class="level1">
<td>+212</td>
<td>140</td>
<td>City 2</td>
<td>22/10</td>
</tr>
<tr class="level2">
<td>212</td>
<td>140</td>
<td>City 2</td>
<td>22/10</td>
</tr>
<tr class="level2">
<td>212</td>
<td>140</td>
<td>City 2</td>
<td>22/10</td>
</tr>
<tr class="level0">
<td>+Apples</td>
<td>100</td>
<td>On Store</td>
<td>22/10</td>
</tr>
<tr class="level1">
<td>120</td>
<td>120</td>
<td>City 1</td>
<td>22/10</td>
</tr>
<tr class="level1">
<td>+120</td>
<td>140</td>
<td>City 2</td>
<td>22/10</td>
<tr class="level2">
<td>120</td>
<td>140</td>
<td>City 2</td>
<td>22/10</td>
</tr>
<tr class="level2">
<td>120</td>
<td>140</td>
<td>City 2</td>
<td>22/10</td>
</tr>
</tbody>
</table>
I hope it helps!
I'm trying to write an efficient javascript function that will loop through a table, grab all numbers, and ignore all tds with strings. The columns will be added and averaged, and rows will be appended for each.
I have the basic functionality for this working. Whereas, if the table does not include a string, the results are as expected. When the table does include a string, the total and average of the column are way off and I'm not exactly sure how the answer is being calculated. I'm hoping somebody can help me figure out a way to ignore these values all together, and create a more efficient way of writing this function.
Finally, I want to be able to call this function by simply passing in the table, e.g. buildRows(table);
Here's what I got so far:
// function to build total and average rows
function buildRow($element) {
var result = [];
$($element).find('tbody tr').each(function() {
// Ignore the first column reserved for labels
$('td:not(:first)', this).each(function(index, val) {
if (!result[index]) result[index] = 0;
result[index] += parseInt($(val).text());
});
});
// Get the total amount rows
var rowCount = $($element).find('tbody tr').length;
// Add Average Row
$($element).append('<tr class="avg-row"></tr>');
$($element).find('tr').last().append('<td>' + 'Averages' + '</td>');
$(result).each(function() {
$($element).find('tr').last().append('<td class="avg-td">' + this / rowCount + '</td>');
});
// Add Total Row
$($element).append('<tr class="total-row"></tr>');
$($element).find('tr').last().append('<td>' + 'Totals' + '</td>');
$(result).each(function() {
$($element).find('tr').last().append('<td class="total-td">' + this + '</td>');
});
}
// ideal function calls
var tableOne = $('.tableOne');
buildRow(tableOne);
var tableTwo = $('.tableTwo');
buildRow(tableTwo);
table {
border: 1px solid #333;
padding: 10px;
margin-right: 5px;
}
table tr:nth-child(odd) {
background-color: #e0e0e0;
}
table td {
padding: 10px;
border: 1px solid #555;
text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table class="tableOne">
<tr>
<td>Row One</td>
<td>45</td>
<td>23.356</td>
<td>88</td>
</tr>
<tr>
<td>Row Two</td>
<td>111440.568</td>
<td>115555</td>
<td>4.21598</td>
</tr>
<tr>
<td>Row Three</td>
<td>27</td>
<td>42</td>
<td>144487.11</td>
</tr>
<tr>
<td>Row Four</td>
<td>23.356</td>
<td>125%</td>
<td>778978523.36</td>
</tr>
</table>
<table class="tableTwo">
<tr>
<td>Row One</td>
<td>45</td>
<td>23.356</td>
<td>Hello</td>
</tr>
<tr>
<td>Row Two</td>
<td>111440.568</td>
<td>115555</td>
<td>4.21598</td>
</tr>
<tr>
<td>Row Three</td>
<td>Dog</td>
<td>true</td>
<td>144487.11</td>
</tr>
<tr>
<td>Row Four</td>
<td>23.356</td>
<td>125%</td>
<td>778978523.36</td>
</tr>
</table>
The first table with no strings seems okay, 2nd table the results are way off and I'm not sure how the totals are being calculated.
You cannot just do parseInt on every value, it sometimes transform a string to a weird number. You have to check if it is not a valid number before you sum it up. I think of a regex or something like that, untested.
var v = $(val).text();
if(v.match(/^[\s0-9\.]+$/)){}
You need to check whether td value has integer or not by using var reg = /^\d+$/;
Since your using jQuery why not just use jQuery's built in is numeric condition check function $.isNumeric which will parse out pretty much all the data you don't want.
// true (numeric)
$.isNumeric( "-10" )
$.isNumeric( "0" )
$.isNumeric( 0xFF )
$.isNumeric( "0xFF" )
$.isNumeric( "8e5" )
$.isNumeric( "3.1415" )
$.isNumeric( +10 )
$.isNumeric( 0144 )
// false (non-numeric)
$.isNumeric( "-0x42" )
$.isNumeric( "7.2acdgs" )
$.isNumeric( "" )
$.isNumeric( {} )
$.isNumeric( NaN )
$.isNumeric( null )
$.isNumeric( true )
$.isNumeric( Infinity )
$.isNumeric( undefined )
Source: https://api.jquery.com/jQuery.isNumeric/
Example: https://jsfiddle.net/1d9Lbnp1/1/
You could just ignore the Nans and use Number instead of parseInt if you want something more accurate:
$('td:not(:first)', this).each(function(index, val) {
if (!result[index]) result[index] = 0;
var num = Number($(val).text());
num = isNaN(num) ? 0 : num;
result[index] += num;
});
Two things to remember:
parseFloat will capture floats and integers
When working with numbers, you can use the ||0 to hack ignore the falsy values, replacing them with a 0
Other than that, I took some liberties with your code, putting the necessary elements in variables:
// function to build total and average rows
function buildRow($table) {
var result = [],
$tbody = $table.find('tbody'),
$rows = $tbody.find('tr'),
row_count = $rows.length;
$rows.each(function() {
// Ignore the first column reserved for labels
var $cells = $('td:not(:first)',this);
$cells.each(function(index,cell) {
if (!result[index])
result[index] = 0;
result[index] += parseFloat($(cell).text()||0);
});
});
// Add Average Row
var $avg_row = $('<tr class="avg-row"></tr>');
$avg_row.append('<td>Averages</td>');
$.each(result,function() {
$avg_row.append('<td class="avg-td">' + ( this / row_count ) + '</td>');
});
$tbody.append($avg_row);
// Add Total Row
var $total_row = $('<tr class="total-row"></tr>');
$total_row.append('<td>Totals</td>');
$.each(result,function() {
$total_row.append('<td class="total-td">' + this + '</td>');
});
$tbody.append($total_row);
}
// ideal function calls
var $tableOne = $('.tableOne');
buildRow($tableOne);
var $tableTwo = $('.tableTwo');
buildRow($tableTwo);
table {
border: 1px solid #333;
padding: 10px;
margin-right: 5px;
}
table tr:nth-child(odd) {
background-color: #e0e0e0;
}
table td {
padding: 10px;
border: 1px solid #555;
text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table class="tableOne">
<tr>
<td>Row One</td>
<td>45</td>
<td>23.356</td>
<td>88</td>
</tr>
<tr>
<td>Row Two</td>
<td>111440.568</td>
<td>115555</td>
<td>4.21598</td>
</tr>
<tr>
<td>Row Three</td>
<td>27</td>
<td>42</td>
<td>144487.11</td>
</tr>
<tr>
<td>Row Four</td>
<td>23.356</td>
<td>125%</td>
<td>778978523.36</td>
</tr>
</table>
<table class="tableTwo">
<tr>
<td>Row One</td>
<td>45</td>
<td>23.356</td>
<td>Hello</td>
</tr>
<tr>
<td>Row Two</td>
<td>111440.568</td>
<td>115555</td>
<td>4.21598</td>
</tr>
<tr>
<td>Row Three</td>
<td>Dog</td>
<td>true</td>
<td>144487.11</td>
</tr>
<tr>
<td>Row Four</td>
<td>23.356</td>
<td>125%</td>
<td>778978523.36</td>
</tr>
</table>
Here's another way to do the same thing, by retrieving the values first by column (using nth-child) and then using reduce to calculate sums:
['.tableOne', '.tableTwo'].forEach(function(tbl){
makeAggregates($(tbl));
});
function makeAggregates($tbl) {
var $tbody = $tbl.find('tbody');
var sums = sumOfColumns($tbody);
var $total_row = $('<tr class="total-row"><td class="total-td">Total</td></tr>');
var $average_row = $('<tr class="avg-row"><td class="avg-td">Averages</td></tr>');
$.each(sums, function(key,col) {
var total = col[1],
items = col[0];
$total_row.append('<td class="total-row">' + total + '</td>');
$average_row.append('<td class="avg-row">' + total / items + '</td>');
});
$tbody.append($average_row,$total_row);
}
function sumOfColumns($tbody) {
var $rows = $tbody.find('tr');
// Get number of columns
var num_cols = $rows.first().find('td').length;
var col_sums = {};
for (var col = 2; col < num_cols + 1; col++) {
var $col_data = $tbody.find('td:nth-child(' + col + ')'),
arr_values = $col_data.map(function() {
return this.textContent
}).get();
col_sums[col - 1] = [
arr_values.length,
arr_values.reduce(function(pv, cv) {
return pv + (parseFloat(cv) || 0)
}, 0)
];
}
return col_sums;
}
table {
border: 1px solid #333;
padding: 10px;
margin-right: 5px;
}
table tr:nth-child(odd) {
background-color: #e0e0e0;
}
table td {
padding: 10px;
border: 1px solid #555;
text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table class="tableOne">
<tr>
<td>Row One</td>
<td>45</td>
<td>23.356</td>
<td>88</td>
</tr>
<tr>
<td>Row Two</td>
<td>111440.568</td>
<td>115555</td>
<td>4.21598</td>
</tr>
<tr>
<td>Row Three</td>
<td>27</td>
<td>42</td>
<td>144487.11</td>
</tr>
<tr>
<td>Row Four</td>
<td>23.356</td>
<td>125%</td>
<td>778978523.36</td>
</tr>
</table>
<table class="tableTwo">
<tr>
<td>Row One</td>
<td>45</td>
<td>23.356</td>
<td>Hello</td>
</tr>
<tr>
<td>Row Two</td>
<td>111440.568</td>
<td>115555</td>
<td>4.21598</td>
</tr>
<tr>
<td>Row Three</td>
<td>Dog</td>
<td>true</td>
<td>144487.11</td>
</tr>
<tr>
<td>Row Four</td>
<td>23.356</td>
<td>125%</td>
<td>778978523.36</td>
</tr>
</table>
As others have mentioned, the trick is to check if the value is a number before using it in your calculations. That's as easy as isNaN(parseInt(string)).
Overall I think you can simplify your code a lot by extracting the (good) data from the table into arrays of numbers (one array for each column). You can see that in the getData function in the below snippet. Then it's a simple matter of iterating over each column's data to calculate its total and average.
You can also simplify things by using semantic markup: <tbody> for your data, <th> for heading cells, and <tfoot> for the Averages and Totals rows. This makes both extracting the data and styling the output much easier.
Here's how it looks with jQuery. Mind you, my jQuery's a bit rusty. At the end of the post you'll find another snippet sans jQuery, which is only slightly more verbose.
jQuery
function getData($rows) {
return $('td', $rows[0]).toArray().map((_, idx) =>
$rows.toArray().reduce((result, row) => {
const value = parseInt($('td', row)[idx].innerText, 10);
return result.concat(isNaN(value) ? [] : value);
}, [])
);
}
function generateRow(label, values, precision=3) {
const mult = Math.pow(10, precision);
return $(`<tr><th>${label}</th></tr>`).append(
values.map(val => $(`<td>${Math.round(val * mult) / mult}</td>`)));
}
function aggregateData($table) {
const averages = [];
const totals = [];
getData($table.find('tbody tr')).forEach(values => {
const total = values.reduce((total, val) => (total + val), 0);
totals.push(total);
averages.push(total / values.length);
});
$('<tfoot>').append(
generateRow('Averages', averages),
generateRow('Totals', totals)
).appendTo($table)
}
aggregateData($('.tableOne'));
aggregateData($('.tableTwo'));
table { width: 300px; margin-bottom: 1em; border: 1px solid gray; }
tbody tr:nth-child(odd) { background-color: #e0e0e0; }
td, th { padding: 7px; text-align: center; }
tfoot tr { background-color: paleturquoise; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table class="tableOne">
<tbody>
<tr>
<th>One</th>
<td>10</td>
<td>300</td>
<td>6000</td>
</tr>
<tr>
<th>Two</th>
<td>20</td>
<td>400</td>
<td>7000</td>
</tr>
<tr>
<th>Three</th>
<td>30</td>
<td>500</td>
<td>8000</td>
</tr>
<tr>
<th>Four</th>
<td>40</td>
<td>600</td>
<td>9000</td>
</tr>
</tbody>
</table>
<table class="tableTwo">
<tr>
<th>One</th>
<td>10</td>
<td>300</td>
<td>Hello</td>
</tr>
<tr>
<th>Two</th>
<td>20</td>
<td>400</td>
<td>7000</td>
</tr>
<tr>
<th>Three</th>
<td>Dog</td>
<td>true</td>
<td>8000</td>
</tr>
<tr>
<th>Four</th>
<td>40</td>
<td>600</td>
<td>9000</td>
</tr>
</table>
No jQuery
function getData(rows) {
const numCols = rows[0].querySelectorAll('td').length;
return new Array(numCols).fill().map((_, idx) =>
Array.prototype.reduce.call(rows, (result, row) => {
const value = parseInt(row.querySelectorAll('td')[idx].innerText, 10);
return result.concat(isNaN(value) ? [] : value);
}, [])
);
}
function generateRow(label, values, precision=3) {
const mult = Math.pow(10, precision);
const row = document.createElement('tr');
row.innerHTML = values.reduce((html, val) => html + `<td>${Math.round(val * mult) / mult}</td>`, `<th>${label}</th>`);
return row;
}
function aggregateData(table) {
const totals = [];
const averages = [];
getData(table.querySelectorAll('tbody tr')).forEach(values => {
const total = values.reduce((total, val) => (total + val), 0);
totals.push(total);
averages.push(total / values.length);
});
const tfoot = document.createElement('tfoot');
tfoot.appendChild(generateRow('Averages', averages));
tfoot.appendChild(generateRow('Totals', totals));;
table.appendChild(tfoot);
}
aggregateData(document.querySelector('.tableOne'));
aggregateData(document.querySelector('.tableTwo'));
table { width: 300px; margin-bottom: 1em; border: 1px solid gray; }
tbody tr:nth-child(odd) { background-color: #e0e0e0; }
td, th { padding: 7px; text-align: center; }
tfoot tr { background-color: paleturquoise; }
<table class="tableOne">
<tbody>
<tr>
<th>One</th>
<td>10</td>
<td>300</td>
<td>6000</td>
</tr>
<tr>
<th>Two</th>
<td>20</td>
<td>400</td>
<td>7000</td>
</tr>
<tr>
<th>Three</th>
<td>30</td>
<td>500</td>
<td>8000</td>
</tr>
<tr>
<th>Four</th>
<td>40</td>
<td>600</td>
<td>9000</td>
</tr>
</tbody>
</table>
<table class="tableTwo">
<tr>
<th>One</th>
<td>10</td>
<td>300</td>
<td>Hello</td>
</tr>
<tr>
<th>Two</th>
<td>20</td>
<td>400</td>
<td>7000</td>
</tr>
<tr>
<th>Three</th>
<td>Dog</td>
<td>true</td>
<td>8000</td>
</tr>
<tr>
<th>Four</th>
<td>40</td>
<td>600</td>
<td>9000</td>
</tr>
</table>
So I'm trying to figure out the best and easiest way to highlight a selection of cells from a table.
#A1lnk, #B1lnk {cursor: pointer;}
<table border="1">
<tr>
<th colspan="2"><a id='A1lnk'>A1</a></th><th colspan="2"><a id='B1lnk'>B1</a></th>
</tr>
<tr>
<td>A1-1</td><td>A1-2</td><td>B1-1</td><td>B1-2</td>
</tr>
<tr>
<td>A1-3</td><td>A1-4</td><td>B1-3</td><td>B1-4</td>
</tr>
<tr>
<td>A1-5</td><td>A1-6</td><td>B1-5</td><td>B1-6</td>
</tr>
<tr>
<th colspan="2"><a id='C1lnk'>C1</a></th><th colspan="2"><a id='D1lnk'>D1</a></th>
</tr>
<tr>
<td>C1-1</td><td>C1-2</td><td>D1-1</td><td>D1-2</td>
</tr>
<tr>
<td>C1-3</td><td>C1-4</td><td>D1-3</td><td>D1-4</td>
</tr>
<tr>
<td>C1-5</td><td>C1-6</td><td>D1-5</td><td>D1-6</td>
</tr>
<tr>
<th colspan="2"><a id='E1lnk'>E1</a></th><th colspan="2"><a id='F1lnk'>F1</a></th>
</tr>
<tr>
<td>E1-1</td><td>E1-2</td><td>F1-1</td><td>F1-2</td>
</tr>
<tr>
<td>E1-3</td><td>E1-4</td><td>F1-3</td><td>F1-4</td>
</tr>
<tr>
<td>E1-5</td><td>E1-6</td><td>F1-5</td><td>F1-6</td>
</tr>
</table>
You can see I have essentially got two columns, A1 and B1. The contents are very simple but suffice to say the actual contents will not be that simple.
I want to be able to click B1 and all the cells below it are highlighted, highlights are the easy part, actually selecting the correct cells is much harder.
I will have multiple other small tables adding C1, D1, E1, F1, G1, H1 etc. So there could be a few extra but only ever in columns of two. They will cascade in the rows and so still be part of the parent table but I'm just trying to figure out the best way to go about it, since the table creates them in rows and not columns.
I tried something like you said, however the code gone very long, that's why I have removed some rows.
var a1lnk = document.getElementById('A1lnk');
var a2lnk = document.getElementById('B1lnk');
var a3lnk = document.getElementById('C1lnk');
var a1 = document.getElementById('a1');
var a2 = document.getElementById('a2');
var c1 = document.getElementById('c1');
var c2 = document.getElementById('c2');
function unhighlight () {
b1.removeAttribute('h');
b2.removeAttribute('h');
a1.removeAttribute('h');
a2.removeAttribute('h');
c1.removeAttribute('h');
c2.removeAttribute('h');
}
var b1 = document.getElementById('b1');
var b2 = document.getElementById('b2');
function highlightA () {
unhighlight();
a1.setAttribute('h', true);
a2.setAttribute('h', true);
}
function highlightB () {
unhighlight();
b1.setAttribute('h', true);
b2.setAttribute('h', true);
}
function highlightC () {
unhighlight();
c1.setAttribute('h', true);
c2.setAttribute('h', true);
}
a1lnk.onclick = highlightA;
a2lnk.onclick = highlightB;
a3lnk.onclick = highlightC;
#A1lnk, #B1lnk, #C1lnk {cursor: pointer;}
td[h] {
background-color: orange;
color: #fff;
}
<table border="1">
<tr>
<th colspan="2"><a id='A1lnk'>A1</a></th><th colspan="2"><a id='B1lnk'>B1</a></th>
</tr>
<tr>
<td id="a1">A1-1</td><td id="a2">A1-2</td><td id="b1">B1-1</td><td id="b2">B1-2</td>
</tr>
<tr>
<th colspan="2"><a id='C1lnk'>C1</a></th>
</tr>
<tr>
<td id="c1">C1-1</td><td id="c2">C1-2</td>
</tr>
</table>
Hope, this should work for you.
You should use a class for header instead of different ids. Then on click of header get it's index. Using this index you can easily select the cells below it using nextUntil() method and :nth-child pseudo selector and highlight them like following.
$('.header').click(function() {
var index = $(this).parent().index(),
a = index * 2 + 1,
b = a + 1;
$('.highlight').removeClass('highlight');
var tr = $(this).closest('tr').nextUntil(':has(th)')
tr.find('td:nth-child(' + a + '), td:nth-child(' + b + ')').addClass('highlight');
});
.header {
cursor: pointer;
}
.highlight {
background-color: green;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table border="1">
<tr>
<th colspan="2"><a class="header">A1</a></th>
<th colspan="2"><a class="header">B1</a></th>
</tr>
<tr>
<td>A1-1</td>
<td>A1-2</td>
<td>B1-1</td>
<td>B1-2</td>
</tr>
<tr>
<td>A1-3</td>
<td>A1-4</td>
<td>B1-3</td>
<td>B1-4</td>
</tr>
<tr>
<td>A1-5</td>
<td>A1-6</td>
<td>B1-5</td>
<td>B1-6</td>
</tr>
<tr>
<th colspan="2"><a class="header">C1</a></th>
<th colspan="2"><a class="header">D1</a></th>
</tr>
<tr>
<td>C1-1</td>
<td>C1-2</td>
<td>D1-1</td>
<td>D1-2</td>
</tr>
<tr>
<td>C1-3</td>
<td>C1-4</td>
<td>D1-3</td>
<td>D1-4</td>
</tr>
<tr>
<td>C1-5</td>
<td>C1-6</td>
<td>D1-5</td>
<td>D1-6</td>
</tr>
<tr>
<th colspan="2"><a class="header">E1</a></th>
<th colspan="2"><a class="header">F1</a></th>
</tr>
<tr>
<td>E1-1</td>
<td>E1-2</td>
<td>F1-1</td>
<td>F1-2</td>
</tr>
<tr>
<td>E1-3</td>
<td>E1-4</td>
<td>F1-3</td>
<td>F1-4</td>
</tr>
<tr>
<td>E1-5</td>
<td>E1-6</td>
<td>F1-5</td>
<td>F1-6</td>
</tr>
</table>
I am admittedly very inexperienced, but I'm trying to enhance the workflow at my company. I'm using the code from here to create internal site to search for current part numbers. My employees are searching by description, but the javascript included in the example searches by exact input instead of individual words.
For example, using the example code on the site, I would like a search for "Trading Island" to return the same result as "Island Trading". I know this is possible, but can't figure out how to make it work.
<!DOCTYPE html>
<html>
<head>
<style>
* {
box-sizing: border-box;
}
#myInput {
background-image: url('/css/searchicon.png');
background-position: 10px 10px;
background-repeat: no-repeat;
width: 100%;
font-size: 16px;
padding: 12px 20px 12px 40px;
border: 1px solid #ddd;
margin-bottom: 12px;
}
#myTable {
border-collapse: collapse;
width: 100%;
border: 1px solid #ddd;
font-size: 18px;
}
#myTable th, #myTable td {
text-align: left;
padding: 12px;
}
#myTable tr {
border-bottom: 1px solid #ddd;
}
#myTable tr.header, #myTable tr:hover {
background-color: #f1f1f1;
}
</style>
</head>
<body>
<h2>My Customers</h2>
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names.." title="Type in a name">
<table id="myTable">
<tr class="header">
<th style="width:60%;">Name</th>
<th style="width:40%;">Country</th>
</tr>
<tr>
<td>Alfreds Futterkiste</td>
<td>Germany</td>
</tr>
<tr>
<td>Berglunds snabbkop</td>
<td>Sweden</td>
</tr>
<tr>
<td>Island Trading</td>
<td>UK</td>
</tr>
<tr>
<td>Koniglich Essen</td>
<td>Germany</td>
</tr>
<tr>
<td>Laughing Bacchus Winecellars</td>
<td>Canada</td>
</tr>
<tr>
<td>Magazzini Alimentari Riuniti</td>
<td>Italy</td>
</tr>
<tr>
<td>North/South</td>
<td>UK</td>
</tr>
<tr>
<td>Paris specialites</td>
<td>France</td>
</tr>
</table>
<script>
function myFunction() {
var input, filter, table, tr, td, i;
input = document.getElementById("myInput");
filter = input.value.toUpperCase();
table = document.getElementById("myTable");
tr = table.getElementsByTagName("tr");
for (i = 0; i < tr.length; i++) {
td = tr[i].getElementsByTagName("td")[0];
if (td) {
if (td.innerHTML.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
}
</script>
</body>
</html>
When searching for word matches, it's best to place the search string
into an array and the cell contents to be searched into an array.
Then you can easily use the Array.prototype.every() or the Array.prototype.some() methods to see if all or some of the search words are present in the cell.
See my comments inline below for additional details.
// Get a reference to the input
var searchElement = document.getElementById("myInput");
// Hook element up to event handler:
searchElement.addEventListener("keyup", search);
// Event handler:
function search(){
// Now, break the search input into an array of words by splitting the string where
// there are spaces (regular expression used here to locate the strings).
// Also, note that strings are case-sensitive, so we are taking the input and
// forcing it to lower case and we'll match by lower case later.
var searchWords = searchElement.value.toLowerCase().split(/\s+/);
// Test to see the individual words
//console.clear();
//console.log(searchWords);
// Now you have to have some content to search against.
// So, we'll get all the cells, which will be our data source:
var theCells = document.querySelectorAll("td");
// Loop over each cell
theCells.forEach(function(cell){
// Reset any previous cell matches
cell.style.backgroundColor = "inherit";
// Get the words in the cell as an array (note: we are forcing lower
// case again here to match lower case against lower case
var cellWords = cell.textContent.toLowerCase().split(/\s+/);
// See if the cell contains all of the search words (You can substitute
// "some" for "every" to just see if the cell contains some of the search words
var result = cellWords.every(elem => searchWords.indexOf(elem) > -1);
// every and some return a boolean letting you know if your condition was met:
if(result){
console.clear();
console.log("Match found in: " + cell.textContent);
cell.style.backgroundColor = "#ff0";
}
});
}
<input type="text" id="myInput" placeholder="Search for names.." title="Type in a name">
<table id="myTable">
<tr class="header">
<th style="width:60%;">Name</th>
<th style="width:40%;">Country</th>
</tr>
<tr>
<td>Alfreds Futterkiste</td>
<td>Germany</td>
</tr>
<tr>
<td>Berglunds snabbkop</td>
<td>Sweden</td>
</tr>
<tr>
<td>Island Trading</td>
<td>UK</td>
</tr>
<tr>
<td>Koniglich Essen</td>
<td>Germany</td>
</tr>
<tr>
<td>Laughing Bacchus Winecellars</td>
<td>Canada</td>
</tr>
<tr>
<td>Magazzini Alimentari Riuniti</td>
<td>Italy</td>
</tr>
<tr>
<td>North/South</td>
<td>UK</td>
</tr>
<tr>
<td>Paris specialites</td>
<td>France</td>
</tr>
</table>