| /* |
| * 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 List from '../../data/List'; |
| import * as zrUtil from 'zrender/src/core/util'; |
| import {defaultEmphasis} from '../../util/model'; |
| import Model from '../../model/Model'; |
| import createGraphFromNodeEdge from '../helper/createGraphFromNodeEdge'; |
| import LegendVisualProvider from '../../visual/LegendVisualProvider'; |
| import { |
| SeriesOption, |
| SeriesOnCartesianOptionMixin, |
| SeriesOnPolarOptionMixin, |
| SeriesOnCalendarOptionMixin, |
| SeriesOnGeoOptionMixin, |
| SeriesOnSingleOptionMixin, |
| OptionDataValue, |
| RoamOptionMixin, |
| SeriesLabelOption, |
| ItemStyleOption, |
| LineStyleOption, |
| SymbolOptionMixin, |
| BoxLayoutOptionMixin, |
| Dictionary, |
| SeriesLineLabelOption, |
| StatesOptionMixin, |
| GraphEdgeItemObject, |
| OptionDataValueNumeric, |
| CallbackDataParams, |
| DefaultEmphasisFocus |
| } from '../../util/types'; |
| import SeriesModel from '../../model/Series'; |
| import Graph from '../../data/Graph'; |
| import GlobalModel from '../../model/Global'; |
| import { VectorArray } from 'zrender/src/core/vector'; |
| import { ForceLayoutInstance } from './forceLayout'; |
| import { LineDataVisual } from '../../visual/commonVisualTypes'; |
| import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup'; |
| import { defaultSeriesFormatTooltip } from '../../component/tooltip/seriesFormatTooltip'; |
| import {initCurvenessList, createEdgeMapForCurveness} from '../helper/multipleGraphEdgeHelper'; |
| |
| |
| type GraphDataValue = OptionDataValue | OptionDataValue[]; |
| |
| interface GraphEdgeLineStyleOption extends LineStyleOption { |
| curveness?: number |
| } |
| |
| export interface GraphNodeStateOption { |
| itemStyle?: ItemStyleOption |
| label?: SeriesLabelOption |
| } |
| |
| |
| interface ExtraEmphasisState { |
| focus?: DefaultEmphasisFocus | 'adjacency' |
| } |
| interface ExtraNodeStateOption { |
| emphasis?: ExtraEmphasisState |
| } |
| |
| interface ExtraEdgeStateOption { |
| emphasis?: ExtraEmphasisState |
| } |
| |
| export interface GraphNodeItemOption extends SymbolOptionMixin, GraphNodeStateOption, |
| GraphNodeStateOption, StatesOptionMixin<GraphNodeStateOption, ExtraNodeStateOption> { |
| |
| id?: string |
| name?: string |
| value?: GraphDataValue |
| |
| /** |
| * Fixed x position |
| */ |
| x?: number |
| /** |
| * Fixed y position |
| */ |
| y?: number |
| |
| /** |
| * If this node is fixed during force layout. |
| */ |
| fixed?: boolean |
| |
| /** |
| * Index or name of category |
| */ |
| category?: number | string |
| |
| draggable?: boolean |
| } |
| |
| export interface GraphEdgeStateOption { |
| lineStyle?: GraphEdgeLineStyleOption |
| label?: SeriesLineLabelOption |
| } |
| export interface GraphEdgeItemOption extends |
| GraphEdgeStateOption, |
| StatesOptionMixin<GraphEdgeStateOption, ExtraEdgeStateOption>, |
| GraphEdgeItemObject<OptionDataValueNumeric> { |
| |
| value?: number |
| |
| /** |
| * Symbol of both line ends |
| */ |
| symbol?: string | string[] |
| |
| symbolSize?: number | number[] |
| |
| ignoreForceLayout?: boolean |
| } |
| |
| export interface GraphCategoryItemOption extends SymbolOptionMixin, |
| GraphNodeStateOption, StatesOptionMixin<GraphNodeStateOption> { |
| name?: string |
| |
| value?: OptionDataValue |
| } |
| |
| export interface GraphSeriesOption extends SeriesOption, |
| SeriesOnCartesianOptionMixin, SeriesOnPolarOptionMixin, SeriesOnCalendarOptionMixin, |
| SeriesOnGeoOptionMixin, SeriesOnSingleOptionMixin, |
| SymbolOptionMixin<CallbackDataParams>, |
| RoamOptionMixin, |
| BoxLayoutOptionMixin { |
| |
| type?: 'graph' |
| |
| coordinateSystem?: string |
| |
| legendHoverLink?: boolean |
| |
| layout?: 'none' | 'force' | 'circular' |
| |
| data?: (GraphNodeItemOption | GraphDataValue)[] |
| nodes?: (GraphNodeItemOption | GraphDataValue)[] |
| |
| edges?: GraphEdgeItemOption[] |
| links?: GraphEdgeItemOption[] |
| |
| categories?: GraphCategoryItemOption[] |
| |
| focusNodeAdjacency?: boolean |
| |
| /** |
| * Symbol size scale ratio in roam |
| */ |
| nodeScaleRatio?: 0.6, |
| |
| draggable?: boolean |
| |
| edgeSymbol?: string | string[] |
| edgeSymbolSize?: number | number[] |
| |
| edgeLabel?: SeriesLineLabelOption |
| label?: SeriesLabelOption |
| |
| itemStyle?: ItemStyleOption |
| lineStyle?: GraphEdgeLineStyleOption |
| |
| emphasis?: { |
| focus?: Exclude<GraphNodeItemOption['emphasis'], undefined>['focus'] |
| scale?: boolean |
| label?: SeriesLabelOption |
| edgeLabel?: SeriesLabelOption |
| itemStyle?: ItemStyleOption |
| lineStyle?: LineStyleOption |
| } |
| |
| blur?: { |
| label?: SeriesLabelOption |
| edgeLabel?: SeriesLabelOption |
| itemStyle?: ItemStyleOption |
| lineStyle?: LineStyleOption |
| } |
| |
| select?: { |
| label?: SeriesLabelOption |
| edgeLabel?: SeriesLabelOption |
| itemStyle?: ItemStyleOption |
| lineStyle?: LineStyleOption |
| } |
| |
| // Configuration of circular layout |
| circular?: { |
| rotateLabel?: boolean |
| } |
| |
| // Configuration of force directed layout |
| force?: { |
| initLayout?: 'circular' | 'none' |
| // Node repulsion. Can be an array to represent range. |
| repulsion?: number | number[] |
| gravity?: number |
| // Initial friction |
| friction?: number |
| |
| // Edge length. Can be an array to represent range. |
| edgeLength?: number | number[] |
| |
| layoutAnimation?: boolean |
| } |
| } |
| |
| class GraphSeriesModel extends SeriesModel<GraphSeriesOption> { |
| static readonly type = 'series.graph'; |
| readonly type = GraphSeriesModel.type; |
| |
| static readonly dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar']; |
| |
| private _categoriesData: List; |
| private _categoriesModels: Model<GraphCategoryItemOption>[]; |
| |
| /** |
| * Preserved points during layouting |
| */ |
| preservedPoints?: Dictionary<VectorArray>; |
| |
| forceLayout?: ForceLayoutInstance; |
| |
| hasSymbolVisual = true; |
| |
| init(option: GraphSeriesOption) { |
| super.init.apply(this, arguments as any); |
| |
| const self = this; |
| function getCategoriesData() { |
| return self._categoriesData; |
| } |
| // Provide data for legend select |
| this.legendVisualProvider = new LegendVisualProvider( |
| getCategoriesData, getCategoriesData |
| ); |
| |
| this.fillDataTextStyle(option.edges || option.links); |
| |
| this._updateCategoriesData(); |
| } |
| |
| mergeOption(option: GraphSeriesOption) { |
| super.mergeOption.apply(this, arguments as any); |
| |
| this.fillDataTextStyle(option.edges || option.links); |
| |
| this._updateCategoriesData(); |
| } |
| |
| mergeDefaultAndTheme(option: GraphSeriesOption) { |
| super.mergeDefaultAndTheme.apply(this, arguments as any); |
| defaultEmphasis(option, 'edgeLabel', ['show']); |
| } |
| |
| getInitialData(option: GraphSeriesOption, ecModel: GlobalModel): List { |
| const edges = option.edges || option.links || []; |
| const nodes = option.data || option.nodes || []; |
| const self = this; |
| |
| if (nodes && edges) { |
| // auto curveness |
| initCurvenessList(this); |
| const graph = createGraphFromNodeEdge(nodes as GraphNodeItemOption[], edges, this, true, beforeLink); |
| zrUtil.each(graph.edges, function (edge) { |
| createEdgeMapForCurveness(edge.node1, edge.node2, this, edge.dataIndex); |
| }, this); |
| return graph.data; |
| } |
| |
| function beforeLink(nodeData: List, edgeData: List) { |
| // Overwrite nodeData.getItemModel to |
| nodeData.wrapMethod('getItemModel', function (model) { |
| const categoriesModels = self._categoriesModels; |
| const categoryIdx = model.getShallow('category'); |
| const categoryModel = categoriesModels[categoryIdx]; |
| if (categoryModel) { |
| categoryModel.parentModel = model.parentModel; |
| model.parentModel = categoryModel; |
| } |
| return model; |
| }); |
| |
| // TODO Inherit resolveParentPath by default in Model#getModel? |
| const oldGetModel = Model.prototype.getModel; |
| function newGetModel(this: Model, path: any, parentModel?: Model) { |
| const model = oldGetModel.call(this, path, parentModel); |
| model.resolveParentPath = resolveParentPath; |
| return model; |
| } |
| |
| edgeData.wrapMethod('getItemModel', function (model: Model) { |
| model.resolveParentPath = resolveParentPath; |
| model.getModel = newGetModel; |
| return model; |
| }); |
| |
| function resolveParentPath(this: Model, pathArr: readonly string[]): string[] { |
| if (pathArr && (pathArr[0] === 'label' || pathArr[1] === 'label')) { |
| const newPathArr = pathArr.slice(); |
| if (pathArr[0] === 'label') { |
| newPathArr[0] = 'edgeLabel'; |
| } |
| else if (pathArr[1] === 'label') { |
| newPathArr[1] = 'edgeLabel'; |
| } |
| return newPathArr; |
| } |
| return pathArr as string[]; |
| } |
| } |
| } |
| |
| getGraph(): Graph { |
| return this.getData().graph; |
| } |
| |
| getEdgeData() { |
| return this.getGraph().edgeData as List<GraphSeriesModel, LineDataVisual>; |
| } |
| |
| getCategoriesData(): List { |
| return this._categoriesData; |
| } |
| |
| formatTooltip( |
| dataIndex: number, |
| multipleSeries: boolean, |
| dataType: string |
| ) { |
| if (dataType === 'edge') { |
| const nodeData = this.getData(); |
| const params = this.getDataParams(dataIndex, dataType); |
| const edge = nodeData.graph.getEdgeByIndex(dataIndex); |
| const sourceName = nodeData.getName(edge.node1.dataIndex); |
| const targetName = nodeData.getName(edge.node2.dataIndex); |
| |
| const nameArr = []; |
| sourceName != null && nameArr.push(sourceName); |
| targetName != null && nameArr.push(targetName); |
| |
| return createTooltipMarkup('nameValue', { |
| name: nameArr.join(' > '), |
| value: params.value, |
| noValue: params.value == null |
| }); |
| } |
| // dataType === 'node' or empty |
| const nodeMarkup = defaultSeriesFormatTooltip({ |
| series: this, |
| dataIndex: dataIndex, |
| multipleSeries: multipleSeries |
| }); |
| return nodeMarkup; |
| } |
| |
| _updateCategoriesData() { |
| const categories = zrUtil.map(this.option.categories || [], function (category) { |
| // Data must has value |
| return category.value != null ? category : zrUtil.extend({ |
| value: 0 |
| }, category); |
| }); |
| const categoriesData = new List(['value'], this); |
| categoriesData.initData(categories); |
| |
| this._categoriesData = categoriesData; |
| |
| this._categoriesModels = categoriesData.mapArray(function (idx) { |
| return categoriesData.getItemModel(idx); |
| }); |
| } |
| |
| setZoom(zoom: number) { |
| this.option.zoom = zoom; |
| } |
| |
| setCenter(center: number[]) { |
| this.option.center = center; |
| } |
| |
| isAnimationEnabled() { |
| return super.isAnimationEnabled() |
| // Not enable animation when do force layout |
| && !(this.get('layout') === 'force' && this.get(['force', 'layoutAnimation'])); |
| } |
| |
| static defaultOption: GraphSeriesOption = { |
| zlevel: 0, |
| z: 2, |
| |
| coordinateSystem: 'view', |
| |
| // Default option for all coordinate systems |
| // xAxisIndex: 0, |
| // yAxisIndex: 0, |
| // polarIndex: 0, |
| // geoIndex: 0, |
| |
| legendHoverLink: true, |
| |
| layout: null, |
| |
| focusNodeAdjacency: false, |
| |
| // Configuration of circular layout |
| circular: { |
| rotateLabel: false |
| }, |
| // Configuration of force directed layout |
| force: { |
| initLayout: null, |
| // Node repulsion. Can be an array to represent range. |
| repulsion: [0, 50], |
| gravity: 0.1, |
| // Initial friction |
| friction: 0.6, |
| |
| // Edge length. Can be an array to represent range. |
| edgeLength: 30, |
| |
| layoutAnimation: true |
| }, |
| |
| left: 'center', |
| top: 'center', |
| // right: null, |
| // bottom: null, |
| // width: '80%', |
| // height: '80%', |
| |
| symbol: 'circle', |
| symbolSize: 10, |
| |
| edgeSymbol: ['none', 'none'], |
| edgeSymbolSize: 10, |
| edgeLabel: { |
| position: 'middle', |
| distance: 5 |
| }, |
| |
| draggable: false, |
| |
| roam: false, |
| |
| // Default on center of graph |
| center: null, |
| |
| zoom: 1, |
| // Symbol size scale ratio in roam |
| nodeScaleRatio: 0.6, |
| |
| // cursor: null, |
| |
| // categories: [], |
| |
| // data: [] |
| // Or |
| // nodes: [] |
| // |
| // links: [] |
| // Or |
| // edges: [] |
| |
| label: { |
| show: false, |
| formatter: '{b}' |
| }, |
| |
| itemStyle: {}, |
| |
| lineStyle: { |
| color: '#aaa', |
| width: 1, |
| opacity: 0.5 |
| }, |
| emphasis: { |
| scale: true, |
| label: { |
| show: true |
| } |
| }, |
| |
| select: { |
| itemStyle: { |
| borderColor: '#212121' |
| } |
| } |
| }; |
| } |
| |
| export default GraphSeriesModel; |