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