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