UNPKG

@antv/g6

Version:

A Graph Visualization Framework in JavaScript

272 lines (237 loc) 9.56 kB
import { AABB, BaseStyleProps, DisplayObject, DisplayObjectConfig, Group } from '@antv/g'; import { isFunction } from '@antv/util'; import type { CollapsedMarkerStyleProps, Combo, ID, NodeLikeData, Padding, Point, Prefix, STDSize, Size, } from '../../types'; import { getBBoxHeight, getBBoxWidth, getCombinedBBox, getExpandedBBox } from '../../utils/bbox'; import { idOf } from '../../utils/id'; import { parsePadding } from '../../utils/padding'; import { getXYByPlacement, hasPosition, positionOf } from '../../utils/position'; import { subStyleProps } from '../../utils/prefix'; import { parseSize } from '../../utils/size'; import { mergeOptions } from '../../utils/style'; import { add, divide } from '../../utils/vector'; import type { BaseNodeStyleProps } from '../nodes'; import { BaseNode } from '../nodes'; import { Icon, IconStyleProps } from '../shapes'; import { connectImage, dispatchPositionChange } from '../shapes/image'; /** * <zh/> 组合通用样式配置项 * * <en/> Common style props for combo */ export interface BaseComboStyleProps extends BaseNodeStyleProps, Prefix<'collapsed', BaseStyleProps>, Prefix<'collapsedMarker', CollapsedMarkerStyleProps> { /** * <zh/> 组合展开后的默认大小 * * <en/> The default size of combo when expanded */ size?: Size; /** * <zh/> 组合收起后的默认大小 * * <en/> The default size of combo when collapsed */ collapsedSize?: Size; /** * <zh/> 组合的子元素,可以是节点或者组合 * * <en/> The children of combo, which can be nodes or combos */ childrenNode?: ID[]; /** * <zh/> 组合的子元素数据 * * <en/> The data of the children of combo * @remarks * <zh/> 如果组合是收起状态,children 可能为空,通过 childrenData 能够获取完整的子元素数据 * * <en/> If the combo is collapsed, children may be empty, and the complete child element data can be obtained through childrenData */ childrenData?: NodeLikeData[]; /** * <zh/> 组合的内边距,只在展开状态下生效 * * <en/> The padding of combo, only effective when expanded */ padding?: Padding; /** * <zh/> 组合收起时是否显示标记 * * <en/> Whether to show the marker when the combo is collapsed */ collapsedMarker?: boolean; } /** * <zh/> 组合元素的基类 * * <en/> Base class of combo * @remarks * <zh/> 自定义组合时,推荐使用这个类作为基类。这样,用户只需要专注于实现 keyShape 的绘制逻辑 * * <en/> When customizing a combo, it is recommended to use this class as the base class. In this way, users only need to focus on the logic of drawing keyShape */ export abstract class BaseCombo<S extends BaseComboStyleProps = BaseComboStyleProps> extends BaseNode<S> implements Combo { public type = 'combo'; static defaultStyleProps: Partial<BaseComboStyleProps> = { childrenNode: [], droppable: true, draggable: true, collapsed: false, collapsedSize: 32, collapsedMarker: true, collapsedMarkerZIndex: 1, collapsedMarkerFontSize: 12, collapsedMarkerTextAlign: 'center', collapsedMarkerTextBaseline: 'middle', collapsedMarkerType: 'child-count', }; constructor(options: DisplayObjectConfig<BaseComboStyleProps>) { super(mergeOptions({ style: BaseCombo.defaultStyleProps }, options)); this.updateComboPosition(this.parsedAttributes); } /** * Draw the key shape of combo */ protected abstract drawKeyShape(attributes: Required<S>, container: Group): DisplayObject | undefined; protected getKeySize(attributes: Required<S>): STDSize { const { collapsed, childrenNode = [] } = attributes; if (childrenNode.length === 0) return this.getEmptyKeySize(attributes); return collapsed ? this.getCollapsedKeySize(attributes) : this.getExpandedKeySize(attributes); } protected getEmptyKeySize(attributes: Required<S>): STDSize { const { padding, collapsedSize } = attributes; const [top, right, bottom, left] = parsePadding(padding); return add(parseSize(collapsedSize), [left + right, top + bottom, 0]) as STDSize; } protected getCollapsedKeySize(attributes: Required<S>): STDSize { return parseSize(attributes.collapsedSize); } protected getExpandedKeySize(attributes: Required<S>): STDSize { const contentBBox = this.getContentBBox(attributes); return [getBBoxWidth(contentBBox), getBBoxHeight(contentBBox), 0]; } protected getContentBBox(attributes: Required<S>): AABB { const { childrenNode = [], padding } = attributes; const children = childrenNode.map((id) => this.context!.element!.getElement(id)).filter(Boolean); if (children.length === 0) { const bbox = new AABB(); const { x = 0, y = 0, size } = attributes; const [width, height] = parseSize(size); bbox.setMinMax([x - width / 2, y - height / 2, 0], [x + width / 2, y + height / 2, 0]); return bbox; } const childrenBBox = getCombinedBBox(children.map((child) => child!.getBounds())); if (!padding) return childrenBBox; return getExpandedBBox(childrenBBox, padding); } protected drawCollapsedMarkerShape(attributes: Required<S>, container: Group): void { const style = this.getCollapsedMarkerStyle(attributes); this.upsert('collapsed-marker', Icon, style, container); connectImage(this); } protected getCollapsedMarkerStyle(attributes: Required<S>): IconStyleProps | false { if (!attributes.collapsed || !attributes.collapsedMarker) return false; const { type, ...collapsedMarkerStyle } = subStyleProps<CollapsedMarkerStyleProps>( this.getGraphicStyle(attributes), 'collapsedMarker', ); const keyShape = this.getShape('key'); const [x, y] = getXYByPlacement(keyShape.getLocalBounds(), 'center'); const style = { ...collapsedMarkerStyle, x, y }; if (type) { const text = this.getCollapsedMarkerText(type, attributes); Object.assign(style, { text }); } return style; } protected getCollapsedMarkerText(type: CollapsedMarkerStyleProps['type'], attributes: Required<S>): string { const { childrenData = [] } = attributes; const { model } = this.context; if (type === 'descendant-count') return model.getDescendantsData(this.id).length.toString(); if (type === 'child-count') return childrenData.length.toString(); if (type === 'node-count') return model .getDescendantsData(this.id) .filter((datum) => model.getElementType(idOf(datum)) === 'node') .length.toString(); if (isFunction(type)) return type(childrenData); return ''; } public getComboPosition(attributes: Required<S>): Point { const { x = 0, y = 0, collapsed, childrenData = [] } = attributes; if (childrenData.length === 0) return [+x, +y, 0]; if (collapsed) { const { model } = this.context; const descendants = model.getDescendantsData(this.id).filter((datum) => !model.isCombo(idOf(datum))); if (descendants.length > 0 && descendants.some(hasPosition)) { // combo 被收起,返回平均中心位置 / combo is collapsed, return the average center position const totalPosition = descendants.reduce((acc, datum) => add(acc, positionOf(datum)), [0, 0, 0] as Point); return divide(totalPosition, descendants.length); } // empty combo return [+x, +y, 0]; } return this.getContentBBox(attributes).center; } protected getComboStyle(attributes: Required<S>) { const [x, y] = this.getComboPosition(attributes); // x/y will be used to calculate position later. return { x, y, transform: [['translate', x, y]] }; } protected updateComboPosition(attributes: Required<S>) { const comboStyle = this.getComboStyle(attributes); Object.assign(this.style, comboStyle); // Sync combo position to model const { x, y } = comboStyle; this.context.model.syncNodeLikeDatum({ id: this.id, style: { x, y } }); dispatchPositionChange(this); } public render(attributes: Required<S>, container: Group = this) { super.render(attributes, container); // collapsed marker this.drawCollapsedMarkerShape(attributes, container); } public update(attr: Partial<S> = {}): void { super.update(attr); this.updateComboPosition(this.parsedAttributes); } protected onframe() { super.onframe(); // 收起状态下,通过动画来更新位置 // Update position through animation in collapsed state if (!this.attributes.collapsed) this.updateComboPosition(this.parsedAttributes); this.drawKeyShape(this.parsedAttributes, this); } public animate(keyframes: Keyframe[], options?: number | KeyframeAnimationOptions) { const animation = super.animate( this.attributes.collapsed ? keyframes : // 如果当前 combo 是展开状态,则动画不受 x, y, z, transform 影响,仅由子元素决定位置 // If the current combo is in the expanded state, the animation is not affected by x, y, z, transform, and the position is determined only by the child elements keyframes.map(({ x, y, z, transform, ...keyframe }: any) => keyframe), options, ); if (!animation) return animation; return new Proxy(animation, { set: (target, propKey, value) => { if (propKey === 'currentTime') Promise.resolve().then(() => this.onframe()); return Reflect.set(target, propKey, value); }, }); } }