sankeyLayout.js 17.0 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 view
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 { __DEV__ } from '../../config';
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 59
        var orient = seriesModel.get('orient');

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

S
sushuang 已提交
63 64 65 66 67 68 69 70 71 72 73 74
/**
 * 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 已提交
75
        }
S
sushuang 已提交
76 77
    );
}
D
deqingli 已提交
78

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

S
sushuang 已提交
85 86 87 88 89 90 91 92 93 94 95 96 97
/**
 * 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 已提交
98

S
sushuang 已提交
99
/**
100
 * Compute the x-position for each node.
101
 *
102 103
 * Here we use Kahn algorithm to detect cycle when we traverse
 * the node to computer the initial x position.
S
sushuang 已提交
104 105 106 107 108
 *
 * @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
 */
109
function computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient) {
110 111 112
    // Used to mark whether the edge is deleted. if it is deleted,
    // the value is 0, otherwise it is 1.
    var remainEdges = [];
113

114 115
    // Storage each node's indegree.
    var indegreeArr = [];
116

117 118
    //Used to storage the node with indegree is equal to 0.
    var zeroIndegrees = [];
119

120
    var nextNode = [];
S
sushuang 已提交
121 122 123
    var x = 0;
    var kx = 0;

124 125 126 127
    for (var i = 0; i < edges.length; i++) {
        remainEdges[i] = 1;
    }

128
    for (i = 0; i < nodes.length; i++) {
129 130 131 132 133
        indegreeArr[i] = nodes[i].inEdges.length;
        if (indegreeArr[i] === 0) {
            zeroIndegrees.push(nodes[i]);
        }
    }
134

135 136
    while (zeroIndegrees.length) {
        zrUtil.each(zeroIndegrees, function (node) {
137 138 139 140 141 142 143 144
            if (orient === 'vertical') {
                node.setLayout({y: x}, true);
                node.setLayout({dy: nodeWidth}, true);
            }
            else {
                node.setLayout({x: x}, true);
                node.setLayout({dx: nodeWidth}, true);
            }
145
            zrUtil.each(node.outEdges, function (edge) {
146 147
                var indexEdge = edges.indexOf(edge);
                remainEdges[indexEdge] = 0;
148
                var targetNode = edge.node2;
149 150
                var nodeIndex = nodes.indexOf(targetNode);
                if (--indegreeArr[nodeIndex] === 0) {
151 152 153 154
                    nextNode.push(targetNode);
                }
            });
        });
S
sushuang 已提交
155
        ++x;
156
        zeroIndegrees = nextNode;
157
        nextNode = [];
D
deqingli 已提交
158
    }
159 160

    for (i = 0; i < remainEdges.length; i++) {
161 162 163 164
        if (__DEV__) {
            if (remainEdges[i] === 1) {
                throw new Error('Sankey is a DAG, the original data has cycle!');
            }
165
        }
166
    }
167

168
    moveSinksRight(nodes, x);
D
deqingli 已提交
169

170 171 172 173 174 175 176
    if (orient === 'vertical') {
        kx = (height - nodeWidth) / (x - 1);
    }
    else {
        kx = (width - nodeWidth) / (x - 1);
    }
    scaleNodeBreadths(nodes, kx, orient);
S
sushuang 已提交
177
}
D
deqingli 已提交
178

S
sushuang 已提交
179 180 181 182 183 184 185 186
/**
 * 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
 */
187
function moveSinksRight(nodes, x, orient) {
S
sushuang 已提交
188 189
    zrUtil.each(nodes, function (node) {
        if (!node.outEdges.length) {
190 191 192 193 194 195
            if (orient === 'vertical') {
                node.setLayout({y: x - 1}, true);
            }
            else {
                node.setLayout({x: x - 1}, true);
            }
D
deqingli 已提交
196
        }
S
sushuang 已提交
197 198
    });
}
D
deqingli 已提交
199

S
sushuang 已提交
200 201 202 203 204 205
/**
 * 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
 */
206
function scaleNodeBreadths(nodes, kx, orient) {
S
sushuang 已提交
207
    zrUtil.each(nodes, function (node) {
208 209 210 211 212 213 214 215
        if (orient === 'vertical') {
            var nodeY = node.getLayout().y * kx;
            node.setLayout({y: nodeY}, true);
        }
        else {
            var nodeX = node.getLayout().x * kx;
            node.setLayout({x: nodeX}, true);
        }
S
sushuang 已提交
216 217 218 219 220 221 222 223 224 225 226 227 228
    });
}

/**
 * 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
 */
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
function computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient) {
    var i = -1;
    var valuesByKey = {};
    while (++i < nodes.length) {
        if (orient === 'vertical') {
            var keyValue = nodes[i].getLayout().y;
        }
        else {
            var keyValue = nodes[i].getLayout().x;
        }
        var values = valuesByKey[keyValue];
        if (values) {
            values.push(nodes[i]);
        }
        else {
            valuesByKey[keyValue] = [nodes[i]];
        }
    }

    var tempArray = [];
    zrUtil.each(valuesByKey, function (value, key) {
        tempArray.push({
            key: key, values: value
D
deqingli 已提交
252
        });
253 254 255 256 257 258 259 260 261
    });

    tempArray.sort(function (a, b) {
        return a.key - b.key;
    });

    var nodesByBreadth = tempArray.map(function (d) {
        return d.values;
    });
262

263 264
    initializeNodeDepth(nodes, nodesByBreadth, edges, height, width, nodeGap, orient);
    resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
S
sushuang 已提交
265 266 267 268 269

    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;
270 271 272 273
        relaxRightToLeft(nodesByBreadth, alpha, orient);
        resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
        relaxLeftToRight(nodesByBreadth, alpha, orient);
        resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
S
sushuang 已提交
274 275 276 277 278 279 280 281 282 283 284 285 286
    }
}

/**
 * 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
 */
287
function initializeNodeDepth(nodes, nodesByBreadth, edges, height, width, nodeGap, orient) {
S
sushuang 已提交
288 289 290 291
    var kyArray = [];
    zrUtil.each(nodesByBreadth, function (nodes) {
        var n = nodes.length;
        var sum = 0;
292
        var ky = 0;
S
sushuang 已提交
293 294
        zrUtil.each(nodes, function (node) {
            sum += node.getLayout().value;
D
deqingli 已提交
295
        });
296 297 298 299 300 301
        if (orient === 'vertical') {
            ky = (width - (n - 1) * nodeGap) / sum;
        }
        else {
            ky = (height - (n - 1) * nodeGap) / sum;
        }
S
sushuang 已提交
302 303 304 305 306 307 308 309 310 311 312
        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;
313 314 315 316 317 318 319 320 321
            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 已提交
322
        });
S
sushuang 已提交
323
    });
D
deqingli 已提交
324

S
sushuang 已提交
325 326 327 328 329 330 331 332 333 334 335 336 337 338
    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
 */
339
function resolveCollisions(nodesByBreadth, nodeGap, height, width, orient) {
S
sushuang 已提交
340 341 342 343 344 345 346
    zrUtil.each(nodesByBreadth, function (nodes) {
        var node;
        var dy;
        var y0 = 0;
        var n = nodes.length;
        var i;

347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
        if (orient === 'vertical') {
            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) {
                    var nodeX = node.getLayout().x + dy;
                    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 已提交
362
            if (dy > 0) {
363 364 365 366 367 368 369 370 371 372 373 374
                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 已提交
375 376
            }
        }
377 378 379 380 381
        else {
            nodes.sort(function (a, b) {
                return a.getLayout().y - b.getLayout().y;
            });
            for (i = 0; i < n; i++) {
D
deqingli 已提交
382
                node = nodes[i];
383
                dy = y0 - node.getLayout().y;
384
                if (dy > 0) {
385
                    var nodeY = node.getLayout().y + dy;
D
deqingli 已提交
386 387
                    node.setLayout({y: nodeY}, true);
                }
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
                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 已提交
405
            }
S
sushuang 已提交
406 407 408
        }
    });
}
D
deqingli 已提交
409

S
sushuang 已提交
410 411 412 413 414 415 416
/**
 * 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
 */
417
function relaxRightToLeft(nodesByBreadth, alpha, orient) {
S
sushuang 已提交
418 419 420
    zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) {
        zrUtil.each(nodes, function (node) {
            if (node.outEdges.length) {
421 422 423 424 425 426 427 428 429
                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 已提交
430
            }
D
deqingli 已提交
431
        });
S
sushuang 已提交
432 433
    });
}
D
deqingli 已提交
434

435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
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 已提交
465
}
D
deqingli 已提交
466

S
sushuang 已提交
467 468 469 470 471 472 473
/**
 * 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
 */
474
function relaxLeftToRight(nodesByBreadth, alpha, orient) {
S
sushuang 已提交
475 476 477
    zrUtil.each(nodesByBreadth, function (nodes) {
        zrUtil.each(nodes, function (node) {
            if (node.inEdges.length) {
478 479 480 481 482 483 484 485 486
                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 已提交
487
            }
D
deqingli 已提交
488
        });
S
sushuang 已提交
489 490
    });
}
D
deqingli 已提交
491

S
sushuang 已提交
492 493 494 495 496
/**
 * Compute the depth(y-position) of each edge
 *
 * @param {module:echarts/data/Graph~Node} nodes  node of sankey view
 */
497
function computeEdgeDepths(nodes, orient) {
S
sushuang 已提交
498
    zrUtil.each(nodes, function (node) {
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
        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 已提交
515 516 517 518 519 520 521
    });
    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 已提交
522
        });
S
sushuang 已提交
523 524 525
        zrUtil.each(node.inEdges, function (edge) {
            edge.setLayout({ty: ty}, true);
            ty += edge.getLayout().dy;
D
deqingli 已提交
526
        });
S
sushuang 已提交
527 528
    });
}