function boxmodel() {
let isContainer,
spanHeight,
edgeMargins,
vAlign;
let padding,
margin,
minContainerSize,
maxLineWidth,
nodeSize;
const lineMap = [];
function compute(root) {
root.eachAfter(scaleNode);
root.eachBefore(scaleToParent);
root.eachBefore(positionNode);
return root;
}
compute.vAlign = function(x) {
return arguments.length ? (vAlign = x, compute) : vAlign;
};
compute.edgeMargins = function(x) {
return arguments.length ? (edgeMargins = typeof x === 'function' ? x : constant(+x), compute) : edgeMargins;
};
compute.isContainer = function(x) {
return arguments.length ? (isContainer = typeof x === 'function' ? x : constant(+x), compute) : isContainer;
};
compute.spanHeight = function(x) {
return arguments.length ? (spanHeight = typeof x === 'function' ? x : constant(+x), compute) : spanHeight;
};
compute.padding = function(x) {
return arguments.length ? (padding = typeof x === 'function' ? x : constant(+x), compute) : padding;
};
compute.margin = function(x) {
return arguments.length ? (margin = typeof x === 'function' ? x : constant(+x), compute) : margin;
};
compute.nodeSize = function(x) {
return arguments.length ? (nodeSize = typeof x === 'function' ? x : constant(+x), compute) : nodeSize;
};
compute.minContainerSize = function(x) {
return arguments.length ? (minContainerSize = typeof x === 'function' ? x : constant(+x), compute) : minContainerSize;
};
compute.maxLineWidth = function(x) {
return arguments.length ? (maxLineWidth = typeof x === 'function' ? x : constant(+x), compute) : maxLineWidth;
};
function scaleNode(node) {
let w = nodeSize(node).width, h = nodeSize(node).height;
if (isContainer(node)) {
w = h = 0;
if (node.children) {
const lines = generateLines(node);
for (let l = 0; l < lines.length; l++) {
lines[l].height = calcLineHeight(node,lines,l);
}
lineMap.push({box: node, lines: lines});
w += d3.max(lines, l => l.width);
h += d3.sum(lines, l => l.height);
}
w += padding(node).left + padding(node).right;
h += padding(node).top + padding(node).bottom;
w = Math.max(w, minContainerSize(node).width);
h = Math.max(h, minContainerSize(node).height);
}
node.x0 = node.y0 = 0;
node.x1 = w, node.y1 = h;
}
function scaleToParent(node) {
let h = node.y1;
if (node.parent && spanHeight(node)) {
h = getOwnLine(node).height;
const parentLines = getLines(node.parent);
const lineIndex = getLineIndex(node, parentLines);
h -= !edgeMargins(node) && lineIndex === 0 ? 0 : margin(node).top;
h -= !edgeMargins(node) && lineIndex === (parentLines.length-1) ? 0 : margin(node).bottom;
const heightDiff = h - node.y1;
if (isContainer(node) && node.children && heightDiff > 0) {
const lines = getLines(node);
const excess = heightDiff / lines.length;
for (const line of lines) {
line.height += excess;
}
}
}
node.y1 = h;
}
function positionNode(node) {
const w = node.x1 - node.x0;
const h = node.y1 - node.y0;
if (node.parent) {
node.y0 = node.parent.y0 + padding(node.parent).top;
const order = node.parent.children.indexOf(node);
if (order === 0 || lineBreak(node)) {
node.x0 += node.parent.x0 + padding(node.parent).left;
if (edgeMargins(node)) node.x0 += margin(node).left;
}
else {
const neighbourLeft = node.parent.children[order-1];
node.x0 = neighbourLeft.x1;
node.x0 += Math.max( margin(neighbourLeft).right, margin(node).left );
}
}
else {
switch (vAlign) {
case 'top':
node.y0 = 0;
break;
case 'middle':
node.y0 = h/2;
break;
case 'bottom':
node.y0 = h;
break;
}
}
switch (vAlign) {
case 'top':
if (node.parent) {
const lineIndex = getLineIndex(node);
node.y0 += !edgeMargins(node) && lineIndex === 0 ? 0 : margin(node).top;
node.y0 += calcLineShift(node);
}
break;
case 'middle':
if (node.parent) node.y0 += calcLineShift(node) + getOwnLine(node).height/2;
node.y0 -= h/2;
break;
case 'bottom':
if (node.parent) {
const lines = getLines(node.parent), lineIndex = getLineIndex(node, lines);
node.y0 -= !edgeMargins(node) && lineIndex === (lines.length-1) ? 0 : margin(node).bottom;
node.y0 += calcLineShift(node, true);
}
node.y0 -= h;
break;
}
node.x1 = node.x0 + w;
node.y1 = node.y0 + h;
}
function generateLines(node) {
const lines = [];
let lineWidth = 0, flexHeight = false, startIndex = 0, newLine = true;
node.children.forEach( (child,i) => {
if (spanHeight(child) && !flexHeight) flexHeight = true;
lineWidth += (child.x1 - child.x0);
lineWidth += newLine ? (edgeMargins(child) ? margin(child).left : 0) :
Math.max(margin(child).left, margin(node.children[i-1]).right);
const marginRight = edgeMargins(child) ? margin(child).right : 0;
if (lineWidth + marginRight > maxLineWidth(node) || i === node.children.length-1)
lineWidth += marginRight;
if (lineWidth > maxLineWidth(node) || i === node.children.length-1) {
lines.push({from: startIndex, to: i, width: lineWidth, flexHeight: flexHeight});
if (i < node.children.length-1) startIndex = i+1, lineWidth = 0, flexHeight = false, newLine = true;
}
else newLine = false;
});
return lines;
}
function calcLineHeight(node, lines, lineIndex) {
const line = lines[lineIndex];
let lineHeight = 0;
for (let i = line.from; i <= line.to; i++) {
const child = node.children[i];
const childH = child.y1 - child.y0;
const marginsVert = (!edgeMargins(child) && lineIndex===0 ? 0 :
margin(child).top) +
(!edgeMargins(child) && lineIndex===(lines.length-1) ? 0 :
margin(child).bottom);
if (childH + marginsVert > lineHeight) lineHeight = childH + marginsVert;
}
return Math.max(lineHeight, minContainerSize(node).height);
}
function getLines(node) {
return lineMap[lineMap.findIndex(m => m.box === node)].lines;
}
function getLineIndex(node, parentLines) {
if (node.parent) {
const lines = (arguments.length > 1) ? parentLines : getLines(node.parent);
const index = node.parent.children.indexOf(node);
return lines.findIndex(l => { return (index >= l.from) && (index <= l.to); });
}
return null;
}
function getOwnLine(node) {
const lines = getLines(node.parent);
const lineIndex = getLineIndex(node, lines);
return lines[lineIndex];
}
function calcLineShift(node, include = false) {
if (node.parent) {
const lines = getLines(node.parent);
const lineIndex = getLineIndex(node, lines);
const lineTo = include ? lineIndex : lineIndex-1;
return d3.sum(lines.filter( (l,i) => (i <= lineTo) ), l => l.height);
}
return null;
}
function lineBreak(node) {
if (node.parent) {
const index = node.parent.children.indexOf(node);
const lines = getLines(node.parent);
const line = lines[getLineIndex(node, lines)];
return line.from === index;
}
return null;
}
function constant(x) {
return function() {
return x;
};
}
return compute;
}