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