| /* | |
| * 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 SeriesModel from '../../model/Series'; | |
| import createGraphFromNodeEdge from '../helper/createGraphFromNodeEdge'; | |
| import Model from '../../model/Model'; | |
| import { | |
| SeriesOption, | |
| BoxLayoutOptionMixin, | |
| OptionDataValue, | |
| SeriesLabelOption, | |
| ItemStyleOption, | |
| LineStyleOption, | |
| LayoutOrient, | |
| ColorString, | |
| StatesOptionMixin, | |
| OptionDataItemObject, | |
| GraphEdgeItemObject, | |
| OptionDataValueNumeric, | |
| DefaultEmphasisFocus, | |
| CallbackDataParams | |
| } from '../../util/types'; | |
| import GlobalModel from '../../model/Global'; | |
| import SeriesData from '../../data/SeriesData'; | |
| import { LayoutRect } from '../../util/layout'; | |
| import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup'; | |
| type FocusNodeAdjacency = boolean | 'inEdges' | 'outEdges' | 'allEdges'; | |
| export interface SankeyNodeStateOption<TCbParams = never> { | |
| label?: SeriesLabelOption | |
| itemStyle?: ItemStyleOption<TCbParams> | |
| } | |
| export interface SankeyEdgeStateOption { | |
| lineStyle?: SankeyEdgeStyleOption | |
| } | |
| interface SankeyBothStateOption<TCbParams> extends SankeyNodeStateOption<TCbParams>, SankeyEdgeStateOption {} | |
| interface SankeyEdgeStyleOption extends LineStyleOption { | |
| curveness?: number | |
| } | |
| interface ExtraStateOption { | |
| emphasis?: { | |
| focus?: DefaultEmphasisFocus | 'adjacency' | |
| } | |
| } | |
| export interface SankeyNodeItemOption extends SankeyNodeStateOption, | |
| StatesOptionMixin<SankeyNodeStateOption, ExtraStateOption>, | |
| OptionDataItemObject<OptionDataValue> { | |
| id?: string | |
| localX?: number | |
| localY?: number | |
| depth?: number | |
| draggable?: boolean | |
| focusNodeAdjacency?: FocusNodeAdjacency | |
| } | |
| export interface SankeyEdgeItemOption extends | |
| SankeyEdgeStateOption, | |
| StatesOptionMixin<SankeyEdgeStateOption, ExtraStateOption>, | |
| GraphEdgeItemObject<OptionDataValueNumeric> { | |
| focusNodeAdjacency?: FocusNodeAdjacency | |
| edgeLabel?: SeriesLabelOption | |
| } | |
| export interface SankeyLevelOption extends SankeyNodeStateOption, SankeyEdgeStateOption { | |
| depth: number | |
| } | |
| export interface SankeySeriesOption | |
| extends SeriesOption<SankeyBothStateOption<CallbackDataParams>, ExtraStateOption>, | |
| SankeyBothStateOption<CallbackDataParams>, | |
| BoxLayoutOptionMixin { | |
| type?: 'sankey' | |
| /** | |
| * color will be linear mapped. | |
| */ | |
| color?: ColorString[] | |
| coordinateSystem?: 'view' | |
| orient?: LayoutOrient | |
| /** | |
| * The width of the node | |
| */ | |
| nodeWidth?: number | |
| /** | |
| * The vertical distance between two nodes | |
| */ | |
| nodeGap?: number | |
| /** | |
| * Control if the node can move or not | |
| */ | |
| draggable?: boolean | |
| /** | |
| * Will be allEdges if true. | |
| * @deprecated | |
| */ | |
| focusNodeAdjacency?: FocusNodeAdjacency | |
| /** | |
| * The number of iterations to change the position of the node | |
| */ | |
| layoutIterations?: number | |
| nodeAlign?: 'justify' | 'left' | 'right' // TODO justify should be auto | |
| data?: SankeyNodeItemOption[] | |
| nodes?: SankeyNodeItemOption[] | |
| edges?: SankeyEdgeItemOption[] | |
| links?: SankeyEdgeItemOption[] | |
| levels?: SankeyLevelOption[] | |
| edgeLabel?: SeriesLabelOption & { | |
| position?: 'inside' | |
| } | |
| } | |
| class SankeySeriesModel extends SeriesModel<SankeySeriesOption> { | |
| static readonly type = 'series.sankey'; | |
| readonly type = SankeySeriesModel.type; | |
| levelModels: Model<SankeyLevelOption>[]; | |
| layoutInfo: LayoutRect; | |
| /** | |
| * Init a graph data structure from data in option series | |
| */ | |
| getInitialData(option: SankeySeriesOption, ecModel: GlobalModel) { | |
| const links = option.edges || option.links; | |
| const nodes = option.data || option.nodes; | |
| const levels = option.levels; | |
| this.levelModels = []; | |
| const levelModels = this.levelModels; | |
| for (let i = 0; i < levels.length; i++) { | |
| if (levels[i].depth != null && levels[i].depth >= 0) { | |
| levelModels[levels[i].depth] = new Model(levels[i], this, ecModel); | |
| } | |
| else { | |
| if (__DEV__) { | |
| throw new Error('levels[i].depth is mandatory and should be natural number'); | |
| } | |
| } | |
| } | |
| if (nodes && links) { | |
| const graph = createGraphFromNodeEdge(nodes, links, this, true, beforeLink); | |
| return graph.data; | |
| } | |
| function beforeLink(nodeData: SeriesData, edgeData: SeriesData) { | |
| nodeData.wrapMethod('getItemModel', function (model: Model, idx: number) { | |
| const seriesModel = model.parentModel as SankeySeriesModel; | |
| const layout = seriesModel.getData().getItemLayout(idx); | |
| if (layout) { | |
| const nodeDepth = layout.depth; | |
| const levelModel = seriesModel.levelModels[nodeDepth]; | |
| if (levelModel) { | |
| model.parentModel = levelModel; | |
| } | |
| } | |
| return model; | |
| }); | |
| edgeData.wrapMethod('getItemModel', function (model: Model, idx: number) { | |
| const seriesModel = model.parentModel as SankeySeriesModel; | |
| const edge = seriesModel.getGraph().getEdgeByIndex(idx); | |
| const layout = edge.node1.getLayout(); | |
| if (layout) { | |
| const depth = layout.depth; | |
| const levelModel = seriesModel.levelModels[depth]; | |
| if (levelModel) { | |
| model.parentModel = levelModel; | |
| } | |
| } | |
| return model; | |
| }); | |
| } | |
| } | |
| setNodePosition(dataIndex: number, localPosition: number[]) { | |
| const nodes = this.option.data || this.option.nodes; | |
| const dataItem = nodes[dataIndex]; | |
| dataItem.localX = localPosition[0]; | |
| dataItem.localY = localPosition[1]; | |
| } | |
| /** | |
| * Return the graphic data structure | |
| * | |
| * @return graphic data structure | |
| */ | |
| getGraph() { | |
| return this.getData().graph; | |
| } | |
| /** | |
| * Get edge data of graphic data structure | |
| * | |
| * @return data structure of list | |
| */ | |
| getEdgeData() { | |
| return this.getGraph().edgeData; | |
| } | |
| formatTooltip( | |
| dataIndex: number, | |
| multipleSeries: boolean, | |
| dataType: 'node' | 'edge' | |
| ) { | |
| function noValue(val: unknown): boolean { | |
| return isNaN(val as number) || val == null; | |
| } | |
| // dataType === 'node' or empty do not show tooltip by default | |
| if (dataType === 'edge') { | |
| const params = this.getDataParams(dataIndex, dataType); | |
| const rawDataOpt = params.data as SankeyEdgeItemOption; | |
| const edgeValue = params.value; | |
| const edgeName = rawDataOpt.source + ' -- ' + rawDataOpt.target; | |
| return createTooltipMarkup('nameValue', { | |
| name: edgeName, | |
| value: edgeValue, | |
| noValue: noValue(edgeValue) | |
| }); | |
| } | |
| // dataType === 'node' | |
| else { | |
| const node = this.getGraph().getNodeByIndex(dataIndex); | |
| const value = node.getLayout().value; | |
| const name = (this.getDataParams(dataIndex, dataType).data as SankeyNodeItemOption).name; | |
| return createTooltipMarkup('nameValue', { | |
| name: name != null ? name + '' : null, | |
| value: value, | |
| noValue: noValue(value) | |
| }); | |
| } | |
| } | |
| optionUpdated() {} | |
| // Override Series.getDataParams() | |
| getDataParams(dataIndex: number, dataType: 'node' | 'edge') { | |
| const params = super.getDataParams(dataIndex, dataType); | |
| if (params.value == null && dataType === 'node') { | |
| const node = this.getGraph().getNodeByIndex(dataIndex); | |
| const nodeValue = node.getLayout().value; | |
| params.value = nodeValue; | |
| } | |
| return params; | |
| } | |
| static defaultOption: SankeySeriesOption = { | |
| // zlevel: 0, | |
| z: 2, | |
| coordinateSystem: 'view', | |
| left: '5%', | |
| top: '5%', | |
| right: '20%', | |
| bottom: '5%', | |
| orient: 'horizontal', | |
| nodeWidth: 20, | |
| nodeGap: 8, | |
| draggable: true, | |
| layoutIterations: 32, | |
| label: { | |
| show: true, | |
| position: 'right', | |
| fontSize: 12 | |
| }, | |
| edgeLabel: { | |
| show: false, | |
| fontSize: 12 | |
| }, | |
| levels: [], | |
| nodeAlign: 'justify', | |
| lineStyle: { | |
| color: '#314656', | |
| opacity: 0.2, | |
| curveness: 0.5 | |
| }, | |
| emphasis: { | |
| label: { | |
| show: true | |
| }, | |
| lineStyle: { | |
| opacity: 0.5 | |
| } | |
| }, | |
| select: { | |
| itemStyle: { | |
| borderColor: '#212121' | |
| } | |
| }, | |
| animationEasing: 'linear', | |
| animationDuration: 1000 | |
| }; | |
| } | |
| export default SankeySeriesModel; |