UNPKG

force-directed-graph-component

Version:

force directed graph component

366 lines (331 loc) 10.7 kB
import * as d3 from 'd3' import {transformColors} from './util/colors' // 获取d3提供的颜色目录 https://d3js.org/d3-scale-chromatic/categorical const defaultColors = d3.scaleOrdinal(d3.schemeSet2); let colors = { ...transformColors(defaultColors, 'group'), } // 默认样式 const defaultStyle = { nodeTextSize: 12, // 节点文字大小 nodeTextWeight: 'bold', // 节点文字加粗情况 nodeTextColor: 'blue', // 节点文字颜色 nodeTextDx: 0, // 节点文字x轴偏移量 nodeTextDy: 3, // 节点文字y轴偏移量 linkTextSize: 12, // 路径文字大小 linkTextColor: 'green', // 路径文字颜色 linkColor: '#999', // 路径颜色 linkWidth: 2, // 路径宽度 linkOpacity: 0.6, // 路径透明度 linkTextDx: 0, // 路径文字x轴偏移量 linkTextDy: 0, // 路径文字y轴偏移量 linkTextWeight: 'bold', // 路径文字加粗情况 markerColor: '#666', // marker颜色 markerWidth: 2, // marker宽度 markerHeight: 10, // marker高度 positiveMarkerPath: 'M0 -5 L 10 0 L 0 5', //positiveMarker路径 negativeMarkerPath: 'M10 -5 L 0 0 L 10 5' //negativeMarker路径 } // 默认配置项 const defaultConfig = { showNodeText: true, showLineText: true, dragable: true, fixWhenDragEnd: false, collide: 60, } class ForceGraph { constructor(el, data, config) { // 力向图对象 this.graph = null // 渲染容器 this.el = el // 渲染数据 this.data = data // 节点是否显示文字 this.showNodeText = Object.hasOwn(config, 'showNodeText') ? config.showNodeText : defaultConfig.showNodeText // 连接线是否显示文字 this.showLineText = Object.hasOwn(config, 'showLineText') ? config.showLineText : defaultConfig.showLineText // 是否可拖拽 this.dragable = Object.hasOwn(config, 'dragable') ? config.dragable : defaultConfig.dragable // 拖拽结束节点位置是否暂时固定 this.fixWhenDragEnd = Object.hasOwn(config, 'fixWhenDragEnd') ? config.fixWhenDragEnd : defaultConfig.fixWhenDragEnd // 节点间互斥半径 this.collide = Object.hasOwn(config, 'collide') ? config.collide : defaultConfig.collide // 最终的配置项 this.finalConfig = { ...defaultConfig, ...config, } // 可取色 if (config.colors && config.colors.property) { colors = { ...colors, ...transformColors(config.colors, config.colors.property) } } this.init() } init() { this.graph = new Graph(this.el, this.data, this.finalConfig) } // 获取节点实例 getNodes() { return this.graph.nodes } // 获取路径实例 getLinks() { return this.graph.links } // 获取画布 getSvg() { return this.graph.svg } // resize() { // const el = document.querySelector(this.el) // const w = el.clientWidth // const h = el.clientHeight // console.log(w, h) // this.graph.svg.attr("viewBox", [-w / 2, -h / 2, w, h]) // } } class Graph { constructor(el, data, config) { this.svg = null this.g = null this.simulation = null this.nodeList = [] this.linkList = [] this.nodes = null this.links = null this.nodesText = null this.linksText = null this.markers = null this.config = config // 配置样式 this.styleObj = {...defaultStyle, ...config.styleObj} this.initForceGraph(el, data, config) this.renderPath() this.renderNode() this.renderMarker() this.onCall() } initForceGraph(el, forceData, config) { this.el = document.querySelector(el) const w = this.el.clientWidth const h = this.el.clientHeight this.svg = d3 .select(el) .append('svg') .attr('width', w) .attr('height', h) .attr("viewBox", [-w / 2, -h / 2, w, h]) .call( d3.zoom().on('zoom', () => { this.g.attr('transform', d3.zoomTransform(this.g.node())) }) ) this.linkList = forceData.links.map(d => ({...d})); this.nodeList = forceData.nodes.map(d => ({...d})); this.g = this.svg.append('g') this.simulation = d3.forceSimulation(this.nodeList) .force('link', d3.forceLink(this.linkList).id(d => d.id)) .force('collide', d3.forceCollide().radius(() => config.collide || 50)) .force('charge', d3.forceManyBody()) .force('x', d3.forceX()) .force('y', d3.forceY()) } renderPath() { // 路径 this.links = this.g.append('g') .selectAll('path') .data(this.linkList) .enter() .append('path') .attr('stroke', (d) => { return d.style && d.style.color ? d.style.color : this.styleObj.linkColor }) .attr('stroke-opacity', this.styleObj.linkOpacity) .attr('id', function(d, i) { return 'edgepath' + i }) .attr('stroke-width', (d) => { return d.style && d.style.width ? d.style.width : this.styleObj.linkWidth }) .attr('marker-end', 'url(#positiveMarker)') // 路径文字 this.linksText = this.g.append('g') .selectAll('text') .data(this.linkList) .enter() .append('text') .attr('dx', this.styleObj.linkTextDx) .attr('dy', this.styleObj.linkTextDy) .append("textPath") .attr('startOffset', '50%') .attr('text-anchor', 'middle') .attr('xlink:href', function(d, i) { return '#edgepath' + i }) .text(function(d) { return d.relationship }) .attr('font-size', (d) => { return d.linkText && d.linkText.fontSize || this.styleObj.linkTextSize }) .attr('font-weight', this.styleObj.linkTextWeight) .attr('fill', (d) => { return d.linkText && d.linkText.color || this.styleObj.linkTextColor }) } renderNode() { // 节点,根据分组定义形状 this.nodes = this.g.append('g') .selectAll('rect') .data(this.nodeList, d => d.id) .join('rect') .attr('rx', d => { return d.node.radius || d.node.radiusX || 2 }) .attr('ry', d => { return d.node.radius || d.node.radiusY || 2 }) .attr('x', 0) .attr('y', 0) .attr('width', d => { return d.node.width || 30 }) .attr('height', d => { return d.node.height || 30 }) .attr('class', 'node-hover') .attr('transform', d => { return `translate(-${d.node.width / 2}, -${d.node.height / 2})` }) .attr('fill', d => colors.content(d[colors.property])) this.nodes.append('title').text(d => d.name) // 节点文字 this.nodesText = this.g.append('g') .selectAll('text') .data(this.nodeList) .join('text') .attr('text-anchor', 'middle') .attr('class', 'force-text') .text(function(d) { return d.name }) .attr('dx', this.styleObj.nodeTextDx) .attr('dy', this.styleObj.nodeTextDy) .attr('font-size', (d) => { return d.nodeText && d.nodeText.fontSize || this.styleObj.nodeTextSize || '' }) .attr('font-weight', this.styleObj.nodeTextWeight) .attr('fill', (d) => { return d.nodeText && d.nodeText.color || this.styleObj.nodeTextColor || '' }) } // 箭头标识 renderMarker() { this.g.append('marker') .attr('id', 'positiveMarker') .attr('orient', 'auto') .attr('stroke-width', 2) .attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse') .attr('viewBox', '0 -5 10 10') .attr('refX', 26) .attr('refY', 0) .attr('markerWidth', this.styleObj.markerWidth) .attr('markerHeight', this.styleObj.markerHeight) .append('path') .attr('d', this.styleObj.positiveMarkerPath) .attr('fill', this.styleObj.markerColor) .attr('stroke-opacity', 0.6) this.g.append('marker') .attr('id', 'negativeMarker') .attr('orient', 'auto') .attr('stroke-width', 2) .attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse') .attr('viewBox', '0 -5 10 10') .attr('refX', -12) .attr('refY', 0) .attr('markerWidth', this.styleObj.markerWidth) .attr('markerHeight', this.styleObj.markerHeight) .append('path') .attr('d', this.styleObj.negativeMarkerPath) .attr('fill', this.styleObj.markerColor) .attr('stroke-opacity', 0.6) } // 添加额外事件 onCall() { // 添加拖拽功能 if (this.config.dragable) { this.nodes.call(d3.drag() .on('start', d => { this.dragStarted(d) }) .on("drag", d => { this.dragged(d) }) .on("end", d => { this.dragended(d) })) } // 拖拽时节点和连接线的位置 this.simulation.on("tick", () => { this.links .attr('d', function(d) { if (d.source.x < d.target.x) { return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y } else { return 'M ' + d.target.x + ' ' + d.target.y + ' L ' + d.source.x + ' ' + d.source.y } }) .attr('marker-end', function(d) { if (d.source.x < d.target.x) { return 'url(#positiveMarker)' } else { return null } }) .attr('marker-start', function(d) { if (d.source.x < d.target.x) { return null } else { return 'url(#negativeMarker)' } }) this.nodes .attr("x", d => d.x) .attr("y", d => d.y) this.nodesText .attr("x", d => d.x) .attr("y", d => d.y) }); } dragStarted(event) { if (!event.active) this.simulation.alphaTarget(0.3).restart(); event.subject.fx = event.subject.x; event.subject.fy = event.subject.y; } dragged(event) { event.subject.fx = event.x; event.subject.fy = event.y; } dragended(event) { if (!event.active) this.simulation.alphaTarget(0); console.log(this.config) // 固定在当前鼠标放开位置 if (this.config.fixWhenDragEnd) { event.subject.fx = event.x; event.subject.fy = event.y; } else { // 回到原点 event.subject.fx = null; event.subject.fy = null; // this.simulation.stop() } } } export default ForceGraph