UNPKG

d3-force-chart

Version:

a Force-Oriented Graphics React Plug-in by d3

584 lines (542 loc) 14.8 kB
import React, { Component } from 'react'; import * as d3 from 'd3'; import { cheackNodesDiff, _enter, _exit } from './utils/functions'; import mock_data from './mock/data.json'; class D3ForceChart extends Component { constructor(props) { super(props); this.state = { nodeData: [], edgesData: [], newNodes: {}, newEdges: {}, singleClick: true, }; this.WIDTH = 0; this.HEIGHT = 0; this.update_edges = []; this.update_nodes = []; this.zoom = null; this.drag = null; this.longPressTImer = null; // 长按标识 this.forceChart = this.forceChart.bind(this); } componentDidMount() { // console.log(this.props); let { OnUpdateData, OnBackToPreStep, ForceData, HEIGHT, WIDTH, ScaleExtent, CenterLocation, TextLocation, Strength, DistanceMax, StrokeWidth, DragStartAlphaTarget, DragEndAlphaTarget, ImageSizeLocation, OnNodesClick, OnLongPressDown, } = this.props; let nodes; let edges; let strength = 0; let distanceMax = 0; let strokeWidth = 0; let dragStartAlphaTarget = 0; let dragEndAlphaTarget = 0; let scaleExtent = []; let centerLocation = {}; let textLocation = {}; let imageSizeLocation = {}; let onUpDateData; let onBackToPreStep; let onLongPressDown; if (!HEIGHT || !WIDTH) { // console.error('必须指定宽高.'); // 默认宽高 HEIGHT = window.innerHeight; WIDTH = window.innerWidth; } else { HEIGHT = HEIGHT; WIDTH = WIDTH; } if (!ScaleExtent || !Array.isArray(ScaleExtent)) { // 默认缩放比例 [1 / 10, 10] -> Array[] scaleExtent = [1 / 10, 10]; } else { scaleExtent = ScaleExtent; } if ( CenterLocation === undefined || JSON.stringify(CenterLocation) === '{}' ) { // 默认中心点 容器中间位置 -> Object{} /** * { x:*, y:*} */ centerLocation.x = WIDTH / 2; centerLocation.y = HEIGHT / 2; } else { centerLocation.x = CenterLocation.x; centerLocation.y = CenterLocation.y; } if (TextLocation === undefined || JSON.stringify(TextLocation) === '{}') { // 默认文字标签的位置 -> Object{} /** * { dx:*, dy:* } */ textLocation.dx = 20; textLocation.dy = 8; } else { textLocation.dx = TextLocation.dx; textLocation.dy = TextLocation.dy; } if (!Strength) { // 默认节点间的作用力 -> Number strength = -380; } else { strength = Strength; } if (!DistanceMax) { // 默认节点间的最大距离 -> Number distanceMax = -300; } else { distanceMax = DistanceMax; } if (!StrokeWidth) { // 默认连线宽度1 -> Number strokeWidth = 1; } else { strokeWidth = StrokeWidth; } if (!DragStartAlphaTarget || !DragEndAlphaTarget) { // 默认拖拽系数 -> Number // dragStartAlphaTarget 开始拖拽系数 // dragEndAlphaTarget 结束拖拽系数 dragStartAlphaTarget = 0.5; dragEndAlphaTarget = 0; } else { dragStartAlphaTarget = DragStartAlphaTarget; dragEndAlphaTarget = DragEndAlphaTarget; } if ( ImageSizeLocation === undefined || JSON.stringify(ImageSizeLocation) === '{}' ) { // 默认节点图片的大小和位置 -> String /** * { x:*, y:*, w:*, h:* } */ imageSizeLocation.x = '-15px'; imageSizeLocation.y = '-15px'; imageSizeLocation.w = '30px'; imageSizeLocation.h = '30px'; } else { imageSizeLocation.x = ImageSizeLocation.x; imageSizeLocation.y = ImageSizeLocation.y; imageSizeLocation.w = ImageSizeLocation.w; imageSizeLocation.h = ImageSizeLocation.h; } if (ForceData === undefined || JSON.stringify(ForceData) === '{}') { // 默认模拟数据 -> Object{} nodes = mock_data.nodes; edges = mock_data.edges; } else { // 真实数据 // eslint-disable-next-line no-console nodes = ForceData.nodes; edges = ForceData.edges; // 内部保存原始数据 this.setState({ newNodes: ForceData.nodes, newEdges: ForceData.edges }); } if (OnNodesClick) { // 传递OnNodesClick为true后 取消节点的点击事件 onUpDateData = function() {}; } else if (!OnUpdateData) { // 默认点击节点事件 onUpDateData = this.mockUpdateDate; } else { onUpDateData = OnUpdateData; } if (!OnBackToPreStep) { // 默认回退事件 onBackToPreStep = () => {}; } else { onBackToPreStep = OnBackToPreStep; } // if (!OnLongPressDown) { // 默认长按事件 onLongPressDown = () => {}; } else { onLongPressDown = OnLongPressDown; } this.forceChart( WIDTH, HEIGHT, nodes, edges, scaleExtent, centerLocation, strength, distanceMax, strokeWidth, textLocation, dragStartAlphaTarget, dragEndAlphaTarget, imageSizeLocation, onUpDateData, onBackToPreStep, onLongPressDown ); } forceChart( WIDTH, HEIGHT, nodes, edges, scaleExtent, centerLocation, strength, distanceMax, strokeWidth, textLocation, dragStartAlphaTarget, dragEndAlphaTarget, imageSizeLocation, onUpDateData, onBackToPreStep, onLongPressDown ) { let { force, g, zoom, drag, longPressTImer } = this; let { newNodes, newEdges } = this.state; let that = this; /*缩放*/ zoom = d3 .zoom() .scaleExtent(scaleExtent) .on('start', function() {}) .on('zoom', function() { g.attr('transform', d3.event.transform); }) .on('end', function() {}); /*力*/ force = d3 .forceSimulation(nodes) .alphaDecay(0.1) .force('link', d3.forceLink(edges)) .force('center', d3.forceCenter(centerLocation.x, centerLocation.y)) .force( 'charge', d3 .forceManyBody() .strength(strength) .distanceMax(distanceMax) ); /*创建svg和g*/ let svg = d3 .select('#theChart') .append('svg') .attr('width', WIDTH) .attr('height', HEIGHT) .call(zoom); g = svg.append('g'); /*线*/ let svg_edges = svg .select('g') .selectAll('line') .data(edges) .enter() .append('line') .style('stroke', 'gray') .style('stroke-width', strokeWidth); /*字*/ let svg_texts = svg .select('g') .selectAll('text') .data(nodes) .enter() .append('text') .style('fill', 'black') .attr('dx', textLocation.dx) .attr('dy', textLocation.dy) .text(function(d) { return d.name; }); /*拖拽*/ drag = d3 .drag() .on('start', function(d) { longPressTImer = setTimeout(() => { onLongPressDown(d); that.setState({ singleClick: false }); }, 500); if (!d3.event.active) { force /*设置衰减系数 它是节点位置移动过程中的模拟,数值越高移动就越快,范围[0,1]*/ .alphaTarget(dragStartAlphaTarget) .restart(); } d.fx = d.x; d.fy = d.y; }) .on('drag', function(d) { clearTimeout(longPressTImer); d.fx = d3.event.x; d.fy = d3.event.y; }) .on('end', function(d) { setTimeout(() => { that.setState({ singleClick: true }); }, 0); clearTimeout(longPressTImer); if (!d3.event.active) { force.alphaTarget(dragEndAlphaTarget); } d.fx = null; d.fy = null; }); /*节点*/ let svg_nodes = svg .select('g') .selectAll('circle') .data(nodes) .enter() /*图片表示节点*/ .append('svg:image') .attr('xlink:href', function(d) { return d.url; }) .attr('x', imageSizeLocation.x) .attr('y', imageSizeLocation.y) .attr('width', imageSizeLocation.w) .attr('height', imageSizeLocation.h) .call(drag); // svg_nodes.on('blur', (d, i) => { // console.log('失去焦点', that.state.newNodes, that.state.newEdges); // onBlur(that.state.newNodes, that.state.newEdges); // }); /*点击节点mock数据*/ svg_nodes.on('click', (d, i) => { if (that.state.singleClick) { onUpDateData(d, i, nodes, edges, restart); } }); function onBlur(newNodes, newEdges) { nodes = newNodes; edges = newEdges; /*线*/ svg_edges = svg .select('g') .selectAll('line') .data(edges) .enter() .append('line') .style('stroke', 'gray') .style('stroke-width', 1) .merge(svg_edges); /*节点*/ svg_nodes.remove(); svg_nodes = svg .select('g') .selectAll('circle') .data(newNodes) .enter() /*图片表示节点*/ .append('svg:image') .attr('xlink:href', function(d) { return d.url; }) .attr('x', imageSizeLocation.x) .attr('y', imageSizeLocation.y) .attr('width', imageSizeLocation.w) .attr('height', imageSizeLocation.h) .call(drag); svg_texts = svg .select('g') .selectAll('text') .data(nodes) .enter() .append('text') .style('fill', 'black') .attr('dx', textLocation.dx) .attr('dy', textLocation.dy) .text(function(d) { return d.name; }) .merge(svg_texts); } function restart(newNodes, newEdges) { that.setState({ newNodes, newEdges }); let diff = cheackNodesDiff(nodes, newNodes); nodes = newNodes; edges = newEdges; if (diff === 'enter') { /*线*/ svg_edges = svg .select('g') .selectAll('line') .data(edges) .enter() .append('line') .style('stroke', 'gray') .style('stroke-width', 1) .merge(svg_edges); /*节点*/ svg_nodes.remove(); svg_nodes = svg .select('g') .selectAll('circle') .data(newNodes) .enter() /*图片表示节点*/ .append('svg:image') .attr( 'xlink:href', function(d) { return d.url; } // 'http://img2.imgtn.bdimg.com/it/u=343877052,3151572224&fm=26&gp=0.jpg' ) .attr('x', imageSizeLocation.x) .attr('y', imageSizeLocation.y) .attr('width', imageSizeLocation.w) .attr('height', imageSizeLocation.h) .call(drag); svg_texts = svg .select('g') .selectAll('text') .data(nodes) .enter() .append('text') .style('fill', 'black') .attr('dx', textLocation.dx) .attr('dy', textLocation.dy) .text(function(d) { return d.name; }) .merge(svg_texts); } else if (diff === 'exit') { onBackToPreStep(diff); /*线*/ svg_edges = svg .select('g') .selectAll('line') .data(edges) .exit() .remove() .merge(svg_edges); /*节点*/ svg_nodes.remove(); svg_nodes = svg .select('g') .selectAll('circle') .data(nodes) .enter() /*图片表示节点*/ .append('svg:image') .attr('xlink:href', function(d) { return d.url; }) .attr('x', imageSizeLocation.x) .attr('y', imageSizeLocation.y) .attr('width', imageSizeLocation.w) .attr('height', imageSizeLocation.h) .call(drag); svg_texts = svg .select('g') .selectAll('text') .data(nodes) .exit() .remove() .merge(svg_texts); } force.nodes(nodes); force.force('link').links(edges); force.alpha(1).restart(); /*点击节点mock数据*/ svg_nodes.on('click', (d, i) => { if (that.state.singleClick) { onUpDateData(d, i, nodes, edges, restart); } }); } force.on('tick', function() { /*更新连线*/ svg_edges .attr('x1', function(d) { return d.source.x; }) .attr('y1', function(d) { return d.source.y; }) .attr('x2', function(d) { return d.target.x; }) .attr('y2', function(d) { return d.target.y; }) .attr('d', function(d) { const path = `M${d.source.x}${d.source.y}L${d.target.x}${d.target.y}`; return path; }); /*更新节点*/ svg_nodes .attr('cx', function(d) { // console.log(d); return d.x; }) .attr('cy', function(d) { return d.y; }) .attr('transform', function(d) { return d && `translate(${d.x},${d.y})`; }); /*更新线*/ svg_texts .attr('x', function(d) { return d.x; }) .attr('y', function(d) { return d.y; }); }); } mockUpdateDate = (d, i, nodes, edges, restart) => { let { update_nodes } = this; let { update_edges } = this; // 组建拆分后 每个子节点点击事件的代码编写处. for (let i = 0; i < 10; i++) { let obj = {}; obj.source = d.index; obj.target = nodes.length + i; update_edges.push(obj); let obj2 = {}; obj2.name = `测试${i + 1}`; obj2.url = 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1555051176446&di=d980895b38cc694e143d01878ef5477e&imgtype=0&src=http%3A%2F%2Fp1.qhimgs4.com%2Ft010204ba62043ec126.jpg'; update_nodes.push(obj2); } let new_nodes = nodes.concat(update_nodes); let new_egdes = edges.concat(update_edges); nodes = new_nodes; edges = new_egdes; update_edges.length = 0; update_nodes.length = 0; restart(nodes, edges); }; OnBackToPreStep = () => {}; render() { return ( <React.Fragment> {/* <button className='d3__force__chart__back__btn' onClick={this.OnBackToPreStep}>回退事件</button> */} <div className="theChart" id="theChart" ref="theChart" /> </React.Fragment> ); } } export default D3ForceChart;