From 697cad1117ef5ce0a2a77197600c0af2eb425e56 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 11 Sep 2024 11:07:38 +0800 Subject: [PATCH 1/6] feat: add hierarchical graph --- packages/graphs/package.json | 30 +++--- .../components/HierarchicalGraph/index.tsx | 5 - .../components/hierarchical-graph/index.tsx | 14 +++ packages/graphs/src/components/index.ts | 4 +- packages/graphs/src/core/base-graph.tsx | 48 ++++++++++ packages/graphs/src/core/hoc/index.ts | 1 + .../src/core/hoc/with-collapsible-node.tsx | 71 +++++++++++++++ packages/graphs/src/core/hooks/index.ts | 1 + packages/graphs/src/core/hooks/useOptions.tsx | 67 ++++++++++++++ packages/graphs/src/core/nodes/index.ts | 1 + packages/graphs/src/core/nodes/plain-node.tsx | 28 ++++++ packages/graphs/src/core/nodes/types.ts | 12 +++ packages/graphs/src/core/registry/build-in.ts | 11 +++ packages/graphs/src/core/registry/register.ts | 11 +++ packages/graphs/src/core/transform/index.ts | 1 + .../src/core/transform/infer-react-style.ts | 30 ++++++ packages/graphs/src/core/utils/node.tsx | 91 +++++++++++++++++++ packages/graphs/src/core/utils/options.ts | 38 ++++++++ packages/graphs/src/index.ts | 5 +- packages/graphs/src/preset.ts | 3 + packages/graphs/src/types.ts | 47 ++++++++++ .../graphs/tests/demos/error-boundary.tsx | 5 - .../graphs/tests/demos/hierarchical-graph.tsx | 27 ++++++ packages/graphs/tests/demos/index.tsx | 1 + packages/graphs/tests/main.tsx | 2 +- packages/graphs/tsconfig.json | 6 +- packages/graphs/vite.config.ts | 1 + packages/plots/src/interface.ts | 50 +--------- packages/util/src/index.ts | 3 +- packages/util/src/types.ts | 46 ++++++++++ 30 files changed, 584 insertions(+), 76 deletions(-) delete mode 100644 packages/graphs/src/components/HierarchicalGraph/index.tsx create mode 100644 packages/graphs/src/components/hierarchical-graph/index.tsx create mode 100644 packages/graphs/src/core/base-graph.tsx create mode 100644 packages/graphs/src/core/hoc/index.ts create mode 100644 packages/graphs/src/core/hoc/with-collapsible-node.tsx create mode 100644 packages/graphs/src/core/hooks/index.ts create mode 100644 packages/graphs/src/core/hooks/useOptions.tsx create mode 100644 packages/graphs/src/core/nodes/index.ts create mode 100644 packages/graphs/src/core/nodes/plain-node.tsx create mode 100644 packages/graphs/src/core/nodes/types.ts create mode 100644 packages/graphs/src/core/registry/build-in.ts create mode 100644 packages/graphs/src/core/registry/register.ts create mode 100644 packages/graphs/src/core/transform/index.ts create mode 100644 packages/graphs/src/core/transform/infer-react-style.ts create mode 100644 packages/graphs/src/core/utils/node.tsx create mode 100644 packages/graphs/src/core/utils/options.ts create mode 100644 packages/graphs/src/preset.ts create mode 100644 packages/graphs/src/types.ts delete mode 100644 packages/graphs/tests/demos/error-boundary.tsx create mode 100644 packages/graphs/tests/demos/hierarchical-graph.tsx create mode 100644 packages/util/src/types.ts diff --git a/packages/graphs/package.json b/packages/graphs/package.json index aaf6b4d41..642e8d152 100644 --- a/packages/graphs/package.json +++ b/packages/graphs/package.json @@ -2,6 +2,16 @@ "name": "@ant-design/graphs", "version": "2.0.0-alpha.0", "description": "A React graph library based on Graphin", + "keywords": [ + "antv", + "g6", + "graph", + "graph analysis", + "graph editor", + "graph visualization", + "relational data", + "react" + ], "bugs": { "url": "https://github.com/ant-design/ant-design-charts/issues" }, @@ -9,6 +19,7 @@ "type": "git", "url": "git+https://github.com/ant-design/ant-design-charts.git" }, + "license": "MIT", "main": "lib/index.js", "unpkg": "dist/graphs.min.js", "module": "es/index.js", @@ -35,20 +46,14 @@ "start": "npm run build:esm --w", "test": "jest" }, - "keywords": [ - "antv", - "g6", - "graph", - "graph analysis", - "graph editor", - "graph visualization", - "relational data", - "react" - ], "dependencies": { + "@ant-design/charts-util": "workspace:*", + "@ant-design/icons": "^5.4.0", "@antv/g6": "^5.0.21", + "@antv/g6-extension-react": "^0.1.6", "@antv/graphin": "^3.0.2", - "lodash-es": "^4.17.21" + "lodash-es": "^4.17.21", + "styled-components": "^6.1.13" }, "devDependencies": { "@types/jest": "^26.0.0", @@ -62,6 +67,5 @@ "peerDependencies": { "react": ">=16.8.4", "react-dom": ">=16.8.4" - }, - "license": "MIT" + } } diff --git a/packages/graphs/src/components/HierarchicalGraph/index.tsx b/packages/graphs/src/components/HierarchicalGraph/index.tsx deleted file mode 100644 index 16bbb3d86..000000000 --- a/packages/graphs/src/components/HierarchicalGraph/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const HierarchicalGraph = () => { - return
HierarchicalGraph
; -}; diff --git a/packages/graphs/src/components/hierarchical-graph/index.tsx b/packages/graphs/src/components/hierarchical-graph/index.tsx new file mode 100644 index 000000000..f98841617 --- /dev/null +++ b/packages/graphs/src/components/hierarchical-graph/index.tsx @@ -0,0 +1,14 @@ +import type { Graph } from '@antv/g6'; +import React, { forwardRef, ForwardRefExoticComponent, PropsWithChildren, PropsWithoutRef, RefAttributes } from 'react'; +import { BaseGraph } from '../../core/base-graph'; +import type { GraphOptions } from '../../types'; + +const HierarchicalGraph: ForwardRefExoticComponent< + PropsWithoutRef> & RefAttributes +> = forwardRef>(({ children, ...props }, ref) => ( + + {children} + +)); + +export default HierarchicalGraph; diff --git a/packages/graphs/src/components/index.ts b/packages/graphs/src/components/index.ts index 83e25a1f1..b6a0316cc 100644 --- a/packages/graphs/src/components/index.ts +++ b/packages/graphs/src/components/index.ts @@ -1 +1,3 @@ -export { HierarchicalGraph } from './HierarchicalGraph'; +import HierarchicalGraph from './hierarchical-graph'; + +export { HierarchicalGraph }; diff --git a/packages/graphs/src/core/base-graph.tsx b/packages/graphs/src/core/base-graph.tsx new file mode 100644 index 000000000..940858f85 --- /dev/null +++ b/packages/graphs/src/core/base-graph.tsx @@ -0,0 +1,48 @@ +import { ChartLoading, ErrorBoundary } from '@ant-design/charts-util'; +import type { Graph } from '@antv/g6'; +import { Graphin } from '@antv/graphin'; +import React, { + forwardRef, + ForwardRefExoticComponent, + PropsWithChildren, + PropsWithoutRef, + RefAttributes, + useImperativeHandle, + useRef, +} from 'react'; +import type { GraphOptions, GraphType } from '../types'; +import { useOptions } from './hooks'; + +interface BaseGraphProps extends GraphOptions { + /** + * 内部属性,只读 + */ + readonly type: GraphType; +} + +export const BaseGraph: ForwardRefExoticComponent< + PropsWithoutRef> & RefAttributes +> = forwardRef>(({ children, ...props }, ref) => { + const { type, containerStyle, className, errorTemplate, loading, loadingTemplate, ...propOptions } = props; + const graphRef = useRef(null); + + const options = useOptions(type, propOptions); + + useImperativeHandle(ref, () => graphRef.current!); + + return ( + + {loading && } + { + graphRef.current = ref; + }} + className={className} + style={containerStyle} + options={options} + > + {children} + + + ); +}); diff --git a/packages/graphs/src/core/hoc/index.ts b/packages/graphs/src/core/hoc/index.ts new file mode 100644 index 000000000..00e1a9ea4 --- /dev/null +++ b/packages/graphs/src/core/hoc/index.ts @@ -0,0 +1 @@ +export { withCollapsibleNode } from './with-collapsible-node'; diff --git a/packages/graphs/src/core/hoc/with-collapsible-node.tsx b/packages/graphs/src/core/hoc/with-collapsible-node.tsx new file mode 100644 index 000000000..a481c2d50 --- /dev/null +++ b/packages/graphs/src/core/hoc/with-collapsible-node.tsx @@ -0,0 +1,71 @@ +import { idOf } from '@antv/g6'; +import { get, isEmpty } from 'lodash-es'; +import React, { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import type { CollapsibleOptions } from '../../types'; +import type { NodeProps } from '../nodes/types'; + +const StyledWrapper = styled.div` + position: relative; + height: inherit; + width: inherit; + + .collapsible-icon { + position: absolute; + left: 50%; + top: calc(100% + 24px); + background-color: #fff; + border-radius: 2px; + color: #99add1; + font-size: 20px; + transform: translate(-50%, -50%); + z-index: 1; + + &:hover { + cursor: pointer; + } + } +`; + +interface CollapsibleNodeProps extends NodeProps, CollapsibleOptions {} + +export const withCollapsibleNode = (NodeComponent: React.FC) => { + return (props: CollapsibleNodeProps) => { + const { data, graph, trigger, iconRender, iconClassName, iconStyle } = props; + const [isCollapsed, setIsCollapsed] = useState(get(data, 'style.collapsed', false)); + + const isIconShown = trigger === 'icon' && !isEmpty(data.children); + + const nodeId = idOf(data); + const handleClickCollapse = async (e) => { + e.stopPropagation(); + + const toggleExpandCollapse = isCollapsed ? 'expandElement' : 'collapseElement'; + await graph[toggleExpandCollapse](nodeId); + setIsCollapsed((prev) => !prev); + + await graph.layout(); + }; + + useEffect(() => { + const target = + typeof trigger === 'string' ? document.getElementById(`${nodeId}-collapsible-${trigger}`) : trigger; + + target?.addEventListener('click', handleClickCollapse); + return () => { + target?.removeEventListener('click', handleClickCollapse); + }; + }, [trigger, isCollapsed, nodeId]); + + return ( + + {isIconShown && ( +
+ {iconRender?.(isCollapsed)} +
+ )} + {NodeComponent.call(graph, data)} +
+ ); + }; +}; diff --git a/packages/graphs/src/core/hooks/index.ts b/packages/graphs/src/core/hooks/index.ts new file mode 100644 index 000000000..52227a29a --- /dev/null +++ b/packages/graphs/src/core/hooks/index.ts @@ -0,0 +1 @@ +export { useOptions } from './useOptions'; diff --git a/packages/graphs/src/core/hooks/useOptions.tsx b/packages/graphs/src/core/hooks/useOptions.tsx new file mode 100644 index 000000000..395b8f46c --- /dev/null +++ b/packages/graphs/src/core/hooks/useOptions.tsx @@ -0,0 +1,67 @@ +import type { NodeData } from '@antv/g6'; +import { idOf } from '@antv/g6'; +import React, { useMemo } from 'react'; +import type { GraphOptions, GraphType } from '../../types'; +import { PlainNode } from '../nodes'; +import { inferCollapsibleStyle, isCollapsible, isReactNode, parseCollapsible, upsertChildrenData } from '../utils/node'; +import { mergeOptions } from '../utils/options'; + +const COMMON_OPTIONS: GraphOptions = { + collapsible: true, + node: { + type: 'react', + style: {}, + state: { + active: { + halo: false, + }, + selected: { + halo: false, + }, + }, + }, + transforms: ['infer-react-style'], +}; + +const hierarchicalGraphOptions: GraphOptions = { + node: { + style: { + component: (data: NodeData) => , + size: [80, 40], + ports: [{ placement: 'top' }, { placement: 'bottom' }], + }, + }, + edge: { + type: 'polyline', + style: { + router: { + type: 'orth', + }, + }, + }, + layout: { + type: 'antv-dagre', + rankdir: 'TB', + }, + animation: false, +}; + +const TEMPLATE_OPTIONS_MAP: Record = { + 'hierarchical-graph': hierarchicalGraphOptions, +}; + +export const useOptions = (type: GraphType, propOptions: GraphOptions) => { + return useMemo(() => { + const defaultOptions = mergeOptions(TEMPLATE_OPTIONS_MAP[type] || {}, COMMON_OPTIONS); + const options = mergeOptions(propOptions, defaultOptions); + + if (isCollapsible(options)) { + const { direction } = parseCollapsible(options.collapsible); + options.data = upsertChildrenData(options.data, direction!); + + if (isReactNode(options)) inferCollapsibleStyle(options); + } + + return options; + }, [type, propOptions]); +}; diff --git a/packages/graphs/src/core/nodes/index.ts b/packages/graphs/src/core/nodes/index.ts new file mode 100644 index 000000000..24b10f732 --- /dev/null +++ b/packages/graphs/src/core/nodes/index.ts @@ -0,0 +1 @@ +export { PlainNode } from './plain-node'; diff --git a/packages/graphs/src/core/nodes/plain-node.tsx b/packages/graphs/src/core/nodes/plain-node.tsx new file mode 100644 index 000000000..02c0608b6 --- /dev/null +++ b/packages/graphs/src/core/nodes/plain-node.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; + +const StyledWrapper = styled.div<{ $isActive?: boolean }>` + --default-color: #1783ff; + + position: relative; + height: inherit; + width: inherit; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--default-color); + color: #fff; + border-radius: 8px; + cursor: pointer; + border: 2px solid var(--default-color); + + ${(props) => + props.$isActive && + css` + border: 2px solid #000; + `} +`; + +export const PlainNode: React.FC<{ text: string; isActive?: boolean }> = ({ text, isActive }) => { + return {text}; +}; diff --git a/packages/graphs/src/core/nodes/types.ts b/packages/graphs/src/core/nodes/types.ts new file mode 100644 index 000000000..9dbc8cf6c --- /dev/null +++ b/packages/graphs/src/core/nodes/types.ts @@ -0,0 +1,12 @@ +import type { Graph, NodeData } from '@antv/g6'; + +export interface NodeProps { + /** + * Node data + */ + data: NodeData; + /** + * G6 Graph instance + */ + graph: Graph; +} diff --git a/packages/graphs/src/core/registry/build-in.ts b/packages/graphs/src/core/registry/build-in.ts new file mode 100644 index 000000000..fe4deff02 --- /dev/null +++ b/packages/graphs/src/core/registry/build-in.ts @@ -0,0 +1,11 @@ +import { ReactNode } from '@antv/g6-extension-react'; +import { InferReactStyle } from '../transform'; + +export const BUILT_IN_EXTENSIONS = { + node: { + react: ReactNode, + }, + transform: { + 'infer-react-style': InferReactStyle, + }, +}; diff --git a/packages/graphs/src/core/registry/register.ts b/packages/graphs/src/core/registry/register.ts new file mode 100644 index 000000000..29636706c --- /dev/null +++ b/packages/graphs/src/core/registry/register.ts @@ -0,0 +1,11 @@ +import type { ExtensionCategory } from '@antv/g6'; +import { register } from '@antv/g6'; +import { BUILT_IN_EXTENSIONS } from './build-in'; + +export function registerBuiltInExtensions() { + Object.entries(BUILT_IN_EXTENSIONS).forEach(([category, extensions]) => { + Object.entries(extensions).forEach(([type, extension]) => { + register(category as ExtensionCategory, type, extension); + }); + }); +} diff --git a/packages/graphs/src/core/transform/index.ts b/packages/graphs/src/core/transform/index.ts new file mode 100644 index 000000000..0383a631a --- /dev/null +++ b/packages/graphs/src/core/transform/index.ts @@ -0,0 +1 @@ +export { InferReactStyle } from './infer-react-style'; diff --git a/packages/graphs/src/core/transform/infer-react-style.ts b/packages/graphs/src/core/transform/infer-react-style.ts new file mode 100644 index 000000000..ff0da5993 --- /dev/null +++ b/packages/graphs/src/core/transform/infer-react-style.ts @@ -0,0 +1,30 @@ +import { BaseTransform, idOf } from '@antv/g6'; + +export class InferReactStyle extends BaseTransform { + public afterLayout() { + const { graph, model, element } = this.context; + + graph.getNodeData().forEach((datum) => { + const nodeId = idOf(datum); + + const node = element!.getElement(nodeId); + if (!node) return; + + const style = graph.getElementRenderStyle(nodeId); + const { size } = style; + + model.updateNodeData([ + { + id: nodeId, + // HTML 元素的默认原点位置在左上角,而 G6 的默认原点位置在中心,所以需要调整 dx 和 dy + style: { + dx: -size[0] / 2, + dy: -size[1] / 2, + }, + }, + ]); + }); + + graph.draw(); + } +} diff --git a/packages/graphs/src/core/utils/node.tsx b/packages/graphs/src/core/utils/node.tsx new file mode 100644 index 000000000..b3d11a5f0 --- /dev/null +++ b/packages/graphs/src/core/utils/node.tsx @@ -0,0 +1,91 @@ +import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import type { EdgeData, EdgeDirection, GraphData, ID, NodeData } from '@antv/g6'; +import { ReactNodeStyleProps } from '@antv/g6-extension-react'; +import { get, has, isObject, set } from 'lodash-es'; +import React from 'react'; +import type { CollapsibleOptions, GraphOptions } from '../../types'; +import { withCollapsibleNode } from '../hoc'; + +/** + * 判断是否为 React 组件节点 + * @param options - 图配置项 + * @returns 是否为 React 组件节点 + */ +export function isReactNode(options: GraphOptions) { + return has(options, 'node.style.component'); +} + +/** + * 判断是否可展开/收起 + * @param options - 图配置项 + * @returns 是否可展开/收起 + */ +export function isCollapsible(options: GraphOptions) { + return Boolean(get(options, 'collapsible', false)); +} + +/** + * 解析展开/收起配置项 + * @param collapsible - 用户配置的展开/收起配置项 + * @returns 展开/收起配置项 + */ +export function parseCollapsible(collapsible: GraphOptions['collapsible']): CollapsibleOptions { + const defaultOptions: CollapsibleOptions = { + trigger: 'icon', + direction: 'out', + iconRender: (isCollapsed: boolean) => (isCollapsed ? : ), + }; + + return collapsible === true ? defaultOptions : isObject(collapsible) ? { ...defaultOptions, ...collapsible } : {}; +} + +/** + * 推断可折叠节点的样式 + * @param options - 图配置项 + */ +export function inferCollapsibleStyle(options: GraphOptions) { + const config = parseCollapsible(options.collapsible); + const { component } = options.node!.style as unknown as ReactNodeStyleProps; + + set(options, 'node.style.component', function (data: NodeData) { + const CollapsibleNode = withCollapsibleNode(component); + // @ts-expect-error this is G6 graph instance + return ; + }); +} + +/** + * 获取邻居节点 + * @param nodeId - 节点 ID + * @param edges - 边数据 + * @param direction - 边的方向 + * @returns 邻居节点 ID + */ +export const getNeighborNodeIds = (nodeId: ID, edges: EdgeData[], direction: EdgeDirection): ID[] => { + const getSuccessorNodeIds = (reverse = false): ID[] => { + const [source, target] = reverse ? ['target', 'source'] : ['source', 'target']; + return edges.filter((edge) => edge[source] === nodeId).map((edge) => edge[target]) as ID[]; + }; + + if (direction === 'out') return getSuccessorNodeIds(); + if (direction === 'in') return getSuccessorNodeIds(true); + return getSuccessorNodeIds().concat(getSuccessorNodeIds(true)); +}; + +/** + * 插入或更新子节点数据,children 字段在展开/折叠节点时被消费 + * @param data - 图数据 + * @param direction - 边的方向 + * @returns 更新后的图数据 + */ +export function upsertChildrenData(data: GraphData, direction: EdgeDirection): GraphData { + const { nodes = [], edges = [] } = data || {}; + + return { + ...data, + nodes: nodes.map((node) => ({ + ...node, + children: getNeighborNodeIds(node.id, edges, direction), + })), + }; +} diff --git a/packages/graphs/src/core/utils/options.ts b/packages/graphs/src/core/utils/options.ts new file mode 100644 index 000000000..5869becaa --- /dev/null +++ b/packages/graphs/src/core/utils/options.ts @@ -0,0 +1,38 @@ +import { isPlainObject } from 'lodash-es'; +import type { GraphOptions, ParsedGraphOptions } from '../../types'; + +/** + * 将用户配置项与默认配置项合并 + * @param options - 用户配置项 + * @param defaultOptions - 默认配置项 + * @returns 最后用于渲染的配置项 + */ +export function mergeOptions(options: GraphOptions, defaultOptions: GraphOptions): ParsedGraphOptions { + const merged = { ...defaultOptions }; + + for (const key in options) { + if (options.hasOwnProperty(key)) { + const propValue = options[key]; + const defaultValue = defaultOptions[key]; + + if (['component', 'data'].includes(key)) { + merged[key] = propValue; + } else if (typeof propValue === 'function') { + merged[key] = function (datum) { + return mergeOptions(propValue.call(this, datum), defaultValue); + }; + } else if ( + isPlainObject(propValue) && + isPlainObject(defaultValue) && + propValue !== null && + defaultValue !== null + ) { + merged[key] = mergeOptions(propValue, defaultValue); + } else { + merged[key] = propValue; + } + } + } + + return merged as ParsedGraphOptions; +} diff --git a/packages/graphs/src/index.ts b/packages/graphs/src/index.ts index 484a28982..9398b0aea 100644 --- a/packages/graphs/src/index.ts +++ b/packages/graphs/src/index.ts @@ -1,4 +1,7 @@ import * as G6 from '@antv/g6'; +import './preset'; -export * from './components'; +export { HierarchicalGraph } from './components'; +export { PlainNode } from './core/nodes'; +export type * from './types'; export { G6 }; diff --git a/packages/graphs/src/preset.ts b/packages/graphs/src/preset.ts new file mode 100644 index 000000000..b65997092 --- /dev/null +++ b/packages/graphs/src/preset.ts @@ -0,0 +1,3 @@ +import { registerBuiltInExtensions } from './core/registry/register'; + +registerBuiltInExtensions(); diff --git a/packages/graphs/src/types.ts b/packages/graphs/src/types.ts new file mode 100644 index 000000000..9bfe1c267 --- /dev/null +++ b/packages/graphs/src/types.ts @@ -0,0 +1,47 @@ +import type { ContainerConfig } from '@ant-design/charts-util'; +import type { GraphOptions as G6GraphOptions } from '@antv/g6'; + +export type GraphType = 'hierarchical-graph'; + +export interface GraphOptions extends ContainerConfig, G6GraphOptions { + /** + * 是否支持节点收起/展开 + * @default true + */ + collapsible?: boolean | CollapsibleOptions; +} + +export type ParsedGraphOptions = Required; + +export interface CollapsibleOptions { + /** + * 点击指定元素,触发节点收起/展开 + * - 'icon': 点击内置图标 + * - 'node': 点击节点 + * - HTMLElement: 自定义元素 + * @default 'icon' + */ + trigger?: 'icon' | 'node' | HTMLElement; + /** + * 收起/展开指定方向上的邻居节点 + * - 'in': 前驱节点 + * - 'out': 后继节点 + * - 'both': 前驱和后继节点 + * @default 'out' + */ + direction?: 'in' | 'out' | 'both'; + /** + * 渲染函数,用于自定义收起/展开图标 + * @param isCollapsed - 当前节点是否已收起 + * @returns 自定义图标 + */ + iconRender?: (isCollapsed: boolean) => React.ReactNode; + /** + * 指定图标的 CSS 类名 + */ + iconClassName?: string; + /** + * 指定图标的内联样式 + */ + iconStyle?: React.CSSProperties; +} diff --git a/packages/graphs/tests/demos/error-boundary.tsx b/packages/graphs/tests/demos/error-boundary.tsx deleted file mode 100644 index 0376b99d1..000000000 --- a/packages/graphs/tests/demos/error-boundary.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const ErrorBoundary = () => { - return
ErrorBoundary
; -}; diff --git a/packages/graphs/tests/demos/hierarchical-graph.tsx b/packages/graphs/tests/demos/hierarchical-graph.tsx new file mode 100644 index 000000000..850e8c268 --- /dev/null +++ b/packages/graphs/tests/demos/hierarchical-graph.tsx @@ -0,0 +1,27 @@ +import { GraphOptions, HierarchicalGraph as HierarchicalGraphComponent } from '@ant-design/graphs'; +import { Graph } from '@antv/g6'; +import React, { useEffect, useState } from 'react'; + +export const HierarchicalGraph = () => { + const [graph, setGraph] = useState(null); + const [data, setData] = useState(undefined); + + useEffect(() => { + fetch('https://assets.antv.antgroup.com/g6/organization-chart.json') + .then((res) => res.json()) + .then(setData); + }, []); + + const options: GraphOptions = { + autoFit: 'view', + data, + }; + + useEffect(() => { + if (graph) { + console.log(graph); + } + }, [graph]); + + return ; +}; diff --git a/packages/graphs/tests/demos/index.tsx b/packages/graphs/tests/demos/index.tsx index 538d0b512..8db89289c 100644 --- a/packages/graphs/tests/demos/index.tsx +++ b/packages/graphs/tests/demos/index.tsx @@ -1 +1,2 @@ +export { HierarchicalGraph } from './hierarchical-graph'; export { ErrorBoundary } from './error-boundary'; diff --git a/packages/graphs/tests/main.tsx b/packages/graphs/tests/main.tsx index a81980fe9..f28406ba5 100644 --- a/packages/graphs/tests/main.tsx +++ b/packages/graphs/tests/main.tsx @@ -1,7 +1,7 @@ import { Alert, Flex, Select } from 'antd'; import React from 'react'; import { createRoot } from 'react-dom/client'; -import { Outlet, RouterProvider, createBrowserRouter, useMatch, useNavigate } from 'react-router-dom'; +import { createBrowserRouter, Outlet, RouterProvider, useMatch, useNavigate } from 'react-router-dom'; import * as demos from './demos'; const App = () => { diff --git a/packages/graphs/tsconfig.json b/packages/graphs/tsconfig.json index 24fbbfd32..ae7aad3bb 100644 --- a/packages/graphs/tsconfig.json +++ b/packages/graphs/tsconfig.json @@ -4,7 +4,11 @@ "pretty": true, "resolveJsonModule": true, "strict": true, - "paths": {} + "baseUrl": ".", + "paths": { + "@ant-design/graphs": ["./src/index.ts"], + "@ant-design/charts-util": ["../util/src/index.ts"] + } }, "exclude": ["node_modules", "dist", "lib", "es"], "extends": "../../tsconfig.json", diff --git a/packages/graphs/vite.config.ts b/packages/graphs/vite.config.ts index ef0ef287e..c53edf895 100644 --- a/packages/graphs/vite.config.ts +++ b/packages/graphs/vite.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ resolve: { alias: { '@ant-design/graphs': path.resolve(__dirname, './src'), + '@ant-design/graphs-util': path.resolve(__dirname, '../../util/lib/index.js'), }, }, }); diff --git a/packages/plots/src/interface.ts b/packages/plots/src/interface.ts index 58c955fde..449c39f6e 100644 --- a/packages/plots/src/interface.ts +++ b/packages/plots/src/interface.ts @@ -1,4 +1,5 @@ -import type { TooltipComponent, Data as G2Data } from '@antv/g2'; +import type { ContainerConfig } from '@ant-design/charts-util'; +import type { Data as G2Data, TooltipComponent } from '@antv/g2'; import { Options, Spec } from './core'; /** @@ -14,53 +15,6 @@ export interface Chart extends Plot { downloadImage?: (name?: string, type?: string, encoderOptions?: number) => string; } -export interface ContainerConfig { - /** - * @title 图表样式 - * @description 配置容器样式 - * @title.en_US Chart containerStyle - * @description.en_US Configure chart container styles - */ - containerStyle?: React.CSSProperties; - /** - * @title 容器自定义属性 - * @description 配置容器自定义属性 - * @title.en_US Chart containerAttr - * @description.en_US Configure chart container attributes - */ - containerAttributes?: Record; - /** - * @title 容器class - * @description 类名添加 - * @title.en_US Container class name - * @description.en_US Class name addition - */ - className?: string; - /** - * @title 加载状态 - * @description 是否加载中 - * @default false - * @title.en_US Loading status - * @description.en_US Is it loading - * @default.en_US false - */ - loading?: boolean; - /** - * @title 加载模板 - * @description 加载模板 - * @title.en_US Load template - * @description.en_US Load template - */ - loadingTemplate?: React.ReactElement; - /** - * @title 出错模板 - * @description 出错时占位模板 - * @title.en_US error template - * @description.en_US Error placeholder template - */ - errorTemplate?: (e: Error) => React.ReactNode; -} - export interface AttachConfig { /** * @title 浮窗提示 diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 987766a78..ea75206a3 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -1,3 +1,4 @@ -export * from './react'; export * from './rc'; +export * from './react'; +export type { ContainerConfig } from './types'; export { uuid } from './uuid'; diff --git a/packages/util/src/types.ts b/packages/util/src/types.ts new file mode 100644 index 000000000..8ee10bf19 --- /dev/null +++ b/packages/util/src/types.ts @@ -0,0 +1,46 @@ +export interface ContainerConfig { + /** + * @title 图表样式 + * @description 配置容器样式 + * @title.en_US Chart containerStyle + * @description.en_US Configure chart container styles + */ + containerStyle?: React.CSSProperties; + /** + * @title 容器自定义属性 + * @description 配置容器自定义属性 + * @title.en_US Chart containerAttr + * @description.en_US Configure chart container attributes + */ + containerAttributes?: Record; + /** + * @title 容器class + * @description 类名添加 + * @title.en_US Container class name + * @description.en_US Class name addition + */ + className?: string; + /** + * @title 加载状态 + * @description 是否加载中 + * @default false + * @title.en_US Loading status + * @description.en_US Is it loading + * @default.en_US false + */ + loading?: boolean; + /** + * @title 加载模板 + * @description 加载模板 + * @title.en_US Load template + * @description.en_US Load template + */ + loadingTemplate?: React.ReactElement; + /** + * @title 出错模板 + * @description 出错时占位模板 + * @title.en_US error template + * @description.en_US Error placeholder template + */ + errorTemplate?: (e: Error) => React.ReactNode; +} From 40c4112b2ab53056c2f99d0cd9602bd1a2c99091 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 11 Sep 2024 11:08:03 +0800 Subject: [PATCH 2/6] chore: add prettier plugins --- .prettierrc.js | 1 + package.json | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.prettierrc.js b/.prettierrc.js index f30f82663..305cdad3a 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -3,4 +3,5 @@ const fabric = require('@umijs/fabric'); module.exports = { ...fabric.prettier, printWidth: 120, + plugins: [require.resolve('prettier-plugin-organize-imports'), require.resolve('prettier-plugin-packagejson')], }; diff --git a/package.json b/package.json index bbe5cc15e..1b83e6b73 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "np": "*", "npm-run-all": "^4.1.5", "prettier": "^2.8.7", + "prettier-plugin-organize-imports": "^4.0.0", + "prettier-plugin-packagejson": "^2.5.2", "pretty-quick": "^3.0.1", "react-dev-utils": "^12.0.1", "style-loader": "^3.3.0", From 500d2142349af35a2851528c03c27d65ad290268 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 11 Sep 2024 11:09:00 +0800 Subject: [PATCH 3/6] feat: add organization chart demo based on hierarchical graph --- .../src/core/behaviors/hover-element.ts | 21 +++++ packages/graphs/src/core/behaviors/index.ts | 1 + packages/graphs/src/core/nodes/index.ts | 1 + .../core/nodes/organization-chart-node.tsx | 88 +++++++++++++++++++ packages/graphs/src/index.ts | 2 +- packages/graphs/tests/demos/index.tsx | 2 +- .../graphs/tests/demos/organization-chart.tsx | 51 +++++++++++ 7 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 packages/graphs/src/core/behaviors/hover-element.ts create mode 100644 packages/graphs/src/core/behaviors/index.ts create mode 100644 packages/graphs/src/core/nodes/organization-chart-node.tsx create mode 100644 packages/graphs/tests/demos/organization-chart.tsx diff --git a/packages/graphs/src/core/behaviors/hover-element.ts b/packages/graphs/src/core/behaviors/hover-element.ts new file mode 100644 index 000000000..41cd5bb8a --- /dev/null +++ b/packages/graphs/src/core/behaviors/hover-element.ts @@ -0,0 +1,21 @@ +import { HoverActivate, idOf } from '@antv/g6'; + +export class HoverElement extends HoverActivate { + getActiveIds(event) { + const { model, graph } = this.context; + const { targetType, target } = event; + const targetId = target.id; + + const ids = [targetId]; + if (targetType === 'edge') { + const edge = model.getEdgeDatum(targetId); + ids.push(edge.source, edge.target); + } else if (targetType === 'node') { + ids.push(...model.getRelatedEdgesData(targetId).map(idOf)); + } + + graph.frontElement(ids); + + return ids; + } +} diff --git a/packages/graphs/src/core/behaviors/index.ts b/packages/graphs/src/core/behaviors/index.ts new file mode 100644 index 000000000..e4aa98e60 --- /dev/null +++ b/packages/graphs/src/core/behaviors/index.ts @@ -0,0 +1 @@ +export { HoverElement } from './hover-element'; diff --git a/packages/graphs/src/core/nodes/index.ts b/packages/graphs/src/core/nodes/index.ts index 24b10f732..9ad1a4e2b 100644 --- a/packages/graphs/src/core/nodes/index.ts +++ b/packages/graphs/src/core/nodes/index.ts @@ -1 +1,2 @@ +export { OrganizationChartNode } from './organization-chart-node'; export { PlainNode } from './plain-node'; diff --git a/packages/graphs/src/core/nodes/organization-chart-node.tsx b/packages/graphs/src/core/nodes/organization-chart-node.tsx new file mode 100644 index 000000000..04d5a2165 --- /dev/null +++ b/packages/graphs/src/core/nodes/organization-chart-node.tsx @@ -0,0 +1,88 @@ +import { UserOutlined } from '@ant-design/icons'; +import { Avatar, Flex } from 'antd'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +interface OrganizationChartNodeProps { + name: string; + position: string; + status: string; + isActive?: boolean; +} + +const StyledWrapper = styled.div<{ color?: string; isActive?: boolean }>` + height: inherit; + width: inherit; + border-radius: 4px; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.1); + position: relative; + border: 3px solid transparent; + background-color: #fff; + + ${(props) => + props.isActive && + css` + border: 3px solid #1783ff; + `} + + .line { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 6px; + background-color: ${(props) => props.color}; + border-radius: 2px 2px 0 0; + } + + .node-content { + height: inherit; + margin: 4px 16px 8px; + + &-avatar { + width: 40px; + height: 40px; + margin-right: 16px; + background-color: ${(props) => props.color}; + } + + &-detail { + flex: 1; + } + + &-name { + color: #242424; + font-weight: 600; + font-size: 18px; + margin-bottom: 5px; + } + + &-post { + color: #616161; + font-size: 14px; + } + } +`; + +export const OrganizationChartNode: React.FC = (props) => { + const { name, position, status, isActive } = props; + + const colorMap = { + online: '#1783FF', + busy: '#00C9C9', + offline: '#F08F56', + }; + + return ( + +
+ + } /> + +
{name}
+
{position}
+
+
+
+ ); +}; diff --git a/packages/graphs/src/index.ts b/packages/graphs/src/index.ts index 9398b0aea..71d4a19fb 100644 --- a/packages/graphs/src/index.ts +++ b/packages/graphs/src/index.ts @@ -2,6 +2,6 @@ import * as G6 from '@antv/g6'; import './preset'; export { HierarchicalGraph } from './components'; -export { PlainNode } from './core/nodes'; +export { OrganizationChartNode, PlainNode } from './core/nodes'; export type * from './types'; export { G6 }; diff --git a/packages/graphs/tests/demos/index.tsx b/packages/graphs/tests/demos/index.tsx index 8db89289c..096b3e706 100644 --- a/packages/graphs/tests/demos/index.tsx +++ b/packages/graphs/tests/demos/index.tsx @@ -1,2 +1,2 @@ export { HierarchicalGraph } from './hierarchical-graph'; -export { ErrorBoundary } from './error-boundary'; +export { OrganizationChart } from './organization-chart'; diff --git a/packages/graphs/tests/demos/organization-chart.tsx b/packages/graphs/tests/demos/organization-chart.tsx new file mode 100644 index 000000000..960c9ebe1 --- /dev/null +++ b/packages/graphs/tests/demos/organization-chart.tsx @@ -0,0 +1,51 @@ +import { + GraphOptions, + HierarchicalGraph as HierarchicalGraphComponent, + OrganizationChartNode, +} from '@ant-design/graphs'; +import React, { useEffect, useState } from 'react'; + +export const OrganizationChart = () => { + const [data, setData] = useState(undefined); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setLoading(true); + fetch('https://assets.antv.antgroup.com/g6/organization-chart.json') + .then((res) => res.json()) + .then((data) => { + setData(data); + setLoading(false); + }); + }, []); + + const options: GraphOptions = { + data, + padding: 20, + autoFit: 'view', + node: { + style: { + component: (d) => { + const { name, position, status } = d.data || {}; + const isActive = d.states?.includes('active'); + return ; + }, + size: [240, 80], + }, + }, + edge: { + style: { + radius: 16, + lineWidth: 2, + endArrow: true, + }, + }, + layout: { + type: 'antv-dagre', + nodesep: 24, + ranksep: -20, + }, + }; + + return ; +}; From 3d0e9b7205c4af38a3ea585e7d66cf2203dfc0d8 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 11 Sep 2024 11:10:35 +0800 Subject: [PATCH 4/6] fix: remove react strict mode --- packages/graphs/tests/main.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/graphs/tests/main.tsx b/packages/graphs/tests/main.tsx index f28406ba5..8b1e0433a 100644 --- a/packages/graphs/tests/main.tsx +++ b/packages/graphs/tests/main.tsx @@ -40,8 +40,4 @@ const router = createBrowserRouter([ const container = document.getElementById('root')!; const root = createRoot(container); -root.render( - - - , -); +root.render(); From 69346d028c56a51695b60bbb13ae9714c1c681df Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 11 Sep 2024 15:26:00 +0800 Subject: [PATCH 5/6] fix: fix cr issues --- packages/graphs/package.json | 2 +- packages/graphs/src/core/base-graph.tsx | 18 +++++++++++++-- .../src/core/behaviors/hover-element.ts | 4 ++-- .../src/core/hoc/with-collapsible-node.tsx | 18 +++++++-------- .../core/nodes/organization-chart-node.tsx | 18 ++++++++++++--- packages/graphs/src/core/nodes/plain-node.tsx | 22 +++++++++++++++++-- packages/graphs/src/core/utils/node.tsx | 4 ++-- packages/graphs/src/core/utils/options.ts | 2 +- packages/graphs/src/index.ts | 2 +- packages/graphs/src/types.ts | 14 +++++++++++- 10 files changed, 80 insertions(+), 24 deletions(-) diff --git a/packages/graphs/package.json b/packages/graphs/package.json index 642e8d152..3cf95e761 100644 --- a/packages/graphs/package.json +++ b/packages/graphs/package.json @@ -52,7 +52,7 @@ "@antv/g6": "^5.0.21", "@antv/g6-extension-react": "^0.1.6", "@antv/graphin": "^3.0.2", - "lodash-es": "^4.17.21", + "lodash": "^4.17.21", "styled-components": "^6.1.13" }, "devDependencies": { diff --git a/packages/graphs/src/core/base-graph.tsx b/packages/graphs/src/core/base-graph.tsx index 940858f85..d50bee9fc 100644 --- a/packages/graphs/src/core/base-graph.tsx +++ b/packages/graphs/src/core/base-graph.tsx @@ -23,7 +23,18 @@ interface BaseGraphProps extends GraphOptions { export const BaseGraph: ForwardRefExoticComponent< PropsWithoutRef> & RefAttributes > = forwardRef>(({ children, ...props }, ref) => { - const { type, containerStyle, className, errorTemplate, loading, loadingTemplate, ...propOptions } = props; + const { + type, + containerStyle, + className, + onInit, + onReady, + onDestroy, + errorTemplate, + loading, + loadingTemplate, + ...propOptions + } = props; const graphRef = useRef(null); const options = useOptions(type, propOptions); @@ -34,12 +45,15 @@ export const BaseGraph: ForwardRefExoticComponent< {loading && } { + ref={(ref) => { graphRef.current = ref; }} className={className} style={containerStyle} options={options} + onInit={onInit} + onReady={onReady} + onDestroy={onDestroy} > {children} diff --git a/packages/graphs/src/core/behaviors/hover-element.ts b/packages/graphs/src/core/behaviors/hover-element.ts index 41cd5bb8a..37ac626f0 100644 --- a/packages/graphs/src/core/behaviors/hover-element.ts +++ b/packages/graphs/src/core/behaviors/hover-element.ts @@ -3,8 +3,8 @@ import { HoverActivate, idOf } from '@antv/g6'; export class HoverElement extends HoverActivate { getActiveIds(event) { const { model, graph } = this.context; - const { targetType, target } = event; - const targetId = target.id; + const targetId = event.target.id; + const targetType = graph.getElementType(targetId); const ids = [targetId]; if (targetType === 'edge') { diff --git a/packages/graphs/src/core/hoc/with-collapsible-node.tsx b/packages/graphs/src/core/hoc/with-collapsible-node.tsx index a481c2d50..e93da34bb 100644 --- a/packages/graphs/src/core/hoc/with-collapsible-node.tsx +++ b/packages/graphs/src/core/hoc/with-collapsible-node.tsx @@ -1,6 +1,6 @@ import { idOf } from '@antv/g6'; -import { get, isEmpty } from 'lodash-es'; -import React, { useEffect, useState } from 'react'; +import { get, isEmpty } from 'lodash'; +import React, { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import type { CollapsibleOptions } from '../../types'; import type { NodeProps } from '../nodes/types'; @@ -33,34 +33,34 @@ export const withCollapsibleNode = (NodeComponent: React.FC) => { return (props: CollapsibleNodeProps) => { const { data, graph, trigger, iconRender, iconClassName, iconStyle } = props; const [isCollapsed, setIsCollapsed] = useState(get(data, 'style.collapsed', false)); + const wrapperRef = useRef(null); + const iconRef = useRef(null); const isIconShown = trigger === 'icon' && !isEmpty(data.children); - const nodeId = idOf(data); const handleClickCollapse = async (e) => { e.stopPropagation(); const toggleExpandCollapse = isCollapsed ? 'expandElement' : 'collapseElement'; - await graph[toggleExpandCollapse](nodeId); + await graph[toggleExpandCollapse](idOf(data)); setIsCollapsed((prev) => !prev); await graph.layout(); }; useEffect(() => { - const target = - typeof trigger === 'string' ? document.getElementById(`${nodeId}-collapsible-${trigger}`) : trigger; + const target = trigger === 'icon' ? iconRef.current : trigger === 'node' ? wrapperRef.current : trigger; target?.addEventListener('click', handleClickCollapse); return () => { target?.removeEventListener('click', handleClickCollapse); }; - }, [trigger, isCollapsed, nodeId]); + }, [trigger, isCollapsed]); return ( - + {isIconShown && ( -
+
{iconRender?.(isCollapsed)}
)} diff --git a/packages/graphs/src/core/nodes/organization-chart-node.tsx b/packages/graphs/src/core/nodes/organization-chart-node.tsx index 04d5a2165..721d3ebcd 100644 --- a/packages/graphs/src/core/nodes/organization-chart-node.tsx +++ b/packages/graphs/src/core/nodes/organization-chart-node.tsx @@ -3,10 +3,22 @@ import { Avatar, Flex } from 'antd'; import React from 'react'; import styled, { css } from 'styled-components'; -interface OrganizationChartNodeProps { +interface OrganizationChartNodeProps extends Pick, 'className' | 'style'> { + /** + * Name of the person + */ name: string; + /** + * Position of the person + */ position: string; + /** + * Working status of the person + */ status: string; + /** + * Whether the node is hovered + */ isActive?: boolean; } @@ -65,7 +77,7 @@ const StyledWrapper = styled.div<{ color?: string; isActive?: boolean }>` `; export const OrganizationChartNode: React.FC = (props) => { - const { name, position, status, isActive } = props; + const { name, position, status, isActive, className, style } = props; const colorMap = { online: '#1783FF', @@ -74,7 +86,7 @@ export const OrganizationChartNode: React.FC = (prop }; return ( - +
} /> diff --git a/packages/graphs/src/core/nodes/plain-node.tsx b/packages/graphs/src/core/nodes/plain-node.tsx index 02c0608b6..dea61acde 100644 --- a/packages/graphs/src/core/nodes/plain-node.tsx +++ b/packages/graphs/src/core/nodes/plain-node.tsx @@ -23,6 +23,24 @@ const StyledWrapper = styled.div<{ $isActive?: boolean }>` `} `; -export const PlainNode: React.FC<{ text: string; isActive?: boolean }> = ({ text, isActive }) => { - return {text}; +interface PlainNodeProps extends Pick, 'className' | 'style'> { + /** + * Text to display + */ + text: string; + /** + * Whether the node is active + * @default false + */ + isActive?: boolean; +} + +export const PlainNode: React.FC = (props) => { + const { text, isActive, className, style } = props; + + return ( + + {text} + + ); }; diff --git a/packages/graphs/src/core/utils/node.tsx b/packages/graphs/src/core/utils/node.tsx index b3d11a5f0..11f5e0612 100644 --- a/packages/graphs/src/core/utils/node.tsx +++ b/packages/graphs/src/core/utils/node.tsx @@ -1,7 +1,7 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import type { EdgeData, EdgeDirection, GraphData, ID, NodeData } from '@antv/g6'; import { ReactNodeStyleProps } from '@antv/g6-extension-react'; -import { get, has, isObject, set } from 'lodash-es'; +import { get, has, isObject, set } from 'lodash'; import React from 'react'; import type { CollapsibleOptions, GraphOptions } from '../../types'; import { withCollapsibleNode } from '../hoc'; @@ -49,7 +49,7 @@ export function inferCollapsibleStyle(options: GraphOptions) { set(options, 'node.style.component', function (data: NodeData) { const CollapsibleNode = withCollapsibleNode(component); - // @ts-expect-error this is G6 graph instance + // @ts-ignore this 指向 G6 Graph 实例 return ; }); } diff --git a/packages/graphs/src/core/utils/options.ts b/packages/graphs/src/core/utils/options.ts index 5869becaa..0e90b6cc5 100644 --- a/packages/graphs/src/core/utils/options.ts +++ b/packages/graphs/src/core/utils/options.ts @@ -1,4 +1,4 @@ -import { isPlainObject } from 'lodash-es'; +import { isPlainObject } from 'lodash'; import type { GraphOptions, ParsedGraphOptions } from '../../types'; /** diff --git a/packages/graphs/src/index.ts b/packages/graphs/src/index.ts index 71d4a19fb..914c70800 100644 --- a/packages/graphs/src/index.ts +++ b/packages/graphs/src/index.ts @@ -3,5 +3,5 @@ import './preset'; export { HierarchicalGraph } from './components'; export { OrganizationChartNode, PlainNode } from './core/nodes'; -export type * from './types'; +export type { GraphOptions } from './types'; export { G6 }; diff --git a/packages/graphs/src/types.ts b/packages/graphs/src/types.ts index 9bfe1c267..fddd10f9b 100644 --- a/packages/graphs/src/types.ts +++ b/packages/graphs/src/types.ts @@ -1,5 +1,5 @@ import type { ContainerConfig } from '@ant-design/charts-util'; -import type { GraphOptions as G6GraphOptions } from '@antv/g6'; +import type { Graph, GraphOptions as G6GraphOptions } from '@antv/g6'; export type GraphType = 'hierarchical-graph'; @@ -9,6 +9,18 @@ export interface GraphOptions extends ContainerConfig, G6GraphOptions { * @default true */ collapsible?: boolean | CollapsibleOptions; + /** + * 当图初始化完成后(即 new Graph() 之后)执行回调 + */ + onInit?: (graph: Graph) => void; + /** + * 当图渲染完成后(即 graph.render() 之后)执行回调 + */ + onReady?: (graph: Graph) => void; + /** + * 当图销毁后(即 graph.destroy() 之后)执行回调 + */ + onDestroy?: () => void; } export type ParsedGraphOptions = Required; From eb63e908c9dbf064c383c8144b2131381a941972 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 11 Sep 2024 17:40:19 +0800 Subject: [PATCH 6/6] fix: remove @ant-design/icons --- packages/graphs/package.json | 1 - .../src/core/hoc/with-collapsible-node.tsx | 6 +----- packages/graphs/src/core/hooks/useOptions.tsx | 1 - .../core/nodes/organization-chart-node.tsx | 5 +++-- packages/graphs/src/core/utils/node.tsx | 19 +++++++++++++++++-- .../graphs/tests/demos/organization-chart.tsx | 1 + 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/graphs/package.json b/packages/graphs/package.json index 3cf95e761..87a0f8f71 100644 --- a/packages/graphs/package.json +++ b/packages/graphs/package.json @@ -48,7 +48,6 @@ }, "dependencies": { "@ant-design/charts-util": "workspace:*", - "@ant-design/icons": "^5.4.0", "@antv/g6": "^5.0.21", "@antv/g6-extension-react": "^0.1.6", "@antv/graphin": "^3.0.2", diff --git a/packages/graphs/src/core/hoc/with-collapsible-node.tsx b/packages/graphs/src/core/hoc/with-collapsible-node.tsx index e93da34bb..ea6768737 100644 --- a/packages/graphs/src/core/hoc/with-collapsible-node.tsx +++ b/packages/graphs/src/core/hoc/with-collapsible-node.tsx @@ -14,10 +14,6 @@ const StyledWrapper = styled.div` position: absolute; left: 50%; top: calc(100% + 24px); - background-color: #fff; - border-radius: 2px; - color: #99add1; - font-size: 20px; transform: translate(-50%, -50%); z-index: 1; @@ -31,7 +27,7 @@ interface CollapsibleNodeProps extends NodeProps, CollapsibleOptions {} export const withCollapsibleNode = (NodeComponent: React.FC) => { return (props: CollapsibleNodeProps) => { - const { data, graph, trigger, iconRender, iconClassName, iconStyle } = props; + const { data, graph, trigger, iconRender, iconClassName = '', iconStyle } = props; const [isCollapsed, setIsCollapsed] = useState(get(data, 'style.collapsed', false)); const wrapperRef = useRef(null); const iconRef = useRef(null); diff --git a/packages/graphs/src/core/hooks/useOptions.tsx b/packages/graphs/src/core/hooks/useOptions.tsx index 395b8f46c..9875b73dc 100644 --- a/packages/graphs/src/core/hooks/useOptions.tsx +++ b/packages/graphs/src/core/hooks/useOptions.tsx @@ -7,7 +7,6 @@ import { inferCollapsibleStyle, isCollapsible, isReactNode, parseCollapsible, up import { mergeOptions } from '../utils/options'; const COMMON_OPTIONS: GraphOptions = { - collapsible: true, node: { type: 'react', style: {}, diff --git a/packages/graphs/src/core/nodes/organization-chart-node.tsx b/packages/graphs/src/core/nodes/organization-chart-node.tsx index 721d3ebcd..47b60fe68 100644 --- a/packages/graphs/src/core/nodes/organization-chart-node.tsx +++ b/packages/graphs/src/core/nodes/organization-chart-node.tsx @@ -1,4 +1,3 @@ -import { UserOutlined } from '@ant-design/icons'; import { Avatar, Flex } from 'antd'; import React from 'react'; import styled, { css } from 'styled-components'; @@ -56,6 +55,8 @@ const StyledWrapper = styled.div<{ color?: string; isActive?: boolean }>` height: 40px; margin-right: 16px; background-color: ${(props) => props.color}; + font-weight: 600; + font-size: 18px; } &-detail { @@ -89,7 +90,7 @@ export const OrganizationChartNode: React.FC = (prop
- } /> + {name.slice(0, 1)}
{name}
{position}
diff --git a/packages/graphs/src/core/utils/node.tsx b/packages/graphs/src/core/utils/node.tsx index 11f5e0612..bbfea0fbf 100644 --- a/packages/graphs/src/core/utils/node.tsx +++ b/packages/graphs/src/core/utils/node.tsx @@ -1,4 +1,3 @@ -import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import type { EdgeData, EdgeDirection, GraphData, ID, NodeData } from '@antv/g6'; import { ReactNodeStyleProps } from '@antv/g6-extension-react'; import { get, has, isObject, set } from 'lodash'; @@ -33,7 +32,23 @@ export function parseCollapsible(collapsible: GraphOptions['collapsible']): Coll const defaultOptions: CollapsibleOptions = { trigger: 'icon', direction: 'out', - iconRender: (isCollapsed: boolean) => (isCollapsed ? : ), + iconRender: (isCollapsed: boolean) => ( +
+ {isCollapsed ? '+' : '-'} +
+ ), }; return collapsible === true ? defaultOptions : isObject(collapsible) ? { ...defaultOptions, ...collapsible } : {}; diff --git a/packages/graphs/tests/demos/organization-chart.tsx b/packages/graphs/tests/demos/organization-chart.tsx index 960c9ebe1..93e8a24c3 100644 --- a/packages/graphs/tests/demos/organization-chart.tsx +++ b/packages/graphs/tests/demos/organization-chart.tsx @@ -20,6 +20,7 @@ export const OrganizationChart = () => { }, []); const options: GraphOptions = { + collapsible: true, data, padding: 20, autoFit: 'view',