| /* | 
 | * 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 graphic from '../../util/graphic'; | 
 | import { enterEmphasis, leaveEmphasis, toggleHoverEmphasis, setStatesStylesFromModel } from '../../util/states'; | 
 | import { LayoutOrient, ECElement } from '../../util/types'; | 
 | import { PathProps } from 'zrender/src/graphic/Path'; | 
 | import SankeySeriesModel, { SankeyEdgeItemOption, SankeyNodeItemOption } from './SankeySeries'; | 
 | import ChartView from '../../view/Chart'; | 
 | import GlobalModel from '../../model/Global'; | 
 | import ExtensionAPI from '../../core/ExtensionAPI'; | 
 | import SeriesData from '../../data/SeriesData'; | 
 | import { RectLike } from 'zrender/src/core/BoundingRect'; | 
 | import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; | 
 | import { getECData } from '../../util/innerStore'; | 
 | import { isString } from 'zrender/src/core/util'; | 
 |  | 
 | class SankeyPathShape { | 
 |     x1 = 0; | 
 |     y1 = 0; | 
 |  | 
 |     x2 = 0; | 
 |     y2 = 0; | 
 |  | 
 |     cpx1 = 0; | 
 |     cpy1 = 0; | 
 |  | 
 |     cpx2 = 0; | 
 |     cpy2 = 0; | 
 |  | 
 |     extent = 0; | 
 |     orient: LayoutOrient; | 
 | } | 
 |  | 
 | interface SankeyPathProps extends PathProps { | 
 |     shape?: Partial<SankeyPathShape> | 
 | } | 
 |  | 
 | class SankeyPath extends graphic.Path<SankeyPathProps> { | 
 |     shape: SankeyPathShape; | 
 |  | 
 |     constructor(opts?: SankeyPathProps) { | 
 |         super(opts); | 
 |     } | 
 |  | 
 |     getDefaultShape() { | 
 |         return new SankeyPathShape(); | 
 |     } | 
 |  | 
 |     buildPath(ctx: CanvasRenderingContext2D, shape: SankeyPathShape) { | 
 |         const extent = shape.extent; | 
 |         ctx.moveTo(shape.x1, shape.y1); | 
 |         ctx.bezierCurveTo( | 
 |             shape.cpx1, shape.cpy1, | 
 |             shape.cpx2, shape.cpy2, | 
 |             shape.x2, shape.y2 | 
 |         ); | 
 |         if (shape.orient === 'vertical') { | 
 |             ctx.lineTo(shape.x2 + extent, shape.y2); | 
 |             ctx.bezierCurveTo( | 
 |                 shape.cpx2 + extent, shape.cpy2, | 
 |                 shape.cpx1 + extent, shape.cpy1, | 
 |                 shape.x1 + extent, shape.y1 | 
 |             ); | 
 |         } | 
 |         else { | 
 |             ctx.lineTo(shape.x2, shape.y2 + extent); | 
 |             ctx.bezierCurveTo( | 
 |                 shape.cpx2, shape.cpy2 + extent, | 
 |                 shape.cpx1, shape.cpy1 + extent, | 
 |                 shape.x1, shape.y1 + extent | 
 |             ); | 
 |         } | 
 |         ctx.closePath(); | 
 |     } | 
 |  | 
 |     highlight() { | 
 |         enterEmphasis(this); | 
 |     } | 
 |  | 
 |     downplay() { | 
 |         leaveEmphasis(this); | 
 |     } | 
 | } | 
 |  | 
 | class SankeyView extends ChartView { | 
 |  | 
 |     static readonly type = 'sankey'; | 
 |     readonly type = SankeyView.type; | 
 |  | 
 |     private _model: SankeySeriesModel; | 
 |  | 
 |     private _focusAdjacencyDisabled = false; | 
 |  | 
 |     private _data: SeriesData; | 
 |  | 
 |     render(seriesModel: SankeySeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         const sankeyView = this; | 
 |         const graph = seriesModel.getGraph(); | 
 |         const group = this.group; | 
 |         const layoutInfo = seriesModel.layoutInfo; | 
 |         // view width | 
 |         const width = layoutInfo.width; | 
 |         // view height | 
 |         const height = layoutInfo.height; | 
 |         const nodeData = seriesModel.getData(); | 
 |         const edgeData = seriesModel.getData('edge'); | 
 |         const orient = seriesModel.get('orient'); | 
 |  | 
 |         this._model = seriesModel; | 
 |  | 
 |         group.removeAll(); | 
 |  | 
 |         group.x = layoutInfo.x; | 
 |         group.y = layoutInfo.y; | 
 |  | 
 |         // generate a bezire Curve for each edge | 
 |         graph.eachEdge(function (edge) { | 
 |             const curve = new SankeyPath(); | 
 |             const ecData = getECData(curve); | 
 |             ecData.dataIndex = edge.dataIndex; | 
 |             ecData.seriesIndex = seriesModel.seriesIndex; | 
 |             ecData.dataType = 'edge'; | 
 |             const edgeModel = edge.getModel<SankeyEdgeItemOption>(); | 
 |             const lineStyleModel = edgeModel.getModel('lineStyle'); | 
 |             const curvature = lineStyleModel.get('curveness'); | 
 |             const n1Layout = edge.node1.getLayout(); | 
 |             const node1Model = edge.node1.getModel<SankeyNodeItemOption>(); | 
 |             const dragX1 = node1Model.get('localX'); | 
 |             const dragY1 = node1Model.get('localY'); | 
 |             const n2Layout = edge.node2.getLayout(); | 
 |             const node2Model = edge.node2.getModel<SankeyNodeItemOption>(); | 
 |             const dragX2 = node2Model.get('localX'); | 
 |             const dragY2 = node2Model.get('localY'); | 
 |             const edgeLayout = edge.getLayout(); | 
 |             let x1: number; | 
 |             let y1: number; | 
 |             let x2: number; | 
 |             let y2: number; | 
 |             let cpx1: number; | 
 |             let cpy1: number; | 
 |             let cpx2: number; | 
 |             let cpy2: number; | 
 |  | 
 |             curve.shape.extent = Math.max(1, edgeLayout.dy); | 
 |             curve.shape.orient = orient; | 
 |  | 
 |             if (orient === 'vertical') { | 
 |                 x1 = (dragX1 != null ? dragX1 * width : n1Layout.x) + edgeLayout.sy; | 
 |                 y1 = (dragY1 != null ? dragY1 * height : n1Layout.y) + n1Layout.dy; | 
 |                 x2 = (dragX2 != null ? dragX2 * width : n2Layout.x) + edgeLayout.ty; | 
 |                 y2 = dragY2 != null ? dragY2 * height : n2Layout.y; | 
 |                 cpx1 = x1; | 
 |                 cpy1 = y1 * (1 - curvature) + y2 * curvature; | 
 |                 cpx2 = x2; | 
 |                 cpy2 = y1 * curvature + y2 * (1 - curvature); | 
 |             } | 
 |             else { | 
 |                 x1 = (dragX1 != null ? dragX1 * width : n1Layout.x) + n1Layout.dx; | 
 |                 y1 = (dragY1 != null ? dragY1 * height : n1Layout.y) + edgeLayout.sy; | 
 |                 x2 = dragX2 != null ? dragX2 * width : n2Layout.x; | 
 |                 y2 = (dragY2 != null ? dragY2 * height : n2Layout.y) + edgeLayout.ty; | 
 |                 cpx1 = x1 * (1 - curvature) + x2 * curvature; | 
 |                 cpy1 = y1; | 
 |                 cpx2 = x1 * curvature + x2 * (1 - curvature); | 
 |                 cpy2 = y2; | 
 |             } | 
 |  | 
 |             curve.setShape({ | 
 |                 x1: x1, | 
 |                 y1: y1, | 
 |                 x2: x2, | 
 |                 y2: y2, | 
 |                 cpx1: cpx1, | 
 |                 cpy1: cpy1, | 
 |                 cpx2: cpx2, | 
 |                 cpy2: cpy2 | 
 |             }); | 
 |  | 
 |             curve.useStyle(lineStyleModel.getItemStyle()); | 
 |             // Special color, use source node color or target node color | 
 |             switch (curve.style.fill) { | 
 |                 case 'source': | 
 |                     curve.style.fill = edge.node1.getVisual('color'); | 
 |                     curve.style.decal = edge.node1.getVisual('style').decal; | 
 |                     break; | 
 |                 case 'target': | 
 |                     curve.style.fill = edge.node2.getVisual('color'); | 
 |                     curve.style.decal = edge.node2.getVisual('style').decal; | 
 |                     break; | 
 |                 case 'gradient': | 
 |                     const sourceColor = edge.node1.getVisual('color'); | 
 |                     const targetColor = edge.node2.getVisual('color'); | 
 |                     if (isString(sourceColor) && isString(targetColor)) { | 
 |                         curve.style.fill = new graphic.LinearGradient( | 
 |                             0, 0, +(orient === 'horizontal'), +(orient === 'vertical'), [{ | 
 |                                 color: sourceColor, | 
 |                                 offset: 0 | 
 |                             }, { | 
 |                                 color: targetColor, | 
 |                                 offset: 1 | 
 |                             }] | 
 |                         ); | 
 |                     } | 
 |             } | 
 |  | 
 |             setLabelStyle( | 
 |                 curve, getLabelStatesModels(edgeModel, 'edgeLabel'), | 
 |                 { | 
 |                     labelFetcher: seriesModel, | 
 |                     labelDataIndex: edge.dataIndex, | 
 |                     defaultText: `${edgeModel.get('value')}` | 
 |                 } | 
 |             ); | 
 |             curve.setTextConfig({ position: 'inside' }); | 
 |  | 
 |             const emphasisModel = edgeModel.getModel('emphasis'); | 
 |  | 
 |             setStatesStylesFromModel(curve, edgeModel, 'lineStyle', (model) => model.getItemStyle()); | 
 |  | 
 |             group.add(curve); | 
 |  | 
 |             edgeData.setItemGraphicEl(edge.dataIndex, curve); | 
 |  | 
 |             const focus = emphasisModel.get('focus'); | 
 |             toggleHoverEmphasis( | 
 |                 curve, | 
 |                 focus === 'adjacency' ? edge.getAdjacentDataIndices() : focus, | 
 |                 emphasisModel.get('blurScope'), | 
 |                 emphasisModel.get('disabled') | 
 |             ); | 
 |  | 
 |             getECData(curve).dataType = 'edge'; | 
 |         }); | 
 |  | 
 |         // Generate a rect for each node | 
 |         graph.eachNode(function (node) { | 
 |             const layout = node.getLayout(); | 
 |             const itemModel = node.getModel<SankeyNodeItemOption>(); | 
 |             const dragX = itemModel.get('localX'); | 
 |             const dragY = itemModel.get('localY'); | 
 |             const emphasisModel = itemModel.getModel('emphasis'); | 
 |  | 
 |             const rect = new graphic.Rect({ | 
 |                 shape: { | 
 |                     x: dragX != null ? dragX * width : layout.x, | 
 |                     y: dragY != null ? dragY * height : layout.y, | 
 |                     width: layout.dx, | 
 |                     height: layout.dy | 
 |                 }, | 
 |                 style: itemModel.getModel('itemStyle').getItemStyle(), | 
 |                 z2: 10 | 
 |             }); | 
 |  | 
 |             setLabelStyle( | 
 |                 rect, getLabelStatesModels(itemModel), | 
 |                 { | 
 |                     labelFetcher: seriesModel, | 
 |                     labelDataIndex: node.dataIndex, | 
 |                     defaultText: node.id | 
 |                 } | 
 |             ); | 
 |  | 
 |             (rect as ECElement).disableLabelAnimation = true; | 
 |  | 
 |             rect.setStyle('fill', node.getVisual('color')); | 
 |             rect.setStyle('decal', node.getVisual('style').decal); | 
 |  | 
 |             setStatesStylesFromModel(rect, itemModel); | 
 |  | 
 |             group.add(rect); | 
 |  | 
 |             nodeData.setItemGraphicEl(node.dataIndex, rect); | 
 |  | 
 |             getECData(rect).dataType = 'node'; | 
 |  | 
 |             const focus = emphasisModel.get('focus'); | 
 |             toggleHoverEmphasis( | 
 |                 rect, | 
 |                 focus === 'adjacency' ? node.getAdjacentDataIndices() : focus, | 
 |                 emphasisModel.get('blurScope'), | 
 |                 emphasisModel.get('disabled') | 
 |             ); | 
 |         }); | 
 |  | 
 |         nodeData.eachItemGraphicEl(function (el: graphic.Rect, dataIndex: number) { | 
 |             const itemModel = nodeData.getItemModel<SankeyNodeItemOption>(dataIndex); | 
 |             if (itemModel.get('draggable')) { | 
 |                 el.drift = function (this: typeof el, dx, dy) { | 
 |                     sankeyView._focusAdjacencyDisabled = true; | 
 |                     this.shape.x += dx; | 
 |                     this.shape.y += dy; | 
 |                     this.dirty(); | 
 |                     api.dispatchAction({ | 
 |                         type: 'dragNode', | 
 |                         seriesId: seriesModel.id, | 
 |                         dataIndex: nodeData.getRawIndex(dataIndex), | 
 |                         localX: this.shape.x / width, | 
 |                         localY: this.shape.y / height | 
 |                     }); | 
 |                 }; | 
 |                 el.ondragend = function () { | 
 |                     sankeyView._focusAdjacencyDisabled = false; | 
 |                 }; | 
 |                 el.draggable = true; | 
 |                 el.cursor = 'move'; | 
 |             } | 
 |         }); | 
 |  | 
 |         if (!this._data && seriesModel.isAnimationEnabled()) { | 
 |             group.setClipPath(createGridClipShape(group.getBoundingRect(), seriesModel, function () { | 
 |                 group.removeClipPath(); | 
 |             })); | 
 |         } | 
 |  | 
 |         this._data = seriesModel.getData(); | 
 |     } | 
 |  | 
 |     dispose() { | 
 |     } | 
 | } | 
 |  | 
 | // Add animation to the view | 
 | function createGridClipShape(rect: RectLike, seriesModel: SankeySeriesModel, cb: () => void) { | 
 |     const rectEl = new graphic.Rect({ | 
 |         shape: { | 
 |             x: rect.x - 10, | 
 |             y: rect.y - 10, | 
 |             width: 0, | 
 |             height: rect.height + 20 | 
 |         } | 
 |     }); | 
 |     graphic.initProps(rectEl, { | 
 |         shape: { | 
 |             width: rect.width + 20 | 
 |         } | 
 |     }, seriesModel, cb); | 
 |  | 
 |     return rectEl; | 
 | } | 
 |  | 
 | export default SankeyView; |