Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(graphs): add hierarchical graph #2692

Merged
merged 6 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
30 changes: 17 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,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",
lxfu1 marked this conversation as resolved.
Show resolved Hide resolved
"@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",
yvonneyx marked this conversation as resolved.
Show resolved Hide resolved
"styled-components": "^6.1.13"
},
"devDependencies": {
"@types/jest": "^26.0.0",
Expand All @@ -62,6 +67,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 };
48 changes: 48 additions & 0 deletions packages/graphs/src/core/base-graph.tsx
Original file line number Diff line number Diff line change
@@ -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<PropsWithChildren<BaseGraphProps>> & RefAttributes<Graph>
> = forwardRef<Graph, PropsWithChildren<BaseGraphProps>>(({ children, ...props }, ref) => {
const { type, containerStyle, className, errorTemplate, loading, loadingTemplate, ...propOptions } = props;
const graphRef = useRef<Graph | null>(null);

const options = useOptions(type, propOptions);

useImperativeHandle(ref, () => graphRef.current!);
yvonneyx marked this conversation as resolved.
Show resolved Hide resolved

return (
<ErrorBoundary errorTemplate={errorTemplate}>
{loading && <ChartLoading loadingTemplate={loadingTemplate} />}
<Graphin
onInit={(ref) => {
graphRef.current = ref;
}}
className={className}
style={containerStyle}
options={options}
>
{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 { 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;
}
}
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';
71 changes: 71 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,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 (
<StyledWrapper id={`${nodeId}-collapsible-node`}>
yvonneyx marked this conversation as resolved.
Show resolved Hide resolved
{isIconShown && (
<div id={`${nodeId}-collapsible-icon`} 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';
67 changes: 67 additions & 0 deletions packages/graphs/src/core/hooks/useOptions.tsx
Original file line number Diff line number Diff line change
@@ -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) => <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
Loading