Skip to content

Commit

Permalink
feat: support labelField in MindMap
Browse files Browse the repository at this point in the history
  • Loading branch information
yvonneyx committed Dec 11, 2024
1 parent 936e409 commit 8d679af
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 55 deletions.
4 changes: 2 additions & 2 deletions packages/graphs/src/components/mind-map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export const MindMap: ForwardRefExoticComponent<
PropsWithoutRef<PropsWithChildren<MindMapOptions>> & RefAttributes<Graph>
> = forwardRef<Graph, PropsWithChildren<MindMapOptions>>(({ 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;
Expand Down
137 changes: 90 additions & 47 deletions packages/graphs/src/components/mind-map/options.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,16 +14,6 @@ const { TextNode } = RCNode;
export const DEFAULT_OPTIONS: MindMapOptions = {
node: {
type: 'react',
style: {
component: (data) => <TextNode type="filled" text={idOf(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,
Expand Down Expand Up @@ -66,22 +56,32 @@ 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: {
duration: 500,
},
};

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, 'type' | 'nodeMaxWidth' | 'nodeMinWidth' | 'direction'>): MindMapOptions {
labelField,
}: Pick<MindMapOptions, 'type' | 'nodeMaxWidth' | 'nodeMinWidth' | 'direction' | 'labelField'>): MindMapOptions {
let options: MindMapOptions = {};

if (type === 'boxed') {
const minWidth = nodeMinWidth || 120;
const maxWidth = nodeMaxWidth || 300;
Expand All @@ -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
Expand All @@ -108,32 +105,35 @@ export function getMindMapOptions({
);
return <TextNode {...props} />;
},
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' }],
},
},
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,
},
};
Expand All @@ -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
Expand All @@ -161,15 +162,20 @@ export function getMindMapOptions({
);
return <TextNode {...props} />;
},
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);
Expand All @@ -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) => [
Expand All @@ -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 <TextNode type="filled" text={label} />;
},
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) {
Expand Down
8 changes: 8 additions & 0 deletions packages/graphs/src/components/mind-map/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { NodeData } from '@antv/g6';
import type { GraphOptions } from '../../types';

export interface MindMapOptions extends GraphOptions {
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}
Expand Down Expand Up @@ -104,9 +104,9 @@ export const ArrowCountIcon: FC<ArrowCountIconProps> = (props) => {
<div className="indented-icon-circle-arrow">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M11,4 L5,8 L11,12"
d="M11,4 L5.5,8 L11,12"
stroke="white"
strokeWidth="2"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
></path>
Expand Down
2 changes: 1 addition & 1 deletion packages/graphs/src/core/base/node/text-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export const TextNode: FC<TextNodeProps> = (props) => {
$borderWidth={borderWidth}
$isActive={isActive}
$isSelected={isSelected}
className={`text-node ${className}`}
className={`text-node text-node-${type} ${className || ''}`}
style={{ ...style, ...font }}
>
<div style={isMultiLine ? { width: 'calc(100% - 12px)' } : {}}>{text}</div>
Expand Down
3 changes: 1 addition & 2 deletions packages/graphs/src/core/utils/measure-text.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { measureTextHeight, measureTextWidth } from '@ant-design/charts-util';
import type { Size } from '@antv/g6';

/**
* 计算文本尺寸
Expand All @@ -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;

Expand Down
7 changes: 7 additions & 0 deletions packages/graphs/tests/demos/mind-map-linear.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <MindMapComponent {...options} />;
Expand Down

0 comments on commit 8d679af

Please sign in to comment.