From 26b72956bdfd18a277d2e595e40f661c337d8a40 Mon Sep 17 00:00:00 2001 From: Yuxin <55794321+yvonneyx@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:07:23 +0800 Subject: [PATCH] feat: support labelField in MindMap, IndentedTree, Fishbone, FlowGraph (#2801) * feat: support labelField in MindMap * feat: support labelField in IndentedTree * feat: support labelField in Fishbone * feat: support labelField in FlowDirectionGraph and FlowGraph * fix: typo --- .../graphs/src/components/fishbone/index.tsx | 13 +- .../src/components/fishbone/options.tsx | 43 +++--- .../graphs/src/components/fishbone/types.ts | 9 +- .../flow-direction-graph/options.tsx | 26 +++- .../components/flow-direction-graph/types.ts | 13 +- .../src/components/flow-graph/index.tsx | 4 +- .../src/components/flow-graph/options.tsx | 42 +++--- .../graphs/src/components/flow-graph/types.ts | 8 ++ .../src/components/indented-tree/index.tsx | 18 ++- .../src/components/indented-tree/options.tsx | 107 +++++++++++---- .../src/components/indented-tree/types.ts | 8 ++ .../graphs/src/components/mind-map/index.tsx | 4 +- .../src/components/mind-map/options.tsx | 129 +++++++++++------- .../graphs/src/components/mind-map/types.ts | 8 ++ .../collapse-expand-icon/arrow-count-icon.tsx | 6 +- .../base/node/organization-chart-node.tsx | 6 +- .../graphs/src/core/base/node/text-node.tsx | 2 +- packages/graphs/src/core/utils/label.ts | 11 ++ .../graphs/src/core/utils/measure-text.ts | 3 +- .../graphs/tests/demos/mind-map-linear.tsx | 7 + 20 files changed, 320 insertions(+), 147 deletions(-) create mode 100644 packages/graphs/src/core/utils/label.ts diff --git a/packages/graphs/src/components/fishbone/index.tsx b/packages/graphs/src/components/fishbone/index.tsx index 69ba8b716..8857b7b90 100644 --- a/packages/graphs/src/components/fishbone/index.tsx +++ b/packages/graphs/src/components/fishbone/index.tsx @@ -13,16 +13,21 @@ import { mergeOptions } from '../../core/utils/options'; import { DEFAULT_OPTIONS, getFishboneOptions } from './options'; import type { FishboneOptions } from './types'; -export const Fishbone: ForwardRefExoticComponent> & RefAttributes> = forwardRef>(({ children, ...props }, ref) => { - const { type = 'cause', ...restProps } = props; +export const Fishbone: ForwardRefExoticComponent< + PropsWithoutRef> & RefAttributes +> = forwardRef>(({ children, ...props }, ref) => { + const { type = 'cause', labelField, ...restProps } = props; - const options = useMemo(() => mergeOptions(COMMON_OPTIONS, DEFAULT_OPTIONS, getFishboneOptions({ type }), restProps), [props]); + const options = useMemo( + () => mergeOptions(COMMON_OPTIONS, DEFAULT_OPTIONS, getFishboneOptions({ type, labelField }), restProps), + [props], + ); return ( {children} ); -}) +}); export type { FishboneOptions }; diff --git a/packages/graphs/src/components/fishbone/options.tsx b/packages/graphs/src/components/fishbone/options.tsx index b40bdd28e..88a584d0f 100644 --- a/packages/graphs/src/components/fishbone/options.tsx +++ b/packages/graphs/src/components/fishbone/options.tsx @@ -1,14 +1,15 @@ import type { ID, NodeData, SingleLayoutOptions, Size } from '@antv/g6'; -import type { FishboneOptions } from "./types"; +import { get } from 'lodash'; +import { formatLabel } from '../../core/utils/label'; import { measureTextSize } from '../../core/utils/measure-text'; +import type { FishboneOptions } from './types'; export const DEFAULT_OPTIONS: FishboneOptions = { node: { style: { size: 10, - labelText: d => d.id, labelPlacement: 'center', - } + }, }, edge: { type: 'polyline', @@ -34,30 +35,34 @@ const getNodeFill = (node: NodeData): string => { if (depth === 0) return '#EFF0F0'; if (depth === 1) return (node.style?.color as string) || '#EFF0F0'; return 'transparent'; -} +}; -export function getFishboneOptions({ type }: Pick): FishboneOptions { +export function getFishboneOptions({ + type, + labelField, +}: Pick): FishboneOptions { const options: FishboneOptions = { node: { type: 'rect', style: { - fill: d => getNodeFill(d), - labelFill: d => d.depth === 1 ? '#fff' : '#262626', + fill: (d) => getNodeFill(d), + labelFill: (d) => (d.depth === 1 ? '#fff' : '#262626'), labelFillOpacity: 1, - labelFontSize: d => d.depth === 0 ? 24 : d.depth === 1 ? 18 : 16, - labelFontWeight: d => d.depth === 0 ? 600 : 400, - labelLineHeight: d => d.depth === 0 ? 26 : d.depth === 1 ? 20 : 18, - labelText: d => d.id, + labelFontSize: (d) => (d.depth === 0 ? 24 : d.depth === 1 ? 18 : 16), + labelFontWeight: (d) => (d.depth === 0 ? 600 : 400), + labelLineHeight: (d) => (d.depth === 0 ? 26 : d.depth === 1 ? 20 : 18), + labelText: (d) => formatLabel(d, labelField), radius: 8, - size: d => getNodeSize(d.id, d.depth!), - } + size: (d) => getNodeSize(d.id, d.depth!), + }, }, edge: { type: 'polyline', style: { lineWidth: 3, stroke: function (data) { - return (this.getNodeData(data.target).style!.color as string) || '#99ADD1'; + const target = this.getNodeData(data.target); + return get(target, 'style.color', '#99ADD1') as string; }, }, }, @@ -71,18 +76,18 @@ export function getFishboneOptions({ type }: Pick): Fis type: 'arrange-edge-z-index', key: 'arrange-edge-z-index', }, - ] + ], }; options.layout ||= {} as SingleLayoutOptions; if (type === 'decision') { // @ts-ignore - options.node.style.labelPlacement = d => d.depth === 0 || d.depth === 1 ? 'center' : 'right'; - Object.assign(options.layout!, { direction: 'LR' }) + options.node.style.labelPlacement = (d) => (d.depth === 0 || d.depth === 1 ? 'center' : 'right'); + Object.assign(options.layout!, { direction: 'LR' }); } else if (type === 'cause') { // @ts-ignore - options.node.style.labelPlacement = d => d.depth === 0 || d.depth === 1 ? 'center' : 'left'; - Object.assign(options.layout!, { direction: 'RL' }) + options.node.style.labelPlacement = (d) => (d.depth === 0 || d.depth === 1 ? 'center' : 'left'); + Object.assign(options.layout!, { direction: 'RL' }); } return options; diff --git a/packages/graphs/src/components/fishbone/types.ts b/packages/graphs/src/components/fishbone/types.ts index 7670f4840..79b2ce9b9 100644 --- a/packages/graphs/src/components/fishbone/types.ts +++ b/packages/graphs/src/components/fishbone/types.ts @@ -1,5 +1,5 @@ -import { GraphOptions } from "../../types"; import type { NodeData } from '@antv/g6'; +import type { GraphOptions } from '../../types'; export interface FishboneOptions extends GraphOptions { /** @@ -9,4 +9,11 @@ export interface FishboneOptions extends GraphOptions { * @default 'cause' */ type?: 'decision' | 'cause'; + /** + * Selects a field from the data to use as the label for the node. + * If a string is provided, it will select the field as `data[labelField]`. + * If a function is provided, it will call the function with the data and use the returned value. + * @default (data) => data.id + */ + labelField?: string | ((node: NodeData) => string); } diff --git a/packages/graphs/src/components/flow-direction-graph/options.tsx b/packages/graphs/src/components/flow-direction-graph/options.tsx index c55faf472..959f9ae51 100644 --- a/packages/graphs/src/components/flow-direction-graph/options.tsx +++ b/packages/graphs/src/components/flow-direction-graph/options.tsx @@ -1,17 +1,14 @@ import React from 'react'; import { RCNode } from '../../core/base'; +import { formatLabel } from '../../core/utils/label'; import type { GraphOptions } from '../../types'; +import type { FlowDirectionGraphOptions } from './types'; const { TextNode } = RCNode; export const DEFAULT_OPTIONS: GraphOptions = { node: { type: 'react', - style: { - component: (data) => , - size: [100, 40], - ports: [{ placement: 'left' }, { placement: 'right' }], - }, state: { active: { halo: false, @@ -39,3 +36,22 @@ export const DEFAULT_OPTIONS: GraphOptions = { }, transforms: ['translate-react-node-origin'], }; + +export const getFlowDirectionGraphOptions = ({ + labelField, +}: Pick): FlowDirectionGraphOptions => { + const options: FlowDirectionGraphOptions = { + node: { + style: { + component: (data) => { + const label = formatLabel(data, labelField); + return ; + }, + size: [100, 40], + ports: [{ placement: 'left' }, { placement: 'right' }], + }, + }, + }; + + return options; +}; diff --git a/packages/graphs/src/components/flow-direction-graph/types.ts b/packages/graphs/src/components/flow-direction-graph/types.ts index d26ee269d..a0e5f871c 100644 --- a/packages/graphs/src/components/flow-direction-graph/types.ts +++ b/packages/graphs/src/components/flow-direction-graph/types.ts @@ -1,3 +1,12 @@ -import { GraphOptions } from '../../types'; +import type { NodeData } from '@antv/g6'; +import type { GraphOptions } from '../../types'; -export interface FlowDirectionGraphOptions extends GraphOptions {} +export interface FlowDirectionGraphOptions extends GraphOptions { + /** + * Selects a field from the data to use as the label for the node. + * If a string is provided, it will select the field as `data[labelField]`. + * If a function is provided, it will call the function with the data and use the returned value. + * @default (data) => data.id + */ + labelField?: string | ((node: NodeData) => string); +} diff --git a/packages/graphs/src/components/flow-graph/index.tsx b/packages/graphs/src/components/flow-graph/index.tsx index 4e508ad70..30f96b9d2 100644 --- a/packages/graphs/src/components/flow-graph/index.tsx +++ b/packages/graphs/src/components/flow-graph/index.tsx @@ -17,8 +17,8 @@ export const FlowGraph: ForwardRefExoticComponent< PropsWithoutRef> & RefAttributes > = forwardRef>(({ children, ...props }, ref) => { const options = useMemo(() => { - const { direction = 'horizontal', ...restProps } = props; - return mergeOptions(COMMON_OPTIONS, DEFAULT_OPTIONS, getFlowGraphOptions({ direction }), restProps); + const { direction = 'horizontal', labelField, ...restProps } = props; + return mergeOptions(COMMON_OPTIONS, DEFAULT_OPTIONS, getFlowGraphOptions({ direction, labelField }), restProps); }, [props]); return ( diff --git a/packages/graphs/src/components/flow-graph/options.tsx b/packages/graphs/src/components/flow-graph/options.tsx index 8938ab44b..6ee647221 100644 --- a/packages/graphs/src/components/flow-graph/options.tsx +++ b/packages/graphs/src/components/flow-graph/options.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { RCNode } from '../../core/base'; +import { formatLabel } from '../../core/utils/label'; import type { FlowGraphOptions } from './types'; const { TextNode } = RCNode; @@ -7,11 +8,6 @@ const { TextNode } = RCNode; export const DEFAULT_OPTIONS: FlowGraphOptions = { node: { type: 'react', - style: { - component: (data) => , - size: [100, 40], - ports: [{ placement: 'left' }, { placement: 'right' }], - }, state: { active: { halo: false, @@ -32,28 +28,34 @@ export const DEFAULT_OPTIONS: FlowGraphOptions = { }, layout: { type: 'dagre', - rankdir: 'LR', animation: false, }, transforms: ['translate-react-node-origin'], }; -export const getFlowGraphOptions = ({ direction }: Pick): FlowGraphOptions => { - let options: FlowGraphOptions = {}; - - if (direction === 'vertical') { - options = { - node: { - style: { - ports: [{ placement: 'top' }, { placement: 'bottom' }], +export const getFlowGraphOptions = ({ + direction, + labelField, +}: Pick): FlowGraphOptions => { + const options: FlowGraphOptions = { + node: { + style: { + component: (data) => { + const label = formatLabel(data, labelField); + return ; }, + size: [100, 40], + ports: + direction === 'vertical' + ? [{ placement: 'top' }, { placement: 'bottom' }] + : [{ placement: 'left' }, { placement: 'right' }], }, - layout: { - type: 'dagre', - rankdir: 'TB', - }, - }; - } + }, + layout: { + type: 'dagre', + rankdir: direction === 'vertical' ? 'TB' : 'LR', + }, + }; return options; }; diff --git a/packages/graphs/src/components/flow-graph/types.ts b/packages/graphs/src/components/flow-graph/types.ts index 0d18aad31..48b80a188 100644 --- a/packages/graphs/src/components/flow-graph/types.ts +++ b/packages/graphs/src/components/flow-graph/types.ts @@ -1,3 +1,4 @@ +import type { NodeData } from '@antv/g6'; import type { GraphOptions } from '../../types'; export interface FlowGraphOptions extends GraphOptions { @@ -6,4 +7,11 @@ export interface FlowGraphOptions extends GraphOptions { * @default 'horizontal' */ direction?: 'horizontal' | 'vertical'; + /** + * Selects a field from the data to use as the label for the node. + * If a string is provided, it will select the field as `data[labelField]`. + * If a function is provided, it will call the function with the data and use the returned value. + * @default (data) => data.id + */ + labelField?: string | ((node: NodeData) => string); } diff --git a/packages/graphs/src/components/indented-tree/index.tsx b/packages/graphs/src/components/indented-tree/index.tsx index e0cf2a827..4bfbbac4a 100644 --- a/packages/graphs/src/components/indented-tree/index.tsx +++ b/packages/graphs/src/components/indented-tree/index.tsx @@ -16,14 +16,18 @@ import type { IndentedTreeOptions } from './types'; export const IndentedTree: ForwardRefExoticComponent< PropsWithoutRef> & RefAttributes > = forwardRef>(({ children, ...props }, ref) => { - const { type = 'default', nodeMinWidth, nodeMaxWidth, direction = 'right', ...restProps } = props; + const { type = 'default', nodeMinWidth, nodeMaxWidth, direction = 'right', labelField, ...restProps } = props; - const options = useMemo(() => mergeOptions( - COMMON_OPTIONS, - DEFAULT_OPTIONS, - getIndentedTreeOptions({ type, nodeMinWidth, nodeMaxWidth, direction }), - restProps, - ), [props]); + const options = useMemo( + () => + mergeOptions( + COMMON_OPTIONS, + DEFAULT_OPTIONS, + getIndentedTreeOptions({ type, nodeMinWidth, nodeMaxWidth, direction, labelField }), + restProps, + ), + [props], + ); return ( diff --git a/packages/graphs/src/components/indented-tree/options.tsx b/packages/graphs/src/components/indented-tree/options.tsx index 4afa62857..ee61ea142 100644 --- a/packages/graphs/src/components/indented-tree/options.tsx +++ b/packages/graphs/src/components/indented-tree/options.tsx @@ -1,7 +1,8 @@ import type { Graph, NodeData, SingleLayoutOptions } from '@antv/g6'; -import { idOf } from '@antv/g6'; +import { get } from 'lodash'; import React from 'react'; import { CollapseExpandIcon, RCNode, TextNodeProps } from '../../core/base'; +import { formatLabel } from '../../core/utils/label'; import { measureTextSize } from '../../core/utils/measure-text'; import { getNodeSide } from '../../core/utils/node'; import { getBoxedTextNodeStyle, getLinearTextNodeStyle } from '../../core/utils/tree'; @@ -13,10 +14,6 @@ const { TextNode } = RCNode; export const DEFAULT_OPTIONS: IndentedTreeOptions = { node: { type: 'react', - style: { - component: (data) => , - size: (data) => measureTextSize(idOf(data), [24, 16]), - }, state: { active: { halo: false, @@ -48,8 +45,6 @@ export const DEFAULT_OPTIONS: IndentedTreeOptions = { type: 'indented', direction: 'LR', indent: (node) => getIndent(node, 20), - getWidth: (data) => measureTextSize(idOf(data), [24, 16])[0], - getHeight: (data) => measureTextSize(idOf(data), [24, 16])[1], getVGap: () => 14, }, animation: { @@ -81,7 +76,11 @@ export const getIndentedTreeOptions = ({ nodeMinWidth, nodeMaxWidth, direction, -}: Pick): IndentedTreeOptions => { + labelField, +}: Pick< + IndentedTreeOptions, + 'type' | 'nodeMinWidth' | 'nodeMaxWidth' | 'direction' | 'labelField' +>): IndentedTreeOptions => { let options: IndentedTreeOptions = {}; const minWidth = nodeMinWidth || 0; const maxWidth = nodeMaxWidth || 300; @@ -91,14 +90,16 @@ export const getIndentedTreeOptions = ({ node: { style: { component: function (data: NodeData) { - const depth = data.depth as number; + const depth = data.depth; const color = data.style?.color as string; + const label = formatLabel(data, labelField); + const { font } = getBoxedTextNodeStyle(label, minWidth, maxWidth, depth); const props: TextNodeProps = { type: depth === 0 || depth === 1 ? 'filled' : 'outlined', - text: idOf(data), + text: label, color: depth === 0 ? '#f1f4f5' : color, maxWidth, - font: getBoxedTextNodeStyle(idOf(data), minWidth, maxWidth, depth).font, + font, style: { textAlign: getNodeTextAlign(this as unknown as Graph, data), ...(depth === 0 ? { color: '#252525' } : {}), @@ -106,13 +107,17 @@ export const getIndentedTreeOptions = ({ }; return ; }, - size: (data: NodeData) => getBoxedTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size, + size: (data: NodeData) => { + const label = formatLabel(data, labelField); + return getBoxedTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + }, }, }, edge: { style: { stroke: function (data) { - return (this.getNodeData(data.source).style!.color as string) || '#99ADD1'; + const source = this.getNodeData(data.source); + return get(source, 'style.color', '#99ADD1') as string; }, radius: 16, }, @@ -126,10 +131,16 @@ export const getIndentedTreeOptions = ({ ], layout: { type: 'indented', - indent: (node) => getIndent(node, 20), - getWidth: (data) => getBoxedTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size[0], - getHeight: (data) => getBoxedTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size[1], - getVGap: () => 14, + getWidth: (data) => { + const label = formatLabel(data, labelField); + const [width] = getBoxedTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return width; + }, + getHeight: (data) => { + const label = formatLabel(data, labelField); + const [, height] = getBoxedTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return height; + }, }, }; } else if (type === 'linear') { @@ -137,14 +148,11 @@ export const getIndentedTreeOptions = ({ node: { style: { component: function (data: NodeData) { - const depth = data.depth as number; + const depth = data.depth; const color = data.style?.color as string; - const props = { - text: idOf(data), - color, - maxWidth, - font: getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, depth).font, - } as TextNodeProps; + const label = formatLabel(data, labelField); + const { font } = getLinearTextNodeStyle(label, minWidth, maxWidth, depth); + const props = { text: label, color, maxWidth, font } as TextNodeProps; Object.assign( props, depth === 0 @@ -156,7 +164,10 @@ export const getIndentedTreeOptions = ({ ); return ; }, - size: (data: NodeData) => getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size, + size: (data: NodeData) => { + const label = formatLabel(data, labelField); + return getLinearTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + }, ports: function (data: NodeData) { const side = getNodeSide(this as unknown as Graph, data); return side === 'left' @@ -170,16 +181,24 @@ export const getIndentedTreeOptions = ({ edge: { style: { stroke: function (data) { - return (this.getNodeData(data.target).style!.color as string) || '#99ADD1'; + const target = this.getNodeData(data.target); + return get(target, 'style.color', '#99ADD1') as string; }, radius: 24, }, }, layout: { type: 'indented', - indent: (node) => getIndent(node, 20), - getWidth: (data) => getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size[0], - getHeight: (data) => getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size[1], + getWidth: (data) => { + const label = formatLabel(data, labelField); + const [width] = getLinearTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return width; + }, + getHeight: (data) => { + const label = formatLabel(data, labelField); + const [, height] = getLinearTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return height; + }, getVGap: () => 12, }, transforms: (prev) => [ @@ -194,6 +213,36 @@ export const getIndentedTreeOptions = ({ }, ], }; + } else { + const PADDING = [24, 16]; + + options = { + node: { + style: { + component: (data) => { + const label = formatLabel(data, labelField); + return ; + }, + size: (data) => { + const label = formatLabel(data, labelField); + return measureTextSize(label, PADDING); + }, + }, + }, + layout: { + type: 'indented', + getWidth: (data) => { + const label = formatLabel(data, labelField); + const [width] = measureTextSize(label, PADDING); + return width; + }, + getHeight: (data) => { + const label = formatLabel(data, labelField); + const [, height] = measureTextSize(label, PADDING); + return height; + }, + }, + }; } if (direction) { diff --git a/packages/graphs/src/components/indented-tree/types.ts b/packages/graphs/src/components/indented-tree/types.ts index 6fb804016..5bb356328 100644 --- a/packages/graphs/src/components/indented-tree/types.ts +++ b/packages/graphs/src/components/indented-tree/types.ts @@ -1,3 +1,4 @@ +import type { NodeData } from '@antv/g6'; import type { GraphOptions } from '../../types'; export interface IndentedTreeOptions extends GraphOptions { @@ -21,4 +22,11 @@ export interface IndentedTreeOptions extends GraphOptions { * @default 300 */ nodeMaxWidth?: number; + /** + * Selects a field from the data to use as the label for the node. + * If a string is provided, it will select the field as `data[labelField]`. + * If a function is provided, it will call the function with the data and use the returned value. + * @default (data) => data.id + */ + labelField?: string | ((data: NodeData) => string); } diff --git a/packages/graphs/src/components/mind-map/index.tsx b/packages/graphs/src/components/mind-map/index.tsx index 67bab1b2f..ed9f43b92 100644 --- a/packages/graphs/src/components/mind-map/index.tsx +++ b/packages/graphs/src/components/mind-map/index.tsx @@ -17,11 +17,11 @@ export const MindMap: ForwardRefExoticComponent< PropsWithoutRef> & RefAttributes > = forwardRef>(({ children, ...props }, ref) => { const options = useMemo(() => { - const { type = 'default', nodeMinWidth, nodeMaxWidth, direction = 'alternate', ...restProps } = props; + const { type = 'default', nodeMinWidth, nodeMaxWidth, direction = 'alternate', labelField, ...restProps } = props; const options = mergeOptions( COMMON_OPTIONS, DEFAULT_OPTIONS, - getMindMapOptions({ type, nodeMinWidth, nodeMaxWidth, direction }), + getMindMapOptions({ type, nodeMinWidth, nodeMaxWidth, direction, labelField }), restProps, ); return options; diff --git a/packages/graphs/src/components/mind-map/options.tsx b/packages/graphs/src/components/mind-map/options.tsx index d8664e755..ff8fcb430 100644 --- a/packages/graphs/src/components/mind-map/options.tsx +++ b/packages/graphs/src/components/mind-map/options.tsx @@ -1,8 +1,9 @@ import type { Graph, NodeData, SingleLayoutOptions } from '@antv/g6'; -import { idOf } from '@antv/g6'; +import { get } from 'lodash'; import React from 'react'; import type { TextNodeProps } from '../../core/base'; import { CollapseExpandIcon, RCNode } from '../../core/base'; +import { formatLabel } from '../../core/utils/label'; import { measureTextSize } from '../../core/utils/measure-text'; import { getNodeSide } from '../../core/utils/node'; import { getBoxedTextNodeStyle, getLinearTextNodeStyle } from '../../core/utils/tree'; @@ -14,16 +15,6 @@ const { TextNode } = RCNode; export const DEFAULT_OPTIONS: MindMapOptions = { node: { type: 'react', - style: { - component: (data) => , - size: (data) => measureTextSize(idOf(data), [24, 16]), - dx: function (data: NodeData) { - const side = getNodeSide(this as unknown as Graph, data); - const size = measureTextSize(idOf(data), [24, 16]); - return side === 'left' ? -size[0] : side === 'center' ? -size[0] / 2 : 0; - }, - ports: [{ placement: 'left' }, { placement: 'right' }], - }, state: { active: { halo: false, @@ -66,8 +57,7 @@ export const DEFAULT_OPTIONS: MindMapOptions = { layout: { type: 'mindmap', direction: 'H', - getWidth: (data) => 120, - getHeight: (data) => measureTextSize(data.id, [24, 16])[1], + getWidth: () => 120, getHGap: () => 64, }, animation: { @@ -80,8 +70,10 @@ export function getMindMapOptions({ direction, nodeMinWidth, nodeMaxWidth, -}: Pick): MindMapOptions { + labelField, +}: Pick): MindMapOptions { let options: MindMapOptions = {}; + if (type === 'boxed') { const minWidth = nodeMinWidth || 120; const maxWidth = nodeMaxWidth || 300; @@ -90,14 +82,11 @@ export function getMindMapOptions({ node: { style: { component: (data: NodeData) => { - const depth = data.depth as number; - const color = data.style?.color as string; - const props = { - text: idOf(data), - color, - maxWidth, - font: getBoxedTextNodeStyle(idOf(data), minWidth, maxWidth, depth).font, - } as TextNodeProps; + const depth = data.depth; + const color = data.style?.color; + const label = formatLabel(data, labelField); + const { font } = getBoxedTextNodeStyle(label, minWidth, maxWidth, depth); + const props = { text: label, color, maxWidth, font } as TextNodeProps; Object.assign( props, depth === 0 @@ -108,11 +97,15 @@ export function getMindMapOptions({ ); return ; }, - size: (data: NodeData) => getBoxedTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size, + size: (data: NodeData) => { + const label = formatLabel(data, labelField); + return getBoxedTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + }, dx: function (data: NodeData) { const side = getNodeSide(this as unknown as Graph, data); - const size = getBoxedTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size; - return side === 'left' ? -size[0] : side === 'center' ? -size[0] / 2 : 0; + const label = formatLabel(data, labelField); + const [width] = getBoxedTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return side === 'left' ? -width : side === 'center' ? -width / 2 : 0; }, ports: [{ placement: 'left' }, { placement: 'right' }], }, @@ -120,20 +113,19 @@ export function getMindMapOptions({ edge: { style: { stroke: function (data) { - return (this.getNodeData(data.source).style!.color as string) || '#99ADD1'; + const source = this.getNodeData(data.source); + return get(source, 'style.color', '#99ADD1') as string; }, }, }, - transforms: (prev) => [ - ...prev, - { - type: 'assign-color-by-branch', - key: 'assign-color-by-branch', - }, - ], + transforms: (prev) => [...prev, { type: 'assign-color-by-branch', key: 'assign-color-by-branch' }], layout: { type: 'mindmap', - getHeight: (data) => getBoxedTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size[1], + getHeight: (data) => { + const label = formatLabel(data, labelField); + const [, height] = getBoxedTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return height; + }, getVGap: () => 14, }, }; @@ -146,10 +138,11 @@ export function getMindMapOptions({ style: { component: function (data: NodeData) { const side = getNodeSide(this as unknown as Graph, data); - const depth = data.depth as number; - const color = data.style?.color as string; - const { font } = getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, depth); - const props = { text: idOf(data), color, maxWidth, font } as TextNodeProps; + const depth = data.depth; + const color = data.style?.color; + const label = formatLabel(data, labelField); + const { font } = getLinearTextNodeStyle(label, minWidth, maxWidth, depth); + const props = { text: label, color, maxWidth, font } as TextNodeProps; Object.assign( props, depth === 0 @@ -161,15 +154,20 @@ export function getMindMapOptions({ ); return ; }, - size: (data: NodeData) => getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size, + size: (data: NodeData) => { + const label = formatLabel(data, labelField); + return getLinearTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + }, dx: function (data: NodeData) { const side = getNodeSide(this as unknown as Graph, data); - const size = getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size; - return side === 'left' ? -size[0] : side === 'center' ? -size[0] / 2 : 0; + const label = formatLabel(data, labelField); + const [width] = getLinearTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return side === 'left' ? -width : side === 'center' ? -width / 2 : 0; }, dy: function (data: NodeData) { - const size = getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size; - return size[1] / 2; + const label = formatLabel(data, labelField); + const [, height] = getLinearTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return height / 2; }, ports: function (data: NodeData) { const side = getNodeSide(this as unknown as Graph, data); @@ -182,13 +180,18 @@ export function getMindMapOptions({ edge: { style: { stroke: function (data) { - return (this.getNodeData(data.target).style!.color as string) || '#99ADD1'; + const target = this.getNodeData(data.target); + return get(target, 'style.color', '#99ADD1') as string; }, }, }, layout: { type: 'mindmap', - getHeight: (data) => getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size[1], + getHeight: (data) => { + const label = formatLabel(data, labelField); + const [, height] = getLinearTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return height; + }, getVGap: () => 12, }, transforms: (prev) => [ @@ -201,12 +204,44 @@ export function getMindMapOptions({ ...(prev.find((t) => (t as any).key === 'collapse-expand-react-node') as any), iconOffsetY: (data) => { if (data.depth === 0) return 0; - const size = getLinearTextNodeStyle(idOf(data), minWidth, maxWidth, data.depth as number).size; - return size[1] / 2; + const label = formatLabel(data, labelField); + const [, height] = getLinearTextNodeStyle(label, minWidth, maxWidth, data.depth).size; + return height / 2; }, }, ], }; + } else { + const PADDING = [24, 16]; + options = { + node: { + style: { + component: (data) => { + const label = formatLabel(data, labelField); + return ; + }, + size: (data) => { + const label = formatLabel(data, labelField); + return measureTextSize(label, PADDING); + }, + dx: function (data: NodeData) { + const side = getNodeSide(this as unknown as Graph, data); + const label = formatLabel(data, labelField); + const [width] = measureTextSize(label, PADDING); + return side === 'left' ? -width : side === 'center' ? -width / 2 : 0; + }, + ports: [{ placement: 'left' }, { placement: 'right' }], + }, + }, + layout: { + type: 'mindmap', + getHeight: (data) => { + const label = formatLabel(data, labelField); + const [, height] = measureTextSize(label, PADDING); + return height; + }, + }, + }; } if (direction) { diff --git a/packages/graphs/src/components/mind-map/types.ts b/packages/graphs/src/components/mind-map/types.ts index 94cb9478f..893ef397c 100644 --- a/packages/graphs/src/components/mind-map/types.ts +++ b/packages/graphs/src/components/mind-map/types.ts @@ -1,3 +1,4 @@ +import type { NodeData } from '@antv/g6'; import type { GraphOptions } from '../../types'; export interface MindMapOptions extends GraphOptions { @@ -21,4 +22,11 @@ export interface MindMapOptions extends GraphOptions { * @default 300 */ nodeMaxWidth?: number; + /** + * Selects a field from the data to use as the label for the node. + * If a string is provided, it will select the field as `data[labelField]`. + * If a function is provided, it will call the function with the data and use the returned value. + * @default (data) => data.id + */ + labelField?: string | ((data: NodeData) => string); } diff --git a/packages/graphs/src/core/base/collapse-expand-icon/arrow-count-icon.tsx b/packages/graphs/src/core/base/collapse-expand-icon/arrow-count-icon.tsx index 97352dcbd..61081c95a 100644 --- a/packages/graphs/src/core/base/collapse-expand-icon/arrow-count-icon.tsx +++ b/packages/graphs/src/core/base/collapse-expand-icon/arrow-count-icon.tsx @@ -46,7 +46,7 @@ const StyledWrapper = styled.div<{ .indented-icon-bar { ${({ $placement }) => { const isVertical = $placement === 'top' || $placement === 'bottom'; - return isVertical ? 'width: 2px; height: 8px; margin: 0 7px;' : 'width: 8px; height: 2px; margin: 7px 0;'; + return isVertical ? 'width: 3px; height: 8px; margin: 0 7px;' : 'width: 8px; height: 3px; margin: 7px 0;'; }} background-color: ${({ $color }) => $color}; } @@ -104,9 +104,9 @@ export const ArrowCountIcon: FC = (props) => {
diff --git a/packages/graphs/src/core/base/node/organization-chart-node.tsx b/packages/graphs/src/core/base/node/organization-chart-node.tsx index 1886ea013..85e808ece 100644 --- a/packages/graphs/src/core/base/node/organization-chart-node.tsx +++ b/packages/graphs/src/core/base/node/organization-chart-node.tsx @@ -23,7 +23,7 @@ export interface OrganizationChartNodeProps extends Pick` height: inherit; width: inherit; - border-radius: 4px; + border-radius: 8px; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.1); position: relative; border: none; @@ -34,14 +34,14 @@ const StyledWrapper = styled.div<{ $color?: string; $isActive?: boolean }>` props.$isActive && css` transform: translate(-3px, -3px); - border: 3px solid #1783ff; + border: 2px solid #1783ff; `} .org-chart-node-line { width: 100%; height: 6px; background-color: ${(props) => props.$color}; - border-radius: 2px 2px 0 0; + border-radius: 8px 8px 0 0; } .org-chart-node-content { diff --git a/packages/graphs/src/core/base/node/text-node.tsx b/packages/graphs/src/core/base/node/text-node.tsx index 3f7be30a7..757182462 100644 --- a/packages/graphs/src/core/base/node/text-node.tsx +++ b/packages/graphs/src/core/base/node/text-node.tsx @@ -133,7 +133,7 @@ export const TextNode: FC = (props) => { $borderWidth={borderWidth} $isActive={isActive} $isSelected={isSelected} - className={`text-node ${className}`} + className={`text-node text-node-${type} ${className || ''}`} style={{ ...style, ...font }} >
{text}
diff --git a/packages/graphs/src/core/utils/label.ts b/packages/graphs/src/core/utils/label.ts new file mode 100644 index 000000000..996831e9a --- /dev/null +++ b/packages/graphs/src/core/utils/label.ts @@ -0,0 +1,11 @@ +import type { NodeData } from '@antv/g6'; +import { get } from 'lodash'; + +export function formatLabel(datum: NodeData, labelField?: string | ((datum: NodeData) => string)): string { + const label = labelField + ? typeof labelField === 'function' + ? labelField(datum) + : get(datum, `data.${labelField}`, datum.id) + : datum.id; + return String(label); +} diff --git a/packages/graphs/src/core/utils/measure-text.ts b/packages/graphs/src/core/utils/measure-text.ts index 2fef27348..c352e66da 100644 --- a/packages/graphs/src/core/utils/measure-text.ts +++ b/packages/graphs/src/core/utils/measure-text.ts @@ -1,5 +1,4 @@ import { measureTextHeight, measureTextWidth } from '@ant-design/charts-util'; -import type { Size } from '@antv/g6'; /** * 计算文本尺寸 @@ -16,7 +15,7 @@ export function measureTextSize( font: any = { fontSize: 16, fontFamily: 'PingFang SC' }, minWidth = 0, maxWith = Infinity, -): Size { +): [number, number] { const height = measureTextHeight(text, font); const width = measureTextWidth(text, font) + 4; diff --git a/packages/graphs/tests/demos/mind-map-linear.tsx b/packages/graphs/tests/demos/mind-map-linear.tsx index 4206b87bf..8afd0f38b 100644 --- a/packages/graphs/tests/demos/mind-map-linear.tsx +++ b/packages/graphs/tests/demos/mind-map-linear.tsx @@ -10,6 +10,13 @@ export const MindMapLinear = () => { autoFit: 'view', type: 'linear', data: treeToGraphData(data), + transforms: (transforms) => [ + ...transforms.filter((transform) => (transform as any).key !== 'collapse-expand-react-node'), + { + ...(transforms.find((transform) => (transform as any).key === 'collapse-expand-react-node') || ({} as any)), + enable: true, + }, + ], }; return ;