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): flowchart #2709

Merged
merged 4 commits into from
Sep 24, 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
28 changes: 28 additions & 0 deletions packages/graphs/src/components/flow-direction-graph/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Graph } from '@antv/g6';
import React, {
ForwardRefExoticComponent,
PropsWithChildren,
PropsWithoutRef,
RefAttributes,
forwardRef,
useMemo,
} from 'react';
import { BaseGraph } from '../../core/base-graph';
import { COMMON_OPTIONS } from '../../core/constants';
import { mergeOptions } from '../../core/utils/options';
import { DEFAULT_OPTIONS } from './options';
import type { FlowDirectionGraphOptions } from './types';

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

return (
<BaseGraph {...options} ref={ref}>
{children}
</BaseGraph>
);
});

export type { FlowDirectionGraphOptions };
40 changes: 40 additions & 0 deletions packages/graphs/src/components/flow-direction-graph/options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { RCNode } from '../../core/base';
import type { GraphOptions } from '../../types';

const { TextNode } = RCNode;

export const DEFAULT_OPTIONS: GraphOptions = {
node: {
type: 'react',
style: {
component: (data) => <TextNode type="filled" text={data.id} />,
size: [80, 40],
ports: [{ placement: 'left' }, { placement: 'right' }],
},
state: {
active: {
halo: false,
},
selected: {
halo: false,
},
},
},
edge: {
type: 'cubic-horizontal',
style: {
strokeOpacity: 0.5,
},
state: {
active: {
strokeOpacity: 1,
},
},
},
layout: {
type: 'antv-dagre',
rankdir: 'LR',
},
transforms: ['translate-react-node-origin'],
};
3 changes: 3 additions & 0 deletions packages/graphs/src/components/flow-direction-graph/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { GraphOptions } from '../../types';

export interface FlowDirectionGraphOptions extends GraphOptions {}
7 changes: 5 additions & 2 deletions packages/graphs/src/components/flow-graph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import React, {
import { BaseGraph } from '../../core/base-graph';
import { COMMON_OPTIONS } from '../../core/constants';
import { mergeOptions } from '../../core/utils/options';
import { DEFAULT_OPTIONS } from './options';
import { DEFAULT_OPTIONS, getFlowGraphOptions } from './options';
import type { FlowGraphOptions } from './types';

export const FlowGraph: ForwardRefExoticComponent<
PropsWithoutRef<PropsWithChildren<FlowGraphOptions>> & RefAttributes<Graph>
> = forwardRef<Graph, PropsWithChildren<FlowGraphOptions>>(({ children, ...props }, ref) => {
const options = useMemo(() => mergeOptions(COMMON_OPTIONS, DEFAULT_OPTIONS, props), [props]);
const options = useMemo(() => {
const { direction = 'horizontal', ...restProps } = props;
return mergeOptions(COMMON_OPTIONS, DEFAULT_OPTIONS, getFlowGraphOptions({ direction }), restProps);
}, [props]);

return (
<BaseGraph {...options} ref={ref}>
Expand Down
41 changes: 30 additions & 11 deletions packages/graphs/src/components/flow-graph/options.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import { RCNode } from '../../core/base';
import type { GraphOptions } from '../../types';
import type { FlowGraphOptions } from './types';

const { TextNode } = RCNode;

export const DEFAULT_OPTIONS: GraphOptions = {
export const DEFAULT_OPTIONS: FlowGraphOptions = {
node: {
type: 'react',
style: {
component: () => <TextNode type="filled" />,
component: (data) => <TextNode type="filled" text={data.id} />,
size: [80, 40],
ports: [{ placement: 'left' }, { placement: 'right' }],
},
Expand All @@ -22,19 +22,38 @@ export const DEFAULT_OPTIONS: GraphOptions = {
},
},
edge: {
type: 'cubic-horizontal',
type: 'polyline',
style: {
strokeOpacity: 0.5,
},
state: {
active: {
strokeOpacity: 1,
},
lineWidth: 2,
endArrow: true,
radius: 8,
router: { type: 'orth' },
},
},
layout: {
type: 'antv-dagre',
type: 'dagre',
rankdir: 'LR',
animation: false,
},
transforms: ['translate-react-node-origin'],
};

export const getFlowGraphOptions = ({ direction }: Pick<FlowGraphOptions, 'direction'>): FlowGraphOptions => {
let options: FlowGraphOptions = {};

if (direction === 'vertical') {
options = {
node: {
style: {
ports: [{ placement: 'top' }, { placement: 'bottom' }],
},
},
layout: {
type: 'dagre',
rankdir: 'TB',
},
};
}

return options;
};
10 changes: 8 additions & 2 deletions packages/graphs/src/components/flow-graph/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { GraphOptions } from '../../types';
import type { GraphOptions } from '../../types';

export interface FlowGraphOptions extends GraphOptions {}
export interface FlowGraphOptions extends GraphOptions {
/**
* The direction of the FlowGraph.
* @default 'horizontal'
*/
direction?: 'horizontal' | 'vertical';
}
3 changes: 2 additions & 1 deletion packages/graphs/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { Dendrogram, type DendrogramOptions } from './dendrogram';
export { FlowGraph, type FlowGraphOptions } from './flow-graph';
export { Dendrogram, type DendrogramOptions } from './dendrogram';
export { FlowDirectionGraph, type FlowDirectionGraphOptions } from './flow-direction-graph';
export { IndentedTree, type IndentedTreeOptions } from './indented-tree';
export { MindMap, type MindMapOptions } from './mind-map';
export { NetworkGraph, type NetworkGraphOptions } from './network-graph';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const DEFAULT_OPTIONS: OrganizationChartOptions = {
node: {
type: 'react',
style: {
component: () => <TextNode type="filled" />,
component: (data) => <TextNode type="filled" text={data.id} />,
size: [80, 40],
ports: [{ placement: 'top' }, { placement: 'bottom' }],
},
Expand Down
32 changes: 31 additions & 1 deletion packages/graphs/src/core/base/node/text-node.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { measureTextWidth } from '@ant-design/charts-util';
import React, { FC } from 'react';
import styled, { css } from 'styled-components';
import { darkenHexColor, hexToRgba } from '../../utils/color';

const StyledWrapper = styled.div<{ $type: TextNodeProps['type']; $color: string; $borderWidth: number }>`
const StyledWrapper = styled.div<{
$type: TextNodeProps['type'];
$color: string;
$borderWidth: number;
$isActive: boolean;
$isSelected: boolean;
}>`
position: relative;
display: flex;
align-items: center;
Expand Down Expand Up @@ -45,6 +52,15 @@ const StyledWrapper = styled.div<{ $type: TextNodeProps['type']; $color: string;
`;
}
}}

${({ $isActive, $isSelected, $borderWidth, $color }) =>
($isActive || $isSelected) &&
css`
height: calc(100% - 2 * ${$borderWidth}px);
width: calc(100% - 2 * ${$borderWidth}px);
border: ${$borderWidth}px solid ${darkenHexColor($color, 100)};
${$isSelected && `box-shadow: 0 0 0 2px ${hexToRgba($color, 0.1)};`}
`}
`;

export interface TextNodeProps extends Pick<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style'> {
Expand Down Expand Up @@ -82,6 +98,16 @@ export interface TextNodeProps extends Pick<React.HTMLAttributes<HTMLDivElement>
fontVariant?: number | string | undefined;
fontWeight?: number | string | undefined;
};
/**
* Whether the node is active
* @default false
*/
isActive?: boolean;
/**
* Whether the node is selected
* @default false
*/
isSelected?: boolean;
}

export const TextNode: FC<TextNodeProps> = (props) => {
Expand All @@ -94,6 +120,8 @@ export const TextNode: FC<TextNodeProps> = (props) => {
color = '#1783ff',
borderWidth = 2,
maxWidth = Infinity,
isActive = false,
isSelected = false,
} = props;
const isMultiLine = measureTextWidth(text, font) > maxWidth;

Expand All @@ -102,6 +130,8 @@ export const TextNode: FC<TextNodeProps> = (props) => {
$type={type}
$color={color}
$borderWidth={borderWidth}
$isActive={isActive}
$isSelected={isSelected}
className={className}
style={{ ...style, ...font }}
>
Expand Down
43 changes: 43 additions & 0 deletions packages/graphs/src/core/behaviors/hover-activate-chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { EdgeDirection, ID } from '@antv/g6';
import { HoverActivate, idOf } from '@antv/g6';

/**
* Behavior to activate the hovered element and its chain (including nodes and edges).
*/
export class HoverActivateChain extends HoverActivate {
protected 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);
this.collectChainNodes(edge.source, 'in', ids);
this.collectChainNodes(edge.target, 'out', ids);
} else if (targetType === 'node') {
this.collectChainNodes(targetId, 'both', ids);
}

graph.frontElement(ids);

return ids;
}

private collectChainNodes(nodeId: ID, direction: EdgeDirection, ids: ID[]) {
const { model } = this.context;
const edges = model.getRelatedEdgesData(nodeId, direction);

edges.forEach((edge) => {
if (!ids.includes(idOf(edge))) ids.push(idOf(edge));
if (!ids.includes(edge.source)) {
ids.push(edge.source);
this.collectChainNodes(edge.source, 'in', ids);
}
if (!ids.includes(edge.target)) {
ids.push(edge.target);
this.collectChainNodes(edge.target, 'out', ids);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HoverActivate, idOf } from '@antv/g6';

export class HoverElement extends HoverActivate {
export class HoverActivateNeighbors extends HoverActivate {
getActiveIds(event) {
const { model, graph } = this.context;
const targetId = event.target.id;
Expand Down
3 changes: 2 additions & 1 deletion packages/graphs/src/core/behaviors/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { HoverElement } from './hover-element';
export { HoverActivateChain } from './hover-activate-chain';
export { HoverActivateNeighbors } from './hover-activate-neighbors';
5 changes: 5 additions & 0 deletions packages/graphs/src/core/registry/build-in.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ReactNode } from '@antv/g6-extension-react';
import { HoverActivateChain, HoverActivateNeighbors } from '../behaviors';
import { IndentedEdge } from '../edges';
import {
ArrangeEdgeZIndex,
Expand All @@ -15,6 +16,10 @@ export const BUILT_IN_EXTENSIONS = {
edge: {
indented: IndentedEdge,
},
behavior: {
'hover-activate-neighbors': HoverActivateNeighbors,
'hover-activate-chain': HoverActivateChain,
},
transform: {
'translate-react-node-origin': TranslateReactNodeOrigin,
'collapse-expand-react-node': CollapseExpandReactNode,
Expand Down
37 changes: 37 additions & 0 deletions packages/graphs/src/core/utils/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function hexToRgba(hex, opacity) {
let r = 0,
g = 0,
b = 0;
if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) {
r = parseInt(hex[1] + hex[2], 16);
g = parseInt(hex[3] + hex[4], 16);
b = parseInt(hex[5] + hex[6], 16);
}
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}

export function darkenHexColor(hex: string, amount: number): string {
let r = 0,
g = 0,
b = 0;
if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) {
r = parseInt(hex[1] + hex[2], 16);
g = parseInt(hex[3] + hex[4], 16);
b = parseInt(hex[5] + hex[6], 16);
}

r = Math.max(0, r - amount);
g = Math.max(0, g - amount);
b = Math.max(0, b - amount);

const toHex = (c: number) => c.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
20 changes: 18 additions & 2 deletions packages/graphs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import * as G6 from '@antv/g6';
import './preset';

export { Dendrogram, FlowGraph, IndentedTree, MindMap, NetworkGraph, OrganizationChart } from './components';
export type { DendrogramOptions, IndentedTreeOptions, MindMapOptions, OrganizationChartOptions } from './components';
export {
Dendrogram,
FlowDirectionGraph,
FlowGraph,
IndentedTree,
MindMap,
NetworkGraph,
OrganizationChart,
} from './components';
export type {
DendrogramOptions,
FlowDirectionGraphOptions,
FlowGraphOptions,
IndentedTreeOptions,
MindMapOptions,
NetworkGraphOptions,
OrganizationChartOptions,
} from './components';
export { CollapseExpandIcon, RCNode } from './core/base';
export type { OrganizationChartNodeProps, TextNodeProps } from './core/base/node';
export { measureTextSize } from './core/utils/measure-text';
Expand Down
Loading
Loading