| /* |
| * 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; |
| }); |
| }); |
| } |