/* * 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 SeriesData from '../../data/SeriesData'; 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 GraphNodeStatesMixin { emphasis?: ExtraEmphasisState } interface GraphEdgeStatesMixin { emphasis?: ExtraEmphasisState } export interface GraphNodeItemOption extends SymbolOptionMixin, GraphNodeStateOption, GraphNodeStateOption, StatesOptionMixin { 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 cursor?: string } export interface GraphEdgeStateOption { lineStyle?: GraphEdgeLineStyleOption label?: SeriesLineLabelOption } export interface GraphEdgeItemOption extends GraphEdgeStateOption, StatesOptionMixin, GraphEdgeItemObject { value?: number /** * Symbol of both line ends */ symbol?: string | string[] symbolSize?: number | number[] ignoreForceLayout?: boolean } export interface GraphCategoryItemOption extends SymbolOptionMixin, GraphNodeStateOption, StatesOptionMixin { name?: string value?: OptionDataValue } export interface GraphSeriesOption extends SeriesOption, GraphNodeStatesMixin>, SeriesOnCartesianOptionMixin, SeriesOnPolarOptionMixin, SeriesOnCalendarOptionMixin, SeriesOnGeoOptionMixin, SeriesOnSingleOptionMixin, SymbolOptionMixin, RoamOptionMixin, BoxLayoutOptionMixin { type?: 'graph' coordinateSystem?: string legendHoverLink?: boolean layout?: 'none' | 'force' | 'circular' data?: (GraphNodeItemOption | GraphDataValue)[] nodes?: (GraphNodeItemOption | GraphDataValue)[] edges?: GraphEdgeItemOption[] links?: GraphEdgeItemOption[] categories?: GraphCategoryItemOption[] /** * @deprecated */ 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['focus'] scale?: boolean | number 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 } /** * auto curveness for multiple edge, invalid when `lineStyle.curveness` is set */ autoCurveness?: boolean | number | number[] } class GraphSeriesModel extends SeriesModel { static readonly type = 'series.graph'; readonly type = GraphSeriesModel.type; static readonly dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar']; private _categoriesData: SeriesData; private _categoriesModels: Model[]; /** * Preserved points during layouting */ preservedPoints?: Dictionary; 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): SeriesData { 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: SeriesData, edgeData: SeriesData) { // 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 SeriesData; } getCategoriesData(): SeriesData { 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 SeriesData(['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, // 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;