From 8d679afb654c792312ec0988e9e26b4bc6b49609 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 11 Dec 2024 11:38:10 +0800 Subject: [PATCH] feat: support labelField in MindMap --- .../graphs/src/components/mind-map/index.tsx | 4 +- .../src/components/mind-map/options.tsx | 137 ++++++++++++------ .../graphs/src/components/mind-map/types.ts | 8 + .../collapse-expand-icon/arrow-count-icon.tsx | 6 +- .../graphs/src/core/base/node/text-node.tsx | 2 +- .../graphs/src/core/utils/measure-text.ts | 3 +- .../graphs/tests/demos/mind-map-linear.tsx | 7 + 7 files changed, 112 insertions(+), 55 deletions(-) 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..bc16c5ce4 100644 --- a/packages/graphs/src/components/mind-map/options.tsx +++ b/packages/graphs/src/components/mind-map/options.tsx @@ -1,5 +1,5 @@ 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'; @@ -14,16 +14,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 +56,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: { @@ -75,13 +64,24 @@ export const DEFAULT_OPTIONS: MindMapOptions = { }, }; +function formatLabel(datum: NodeData, labelField: MindMapOptions['labelField']): string { + const label = labelField + ? typeof labelField === 'function' + ? labelField(datum) + : get(datum, `data.${labelField}`, datum.id) + : datum.id; + return String(label); +} + export function getMindMapOptions({ type, direction, nodeMinWidth, nodeMaxWidth, -}: Pick): MindMapOptions { + labelField, +}: Pick): MindMapOptions { let options: MindMapOptions = {}; + if (type === 'boxed') { const minWidth = nodeMinWidth || 120; const maxWidth = nodeMaxWidth || 300; @@ -90,14 +90,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 +105,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 +121,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 +146,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 +162,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 +188,18 @@ export function getMindMapOptions({ edge: { style: { stroke: function (data) { - return (this.getNodeData(data.target).style!.color as string) || '#99ADD1'; + const source = this.getNodeData(data.target); + return get(source, '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 +212,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/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/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 ;