diff --git a/packages/g6/src/stdlib/index.ts b/packages/g6/src/stdlib/index.ts index bc18cef698..8c3a2ea82b 100644 --- a/packages/g6/src/stdlib/index.ts +++ b/packages/g6/src/stdlib/index.ts @@ -24,7 +24,7 @@ import RotateCanvas3D from './behavior/rotate-canvas-3d'; import TrackCanvas3D from './behavior/track-canvas-3d'; import OrbitCanvas3D from './behavior/orbit-canvas-3d'; import { HoverActivate } from './behavior/hover-activate'; - +import { Quadratic } from './item/edge'; const stdLib = { transforms: { comboFromNode, @@ -63,6 +63,7 @@ const stdLib = { }, edges: { 'line-edge': LineEdge, + 'quadratic-edge': Quadratic }, combos: {}, }; diff --git a/packages/g6/src/stdlib/item/edge/index.ts b/packages/g6/src/stdlib/item/edge/index.ts index bd91c09679..c143494e74 100644 --- a/packages/g6/src/stdlib/item/edge/index.ts +++ b/packages/g6/src/stdlib/item/edge/index.ts @@ -1 +1,2 @@ export * from './line'; +export * from './quadratic' \ No newline at end of file diff --git a/packages/g6/src/stdlib/item/edge/quadratic.ts b/packages/g6/src/stdlib/item/edge/quadratic.ts new file mode 100644 index 0000000000..d3a6562ac5 --- /dev/null +++ b/packages/g6/src/stdlib/item/edge/quadratic.ts @@ -0,0 +1,119 @@ +import { Point } from '../../../types/common'; +import { + EdgeDisplayModel, + EdgeModelData, + EdgeShapeMap, +} from '../../../types/edge'; +import { State } from '../../../types/item'; +import { BaseEdge } from './base'; + +export class Quadratic extends BaseEdge { + public type = 'line-edge'; + public defaultStyles = { + keyShape: { + controlPoints: [0, 0], //precise x-axis, y-axis coordinates of control points + curvePosition: 0, //control point coordinates described by percentage,range 0 to 1 + curveOffset: [0, 0], //a point coordinate that quadratic curve to + stroke: '#000000', + isBillboard: true, + }, + }; + constructor(props) { + super(props); + // suggest to merge default styles like this to avoid style value missing + // this.defaultStyles = mergeStyles([this.baseDefaultStyles, this.defaultStyles]); + } + public draw( + model: EdgeDisplayModel, + sourcePoint: Point, + targetPoint: Point, + shapeMap: EdgeShapeMap, + diffData?: { previous: EdgeModelData; current: EdgeModelData }, + diffState?: { previous: State[]; current: State[] }, + ): EdgeShapeMap { + const { data = {} } = model; + + const shapes: EdgeShapeMap = { keyShape: undefined }; + + shapes.keyShape = this.drawKeyShape( + model, + sourcePoint, + targetPoint, + shapeMap, + diffData, + ); + + if (data.haloShape) { + shapes.haloShape = this.drawHaloShape(model, shapeMap, diffData); + } + + if (data.labelShape) { + shapes.labelShape = this.drawLabelShape(model, shapeMap, diffData); + } + + // labelBackgroundShape + if (data.labelBackgroundShape) { + shapes.labelBackgroundShape = this.drawLabelBackgroundShape( + model, + shapeMap, + diffData, + ); + } + + if (data.iconShape) { + shapes.iconShape = this.drawIconShape(model, shapeMap, diffData); + } + + // TODO: other shapes + return shapes; + } + + public drawKeyShape( + model: EdgeDisplayModel, + sourcePoint: Point, + targetPoint: Point, + shapeMap: EdgeShapeMap, + diffData?: { previous: EdgeModelData; current: EdgeModelData }, + diffState?: { previous: State[]; current: State[] }, + ) { + const { keyShape: keyShapeStyle } = this.mergedStyles as any; + + const controlPoint = this.getControlPoint(sourcePoint, targetPoint, keyShapeStyle.curvePosition, keyShapeStyle.controlPoints, keyShapeStyle.curveOffset); + return this.upsertShape( + 'path', + 'keyShape', + { + ...keyShapeStyle, + path: [['M', sourcePoint.x, sourcePoint.y], ['Q', controlPoint.x, controlPoint.y, targetPoint.x, targetPoint.y]], + }, + shapeMap, + model, + ); + } + + /** + * calculate the control point by curvePosition|controlPoints|curveOffset + * @param startPoint: source point position of edge + * @param endPoint target point position of edge + * @param percent the proportion of control points' in the segment, Range 0 to 1 + * @param controlPoints the control point position + * @param offset the curveOffset + * @returns control points + */ + private getControlPoint: (startPoint: Point, + endPoint: Point, + percent: number, + controlPoints: number[], + offset: number[] + ) => Point = ( + startPoint: Point, + endPoint: Point, + percent = 0, + controlPoints, + offset, + ) => ({ + x: (1 - percent) * startPoint.x + percent * endPoint.x + controlPoints[0] + offset[0], + y: (1 - percent) * startPoint.y + percent * endPoint.y + controlPoints[1] + offset[1], + }) + +} diff --git a/packages/g6/tests/intergration/demo/quadratic.ts b/packages/g6/tests/intergration/demo/quadratic.ts new file mode 100644 index 0000000000..ada810b8fe --- /dev/null +++ b/packages/g6/tests/intergration/demo/quadratic.ts @@ -0,0 +1,91 @@ +import G6 from '../../../src/index'; +import { container, height, width } from '../../datasets/const'; + +export default () => { + const data = { + nodes: [ + { + id: 1, + data: { + x: 100, + y: 100, + type: 'circle-node', + }, + }, + { + id: 2, + data: { + x: 200, + y: 100, + type: 'circle-node', + }, + }, + ], + edges: [ + { + id: 'edge1', + source: 1, + target: 2, + data: { + type: 'quadratic-edge' + } + } + ] + }; + + const edge: ((data: any) => any) = (edgeInnerModel: any) => { + + const { id, data } = edgeInnerModel + return { + id, + data: { + ...data, + keyShape: { + controlPoints: [150, 100], + // curvePosition: 0.5, + curveOffset: [0, 20], + stroke: 'blue' + }, + // iconShape: { + // // img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg', + // text: 'label', + // fill: 'blue' + // }, + labelShape: { + text: 'label', + position: 'middle', + fill: 'blue' + }, + labelBackgroundShape: { + fill: 'white' + }, + } + } + } + const graph = new G6.Graph({ + container, + width, + height, + data, + type: 'graph', + modes: { + default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], + }, + node: (nodeInnerModel: any) => { + const { id, data } = nodeInnerModel; + return { + id, + data: { + ...data, + keyShape: { + r: 16 + }, + } + } + }, + edge, + }); + + return graph; + +} \ No newline at end of file diff --git a/packages/g6/tests/intergration/index.ts b/packages/g6/tests/intergration/index.ts index 37ea6bfa1c..b717b49aad 100644 --- a/packages/g6/tests/intergration/index.ts +++ b/packages/g6/tests/intergration/index.ts @@ -17,6 +17,7 @@ import demoFor4 from './demo/demoFor4'; import bugReproduce from './demo/bugReproduce'; import rect from './demo/rect'; import visual from './visual/visual'; +import quadratic from './demo/quadratic'; export { behaviors_activateRelations, layouts_circular, @@ -37,4 +38,5 @@ export { bugReproduce, rect, visual, + quadratic }; diff --git a/packages/g6/tests/unit/quadratic-spec.ts b/packages/g6/tests/unit/quadratic-spec.ts new file mode 100644 index 0000000000..e29c2dcc88 --- /dev/null +++ b/packages/g6/tests/unit/quadratic-spec.ts @@ -0,0 +1,948 @@ +// @ts-nocheck + +import { DisplayObject } from '@antv/g'; +import { clone } from '@antv/util'; +import G6, { + EdgeDisplayModel, + Graph, + IGraph, + NodeDisplayModel, +} from '../../src/index'; +import { LineEdge } from '../../src/stdlib/item/edge'; +import { CircleNode } from '../../src/stdlib/item/node'; +import { NodeModelData, NodeShapeMap } from '../../src/types/node'; +import { extend } from '../../src/util/extend'; +import { upsertShape } from '../../src/util/shape'; + +const container = document.createElement('div'); +const width = document.getElementById('container')?.clientWidth; +const height = document.getElementById('container')?.clientHeight; +document.querySelector('body').appendChild(container); + +let graph: IGraph; + +const data = { + nodes: [ + { + id: 1, + data: { + x: 100, + y: 100, + type: 'circle-node', + }, + }, + { + id: 2, + data: { + x: 200, + y: 100, + type: 'circle-node', + }, + }, + ], + edges: [ + { + id: 'edge1', + source: 1, + target: 2, + data: { + type: 'quadratic-edge' + } + } + ] +}; + +const edge: ((data: any) => any) = (edgeInnerModel: any) => { + + const { id, data } = edgeInnerModel + return { + id, + data: { + keyShape: { + controlPoints: [0, 0], + curvePosition: 0.5, + curveOffset: [10, 10], + stroke: 'blue' + }, + labelShape: { + text: 'label', + fill: 'blue' + }, + labelBackgroundShape: { + fill: 'white' + }, + ...data, + } + } +} + +describe('edge item', () => { + it('new graph with two nodes and one edge', (done) => { + graph = new G6.Graph({ + container, + width, + height, + data, + type: 'graph', + modes: { + default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], + }, + node: (nodeInnerModel: any) => { + const { id, data } = nodeInnerModel; + return { + id, + data: { + ...data, + keyShape: { + r: 16 + }, + } + } + }, + edge, + }); + + graph.on('afterrender', () => { + const edgeItem = graph.itemController.itemMap['edge1']; + expect(edgeItem).not.toBe(undefined); + expect(edgeItem.shapeMap.labelShape.attributes.text).toBe('label'); + expect(edgeItem.shapeMap.labelShape.attributes.fill).toBe('blue'); + expect(edgeItem.shapeMap.labelBackgroundShape.attributes.fill).toBe('white'); + done(); + }); + }); + + it('update edge label', (done) => { + const padding = [4, 16, 4, 8]; + graph.updateData('edge', { + id: 'edge1', + data: { + labelShape: { + text: 'edge-label', + }, + labelBackgroundShape: { + radius: 10, + padding, + fill: '#f00', + }, + iconShape: { + text: 'A', + fill: '#f00', + }, + }, + }); + const edgeItem = graph.itemController.itemMap['edge1']; + expect(edgeItem.shapeMap.labelShape).not.toBe(undefined); + expect(edgeItem.shapeMap.labelShape.attributes.text).toBe('edge-label'); + const fill = edgeItem.shapeMap.labelShape.attributes.fill; + expect(fill).toBe('rgba(0,0,0,0.85)'); + let labelBounds = edgeItem.shapeMap.labelShape.getGeometryBounds(); + expect(edgeItem.shapeMap.labelBackgroundShape.attributes.width).toBe( + labelBounds.max[0] - labelBounds.min[0] + padding[1] + padding[3], + ); + expect(edgeItem.shapeMap.labelBackgroundShape.attributes.height).toBe( + labelBounds.max[1] - labelBounds.min[1] + padding[0] + padding[2], + ); + + graph.updateData('edge', { + id: 'edge1', + data: { + labelShape: { + fill: '#00f', + position: 'start', + }, + }, + }); + expect(edgeItem.shapeMap.labelShape.attributes.fill).toBe('#00f'); + expect( + edgeItem.shapeMap.labelShape.attributes.x - + edgeItem.shapeMap.labelBackgroundShape.attributes.x, + ).toBe(padding[3]); + labelBounds = edgeItem.shapeMap.labelShape.getGeometryBounds(); + const labelWidth = labelBounds.max[0] - labelBounds.min[0]; + const labelHeight = labelBounds.max[1] - labelBounds.min[1]; + const labelBgBounds = + edgeItem.shapeMap.labelBackgroundShape.getGeometryBounds(); + const labelBgWidth = labelBgBounds.max[0] - labelBgBounds.min[0]; + const labelBgHeight = labelBgBounds.max[1] - labelBgBounds.min[1]; + expect(labelBgWidth - labelWidth).toBe(padding[1] + padding[3]); + expect(labelBgHeight - labelHeight).toBe(padding[0] + padding[2]); + + graph.updateData('edge', { + id: 'edge1', + data: { + labelShape: undefined, + }, + }); + expect(edgeItem.shapeMap.labelShape).toBe(undefined); + expect(edgeItem.shapeMap.labelBackgroundShape).toBe(undefined); + done(); + }); + it('update edge icon', (done) => { + // add image icon to follow the label at path's center + graph.updateData('edge', { + id: 'edge1', + data: { + labelShape: { + text: 'abcddd', + fill: '#f00', + position: 'center', + }, + iconShape: { + text: '', + img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg', + // text: 'A', + fill: '#f00', + fontSize: 20, + }, + }, + }); + const edgeItem = graph.itemController.itemMap['edge1']; + let { labelShape, iconShape, labelBackgroundShape } = edgeItem.shapeMap; + expect(iconShape.attributes.x + iconShape.attributes.width + 6).toBe( + labelBackgroundShape.getGeometryBounds().min[0] + + labelBackgroundShape.attributes.x, + ); + expect(iconShape.attributes.transform).toBe( + labelBackgroundShape.attributes.transform, + ); + expect( + Math.floor(iconShape.attributes.y + iconShape.attributes.height / 2), + ).toBeCloseTo( + Math.floor( + labelBackgroundShape.getGeometryBounds().center[1] + + labelBackgroundShape.attributes.y, + ), + 0.01, + ); + + // update icon to be a text + graph.updateData('edge', { + id: 'edge1', + data: { + iconShape: { + text: 'A', + fill: '#f00', + fontWeight: 800, + }, + }, + }); + labelShape = edgeItem.shapeMap['labelShape']; + iconShape = edgeItem.shapeMap['iconShape']; + labelBackgroundShape = edgeItem.shapeMap['labelBackgroundShape']; + expect(iconShape.attributes.x + iconShape.attributes.fontSize + 6).toBe( + labelBackgroundShape.getGeometryBounds().min[0] + + labelBackgroundShape.attributes.x, + ); + expect(iconShape.attributes.transform).toBe( + labelBackgroundShape.attributes.transform, + ); + expect( + Math.floor(iconShape.attributes.y + iconShape.attributes.fontSize / 2), + ).toBeCloseTo( + Math.floor( + labelBackgroundShape.getGeometryBounds().center[1] + + labelBackgroundShape.attributes.y, + ), + 0.01, + ); + + // move label to the start, and the icon follows + graph.updateData('edge', { + id: 'edge1', + data: { + labelShape: { + position: 'start', + }, + }, + }); + labelShape = edgeItem.shapeMap['labelShape']; + iconShape = edgeItem.shapeMap['iconShape']; + labelBackgroundShape = edgeItem.shapeMap['labelBackgroundShape']; + expect(iconShape.attributes.x + iconShape.attributes.fontSize + 6).toBe( + labelBackgroundShape.getGeometryBounds().min[0] + + labelBackgroundShape.attributes.x, + ); + expect(iconShape.attributes.transform).toBe( + labelShape.attributes.transform, + ); + expect(iconShape.attributes.y + iconShape.attributes.fontSize / 2).toBe( + labelBackgroundShape.getGeometryBounds().center[1] + + labelBackgroundShape.attributes.y, + ); + graph.destroy(); + done(); + }); +}); + +describe('state', () => { + it('edge state selected', (done) => { + const graph = new Graph({ + container, + width: 500, + height: 500, + type: 'graph', + data: { + nodes: [ + { + id: 'node1', + data: { x: 100, y: 200 }, + }, + { + id: 'node2', + data: { x: 100, y: 300 }, + }, + { + id: 'node3', + data: { x: 200, y: 300 }, + }, + ], + edges: [ + { + id: 'edge1', + source: 'node1', + target: 'node2', + data: {}, + }, + { + id: 'edge2', + source: 'node1', + target: 'node3', + data: {}, + }, + ], + }, + edgeState: { + selected: { + keyShape: { + stroke: '#0f0', + lineWidth: 2, + }, + }, + highlight: { + keyShape: { + stroke: '#00f', + opacity: 0.5, + }, + }, + }, + }); + graph.on('afterrender', () => { + expect(graph.findIdByState('edge', 'selected').length).toBe(0); + graph.setItemState('edge1', 'selected', true); + expect(graph.findIdByState('edge', 'selected').length).toBe(1); + expect(graph.findIdByState('edge', 'selected')[0]).toBe('edge1'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + graph.setItemState('edge1', 'selected', false); + expect(graph.findIdByState('edge', 'selected').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + // set multiple edges state + graph.setItemState(['edge1', 'edge2'], 'selected', true); + expect(graph.findIdByState('edge', 'selected').length).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + graph.setItemState('edge1', 'selected', false); + expect(graph.findIdByState('edge', 'selected').length).toBe(1); + expect(graph.findIdByState('edge', 'selected')[0]).toBe('edge2'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + graph.setItemState(['edge1', 'edge2'], 'selected', false); + expect(graph.findIdByState('edge', 'selected').length).toBe(0); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + graph.setItemState(['edge2', 'edge1'], ['selected', 'highlight'], true); + expect(graph.findIdByState('edge', 'selected').length).toBe(2); + expect(graph.findIdByState('edge', 'highlight').length).toBe(2); + // should be merged styles from selected and highlight + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + + // clear states + graph.clearItemState(['edge1', 'edge2']); + expect(graph.findIdByState('edge', 'selected').length).toBe(0); + expect(graph.findIdByState('edge', 'highlight').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(1); + + graph.destroy(); + done(); + }); + }); + + it('edge state active', (done) => { + const graph = new Graph({ + container, + width: 500, + height: 500, + type: 'graph', + data: { + nodes: [ + { + id: 'node1', + data: { x: 100, y: 200 }, + }, + { + id: 'node2', + data: { x: 100, y: 300 }, + }, + { + id: 'node3', + data: { x: 200, y: 300 }, + }, + ], + edges: [ + { + id: 'edge1', + source: 'node1', + target: 'node2', + data: {}, + }, + { + id: 'edge2', + source: 'node1', + target: 'node3', + data: {}, + }, + ], + }, + edgeState: { + active: { + keyShape: { + stroke: '#0f0', + lineWidth: 2, + }, + }, + highlight: { + keyShape: { + stroke: '#00f', + opacity: 0.5, + }, + }, + }, + }); + graph.on('afterrender', () => { + expect(graph.findIdByState('edge', 'active').length).toBe(0); + graph.setItemState('edge1', 'active', true); + expect(graph.findIdByState('edge', 'active').length).toBe(1); + expect(graph.findIdByState('edge', 'active')[0]).toBe('edge1'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + graph.setItemState('edge1', 'active', false); + expect(graph.findIdByState('edge', 'active').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + // set multiple edges state + graph.setItemState(['edge1', 'edge2'], 'active', true); + expect(graph.findIdByState('edge', 'active').length).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + graph.setItemState('edge1', 'active', false); + expect(graph.findIdByState('edge', 'active').length).toBe(1); + expect(graph.findIdByState('edge', 'active')[0]).toBe('edge2'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + graph.setItemState(['edge1', 'edge2'], 'active', false); + expect(graph.findIdByState('edge', 'active').length).toBe(0); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + graph.setItemState(['edge2', 'edge1'], ['active', 'highlight'], true); + expect(graph.findIdByState('edge', 'active').length).toBe(2); + expect(graph.findIdByState('edge', 'highlight').length).toBe(2); + // should be merged styles from active and highlight + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + + // clear states + graph.clearItemState(['edge1', 'edge2']); + expect(graph.findIdByState('edge', 'active').length).toBe(0); + expect(graph.findIdByState('edge', 'highlight').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(1); + + graph.destroy(); + done(); + }); + }); + + it('edge state highlight', (done) => { + const graph = new Graph({ + container, + width: 500, + height: 500, + type: 'graph', + data: { + nodes: [ + { + id: 'node1', + data: { x: 100, y: 200 }, + }, + { + id: 'node2', + data: { x: 100, y: 300 }, + }, + { + id: 'node3', + data: { x: 200, y: 300 }, + }, + ], + edges: [ + { + id: 'edge1', + source: 'node1', + target: 'node2', + data: {}, + }, + { + id: 'edge2', + source: 'node1', + target: 'node3', + data: {}, + }, + ], + }, + edgeState: { + highlight: { + keyShape: { + stroke: '#0f0', + lineWidth: 2, + }, + }, + highlight: { + keyShape: { + stroke: '#00f', + opacity: 0.5, + }, + }, + }, + }); + graph.on('afterrender', () => { + expect(graph.findIdByState('edge', 'highlight').length).toBe(0); + graph.setItemState('edge1', 'highlight', true); + expect(graph.findIdByState('edge', 'highlight').length).toBe(1); + expect(graph.findIdByState('edge', 'highlight')[0]).toBe('edge1'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + graph.setItemState('edge1', 'highlight', false); + expect(graph.findIdByState('edge', 'highlight').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + // set multiple edges state + graph.setItemState(['edge1', 'edge2'], 'highlight', true); + expect(graph.findIdByState('edge', 'highlight').length).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + graph.setItemState('edge1', 'highlight', false); + expect(graph.findIdByState('edge', 'highlight').length).toBe(1); + expect(graph.findIdByState('edge', 'highlight')[0]).toBe('edge2'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + graph.setItemState(['edge1', 'edge2'], 'highlight', false); + expect(graph.findIdByState('edge', 'highlight').length).toBe(0); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + graph.setItemState(['edge2', 'edge1'], ['highlight', 'highlight'], true); + expect(graph.findIdByState('edge', 'highlight').length).toBe(2); + expect(graph.findIdByState('edge', 'highlight').length).toBe(2); + // should be merged styles from highlight and highlight + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + + // clear states + graph.clearItemState(['edge1', 'edge2']); + expect(graph.findIdByState('edge', 'highlight').length).toBe(0); + expect(graph.findIdByState('edge', 'highlight').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(1); + + graph.destroy(); + done(); + }); + }); + + it('edge state inactive', (done) => { + const graph = new Graph({ + container, + width: 500, + height: 500, + type: 'graph', + data: { + nodes: [ + { + id: 'node1', + data: { x: 100, y: 200 }, + }, + { + id: 'node2', + data: { x: 100, y: 300 }, + }, + { + id: 'node3', + data: { x: 200, y: 300 }, + }, + ], + edges: [ + { + id: 'edge1', + source: 'node1', + target: 'node2', + data: {}, + }, + { + id: 'edge2', + source: 'node1', + target: 'node3', + data: {}, + }, + ], + }, + edgeState: { + inactive: { + keyShape: { + stroke: '#0f0', + lineWidth: 2, + }, + }, + highlight: { + keyShape: { + stroke: '#00f', + opacity: 0.5, + }, + }, + }, + }); + graph.on('afterrender', () => { + expect(graph.findIdByState('edge', 'inactive').length).toBe(0); + graph.setItemState('edge1', 'inactive', true); + expect(graph.findIdByState('edge', 'inactive').length).toBe(1); + expect(graph.findIdByState('edge', 'inactive')[0]).toBe('edge1'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + graph.setItemState('edge1', 'inactive', false); + expect(graph.findIdByState('edge', 'inactive').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + // set multiple edges state + graph.setItemState(['edge1', 'edge2'], 'inactive', true); + expect(graph.findIdByState('edge', 'inactive').length).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + graph.setItemState('edge1', 'inactive', false); + expect(graph.findIdByState('edge', 'inactive').length).toBe(1); + expect(graph.findIdByState('edge', 'inactive')[0]).toBe('edge2'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + graph.setItemState(['edge1', 'edge2'], 'inactive', false); + expect(graph.findIdByState('edge', 'inactive').length).toBe(0); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + graph.setItemState(['edge2', 'edge1'], ['inactive', 'highlight'], true); + expect(graph.findIdByState('edge', 'inactive').length).toBe(2); + expect(graph.findIdByState('edge', 'highlight').length).toBe(2); + // should be merged styles from inactive and highlight + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + + // clear states + graph.clearItemState(['edge1', 'edge2']); + expect(graph.findIdByState('edge', 'inactive').length).toBe(0); + expect(graph.findIdByState('edge', 'highlight').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(1); + + graph.destroy(); + done(); + }); + }); + + it('edge state disable', (done) => { + const graph = new Graph({ + container, + width: 500, + height: 500, + type: 'graph', + data: { + nodes: [ + { + id: 'node1', + data: { x: 100, y: 200 }, + }, + { + id: 'node2', + data: { x: 100, y: 300 }, + }, + { + id: 'node3', + data: { x: 200, y: 300 }, + }, + ], + edges: [ + { + id: 'edge1', + source: 'node1', + target: 'node2', + data: {}, + }, + { + id: 'edge2', + source: 'node1', + target: 'node3', + data: {}, + }, + ], + }, + edgeState: { + disable: { + keyShape: { + stroke: '#0f0', + lineWidth: 2, + }, + }, + highlight: { + keyShape: { + stroke: '#00f', + opacity: 0.5, + }, + }, + }, + }); + graph.on('afterrender', () => { + expect(graph.findIdByState('edge', 'disable').length).toBe(0); + graph.setItemState('edge1', 'disable', true); + expect(graph.findIdByState('edge', 'disable').length).toBe(1); + expect(graph.findIdByState('edge', 'disable')[0]).toBe('edge1'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + graph.setItemState('edge1', 'disable', false); + expect(graph.findIdByState('edge', 'disable').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + // set multiple edges state + graph.setItemState(['edge1', 'edge2'], 'disable', true); + expect(graph.findIdByState('edge', 'disable').length).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#0f0'); + graph.setItemState('edge1', 'disable', false); + expect(graph.findIdByState('edge', 'disable').length).toBe(1); + expect(graph.findIdByState('edge', 'disable')[0]).toBe('edge2'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + graph.setItemState(['edge1', 'edge2'], 'disable', false); + expect(graph.findIdByState('edge', 'disable').length).toBe(0); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + + graph.setItemState(['edge2', 'edge1'], ['disable', 'highlight'], true); + expect(graph.findIdByState('edge', 'disable').length).toBe(2); + expect(graph.findIdByState('edge', 'highlight').length).toBe(2); + // should be merged styles from disable and highlight + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth, + ).toBe(2); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke, + ).toBe('#00f'); + expect( + graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.opacity, + ).toBe(0.5); + + // clear states + graph.clearItemState(['edge1', 'edge2']); + expect(graph.findIdByState('edge', 'disable').length).toBe(0); + expect(graph.findIdByState('edge', 'highlight').length).toBe(0); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth, + ).toBe(1); + expect( + graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity, + ).toBe(1); + + graph.destroy(); + done(); + }); + }); +}); \ No newline at end of file