Skip to content

Commit

Permalink
feat(graphs): add hierarchical graph (#2692)
Browse files Browse the repository at this point in the history
* feat: add hierarchical graph

* chore: add prettier plugins

* feat: add organization chart demo based on hierarchical graph

* fix: remove react strict mode

* fix: fix cr issues

* fix: remove @ant-design/icons
  • Loading branch information
yvonneyx authored Sep 11, 2024
1 parent 055dc02 commit 6528d95
Show file tree
Hide file tree
Showing 36 changed files with 818 additions and 82 deletions.
1 change: 1 addition & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')],
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 16 additions & 13 deletions packages/graphs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@
"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"
},
"repository": {
"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",
Expand All @@ -35,20 +46,13 @@
"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:*",
"@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": {
"@types/jest": "^26.0.0",
Expand All @@ -62,6 +66,5 @@
"peerDependencies": {
"react": ">=16.8.4",
"react-dom": ">=16.8.4"
},
"license": "MIT"
}
}
5 changes: 0 additions & 5 deletions packages/graphs/src/components/HierarchicalGraph/index.tsx

This file was deleted.

14 changes: 14 additions & 0 deletions packages/graphs/src/components/hierarchical-graph/index.tsx
Original file line number Diff line number Diff line change
@@ -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<PropsWithChildren<GraphOptions>> & RefAttributes<Graph>
> = forwardRef<Graph, PropsWithChildren<GraphOptions>>(({ children, ...props }, ref) => (
<BaseGraph type="hierarchical-graph" {...props} ref={ref}>
{children}
</BaseGraph>
));

export default HierarchicalGraph;
4 changes: 3 additions & 1 deletion packages/graphs/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { HierarchicalGraph } from './HierarchicalGraph';
import HierarchicalGraph from './hierarchical-graph';

export { HierarchicalGraph };
62 changes: 62 additions & 0 deletions packages/graphs/src/core/base-graph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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<PropsWithChildren<BaseGraphProps>> & RefAttributes<Graph>
> = forwardRef<Graph, PropsWithChildren<BaseGraphProps>>(({ children, ...props }, ref) => {
const {
type,
containerStyle,
className,
onInit,
onReady,
onDestroy,
errorTemplate,
loading,
loadingTemplate,
...propOptions
} = props;
const graphRef = useRef<Graph | null>(null);

const options = useOptions(type, propOptions);

useImperativeHandle(ref, () => graphRef.current!);

return (
<ErrorBoundary errorTemplate={errorTemplate}>
{loading && <ChartLoading loadingTemplate={loadingTemplate} />}
<Graphin
ref={(ref) => {
graphRef.current = ref;
}}
className={className}
style={containerStyle}
options={options}
onInit={onInit}
onReady={onReady}
onDestroy={onDestroy}
>
{children}
</Graphin>
</ErrorBoundary>
);
});
21 changes: 21 additions & 0 deletions packages/graphs/src/core/behaviors/hover-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { HoverActivate, idOf } from '@antv/g6';

export class HoverElement extends HoverActivate {
getActiveIds(event) {
const { model, graph } = this.context;
const targetId = event.target.id;
const targetType = graph.getElementType(targetId);

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;
}
}
1 change: 1 addition & 0 deletions packages/graphs/src/core/behaviors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { HoverElement } from './hover-element';
1 change: 1 addition & 0 deletions packages/graphs/src/core/hoc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { withCollapsibleNode } from './with-collapsible-node';
67 changes: 67 additions & 0 deletions packages/graphs/src/core/hoc/with-collapsible-node.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { idOf } from '@antv/g6';
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';

const StyledWrapper = styled.div`
position: relative;
height: inherit;
width: inherit;
.collapsible-icon {
position: absolute;
left: 50%;
top: calc(100% + 24px);
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 wrapperRef = useRef(null);
const iconRef = useRef(null);

const isIconShown = trigger === 'icon' && !isEmpty(data.children);

const handleClickCollapse = async (e) => {
e.stopPropagation();

const toggleExpandCollapse = isCollapsed ? 'expandElement' : 'collapseElement';
await graph[toggleExpandCollapse](idOf(data));
setIsCollapsed((prev) => !prev);

await graph.layout();
};

useEffect(() => {
const target = trigger === 'icon' ? iconRef.current : trigger === 'node' ? wrapperRef.current : trigger;

target?.addEventListener('click', handleClickCollapse);
return () => {
target?.removeEventListener('click', handleClickCollapse);
};
}, [trigger, isCollapsed]);

return (
<StyledWrapper ref={wrapperRef}>
{isIconShown && (
<div ref={iconRef} className={`collapsible-icon ${iconClassName}`} style={iconStyle}>
{iconRender?.(isCollapsed)}
</div>
)}
{NodeComponent.call(graph, data)}
</StyledWrapper>
);
};
};
1 change: 1 addition & 0 deletions packages/graphs/src/core/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useOptions } from './useOptions';
66 changes: 66 additions & 0 deletions packages/graphs/src/core/hooks/useOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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 = {
node: {
type: 'react',
style: {},
state: {
active: {
halo: false,
},
selected: {
halo: false,
},
},
},
transforms: ['infer-react-style'],
};

const hierarchicalGraphOptions: GraphOptions = {
node: {
style: {
component: (data: NodeData) => <PlainNode text={idOf(data)} isActive={data.states?.includes('active')} />,
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<GraphType, GraphOptions> = {
'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]);
};
2 changes: 2 additions & 0 deletions packages/graphs/src/core/nodes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { OrganizationChartNode } from './organization-chart-node';
export { PlainNode } from './plain-node';
Loading

0 comments on commit 6528d95

Please sign in to comment.