data sorting in html webpage with script.js file but data is not being sorted correctly? - javascript

okay I have bunch of csv files in my folder I am running python script to convert all of them in html files. so the python script is working fine I have my csv files in html table files with styles.css correctly applied. but when I am clicking on percentage column for sorting the high percentages are coming on second third of fifth position instead of top position same is true for remaining two columns. now the sorting is not 100 % correct or incorrect.
structure of my data
structure of my csv data
python script:
import pandas as pd
import os
for file_name in os.listdir():
if file_name.endswith(".csv"):
df = pd.read_csv(file_name)
html_table = df.to_html(table_id="sortTable", classes=["sortTable"],
index=False, justify='center', header=True, render_links=True)
with open(file_name.replace('.csv', '.html'), 'w') as f:
f.write("<!DOCTYPE html>\n<html>\n<head>\n")
with open("styles.css", "r") as css:
f.write("<style>\n")
f.write(css.read())
f.write("\n</style>\n")
f.write("</head>\n<body>\n")
f.write(html_table)
f.write("<script src='https://code.jquery.com/jquery-3.6.0.min.js'></script>")
with open("script.js", "r") as js:
f.write("<script>\n")
f.write(js.read())
f.write("\n</script>\n")
f.write("</body>\n</html>")
***************here, is the structure of **styles.css **file *********************************
.sortTable {
border-collapse: collapse;
margin: 25px 0;
font-size: 0.9em;
min-width: 400px;
border-radius: 5px 5px 0 0;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
.sortTable thead tr {
background-color: #009879;
color: #ffffff;
text-align: left;
font-weight: bold;
cursor: pointer;
}
.sortTable th,
.sortTable td {
padding: 12px 15px;
}
.sortTable tbody tr {
border-bottom: 1px solid #dddddd;
}
.sortTable tbody tr:nth-of-type(even) {
background-color: #f3f3f3;
}
.sortTable tbody tr:last-of-type {
border-bottom: 2px solid #009879;
}
th i{
margin-left: 1px;
visibility: hidden;
}
th .active{
visibility: visible;
}
******************************* here is the **script.js **file **********************************
const table = document.getElementById('sortTable');
const headers = table.querySelectorAll('th');
const rows = table.querySelectorAll('tr');
headers.forEach((header,headerIndex)=>{
header.addEventListener('click',()=>{
sortColumn(headerIndex);
});
});
// Transform the content of given cell in given column
const transform = function (index, content) {
// Get the data type of column
const type = headers[index].getAttribute('type');
switch (type) {
case 'number':
return parseFloat(content);
case 'string':
default:
return content;
}
};
// Track sort directions
let directions = Array(headers.length).fill("");
console.log(directions);
function sortColumn(headerIndex){
//Check the direction asc or desc
const direction = directions[headerIndex] || 'asc';
const multiplier = (direction=='asc')? 1 :-1;
//lets make new instance of rows
let arrayRows = Array.from(rows);
arrayRows.shift();//Exclude header
let newRows = Array.from(arrayRows);
newRows.sort(function(rowA,rowB){
//Get the content of cells
const cellA = rowA.querySelectorAll('td')[headerIndex].innerHTML;
const cellB = rowB.querySelectorAll('td')[headerIndex].innerHTML;
let a = transform(headerIndex,cellA);
let b = transform(headerIndex,cellB);
if(a>b)
return 1*multiplier;
else if(a<b)
return -1*multiplier;
else
return 0;
});
//Remove old rows
let tbody = document.getElementsByTagName("tbody");
rows.forEach((row,index)=>{
if(index!=0)
tbody[0].removeChild(row);
});
//Append new row
newRows.forEach((newRow)=>{
tbody[0].appendChild(newRow);
});
// Reverse the direction
directions[headerIndex] = direction === 'asc' ? 'desc' : 'asc';
// console.log(directions);
}
// How sort function works:
// arr = ['3', '1', 4, 1, 5, 9];
// function compareFun(a,b){
// if(a>b)
// {
// return 1; //Place a after b
// }
// else if(a<b)
// {
// return -1;//Place a before b
// }
// else
// return 0;//Don' change the position keep original order
// }
// arr.sort(compareFun);
**************************** output after running all scripts *******
notice how 24 is not ahead in the list
same as above big numbers are not in top
**************the scripts are obtained from youtubers! thanking them for their help.
chatgpt provided the csv to html script
youtube channel "Computer Nerds" provided the css and java script. (https://youtu.be/sqS5Jcttupo)
i tried to understand but i am microbiologist not a software engineer so not much.
type here

Your table headers <th> elements are missing type attribute and an icon <i> to trigger sorting. In your html, instead of
<tr style="text-align: center">
<th>Taxa</th>
<th>sample</th>
<th>Percentage</th>
</tr>
the table headers should look more like this
<tr style="text-align: center">
<th type="string">Taxa<i class="fa-solid"></i></th>
<th type="number">sample<i class="fa-solid"></i></th>
<th type="number">Percentage<i class="fa-solid"></i></th>
</tr>
To generate this with your script, you can do something like this:
# new function to map pandas dtypes to html types
# should work for your current data, but this does not exhaust dtype options
def choose_type(dtype):
return {
'object': 'string',
'int64': 'number',
'float64': 'number',
}[dtype]
for file_name in os.listdir():
if file_name.endswith(".csv"):
df = pd.read_csv(file_name)
html_table = df.to_html(table_id="sortTable", classes=["sortTable"],
index=False, justify='center', header=True, render_links=True)
### new lines to add ##
# add types attribute
columns_dtypes = [str(dtype) for dtype in df.dtypes]
columns = dict(zip(df.columns, columns_dtypes))
for col, dtype in columns.items():
html_table = html_table.replace(f'<th>{col}', f'<th type="{choose_type(dtype)}">{col}')
# add icon elenement
html_table = html_table.replace('</th>', '<i class="fa-solid"></i></th>')
###
with open(file_name.replace('.csv', '.html'), 'w') as f:
f.write("<!DOCTYPE html>\n<html>\n<head>\n")
# new line to add font-awsome for the sort icons
f.write('<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A==" crossorigin="anonymous" referrerpolicy="no-referrer"/>')
# ... and so on

After analyzing the script.js file, I realized that Java was reading my numerical columns as text. Hence, I updated the script and forced java to read my column as numbers, and then compared which produced the right order of sorting. Here is the corrected.js file. 1.) python script (in question) 2.) styles.css (in question) 3.) and script.js (here), put all this files in cwd and all your cwd csv files will be converted in a HTML tables.
const table = document.getElementById('sortTable');
const headers = table.querySelectorAll('th');
const rows = table.querySelectorAll('tr');
headers.forEach((header,headerIndex)=>{
header.addEventListener('click',()=>{
sortColumn(headerIndex);
});
});
// Transform the content of given cell in given column
const transform = function (index, content) {
// Get the data type of column
const type = headers[index].getAttribute('type');
switch (type) {
case 'number':
return parseFloat(content);
case 'string':
default:
return content;
}
};
// Track sort directions
let directions = Array(headers.length).fill("");
console.log(directions);
function sortColumn(headerIndex) {
// Check if the header text is "percentage"
if (headers[headerIndex].textContent !== "Percentage") {
return;
}
const transform = function (index, content) {
// Get the data type of column
if (headers[index].textContent === "Percentage") {
return parseFloat(content);
} else {
const type = headers[index].getAttribute('type');
switch (type) {
case 'number':
return parseFloat(content);
case 'string':
default:
return content;
}
}
};
//Check the direction asc or desc
const direction = directions[headerIndex] || 'asc';
const multiplier = (direction=='asc')? 1 :-1;
//lets make new instance of rows
let arrayRows = Array.from(rows);
arrayRows.shift();//Exclude header
let newRows = Array.from(arrayRows);
newRows.sort(function(rowA,rowB){
//Get the content of cells
const cellA = rowA.querySelectorAll('td')[headerIndex].innerHTML;
const cellB = rowB.querySelectorAll('td')[headerIndex].innerHTML;
let a = transform(headerIndex,cellA);
let b = transform(headerIndex,cellB);
if(a>b)
return 1*multiplier;
else if(a<b)
return -1*multiplier;
else
return 0;
});
let tbody = document.getElementsByTagName("tbody");
rows.forEach((row,index)=>{
if(index!=0)
tbody[0].removeChild(row);
});
newRows.forEach((newRow)=>{
tbody[0].appendChild(newRow);
});
// Reverse the direction
directions[headerIndex] = direction === 'asc' ? 'desc' : 'asc';
}
// How sort function works:
// arr = ['3', '1', 4, 1, 5, 9];
// function compareFun(a,b){
// if(a>b)
// {
// return 1; //Place a after b
// }
// else if(a<b)
// {
// return -1;//Place a before b
// }
// else
// return 0;//Don' change the position keep original order
// }
// arr.sort(compareFun);

Related

JavaScript Minesweeper - Opening whole mine-free area at once not working properly (flood-fill)

I am trying to build a simple minesweeper game in Javascript. It works propererly apart from the function to open the entire mine-free area when clicking on a mine-free tile. It starts checking the neighbouring tiles, but stops when the first neighbouring tile has a mine.
As you can see on the screenshot below (after clicking on tile 1/5) only the tiles until the first "1" are opened. It should actually open a much larger area:
It seems I am pretty close. THis is my code:
const gridSize = 10
// generate grid
const board = document.querySelector("#minesweeper");
// loop over num for rows
let header = 0;
for(let i = 0; i < gridSize+1; i++) {
const row = document.createElement("tr");
// loop over num for cols
for (let j = 0; j < gridSize+1; j++) {
// add col to row
if ( i === 0 ) {
row.insertAdjacentHTML("beforeend", `<th>${header}</th>`);
header += 1;
} else if (j === 0) {
row.insertAdjacentHTML("beforeend", `<th>${header-10}</th>`);
header += 1;
} else {
row.insertAdjacentHTML("beforeend", `<td class='unopened' dataset-column=${j}></td>`);
};
};
// add row to board
board.append(row);
};
// functions -------------------
function getNeighbour(tile, i, j) {
const column = tile.cellIndex; // so the columns get the cellIndex
const row = tile.parentElement.rowIndex; // row gets the rowIndex(tr)
const offsetY = row + i;
const offsetX = column + j;
return document.querySelector(`[data-row="${offsetY}"][data-column="${offsetX}"]`);
}
// count mines of neighbours
function countMines(tile) {
let mines = 0;
for(i = -1; i <= 1; i++) {
for(j = -1; j <= 1; j++ ) {
// check if neighbour has mine
// get cell values from neighbour in DOM
nb = getNeighbour(tile, i, j);
if (nb && nb.classList.contains('has-mine') || (nb && nb.classList.contains('mine'))) mines += 1; // if nb exists and has a mine increase mines
}
}
// write into DOM
if (mines === 0) {
tile.classList.add(`opened`);
} else {
tile.classList.add(`neighbour-${mines}`);
}
tile.classList.remove(`unopened`);
// if mines are 0, go to neigbours and count mines there
// console.log(tile.classList);
if (mines === 0) {
// alert("mines are zero");
for (i = -1; i <= 1; i+=1) {
for (j = -1; j <= 1; j+=1) {
nb = getNeighbour(tile, i, j);
if (nb && nb.classList.contains("unopened")) {
countMines(nb);
}
}
}
}
return mines;
}
// function open tile on click
function openTile(event) {
const tile = event.currentTarget;
// if there is a mine you lose
if (tile.classList.contains("has-mine")) {
document.querySelectorAll(".has-mine").forEach((cell) => {
cell.classList.remove("has-mine", "unopened");
cell.classList.add("mine", "opened");
});
alert("booooooooom!");
} else {
countMines(tile);
}
}
const tiles = document.querySelectorAll("td");
tiles.forEach((td) => {
td.dataset.column = td.cellIndex; // so the columns get the cellIndex
td.dataset.row = td.parentElement.rowIndex; // row gets the rowIndex(tr)
// add mines randomly
const freq = 0.1;
if (Math.random() < freq) {
td.classList.add("has-mine");
}
// eventlisteners per tile
td.addEventListener("click", openTile);
});
I have been thinking hours about it but could not find a way to work on with this code. Not sure if I am close or if I would need to modify the whole approach?
Many thanks for any ideas!
the principle is simple, for each empty cell, you must add all the adjacent empty cells.
it is also necessary to collect the number of adjacent mines of each cell
a) list the 8 adjacent cells, except for the cells placed at the edge
this is the prxElm() function in my code
b) count the mines present around a cell -> prxMne()
starting from the first cell
1- we count (a) nearby mines
2- it becomes the first element of a stack of cells to be mapped
3- if its number of nearby mines is zero, repeat this operation for all adjacent cells
the particularity of this algorithm is to use only one stack to accumulate the coordinates to be mapped.
it places the elements with adjacent mines at the top of the stack, and those with none at the end of the stack.
as there can be several cells without adjacent mines, we keep an indexiExp of the last empty cell processed.
of course when you add a cell with mines nearby at the start of the stack, this index is shifted.
the algorithm also take care not to add duplicate the same cell by checking before if this cell is not in the stack.
see .filter(x=>!explor.some(e=>e.p===x.p))
this ends when the exploration index iExp reaches the end of the stack.
here is the whole code, it is not completely finalized, but the essentials are there.
const
MinesCount = 17 // adjusted values to fit this snippet display area
, gridSz = { r:7, c:20 } // grid rows x cols
, gridMx = gridSz.r * gridSz.c
, proxim = [ {v:-1,h:-1}, {v:-1,h:0}, {v:-1,h:+1}, {v:0,h:-1}, {v:0,h:+1}, {v:+1,h:-1}, {v:+1,h:0}, {v:+1,h:+1} ]
, prxElm = (r,c) => proxim.reduce((a,{v,h})=>
{
let rv = r+v, ch = c+h;
if (rv>=0 && ch>=0 && rv<gridSz.r && ch<gridSz.c) a.push({p:((rv * gridSz.c) + ch), r:rv, c:ch} )
return a
},[])
, GenNbX = (nb,vMax) => [null].reduce(arr=>
{
while (arr.length < nb)
{
let numGen = Math.floor(Math.random() * vMax)
if (!arr.includes(numGen)) arr.push(numGen);
}
return arr //.sort((a,b)=>a-b)
},[])
, minesP = GenNbX( MinesCount, gridMx )
, prxMne = (r,c) => prxElm(r,c).reduce((a,{p})=>minesP.includes(p)?++a:a,0) // count mines arroub=nd
, td2rcp = td =>
{
let r = td.closest('tr').rowIndex -1 // -1 for thead count of rows
, c = td.cellIndex
, p = (r * gridSz.c) +c
return {r,c,p}
}
, p2rc = p =>({r: Math.floor(p / gridSz.c), c: (p % gridSz.c)})
, { timE, cFlags, minesArea } = drawTable('mines-area', gridSz, MinesCount )
;
const chrono = (function( timeElm )
{
const
one_Sec = 1000
, one_Min = one_Sec * 60
, twoDgts = t => (t<10) ? `0${t}` : t.toString(10)
, chronos =
{ timZero : null
, timDisp : timeElm
, timIntv : null
, running : false
}
, obj =
{ start()
{
if (chronos.running) return
chronos.timDisp.textContent = '00:00'
chronos.running = true
chronos.timZero = new Date().getTime()
chronos.timIntv = setInterval(() =>
{
let tim = (new Date().getTime()) - chronos.timZero
chronos.timDisp.textContent = `${Math.floor(tim/one_Min)}:${twoDgts(Math.floor((tim % one_Min)/one_Sec))}`
}
, 250);
}
, stop()
{
if (!chronos.running) return
chronos.running = false
clearInterval( chronos.timIntv )
}
}
return obj
}(timE))
function drawTable(tName, gSz, mines )
{
let table = document.getElementById(tName)
// table.innerHTML = '' // eraze table
let tHead = table.createTHead()
, tBody = table.createTBody()
, xRow = tHead.insertRow()
, timE = xRow.insertCell()
, cFlags = xRow.insertCell()
;
timE.setAttribute('colspan', gSz.c -4)
timE.className = 'time'
timE.textContent = '0:00'
cFlags.setAttribute('colspan', 4)
cFlags.className = 'flag'
cFlags.textContent = ' 0/' + mines
for (let r=gSz.r;r--;)
{
xRow = tBody.insertRow()
for(let c = gSz.c;c--;) xRow.insertCell()
}
return { timE, cFlags, minesArea: tBody }
}
minesArea.onclick = ({target}) =>
{
if (!target.matches('td')) return
if (target.hasAttribute('class')) return // already done
chrono.start()
let {r,c,p} = td2rcp(target)
if (minesP.includes(p)) // you are dead!
{
chrono.stop()
minesArea.className = 'Boom'
minesP.forEach(p=> // show mines
{
let {r,c} = p2rc(p)
let td = minesArea.rows[r].cells[c]
if (!td.hasAttribute('class')) td.className = 'mineOff'
})
minesArea.rows[r].cells[c].className = 'mineBoom' // this one is for you
minesArea.querySelectorAll('td:not([class]), td.flag') // jusr disable click
.forEach(td=>td.classList.add('off')) // and cursor
}
else
{
let explor = [ {p, r, c, m:prxMne(r,c) } ]
, iExp = 0
;
while (iExp < explor.length && explor[iExp].m === 0) // Open mine-free area
{
prxElm(explor[iExp].r,explor[iExp].c) // look around
.filter(x=>!explor.some(e=>e.p===x.p)) // if not already in
.forEach(x=>
{
M = prxMne(x.r,x.c)
if (M>0 ) { explor.unshift( { p:x.p, r:x.r, c:x.c, m:M} ); iExp++ }
else explor.push( { p:x.p, r:x.r, c:x.c, m:M} ) // mine-free space
})
iExp++
}
explor.forEach(({r,c,m})=>minesArea.rows[r].cells[c].className = 'm'+m )
}
if (checkEnd()) // some kind of victory!?
{
chrono.stop()
minesArea.querySelectorAll('td.flag').forEach(td=>td.classList.add('off'))
minesArea.className = 'win'
}
}
minesArea.oncontextmenu = e => // Yes, there is a right click for flag mines
{
if (!e.target.matches('td')) return
e.preventDefault()
let {r,c,p} = td2rcp( e.target)
, cell_rc = minesArea.rows[r].cells[c]
;
if (!cell_rc.hasAttribute('class')) cell_rc.className = 'flag'
else if (cell_rc.className === 'flag') cell_rc.removeAttribute('class')
let nbFlags = minesArea.querySelectorAll('td.flag').length
cFlags.textContent = ` ${nbFlags} / ${MinesCount}`
}
function checkEnd()
{ // what about us ?
let count = 0
, reject = 0
, tdNotSeen = minesArea.querySelectorAll('td:not([class])')
, flagPos = minesArea.querySelectorAll('td.flag')
;
cFlags.textContent = ` ${flagPos.length} / ${MinesCount}`
if (tdNotSeen.length > MinesCount ) return false
flagPos.forEach(td=>
{
let {r,c,p} = td2rcp(td)
if (minesP.includes(p)) count++ // correct place
else reject++
})
tdNotSeen.forEach(td=>
{
let {r,c,p} = td2rcp(td)
if (minesP.includes(p)) count++
else reject++ // no mines there
})
if (count != MinesCount || reject != 0 ) return false
tdNotSeen.forEach(td=>
{
let {r,c,p} = td2rcp(td)
minesArea.rows[r].cells[c].className = 'mineOff'
})
cFlags.textContent = ` ${MinesCount} / ${MinesCount}`
return true
}
body { background-color: #383947; } /* dark mode ? ;-) */
table {
border-collapse : collapse;
margin : 1em auto;
--szRC : 18px;
font-family : Arial, Helvetica, sans-serif;
}
table td {
border : 1px solid #1a1a1a80;
text-align : center;
}
table thead {
font-size : .8em;
background-color : #c3c5db;
}
table tbody {
background-color : #a39999;
cursor : cell;
}
table tbody td {
width : var(--szRC);
height : var(--szRC);
overflow : hidden;
}
.m0, .m1, .m2, .m3, .m4, .m5, .m6, .m7, .m8 { background-color: whitesmoke; font-size: 12px; font-weight: bold; cursor: default; }
.m1::after { content: '1'; color: #0000ff; }
.m2::after { content: '2'; color: #008000; }
.m3::after { content: '3'; color: #ff0000; }
.m4::after { content: '4'; color: #000080; }
.m5::after { content: '5'; color: #800000; }
.m6::after { content: '6'; color: #008080; }
.m7::after { content: '7'; color: #000000; }
.m8::after { content: '8'; color: #808080; }
.off { cursor: default; }
.Boom { background-color: yellow; cursor: default; }
.mineOff { cursor: default; padding: 0; }
.flag { background-color: lightgray; padding: 0; }
.mineBoom { color: crimson; padding: 0; }
.mineOff::after,
.mineBoom::after { content: '\2738'; }
.flag::before { content: '\2691'; color: crimson; }
.time::before { content: 'Time elapsed : '; color: darkblue; }
.win td { border-color: gold;}
<table id="mines-area"></table>
I don't think a recursive method is suitable for this kind of problem.
It requires having a complex strategy for exploring empty spaces.
For example, spiraling around the starting point.
But this strategy comes up against the problem of the island hindering this progress, and which requires, once crossed, to carry out a new spiral advance to recover the points hidden during the previous spiral, but having to avoid the points already explored during the previous spiral.
You can also move forward by sectors from an initial point, but you will still encounter the same problem of the islet and its backtracking, which also multiply here with each roughness on the explored shores.
This requires complex calculations that are difficult to master and debug, not to mention the fact that recursive methods intensively use call stacks which it adds up for each new branch.
(Without neglecting the risk of going crazy, by striving to develop its recursive algorithms.)
The larger the grid and the more its recursive lists will conflict with each other, and more the computationnal time will be affected.
Of course there are already winning heuristics on this type of strategy, they are of a very high level, and we are here just on a minesweeper game where hundreds of lines of code have nothing to do.
My method only uses a single stack, its algorithm is easy to understand and requires a few lines of code.
What more ?

How to display XML content in HTML file with Javascript

This is my xml code . I want to display the contents in a html page with Javascript.
<businesses>
<business bfsId="" id="41481">
<advertHeader>Welding Supplies, Equipment and Service Business</advertHeader>
<Price>265000</Price>
<catalogueDescription>Extremely profitable (Sales £500k, GP £182k) business</catalogueDescription>
<keyfeature1>
Well established 25 year business with excellent trading record
</keyfeature1>
<keyfeature2>
Consistently high levels of turnover and profitability over last 5 years
</keyfeature2>
</business>
<business bfsId="" id="42701">
<broker bfsRef="1771" ref="003">Birmingham South, Wolverhampton & West Midlands</broker>
<tenure>freehold</tenure>
<advertHeader>Prestigious Serviced Office Business</advertHeader>
<Price>1200000</Price>
<reasonForSale>This is a genuine retirement sale.</reasonForSale>
<turnoverperiod>Annual</turnoverperiod>
<established>28</established>
<catalogueDescription>This well-located and long-established serviced office</catalogueDescription>
<underOffer>No</underOffer>
<image1>https://www.business-partnership.com/uploads/business/businessimg15977.jpg</image1>
<keyfeature1>other connections</keyfeature1>
<keyfeature2> Investment Opportunity</keyfeature2>
<keyfeature3>Over 6,000 sq.ft.</keyfeature3>
<keyfeature4>Well-maintained </keyfeature4>
<keyfeature5>In-house services & IT provided</keyfeature5>
</business>
</businesses>
This is the original xml file https://alpha.business-sale.com/bfs.xml I have just took a short portion to describe the situation.
Requirements
Print a row for every <business> element
For every <business> pick some specific child element and print column only for those element.( Not all ). For an example in this case I only want to print the value for <advertHeader> ; <Price> and <description> and want to ignore other elements.
Only print the row those <business> where value of <Price> is > 10000 . if it is then less than 10000 do not print that row
pagination after every 10 row
This is the html table
<table id="MainTable"><tbody id="BodyRows"></tbody></table>
And this is the javascript code that i have tried .
window.addEventListener("load", function() {
getRows();
});
function getRows() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("get", "2l.xml", true);
xmlhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
showResult(this);
}
};
xmlhttp.send(null);
}
function showResult(xmlhttp) {
var xmlDoc = xmlhttp.responseXML.documentElement;
removeWhitespace(xmlDoc);
var outputResult = document.getElementById("BodyRows");
var rowData = xmlDoc.getElementsByTagName("business");
addTableRowsFromXmlDoc(rowData,outputResult);
}
function addTableRowsFromXmlDoc(xmlNodes,tableNode) {
var theTable = tableNode.parentNode;
var newRow, newCell, i;
console.log ("Number of nodes: " + xmlNodes.length);
for (i=0; i<xmlNodes.length; i++) {
newRow = tableNode.insertRow(i);
newRow.className = (i%2) ? "OddRow" : "EvenRow";
for (j=0; j<xmlNodes[i].childNodes.length; j++) {
newCell = newRow.insertCell(newRow.cells.length);
if (xmlNodes[i].childNodes[j].firstChild) {
newCell.innerHTML = xmlNodes[i].childNodes[j].firstChild.nodeValue;
} else {
newCell.innerHTML = "-";
}
console.log("cell: " + newCell);
}
}
theTable.appendChild(tableNode);
}
function removeWhitespace(xml) {
var loopIndex;
for (loopIndex = 0; loopIndex < xml.childNodes.length; loopIndex++)
{
var currentNode = xml.childNodes[loopIndex];
if (currentNode.nodeType == 1)
{
removeWhitespace(currentNode);
}
if (!(/\S/.test(currentNode.nodeValue)) && (currentNode.nodeType == 3))
{
xml.removeChild(xml.childNodes[loopIndex--]);
}
}
}
But this code prints columns for all the nodes under <business> element. And the number of child elements under <business> are different . So the result comes like this
I dont want that. I want to only display the value of specific nodes under <business> element (in this case only include <advertHeader> ; <Price> and <description> ) so that the number of columns are equal in every row. How to do that?
Try finding the <business>-element with the most values and build your table around that. Here's an example snippet that does that for the data you presented.
{
const xml = new DOMParser()
.parseFromString(getData(), `text/xml`);
// the <business>-elements from xml
const businessElems = [...xml.querySelectorAll(`business`)];
// the nodeNames will be the header. While we're at it,
// we can count the number of headers (len) for later use
const headersFromXml = businessElems.map( v =>
[...v.querySelectorAll(`*`)]
.map( v => v.nodeName) )
.map( v => ( {len: v.length, headers: v} )
);
// now determine the longest header using a reducer
const businessElemWithMostNodes = headersFromXml
.reduce( (acc, v) => v.len > acc.len ? v : acc, {len: 0});
// utility to create a tablecell/header and append it to a row
const createCell = (rowToAppendTo, cellType, value) => {
const cell = document.createElement(cellType);
cell.innerHTML = value;
rowToAppendTo.appendChild(cell);
}
// utility to create a datarow and append it to a table
const createDataRow = (tableToAppendTo, businessElem) => {
const row = document.createElement(`tr`);
const rowValues = [];
// create values using the businessElemWithMostNodes order
businessElemWithMostNodes.headers
.forEach( head => {
const valueElem = businessElem.querySelector(`${head}`);
rowValues.push(valueElem ? valueElem.innerHTML : `-`);
});
rowValues.forEach( v => createCell(row, `td`, v) );
tableToAppendTo.appendChild(row);
};
// now we know enough to create the table
const table = document.createElement(`table`);
// the headerRow first
const headRow = document.createElement(`tr`);
businessElemWithMostNodes.headers.forEach( hv => createCell(headRow, `th`, hv) );
table.appendChild(headRow);
// next create and append the rows
businessElems.forEach(be => createDataRow(table, be));
// finally, append the table to document.body
document.body.appendChild(table);
// your xml string
function getData() {
return `
<businesses>
<business bfsId="" id="41481">
<advertHeader>Welding Supplies, Equipment and Service Business</advertHeader>
<Price>265000</Price>
<catalogueDescription>Extremely profitable (Sales £500k, GP £182k) business</catalogueDescription>
<keyfeature1>
Well established 25 year business with excellent trading record
</keyfeature1>
<keyfeature2>
Consistently high levels of turnover and profitability over last 5 years
</keyfeature2>
</business>
<business bfsId="" id="42701">
<broker bfsRef="1771" ref="003">Birmingham South, Wolverhampton & West Midlands</broker>
<tenure>freehold</tenure>
<advertHeader>Prestigious Serviced Office Business</advertHeader>
<Price>1200000</Price>
<reasonForSale>This is a genuine retirement sale.</reasonForSale>
<turnoverperiod>Annual</turnoverperiod>
<established>28</established>
<catalogueDescription>This well-located and long-established serviced office</catalogueDescription>
<underOffer>No</underOffer>
<image1>https://www.business-partnership.com/uploads/business/businessimg15977.jpg</image1>
<keyfeature1>other connections</keyfeature1>
<keyfeature2> Investment Opportunity</keyfeature2>
<keyfeature3>Over 6,000 sq.ft.</keyfeature3>
<keyfeature4>Well-maintained </keyfeature4>
<keyfeature5>In-house services & IT provided</keyfeature5>
</business>
</businesses>`;
}
}
body {
margin: 1rem;
font: 12px/15px normal consolas, verdana, arial;
}
.hidden {
display: none;
}
th {
background-color: black;
color: white;
border: 1px solid transparent;
padding: 2px;
text-align: left;
}
td {
border: 1px solid #c0c0c0;
padding: 2px;
vertical-align: top;
}

How to sort table rows using Javascript

Description
Table rows must be swapped at arbitrary positions in the table, i.e. row i and row j must change positions, where i and j are not necessarily adjacent. See current implementation below. Table rows should be sorted by column; a column is specified by sort_index which is generated by pressing one of the table headers.
The problem with the implementation is that the table is sorted incrementally by each click of a header, while it should be sorted by a single click.
var table_index = {
watched: 0,
title: 1,
director: 2,
year: 3
};
var sort_index = 0;
$(document).ready(function()
{
var table_headers = document.getElementById("header-item").children;
for (var k = 0; k < table_headers.length; k++)
{
$("#" + table_headers[k].id).bind("click", function(e)
{
sort_index = table_index[e.target.id];
var table = document.getElementById("film-list");
for (var i = 1; i < table.rows.length - 1; i++)
{
var a = table.rows[i].getElementsByTagName("td")[sort_index].innerHTML;
for (var j = i + 1; j < table.rows.length; j++)
{
var b = table.rows[j].getElementsByTagName("td")[sort_index].innerHTML;
var swap = 0;
switch (sort_index)
{
// Alphabetic sort
case 0:
case 1:
case 2:
if (b.toLowerCase() < a.toLowerCase())
swap = 1;
break;
// Numeric sort
case 3:
if (b - a < 0)
swap = 1;
break;
}
if (swap == 1)
{
$(".row-item").eq(i - 1).after(table.rows[j]);
$(".row-item").eq(j - 1).after(table.rows[i]);
}
}
}
});
}
});
Edit
It appears the real problem was related to closures inside loops. When a header is clicked, only the last table-row swap is actually updated in DOM, and as such multiple clicks are needed to sort the table properly.
I will post my own solution to this, to clarify the real problem.
I agree with Mr Polywhirl that doing this in the DOM itself probably isn't ideal (though it's entirely possible, see below). Modern web development leans toward using model/view/controller-style architectures (of various types) where your model (your actual data) is held separately from the view of it (the DOM), and the controller (the browser, DOM, and your code operating together), which takes actions upon your model (which are then reflected in the view). There are many popular MVC-style frameworks, probably the most significant as I write this (it changes over time) are React, Vue.js, and Angular. I've included a React example below.
But again, you can do this directly on the DOM. I see you're using jQuery, so I've used it below — See inline comments.
// Hook `click` on the table header, but only call our callback if
// that click passes through a `th`
$(".sortable thead").on("click", "th", function() {
// Which column is this?
var index = $(this).index();
// Get the tbody
var tbody = $(this).closest("table").find("tbody");
// Disconnect the rows and get them as an array
var rows = tbody.children().detach().get();
// Sort it
rows.sort(function(left, right) {
// Get the text of the relevant td from left and right
var $left = $(left).children().eq(index);
var $right = $(right).children().eq(index);
return $left.text().localeCompare($right.text());
});
// Put them back in the tbody
tbody.append(rows);
});
td, th {
padding: 4px;
}
th {
cursor: pointer;
}
table {
border-collapse: collapse;
}
table, td, th {
border: 1px solid #ddd;
}
To sort the rows alphabetically by a column's contents, click its header.
<table class="sortable">
<thead>
<th>English</th>
<th>Spanish</th>
<th>Italian</th>
</thead>
<tbody>
<tr>
<td>One</td>
<td>Uno</td>
<td>Uno</td>
</tr>
<tr>
<td>Two</td>
<td>Dos</td>
<td>Due</td>
</tr>
<tr>
<td>Three</td>
<td>Tres</td>
<td>Tre</td>
</tr>
<tr>
<td>Four</td>
<td>Cuatro</td>
<td>Quattro</td>
</tr>
</tbody>
</table>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
It can be a bit shorter, but I wanted to be clear rather than hyper-concise.
Notice that this removes the rows, sorts them, and puts them back, rather than causing all kinds of in-place DOM modifications.
Here's the React example:
// A React "component" for the table
class MyTable extends React.Component {
// Initializes the component
constructor(props) {
super(props);
this.state = {
// Start out unsorted, copying the array (but reusing the entries, as `row`
// properties -- we do that so we can use their original index as a key)
sorted: props.data.map((row, index) => ({index, row})),
sortKey: null
};
}
// Sort the view
sort(by) {
// Update state...
this.setState(({sorted, sortKey}) => {
if (sortKey === by) {
// ...no update needed, already sorting this way
return;
}
// Copy the array, then sort it (never change state in place)
sorted = sorted.slice();
sorted.sort((left, right) => left.row[by].localeCompare(right.row[by]));
// Return the state updates
return {sorted, sortKey: by};
});
}
// Render the component per current state
render() {
const {sorted} = this.state;
const {headers} = this.props;
return (
<table className="sortable">
<thead>
{headers.map(({title, lang}) => <th key={lang} onClick={() => this.sort(lang)}>{title}</th>)}
</thead>
<tbody>
{sorted.map(({row, index}) =>
<tr key={index}>
{headers.map(({lang}) => <td key={lang}>{row[lang]}</td>)}
</tr>
)}
</tbody>
</table>
);
}
}
// Mount the component
ReactDOM.render(
<MyTable
headers={[
{title: "English", lang: "en"},
{title: "Spanish", lang: "es"},
{title: "Italian", lang: "it"}
]}
data={[
{en: "One", es: "Uno", it: "Uno"},
{en: "Two", es: "Dos", it: "Due"},
{en: "Three", es: "Tres", it: "Tre"},
{en: "Four", es: "Cuatro", it: "Quattro"}
]}
/>,
document.getElementById("root")
);
td, th {
padding: 4px;
}
th {
cursor: pointer;
}
table {
border-collapse: collapse;
}
table, td, th {
border: 1px solid #ddd;
}
To sort the rows alphabetically by a column's contents, click its header.
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
That uses various ES2015+ features, such as destructuring, arrow functions, and shorthand properties; and also uses JSX syntax (which isn't a JavaScript feature; it's handled by Babel in the Snippet).
You can either sort the HTML in-place or you can bind data to the table and re-render it whenever you need to sort.
I used T.J. Crowder's code for in-place sorting, but turned it into a jQuery plugin and added a bindable table. You can see both examples below.
(function($) {
$.fn.sortable = function() {
this.find('thead').on('click', 'th', function(e) {
var columnIndex = $(this).index();
var $tbody = $(this).closest('table').find('tbody');
var rows = $tbody.children().detach().get();
rows.sort(function(left, right) {
var $left = $(left).children().eq(columnIndex);
var $right = $(right).children().eq(columnIndex);
return $left.text().localeCompare($right.text());
});
$tbody.append(rows);
});
return this;
};
$.fn.renderTable = function(data) {
var fields = Object.keys(data[0]);
return this.renderTableHeaders(fields).renderTableRows(fields, data);
};
$.fn.renderTableHeaders = function(fields) {
return this.append($.renderTableHeaders(fields));
}
$.fn.renderTableRows = function(fields, data) {
return this.append($.renderTableRows(fields, data));
};
$.tableFromJson = function(data) {
return $('<table>').renderTable(data);
};
$.renderTableHeaders = function(fields) {
return $('<thead>').append($('<tr>').append(fields
.map(field => $('<th>').text(field))));
};
$.renderTableRows = function(fields, data) {
return $('<tbody>').append(data
.map((rec, row) => $('<tr>').append(fields
.map((field, col) => $('<td>').text(rec[field])))));
};
$.bindableTable = function(data, sortable) {
var $table = $.tableFromJson(data).addClass('bindable');
if (sortable) {
$table.dataRef = data;
$table.addClass('sortable').find('thead').on('click', 'th', function(e) {
var dataIndex = $(this).text();
$table.dataRef.sort(function (a, b) {
var left = new String(a[dataIndex]);
var right = new String(b[dataIndex]);
return left.localeCompare(right);
});
var fields = Object.keys($table.dataRef[0]);
$table.find('tbody').replaceWith($.renderTableRows(fields, $table.dataRef));
});
}
return $table;
};
})(jQuery);
var jsonData = [
{ "id": 1, "name": "John", "age": 24, "make": "Chevrolet", "model": "Silverado", "year": 2016 },
{ "id": 2, "name": "Jack", "age": 36, "make": "Toyota", "model": "Corolla", "year": 2018 },
{ "id": 3, "name": "Jill", "age": 29, "make": "Ford", "model": "Escape", "year": 2015 }
];
$('body').append($('<h1>').text('HTML sort'));
$.tableFromJson(jsonData).addClass('stylized sortable').sortable().appendTo('body');
$('body').append($('<h1>').text('Databinding sort'));
$.bindableTable(jsonData, true).addClass('stylized').appendTo('body');
body {
padding: 0.25em !important;
}
h1 {
font-weight: bold !important;
margin-top: 0.75em !important;
margin-bottom: 0.33em !important;
}
table.stylized {
font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
font-size: 12px;
text-align: left;
border-collapse: collapse;
margin: 4px;
width: 600px;
}
table.stylized thead th {
text-transform: capitalize;
font-size: 13px;
color: #039;
background: #b9c9fe;
padding: 6px;
cursor: pointer;
}
table.stylized tbody tr:nth-child(odd) {
background: #f2f5ff;
}
table.stylized tbody tr:nth-child(even) {
background: #e8edff;
}
table.stylized tbody td {
border-top: 1px solid #fff;
color: #669;
padding: 6px;
}
table.stylized tbody tr:hover td {
background: #d0dafd;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Description
As stated, the original problem was swapping of rows. It appears the real problem was related to closures inside loops. When a header is clicked, only the last table-row swap is actually updated in DOM, and as such multiple clicks are needed to sort the table properly.
Solution
One possible solution is to use the built-in sort function, as shown below.
var table_index = {
watched: 0,
title: 1,
director: 2,
year: 3
};
var sort_index = 0;
var tbody = $("tbody").children().get();
$(document).ready(function()
{
var table_headers = $("thead").children();
for (var k = 0; k < table_headers.length; k++)
{
$("#" + table_headers[k].id).bind("click", function(e)
{
sort_index = table_index[e.target.id];
switch (sort_index)
{
// Alphabetic sort
case 0:
case 1:
case 2:
tbody.sort(function(a, b) {
var l = $(a).children().eq(sort_index).text();
var r = $(b).children().eq(sort_index).text();
if (r.toLowerCase() < l.toLowerCase())
return 1;
else if (r.toLowerCase() > l.toLowerCase())
return -1;
else
return 0;
});
break;
// Numeric sort
case 3:
tbody.sort(function(a, b) {
var l = $(a).children().eq(sort_index).text();
var r = $(b).children().eq(sort_index).text();
return l - r;
});
break;
}
$("tbody").children().detach();
$("tbody").append(tbody);
});
}
});

Group JSON data depending on the first 3 columns and sum the number of the 4th column in HTML/JS

I have a JSON file and I want to create multiple HTML tables depending on a combination of values in the JSON objects.
Let's have a look at a sample of a JSON to give a better explanation:
var data = {"headers":["plat","chan","cat","num"],"rows":[["plat1","chan1","cat1",1],
["plat1","chan1","cat1",2],["plat2","chan1","cat2",5]]};
In the example above, we have 2 rows with the same values in the first three columns. I want to generate an HTML table with one row (because the values are the same) but the fourth column must have the SUM.
So, the HTML table must be like:
plat1, chan1, cat1, 3
plat2, chan1, cat2, 5
The snippet below was based on an initial script and the help of a fellow stackoverflow user :)
var data = {"headers":["plat","chan","group","cat","num"],"rows":[["plat1","chan1","bbb","cat1",222],
["plat1","chan1","bbb","cat1",333],
["plat1","chan1","bbb","cat2",850]]};
// transform the data to get a clear connection between the
// heading and the corresponding data
const transformedData = data.rows.map((row) => {
const emptyRowDataObject = {};
return row.reduce((dataObjSoFar, rowData, rowDataIndex) => {
// for each of the values in the row, add a property
// with the name of the corresponding header
const correspondingHeader = data.headers[rowDataIndex];
dataObjSoFar[correspondingHeader] = rowData;
return dataObjSoFar;
}, emptyRowDataObject);
});
const headersOfInterest = ['plat','chan','cat','num'];
printTable(headersOfInterest, transformedData);
function printTable(headers, rowDataWithHeaders) {
let tableHeader = createTableHeaders(headers);
let tableRows = rowDataWithHeaders.map(row => createTableRow(headers, row));
let table = `<table>${ tableHeader }<tbody>${ tableRows }</tbody></table>`;
$("#one").html(table);
}
function createTableHeaders(headers) {
let headersHTML = '<thead><tr>';
headers.forEach(header => {
headersHTML += `<th>${ header }</th>`;
});
headersHTML += '</tr></thead>';
return headersHTML;
}
function createTableRow(headers, dataRow) {
let tr = '<tr>';
// go through all the headers we are interested in
// and get the corresponding value from this data row
headers.forEach(header => {
tr += `<td>${ dataRow[header] }</td>`;
});
tr += '</tr>';
return tr;
}
function getSumOfValuesInColumn(headerName, rowDataWithHeaders) {
// this could be done with Array.reduce as well
let sum = 0;
for(let i = 0; i < rowDataWithHeaders.length; i++) {
sum += rowDataWithHeaders[i][headerName]
}
return sum;
}
function printSummationTable(headersToSum, dataRows) {
const tableHeader = createTableHeaders(headersToSum);
const sumData = {};
headersToSum.forEach(header => {
sumData[header] = getSumOfValuesInColumn(header, dataRows);
});
// treat sumData as a single table row
const tableRows = createTableRow(headersToSum, sumData);
let table = `<table>${ tableHeader }<tbody>${ tableRows }</tbody></table>`;
$("#two").html(table);
}
const headersToSum = ['plat','chan','cat','num'];
printSummationTable(headersToSum, transformedData);
#import url('https://fonts.googleapis.com/css?family=Roboto');
body {
margin: 0;
color:#fff;
font-family: Roboto; }
.row {
display: table;
width: 100%;
height: 241px;
background-color:#454545;
}
.row > .col-lg-6 {
display: table-cell;
vertical-align: middle;
}
.container {
/*display: flex;*/
flex-wrap: wrap;
}
.container > div {
padding: 15px;
margin: 5px;
flex: 0 0 calc(100% - 20px);
text-align: left;
}
/*img {
padding-left: 7%;
max-height:55px;
width:auto;
}*/
td{
padding: 2px 2px;
text-align: center;
margin: 6px 0;
border: none;
}
table{
width: 100%;
background-color:#454545;
font-weight:500;
border-collapse: separate;
border-spacing:0.3em 1.1em;
color: #fff;
border: 0;
}
tr{
font-size: 1.5em;
text-transform:capitalize;
}
tr:nth-child(1) {
color: #CCC;
font-size: 1.5em;
}
#one,#two,#three,#four{
padding-top:2%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h4 style="color:#000;">Table before summing the number for the rows that have the same value in the first three columns</h4>
<div id="one"></div>
<h4 style="color:#000;">Table where we can see the SUM but for all the rows. The first three columns are a concatenation of the values which should not happen.</h4>
<div id="two"></div>
<h4 style="color:#000;">Table that I want to get (static)</h4>
<div><table> <thead> <tr> <th>plat</th> <th>chan</th> <th>cat</th> <th>num</th> </tr></thead> <tbody> <tr> <td>plat1</td><td>chan1</td><td>cat1</td><td>555</td></tr><tr> <td>plat1</td><td>chan1</td><td>cat2</td><td>850</td></tr></tbody></table></div>
I've created the following answer, I hope it meets your needs.
var data = {
"headers":["plat","chan","group","cat","num"],
"rows":[
["plat1","chan1","bbb","cat1",222],
["plat1","chan1","bbb","cat1",333],
["plat1","chan1","bbb","cat2",850]
]
};
function transformData(rows) {
const
rowMap = new Map(),
result = [];
// Iterate over the rows.
rows.forEach(row => {
const
// Create a key, it is the first 4 elements joined together.
key = row.slice(0,4).join();
// Check if the Map has the generated key...
if (rowMap.has(key)) {
// The map has the key, we need to add up the values
const
// Get the value for the current key.
storedRow = rowMap.get(key);
// Add the value of the current row to the row in the map.
storedRow[4] += row[4];
} else {
// The key doens't exist yet, add the row to the map.
rowMap.set(key, row);
}
});
// Iterate over all the entries in the map and push each value with the
// summed up value into the array.
rowMap.forEach(value => {
result.push(value);
});
// Return the array.
return result;
}
// Creates cells for all items in the row array. When no cell type is set
// the method will create td elements.
function getCells(row, element = 'td') {
// Reduce the array to a single string.
return row.reduce((result, cell) => {
result += `<${element}>${cell}</${element}>`;
return result;
}, '');
}
// Creates tr elements for each item in the rows array, each tr element
// will be filled with td elements.
function getBody(rows) {
// Reduce the array to a single string.
return rows.reduce((result, row) => {
result += `<tr>${getCells(row)}</tr>`
return result;
}, '');
}
// Create the HTML table.
function createTable(tableData) {
let
tableHTML = '';
tableHTML = `<table>
<thead><tr>${getCells(tableData.headers, 'th')}</tr></thead>
<tbody>${getBody(tableData.rows)}</tbody>
</table>`;
return tableHTML;
}
data.rows = transformData(data.rows);
const
generateHTML = createTable(data);
$("#two").html(generateHTML);
#import url('https://fonts.googleapis.com/css?family=Roboto');
body {
margin: 0;
color:#fff;
font-family: Roboto; }
.row {
display: table;
width: 100%;
height: 241px;
background-color:#454545;
}
.row > .col-lg-6 {
display: table-cell;
vertical-align: middle;
}
.container {
/*display: flex;*/
flex-wrap: wrap;
}
.container > div {
padding: 15px;
margin: 5px;
flex: 0 0 calc(100% - 20px);
text-align: left;
}
/*img {
padding-left: 7%;
max-height:55px;
width:auto;
}*/
td{
padding: 2px 2px;
text-align: center;
margin: 6px 0;
border: none;
}
table{
width: 100%;
background-color:#454545;
font-weight:500;
border-collapse: separate;
border-spacing:0.3em 1.1em;
color: #fff;
border: 0;
}
tr{
font-size: 1.5em;
text-transform:capitalize;
}
tr:nth-child(1) {
color: #CCC;
font-size: 1.5em;
}
#one,#two,#three,#four{
padding-top:2%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h4 style="color:#000;">Result table (dynamic).</h4>
<div id="two"></div>
<h4 style="color:#000;">Table that I want to get (static)</h4>
<div><table> <thead> <tr> <th>plat</th> <th>chan</th> <th>cat</th> <th>num</th> </tr></thead> <tbody> <tr> <td>plat1</td><td>chan1</td><td>cat1</td><td>555</td></tr><tr> <td>plat1</td><td>chan1</td><td>cat2</td><td>850</td></tr></tbody></table></div>
Here is a partial solution. I hope OP can format transformed data into HTML table. Note that I changed plat1 to plat 1 to make sure it works with spaces in values as well.
const data = {
"headers": ["plat", "chan", "group", "cat", "num"], "rows": [
["plat 1", "chan1", "bbb", "cat1", 222],
["plat 1", "chan1", "bbb", "cat1", 333],
["plat1", "chan1", "bbb", "cat2", 850]]
};
//only "groupby" columns
const headersOfInterest = ['plat', 'chan', 'cat'];//, 'num'];
//aggregation column here
const numInd = data.headers.indexOf('num');
const headInd = [];
//put indeces of headersOfInterest into a separate array
headersOfInterest.forEach(txt => { headInd.push(data.headers.indexOf(txt)); });
//transformed data will go here
const transform = {};
for (let i = 0, row; row = data.rows[i]; ++i) {
let propName = '';
headInd.forEach(val => { propName += row[val]; });
if (!transform[propName]) {//this way groupping works
transform[propName] = [];
headInd.forEach(val => { transform[propName].push(row[val]); });
transform[propName].push(0);
}
transform[propName][transform[propName].length - 1] += row[numInd];
}
console.log(transform);
Nice with so many different approaches. Here's one using (lodash)[https://lodash.com]. I added some console.logs because it can be hard to follow what happens when you chain lodash methods. The whole thing can be viewed here as a JSFiddle demo.
But the only new parts are
function printSummationTable(relevantHeaders, headerToSum, dataRows) {
const tableHeader = createTableHeaders(relevantHeaders);
const collapsedRows =
_(dataRows) // enhance the array, to enable chaining of (lodash) method calls
.groupBy(comparisonStringForColumns(['plat', 'chan', 'cat'])) // results in an array (actually object.. but ignore that) of row groups (arrays)
.map(peekWithMessage('After .groupBy'))
.map(rowGroup => {
// if we know we won't have to support more columns in the future
// we can just copy the first of the groups (since the rows in the group are "duplicate")
// BUT we add the "sum of the group" for the specific num value
const collapsedRow = {...rowGroup[0]}; // check out "spread operator"
collapsedRow.num = getSumOfValuesInColumn(headerToSum, rowGroup);
return collapsedRow;
})
.map(peekWithMessage('After "collapse" step'))
.value();
// treat sumData as a single table row
const tableRows = collapsedRows.map(row => createTableRow(relevantHeaders, row));
let table = `<table>${ tableHeader }<tbody>${ tableRows }</tbody></table>`;
$("#two").html(table);
}
// given a list of headerNames, give back a FUNCTION that
// takes a row an returns a concatinated string
// of the values corresponding to the headerNames
function comparisonStringForColumns(headerNames) {
return function(row) {
// make a string from the the relevant values
// Used for grouping rows that should be considered the "same".
let comparisonString = '';
headerNames.forEach(header => {
comparisonString += row[header];
});
return comparisonString;
}
}
function peekWithMessage(message) {
return function logAndPassThrough(x) {
console.log(message, x);
return x; // to be able to continue chaining
}
}
I have updated code for html and javascript. Please try this.
Let me know if this works for you I will add explanations if needed.
<h4 style="color:#000;">Table before summing the number for the rows that have the same value in the first three columns</h4>
<div id="one"></div>
<h4 style="color:#000;">Table where we can see the SUM but for all the rows. The first three columns are a concatenation of the values which should not happen.</h4>
<div id="two"></div>
<h4 style="color:#000;">Table that I want to get (static)</h4>
<div>
<table>
<thead> </thead>
<tbody> </tbody>
</table></div>
<script type="text/javascript">
var data = {"headers":["plat","chan","group","cat","num"],"rows":[["plat1","chan1","bbb","cat1",222],
["plat1","chan1","bbb","cat1",333],
["plat1","chan1","bbb","cat2",850]]};
createHeader();
createBody();
function createHeader(){
$('thead').append((function(){
var row = '<tr>';
data.headers.forEach(function(element, index){
row +='<th>' +element+'</th>'
})
row += '</tr>'
return row;
})())
}
function createBody(){
var test = {};
data.rows.map(function(element, index){
var count = element.pop();
var content = element.join(':')
if(test[content]){
test[content]= test[content]+count;
}else{
test[content] = count;
}
})
$('tbody').empty().append($.map(test, function(index, element){
var row = '<tr>'
element.split(':').map(function(ele, ind){
row +='<td>'+ele+ '</td>'
})
row += '<td>'+index+ '</td></tr>'
return row;
}))
}
</script>

Checking function for sliding puzzle javascript

I created a sliding puzzle with different formats like: 3x3, 3x4, 4x3 and 4x4. When you run my code you can see on the right side a selection box where you can choose the 4 formats. The slidingpuzzle is almost done. But I need a function which checks after every move if the puzzle is solved and if that is the case it should give out a line like "Congrantulations you solved it!" or "You won!". Any idea how to make that work?
In the javascript code you can see the first function loadFunc() is to replace every piece with the blank one and the functions after that are to select a format and change the format into it. The function Shiftpuzzlepieces makes it so that you can move each piece into the blank space. Function shuffle randomizes every pieces position. If you have any more question or understanding issues just feel free to ask in the comments. Many thanks in advance.
Since I don't have enough reputation I will post a link to the images here: http://imgur.com/a/2nMlt . These images are just placeholders right now.
Here is the jsfiddle:
http://jsfiddle.net/Cuttingtheaces/vkyxgwo6/19/
As always, there is a "hacky", easy way to do this, and then there is more elegant but one that requires significant changes to your code.
Hacky way
To accomplish this as fast and dirty as possible, I would go with parsing id-s of pieces to check if they are in correct order, because they have this handy pattern "position" + it's expected index or "blank":
function isFinished() {
var puzzleEl = document.getElementById('slidingpuzzleContainer').children[0];
// convert a live list of child elements into regular array
var pieces = [].slice.call(puzzleEl.children);
return pieces
.map(function (piece) {
return piece.id.substr(8); // strip "position" prefix
})
.every(function (id, index, arr) {
if (arr.length - 1 == index) {
// last peace, check if it's blank
return id == "blank";
}
// check that every piece has an index that matches its expected position
return index == parseInt(id);
});
}
Now we need to check it somewhere, and naturally the best place would be after each move, so shiftPuzzlepieces() should be updated to call isFinished() function, and show the finishing message if it returns true:
function shiftPuzzlepieces(el) {
// ...
if (isFinished()) {
alert("You won!");
}
}
And voilà: live version.
How would I implement this game
For me, the proper way of implementing this would be to track current positions of pieces in some data structure and check it in similar way, but without traversing DOM or checking node's id-s. Also, it would allow to implement something like React.js application: onclick handler would mutate current game's state and then just render it into the DOM.
Here how I would implement the game:
/**
* Provides an initial state of the game
* with default size 4x4
*/
function initialState() {
return {
x: 4,
y: 4,
started: false,
finished: false
};
}
/**
* Inits a game
*/
function initGame() {
var gameContainer = document.querySelector("#slidingpuzzleContainer");
var gameState = initialState();
initFormatControl(gameContainer, gameState);
initGameControls(gameContainer, gameState);
// kick-off rendering
render(gameContainer, gameState);
}
/**
* Handles clicks on the container element
*/
function initGameControls(gameContainer, gameState) {
gameContainer.addEventListener("click", function hanldeClick(event) {
if (!gameState.started || gameState.finished) {
// game didn't started yet or already finished, ignore clicks
return;
}
if (event.target.className.indexOf("piece") == -1) {
// click somewhere not on the piece (like, margins between them)
return;
}
// try to move piece somewhere
movePiece(gameState, parseInt(event.target.dataset.index));
// check if we're done here
checkFinish(gameState);
// render the state of game
render(gameContainer, gameState);
event.stopPropagation();
return false;
});
}
/**
* Checks whether game is finished
*/
function checkFinish(gameState) {
gameState.finished = gameState.pieces.every(function(id, index, arr) {
if (arr.length - 1 == index) {
// last peace, check if it's blank
return id == "blank";
}
// check that every piece has an index that matches its expected position
return index == id;
});
}
/**
* Moves target piece around if there's blank somewhere near it
*/
function movePiece(gameState, targetIndex) {
if (isBlank(targetIndex)) {
// ignore clicks on the "blank" piece
return;
}
var blankPiece = findBlankAround();
if (blankPiece == null) {
// nowhere to go :(
return;
}
swap(targetIndex, blankPiece);
function findBlankAround() {
var up = targetIndex - gameState.x;
if (targetIndex >= gameState.x && isBlank(up)) {
return up;
}
var down = targetIndex + gameState.x;
if (targetIndex < ((gameState.y - 1) * gameState.x) && isBlank(down)) {
return down;
}
var left = targetIndex - 1;
if ((targetIndex % gameState.x) > 0 && isBlank(left)) {
return left;
}
var right = targetIndex + 1;
if ((targetIndex % gameState.x) < (gameState.x - 1) && isBlank(right)) {
return right;
}
}
function isBlank(index) {
return gameState.pieces[index] == "blank";
}
function swap(i1, i2) {
var t = gameState.pieces[i1];
gameState.pieces[i1] = gameState.pieces[i2];
gameState.pieces[i2] = t;
}
}
/**
* Handles form for selecting and starting the game
*/
function initFormatControl(gameContainer, state) {
var formatContainer = document.querySelector("#formatContainer");
var formatSelect = formatContainer.querySelector("select");
var formatApply = formatContainer.querySelector("button");
formatSelect.addEventListener("change", function(event) {
formatApply.disabled = false;
});
formatContainer.addEventListener("submit", function(event) {
var rawValue = event.target.format.value;
var value = rawValue.split("x");
// update state
state.x = parseInt(value[0], 10);
state.y = parseInt(value[1], 10);
state.started = true;
state.pieces = generatePuzzle(state.x * state.y);
// render game
render(gameContainer, state);
event.preventDefault();
return false;
});
}
/**
* Renders game's state into container element
*/
function render(container, state) {
var numberOfPieces = state.x * state.y;
updateClass(container, state.x, state.y);
clear(container);
var containerHTML = "";
if (!state.started) {
for (var i = 0; i < numberOfPieces; i++) {
containerHTML += renderPiece("", i) + "\n";
}
} else if (state.finished) {
containerHTML = "<div class='congratulation'><h2 >You won!</h2><p>Press 'Play!' to start again.</p></div>";
} else {
containerHTML = state.pieces.map(renderPiece).join("\n");
}
container.innerHTML = containerHTML;
function renderPiece(id, index) {
return "<div class='piece' data-index='" + index + "'>" + id + "</div>";
}
function updateClass(container, x, y) {
container.className = "slidingpuzzleContainer" + x + "x" + y;
}
function clear(container) {
container.innerHTML = "";
}
}
/**
* Generates a shuffled array of id-s ready to be rendered
*/
function generatePuzzle(n) {
var pieces = ["blank"];
for (var i = 0; i < n - 1; i++) {
pieces.push(i);
}
return shuffleArray(pieces);
function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
}
body {
font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Helvetica, Arial, sans-serif;
font-size: 12px;
color: #000;
}
#formatContainer {
position: absolute;
top: 50px;
left: 500px;
}
#formatContainer label {
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
}
#formatContainer select {
display: block;
width: 100%;
margin-top: 10px;
margin-bottom: 10px;
}
#formatContainer button {
display: inline-block;
width: 100%;
}
.piece {
width: 96px;
height: 96px;
margin: 1px;
float: left;
border: 1px solid black;
}
.slidingpuzzleContainer3x3,
.slidingpuzzleContainer3x4,
.slidingpuzzleContainer4x3,
.slidingpuzzleContainer4x4 {
position: absolute;
top: 50px;
left: 50px;
border: 10px solid black;
}
.slidingpuzzleContainer3x3 {
width: 300px;
height: 300px;
}
.slidingpuzzleContainer3x4 {
width: 300px;
height: 400px;
}
.slidingpuzzleContainer4x3 {
width: 400px;
height: 300px;
}
.slidingpuzzleContainer4x4 {
width: 400px;
height: 400px;
}
.congratulation {
margin: 10px;
}
}
<body onload="initGame();">
<div id="slidingpuzzleContainer"></div>
<form id="formatContainer">
<label for="format">select format:</label>
<select name="format" id="format" size="1">
<option value="" selected="true" disabled="true"></option>
<option value="3x3">Format 3 x 3</option>
<option value="3x4">Format 3 x 4</option>
<option value="4x3">Format 4 x 3</option>
<option value="4x4">Format 4 x 4</option>
</select>
<button type="submit" disabled="true">Play!</button>
</form>
</body>
Here we have the initGame() function that starts everything. When called it will create an initial state of the game (we have default size and state properties to care about there), add listeners on the controls and call render() function with the current state.
initGameControls() sets up a listener for clicks on the field that will 1) call movePiece() which will try to move clicked piece on the blank spot if the former is somewhere around, 2) check if after move game is finished with checkFinish(), 3) call render() with updated state.
Now render() is a pretty simple function: it just gets the state and updates the DOM on the page accordingly.
Utility function initFormatControl() handles clicks and updates on the form for field size selection, and when the 'Play!' button is pressed will generate initial order of the pieces on the field and call render() with new state.
The main benefit of this approach is that almost all functions are decoupled from one another: you can tweak logic for finding blank space around target piece, to allow, for example, to swap pieces with adjacent ids, and even then functions for rendering, initialization and click handling will stay the same.
$(document).on('click','.puzzlepiece', function(){
var count = 0;
var imgarray = [];
var test =[0,1,2,3,4,5,6,7,8,'blank']
$('#slidingpuzzleContainer img').each(function(i){
var imgalt = $(this).attr('alt');
imgarray[i] = imgalt;
count++;
});
var is_same = (imgarray.length == test.length) && imgarray.every(function(element, index) {
return element === array2[index];
});
console.log(is_same); ///it will true if two array is same
});
try this... this is for only 3*3.. you pass the parameter and makethe array value as dynamically..

Categories