Я разбил свое решение на некоторый псевдокод:
get_table(data):
# Map resulting rows with "tr" elements
return recurse_dataset.map(row => "<tr>row</tr>")
# Return list of rows (only "td" elements)
recurse_dataset(data):
if data is root:
return data
else:
# Get all child rows and add to a result
result = []
for key, value in data:
roots = number_roots(value)
# Recurse further into object to get root rows
for index, row in recurse_dataset(value):
# If we are the first result and we have children,
# add our key cell with overlap
if index == 0 and roots > 0:
row = "<td rowspan=roots>key</td>" + row
result += row
return result
number_roots(data):
if data is root:
return 1
else:
count = 0
for value in data:
count += number_roots(value)
return count
Или, во фрагменте:
const getTable = function(data) {
return getRows(data).map(row => `<tr>${row}</tr>`).join("");
};
const getRows = function(data) {
if(typeof data !== "object") {
return [
`<td>${data}</td>`
];
}
else {
let result = [];
for(let [key, value] of Object.entries(data)) {
let children = getChildrenCount(value);
let rows = getRows(value);
rows.forEach((row, index) => {
if(index == 0 && children) {
row = `<td rowspan="${children}">${key}</td>
${row}`;
}
result.push(row);
});
};
return result;
}
};
const getChildrenCount = function(data) {
if(typeof data !== "object") {
return 1;
}
else {
let count = 0;
for(let [key, value] of Object.entries(data)) {
count += getChildrenCount(value);
};
return count;
}
};
let data = {
A: {
B1: {
C1: {
D1: "foo"
},
C2: {
D2: "bar",
D3: "baz"
}
},
B2: {
C3: {
D4: "qux"
},
C4: {
D5: "quux"
}
}
}
};
$("tbody").append(getTable(data));
table td,
table th {
min-width: 2em;
min-height: 2em;
border: 1px solid #000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<table>
<tbody></tbody>
</table>
И если вам нужна строка «итоговая», которая показывает, сколько элементов в каждой группе:
const getTable = function(data) {
return getRows(data).map(row => `<tr>${row}</tr>`).join("");
};
const getRows = function(data) {
if(typeof data !== "object") {
return [
`<td>${data}</td>`
];
}
else {
let result = [];
for(let [key, value] of Object.entries(data)) {
let children = getChildrenCount(value);
let depth = getDepth(value);
let rows = getRows(value);
rows.forEach((row, index) => {
if(index == 0 && children) {
result.push(`<td rowspan="${children + getExtraSpan(value)}">${key}</td>${row}`);
}
else {
result.push(row);
}
});
if(depth) {
result.push(`<td ${depth ? `colspan="${depth}"` : ""}>Count</td><td>${children}</td>`);
}
};
return result;
}
};
const getDepth = function(data) {
if(typeof data !== "object") {
return 0;
}
else {
let count = 0;
for(let [key, value] of Object.entries(data)) {
count = Math.max(count, getDepth(value));
};
return count + 1;
}
};
const getChildrenCount = function(data) {
if(typeof data !== "object") {
return 1;
}
else {
let count = 0;
for(let [key, value] of Object.entries(data)) {
count += getChildrenCount(value);
};
return count;
}
};
const getExtraSpan = function(data) {
if(typeof data !== "object") {
return 0;
}
else {
let count = getDepth(data) ? 1 : 0;
for(let [key, value] of Object.entries(data)) {
count += getExtraSpan(value);
};
return count;
}
}
let data = {
A: {
B1: {
C1: {
D1: "foo"
},
C2: {
D2: "bar",
D3: "baz"
}
},
B2: {
C3: {
D4: "qux"
},
C4: {
D5: "quux"
}
}
}
};
$("tbody").append(getTable(data));
table td,
table th {
min-width: 2em;
min-height: 2em;
border: 1px solid #000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<table>
<tbody></tbody>
</table>