版本
d3: 6.3.1
react: 16.8.6
参考:observablehq.com/@nitaku/tan…
效果图
npm install d3
在需要用的组件页面引入d3
import * as d3 from 'd3';
初始化Simulation
componentDidMount() {
this.initSimulation();
}
initSimulation = () => {
const width = this.chartRef.current.clientWidth;
const height = this.chartRef.current.clientHeight;
// 初始化simulation,如果指定了 force 则表示为仿真添加指定 name 的 force(力学模型) 并返回仿真。
this.simulation = d3
.forceSimulation() // 这里可以指定被引用的nodes数组,如果没有指定 nodes 则默认为空数组
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collide', d3.forceCollide().radius(() => 60))
.force('yPos', d3.forceY().strength(1))
.force('xPos', d3.forceX().strength(1))
.force('charge', d3.forceManyBody().strength(-520));
// 绘制svg,并制定宽高
this.svg = d3
.select('#theChart')
.append('svg') // 在id为‘theChart’的标签内创建svg
.attr('width', width)
.attr('height', height * 0.9)
.call(this.zoom()); // 放大缩小
this.graph = this.svg.append('g');
};
render() {
return (
<div
className="theChart"
id="theChart"
ref={this.chartRef}
style={{ width: 600, height: 600 }}
></div>
);
}
获取到数据后往simulation里加入nodes
// 这里的levels的结构应该是
const levels = [ [{ id: 'Chaos' }], // 第一列数据
[{ id: 'Gaea', parents: ['Chaos'] }], // 第二列数据
[ { id: 'Oceanus', parents: ['Gaea'] },
{ id: 'Thethys', parents: ['Gaea'] },
{ id: 'Pontus' },
{ id: 'Rhea', parents: ['Gaea'] },
{ id: 'Cronus', parents: ['Gaea'] },
{ id: 'Coeus', parents: ['Gaea'] },
{ id: 'Phoebe', parents: ['Gaea'] },
{ id: 'Crius', parents: ['Gaea'] },
{ id: 'Hyperion', parents: ['Gaea'] },
{ id: 'Iapetus', parents: ['Gaea']' },
{ id: 'Thea', parents: ['Gaea'] },
{ id: 'Themis', parents: ['Gaea'] },
{ id: 'Mnemosyne', parents: ['Gaea'] }
] // 第三列数据
]
const data = this.tranferData(d3, levels);
this.simulation.nodes = data.nodes;
this.draw(data);
绘制节点连线
draw = data => {
// 绘制节点后面的连线
const color = d3.scaleOrdinal(d3.schemeDark2); // 添加颜色
data.bundles.map(b => {
const d = b.links
.map(
l => `
M${l.xt} ${l.yt}
L${l.xb - l.c1} ${l.yt}
A${l.c1} ${l.c1} 90 0 1 ${l.xb} ${l.yt + l.c1}
L${l.xb} ${l.ys - l.c2}
A${l.c2} ${l.c2} 90 0 0 ${l.xb + l.c2} ${l.ys}
L${l.xs} ${l.ys}`
)
.join('');
this.svg
.select('g')
.append('path')
.attr('class', 'topo-line')
.style('fill', 'none')
.attr('d', d)
.attr('stroke', 'white')
.attr('stroke-width', 5);
this.svg
.select('g')
.append('path')
.attr('class', 'topo-line'')
.style('fill', 'none')
.attr('d', d)
.attr('stroke', color(b.id)) // 连线颜色
.attr('stroke-width', 2);
});
// 绘制节点
data.nodes.map(n => {
// 绘制连线后的圆点
const line = this.svg
.select('g')
.append('line')
.style('stroke-linecap', 'round')
.attr('stroke-width', 8)
.attr('x1', n.x)
.attr('y1', n.y - n.height / 2)
.attr('x2', n.x)
.attr('stroke', color(n.id))
.attr('y2', n.y + n.height / 2);
// 控制圆点空心部分
this.svg
.select('g')
.append('line')
.style('stroke-linecap', 'round')
.attr('stroke', 'white')
.attr('stroke-width', 4)
.attr('x1', n.x)
.attr('y1', n.y - n.height / 2)
.attr('x2', n.x)
.attr('y2', n.y + n.height / 2);
// 绘制节点文本
this.svg
.select('g')
.append('text')
.style('font-size', '10px')
.attr('stroke', 'white')
.attr('stroke-width', 2)
.attr('x', n.x + 4)
.attr('y', n.y - n.height / 2 - 4)
.text(n.id);
this.svg
.select('g')
.append('text')
.attr('fill', 'black') // 文本颜色
.style('font-size', '10px')
.style('cursor', 'pointer')
.attr('x', n.x + 4)
.attr('y', n.y - n.height / 2 - 4)
.text(n.id)
.on('click', () => { // 文本点击事件
if (this.props.itemClick) {
this.props.itemClick(n.id);
}
});
});
};
放大缩小
zoom = () =>
d3
.zoom()
.scaleExtent([1 / 10, 10]) // 设置最大缩放比例
.on('start', this.onZoomStart)
.on('zoom', this.zooming)
.on('end', this.onZoomEnd);
onZoomStart = (event, d) => {
// console.log('start zoom');
};
zooming = (event, d) => {
// 缩放和拖拽整个g
this.graph.attr('transform', event.transform); // 获取g的缩放系数和平移的坐标值。
};
onZoomEnd = () => {
// console.log('zoom end');
};
拖拽
drag = () =>
d3
.drag()
.on('start', this.onDragStart)
.on('drag', this.dragging) // 拖拽过程
.on('end', this.onDragEnd);
onDragStart = (event, d) => {
if (!event.active) {
this.simulation
.alphaTarget(1) // 设置衰减系数,对节点位置移动过程的模拟,数值越高移动越快,数值范围[0,1]
.restart(); // 拖拽节点后,重新启动模拟
}
d.fx = d.x; // d.x是当前位置,d.fx是静止时位置
d.fy = d.y;
};
dragging = (event, d) => {
d.fx = event.x;
d.fy = event.y;
};
onDragEnd = (event, d) => {
if (!event.active) this.simulation.alphaTarget(0);
d.fx = null; // 解除dragged中固定的坐标
d.fy = null;
};
自定义连线后面的图标
- 定义图标,append到svg里。
// 这里绘制的是连线后面的叉号。
const defs = this.svg.append('defs');
const marker = defs
.append('marker')
.attr('id', 'close')
.attr('viewBox', '0 0 1024 1024')
.attr('markerWidth', '1.5')
.attr('markerHeight', '1.5')
.attr('refX', '512')
.attr('refY', '512')
.attr('orient', 'auto');
marker
.append('path')
.attr(
'd',
'M810.666667 273.493333L750.506667 213.333333 512 451.84 273.493333 213.333333 213.333333 273.493333 451.84 512 213.333333 750.506667 273.493333 810.666667 512 572.16 750.506667 810.666667 810.666667 750.506667 572.16 512z'
) //绘制叉号
.attr('fill', 'red'); // 添加颜色
- 使用,只需要添加marker-end属性
line.attr('marker-end', 'url(#close)');
tranferData
tranferData = (d3, data) => {
// precompute level depth
data.forEach((l, i) => l.forEach(n => (n.level = i)));
const nodes = data.reduce((a, x) => a.concat(x), []);
const nodes_index = {};
nodes.forEach(d => (nodes_index[d.id] = d));
// objectification
nodes.forEach(d => {
d.parents = (d.parents === undefined ? [] : d.parents).map(
p => nodes_index[p]
);
});
// precompute bundles
data.forEach((l, i) => {
const index = {};
l.forEach(n => {
if (n.parents.length === 0) {
return;
}
const id = n.parents
.map(d => d.id)
.sort()
.join('--');
if (id in index) {
index[id].parents = index[id].parents.concat(n.parents);
} else {
index[id] = { id, parents: n.parents.slice(), level: i };
}
n.bundle = index[id];
});
l.bundles = Object.keys(index).map(k => index[k]);
l.bundles.forEach((b, i) => (b.i = i));
});
const links = [];
nodes.forEach(d => {
d.parents.forEach(p =>
links.push({ source: d, bundle: d.bundle, target: p })
);
});
const bundles = data.reduce((a, x) => a.concat(x.bundles), []);
// reverse pointer from parent to bundles
bundles.forEach(b =>
b.parents.forEach(p => {
if (p.bundles_index === undefined) {
p.bundles_index = {};
}
if (!(b.id in p.bundles_index)) {
p.bundles_index[b.id] = [];
}
p.bundles_index[b.id].push(b);
})
);
nodes.forEach(n => {
if (n.bundles_index !== undefined) {
n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k]);
} else {
n.bundles_index = {};
n.bundles = [];
}
n.bundles.forEach((b, i) => (b.i = i));
});
links.forEach(l => {
if (l.bundle.links === undefined) {
l.bundle.links = [];
}
l.bundle.links.push(l);
});
// layout
const padding = 8;
const node_height = 22;
const node_width = 70;
const bundle_width = 40;
const level_y_padding = 16;
const metro_d = 4;
const c = 16;
const min_family_height = 16;
nodes.forEach(
n => (n.height = (Math.max(1, n.bundles.length) - 1) * metro_d)
);
let x_offset = padding;
let y_offset = padding;
data.forEach(l => {
x_offset += l.bundles.length * bundle_width;
y_offset += level_y_padding;
l.forEach((n, i) => {
n.x = n.level * node_width + x_offset;
n.y = node_height + y_offset + n.height / 2;
y_offset += node_height + n.height;
});
});
let i = 0;
data.forEach(l => {
l.bundles.forEach(b => {
b.x =
b.parents[0].x +
node_width +
(l.bundles.length - 1 - b.i) * bundle_width;
b.y = i * node_height;
});
i += l.length;
});
links.forEach(l => {
l.xt = l.target.x;
l.yt =
l.target.y +
l.target.bundles_index[l.bundle.id].i * metro_d -
(l.target.bundles.length * metro_d) / 2 +
metro_d / 2;
l.xb = l.bundle.x;
l.xs = l.source.x;
l.ys = l.source.y;
});
// compress vertical space
let y_negative_offset = 0;
data.forEach(l => {
y_negative_offset +=
-min_family_height +
d3.min(l.bundles, b =>
d3.min(b.links, link => link.ys - c - (link.yt + c))
) || 0;
l.forEach(n => (n.y -= y_negative_offset));
});
// very ugly, I know
links.forEach(l => {
l.yt =
l.target.y +
l.target.bundles_index[l.bundle.id].i * metro_d -
(l.target.bundles.length * metro_d) / 2 +
metro_d / 2;
l.ys = l.source.y;
l.c1 = l.source.level - l.target.level > 1 ? node_width + c : c;
l.c2 = c;
});
const layout = {
height: d3.max(nodes, n => n.y) + node_height / 2 + 2 * padding,
node_height,
node_width,
bundle_width,
level_y_padding,
metro_d
};
return { data, nodes, nodes_index, links, bundles, layout };
}