devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
300 lines (299 loc) • 12.8 kB
JavaScript
/**
* DevExtreme (esm/viz/sankey/layout.js)
* Version: 21.1.4
* Build date: Mon Jun 21 2021
*
* Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
var _SPLINE_TENSION = .3;
var _ALIGNMENT_CENTER = "center";
var _ALIGNMENT_BOTTOM = "bottom";
var _ALIGNMENT_DEFAULT = _ALIGNMENT_CENTER;
import graphModule from "./graph";
import validatorModule from "./data_validator";
export var layout = {
_weightPerPixel: null,
_getCascadeIdx: function(nodeTitle, cascadesConfig) {
var nodeInfo = cascadesConfig.filter(c => c.name === nodeTitle)[0];
if (nodeInfo.outgoing.length > 0) {
return nodeInfo.lp
} else {
return graphModule.routines.maxOfArray(cascadesConfig.map(c => c.lp))
}
},
_getInWeightForNode: function(nodeTitle, links) {
var w = 0;
links.forEach(link => {
if (link[1] === nodeTitle) {
w += link[2]
}
});
return w
},
_getOutWeightForNode: function(nodeTitle, links) {
var w = 0;
links.forEach(link => {
if (link[0] === nodeTitle) {
w += link[2]
}
});
return w
},
_computeCascades: function(links) {
var cascadesConfig = graphModule.struct.computeLongestPaths(links);
var maxCascade = graphModule.routines.maxOfArray(cascadesConfig.map(c => c.lp));
var cascades = [];
for (var i = 0; i < maxCascade + 1; i++) {
cascades.push({})
}
links.forEach(link => {
var cascade = cascades[this._getCascadeIdx(link[0], cascadesConfig)];
if (!cascade[link[0]]) {
cascade[link[0]] = {
nodeTitle: link[0]
}
}
cascade = cascades[this._getCascadeIdx(link[1], cascadesConfig)];
if (!cascade[link[1]]) {
cascade[link[1]] = {
nodeTitle: link[1]
}
}
});
cascades.forEach(cascade => {
Object.keys(cascade).forEach(nodeTitle => {
var node = cascade[nodeTitle];
node.inWeight = this._getInWeightForNode(node.nodeTitle, links);
node.outWeight = this._getOutWeightForNode(node.nodeTitle, links);
node.maxWeight = Math.max(node.inWeight, node.outWeight)
})
});
return cascades
},
_getWeightForCascade: function(cascades, cascadeIdx) {
var wMax = 0;
var cascade = cascades[cascadeIdx];
Object.keys(cascade).forEach(nodeTitle => {
wMax += Math.max(cascade[nodeTitle].inWeight, cascade[nodeTitle].outWeight)
});
return wMax
},
_getMaxWeightThroughCascades: function(cascades) {
var max = [];
cascades.forEach(cascade => {
var mW = 0;
Object.keys(cascade).forEach(nodeTitle => {
var node = cascade[nodeTitle];
mW += Math.max(node.inWeight, node.outWeight)
});
max.push(mW)
});
return graphModule.routines.maxOfArray(max)
},
_computeNodes: function(cascades, options) {
var rects = [];
var maxWeight = this._getMaxWeightThroughCascades(cascades);
var maxNodeNum = graphModule.routines.maxOfArray(cascades.map(nodesInCascade => Object.keys(nodesInCascade).length));
var nodePadding = options.nodePadding;
var heightAvailable = options.height - nodePadding * (maxNodeNum - 1);
if (heightAvailable < 0) {
nodePadding = 0;
heightAvailable = options.height - nodePadding * (maxNodeNum - 1)
}
this._weightPerPixel = maxWeight / heightAvailable;
var cascadeIdx = 0;
cascades.forEach(cascade => {
var cascadeRects = [];
var y = 0;
var nodesInCascade = Object.keys(cascade).length;
var cascadeHeight = this._getWeightForCascade(cascades, cascadeIdx) / this._weightPerPixel + nodePadding * (nodesInCascade - 1);
var cascadeAlign;
if (Array.isArray(options.nodeAlign)) {
cascadeAlign = cascadeIdx < options.nodeAlign.length ? options.nodeAlign[cascadeIdx] : _ALIGNMENT_DEFAULT
} else {
cascadeAlign = options.nodeAlign
}
if (cascadeAlign === _ALIGNMENT_BOTTOM) {
y = options.height - cascadeHeight
} else if (cascadeAlign === _ALIGNMENT_CENTER) {
y = .5 * (options.height - cascadeHeight)
}
y = Math.round(y);
Object.keys(cascade).forEach(nodeTitle => {
cascade[nodeTitle].sort = this._sort && Object.prototype.hasOwnProperty.call(this._sort, nodeTitle) ? this._sort[nodeTitle] : 1
});
Object.keys(cascade).sort((a, b) => cascade[a].sort - cascade[b].sort).forEach(nodeTitle => {
var node = cascade[nodeTitle];
var height = Math.floor(heightAvailable * node.maxWeight / maxWeight);
var x = Math.round(cascadeIdx * options.width / (cascades.length - 1)) - (0 === cascadeIdx ? 0 : options.nodeWidth);
var rect = {};
rect._name = nodeTitle;
rect.width = options.nodeWidth;
rect.height = height;
rect.x = x + options.x;
rect.y = y + options.y;
y += height + nodePadding;
cascadeRects.push(rect)
});
cascadeIdx++;
rects.push(cascadeRects)
});
return rects
},
_findRectByName: function(rects, name) {
for (var c = 0; c < rects.length; c++) {
for (var r = 0; r < rects[c].length; r++) {
if (name === rects[c][r]._name) {
return rects[c][r]
}
}
}
return null
},
_findIndexByName: function(rects, nodeTitle) {
var index = 0;
for (var c = 0; c < rects.length; c++) {
for (var r = 0; r < rects[c].length; r++) {
if (nodeTitle === rects[c][r]._name) {
return index
}
index++
}
}
return null
},
_computeLinks: function(links, rects, cascades) {
var yOffsets = {};
var paths = [];
var result = [];
cascades.forEach(cascade => {
Object.keys(cascade).forEach(nodeTitle => {
yOffsets[nodeTitle] = {
in: 0,
out: 0
}
})
});
rects.forEach(rectsOfCascade => {
rectsOfCascade.forEach(nodeRect => {
var nodeTitle = nodeRect._name;
var rectFrom = this._findRectByName(rects, nodeTitle);
var linksFromNode = links.filter(link => link[0] === nodeTitle);
linksFromNode.forEach(link => {
link.sort = this._findIndexByName(rects, link[1])
});
linksFromNode.sort((a, b) => a.sort - b.sort).forEach(link => {
var rectTo = this._findRectByName(rects, link[1]);
var height = Math.round(link[2] / this._weightPerPixel);
var yOffsetFrom = yOffsets[link[0]].out;
var yOffsetTo = yOffsets[link[1]].in;
var heightFrom = yOffsets[link[0]].out + height > rectFrom.height ? rectFrom.height - yOffsets[link[0]].out : height;
var heightTo = yOffsets[link[1]].in + height > rectTo.height ? rectTo.height - yOffsets[link[1]].in : height;
paths.push({
from: {
x: rectFrom.x,
y: rectFrom.y + yOffsetFrom,
width: rectFrom.width,
height: heightFrom,
node: rectFrom,
weight: link[2]
},
to: {
x: rectTo.x,
y: rectTo.y + yOffsetTo,
width: rectTo.width,
height: heightTo,
node: rectTo
}
});
yOffsets[link[0]].out += height;
yOffsets[link[1]].in += height
})
})
});
paths.forEach(link => {
var path = {
d: this._spline(link.from, link.to),
_boundingRect: {
x: link.from.x + link.from.width,
y: Math.min(link.from.y, link.to.y),
width: link.to.x - (link.from.x + link.from.width),
height: Math.max(link.from.x + link.from.height, link.to.y + link.to.height) - Math.min(link.from.y, link.to.y)
},
_weight: link.from.weight,
_from: link.from.node,
_to: link.to.node
};
result.push(path)
});
this._fitAllNodesHeight(rects, paths);
return result
},
_fitNodeHeight: function(nodeName, nodeRects, paths) {
var targetRect = this._findRectByName(nodeRects, nodeName);
var heightOfLinksSummaryIn = 0;
var heightOfLinksSummaryOut = 0;
paths.forEach((function(path) {
if (path.from.node._name === nodeName) {
heightOfLinksSummaryOut += path.from.height
}
if (path.to.node._name === nodeName) {
heightOfLinksSummaryIn += path.to.height
}
}));
targetRect.height = Math.max(heightOfLinksSummaryIn, heightOfLinksSummaryOut)
},
_fitAllNodesHeight: function(nodeRects, paths) {
for (var c = 0; c < nodeRects.length; c++) {
for (var r = 0; r < nodeRects[c].length; r++) {
this._fitNodeHeight(nodeRects[c][r]._name, nodeRects, paths)
}
}
},
_spline: function(rectLeft, rectRight) {
var p_UpLeft = {
x: rectLeft.x + rectLeft.width,
y: rectLeft.y
};
var p_DownLeft = {
x: rectLeft.x + rectLeft.width,
y: rectLeft.y + rectLeft.height
};
var p_UpRight = {
x: rectRight.x,
y: rectRight.y
};
var p_DownRight = {
x: rectRight.x,
y: rectRight.y + rectRight.height
};
var curve_width = _SPLINE_TENSION * (p_UpRight.x - p_UpLeft.x);
var result = "M ".concat(p_UpLeft.x, " ").concat(p_UpLeft.y, " C ").concat(p_UpLeft.x + curve_width, " ").concat(p_UpLeft.y, " ").concat(p_UpRight.x - curve_width, " ").concat(p_UpRight.y, " ").concat(p_UpRight.x, " ").concat(p_UpRight.y, " L ").concat(p_DownRight.x, " ").concat(p_DownRight.y, " C ").concat(p_DownRight.x - curve_width, " ").concat(p_DownRight.y, " ").concat(p_DownLeft.x + curve_width, " ").concat(p_DownLeft.y, " ").concat(p_DownLeft.x, " ").concat(p_DownLeft.y, " Z");
return result
},
computeLayout: function(linksData, sortData, options, incidentOccurred) {
this._sort = sortData;
var result = {};
var validateResult = validatorModule.validate(linksData, incidentOccurred);
if (!validateResult) {
result.cascades = this._computeCascades(linksData);
result.nodes = this._computeNodes(result.cascades, {
width: options.availableRect.width,
height: options.availableRect.height,
x: options.availableRect.x,
y: options.availableRect.y,
nodePadding: options.nodePadding,
nodeWidth: options.nodeWidth,
nodeAlign: options.nodeAlign
});
result.links = this._computeLinks(linksData, result.nodes, result.cascades)
} else {
result.error = validateResult
}
return result
},
overlap: function(box1, box2) {
return !(box2.x > box1.x + box1.width || box2.x + box2.width < box1.x || box2.y >= box1.y + box1.height || box2.y + box2.height <= box1.y)
}
};