Skip to content

Commit

Permalink
feat(graphs): add mindmap (#2697)
Browse files Browse the repository at this point in the history
* refactor: extract options logic into upper component

* feat: icon supports placement, offsetX, offsetY

* feat: mindmap

* fix: fix ci issues

* feat: custom styled mindmap

* refactor: change collapsible option to transform

* fix: fix ci issues

* refactor: rename infer-react-style to translate-react-node-origin

* refactor: modify field names

* fix: fix ci issues
  • Loading branch information
yvonneyx authored Sep 18, 2024
1 parent 6528d95 commit bfcfa95
Show file tree
Hide file tree
Showing 36 changed files with 817 additions and 338 deletions.
64 changes: 55 additions & 9 deletions packages/graphs/src/components/hierarchical-graph/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,60 @@
import type { Graph } from '@antv/g6';
import React, { forwardRef, ForwardRefExoticComponent, PropsWithChildren, PropsWithoutRef, RefAttributes } from 'react';
import type { Graph, NodeData } from '@antv/g6';
import { idOf } from '@antv/g6';
import React, {
forwardRef,
ForwardRefExoticComponent,
PropsWithChildren,
PropsWithoutRef,
RefAttributes,
useMemo,
} from 'react';
import { BaseGraph } from '../../core/base-graph';
import { COMMON_OPTIONS } from '../../core/constants';
import { PlainNode } from '../../core/nodes';
import { mergeOptions } from '../../core/utils/options';
import type { GraphOptions } from '../../types';

const HierarchicalGraph: ForwardRefExoticComponent<
const DEFAULT_OPTIONS: GraphOptions = {
node: {
type: 'react',
style: {
component: (data: NodeData) => <PlainNode text={idOf(data)} isActive={data.states?.includes('active')} />,
size: [80, 40],
ports: [{ placement: 'top' }, { placement: 'bottom' }],
},
state: {
active: {
halo: false,
},
selected: {
halo: false,
},
},
},
edge: {
type: 'polyline',
style: {
router: {
type: 'orth',
},
},
},
layout: {
type: 'antv-dagre',
rankdir: 'TB',
},
transforms: ['translate-react-node-origin'],
animation: false,
};

export const HierarchicalGraph: ForwardRefExoticComponent<
PropsWithoutRef<PropsWithChildren<GraphOptions>> & RefAttributes<Graph>
> = forwardRef<Graph, PropsWithChildren<GraphOptions>>(({ children, ...props }, ref) => (
<BaseGraph type="hierarchical-graph" {...props} ref={ref}>
{children}
</BaseGraph>
));
> = forwardRef<Graph, PropsWithChildren<GraphOptions>>(({ children, ...props }, ref) => {
const options = useMemo(() => mergeOptions(COMMON_OPTIONS, DEFAULT_OPTIONS, props), [props]);

export default HierarchicalGraph;
return (
<BaseGraph {...options} ref={ref}>
{children}
</BaseGraph>
);
});
5 changes: 2 additions & 3 deletions packages/graphs/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
import HierarchicalGraph from './hierarchical-graph';

export { HierarchicalGraph };
export { HierarchicalGraph } from './hierarchical-graph';
export { MindMap } from './mind-map';
90 changes: 90 additions & 0 deletions packages/graphs/src/components/mind-map/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Graph, NodeData } from '@antv/g6';
import { idOf } from '@antv/g6';
import React, {
forwardRef,
ForwardRefExoticComponent,
PropsWithChildren,
PropsWithoutRef,
RefAttributes,
useMemo,
} from 'react';
import { BaseGraph } from '../../core/base-graph';
import { COMMON_OPTIONS } from '../../core/constants';
import { measureMindMapNodeSize, MindMapNode } from '../../core/nodes';
import { getNodeSide } from '../../core/utils/node';
import { mergeOptions } from '../../core/utils/options';
import type { GraphOptions } from '../../types';

const DEFAULT_OPTIONS: GraphOptions = {
node: {
type: 'react',
style: {
component: (data: NodeData) => (
<MindMapNode text={idOf(data)} depth={data.data!.depth as number} color={data.style!.color as string} />
),
size: (data: NodeData) => measureMindMapNodeSize(data),
dx: function (data: NodeData) {
const parentData = (this as unknown as Graph).getParentData(idOf(data), 'tree');
const side = getNodeSide(data, parentData);
const size = measureMindMapNodeSize(data);
return side === 'left' ? -size[0] : side === 'center' ? -size[0] / 2 : 0;
},
ports: [{ placement: 'left' }, { placement: 'right' }],
},
state: {
active: {
halo: false,
},
selected: {
halo: false,
},
},
},
edge: {
type: 'cubic-horizontal',
style: {
stroke: function (data) {
return (this.getNodeData(data.source).style!.color as string) || '#99ADD1';
},
lineWidth: 2,
},
},
layout: {
type: 'mindmap',
direction: 'H',
getWidth: () => 120,
getHeight: (data) => measureMindMapNodeSize(data)[1],
getVGap: () => 28,
getHGap: () => 64,
animation: false,
},
transforms: (prev) => [
...prev,
'assign-color-by-branch',
{
type: 'collapse-expand-react-node',
key: 'collapse-expand-react-node',
trigger: 'node',
iconPlacement: function (data: NodeData) {
const parentData = (this as unknown as Graph).getParentData(idOf(data), 'tree');
const side = getNodeSide(data, parentData);
return side === 'left' ? 'left' : 'right';
},
},
],
animation: {
duration: 500,
},
};

export const MindMap: ForwardRefExoticComponent<
PropsWithoutRef<PropsWithChildren<GraphOptions>> & RefAttributes<Graph>
> = forwardRef<Graph, PropsWithChildren<GraphOptions>>(({ children, ...props }, ref) => {
const options = useMemo(() => mergeOptions(COMMON_OPTIONS, DEFAULT_OPTIONS, props), [props]);

return (
<BaseGraph {...options} ref={ref}>
{children}
</BaseGraph>
);
});
61 changes: 22 additions & 39 deletions packages/graphs/src/core/base-graph.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChartLoading, ErrorBoundary } from '@ant-design/charts-util';
import type { Graph } from '@antv/g6';
import type { Graph, GraphOptions as G6GraphOptions } from '@antv/g6';
import { Graphin } from '@antv/graphin';
import { isEmpty } from 'lodash';
import React, {
forwardRef,
ForwardRefExoticComponent,
Expand All @@ -10,53 +11,35 @@ import React, {
useImperativeHandle,
useRef,
} from 'react';
import type { GraphOptions, GraphType } from '../types';
import { useOptions } from './hooks';

interface BaseGraphProps extends GraphOptions {
/**
* 内部属性,只读
*/
readonly type: GraphType;
}
import type { GraphOptions } from '../types';

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;
PropsWithoutRef<PropsWithChildren<GraphOptions>> & RefAttributes<Graph>
> = forwardRef<Graph, PropsWithChildren<GraphOptions>>(({ children, ...props }, ref) => {
const { containerStyle, className, onInit, onReady, onDestroy, errorTemplate, loading, loadingTemplate, ...options } =
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>
{!isEmpty(options.data) && (
<Graphin
ref={(ref) => {
graphRef.current = ref;
}}
className={className}
style={containerStyle}
options={options as G6GraphOptions}
onInit={onInit}
onReady={onReady}
onDestroy={onDestroy}
>
{children}
</Graphin>
)}
</ErrorBoundary>
);
});
1 change: 1 addition & 0 deletions packages/graphs/src/core/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { COMMON_OPTIONS } from './options';
5 changes: 5 additions & 0 deletions packages/graphs/src/core/constants/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { GraphOptions } from '../../types';

export const COMMON_OPTIONS: GraphOptions = {
behaviors: ['drag-canvas', 'zoom-canvas'],
};
54 changes: 39 additions & 15 deletions packages/graphs/src/core/hoc/with-collapsible-node.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
import type { CardinalPlacement } from '@antv/g6';
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 styled, { css } from 'styled-components';
import type { NodeProps } from '../nodes/types';
import type { CollapseExpandReactNodeOptions } from '../transform';

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;
const StyledIcon = styled.div<{ placement: CardinalPlacement; offsetX: number; offsetY: number }>`
position: absolute;
transform: translate(-50%, -50%);
z-index: 1;
&:hover {
cursor: pointer;
}
&:hover {
cursor: pointer;
}
${({ placement, offsetX, offsetY }) => {
const positions = {
top: `left: calc(50% + ${offsetX}px); top: ${offsetY}px;`,
bottom: `left: calc(50% + ${offsetX}px); top: calc(100% + ${offsetY}px);`,
right: `left: calc(100% + ${offsetX}px); top: calc(50% + ${offsetY}px);`,
left: `left: ${offsetX}px; top: calc(50% + ${offsetY}px);`,
};
return css`
${positions[placement]}
`;
}}
`;

interface CollapsibleNodeProps extends NodeProps, CollapsibleOptions {}
interface CollapsibleNodeProps extends NodeProps, CollapseExpandReactNodeOptions {}

export const withCollapsibleNode = (NodeComponent: React.FC) => {
return (props: CollapsibleNodeProps) => {
const { data, graph, trigger, iconRender, iconClassName = '', iconStyle } = props;
const { data, graph, trigger, iconRender, iconPlacement, iconOffsetX, iconOffsetY, iconClassName, iconStyle } =
props as Required<CollapsibleNodeProps>;
const [isCollapsed, setIsCollapsed] = useState(get(data, 'style.collapsed', false));
const wrapperRef = useRef(null);
const iconRef = useRef(null);
Expand All @@ -53,12 +66,23 @@ export const withCollapsibleNode = (NodeComponent: React.FC) => {
};
}, [trigger, isCollapsed]);

const computeCallbackStyle = (callableStyle: Function | number | string) => {
return typeof callableStyle === 'function' ? callableStyle.call(graph, data) : callableStyle;
};

return (
<StyledWrapper ref={wrapperRef}>
{isIconShown && (
<div ref={iconRef} className={`collapsible-icon ${iconClassName}`} style={iconStyle}>
<StyledIcon
ref={iconRef}
placement={computeCallbackStyle(iconPlacement)}
offsetX={computeCallbackStyle(iconOffsetX)}
offsetY={computeCallbackStyle(iconOffsetY)}
className={iconClassName}
style={iconStyle}
>
{iconRender?.(isCollapsed)}
</div>
</StyledIcon>
)}
{NodeComponent.call(graph, data)}
</StyledWrapper>
Expand Down
1 change: 0 additions & 1 deletion packages/graphs/src/core/hooks/index.ts

This file was deleted.

Loading

0 comments on commit bfcfa95

Please sign in to comment.