d3和dagre-d3版本
"dependencies": {
"d3": "^7.9.0",
"dagre-d3": "^0.6.4",
}
<template>
<div>
<div class="top-box">ssss</div>
<div>
<svg ref="svgRef" width="100%" height="600" class="svg-box"></svg>
<!-- Tooltip 元素 -->
<div ref="tooltipRef" class="dagre-tooltip"></div>
</div>
</div>
</template>
<script setup lang="ts">
import * as d3 from 'd3';
import * as dagreD3 from 'dagre-d3';
import { graphlib } from 'dagre-d3';
import { ref, onMounted } from 'vue';
const svgRef = ref(null);
const tooltipRef = ref();
// 定义图形配置类型
interface GraphConfig {
rankdir?: 'TB' | 'LR' | 'BT' | 'RL';
marginx?: number;
marginy?: number;
width?: number;
height?: number;
// 其他可能的属性
}
onMounted(() => {
renderGraph();
});
const renderGraph = () => {
// var g = new dagreD3.graphlib.Graph()
var g = new graphlib.Graph({
compound: true, //分组时候必须设置
})
.setGraph({})
.setDefaultEdgeLabel(function () {
return {};
});
// Here we're setting nodeclass, which is used by our custom drawNodes function
// below.
g.setNode('A', {
label: '<span style="color:red">A</span>',
labelType: 'html',
width: 20,
height: 20,
shape: 'house',
class: 'type-TOP',
labelStyle: 'fill:#000,font-weight: bold',
style: 'fill: #afa',
title: '啊啊啊啊啊啊',
});
g.setNode('B', {
label: 'B',
class: 'circle',
shape: 'circle',
style: ' stroke: #333; fill: #fff; stroke-width: 1.5px;',
});
g.setNode('C', { label: 'C', class: 'diamond', shape: 'diamond' });
g.setNode('D', { label: 'D', class: 'ellipse', shape: 'ellipse' });
g.setNode('E', { label: 'E', class: 'last-node', shape: 'rect' });
g.setNode('F', {
label: 'F',
shape: 'rect',
width: 50,
height: 50,
rx: 10, // 圆角半径
ry: 20,
});
g.setNode('G', { label: 'G', class: '' });
g.setNode('H', { label: 'H', class: 'last-node' });
g.setNode('I', { label: 'I', class: '' });
g.setNode('J', { label: 'J', class: '' });
g.setNode('K', { label: 'K', class: 'last-node' });
g.setNode('L', { label: 'L', class: '' });
g.setNode('M', { label: 'M', class: 'last-node' });
g.setNode('N', { label: 'N', class: '' });
g.setNode('O', { label: 'O', class: 'last-node' });
g.nodes().forEach(function (v) {
var node = g.node(v);
node.rx = node.ry = 5;
});
// Set up edges, no special attributes.
g.setEdge('A', 'B', {
label: 'a to b',
labelStyle: 'font-style: italic; text-decoration: underline;',
style: 'stroke: #f66; stroke-width: 1px; stroke-dasharray: 5, 5;',
arrowheadStyle: 'fill: #f66',
arrowheadClass: 'arrowhead',
curve: d3.curveBasis,
arrowhead: 'vee', //"normal", "vee", "undirected"
});
g.setNode('group', {
label: 'Group',
clusterLabelPos: 'top',
style: 'fill: #d3d7e8',
});
g.setNode('top_group', {
label: 'Top Group',
clusterLabelPos: 'top',
style: 'fill: #ffd47f',
});
g.setNode('bottom_group', {
label: 'Bottom Group',
style: 'fill: #5f9488',
clusterLabelPos: 'bottom',
});
g.setParent('top_group', 'group');
g.setParent('bottom_group', 'group');
g.setParent('B', 'top_group');
g.setParent('C', 'bottom_group');
g.setParent('F', 'bottom_group');
g.setParent('N', 'bottom_group');
g.setEdge('B', 'C', { curve: d3.curveBasis });
g.setEdge('C', 'D');
g.setEdge('D', 'E');
g.setEdge('B', 'F');
g.setEdge('F', 'G');
g.setEdge('G', 'H');
g.setEdge('F', 'I');
g.setEdge('I', 'J');
g.setEdge('J', 'K');
g.setEdge('I', 'L');
g.setEdge('L', 'M');
g.setEdge('B', 'N');
g.setEdge('N', 'O');
// 4. 渲染设置
const svg = d3.select(svgRef.value);
const inner = svg.append('g');
const render = new dagreD3.render();
// 自定义形状
render.shapes().house = function (parent, bbox, node) {
var w = bbox.width,
h = bbox.height,
points = [
{ x: 0, y: 0 },
{ x: w, y: 0 },
{ x: w, y: -h },
{ x: w / 2, y: (-h * 3) / 2 },
{ x: 0, y: -h },
];
const shapeSvg = parent
.insert('polygon', ':first-child')
.attr(
'points',
points
.map(function (d) {
return d.x + ',' + d.y;
})
.join(' ')
)
.attr('transform', 'translate(' + -w / 2 + ',' + (h * 3) / 4 + ')');
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
// 5. 执行渲染
render(inner, g as any);
// 6. 添加缩放和平移
const zoom = d3
.zoom()
.scaleExtent([0.5, 3])
.on('zoom', (event) => {
inner.attr('transform', event.transform);
}) as any;
svg.call(zoom);
// 7. 自动居中
const graphHeight = (g.graph() as GraphConfig)?.height || 0;
const graphWidth = (g.graph() as GraphConfig)?.width || 0;
const initialScale = 0.8;
const centerX = (svg.node().clientWidth - graphWidth * initialScale) / 2;
const centerY = (svg.node().clientHeight - graphHeight * initialScale) / 2;
svg.call(
zoom.transform,
d3.zoomIdentity.translate(centerX, centerY).scale(initialScale)
);
//显示tootip
const tooltip = d3.select(tooltipRef.value);
const svgNode = svg.node() as any;
const left = svgNode.getBoundingClientRect().left;
const top = svgNode.getBoundingClientRect().top;
const transform = d3.zoomTransform(svgNode);
inner
.selectAll('g.node')
.on('mouseover', (event: any, nodeId: string) => {
const node = g.node(nodeId);
console.log(node.x, left);
if (node.title) {
tooltip
.html(node.title)
.style('opacity', 1)
.style('left', `${node.x * transform.k + transform.x + left + 10}px`)
.style('top', `${node.y * transform.k + transform.y + top + 10}px`);
}
})
.on('mouseout', () => {
tooltip.style('opacity', 0);
});
// .on('mousemove', (event: MouseEvent) => {
// tooltip
// .style('left', `${event.pageX + 10}px`)
// .style('top', `${event.pageY + 10}px`);
// });
};
</script>
<style>
.svg-box {
border: 1px solid #f00;
}
/* This sets the color for "TK" nodes to a light blue green. */
g.last-node > rect {
fill: #00ffd0;
}
text {
font-weight: 300;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 14px;
}
.node rect,
.node circle,
.node polygon,
.node ellipse {
stroke: #999;
fill: #fff;
stroke-width: 1.5px;
}
.edgePath path {
stroke: #333;
stroke-width: 1.5px;
}
.arrowhead {
stroke: blue;
fill: blue;
stroke-width: 1.5px;
}
.dagre-tooltip {
position: fixed;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 4px;
pointer-events: none; /* 防止遮挡鼠标事件 */
opacity: 0;
transition: opacity 0.2s;
font-size: 14px;
z-index: 100;
}
.top-box {
height: 200px;
}
</style>