|  | /* | 
|  | * 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. | 
|  | */ | 
|  |  | 
|  | import * as layout from '../../util/layout'; | 
|  | import * as zrUtil from 'zrender/src/core/util'; | 
|  | import {groupData} from '../../util/model'; | 
|  | import ExtensionAPI from '../../core/ExtensionAPI'; | 
|  | import SankeySeriesModel, { SankeySeriesOption, SankeyNodeItemOption } from './SankeySeries'; | 
|  | import { GraphNode, GraphEdge } from '../../data/Graph'; | 
|  | import { LayoutOrient } from '../../util/types'; | 
|  | import GlobalModel from '../../model/Global'; | 
|  |  | 
|  | export default function sankeyLayout(ecModel: GlobalModel, api: ExtensionAPI) { | 
|  |  | 
|  | ecModel.eachSeriesByType('sankey', function (seriesModel: SankeySeriesModel) { | 
|  |  | 
|  | const nodeWidth = seriesModel.get('nodeWidth'); | 
|  | const nodeGap = seriesModel.get('nodeGap'); | 
|  |  | 
|  | const layoutInfo = getViewRect(seriesModel, api); | 
|  |  | 
|  | seriesModel.layoutInfo = layoutInfo; | 
|  |  | 
|  | const width = layoutInfo.width; | 
|  | const height = layoutInfo.height; | 
|  |  | 
|  | const graph = seriesModel.getGraph(); | 
|  |  | 
|  | const nodes = graph.nodes; | 
|  | const edges = graph.edges; | 
|  |  | 
|  | computeNodeValues(nodes); | 
|  |  | 
|  | const filteredNodes = zrUtil.filter(nodes, function (node) { | 
|  | return node.getLayout().value === 0; | 
|  | }); | 
|  |  | 
|  | const iterations = filteredNodes.length !== 0 ? 0 : seriesModel.get('layoutIterations'); | 
|  |  | 
|  | const orient = seriesModel.get('orient'); | 
|  |  | 
|  | const nodeAlign = seriesModel.get('nodeAlign'); | 
|  |  | 
|  | layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Get the layout position of the whole view | 
|  | */ | 
|  | function getViewRect(seriesModel: SankeySeriesModel, api: ExtensionAPI) { | 
|  | return layout.getLayoutRect( | 
|  | seriesModel.getBoxLayoutParams(), { | 
|  | width: api.getWidth(), | 
|  | height: api.getHeight() | 
|  | } | 
|  | ); | 
|  | } | 
|  |  | 
|  | function layoutSankey( | 
|  | nodes: GraphNode[], | 
|  | edges: GraphEdge[], | 
|  | nodeWidth: number, | 
|  | nodeGap: number, | 
|  | width: number, | 
|  | height: number, | 
|  | iterations: number, | 
|  | orient: LayoutOrient, | 
|  | nodeAlign: SankeySeriesOption['nodeAlign'] | 
|  | ) { | 
|  | computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign); | 
|  | computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient); | 
|  | computeEdgeDepths(nodes, orient); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Compute the value of each node by summing the associated edge's value | 
|  | */ | 
|  | function computeNodeValues(nodes: GraphNode[]) { | 
|  | zrUtil.each(nodes, function (node) { | 
|  | const value1 = sum(node.outEdges, getEdgeValue); | 
|  | const value2 = sum(node.inEdges, getEdgeValue); | 
|  | const nodeRawValue = node.getValue() as number || 0; | 
|  | const value = Math.max(value1, value2, nodeRawValue); | 
|  | node.setLayout({value: value}, true); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Compute the x-position for each node. | 
|  | * | 
|  | * Here we use Kahn algorithm to detect cycle when we traverse | 
|  | * the node to computer the initial x position. | 
|  | */ | 
|  | function computeNodeBreadths( | 
|  | nodes: GraphNode[], | 
|  | edges: GraphEdge[], | 
|  | nodeWidth: number, | 
|  | width: number, | 
|  | height: number, | 
|  | orient: LayoutOrient, | 
|  | nodeAlign: SankeySeriesOption['nodeAlign'] | 
|  | ) { | 
|  | // Used to mark whether the edge is deleted. if it is deleted, | 
|  | // the value is 0, otherwise it is 1. | 
|  | const remainEdges = []; | 
|  | // Storage each node's indegree. | 
|  | const indegreeArr = []; | 
|  | // Used to storage the node with indegree is equal to 0. | 
|  | let zeroIndegrees: GraphNode[] = []; | 
|  | let nextTargetNode: GraphNode[] = []; | 
|  | let x = 0; | 
|  | // let kx = 0; | 
|  |  | 
|  | for (let i = 0; i < edges.length; i++) { | 
|  | remainEdges[i] = 1; | 
|  | } | 
|  | for (let i = 0; i < nodes.length; i++) { | 
|  | indegreeArr[i] = nodes[i].inEdges.length; | 
|  | if (indegreeArr[i] === 0) { | 
|  | zeroIndegrees.push(nodes[i]); | 
|  | } | 
|  | } | 
|  | let maxNodeDepth = -1; | 
|  | // Traversing nodes using topological sorting to calculate the | 
|  | // horizontal(if orient === 'horizontal') or vertical(if orient === 'vertical') | 
|  | // position of the nodes. | 
|  | while (zeroIndegrees.length) { | 
|  | for (let idx = 0; idx < zeroIndegrees.length; idx++) { | 
|  | const node = zeroIndegrees[idx]; | 
|  | const item = node.hostGraph.data.getRawDataItem(node.dataIndex) as SankeyNodeItemOption; | 
|  | const isItemDepth = item.depth != null && item.depth >= 0; | 
|  | if (isItemDepth && item.depth > maxNodeDepth) { | 
|  | maxNodeDepth = item.depth; | 
|  | } | 
|  | node.setLayout({depth: isItemDepth ? item.depth : x}, true); | 
|  | orient === 'vertical' | 
|  | ? node.setLayout({dy: nodeWidth}, true) | 
|  | : node.setLayout({dx: nodeWidth}, true); | 
|  |  | 
|  | for (let edgeIdx = 0; edgeIdx < node.outEdges.length; edgeIdx++) { | 
|  | const edge = node.outEdges[edgeIdx]; | 
|  | const indexEdge = edges.indexOf(edge); | 
|  | remainEdges[indexEdge] = 0; | 
|  | const targetNode = edge.node2; | 
|  | const nodeIndex = nodes.indexOf(targetNode); | 
|  | if (--indegreeArr[nodeIndex] === 0 && nextTargetNode.indexOf(targetNode) < 0) { | 
|  | nextTargetNode.push(targetNode); | 
|  | } | 
|  | } | 
|  | } | 
|  | ++x; | 
|  | zeroIndegrees = nextTargetNode; | 
|  | nextTargetNode = []; | 
|  | } | 
|  |  | 
|  | for (let i = 0; i < remainEdges.length; i++) { | 
|  | if (remainEdges[i] === 1) { | 
|  | throw new Error('Sankey is a DAG, the original data has cycle!'); | 
|  | } | 
|  | } | 
|  |  | 
|  | const maxDepth = maxNodeDepth > x - 1 ? maxNodeDepth : x - 1; | 
|  | if (nodeAlign && nodeAlign !== 'left') { | 
|  | adjustNodeWithNodeAlign(nodes, nodeAlign, orient, maxDepth); | 
|  | } | 
|  | const kx = orient === 'vertical' | 
|  | ? (height - nodeWidth) / maxDepth | 
|  | : (width - nodeWidth) / maxDepth; | 
|  |  | 
|  | scaleNodeBreadths(nodes, kx, orient); | 
|  | } | 
|  |  | 
|  | function isNodeDepth(node: GraphNode) { | 
|  | const item = node.hostGraph.data.getRawDataItem(node.dataIndex) as SankeyNodeItemOption; | 
|  | return item.depth != null && item.depth >= 0; | 
|  | } | 
|  |  | 
|  | function adjustNodeWithNodeAlign( | 
|  | nodes: GraphNode[], | 
|  | nodeAlign: SankeySeriesOption['nodeAlign'], | 
|  | orient: LayoutOrient, | 
|  | maxDepth: number | 
|  | ) { | 
|  | if (nodeAlign === 'right') { | 
|  | let nextSourceNode: GraphNode[] = []; | 
|  | let remainNodes = nodes; | 
|  | let nodeHeight = 0; | 
|  | while (remainNodes.length) { | 
|  | for (let i = 0; i < remainNodes.length; i++) { | 
|  | const node = remainNodes[i]; | 
|  | node.setLayout({skNodeHeight: nodeHeight}, true); | 
|  | for (let j = 0; j < node.inEdges.length; j++) { | 
|  | const edge = node.inEdges[j]; | 
|  | if (nextSourceNode.indexOf(edge.node1) < 0) { | 
|  | nextSourceNode.push(edge.node1); | 
|  | } | 
|  | } | 
|  | } | 
|  | remainNodes = nextSourceNode; | 
|  | nextSourceNode = []; | 
|  | ++nodeHeight; | 
|  | } | 
|  |  | 
|  | zrUtil.each(nodes, function (node) { | 
|  | if (!isNodeDepth(node)) { | 
|  | node.setLayout({depth: Math.max(0, maxDepth - node.getLayout().skNodeHeight)}, true); | 
|  | } | 
|  | }); | 
|  | } | 
|  | else if (nodeAlign === 'justify') { | 
|  | moveSinksRight(nodes, maxDepth); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * All the node without outEgdes are assigned maximum x-position and | 
|  | *     be aligned in the last column. | 
|  | * | 
|  | * @param nodes.  node of sankey view. | 
|  | * @param maxDepth.  use to assign to node without outEdges as x-position. | 
|  | */ | 
|  | function moveSinksRight(nodes: GraphNode[], maxDepth: number) { | 
|  | zrUtil.each(nodes, function (node) { | 
|  | if (!isNodeDepth(node) && !node.outEdges.length) { | 
|  | node.setLayout({depth: maxDepth}, true); | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Scale node x-position to the width | 
|  | * | 
|  | * @param nodes  node of sankey view | 
|  | * @param kx   multiple used to scale nodes | 
|  | */ | 
|  | function scaleNodeBreadths(nodes: GraphNode[], kx: number, orient: LayoutOrient) { | 
|  | zrUtil.each(nodes, function (node) { | 
|  | const nodeDepth = node.getLayout().depth * kx; | 
|  | orient === 'vertical' | 
|  | ? node.setLayout({y: nodeDepth}, true) | 
|  | : node.setLayout({x: nodeDepth}, true); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Using Gauss-Seidel iterations method to compute the node depth(y-position) | 
|  | * | 
|  | * @param nodes  node of sankey view | 
|  | * @param edges  edge of sankey view | 
|  | * @param height  the whole height of the area to draw the view | 
|  | * @param nodeGap  the vertical distance between two nodes | 
|  | *     in the same column. | 
|  | * @param iterations  the number of iterations for the algorithm | 
|  | */ | 
|  | function computeNodeDepths( | 
|  | nodes: GraphNode[], | 
|  | edges: GraphEdge[], | 
|  | height: number, | 
|  | width: number, | 
|  | nodeGap: number, | 
|  | iterations: number, | 
|  | orient: LayoutOrient | 
|  | ) { | 
|  | const nodesByBreadth = prepareNodesByBreadth(nodes, orient); | 
|  |  | 
|  | initializeNodeDepth(nodesByBreadth, edges, height, width, nodeGap, orient); | 
|  | resolveCollisions(nodesByBreadth, nodeGap, height, width, orient); | 
|  |  | 
|  | for (let alpha = 1; iterations > 0; iterations--) { | 
|  | // 0.99 is a experience parameter, ensure that each iterations of | 
|  | // changes as small as possible. | 
|  | alpha *= 0.99; | 
|  | relaxRightToLeft(nodesByBreadth, alpha, orient); | 
|  | resolveCollisions(nodesByBreadth, nodeGap, height, width, orient); | 
|  | relaxLeftToRight(nodesByBreadth, alpha, orient); | 
|  | resolveCollisions(nodesByBreadth, nodeGap, height, width, orient); | 
|  | } | 
|  | } | 
|  |  | 
|  | function prepareNodesByBreadth(nodes: GraphNode[], orient: LayoutOrient) { | 
|  | const nodesByBreadth: GraphNode[][] = []; | 
|  | const keyAttr = orient === 'vertical' ? 'y' : 'x'; | 
|  |  | 
|  | const groupResult = groupData(nodes, function (node) { | 
|  | return node.getLayout()[keyAttr] as number; | 
|  | }); | 
|  | groupResult.keys.sort(function (a, b) { | 
|  | return a - b; | 
|  | }); | 
|  | zrUtil.each(groupResult.keys, function (key) { | 
|  | nodesByBreadth.push(groupResult.buckets.get(key)); | 
|  | }); | 
|  |  | 
|  | return nodesByBreadth; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Compute the original y-position for each node | 
|  | */ | 
|  | function initializeNodeDepth( | 
|  | nodesByBreadth: GraphNode[][], | 
|  | edges: GraphEdge[], | 
|  | height: number, | 
|  | width: number, | 
|  | nodeGap: number, | 
|  | orient: LayoutOrient | 
|  | ) { | 
|  | let minKy = Infinity; | 
|  | zrUtil.each(nodesByBreadth, function (nodes) { | 
|  | const n = nodes.length; | 
|  | let sum = 0; | 
|  | zrUtil.each(nodes, function (node) { | 
|  | sum += node.getLayout().value; | 
|  | }); | 
|  | const ky = orient === 'vertical' | 
|  | ? (width - (n - 1) * nodeGap) / sum | 
|  | : (height - (n - 1) * nodeGap) / sum; | 
|  |  | 
|  | if (ky < minKy) { | 
|  | minKy = ky; | 
|  | } | 
|  | }); | 
|  |  | 
|  | zrUtil.each(nodesByBreadth, function (nodes) { | 
|  | zrUtil.each(nodes, function (node, i) { | 
|  | const nodeDy = node.getLayout().value * minKy; | 
|  | if (orient === 'vertical') { | 
|  | node.setLayout({x: i}, true); | 
|  | node.setLayout({dx: nodeDy}, true); | 
|  | } | 
|  | else { | 
|  | node.setLayout({y: i}, true); | 
|  | node.setLayout({dy: nodeDy}, true); | 
|  | } | 
|  | }); | 
|  | }); | 
|  |  | 
|  | zrUtil.each(edges, function (edge) { | 
|  | const edgeDy = +edge.getValue() * minKy; | 
|  | edge.setLayout({dy: edgeDy}, true); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Resolve the collision of initialized depth (y-position) | 
|  | */ | 
|  | function resolveCollisions( | 
|  | nodesByBreadth: GraphNode[][], | 
|  | nodeGap: number, | 
|  | height: number, | 
|  | width: number, | 
|  | orient: LayoutOrient | 
|  | ) { | 
|  | const keyAttr = orient === 'vertical' ? 'x' : 'y'; | 
|  | zrUtil.each(nodesByBreadth, function (nodes) { | 
|  | nodes.sort(function (a, b) { | 
|  | return a.getLayout()[keyAttr] - b.getLayout()[keyAttr]; | 
|  | }); | 
|  | let nodeX; | 
|  | let node; | 
|  | let dy; | 
|  | let y0 = 0; | 
|  | const n = nodes.length; | 
|  | const nodeDyAttr = orient === 'vertical' ? 'dx' : 'dy'; | 
|  | for (let i = 0; i < n; i++) { | 
|  | node = nodes[i]; | 
|  | dy = y0 - node.getLayout()[keyAttr]; | 
|  | if (dy > 0) { | 
|  | nodeX = node.getLayout()[keyAttr] + dy; | 
|  | orient === 'vertical' | 
|  | ? node.setLayout({x: nodeX}, true) | 
|  | : node.setLayout({y: nodeX}, true); | 
|  | } | 
|  | y0 = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap; | 
|  | } | 
|  | const 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; | 
|  | orient === 'vertical' | 
|  | ? node.setLayout({x: nodeX}, true) | 
|  | : node.setLayout({y: nodeX}, true); | 
|  |  | 
|  | y0 = nodeX; | 
|  | for (let i = n - 2; i >= 0; --i) { | 
|  | node = nodes[i]; | 
|  | dy = node.getLayout()[keyAttr] + node.getLayout()[nodeDyAttr] + nodeGap - y0; | 
|  | if (dy > 0) { | 
|  | nodeX = node.getLayout()[keyAttr] - dy; | 
|  | orient === 'vertical' | 
|  | ? node.setLayout({x: nodeX}, true) | 
|  | : node.setLayout({y: nodeX}, true); | 
|  | } | 
|  | y0 = node.getLayout()[keyAttr]; | 
|  | } | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Change the y-position of the nodes, except most the right side nodes | 
|  | * @param nodesByBreadth | 
|  | * @param alpha  parameter used to adjust the nodes y-position | 
|  | */ | 
|  | function relaxRightToLeft( | 
|  | nodesByBreadth: GraphNode[][], | 
|  | alpha: number, | 
|  | orient: LayoutOrient | 
|  | ) { | 
|  | zrUtil.each(nodesByBreadth.slice().reverse(), function (nodes) { | 
|  | zrUtil.each(nodes, function (node) { | 
|  | if (node.outEdges.length) { | 
|  | let y = sum(node.outEdges, weightedTarget, orient) | 
|  | / sum(node.outEdges, getEdgeValue); | 
|  |  | 
|  | if (isNaN(y)) { | 
|  | const len = node.outEdges.length; | 
|  | y = len ? sum(node.outEdges, centerTarget, orient) / len : 0; | 
|  | } | 
|  |  | 
|  | if (orient === 'vertical') { | 
|  | const nodeX = node.getLayout().x + (y - center(node, orient)) * alpha; | 
|  | node.setLayout({x: nodeX}, true); | 
|  | } | 
|  | else { | 
|  | const nodeY = node.getLayout().y + (y - center(node, orient)) * alpha; | 
|  | node.setLayout({y: nodeY}, true); | 
|  | } | 
|  | } | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | function weightedTarget(edge: GraphEdge, orient: LayoutOrient) { | 
|  | return center(edge.node2, orient) * (edge.getValue() as number); | 
|  | } | 
|  | function centerTarget(edge: GraphEdge, orient: LayoutOrient) { | 
|  | return center(edge.node2, orient); | 
|  | } | 
|  |  | 
|  | function weightedSource(edge: GraphEdge, orient: LayoutOrient) { | 
|  | return center(edge.node1, orient) * (edge.getValue() as number); | 
|  | } | 
|  | function centerSource(edge: GraphEdge, orient: LayoutOrient) { | 
|  | return center(edge.node1, orient); | 
|  | } | 
|  |  | 
|  | function center(node: GraphNode, orient: LayoutOrient) { | 
|  | return orient === 'vertical' | 
|  | ? node.getLayout().x + node.getLayout().dx / 2 | 
|  | : node.getLayout().y + node.getLayout().dy / 2; | 
|  | } | 
|  |  | 
|  | function getEdgeValue(edge: GraphEdge) { | 
|  | return edge.getValue() as number; | 
|  | } | 
|  |  | 
|  | function sum<T>(array: T[], cb: (item: T, orient?: LayoutOrient) => number, orient?: LayoutOrient) { | 
|  | let sum = 0; | 
|  | const len = array.length; | 
|  | let i = -1; | 
|  | while (++i < len) { | 
|  | const value = +cb(array[i], orient); | 
|  | if (!isNaN(value)) { | 
|  | sum += value; | 
|  | } | 
|  | } | 
|  | return sum; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Change the y-position of the nodes, except most the left side nodes | 
|  | */ | 
|  | function relaxLeftToRight(nodesByBreadth: GraphNode[][], alpha: number, orient: LayoutOrient) { | 
|  | zrUtil.each(nodesByBreadth, function (nodes) { | 
|  | zrUtil.each(nodes, function (node) { | 
|  | if (node.inEdges.length) { | 
|  | let y = sum(node.inEdges, weightedSource, orient) | 
|  | / sum(node.inEdges, getEdgeValue); | 
|  |  | 
|  | if (isNaN(y)) { | 
|  | const len = node.inEdges.length; | 
|  | y = len ? sum(node.inEdges, centerSource, orient) / len : 0; | 
|  | } | 
|  |  | 
|  | if (orient === 'vertical') { | 
|  | const nodeX = node.getLayout().x + (y - center(node, orient)) * alpha; | 
|  | node.setLayout({x: nodeX}, true); | 
|  | } | 
|  | else { | 
|  | const nodeY = node.getLayout().y + (y - center(node, orient)) * alpha; | 
|  | node.setLayout({y: nodeY}, true); | 
|  | } | 
|  | } | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Compute the depth(y-position) of each edge | 
|  | */ | 
|  | function computeEdgeDepths(nodes: GraphNode[], orient: LayoutOrient) { | 
|  | const keyAttr = orient === 'vertical' ? 'x' : 'y'; | 
|  | zrUtil.each(nodes, function (node) { | 
|  | 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]; | 
|  | }); | 
|  | }); | 
|  | zrUtil.each(nodes, function (node) { | 
|  | let sy = 0; | 
|  | let ty = 0; | 
|  | zrUtil.each(node.outEdges, function (edge) { | 
|  | edge.setLayout({sy: sy}, true); | 
|  | sy += edge.getLayout().dy; | 
|  | }); | 
|  | zrUtil.each(node.inEdges, function (edge) { | 
|  | edge.setLayout({ty: ty}, true); | 
|  | ty += edge.getLayout().dy; | 
|  | }); | 
|  | }); | 
|  | } |