sankeyLayout.js 17.4 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.
*/

S
sushuang 已提交
20
import * as layout from '../../util/layout';
S
sushuang 已提交
21
import * as zrUtil from 'zrender/src/core/util';
22
import {groupData} from '../../util/model';
D
deqingli 已提交
23

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

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

S
sushuang 已提交
28 29
        var nodeWidth = seriesModel.get('nodeWidth');
        var nodeGap = seriesModel.get('nodeGap');
D
deqingli 已提交
30

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

S
sushuang 已提交
33
        seriesModel.layoutInfo = layoutInfo;
D
deqingli 已提交
34

S
sushuang 已提交
35 36
        var width = layoutInfo.width;
        var height = layoutInfo.height;
D
deqingli 已提交
37

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

S
sushuang 已提交
40 41
        var nodes = graph.nodes;
        var edges = graph.edges;
D
deqingli 已提交
42

S
sushuang 已提交
43
        computeNodeValues(nodes);
D
deqingli 已提交
44

S
sushuang 已提交
45 46
        var filteredNodes = zrUtil.filter(nodes, function (node) {
            return node.getLayout().value === 0;
D
deqingli 已提交
47 48
        });

49
        var iterations = filteredNodes.length !== 0 ? 0 : seriesModel.get('layoutIterations');
D
deqingli 已提交
50

51 52
        var orient = seriesModel.get('orient');

53 54 55
        var nodeAlign = seriesModel.get('nodeAlign');

        layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign);
S
sushuang 已提交
56 57
    });
}
D
deqingli 已提交
58

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

75
function layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign) {
D
deqingli 已提交
76
    computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign);
77 78
    computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient);
    computeEdgeDepths(nodes, orient);
S
sushuang 已提交
79
}
D
deqingli 已提交
80

S
sushuang 已提交
81 82 83 84 85 86 87 88 89
/**
 * 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);
D
deqingli 已提交
90 91
        var nodeRawValue = node.getValue() || 0;
        var value = Math.max(value1, value2, nodeRawValue);
S
sushuang 已提交
92 93 94
        node.setLayout({value: value}, true);
    });
}
D
deqingli 已提交
95

S
sushuang 已提交
96
/**
97
 * Compute the x-position for each node.
98
 *
99 100
 * Here we use Kahn algorithm to detect cycle when we traverse
 * the node to computer the initial x position.
S
sushuang 已提交
101 102 103 104 105
 *
 * @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
 */
106
function computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign) {
107 108 109 110 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 = [];
    // Storage each node's indegree.
    var indegreeArr = [];
    //Used to storage the node with indegree is equal to 0.
    var zeroIndegrees = [];
114
    var nextTargetNode = [];
S
sushuang 已提交
115 116 117
    var x = 0;
    var kx = 0;

118 119 120
    for (var i = 0; i < edges.length; i++) {
        remainEdges[i] = 1;
    }
121
    for (i = 0; i < nodes.length; i++) {
122 123 124 125 126
        indegreeArr[i] = nodes[i].inEdges.length;
        if (indegreeArr[i] === 0) {
            zeroIndegrees.push(nodes[i]);
        }
    }
127
    var maxNodeDepth = -1;
128 129 130
    // Traversing nodes using topological sorting to calculate the
    // horizontal(if orient === 'horizontal') or vertical(if orient === 'vertical')
    // position of the nodes.
131
    while (zeroIndegrees.length) {
S
sushuang 已提交
132 133
        for (var idx = 0; idx < zeroIndegrees.length; idx++) {
            var node = zeroIndegrees[idx];
134
            var item = node.hostGraph.data.getRawDataItem(node.dataIndex);
135
            var isItemDepth = item.depth != null && item.depth >= 0;
136 137 138
            if (isItemDepth && item.depth > maxNodeDepth) {
                maxNodeDepth = item.depth;
            }
139
            node.setLayout({depth: isItemDepth ? item.depth : x}, true);
140 141 142
            orient === 'vertical'
                ? node.setLayout({dy: nodeWidth}, true)
                : node.setLayout({dx: nodeWidth}, true);
143

144 145
            for (var edgeIdx = 0; edgeIdx < node.outEdges.length; edgeIdx++) {
                var edge = node.outEdges[edgeIdx];
146 147
                var indexEdge = edges.indexOf(edge);
                remainEdges[indexEdge] = 0;
148
                var targetNode = edge.node2;
149
                var nodeIndex = nodes.indexOf(targetNode);
150 151
                if (--indegreeArr[nodeIndex] === 0 && nextTargetNode.indexOf(targetNode) < 0) {
                    nextTargetNode.push(targetNode);
152
                }
S
sushuang 已提交
153 154
            }
        }
S
sushuang 已提交
155
        ++x;
156 157
        zeroIndegrees = nextTargetNode;
        nextTargetNode = [];
D
deqingli 已提交
158
    }
159 160

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

166 167 168 169
    var maxDepth = maxNodeDepth > x - 1 ? maxNodeDepth : x - 1;
    if (nodeAlign && nodeAlign !== 'left') {
        adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth);
    }
170 171
    var kx = orient === 'vertical'
                ? (height - nodeWidth) / maxDepth
172
                : (width - nodeWidth) / maxDepth;
173

174 175 176 177 178
    scaleNodeBreadths(nodes, kx, orient);
}

function isNodeDepth(node) {
    var item = node.hostGraph.data.getRawDataItem(node.dataIndex);
179
    return item.depth != null && item.depth >= 0;
180 181 182
}

function adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth) {
183 184
    if (nodeAlign === 'right') {
        var nextSourceNode = [];
185
        var remainNodes = nodes;
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
        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;
        }

203
        zrUtil.each(nodes, function (node) {
204
            if (!isNodeDepth(node)) {
205
                node.setLayout({depth: Math.max(0, maxDepth - node.getLayout().skNodeHeight)}, true);
206 207 208
            }
        });
    }
209
    else if (nodeAlign === 'justify') {
210
        moveSinksRight(nodes, maxDepth);
211
    }
S
sushuang 已提交
212
}
D
deqingli 已提交
213

S
sushuang 已提交
214 215 216 217
/**
 * All the node without outEgdes are assigned maximum x-position and
 *     be aligned in the last column.
 *
218 219
 * @param {module:echarts/data/Graph~Node} nodes.  node of sankey view.
 * @param {number} maxDepth.  use to assign to node without outEdges as x-position.
S
sushuang 已提交
220
 */
221
function moveSinksRight(nodes, maxDepth) {
S
sushuang 已提交
222
    zrUtil.each(nodes, function (node) {
223
        if (!isNodeDepth(node) && !node.outEdges.length) {
224
            node.setLayout({depth: maxDepth}, true);
D
deqingli 已提交
225
        }
S
sushuang 已提交
226 227
    });
}
D
deqingli 已提交
228

S
sushuang 已提交
229 230 231 232 233 234
/**
 * 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
 */
235
function scaleNodeBreadths(nodes, kx, orient) {
S
sushuang 已提交
236
    zrUtil.each(nodes, function (node) {
237
        var nodeDepth = node.getLayout().depth * kx;
238 239 240
        orient === 'vertical'
            ? node.setLayout({y: nodeDepth}, true)
            : node.setLayout({x: nodeDepth}, true);
S
sushuang 已提交
241 242 243 244 245 246 247 248 249 250 251 252 253
    });
}

/**
 * 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
 */
254
function computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient) {
255
    var nodesByBreadth = prepareNodesByBreadth(nodes, orient);
256

257
    initializeNodeDepth(nodesByBreadth, edges, height, width, nodeGap, orient);
258
    resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
S
sushuang 已提交
259 260 261 262 263

    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;
264 265 266 267
        relaxRightToLeft(nodesByBreadth, alpha, orient);
        resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
        relaxLeftToRight(nodesByBreadth, alpha, orient);
        resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);
S
sushuang 已提交
268 269 270
    }
}

271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
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;
286 287
}

S
sushuang 已提交
288 289 290 291 292 293 294 295 296 297
/**
 * 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
 */
298
function initializeNodeDepth(nodesByBreadth, edges, height, width, nodeGap, orient) {
299
    var minKy = Infinity;
S
sushuang 已提交
300 301 302 303 304
    zrUtil.each(nodesByBreadth, function (nodes) {
        var n = nodes.length;
        var sum = 0;
        zrUtil.each(nodes, function (node) {
            sum += node.getLayout().value;
D
deqingli 已提交
305
        });
306 307 308 309
        var ky = orient === 'vertical'
                    ? (width - (n - 1) * nodeGap) / sum
                    : (height - (n - 1) * nodeGap) / sum;

310 311
        if (ky < minKy) {
            minKy = ky;
312
        }
S
sushuang 已提交
313 314 315 316
    });

    zrUtil.each(nodesByBreadth, function (nodes) {
        zrUtil.each(nodes, function (node, i) {
317
            var nodeDy = node.getLayout().value * minKy;
318 319 320 321 322 323 324 325
            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 已提交
326
        });
S
sushuang 已提交
327
    });
D
deqingli 已提交
328

S
sushuang 已提交
329
    zrUtil.each(edges, function (edge) {
330
        var edgeDy = +edge.getValue() * minKy;
S
sushuang 已提交
331 332 333 334 335 336 337 338 339 340 341 342
        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
 */
343
function resolveCollisions(nodesByBreadth, nodeGap, height, width, orient) {
344
    var keyAttr = orient === 'vertical' ? 'x' : 'y';
S
sushuang 已提交
345
    zrUtil.each(nodesByBreadth, function (nodes) {
346 347 348 349
        nodes.sort(function (a, b) {
            return a.getLayout()[keyAttr] - b.getLayout()[keyAttr];
        });
        var nodeX;
S
sushuang 已提交
350 351 352 353
        var node;
        var dy;
        var y0 = 0;
        var n = nodes.length;
354 355 356 357
        var nodeDyAttr = orient === 'vertical' ? 'dx' : 'dy';
        for (var i = 0; i < n; i++) {
            node = nodes[i];
            dy = y0 - node.getLayout()[keyAttr];
S
sushuang 已提交
358
            if (dy > 0) {
359
                nodeX = node.getLayout()[keyAttr] + dy;
360 361 362
                orient === 'vertical'
                    ? node.setLayout({x: nodeX}, true)
                    : node.setLayout({y: nodeX}, true);
S
sushuang 已提交
363
            }
364
            y0 = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap;
S
sushuang 已提交
365
        }
366 367 368 369 370
        var viewWidth = orient === 'vertical' ? width : height;
        // If the bottommost node goes outside the bounds, push it back up
        dy = y0 - nodeGap - viewWidth;
        if (dy > 0) {
            nodeX = node.getLayout()[keyAttr] - dy;
371 372 373 374
            orient === 'vertical'
                ? node.setLayout({x: nodeX}, true)
                : node.setLayout({y: nodeX}, true);

375 376
            y0 = nodeX;
            for (i = n - 2; i >= 0; --i) {
D
deqingli 已提交
377
                node = nodes[i];
378
                dy = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap - y0;
379
                if (dy > 0) {
380
                    nodeX = node.getLayout()[keyAttr] - dy;
381 382 383
                    orient === 'vertical'
                        ? node.setLayout({x: nodeX}, true)
                        : node.setLayout({y: nodeX}, true);
384
                }
385
                y0 = node.getLayout()[keyAttr];
D
deqingli 已提交
386
            }
S
sushuang 已提交
387 388 389
        }
    });
}
D
deqingli 已提交
390

S
sushuang 已提交
391 392 393 394 395 396 397
/**
 * 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
 */
398
function relaxRightToLeft(nodesByBreadth, alpha, orient) {
S
sushuang 已提交
399 400 401
    zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) {
        zrUtil.each(nodes, function (node) {
            if (node.outEdges.length) {
402 403
                var y = sum(node.outEdges, weightedTarget, orient)
                        / sum(node.outEdges, getEdgeValue, orient);
404 405 406 407 408 409 410 411
                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 已提交
412
            }
D
deqingli 已提交
413
        });
S
sushuang 已提交
414 415
    });
}
D
deqingli 已提交
416

417 418 419 420 421 422 423 424 425
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) {
426 427
    return orient === 'vertical'
            ? node.getLayout().x + node.getLayout().dx / 2
428
            : node.getLayout().y + node.getLayout().dy / 2;
429 430 431 432 433 434
}

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

D
deqingli 已提交
435
function sum(array, cb, orient) {
436 437 438 439
    var sum = 0;
    var len = array.length;
    var i = -1;
    while (++i < len) {
D
deqingli 已提交
440
        var value = +cb.call(array, array[i], orient);
441 442 443 444 445
        if (!isNaN(value)) {
            sum += value;
        }
    }
    return sum;
S
sushuang 已提交
446
}
D
deqingli 已提交
447

S
sushuang 已提交
448 449 450 451 452 453 454
/**
 * 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
 */
455
function relaxLeftToRight(nodesByBreadth, alpha, orient) {
S
sushuang 已提交
456 457 458
    zrUtil.each(nodesByBreadth, function (nodes) {
        zrUtil.each(nodes, function (node) {
            if (node.inEdges.length) {
459 460
                var y = sum(node.inEdges, weightedSource, orient)
                        / sum(node.inEdges, getEdgeValue, orient);
461 462 463 464 465 466 467 468
                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 已提交
469
            }
D
deqingli 已提交
470
        });
S
sushuang 已提交
471 472
    });
}
D
deqingli 已提交
473

S
sushuang 已提交
474 475 476 477 478
/**
 * Compute the depth(y-position) of each edge
 *
 * @param {module:echarts/data/Graph~Node} nodes  node of sankey view
 */
479
function computeEdgeDepths(nodes, orient) {
480
    var keyAttr = orient === 'vertical' ? 'x' : 'y';
S
sushuang 已提交
481
    zrUtil.each(nodes, function (node) {
482 483 484 485 486 487
        node.outEdges.sort(function (a, b) {
            return a.node2.getLayout()[keyAttr] - b.node2.getLayout()[keyAttr];
        });
        node.inEdges.sort(function (a, b) {
            return a.node1.getLayout()[keyAttr] - b.node1.getLayout()[keyAttr];
        });
S
sushuang 已提交
488 489 490 491 492 493 494
    });
    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 已提交
495
        });
S
sushuang 已提交
496 497 498
        zrUtil.each(node.inEdges, function (edge) {
            edge.setLayout({ty: ty}, true);
            ty += edge.getLayout().dy;
D
deqingli 已提交
499
        });
S
sushuang 已提交
500 501
    });
}