Reputation: 1485
i am building a vue js application and i am trying to build an html table out of data like this.
{Header}:
{
{Subheader}: {
2021-05-26 00:09: [1, 2, 3]
2021-05-26 00:13: [10]
2021-05-26 00:16: [6]
{Header}
{Subheader}: {
2021-05-26 00:09: [2, 6, 1]
2021-05-26 00:13: [50]
2021-05-26 00:16: [10]
{Header}
{Subheader}: {
2021-05-26 00:09: [4]
2021-05-26 00:13: [5, 5, 8]
2021-05-26 00:16: [4]
...
Now the header and subheader can be anything thats why i put it in parentheses, the trick is to display the headers and then colspan it so it can match the lenght of its own subheaders and finally on each row put the corresponding item from Subheader values so it will be something like this according the example above
| Header | Header |
|Subheader|Subheader|Subheader|
| 1 | 2 | 4 |
| 2 | 6 | 5 |
| 3 | 1 | 5 |
| 10 | 50 | 8 |
| 6 | 10 | 4 |
Here's my vue.js code so far
<template>
<div ref="main" class="relative h-full pt-10 analytics">
<table class="w-full" v-if="fullCycle">
<thead>
<tr>
<th class="border" :colspan="Object.values(header).length" v-for="(header, i) in fullCycle" :key="i">
{{ i }}
</th>
</tr>
<tr>
<th class="border" v-for="(item, index) in getSubValues(fullCycle)" :key="index">
{{ item }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, i) of getThirdLevel()" :key="i">
<td class="border" v-for="(val, index) in item" :key="index">{{ val }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
data() {
return {
fullCycle: null,
};
},
mounted() {
this.init();
},
methods: {
async init() {
await this.fetchData();
},
async fetchData() {
try {
const res = await this.$axios.get('/redacted/cool-api');
this.fullCycle = res.data;
} catch (err) {
console.log(err);
}
},
getSubValues(obj) {
const values = [];
for (const item of Object.keys(obj)) {
for (const index in obj[item]) {
values.push(index);
}
}
return values;
},
getThirdLevel() {
const obj = this.fullCycle;
const levelTwoLength = this.getSubValues(obj).length;
const values = [];
for (const item of Object.keys(obj)) {
for (const index of Object.keys(obj[item])) {
values.push(Object.values(obj[item][index]));
}
}
let max = 0;
for (const item of values) {
if (max < item.length) max = item.length;
}
let iterator = 0;
let finalArray = [];
for (const item of values) {
for (let i = 0; i < max; i++) {
console.log(item[i]);
if (item[i]) {
for (let val of item[i]) {
if (!finalArray[iterator]) finalArray[iterator] = [];
finalArray[iterator].push(val ? val : '');
if (finalArray[iterator].length === levelTwoLength) iterator++;
}
} else {
if (!finalArray[iterator]) finalArray[iterator] = [];
finalArray[iterator].push('');
if (finalArray[iterator].length === levelTwoLength) iterator++;
}
}
}
return finalArray;
},
},
};
</script>
Now the code above somewhat works, but the issue is since there can be many items per day it puts them side by side instead of top to bottom. The finalArray
is a newly created multidimensional array which is built by looping through every value of each subheader and limiting it by total subheader length, basically pushing it into a new one. Again each header,subheader, and date can be different. Each header can have multiple subheaders
Thank you
Upvotes: 1
Views: 810
Reputation: 8329
I don't exactly get your example, but I assume that's something like this:
const data = {
"header_1_1": {
"subheader_1_1_1": {
"2021-05-26 00:09": [1, 2, 3],
"2021-05-26 00:13": [10],
"2021-05-26 00:16": [6],
},
"subheader_1_1_2": {
"2021-05-26 00:09": [2, 6, 1],
"2021-05-26 00:13": [50],
"2021-05-26 00:16": [10],
},
},
"header_1_2": {
"subheader_1_2_1": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
}
}
I think it's easier to create the rows that the table will have, so let's start with that:
// sample data
const data = {
"header_1_1": {
"subheader_1_1_1": {
"2021-05-26 00:09": [1, 2, 3],
"2021-05-26 00:13": [10],
"2021-05-26 00:16": [6],
},
"subheader_1_1_2": {
"2021-05-26 00:09": [2, 6, 1],
"2021-05-26 00:13": [50],
"2021-05-26 00:16": [10],
},
},
"header_1_2": {
"subheader_1_2_1": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
},
}
// reading the tree structure
const readTree = (arr) => {
let ret = []
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
ret = [...ret, ...arr[i]]
} else {
ret = [...ret, readTree(Object.values(arr[i]))]
}
}
return ret
}
// creating rows from columns
const createRows = (columns) => {
let ret = []
for (let i = 0; i < columns[0].length; i++) {
if (typeof ret[i] === "undefined") ret[i] = []
for (let j = 0; j < columns.length; j++) {
ret[i].push(columns[j][i])
}
}
return ret
}
const columns = readTree(Object.values(data)).flat()
const rows = createRows(columns)
console.log("result of readTree:", columns)
console.log("result of createRows:", rows)
// creating & adding row HTML
const tbody = document.getElementById("tbody")
const createRowHtml = (data) => {
let html = ''
data.forEach(row => {
html += '<tr>'
row.forEach(cell => {
html += `<td>${cell}</td>`
})
html += '</tr>'
})
return html
}
tbody.innerHTML = createRowHtml(rows)
<table id="table">
<tbody id="tbody">
</tbody>
</table>
Now, that we have the rows, let's deal with the headers.
// sample data
const data = {
"header_1_1": {
"subheader_1_1_1": {
"2021-05-26 00:09": [1, 2, 3],
"2021-05-26 00:13": [10],
"2021-05-26 00:16": [6],
},
"subheader_1_1_2": {
"2021-05-26 00:09": [2, 6, 1],
"2021-05-26 00:13": [50],
"2021-05-26 00:16": [10],
},
},
"header_1_2": {
"subheader_1_2_1": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
},
}
// reading the tree structure for headers
const readHeaders = (obj) => {
let ret = []
ret = [...ret, ...Object.entries(obj).map(([key, val]) => {
const a = readHeaders(val)
return {
text: key,
items: a.filter(({
items
}) => !items.length),
colspan: a.length || 0,
}
})]
return ret
}
const reduceHeaders = (a, c, i) => {
const {
text,
colspan
} = c
if (typeof a[i] === "undefined") a[i] = []
if (c.items.length) {
a[i + 1] = [...(a[i + 1] || []), ...c.items.reduce((a, c) => reduceHeaders(a, c, i + 1), []).flat()]
}
a[i].push({
text,
colspan
})
return a
}
const headers = readHeaders(data)
const reducedHeaders = headers.reduce((a, c) => {
return reduceHeaders(a, c, 0)
}, [])
console.log("headers:", headers)
console.log("reducedHeaders:", reducedHeaders)
// creating & adding header HTML
const thead = document.getElementById("thead")
const createHeaderRowHtml = (data) => {
let html = ''
data.forEach((row, i) => {
html += '<tr>'
row.forEach(cell => {
html += `<th colspan="${ i < data.length - 1 ? cell.colspan : 1 }">${cell.text}</th>`
})
html += '</tr>'
})
return html
}
thead.innerHTML = createHeaderRowHtml(reducedHeaders)
table {
border-collapse: collapse;
}
table,
tr,
th,
td {
border: 1px solid black;
}
<table id="table">
<thead id="thead">
</thead>
</table>
So, there are the headers.
(Using a modified dataset to show flexibility):
// sample data
const data = {
"header_1_1": {
"subheader_1_1_1": {
"2021-05-26 00:09": [1, 2, 3],
"2021-05-26 00:13": [10],
"2021-05-26 00:16": [6],
},
"subheader_1_1_2": {
"2021-05-26 00:09": [2, 6, 1],
"2021-05-26 00:13": [50],
"2021-05-26 00:16": [10],
},
"subheader_1_1_3": {
"2021-05-26 00:09": [2, 6, 1],
"2021-05-26 00:13": [50],
"2021-05-26 00:16": [10],
},
},
"header_1_2": {
"subheader_1_2_1": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
},
"header_1_3": {
"subheader_1_3_1": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
"subheader_1_3_2": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
},
}
// reading the tree structure
const readTree = (arr) => {
let ret = []
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
ret = [...ret, ...arr[i]]
} else {
ret = [...ret, readTree(Object.values(arr[i]))]
}
}
return ret
}
// creating rows from columns
const createRows = (columns) => {
let ret = []
for (let i = 0; i < columns[0].length; i++) {
if (typeof ret[i] === "undefined") ret[i] = []
for (let j = 0; j < columns.length; j++) {
ret[i].push(columns[j][i])
}
}
return ret
}
const columns = readTree(Object.values(data)).flat()
const rows = createRows(columns)
// creating & adding row HTML
const tbody = document.getElementById("tbody")
const createRowHtml = (data) => {
let html = ''
data.forEach(row => {
html += '<tr>'
row.forEach(cell => {
html += `<td>${cell}</td>`
})
html += '</tr>'
})
return html
}
tbody.innerHTML = createRowHtml(rows)
// reading the tree structure for headers
const readHeaders = (obj) => {
let ret = []
ret = [...ret, ...Object.entries(obj).map(([key, val]) => {
const a = readHeaders(val)
return {
text: key,
items: a.filter(({
items
}) => !items.length),
colspan: a.length || 0,
}
})]
return ret
}
const reduceHeaders = (a, c, i) => {
const {
text,
colspan
} = c
if (typeof a[i] === "undefined") a[i] = []
if (c.items.length) {
a[i + 1] = [...(a[i + 1] || []), ...c.items.reduce((a, c) => reduceHeaders(a, c, i + 1), []).flat()]
}
a[i].push({
text,
colspan
})
return a
}
const headers = readHeaders(data)
const reducedHeaders = headers.reduce((a, c) => {
return reduceHeaders(a, c, 0)
}, [])
// creating & adding header HTML
const thead = document.getElementById("thead")
const createHeaderRowHtml = (data) => {
let html = ''
data.forEach((row, i) => {
html += '<tr>'
row.forEach(cell => {
html += `<th colspan="${ i < data.length - 1 ? cell.colspan : 1 }">${cell.text}</th>`
})
html += '</tr>'
})
return html
}
thead.innerHTML = createHeaderRowHtml(reducedHeaders)
table {
border-collapse: collapse;
}
table,
tr,
th,
td {
border: 1px solid black;
}
<table id="table">
<thead id="thead">
</thead>
<tbody id="tbody">
</tbody>
</table>
As you can see, this is a quite complex solution. It could be simplified by unifying the header & row HTML creation functions, also made more efficient if all the functions for the header & the items would be done in one pass. It's not totally flexible (doesn't handle arbitrary depth of the source data) & prone to error if the datasets under the subheaders are not of the same length. But the overall concept is quite nice - it was worth unfolding (at this level), even if you've already managed to solve the question. :)
// mock dataset large
const initData = () => ({
"header_1_1": {
"subheader_1_1_1": {
"2021-05-26 00:09": [1, 2, 3],
"2021-05-26 00:13": [10],
"2021-05-26 00:16": [6],
},
"subheader_1_1_2": {
"2021-05-26 00:09": [2, 6, 1],
"2021-05-26 00:13": [50],
"2021-05-26 00:16": [10],
},
"subheader_1_1_3": {
"2021-05-26 00:09": [2, 6, 1],
"2021-05-26 00:13": [50],
"2021-05-26 00:16": [10],
},
},
"header_1_2": {
"subheader_1_2_1": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
},
"header_1_3": {
"subheader_1_3_1": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
"subheader_1_3_2": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
},
})
// mock dataset short
const initDataShort = () => ({
"header_1_1": {
"subheader_1_1_1": {
"2021-05-26 00:09": [1, 2, 3],
"2021-05-26 00:13": [10],
"2021-05-26 00:16": [6],
},
"subheader_1_1_2": {
"2021-05-26 00:09": [2, 6, 1],
"2021-05-26 00:13": [50],
"2021-05-26 00:16": [10],
},
},
"header_1_2": {
"subheader_1_2_1": {
"2021-05-26 00:09": [4],
"2021-05-26 00:13": [5, 5, 8],
"2021-05-26 00:16": [4],
},
},
})
// utility function: transpose
const transpose = (arr) => {
return arr[0].map((_, colIndex) => arr.map(row => row[colIndex]));
}
// rendering the cell: using a render
// function, so the "th" or "td" can
// be passed down dynamically from the
// parent component
Vue.component("TableCell", {
props: ["colname", "tag"],
render(h) {
return h(this.tag, this.colname)
},
})
// header row
Vue.component("HeaderRow", {
props: ["rowData"],
template: `
<tr>
<table-cell
v-for="(cell, i) in rowData"
:key="i"
:colname="cell.colname"
:colspan="cell.colspan"
:tag="'th'"
/>
</tr>
`
})
// body row
Vue.component("BodyRow", {
props: ["rowData"],
template: `
<tr>
<table-cell
v-for="(cell, i) in rowData"
:key="i"
:colname="cell"
:tag="'td'"
/>
</tr>
`
})
new Vue({
el: "#app",
data() {
return {
tableData: initData(),
dataSource: "long",
}
},
computed: {
// mapped data
mapped() {
return Object.entries(this.tableData).map(([key, val]) => this.mapLeafs(val, key))
},
// header rows of the table
headers() {
return this.createHeaders(this.mapped)
},
// body rows of the table
items() {
return transpose(this.createColumns(this.mapped))
},
},
methods: {
// just to help feel the flexibility
onChangeDataSource() {
if (this.dataSource === "short") {
this.tableData = initData()
this.dataSource = "long"
} else {
this.tableData = initDataShort()
this.dataSource = "short"
}
},
// processing/mapping array of values to subheaders
mapLeafs(obj, key) {
let acc = []
const mapped = Object.entries(obj).map(([key, val]) => {
if (Array.isArray(val)) acc = [...acc, ...val]
return this.mapLeafs(val, key)
})
return {
colname: key,
colspan: acc.length ? 1 : mapped.length,
subheaders: acc.length ? [] : mapped,
values: acc,
}
},
// creating headers rows from the processed (mapped) data
createHeaders(obj, d = 0, ret = []) {
obj.forEach(({
colname,
colspan,
subheaders
}) => {
if (!ret[d]) ret[d] = []
ret[d].push({
colname,
colspan,
})
if (subheaders.length) {
this.createHeaders(subheaders, d + 1, ret)
}
})
return ret
},
// creating body rows from the processed (mapped) data
createColumns(obj, d = 0, ret = []) {
obj.forEach(({
values,
subheaders
}) => {
if (!values.length) {
this.createColumns(subheaders, d + 1, ret)
} else {
ret.push(values)
}
})
return ret
}
},
template: `
<div>
<div>
<button
@click="onChangeDataSource"
>
SWITCH DATA SOURCE
</button>
</div>
<div
class="table-container"
>
<table>
<thead>
<header-row
v-for="(row, i) in headers"
:key="i"
:row-data="row"
/>
</thead>
<tbody>
<body-row
v-for="(row, i) in items"
:key="i"
:row-data="row"
/>
</tbody>
</table>
</div>
</div>
`
})
html,
body {
margin: 0;
body: 0;
}
table {
border-collapse: collapse;
}
table,
tr,
th,
td {
border: 1px solid black;
}
.table-container {
padding: 8px 16px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
Upvotes: 1
Reputation: 1485
After sleepless nights of dark magic i managed to reconstruct the initial array and return it inside the method. Look for comments to see details.
getThirdLevel() {
// Our initial object
const obj = this.fullCycle;
// An empty object to reconstruct so it will have first level as second level and each value of the fourth one pushed into it
const reconstruct = [];
// Iteration for level two, so we can determine where the value of the fourth one will go
let levelTwoIteration = 0;
// Level one
for (const indexOne of Object.keys(obj)) {
// Level two
for (const indexTwo of Object.keys(obj[indexOne])) {
reconstruct[levelTwoIteration] = [];
// Sorted level three because Object.keys break the order for some reason
const sortedLevelThree = Object.keys(obj[indexOne][indexTwo]).sort((a, b) => new Date(b) - new Date(a));
for (const indexThree of sortedLevelThree) {
// Level four (value)
for (const value of Object.values(obj[indexOne][indexTwo][indexThree])) {
// We only need level two length and level three's values
reconstruct[levelTwoIteration].push(new String(value));
}
}
levelTwoIteration++;
}
}
// Determine the height of the matrix based on the maximum amount of items in the second level of new reconstructed array
let height = 0;
for (const item in reconstruct) {
if (height < Object.values(reconstruct[item]).length) height = Object.values(reconstruct[item]).length;
}
// Create 1D Matrix only height
let matrix = [];
for (let i = 0; i < height; i++) {
if (!matrix[i]) matrix[i] = [];
}
for (const posX in reconstruct) {
for (const posY in reconstruct[posX]) {
matrix[posY][posX] = reconstruct[posX][posY];
}
}
return matrix;
}
So i basically took each subheader into an array and pushed every value inside date values into new subheader array according to that subheaders iteration.
Original
{Header}:
{
{Subheader}: {
2021-05-26 00:09: [1, 2, 3]
2021-05-26 00:13: [10]
2021-05-26 00:16: [6]
{Header}
{Subheader}: {
2021-05-26 00:09: [2, 6, 1]
2021-05-26 00:13: [50]
2021-05-26 00:16: [10]
{Header}
{Subheader}: {
2021-05-26 00:09: [4]
2021-05-26 00:13: [5, 5, 8]
2021-05-26 00:16: [4]
reconstruct
variable
{Subheader}: [
1, 2, 3, 10, 6
{Subheader}: [
2, 6, 1, 50, 10
{Subheader}: [
4, 5, 5, 8, 4
Then i initialized 1 dimensional matrix and its length is based on which subheader has the most items, so later on we can easly put each one into its place.
|1|
|2|
|3|
|4|
|5|
Then i just loop into the reconstruct array and the first loop iteration is the position where on Y axis it should be on matrix which is the subheader, and then i loop into the values and get the iteration number of the value, and finally i assign that [x][y] position its value. And after all this mess i return the matrix to the v-for
and loop into them to display.
Upvotes: 0