diff --git a/packages/g6/__tests__/bugs/api-expand-element-z-index.spec.ts b/packages/g6/__tests__/bugs/api-expand-element-z-index.spec.ts new file mode 100644 index 00000000000..fc71b6f936f --- /dev/null +++ b/packages/g6/__tests__/bugs/api-expand-element-z-index.spec.ts @@ -0,0 +1,35 @@ +import { createGraph } from '@@/utils'; + +describe('api expand element z-index', () => { + it('when expand element, the z-index of descendant elements should be updated', async () => { + const graph = createGraph({ + animation: false, + data: { + nodes: [{ id: 'node-1' }, { id: 'node-2', combo: 'combo-2' }], + combos: [ + { id: 'combo-1', style: { collapsed: true } }, + { id: 'combo-2', combo: 'combo-1', style: { collapsed: true } }, + ], + }, + }); + + await graph.draw(); + + // @ts-expect-error context is private + const context = graph.context; + + graph.frontElement('node-1'); + + expect(context.element!.getElement('node-1')!.style.zIndex).toBe(1); + + graph.frontElement('combo-1'); + + expect(context.element!.getElement('combo-1')!.style.zIndex).toBe(2); + + await graph.expandElement('combo-1', false); + + await graph.expandElement('combo-2', false); + + expect(context.element!.getElement('node-2')!.style.zIndex).toBe(2); + }); +}); diff --git a/packages/g6/__tests__/demos/element-html-sub-graph.ts b/packages/g6/__tests__/demos/element-html-sub-graph.ts new file mode 100644 index 00000000000..fe68f40dcc0 --- /dev/null +++ b/packages/g6/__tests__/demos/element-html-sub-graph.ts @@ -0,0 +1,394 @@ +import type { DisplayObject, Group } from '@antv/g'; +import type { BaseComboStyleProps, GraphData, HTMLStyleProps, IElementEvent, NodeData } from '@antv/g6'; +import { BaseCombo, effect, ExtensionCategory, Graph, HTML, isCollapsed, register } from '@antv/g6'; + +export const elementHTMLSubGraph: TestCase = async (context) => { + interface CardNodeData { + type: 'card'; + status: 'expanded' | 'collapsed'; + data: { name: string; value: number }[]; + children: CardNodeData[] | [GraphNodeData]; + } + interface GraphNodeData { + type: 'graph'; + data: GraphData; + } + type Data = CardNodeData | GraphNodeData; + + const getSize = (d: NodeData) => { + const data = d.data as unknown as Data; + if (data.type === 'card') return data.status === 'expanded' ? [200, 100 * data.children.length] : [200, 100]; + else return [200, 200]; + }; + + class SubGraph extends HTML { + public connectedCallback(): void { + super.connectedCallback(); + this.drawSubGraph(); + } + + public render(attributes?: Required, container?: Group): void { + super.render(attributes, container); + this.drawSubGraph(); + } + + private get data() { + return this.context.graph.getElementData(this.id).data; + } + + private graph?: Graph; + + @effect((self, attributes) => { + const { data } = self.data; + return { data }; + }) + private drawSubGraph() { + if (!this.isConnected) return; + const data = this.data; + this.drawGraphNode(data!.data as GraphData); + } + + private drawGraphNode(data: GraphData) { + const [width, height] = this.getSize(); + const container = this.getDomElement(); + container.innerHTML = ''; + + const subGraph = new Graph({ + container, + width, + height, + animation: false, + data: data, + node: { + style: { + labelText: (d) => d.id, + iconFontFamily: 'iconfont', + iconText: '\ue6e5', + }, + }, + layout: { + type: 'force', + linkDistance: 50, + }, + behaviors: ['zoom-canvas', { type: 'drag-canvas', enable: (event: MouseEvent) => event.shiftKey === true }], + autoFit: 'view', + }); + + subGraph.render(); + + this.graph = subGraph; + } + + public destroy(): void { + this.graph?.destroy(); + super.destroy(); + } + } + + class CardCombo extends BaseCombo { + protected getKeyStyle(attributes: Required) { + const keyStyle = super.getKeyStyle(attributes); + const [width, height] = this.getKeySize(attributes); + return { + ...keyStyle, + width, + height, + x: -width / 2, + y: -height / 2, + }; + } + + protected drawKeyShape(attributes: Required, container: Group): DisplayObject | undefined { + const { collapsed } = attributes; + const outer = this.upsert('key', 'rect', this.getKeyStyle(attributes), container); + if (!outer || !collapsed) { + this.removeCardShape(); + return outer; + } + + this.drawCardShape(attributes, container); + + return outer; + } + + protected drawCardShape(attributes: Required, container: Group) { + const [width, height] = this.getCollapsedKeySize(attributes); + const data = this.context.graph.getComboData(this.id).data as unknown as CardNodeData; + + const baseX = -width / 2; + const baseY = -height / 2; + + this.upsert( + 'card-title', + 'text', + { + x: baseX, + y: baseY, + text: '点分组: ' + this.id, + textAlign: 'left', + textBaseline: 'top', + fontSize: 16, + fontWeight: 'bold', + fill: '#4083f7', + }, + container, + ); + + const gap = 10; + const sep = (width + gap) / data.data.length; + data.data.forEach(({ name, value }, index) => { + this.upsert( + `card-item-name-${index}`, + 'text', + { + x: baseX + index * sep, + y: baseY + 40, + text: name, + textAlign: 'left', + textBaseline: 'top', + fontSize: 12, + fill: 'gray', + }, + container, + ); + this.upsert( + `card-item-value-${index}`, + 'text', + { + x: baseX + index * sep, + y: baseY + 60, + text: value + '%', + textAlign: 'left', + textBaseline: 'top', + fontSize: 24, + }, + container, + ); + }); + } + + protected removeCardShape() { + Object.entries(this.shapeMap).forEach(([key, shape]) => { + if (key.startsWith('card-')) { + delete this.shapeMap[key]; + shape.destroy(); + } + }); + } + } + + register(ExtensionCategory.NODE, 'sub-graph', SubGraph); + register(ExtensionCategory.COMBO, 'card', CardCombo); + + const graph = new Graph({ + ...context, + animation: false, + zoom: 0.8, + data: { + nodes: [ + { + id: 'node-1', + combo: 'combo-1-1', + style: { x: 120, y: 70 }, + data: { + data: { + nodes: [ + { id: 'node-1' }, + { id: 'node-2' }, + { id: 'node-3' }, + { id: 'node-4' }, + { id: 'node-5' }, + { id: 'node-6' }, + { id: 'node-7' }, + { id: 'node-8' }, + ], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-1', target: 'node-4' }, + { source: 'node-1', target: 'node-5' }, + { source: 'node-1', target: 'node-6' }, + { source: 'node-1', target: 'node-7' }, + { source: 'node-1', target: 'node-8' }, + ], + }, + }, + }, + { + id: 'node-2', + combo: 'combo-1-2', + style: { x: 370, y: 70 }, + data: { + data: { + nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-1', target: 'node-4' }, + ], + }, + }, + }, + { + id: 'node-3', + combo: 'combo-1-3-1', + style: { x: 120, y: 220 }, + data: { + data: { + nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-1', target: 'node-4' }, + ], + }, + }, + }, + { + id: 'node-4', + combo: 'combo-1-3-2', + style: { x: 120, y: 370 }, + data: { + data: { + nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-1', target: 'node-4' }, + ], + }, + }, + }, + ], + edges: [], + combos: [ + { + id: 'combo-1', + data: { + data: [ + { name: '指标名1', value: 33 }, + { name: '指标名2', value: 44 }, + { name: '指标名3', value: 55 }, + ], + }, + }, + { + id: 'combo-1-1', + combo: 'combo-1', + style: { collapsed: true }, + data: { + data: [ + { name: '指标名1', value: 33 }, + { name: '指标名2', value: 44 }, + { name: '指标名3', value: 55 }, + ], + }, + }, + { + id: 'combo-1-2', + combo: 'combo-1', + style: { collapsed: true }, + data: { + data: [ + { name: '指标名1', value: 33 }, + { name: '指标名2', value: 44 }, + { name: '指标名3', value: 55 }, + ], + }, + }, + { + id: 'combo-1-3', + combo: 'combo-1', + style: { collapsed: true }, + data: { + data: [ + { name: '指标名1', value: 33 }, + { name: '指标名2', value: 44 }, + { name: '指标名3', value: 55 }, + ], + }, + }, + { + id: 'combo-1-3-1', + combo: 'combo-1-3', + style: { collapsed: true }, + data: { + data: [ + { name: '指标名1', value: 33 }, + { name: '指标名2', value: 44 }, + { name: '指标名3', value: 55 }, + ], + }, + }, + { + id: 'combo-1-3-2', + combo: 'combo-1-3', + style: { collapsed: true }, + data: { + data: [ + { name: '指标名1', value: 33 }, + { name: '指标名2', value: 44 }, + { name: '指标名3', value: 55 }, + ], + }, + }, + ], + }, + node: { + type: 'sub-graph', + style: { + dx: -100, + dy: -50, + size: getSize, + }, + }, + combo: { + type: 'card', + style: { + collapsedSize: [200, 100], + collapsedMarker: false, + radius: 10, + }, + }, + behaviors: [ + { type: 'drag-element', enable: (event: MouseEvent) => event.shiftKey !== true }, + 'collapse-expand', + 'zoom-canvas', + 'drag-canvas', + ], + plugins: [ + { + type: 'contextmenu', + getItems: (event: IElementEvent) => { + const { targetType, target } = event; + if (!['node', 'combo'].includes(targetType)) return []; + const id = target.id; + + if (targetType === 'combo') { + const data = graph.getComboData(id); + if (isCollapsed(data)) { + return [{ name: '展开', value: 'expanded' }]; + } else return [{ name: '收起', value: 'collapsed' }]; + } + return [{ name: '收起', value: 'collapsed' }]; + }, + onClick: (value: CardNodeData['status'], target: HTMLElement, current: SubGraph) => { + const id = current.id; + const elementType = graph.getElementType(id); + + if (elementType === 'node') { + const parent = graph.getParentData(id, 'combo'); + if (parent) return graph.collapseElement(parent.id, false); + } + + if (value === 'expanded') graph.expandElement(id, false); + else graph.collapseElement(id, false); + }, + }, + ], + }); + + await graph.render(); + + return graph; +}; diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts index faaac7c9cb7..71dda06dc8b 100644 --- a/packages/g6/__tests__/demos/index.ts +++ b/packages/g6/__tests__/demos/index.ts @@ -44,6 +44,7 @@ export { elementEdgePolylineAstar } from './element-edge-polyline-astar'; export { elementEdgePort } from './element-edge-port'; export { elementEdgeQuadratic } from './element-edge-quadratic'; export { elementEdgeSize } from './element-edge-size'; +export { elementHTMLSubGraph } from './element-html-sub-graph'; export { elementLabelBackground } from './element-label-background'; export { elementLabelOversized } from './element-label-oversized'; export { elementNodeAvatar } from './element-node-avatar'; diff --git a/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-0.svg b/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-0.svg index c4b6890bb07..46c1c19c708 100644 --- a/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-0.svg +++ b/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-0.svg @@ -3,13 +3,6 @@ - - - - - - - @@ -41,6 +34,13 @@ + + + + + + + diff --git a/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-1000.svg b/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-1000.svg index 12aa87498a4..2aa7dd18f26 100644 --- a/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-1000.svg +++ b/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-1000.svg @@ -3,13 +3,6 @@ - - - - - - - @@ -41,6 +34,13 @@ + + + + + + + diff --git a/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-500.svg b/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-500.svg index ba3b1e1ea24..37c68ae3e0c 100644 --- a/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-500.svg +++ b/packages/g6/__tests__/snapshots/behaviors/collapse-expand-combo/collapse-combo-1-expand-combo-2-500.svg @@ -3,13 +3,6 @@ - - - - - - - @@ -41,6 +34,13 @@ + + + + + + + diff --git a/packages/g6/__tests__/snapshots/behaviors/collapse-expand/expand-combo-1.svg b/packages/g6/__tests__/snapshots/behaviors/collapse-expand/expand-combo-1.svg index 5a04a9f5b34..8e8287c7595 100644 --- a/packages/g6/__tests__/snapshots/behaviors/collapse-expand/expand-combo-1.svg +++ b/packages/g6/__tests__/snapshots/behaviors/collapse-expand/expand-combo-1.svg @@ -10,13 +10,6 @@ - - - - - - - @@ -29,50 +22,57 @@ - + - + - + - - node-2 + + combo-1 - + - node-3 + node-1 - + + - + + - + + + + + + - - combo-1 + + node-2 - + - node-1 + node-3 diff --git a/packages/g6/__tests__/snapshots/plugins/history/plugin-history/expand.svg b/packages/g6/__tests__/snapshots/plugins/history/plugin-history/expand.svg index 5a04a9f5b34..8e8287c7595 100644 --- a/packages/g6/__tests__/snapshots/plugins/history/plugin-history/expand.svg +++ b/packages/g6/__tests__/snapshots/plugins/history/plugin-history/expand.svg @@ -10,13 +10,6 @@ - - - - - - - @@ -29,50 +22,57 @@ - + - + - + - - node-2 + + combo-1 - + - node-3 + node-1 - + + - + + - + + + + + + - - combo-1 + + node-2 - + - node-1 + node-3 diff --git a/packages/g6/__tests__/unit/utils/dom.spec.ts b/packages/g6/__tests__/unit/utils/dom.spec.ts index cbdab31d8ff..817f505d4ef 100644 --- a/packages/g6/__tests__/unit/utils/dom.spec.ts +++ b/packages/g6/__tests__/unit/utils/dom.spec.ts @@ -37,6 +37,12 @@ describe('sizeOf', () => { expect(el.style.pointerEvents).not.toBe('none'); }); + it('createPluginContainer with style', () => { + const el = createPluginContainer('test', false, { color: 'red' }); + expect(el.getAttribute('class')).toBe('g6-test'); + expect(el.style.color).toBe('red'); + }); + it('insertDOM', () => { insertDOM('g6-test', 'div', { color: 'red' }, 'test', document.body); diff --git a/packages/g6/src/elements/effect.ts b/packages/g6/src/elements/effect.ts index 0c3a6e01db5..b344967e84d 100644 --- a/packages/g6/src/elements/effect.ts +++ b/packages/g6/src/elements/effect.ts @@ -2,9 +2,9 @@ import type { Element } from '../types'; import { getCachedStyle, setCacheStyle } from '../utils/cache'; /** - * 基于样式属性是否变化控制函数是否执行 + * 优化方法执行次数,仅在样式属性发生变化时执行函数 * - * Control whether the function is executed based on whether the style attribute changes + * Optimize the number of method executions, and only execute the function when the style attributes change * @param styler - 获取样式属性函数 | Get style attribute function * @returns 装饰器 | Decorator * @remarks @@ -15,6 +15,23 @@ import { getCachedStyle, setCacheStyle } from '../utils/cache'; * Only when getStyle is specified, the function will be called with the current attributes and the new attributes respectively. If they are the same, the function will not be executed. * * If shapeKey is specified, the attributes of the shape will be directly obtained as the original style attributes, which is usually used when the bounding box of the element is used in the getStyle function. + * @example + * 仅当 value 发生变化时执行函数 + * + * Execute the function only when value changes + * + * ```typescript + * class CustomNode extends BaseNode { + * + * @effect((self, attributes) => { + * const { value } = attributes; + * return { value } + * }) + * drawCustomShape(attributes, container) { + * this.upsert('custom', 'circle', { ...attributes }, container); + * } + * } + * ``` */ export function effect(styler: (self: any, attributes: Record) => Record) { return function (target: Element, propertyKey: string, descriptor: PropertyDescriptor) { diff --git a/packages/g6/src/elements/nodes/html.ts b/packages/g6/src/elements/nodes/html.ts index 1b0b85fb3da..a453d0b9935 100644 --- a/packages/g6/src/elements/nodes/html.ts +++ b/packages/g6/src/elements/nodes/html.ts @@ -9,6 +9,7 @@ import { IEventTarget, Rect, } from '@antv/g'; +import { Renderer } from '@antv/g-canvas'; import { isNil, isUndefined, pick } from '@antv/util'; import { CommonEvent } from '../../constants'; import type { BaseNodeStyleProps } from './base-node'; @@ -94,12 +95,17 @@ export class HTML extends BaseNode { protected drawKeyShape(attributes: Required, container: Group) { const style = this.getKeyStyle(attributes); - const { width = 0, height = 0 } = style; - const bounds = this.upsert('key-container', Rect, { width, height, opacity: 0 }, container)!; + const { x, y, width = 0, height = 0 } = style; + const bounds = this.upsert('key-container', Rect, { x, y, width, height, opacity: 0 }, container)!; return this.upsert('key', GHTML, style, bounds); } public connectedCallback() { + // only enable in canvas renderer + const renderer = this.context.canvas.getRenderer('main'); + const isCanvasRenderer = renderer instanceof Renderer; + if (!isCanvasRenderer) return; + const element = this.getDomElement(); this.events.forEach((eventName) => { // @ts-expect-error assert event is PointerEvent diff --git a/packages/g6/src/exports.ts b/packages/g6/src/exports.ts index 7de656410d4..ee2cd8c66b6 100644 --- a/packages/g6/src/exports.ts +++ b/packages/g6/src/exports.ts @@ -28,6 +28,7 @@ export { } from './constants'; export { BaseCombo, CircleCombo, RectCombo } from './elements/combos'; export { BaseEdge, Cubic, CubicHorizontal, CubicVertical, Line, Polyline, Quadratic } from './elements/edges'; +export { effect } from './elements/effect'; export { BaseNode, Circle, diff --git a/packages/g6/src/plugins/contextmenu/index.ts b/packages/g6/src/plugins/contextmenu/index.ts index 7c8abf49084..05a9dc7435f 100644 --- a/packages/g6/src/plugins/contextmenu/index.ts +++ b/packages/g6/src/plugins/contextmenu/index.ts @@ -100,7 +100,7 @@ export class Contextmenu extends BasePlugin { } private initElement() { - this.$element = createPluginContainer('contextmenu', false); + this.$element = createPluginContainer('contextmenu', false, { zIndex: '99' }); const { className } = this.options; if (className) this.$element.classList.add(className); diff --git a/packages/g6/src/runtime/element.ts b/packages/g6/src/runtime/element.ts index 0d4c121a848..59ebf676286 100644 --- a/packages/g6/src/runtime/element.ts +++ b/packages/g6/src/runtime/element.ts @@ -224,7 +224,7 @@ export class ElementController { return this.elementMap[id] as T; } - public getElementZIndex(id: ID) { + public getElementZIndex(id: ID): number { const element = this.getElement(id); if (!element) return 0; return element.style.zIndex ?? 0; @@ -382,6 +382,12 @@ export class ElementController { this.emit(new ElementLifeCycleEvent(GraphEvent.BEFORE_ELEMENT_CREATE, elementType, datum), context); + if (context.stage === 'expand') { + // 如果是展开的元素,需要将其 zIndex 提升至目标元素的上层 + const targetZIndex = this.getElementZIndex(context.target!); + if (!style.zIndex || style.zIndex < targetZIndex) style.zIndex = targetZIndex + (style.zIndex ?? 0); + } + const element = this.container.appendChild( new Ctor({ id, @@ -636,7 +642,7 @@ export class ElementController { const { drawData: { add }, } = this.computeChangesAndDrawData({ stage: 'collapse', animation })!; - this.createElements(add, { animation: false, stage: 'expand' }); + this.createElements(add, { animation: false, stage: 'expand', target: id }); // 重置动画 / Reset animation this.context.animation!.clear(); @@ -725,7 +731,7 @@ export class ElementController { this.computeStyle('expand'); const { dataChanges, drawData } = this.computeChangesAndDrawData({ stage: 'expand', animation })!; const { add, update } = drawData; - const context = { animation, stage: 'expand', data: drawData } as const; + const context = { animation, stage: 'expand', data: drawData, target: id } as const; this.createElements(add, context); this.updateElements(update, context); @@ -819,4 +825,6 @@ export interface DrawContext { collapseExpandTarget?: ID; /** 绘制类型 | Draw type */ type?: 'render' | 'draw'; + /** 展开阶段的目标元素 id | ID of the target element in the expand stage */ + target?: ID; } diff --git a/packages/g6/src/utils/dom.ts b/packages/g6/src/utils/dom.ts index f972cc812f9..bb4cd196a37 100644 --- a/packages/g6/src/utils/dom.ts +++ b/packages/g6/src/utils/dom.ts @@ -44,28 +44,37 @@ export function sizeOf(container: HTMLElement): [number, number] { } /** - * Create a plugin DOM element. - * @param type - plugin type - * @param cover - cover the container - * @returns plugin DOM element + * 创建插件容器 + * + * Create a plugin container + * @param type - 插件类型 | plugin type + * @param cover - 容器是否覆盖整个画布 | Whether the container covers the entire canvas + * @param style - 额外样式 | Additional style + * @returns 插件容器 | plugin container */ -export function createPluginContainer(type: string, cover = true) { - const el = document.createElement('div'); +export function createPluginContainer(type: string, cover = true, style?: Partial): HTMLElement { + const container = document.createElement('div'); - el.setAttribute('class', `g6-${type}`); + container.setAttribute('class', `g6-${type}`); - el.style.position = 'absolute'; - el.style.display = 'block'; + Object.assign(container.style, { + position: 'absolute', + display: 'block', + }); if (cover) { - el.style.inset = '0px'; - el.style.height = '100%'; - el.style.width = '100%'; - el.style.overflow = 'hidden'; - el.style.pointerEvents = 'none'; + Object.assign(container.style, { + inset: '0px', + height: '100%', + width: '100%', + overflow: 'hidden', + pointerEvents: 'none', + }); } - return el; + if (style) Object.assign(container.style, style); + + return container; } /** diff --git a/packages/site/examples/scene-case/default/demo/meta.json b/packages/site/examples/scene-case/default/demo/meta.json index c5ab6111981..e29d85c0585 100644 --- a/packages/site/examples/scene-case/default/demo/meta.json +++ b/packages/site/examples/scene-case/default/demo/meta.json @@ -51,6 +51,14 @@ "en": "Organization Chart" }, "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wgoUR6dnVcsAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "sub-graph.js", + "title": { + "zh": "子图", + "en": "Sub Graph" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*2HzDTrQZ910AAAAAAAAAAAAADmJ7AQ/original" } ] } diff --git a/packages/site/examples/scene-case/default/demo/sub-graph.js b/packages/site/examples/scene-case/default/demo/sub-graph.js new file mode 100644 index 00000000000..47632cd2fbf --- /dev/null +++ b/packages/site/examples/scene-case/default/demo/sub-graph.js @@ -0,0 +1,336 @@ +import { BaseCombo, ExtensionCategory, Graph, HTML, isCollapsed, register } from '@antv/g6'; +import { isEqual } from '@antv/util'; + +class SubGraphNode extends HTML { + connectedCallback() { + super.connectedCallback(); + this.drawSubGraph(); + } + + render(attributes, container) { + super.render(attributes, container); + this.drawSubGraph(); + } + + get data() { + return this.context.graph.getElementData(this.id).data; + } + + drawSubGraph() { + if (!this.isConnected) return; + if (isEqual(this.previousData, this.data)) return; + this.previousData = this.data; + + const data = this.data; + this.drawGraphNode(data.data); + } + + drawGraphNode(data) { + const [width, height] = this.getSize(); + const container = this.getDomElement(); + container.innerHTML = ''; + + const subGraph = new Graph({ + container, + width, + height, + animation: false, + data: data, + node: { + style: { + labelText: (d) => d.id, + iconFontFamily: 'iconfont', + iconText: '\ue6e5', + }, + }, + layout: { + type: 'force', + linkDistance: 50, + }, + behaviors: ['zoom-canvas', { type: 'drag-canvas', enable: (event) => event.shiftKey === true }], + autoFit: 'view', + }); + + subGraph.render(); + + this.graph = subGraph; + } + + destroy() { + this.graph?.destroy(); + super.destroy(); + } +} + +class CardCombo extends BaseCombo { + getKeyStyle(attributes) { + const keyStyle = super.getKeyStyle(attributes); + const [width, height] = this.getKeySize(attributes); + return { + ...keyStyle, + width, + height, + x: -width / 2, + y: -height / 2, + }; + } + + drawKeyShape(attributes, container) { + const { collapsed } = attributes; + const outer = this.upsert('key', 'rect', this.getKeyStyle(attributes), container); + if (!outer || !collapsed) { + this.removeCardShape(); + return outer; + } + + this.drawCardShape(attributes, container); + + return outer; + } + + drawCardShape(attributes, container) { + const [width, height] = this.getCollapsedKeySize(attributes); + const data = this.context.graph.getComboData(this.id).data; + + const baseX = -width / 2; + const baseY = -height / 2; + + this.upsert( + 'card-title', + 'text', + { + x: baseX, + y: baseY, + text: 'Group: ' + this.id, + textAlign: 'left', + textBaseline: 'top', + fontSize: 16, + fontWeight: 'bold', + fill: '#4083f7', + }, + container, + ); + + const gap = 10; + const sep = (width + gap) / data.data.length; + data.data.forEach(({ name, value }, index) => { + this.upsert( + `card-item-name-${index}`, + 'text', + { + x: baseX + index * sep, + y: baseY + 40, + text: name, + textAlign: 'left', + textBaseline: 'top', + fontSize: 12, + fill: 'gray', + }, + container, + ); + this.upsert( + `card-item-value-${index}`, + 'text', + { + x: baseX + index * sep, + y: baseY + 60, + text: value + '%', + textAlign: 'left', + textBaseline: 'top', + fontSize: 24, + }, + container, + ); + }); + } + + removeCardShape() { + Object.entries(this.shapeMap).forEach(([key, shape]) => { + if (key.startsWith('card-')) { + delete this.shapeMap[key]; + shape.destroy(); + } + }); + } +} + +register(ExtensionCategory.NODE, 'sub-graph', SubGraphNode); +register(ExtensionCategory.COMBO, 'card', CardCombo); + +const getSize = (d) => { + const data = d.data; + if (data.type === 'card') return data.status === 'expanded' ? [200, 100 * data.children.length] : [200, 100]; + else return [200, 200]; +}; + +const graph = new Graph({ + container: 'container', + animation: false, + zoom: 0.8, + data: { + nodes: [ + { + id: '1', + combo: 'A', + style: { x: 120, y: 70 }, + data: { + data: { + nodes: [ + { id: 'node-1' }, + { id: 'node-2' }, + { id: 'node-3' }, + { id: 'node-4' }, + { id: 'node-5' }, + { id: 'node-6' }, + { id: 'node-7' }, + { id: 'node-8' }, + ], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-1', target: 'node-4' }, + { source: 'node-1', target: 'node-5' }, + { source: 'node-1', target: 'node-6' }, + { source: 'node-1', target: 'node-7' }, + { source: 'node-1', target: 'node-8' }, + ], + }, + }, + }, + { + id: '2', + combo: 'C', + style: { x: 370, y: 70 }, + data: { + data: { + nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-1', target: 'node-4' }, + ], + }, + }, + }, + { + id: 'node-4', + combo: 'D', + style: { x: 370, y: 200 }, + data: { + data: { + nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-1', target: 'node-4' }, + ], + }, + }, + }, + ], + edges: [], + combos: [ + { + id: 'root', + data: { + data: [ + { name: 'percent', value: 50 }, + { name: 'percent', value: 45 }, + { name: 'percent', value: 70 }, + ], + }, + }, + { + id: 'A', + combo: 'root', + data: { + data: [ + { name: 'percent', value: 30 }, + { name: 'percent', value: 90 }, + ], + }, + }, + { + id: 'B', + combo: 'root', + style: { collapsed: true }, + data: { + data: [ + { name: 'percent', value: 60 }, + { name: 'percent', value: 80 }, + ], + }, + }, + { + id: 'C', + combo: 'B', + style: { collapsed: true }, + data: { + data: [{ name: 'percent', value: 60 }], + }, + }, + { + id: 'D', + combo: 'B', + style: { collapsed: true }, + data: { + data: [{ name: 'percent', value: 80 }], + }, + }, + ], + }, + node: { + type: 'sub-graph', + style: { + dx: -100, + dy: -50, + size: getSize, + }, + }, + combo: { + type: 'card', + style: { + collapsedSize: [200, 100], + collapsedMarker: false, + radius: 10, + }, + }, + behaviors: [ + { type: 'drag-element', enable: (event) => event.shiftKey !== true }, + 'collapse-expand', + 'zoom-canvas', + 'drag-canvas', + ], + plugins: [ + { + type: 'contextmenu', + getItems: (event) => { + const { targetType, target } = event; + if (!['node', 'combo'].includes(targetType)) return []; + const id = target.id; + + if (targetType === 'combo') { + const data = graph.getComboData(id); + if (isCollapsed(data)) { + return [{ name: '展开', value: 'expanded' }]; + } else return [{ name: '收起', value: 'collapsed' }]; + } + return [{ name: '收起', value: 'collapsed' }]; + }, + onClick: (value, target, current) => { + const id = current.id; + const elementType = graph.getElementType(id); + + if (elementType === 'node') { + const parent = graph.getParentData(id, 'combo'); + if (parent) return graph.collapseElement(parent.id, false); + } + + if (value === 'expanded') graph.expandElement(id, false); + else graph.collapseElement(id, false); + }, + }, + ], +}); + +graph.render();