sankeyLayout.js 16.9 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';
26
import nest from '../../util/nest';
S
sushuang 已提交
27
import * as zrUtil from 'zrender/src/core/util';
28
import { __DEV__ } from '../../config';
D
deqingli 已提交
29

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

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

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

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

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

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

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

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

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

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

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

58 59 60
        var orient = seriesModel.get('orient');

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

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

80 81 82 83
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 已提交
84
}
D
deqingli 已提交
85

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

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

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

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

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

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

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

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 144 145 146
            if (orient === 'vertical') {
                node.setLayout({y: x}, true);
                node.setLayout({dy: nodeWidth}, true);
            }
            else {
                node.setLayout({x: x}, true);
                node.setLayout({dx: nodeWidth}, true);
            }
S
sushuang 已提交
147 148
            for (var oidx = 0; oidx < node.outEdges.length; oidx++) {
                var edge = node.outEdges[oidx];
149 150
                var indexEdge = edges.indexOf(edge);
                remainEdges[indexEdge] = 0;
151
                var targetNode = edge.node2;
152 153
                var nodeIndex = nodes.indexOf(targetNode);
                if (--indegreeArr[nodeIndex] === 0) {
154 155
                    nextNode.push(targetNode);
                }
S
sushuang 已提交
156 157
            }
        }
S
sushuang 已提交
158
        ++x;
159
        zeroIndegrees = nextNode;
160
        nextNode = [];
D
deqingli 已提交
161
    }
162 163

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

171
    moveSinksRight(nodes, x, orient);
D
deqingli 已提交
172

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

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

S
sushuang 已提交
203 204 205 206 207 208
/**
 * 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
 */
209
function scaleNodeBreadths(nodes, kx, orient) {
S
sushuang 已提交
210
    zrUtil.each(nodes, function (node) {
211 212 213 214 215 216 217 218
        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 已提交
219 220 221 222 223 224 225 226 227 228 229 230 231
    });
}

/**
 * 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
 */
232
function computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient) {
233 234 235 236 237 238 239 240
    var nodesByBreadth = nest()
        .key(getKeyFunction(orient))
        .sortKeys(function (a, b) {
            return a - b;
        })
        .entries(nodes)
        .map(function (d) {
            return d.values;
D
deqingli 已提交
241
        });
242

243 244
    initializeNodeDepth(nodes, nodesByBreadth, edges, height, width, nodeGap, orient);
    resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
S
sushuang 已提交
245 246 247 248 249

    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;
250 251 252 253
        relaxRightToLeft(nodesByBreadth, alpha, orient);
        resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
        relaxLeftToRight(nodesByBreadth, alpha, orient);
        resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
S
sushuang 已提交
254 255 256
    }
}

257 258 259 260 261 262 263 264 265 266 267
function getKeyFunction(orient) {
    if (orient === 'vertical') {
        return function (d) {
            return d.getLayout().y;
        };
    }
    return function (d) {
        return d.getLayout().x;
    };
}

S
sushuang 已提交
268 269 270 271 272 273 274 275 276 277
/**
 * 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
 */
278
function initializeNodeDepth(nodes, nodesByBreadth, edges, height, width, nodeGap, orient) {
S
sushuang 已提交
279 280 281 282
    var kyArray = [];
    zrUtil.each(nodesByBreadth, function (nodes) {
        var n = nodes.length;
        var sum = 0;
283
        var ky = 0;
S
sushuang 已提交
284 285
        zrUtil.each(nodes, function (node) {
            sum += node.getLayout().value;
D
deqingli 已提交
286
        });
287 288 289 290 291 292
        if (orient === 'vertical') {
            ky = (width - (n - 1) * nodeGap) / sum;
        }
        else {
            ky = (height - (n - 1) * nodeGap) / sum;
        }
S
sushuang 已提交
293 294 295 296 297 298 299 300 301 302 303
        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;
304 305 306 307 308 309 310 311 312
            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 已提交
313
        });
S
sushuang 已提交
314
    });
D
deqingli 已提交
315

S
sushuang 已提交
316 317 318 319 320 321 322 323 324 325 326 327 328 329
    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
 */
330
function resolveCollisions(nodesByBreadth, nodeGap, height, width, orient) {
S
sushuang 已提交
331 332 333 334 335 336 337
    zrUtil.each(nodesByBreadth, function (nodes) {
        var node;
        var dy;
        var y0 = 0;
        var n = nodes.length;
        var i;

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

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

428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
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 已提交
458
}
D
deqingli 已提交
459

S
sushuang 已提交
460 461 462 463 464 465 466
/**
 * 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
 */
467
function relaxLeftToRight(nodesByBreadth, alpha, orient) {
S
sushuang 已提交
468 469 470
    zrUtil.each(nodesByBreadth, function (nodes) {
        zrUtil.each(nodes, function (node) {
            if (node.inEdges.length) {
471 472 473 474 475 476 477 478 479
                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 已提交
480
            }
D
deqingli 已提交
481
        });
S
sushuang 已提交
482 483
    });
}
D
deqingli 已提交
484

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