sankeyLayout.js 19.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

20
/**
21
 * @file The layout algorithm of sankey diagram.
22
 * @author Deqing Li(annong035@gmail.com)
23
 */
D
deqingli 已提交
24

S
sushuang 已提交
25
import * as layout from '../../util/layout';
S
sushuang 已提交
26
import * as zrUtil from 'zrender/src/core/util';
27
import {groupData} from '../../util/model';
D
deqingli 已提交
28

S
sushuang 已提交
29
export default function (ecModel, api, payload) {
D
deqingli 已提交
30

S
sushuang 已提交
31
    ecModel.eachSeriesByType('sankey', function (seriesModel) {
D
deqingli 已提交
32

S
sushuang 已提交
33 34
        var nodeWidth = seriesModel.get('nodeWidth');
        var nodeGap = seriesModel.get('nodeGap');
D
deqingli 已提交
35

S
sushuang 已提交
36
        var layoutInfo = getViewRect(seriesModel, api);
D
deqingli 已提交
37

S
sushuang 已提交
38
        seriesModel.layoutInfo = layoutInfo;
D
deqingli 已提交
39

S
sushuang 已提交
40 41
        var width = layoutInfo.width;
        var height = layoutInfo.height;
D
deqingli 已提交
42

S
sushuang 已提交
43
        var graph = seriesModel.getGraph();
D
deqingli 已提交
44

S
sushuang 已提交
45 46
        var nodes = graph.nodes;
        var edges = graph.edges;
D
deqingli 已提交
47

S
sushuang 已提交
48
        computeNodeValues(nodes);
D
deqingli 已提交
49

S
sushuang 已提交
50 51
        var filteredNodes = zrUtil.filter(nodes, function (node) {
            return node.getLayout().value === 0;
D
deqingli 已提交
52 53
        });

S
sushuang 已提交
54 55
        var iterations = filteredNodes.length !== 0
            ? 0 : seriesModel.get('layoutIterations');
D
deqingli 已提交
56

57 58
        var orient = seriesModel.get('orient');

59 60 61
        var nodeAlign = seriesModel.get('nodeAlign');

        layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign);
S
sushuang 已提交
62 63
    });
}
D
deqingli 已提交
64

S
sushuang 已提交
65 66 67 68 69 70 71 72 73 74 75 76
/**
 * Get the layout position of the whole view
 *
 * @param {module:echarts/model/Series} seriesModel  the model object of sankey series
 * @param {module:echarts/ExtensionAPI} api  provide the API list that the developer can call
 * @return {module:zrender/core/BoundingRect}  size of rect to draw the sankey view
 */
function getViewRect(seriesModel, api) {
    return layout.getLayoutRect(
        seriesModel.getBoxLayoutParams(), {
            width: api.getWidth(),
            height: api.getHeight()
D
deqingli 已提交
77
        }
S
sushuang 已提交
78 79
    );
}
D
deqingli 已提交
80

81
function layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign) {
D
deqingli 已提交
82
    computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign);
83 84
    computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient);
    computeEdgeDepths(nodes, orient);
S
sushuang 已提交
85
}
D
deqingli 已提交
86

S
sushuang 已提交
87 88 89 90 91 92 93 94 95 96 97 98 99
/**
 * Compute the value of each node by summing the associated edge's value
 *
 * @param {module:echarts/data/Graph~Node} nodes  node of sankey view
 */
function computeNodeValues(nodes) {
    zrUtil.each(nodes, function (node) {
        var value1 = sum(node.outEdges, getEdgeValue);
        var value2 = sum(node.inEdges, getEdgeValue);
        var value = Math.max(value1, value2);
        node.setLayout({value: value}, true);
    });
}
D
deqingli 已提交
100

S
sushuang 已提交
101
/**
102
 * Compute the x-position for each node.
103
 *
104 105
 * Here we use Kahn algorithm to detect cycle when we traverse
 * the node to computer the initial x position.
S
sushuang 已提交
106 107 108 109 110
 *
 * @param {module:echarts/data/Graph~Node} nodes  node of sankey view
 * @param  {number} nodeWidth  the dx of the node
 * @param  {number} width  the whole width of the area to draw the view
 */
111
function computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign) {
112 113 114 115 116 117 118
    // Used to mark whether the edge is deleted. if it is deleted,
    // the value is 0, otherwise it is 1.
    var remainEdges = [];
    // Storage each node's indegree.
    var indegreeArr = [];
    //Used to storage the node with indegree is equal to 0.
    var zeroIndegrees = [];
119
    var nextTargetNode = [];
S
sushuang 已提交
120 121 122
    var x = 0;
    var kx = 0;

123 124 125
    for (var i = 0; i < edges.length; i++) {
        remainEdges[i] = 1;
    }
126
    for (i = 0; i < nodes.length; i++) {
127 128 129 130 131
        indegreeArr[i] = nodes[i].inEdges.length;
        if (indegreeArr[i] === 0) {
            zeroIndegrees.push(nodes[i]);
        }
    }
132
    var maxNodeDepth = -1;
133 134 135
    // Traversing nodes using topological sorting to calculate the
    // horizontal(if orient === 'horizontal') or vertical(if orient === 'vertical')
    // position of the nodes.
136
    while (zeroIndegrees.length) {
S
sushuang 已提交
137 138
        for (var idx = 0; idx < zeroIndegrees.length; idx++) {
            var node = zeroIndegrees[idx];
139 140 141 142 143
            var item = node.hostGraph.data.getRawDataItem(node.dataIndex);
            var isItemDepth = item.depth && !isNaN(item.depth) && item.depth >= 0;
            if (isItemDepth && item.depth > maxNodeDepth) {
                maxNodeDepth = item.depth;
            }
144
            if (orient === 'vertical') {
145
                node.setLayout({y: isItemDepth ? item.depth : x}, true);
146 147 148
                node.setLayout({dy: nodeWidth}, true);
            }
            else {
149
                node.setLayout({x: isItemDepth ? item.depth : x}, true);
150 151
                node.setLayout({dx: nodeWidth}, true);
            }
152 153
            for (var edgeIdx = 0; edgeIdx < node.outEdges.length; edgeIdx++) {
                var edge = node.outEdges[edgeIdx];
154 155
                var indexEdge = edges.indexOf(edge);
                remainEdges[indexEdge] = 0;
156
                var targetNode = edge.node2;
157
                var nodeIndex = nodes.indexOf(targetNode);
158 159
                if (--indegreeArr[nodeIndex] === 0 && nextTargetNode.indexOf(targetNode) < 0) {
                    nextTargetNode.push(targetNode);
160
                }
S
sushuang 已提交
161 162
            }
        }
S
sushuang 已提交
163
        ++x;
164 165
        zeroIndegrees = nextTargetNode;
        nextTargetNode = [];
D
deqingli 已提交
166
    }
167 168

    for (i = 0; i < remainEdges.length; i++) {
169
        if (remainEdges[i] === 1) {
170
            throw new Error('Sankey is a DAG, the original data has cycle!');
171
        }
172
    }
173

174 175 176 177 178 179 180 181 182 183 184 185 186 187
    var maxDepth = maxNodeDepth > x - 1 ? maxNodeDepth : x - 1;
    if (nodeAlign && nodeAlign !== 'left') {
        adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth);
    }
    var kx = orient === 'vertical' ? (height - nodeWidth) / maxDepth : (width - nodeWidth) / maxDepth;
    scaleNodeBreadths(nodes, kx, orient);
}

function isNodeDepth(node) {
    var item = node.hostGraph.data.getRawDataItem(node.dataIndex);
    return item.depth && !isNaN(item.depth) && item.depth >= 0;
}

function adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth) {
188 189
    if (nodeAlign === 'right') {
        var nextSourceNode = [];
190
        var remainNodes = nodes;
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
        var nodeHeight = 0;
        while (remainNodes.length) {
            for (var i = 0; i < remainNodes.length; i++) {
                var node = remainNodes[i];
                node.setLayout({skNodeHeight: nodeHeight}, true);
                for (var j = 0; j < node.inEdges.length; j++) {
                    var edge = node.inEdges[j];
                    if (nextSourceNode.indexOf(edge.node1) < 0) {
                        nextSourceNode.push(edge.node1);
                    }
                }
            }
            remainNodes = nextSourceNode;
            nextSourceNode = [];
            ++nodeHeight;
        }

208
        zrUtil.each(nodes, function (node) {
209 210 211 212 213 214 215
            if (!isNodeDepth(node)) {
                if (orient === 'vertical') {
                    node.setLayout({y: Math.max(0, maxDepth - node.getLayout().skNodeHeight)}, true);
                }
                else {
                    node.setLayout({x: Math.max(0, maxDepth - node.getLayout().skNodeHeight)}, true);
                }
216 217 218
            }
        });
    }
219
    else if (nodeAlign === 'justify') {
220
        moveSinksRight(nodes, maxDepth, orient);
221
    }
S
sushuang 已提交
222
}
D
deqingli 已提交
223

S
sushuang 已提交
224 225 226 227 228 229 230 231
/**
 * All the node without outEgdes are assigned maximum x-position and
 *     be aligned in the last column.
 *
 * @param {module:echarts/data/Graph~Node} nodes  node of sankey view
 * @param {number} x  value (x-1) use to assign to node without outEdges
 *     as x-position
 */
232
function moveSinksRight(nodes, maxDepth, orient) {
S
sushuang 已提交
233
    zrUtil.each(nodes, function (node) {
234
        if (!isNodeDepth(node) && !node.outEdges.length) {
235
            if (orient === 'vertical') {
236
                node.setLayout({y: maxDepth}, true);
237 238
            }
            else {
239
                node.setLayout({x: maxDepth}, true);
240
            }
D
deqingli 已提交
241
        }
S
sushuang 已提交
242 243
    });
}
D
deqingli 已提交
244

S
sushuang 已提交
245 246 247 248 249 250
/**
 * Scale node x-position to the width
 *
 * @param {module:echarts/data/Graph~Node} nodes  node of sankey view
 * @param {number} kx   multiple used to scale nodes
 */
251
function scaleNodeBreadths(nodes, kx, orient) {
S
sushuang 已提交
252
    zrUtil.each(nodes, function (node) {
D
deqingli 已提交
253 254 255
        if (orient === 'vertical') {
            var nodeY = node.getLayout().y * kx;
            node.setLayout({y: nodeY}, true);
256 257
        }
        else {
D
deqingli 已提交
258 259
            var nodeX = node.getLayout().x * kx;
            node.setLayout({x: nodeX}, true);
260
        }
S
sushuang 已提交
261 262 263 264 265 266 267 268 269 270 271 272 273
    });
}

/**
 * Using Gauss-Seidel iterations method to compute the node depth(y-position)
 *
 * @param {module:echarts/data/Graph~Node} nodes  node of sankey view
 * @param {module:echarts/data/Graph~Edge} edges  edge of sankey view
 * @param {number} height  the whole height of the area to draw the view
 * @param {number} nodeGap  the vertical distance between two nodes
 *     in the same column.
 * @param {number} iterations  the number of iterations for the algorithm
 */
274
function computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient) {
275
    var nodesByBreadth = prepareNodesByBreadth(nodes, orient);
276

277 278
    initializeNodeDepth(nodes, nodesByBreadth, edges, height, width, nodeGap, orient);
    resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
S
sushuang 已提交
279 280 281 282 283

    for (var alpha = 1; iterations > 0; iterations--) {
        // 0.99 is a experience parameter, ensure that each iterations of
        // changes as small as possible.
        alpha *= 0.99;
284 285 286 287
        relaxRightToLeft(nodesByBreadth, alpha, orient);
        resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
        relaxLeftToRight(nodesByBreadth, alpha, orient);
        resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
S
sushuang 已提交
288 289 290
    }
}

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
function prepareNodesByBreadth(nodes, orient) {
    var nodesByBreadth = [];
    var keyAttr = orient === 'vertical' ? 'y' : 'x';

    var groupResult = groupData(nodes, function (node) {
        return node.getLayout()[keyAttr];
    });
    groupResult.keys.sort(function (a, b) {
        return a - b;
    });
    zrUtil.each(groupResult.keys, function (key) {
        nodesByBreadth.push(groupResult.buckets.get(key));
    });

    return nodesByBreadth;
306 307
}

S
sushuang 已提交
308 309 310 311 312 313 314 315 316 317
/**
 * Compute the original y-position for each node
 *
 * @param {module:echarts/data/Graph~Node} nodes  node of sankey view
 * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
 *     group by the array of all sankey nodes based on the nodes x-position.
 * @param {module:echarts/data/Graph~Edge} edges  edge of sankey view
 * @param {number} height  the whole height of the area to draw the view
 * @param {number} nodeGap  the vertical distance between two nodes
 */
318
function initializeNodeDepth(nodes, nodesByBreadth, edges, height, width, nodeGap, orient) {
S
sushuang 已提交
319 320 321 322
    var kyArray = [];
    zrUtil.each(nodesByBreadth, function (nodes) {
        var n = nodes.length;
        var sum = 0;
323
        var ky = 0;
S
sushuang 已提交
324 325
        zrUtil.each(nodes, function (node) {
            sum += node.getLayout().value;
D
deqingli 已提交
326
        });
327 328 329 330 331 332
        if (orient === 'vertical') {
            ky = (width - (n - 1) * nodeGap) / sum;
        }
        else {
            ky = (height - (n - 1) * nodeGap) / sum;
        }
S
sushuang 已提交
333 334 335 336 337 338 339 340 341 342 343
        kyArray.push(ky);
    });

    kyArray.sort(function (a, b) {
        return a - b;
    });
    var ky0 = kyArray[0];

    zrUtil.each(nodesByBreadth, function (nodes) {
        zrUtil.each(nodes, function (node, i) {
            var nodeDy = node.getLayout().value * ky0;
344 345 346 347 348 349 350 351 352
            if (orient === 'vertical') {
                node.setLayout({x: i}, true);
                node.setLayout({dx: nodeDy}, true);
            }
            else {
                node.setLayout({y: i}, true);
                node.setLayout({dy: nodeDy}, true);
            }

D
deqingli 已提交
353
        });
S
sushuang 已提交
354
    });
D
deqingli 已提交
355

S
sushuang 已提交
356 357 358 359 360 361 362 363 364 365 366 367 368 369
    zrUtil.each(edges, function (edge) {
        var edgeDy = +edge.getValue() * ky0;
        edge.setLayout({dy: edgeDy}, true);
    });
}

/**
 * Resolve the collision of initialized depth (y-position)
 *
 * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
 *     group by the array of all sankey nodes based on the nodes x-position.
 * @param {number} nodeGap  the vertical distance between two nodes
 * @param {number} height  the whole height of the area to draw the view
 */
370
function resolveCollisions(nodesByBreadth, nodeGap, height, width, orient) {
S
sushuang 已提交
371 372 373 374 375 376 377
    zrUtil.each(nodesByBreadth, function (nodes) {
        var node;
        var dy;
        var y0 = 0;
        var n = nodes.length;
        var i;

378
        if (orient === 'vertical') {
S
sushuang 已提交
379
            var nodeX;
380 381 382 383 384 385 386
            nodes.sort(function (a, b) {
                return a.getLayout().x - b.getLayout().x;
            });
            for (i = 0; i < n; i++) {
                node = nodes[i];
                dy = y0 - node.getLayout().x;
                if (dy > 0) {
S
sushuang 已提交
387
                    nodeX = node.getLayout().x + dy;
388 389 390 391 392 393
                    node.setLayout({x: nodeX}, true);
                }
                y0 = node.getLayout().x + node.getLayout().dx + nodeGap;
            }
            // If the bottommost node goes outside the bounds, push it back up
            dy = y0 - nodeGap - width;
S
sushuang 已提交
394
            if (dy > 0) {
395 396 397 398 399 400 401 402 403 404 405 406
                nodeX = node.getLayout().x - dy;
                node.setLayout({x: nodeX}, true);
                y0 = nodeX;
                for (i = n - 2; i >= 0; --i) {
                    node = nodes[i];
                    dy = node.getLayout().x + node.getLayout().dx + nodeGap - y0;
                    if (dy > 0) {
                        nodeX = node.getLayout().x - dy;
                        node.setLayout({x: nodeX}, true);
                    }
                    y0 = node.getLayout().x;
                }
S
sushuang 已提交
407 408
            }
        }
409
        else {
S
sushuang 已提交
410
            var nodeY;
411 412 413 414
            nodes.sort(function (a, b) {
                return a.getLayout().y - b.getLayout().y;
            });
            for (i = 0; i < n; i++) {
D
deqingli 已提交
415
                node = nodes[i];
416
                dy = y0 - node.getLayout().y;
417
                if (dy > 0) {
S
sushuang 已提交
418
                    nodeY = node.getLayout().y + dy;
D
deqingli 已提交
419 420
                    node.setLayout({y: nodeY}, true);
                }
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
                y0 = node.getLayout().y + node.getLayout().dy + nodeGap;
            }
            // If the bottommost node goes outside the bounds, push it back up
            dy = y0 - nodeGap - height;
            if (dy > 0) {
                nodeY = node.getLayout().y - dy;
                node.setLayout({y: nodeY}, true);
                y0 = nodeY;
                for (i = n - 2; i >= 0; --i) {
                    node = nodes[i];
                    dy = node.getLayout().y + node.getLayout().dy + nodeGap - y0;
                    if (dy > 0) {
                        nodeY = node.getLayout().y - dy;
                        node.setLayout({y: nodeY}, true);
                    }
                    y0 = node.getLayout().y;
                }
D
deqingli 已提交
438
            }
S
sushuang 已提交
439 440 441
        }
    });
}
D
deqingli 已提交
442

S
sushuang 已提交
443 444 445 446 447 448 449
/**
 * Change the y-position of the nodes, except most the right side nodes
 *
 * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
 *     group by the array of all sankey nodes based on the node x-position.
 * @param {number} alpha  parameter used to adjust the nodes y-position
 */
450
function relaxRightToLeft(nodesByBreadth, alpha, orient) {
S
sushuang 已提交
451 452 453
    zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) {
        zrUtil.each(nodes, function (node) {
            if (node.outEdges.length) {
454 455 456 457 458 459 460 461 462
                var y = sum(node.outEdges, weightedTarget, orient) / sum(node.outEdges, getEdgeValue, orient);
                if (orient === 'vertical') {
                    var nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
                    node.setLayout({x: nodeX}, true);
                }
                else {
                    var nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
                    node.setLayout({y: nodeY}, true);
                }
L
lang 已提交
463
            }
D
deqingli 已提交
464
        });
S
sushuang 已提交
465 466
    });
}
D
deqingli 已提交
467

468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
function weightedTarget(edge, orient) {
    return center(edge.node2, orient) * edge.getValue();
}

function weightedSource(edge, orient) {
    return center(edge.node1, orient) * edge.getValue();
}

function center(node, orient) {
    if (orient === 'vertical') {
        return node.getLayout().x + node.getLayout().dx / 2;
    }
    return node.getLayout().y + node.getLayout().dy / 2;
}

function getEdgeValue(edge) {
    return edge.getValue();
}

function sum(array, f, orient) {
    var sum = 0;
    var len = array.length;
    var i = -1;
    while (++i < len) {
        var value = +f.call(array, array[i], orient);
        if (!isNaN(value)) {
            sum += value;
        }
    }
    return sum;
S
sushuang 已提交
498
}
D
deqingli 已提交
499

S
sushuang 已提交
500 501 502 503 504 505 506
/**
 * Change the y-position of the nodes, except most the left side nodes
 *
 * @param {Array.<Array.<module:echarts/data/Graph~Node>>} nodesByBreadth
 *     group by the array of all sankey nodes based on the node x-position.
 * @param {number} alpha  parameter used to adjust the nodes y-position
 */
507
function relaxLeftToRight(nodesByBreadth, alpha, orient) {
S
sushuang 已提交
508 509 510
    zrUtil.each(nodesByBreadth, function (nodes) {
        zrUtil.each(nodes, function (node) {
            if (node.inEdges.length) {
511 512 513 514 515 516 517 518 519
                var y = sum(node.inEdges, weightedSource, orient) / sum(node.inEdges, getEdgeValue, orient);
                if (orient === 'vertical') {
                    var nodeX = node.getLayout().x + (y - center(node, orient)) * alpha;
                    node.setLayout({x: nodeX}, true);
                }
                else {
                    var nodeY = node.getLayout().y + (y - center(node, orient)) * alpha;
                    node.setLayout({y: nodeY}, true);
                }
S
sushuang 已提交
520
            }
D
deqingli 已提交
521
        });
S
sushuang 已提交
522 523
    });
}
D
deqingli 已提交
524

S
sushuang 已提交
525 526 527 528 529
/**
 * Compute the depth(y-position) of each edge
 *
 * @param {module:echarts/data/Graph~Node} nodes  node of sankey view
 */
530
function computeEdgeDepths(nodes, orient) {
S
sushuang 已提交
531
    zrUtil.each(nodes, function (node) {
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
        if (orient === 'vertical') {
            node.outEdges.sort(function (a, b) {
                return a.node2.getLayout().x - b.node2.getLayout().x;
            });
            node.inEdges.sort(function (a, b) {
                return a.node1.getLayout().x - b.node1.getLayout().x;
            });
        }
        else {
            node.outEdges.sort(function (a, b) {
                return a.node2.getLayout().y - b.node2.getLayout().y;
            });
            node.inEdges.sort(function (a, b) {
                return a.node1.getLayout().y - b.node1.getLayout().y;
            });
        }
S
sushuang 已提交
548 549 550 551 552 553 554
    });
    zrUtil.each(nodes, function (node) {
        var sy = 0;
        var ty = 0;
        zrUtil.each(node.outEdges, function (edge) {
            edge.setLayout({sy: sy}, true);
            sy += edge.getLayout().dy;
D
deqingli 已提交
555
        });
S
sushuang 已提交
556 557 558
        zrUtil.each(node.inEdges, function (edge) {
            edge.setLayout({ty: ty}, true);
            ty += edge.getLayout().dy;
D
deqingli 已提交
559
        });
S
sushuang 已提交
560 561
    });
}