| /* |
| * 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, enableHoverEmphasis, setStatesStylesFromModel } from '../../util/states'; |
| import { LayoutOrient, Payload, 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 List from '../../data/List'; |
| import { RectLike } from 'zrender/src/core/BoundingRect'; |
| import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; |
| import { getECData } from '../../util/innerStore'; |
| |
| interface FocusNodeAdjacencyPayload extends Payload { |
| dataIndex?: number |
| edgeDataIndex?: number |
| } |
| |
| 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: List; |
| |
| 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 (typeof sourceColor === 'string' && typeof targetColor === 'string') { |
| curve.style.fill = new graphic.LinearGradient(0, 0, 1, 0, [{ |
| color: sourceColor, |
| offset: 0 |
| }, { |
| color: targetColor, |
| offset: 1 |
| }]); |
| } |
| } |
| |
| 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'); |
| enableHoverEmphasis( |
| curve, |
| focus === 'adjacency' ? edge.getAdjacentDataIndices() : focus, |
| emphasisModel.get('blurScope') |
| ); |
| |
| 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() |
| }); |
| |
| 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'); |
| enableHoverEmphasis( |
| rect, |
| focus === 'adjacency' ? node.getAdjacentDataIndices() : focus, |
| emphasisModel.get('blurScope') |
| ); |
| }); |
| |
| 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; |