Skip to content

Commit

Permalink
docs: add fishbone diagram demo (#6415)
Browse files Browse the repository at this point in the history
  • Loading branch information
yvonneyx authored Oct 17, 2024
1 parent fb22f3a commit 0ab5f8d
Show file tree
Hide file tree
Showing 4 changed files with 391 additions and 0 deletions.
193 changes: 193 additions & 0 deletions packages/g6/__tests__/demos/case-fishbone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import type { TextStyleProps } from '@antv/g';
import { Text } from '@antv/g';
import type { EdgeData, GraphData, NodeData } from '@antv/g6';
import { BaseLayout, ExtensionCategory, Graph, register, treeToGraphData } from '@antv/g6';

const data = {
id: '克服拖延',
children: [
{ id: '完美主义情结', children: [{ id: '正确评估事情难度' }, { id: '先完成,再完善' }, { id: 'Just do it' }] },
{
id: '提高专注度',
children: [{ id: '番茄工作法' }, { id: '限时、限量,一次只做一件事' }, { id: '提高抗干扰能力,减少打断' }],
},
{
id: '设定清晰的任务管理流程',
children: [
{ id: '设立完成事项的优先级' },
{ id: '拆解具体可执行的目标' },
{ id: '收集-整理-排序-执行反馈-总结' },
],
},
{
id: '建立积极反馈',
children: [{ id: '做喜欢的事情' }, { id: '精神激励' }, { id: '物质激励' }],
},
{
id: '放松、享受',
children: [{ id: '注重过程而非结果' }, { id: '靠需求驱动而非焦虑' }, { id: '接受、理解' }],
},
],
};

const palette = [
'#1783FF',
'#00C9C9',
'#F08F56',
'#D580FF',
'#7863FF',
'#DB9D0D',
'#60C42D',
'#FF80CA',
'#2491B3',
'#17C76F',
];

export const caseFishbone: TestCase = async (context) => {
const assignElementStyle = (element: any, style: any) => {
return { ...element, style: { ...(element.style || {}), ...style } };
};

class FishboneLayout extends BaseLayout {
id = 'fishbone';

async execute(data: GraphData, options: any): Promise<GraphData> {
const { rankSep = 30, branchSep = 30, nodeSep = 48, size = 32 } = { ...this.options, ...options };

const { model } = this.context;
const root = model.getRootsData()[0];
Object.assign(root.style || {}, { x: 0, y: 0 });
const rootSize = typeof size === 'function' ? size(root) : size;

const nodes: NodeData[] = [root];
const edges: EdgeData[] = [];

let branchStartX = rootSize[0] / 2 + branchSep;
let leafNodeMaxX = branchStartX;

const findEdgeByTarget = (target: string) => (data.edges || []).find((edge) => edge.target === target)!;

(data.nodes || [])
.filter((node: NodeData) => node.depth === 1)
.forEach((node: NodeData, i: number) => {
const nodeSize = typeof size === 'function' ? size(node) : size;

const leafNodeIds = node.children || [];
const isUpper = i % 2 === 0;
const sign = isUpper ? 1 : -1;

leafNodeIds.forEach((leafNodeId: string, j: number) => {
const order = j + 1;
const leafNode = model.getNodeLikeDatum(leafNodeId);
const leafNodeSize = typeof size === 'function' ? size(leafNode) : size;

const x = branchStartX + rankSep * (order + 1) + leafNodeSize[0] / 2;
const y = sign * nodeSep * order;
nodes.push(assignElementStyle(leafNode, { x, y }) as NodeData);

leafNodeMaxX = Math.max(leafNodeMaxX, x + leafNodeSize[0] / 2);
const edge = findEdgeByTarget(leafNodeId);
edges.push(
assignElementStyle(edge, {
stroke: palette[i % palette.length],
controlPoints: [[branchStartX + rankSep * order, y]],
zIndex: -i,
}) as EdgeData,
);
});
nodes.push(
assignElementStyle(node, {
x: branchStartX + rankSep * (leafNodeIds.length + 1),
y: sign * (nodeSep * (leafNodeIds.length + 1) + nodeSize[1] / 2),
fill: palette[i % palette.length],
}) as NodeData,
);
const edge = findEdgeByTarget(node.id);
edges.push(
assignElementStyle(edge, {
stroke: palette[i % palette.length],
controlPoints: [[branchStartX, 0]],
zIndex: -i,
}) as EdgeData,
);
branchStartX = (isUpper ? branchStartX : leafNodeMaxX) + branchSep;
});

return { nodes, edges };
}
}

register(ExtensionCategory.LAYOUT, 'fishbone', FishboneLayout);

let textShape: Text | null;
const measureText = (style: TextStyleProps) => {
if (!textShape) textShape = new Text({ style });
textShape.attr(style);
return textShape.getBBox().width;
};

const getNodeSize = (id: string, depth: number) => {
return depth === 0
? [measureText({ text: id, fontSize: 18, fontWeight: 'bold', fontFamily: 'system-ui, sans-serif' }) + 60, 42]
: depth === 1
? [measureText({ text: id, fontSize: 16, fontFamily: 'system-ui, sans-serif' }) + 50, 36]
: [measureText({ text: id, fontSize: 12, fontFamily: 'system-ui, sans-serif' }) + 16, 30];
};

const graph = new Graph({
...context,
autoFit: 'view',
padding: 20,
data: treeToGraphData(data),
node: {
type: 'rect',
style: (d) => {
const style = {
radius: 8,
size: getNodeSize(d.id, d.depth!),
labelText: d.id,
labelPlacement: 'center',
};

if (d.depth === 0) {
Object.assign(style, {
fill: '#EFF0F0',
labelFill: '#262626',
labelFontWeight: 'bold',
labelFontSize: 18,
labelOffsetY: 4,
ports: [{ placement: 'right' }],
});
} else if (d.depth === 1) {
Object.assign(style, {
ports: [{ placement: 'bottom' }, { placement: 'top' }],
labelFontSize: 14,
labelFill: '#fff',
labelOffsetY: 2,
});
} else {
Object.assign(style, {
fill: 'transparent',
ports: [{ placement: 'left' }],
labeFill: '#262626',
});
}
return style;
},
},
edge: {
type: 'polyline',
style: { lineWidth: 2 },
},
layout: {
type: 'fishbone',
size: (d: NodeData) => getNodeSize(d.id, d.depth!),
},
behaviors: ['zoom-canvas', 'drag-canvas'],
animation: false,
});

await graph.render();

return graph;
};
1 change: 1 addition & 0 deletions packages/g6/__tests__/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { behaviorScrollCanvas } from './behavior-scroll-canvas';
export { behaviorZoomCanvas } from './behavior-zoom-canvas';
export { bugTooltipResize } from './bug-tooltip-resize';
export { canvasCursor } from './canvas-cursor';
export { caseFishbone } from './case-fishbone';
export { caseFundFlow } from './case-fund-flow';
export { caseIndentedTree } from './case-indented-tree';
export { caseLanguageTree } from './case-language-tree';
Expand Down
189 changes: 189 additions & 0 deletions packages/site/examples/scene-case/default/demo/fishbone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@

import { Text } from '@antv/g';
import { BaseLayout, ExtensionCategory, Graph, register, treeToGraphData } from '@antv/g6';

const data = {
id: '克服拖延',
children: [
{ id: '完美主义情结', children: [{ id: '正确评估事情难度' }, { id: '先完成,再完善' }, { id: 'Just do it' }] },
{
id: '提高专注度',
children: [{ id: '番茄工作法' }, { id: '限时、限量,一次只做一件事' }, { id: '提高抗干扰能力,减少打断' }],
},
{
id: '设定清晰的任务管理流程',
children: [
{ id: '设立完成事项的优先级' },
{ id: '拆解具体可执行的目标' },
{ id: '收集-整理-排序-执行反馈-总结' },
],
},
{
id: '建立积极反馈',
children: [{ id: '做喜欢的事情' }, { id: '精神激励' }, { id: '物质激励' }],
},
{
id: '放松、享受',
children: [{ id: '注重过程而非结果' }, { id: '靠需求驱动而非焦虑' }, { id: '接受、理解' }],
},
],
};

const palette = [
'#1783FF',
'#00C9C9',
'#F08F56',
'#D580FF',
'#7863FF',
'#DB9D0D',
'#60C42D',
'#FF80CA',
'#2491B3',
'#17C76F',
];

const assignElementStyle = (element, style) => {
return { ...element, style: { ...(element.style || {}), ...style } };
};

class FishboneLayout extends BaseLayout {
id = 'fishbone';

async execute(data, options) {
const { rankSep = 30, branchSep = 30, nodeSep = 48, size = 32 } = { ...this.options, ...options };

const { model } = this.context;
const root = model.getRootsData()[0];
Object.assign(root.style || {}, { x: 0, y: 0 });
const rootSize = typeof size === 'function' ? size(root) : size;

const nodes = [root];
const edges = [];

let branchStartX = rootSize[0] / 2 + branchSep;
let leafNodeMaxX = branchStartX;

const findEdgeByTarget = (target) => (data.edges || []).find((edge) => edge.target === target);

(data.nodes || [])
.filter((node) => node.depth === 1)
.forEach((node, i) => {
const nodeSize = typeof size === 'function' ? size(node) : size;

const leafNodeIds = node.children || [];
const isUpper = i % 2 === 0;
const sign = isUpper ? 1 : -1;

leafNodeIds.forEach((leafNodeId, j) => {
const order = j + 1;
const leafNode = model.getNodeLikeDatum(leafNodeId);
const leafNodeSize = typeof size === 'function' ? size(leafNode) : size;

const x = branchStartX + rankSep * (order + 1) + leafNodeSize[0] / 2;
const y = sign * nodeSep * order;
nodes.push(assignElementStyle(leafNode, { x, y }));

leafNodeMaxX = Math.max(leafNodeMaxX, x + leafNodeSize[0] / 2);
const edge = findEdgeByTarget(leafNodeId);
edges.push(
assignElementStyle(edge, {
stroke: palette[i % palette.length],
controlPoints: [[branchStartX + rankSep * order, y]],
zIndex: -i,
}),
);
});
nodes.push(
assignElementStyle(node, {
x: branchStartX + rankSep * (leafNodeIds.length + 1),
y: sign * (nodeSep * (leafNodeIds.length + 1) + nodeSize[1] / 2),
fill: palette[i % palette.length],
}),
);
const edge = findEdgeByTarget(node.id);
edges.push(
assignElementStyle(edge, {
stroke: palette[i % palette.length],
controlPoints: [[branchStartX, 0]],
zIndex: -i,
}),
);
branchStartX = (isUpper ? branchStartX : leafNodeMaxX) + branchSep;
});

return { nodes, edges };
}
}

register(ExtensionCategory.LAYOUT, 'fishbone', FishboneLayout);

let textShape;
const measureText = (style) => {
if (!textShape) textShape = new Text({ style });
textShape.attr(style);
return textShape.getBBox().width;
};

const getNodeSize = (id, depth) => {
return depth === 0
? [measureText({ text: id, fontSize: 18, fontWeight: 'bold', fontFamily: 'system-ui, sans-serif' }) + 60, 42]
: depth === 1
? [measureText({ text: id, fontSize: 16, fontFamily: 'system-ui, sans-serif' }) + 50, 36]
: [measureText({ text: id, fontSize: 12, fontFamily: 'system-ui, sans-serif' }) + 16, 30];
};

const graph = new Graph({
container: 'container',
autoFit: 'view',
padding: 20,
data: treeToGraphData(data),
node: {
type: 'rect',
style: (d) => {
const style = {
radius: 8,
size: getNodeSize(d.id, d.depth),
labelText: d.id,
labelPlacement: 'center',
labelFillOpacity: 1,
};

if (d.depth === 0) {
Object.assign(style, {
fill: '#EFF0F0',
labelFill: '#262626',
labelFontWeight: 'bold',
labelFontSize: 18,
labelOffsetY: 4,
ports: [{ placement: 'right' }],
});
} else if (d.depth === 1) {
Object.assign(style, {
ports: [{ placement: 'bottom' }, { placement: 'top' }],
labelFontSize: 14,
labelFill: '#fff',
labelOffsetY: 2,
});
} else {
Object.assign(style, {
fill: 'transparent',
ports: [{ placement: 'left' }],
labeFill: '#262626',
});
}
return style;
},
},
edge: {
type: 'polyline',
style: { lineWidth: 2 },
},
layout: {
type: 'fishbone',
size: (d) => getNodeSize(d.id, d.depth),
},
behaviors: ['zoom-canvas', 'drag-canvas'],
animation: false,
});

graph.render();
Loading

0 comments on commit 0ab5f8d

Please sign in to comment.