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 all 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
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!);
yvonneyx marked this conversation as resolved.
Show resolved Hide resolved

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
Loading