简体   繁体   English

如何使用React创建d3力布局图

[英]How to create a d3 force layout graph using React

I would like to create a d3 force layout graph using ReactJS. 我想使用ReactJS创建一个d3力布局图

I've created other graphs using React + d3 such as pie charts, line graphs, histograms. 我使用React + d3创建了其他图形,如饼图,折线图,直方图。 Now I wonder how to build a svg graphic like the d3 force layout which involves physics and user interaction. 现在我想知道如何构建像d3力布局这样涉及物理和用户交互的svg图形。

Here is an example of what I want to build http://bl.ocks.org/mbostock/4062045 以下是我想要构建的示例http://bl.ocks.org/mbostock/4062045

Since D3 and React haven't decreased in popularity the last three years, I figured a more concrete answer might help someone here who wants to make a D3 force layout in React. 由于D3和React在过去三年里没有减少流行度,我认为一个更具体的答案可能会帮助那些想要在React中制作D3力量布局的人。

Creating a D3 graph can be exactly the same as for any other D3 graph. 创建D3图可以与任何其他D3图完全相同。 But you can also use React to replace D3's enter, update and exit functions. 但您也可以使用React替换D3的输入,更新和退出功能。 So React takes care of rendering the lines, circles and svg. 所以React负责渲染线条,圆圈和svg。

This could be helpfull when a user should be able to interact a lot with the graph. 当用户应该能够与图表进行大量交互时,这可能会有所帮助。 Where it would be possible for a user to add, delete, edit and do a bunch of other stuff to the nodes and links of the graph. 用户可以在图形的节点和链接中添加,删除,编辑和执行大量其他操作。

There are 3 components in the example below. 以下示例中有3个组件。 The App component holds the app's state. App组件保存应用程序的状态。 In particular the 2 standard arrays with node and link data that should be passed to D3's d3.forceSimulation function. 特别是具有节点和链接数据的2个标准数组应该传递给D3的d3.forceSimulation函数。

Then there's one component for the links and one component for the nodes. 然后是链接的一个组件和节点的一个组件。 You can use React to do anything you want with the lines and circles. 您可以使用React通过直线和圆圈执行任何操作。 You could use React's onClick , for example. 例如,您可以使用React的onClick

The functions enterNode(selection) and enterLink(selection) render the lines and circles. 函数enterNode(selection)enterLink(selection)渲染线条和圆圈。 These functions are called from within the Node and Link components. 这些函数在Node和Link组件中调用。 These components take the nodes' and links' data as prop before they pass it to these enter functions. 这些组件在将节点'和链接'数据传递给这些输入函数之前将其作为prop。

The functions updateNode(selection) and updateLink(selection) update the nodes' and links' positions. 函数updateNode(selection)updateLink(selection)更新节点和链接的位置。 They are called from D3's tick function. 它们是从D3的tick函数调用的。

I used these functions from a React + D3 force layout example from Shirley Wu . 我使用了来自Shirley WuReact + D3力布局示例中的这些函数。

It's only possible to add nodes in the example below. 只能在下面的示例中添加节点。 But I hope it shows how to make the force layout more interactive using React. 但我希望它展示了如何使用React使力布局更具交互性。

Codepen live example Codepen实例

 /////////////////////////////////////////////////////////// /////// Functions and variables /////////////////////////////////////////////////////////// var FORCE = (function(nsp) { var width = 1080, height = 250, color = d3.scaleOrdinal(d3.schemeCategory10), initForce = (nodes, links) => { nsp.force = d3.forceSimulation(nodes) .force("charge", d3.forceManyBody().strength(-200)) .force("link", d3.forceLink(links).distance(70)) .force("center", d3.forceCenter().x(nsp.width / 2).y(nsp.height / 2)) .force("collide", d3.forceCollide([5]).iterations([5])); }, enterNode = (selection) => { var circle = selection.select('circle') .attr("r", 25) .style("fill", function (d) { if (d.id > 3) { return 'darkcyan' } else { return 'tomato' }}) .style("stroke", "bisque") .style("stroke-width", "3px") selection.select('text') .style("fill", "honeydew") .style("font-weight", "600") .style("text-transform", "uppercase") .style("text-anchor", "middle") .style("alignment-baseline", "middle") .style("font-size", "10px") .style("font-family", "cursive") }, updateNode = (selection) => { selection .attr("transform", (d) => "translate(" + dx + "," + dy + ")") .attr("cx", function(d) { return dx = Math.max(30, Math.min(width - 30, dx)); }) .attr("cy", function(d) { return dy = Math.max(30, Math.min(height - 30, dy)); }) }, enterLink = (selection) => { selection .attr("stroke-width", 3) .attr("stroke", "bisque") }, updateLink = (selection) => { selection .attr("x1", (d) => d.source.x) .attr("y1", (d) => d.source.y) .attr("x2", (d) => d.target.x) .attr("y2", (d) => d.target.y); }, updateGraph = (selection) => { selection.selectAll('.node') .call(updateNode) selection.selectAll('.link') .call(updateLink); }, dragStarted = (d) => { if (!d3.event.active) nsp.force.alphaTarget(0.3).restart(); d.fx = dx; d.fy = dy }, dragging = (d) => { d.fx = d3.event.x; d.fy = d3.event.y }, dragEnded = (d) => { if (!d3.event.active) nsp.force.alphaTarget(0); d.fx = null; d.fy = null }, drag = () => d3.selectAll('g.node') .call(d3.drag() .on("start", dragStarted) .on("drag", dragging) .on("end", dragEnded) ), tick = (that) => { that.d3Graph = d3.select(ReactDOM.findDOMNode(that)); nsp.force.on('tick', () => { that.d3Graph.call(updateGraph) }); }; nsp.width = width; nsp.height = height; nsp.enterNode = enterNode; nsp.updateNode = updateNode; nsp.enterLink = enterLink; nsp.updateLink = updateLink; nsp.updateGraph = updateGraph; nsp.initForce = initForce; nsp.dragStarted = dragStarted; nsp.dragging = dragging; nsp.dragEnded = dragEnded; nsp.drag = drag; nsp.tick = tick; return nsp })(FORCE || {}) //////////////////////////////////////////////////////////////////////////// /////// class App is the parent component of Link and Node //////////////////////////////////////////////////////////////////////////// class App extends React.Component { constructor(props) { super(props) this.state = { addLinkArray: [], name: "", nodes: [{ "name": "fruit", "id": 0 }, { "name": "apple", "id": 1 }, { "name": "orange", "id": 2 }, { "name": "banana", "id": 3 } ], links: [{ "source": 0, "target": 1, "id": 0 }, { "source": 0, "target": 2, "id": 1 }, { "source": 0, "target": 3, "id": 2 } ] } this.handleAddNode = this.handleAddNode.bind(this) this.addNode = this.addNode.bind(this) } componentDidMount() { const data = this.state; FORCE.initForce(data.nodes, data.links) FORCE.tick(this) FORCE.drag() } componentDidUpdate(prevProps, prevState) { if (prevState.nodes !== this.state.nodes || prevState.links !== this.state.links) { const data = this.state; FORCE.initForce(data.nodes, data.links) FORCE.tick(this) FORCE.drag() } } handleAddNode(e) { this.setState({ [e.target.name]: e.target.value }); } addNode(e) { e.preventDefault(); this.setState(prevState => ({ nodes: [...prevState.nodes, { name: this.state.name, id: prevState.nodes.length + 1, x: FORCE.width / 2, y: FORCE.height / 2 }], name: '' })); } render() { var links = this.state.links.map((link) => { return ( < Link key = { link.id } data = { link } />); }); var nodes = this.state.nodes.map((node) => { return ( < Node data = { node } name = { node.name } key = { node.id } />); }); return ( < div className = "graph__container" > < form className = "form-addSystem" onSubmit = { this.addNode.bind(this) } > < h4 className = "form-addSystem__header" > New Node < /h4> < div className = "form-addSystem__group" > < input value = { this.state.name } onChange = { this.handleAddNode.bind(this) } name = "name" className = "form-addSystem__input" id = "name" placeholder = "Name" / > < label className = "form-addSystem__label" htmlFor = "title" > Name < /label> < / div > < div className = "form-addSystem__group" > < input className = "btnn" type = "submit" value = "add node" / > < /div> < / form > < svg className = "graph" width = { FORCE.width } height = { FORCE.height } > < g > { links } < /g> < g > { nodes } < /g> < / svg > < /div> ); } } /////////////////////////////////////////////////////////// /////// Link component /////////////////////////////////////////////////////////// class Link extends React.Component { componentDidMount() { this.d3Link = d3.select(ReactDOM.findDOMNode(this)) .datum(this.props.data) .call(FORCE.enterLink); } componentDidUpdate() { this.d3Link.datum(this.props.data) .call(FORCE.updateLink); } render() { return ( < line className = 'link' / > ); } } /////////////////////////////////////////////////////////// /////// Node component /////////////////////////////////////////////////////////// class Node extends React.Component { componentDidMount() { this.d3Node = d3.select(ReactDOM.findDOMNode(this)) .datum(this.props.data) .call(FORCE.enterNode) } componentDidUpdate() { this.d3Node.datum(this.props.data) .call(FORCE.updateNode) } render() { return ( < g className = 'node' > < circle onClick = { this.props.addLink } /> < text > { this.props.data.name } < /text> < / g > ); } } ReactDOM.render( < App / > , document.querySelector('#root')) 
 .graph__container { display: grid; grid-template-columns: 1fr 1fr; } .graph { background-color: steelblue; } .form-addSystem { display: grid; grid-template-columns: min-content min-content; background-color: aliceblue; padding-bottom: 15px; margin-right: 10px; } .form-addSystem__header { grid-column: 1/-1; text-align: center; margin: 1rem; padding-bottom: 1rem; text-transform: uppercase; text-decoration: none; font-size: 1.2rem; color: steelblue; border-bottom: 1px dotted steelblue; font-family: cursive; } .form-addSystem__group { display: grid; margin: 0 1rem; align-content: center; } .form-addSystem__input, input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active { outline: none; border: none; border-bottom: 3px solid teal; padding: 1.5rem 2rem; border-radius: 3px; background-color: transparent; color: steelblue; transition: all .3s; font-family: cursive; transition: background-color 5000s ease-in-out 0s; } .form-addSystem__input:focus { outline: none; background-color: platinum; border-bottom: none; } .form-addSystem__input:focus:invalid { border-bottom: 3px solid steelblue; } .form-addSystem__input::-webkit-input-placeholder { color: steelblue; } .btnn { text-transform: uppercase; text-decoration: none; border-radius: 10rem; position: relative; font-size: 12px; height: 30px; align-self: center; background-color: cadetblue; border: none; color: aliceblue; transition: all .2s; } .btnn:hover { transform: translateY(-3px); box-shadow: 0 1rem 2rem rgba(0, 0, 0, .2) } .btnn:hover::after { transform: scaleX(1.4) scaleY(1.6); opacity: 0; } .btnn:active, .btnn:focus { transform: translateY(-1px); box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .2); outline: 0; } .form-addSystem__label { color: lightgray; font-size: 20px; font-family: cursive; font-weight: 700; margin-left: 1.5rem; margin-top: .7rem; display: block; transition: all .3s; } .form-addSystem__input:placeholder-shown+.form-addSystem__label { opacity: 0; visibility: hidden; transform: translateY(-4rem); } .form-addSystem__link { grid-column: 2/4; justify-self: center; align-self: center; text-transform: uppercase; text-decoration: none; font-size: 1.2rem; color: steelblue; } 
 <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> </script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script> <div id="root"></div> 

Colin Megill has a great blog post on this: http://formidable.com/blog/2015/05/21/react-d3-layouts/ . Colin Megill有一篇很棒的博客文章: http//formidable.com/blog/2015/05/21/react-d3-layouts/ There is also a working jsbin http://jsbin.com/fanofa/14/embed?js,output . 还有一个工作jsbin http://jsbin.com/fanofa/14/embed?js,output There is a b.locks.org account, JMStewart, who has an interesting implementation that wraps React in d3 code: http://bl.ocks.org/JMStewart/f0dc27409658ab04d1c8 . 有一个b.locks.org帐户,JMStewart,他有一个有趣的实现,它在d3代码中包含了React: http ://bl.ocks.org/JMStewart/f0dc27409658ab04d1c8。

Everyone who implements force-layouts in React notices a minor performance loss. 在React中实施强制布局的每个人都注意到轻微的性能损失。 For complex charts (beyond 100 nodes) this becomes much more severe. 对于复杂的图表(超过100个节点),这变得更加严重。

Note: There is an open issue on react-motion for applying forces (which would otherwise be a good react solution to this) but its gone silent. 注意:对于施加力的反应运动存在一个未解决的问题 (否则这将是一个很好的反应解决方案),但它已经沉默。

**THIS IS NOT AN ANSWER BUT STACKOVERFLOW DOES NOT HAVE THE FACILITY TO ADD A COMMENT FOR ME. **这不是回答,但是STACKOVERFLOW没有为我添加评论的设施。 ** **

My question is to vincent. 我的问题是对文森特来说。 The code compiles perfectly but when i run it the background gets drawn with the blue color but the graph actually renders as 4 dots on the top left corner. 代码完美编译,但是当我运行它时,背景会以蓝色绘制,但图形实际上会在左上角呈现4个点。 That is all gets drawn. 这一切都得到了解决。 I have tried may approaches but always seem to be getting the same results just 4 dots on the top left corner. 我尝试了可能的方法,但似乎总是在左上角只有4个点得到相同的结果。 My email id is RVELUTHATTIL@YAHOO.COM. 我的电子邮件ID是RVELUTHATTIL@YAHOO.COM。 Would appreciate it if you could let me know if you had this problem 如果您有这个问题可以让我知道,我将不胜感激

 /////////////////////////////////////////////////////////// /////// Functions and variables /////////////////////////////////////////////////////////// var FORCE = (function(nsp) { var width = 1080, height = 250, color = d3.scaleOrdinal(d3.schemeCategory10), initForce = (nodes, links) => { nsp.force = d3.forceSimulation(nodes) .force("charge", d3.forceManyBody().strength(-200)) .force("link", d3.forceLink(links).distance(70)) .force("center", d3.forceCenter().x(nsp.width / 2).y(nsp.height / 2)) .force("collide", d3.forceCollide([5]).iterations([5])); }, enterNode = (selection) => { var circle = selection.select('circle') .attr("r", 25) .style("fill", function (d) { if (d.id > 3) { return 'darkcyan' } else { return 'tomato' }}) .style("stroke", "bisque") .style("stroke-width", "3px") selection.select('text') .style("fill", "honeydew") .style("font-weight", "600") .style("text-transform", "uppercase") .style("text-anchor", "middle") .style("alignment-baseline", "middle") .style("font-size", "10px") .style("font-family", "cursive") }, updateNode = (selection) => { selection .attr("transform", (d) => "translate(" + dx + "," + dy + ")") .attr("cx", function(d) { return dx = Math.max(30, Math.min(width - 30, dx)); }) .attr("cy", function(d) { return dy = Math.max(30, Math.min(height - 30, dy)); }) }, enterLink = (selection) => { selection .attr("stroke-width", 3) .attr("stroke", "bisque") }, updateLink = (selection) => { selection .attr("x1", (d) => d.source.x) .attr("y1", (d) => d.source.y) .attr("x2", (d) => d.target.x) .attr("y2", (d) => d.target.y); }, updateGraph = (selection) => { selection.selectAll('.node') .call(updateNode) selection.selectAll('.link') .call(updateLink); }, dragStarted = (d) => { if (!d3.event.active) nsp.force.alphaTarget(0.3).restart(); d.fx = dx; d.fy = dy }, dragging = (d) => { d.fx = d3.event.x; d.fy = d3.event.y }, dragEnded = (d) => { if (!d3.event.active) nsp.force.alphaTarget(0); d.fx = null; d.fy = null }, drag = () => d3.selectAll('g.node') .call(d3.drag() .on("start", dragStarted) .on("drag", dragging) .on("end", dragEnded) ), tick = (that) => { that.d3Graph = d3.select(ReactDOM.findDOMNode(that)); nsp.force.on('tick', () => { that.d3Graph.call(updateGraph) }); }; nsp.width = width; nsp.height = height; nsp.enterNode = enterNode; nsp.updateNode = updateNode; nsp.enterLink = enterLink; nsp.updateLink = updateLink; nsp.updateGraph = updateGraph; nsp.initForce = initForce; nsp.dragStarted = dragStarted; nsp.dragging = dragging; nsp.dragEnded = dragEnded; nsp.drag = drag; nsp.tick = tick; return nsp })(FORCE || {}) //////////////////////////////////////////////////////////////////////////// /////// class App is the parent component of Link and Node //////////////////////////////////////////////////////////////////////////// class App extends React.Component { constructor(props) { super(props) this.state = { addLinkArray: [], name: "", nodes: [{ "name": "fruit", "id": 0 }, { "name": "apple", "id": 1 }, { "name": "orange", "id": 2 }, { "name": "banana", "id": 3 } ], links: [{ "source": 0, "target": 1, "id": 0 }, { "source": 0, "target": 2, "id": 1 }, { "source": 0, "target": 3, "id": 2 } ] } this.handleAddNode = this.handleAddNode.bind(this) this.addNode = this.addNode.bind(this) } componentDidMount() { const data = this.state; FORCE.initForce(data.nodes, data.links) FORCE.tick(this) FORCE.drag() } componentDidUpdate(prevProps, prevState) { if (prevState.nodes !== this.state.nodes || prevState.links !== this.state.links) { const data = this.state; FORCE.initForce(data.nodes, data.links) FORCE.tick(this) FORCE.drag() } } handleAddNode(e) { this.setState({ [e.target.name]: e.target.value }); } addNode(e) { e.preventDefault(); this.setState(prevState => ({ nodes: [...prevState.nodes, { name: this.state.name, id: prevState.nodes.length + 1, x: FORCE.width / 2, y: FORCE.height / 2 }], name: '' })); } render() { var links = this.state.links.map((link) => { return ( < Link key = { link.id } data = { link } />); }); var nodes = this.state.nodes.map((node) => { return ( < Node data = { node } name = { node.name } key = { node.id } />); }); return ( < div className = "graph__container" > < form className = "form-addSystem" onSubmit = { this.addNode.bind(this) } > < h4 className = "form-addSystem__header" > New Node < /h4> < div className = "form-addSystem__group" > < input value = { this.state.name } onChange = { this.handleAddNode.bind(this) } name = "name" className = "form-addSystem__input" id = "name" placeholder = "Name" / > < label className = "form-addSystem__label" htmlFor = "title" > Name < /label> < / div > < div className = "form-addSystem__group" > < input className = "btnn" type = "submit" value = "add node" / > < /div> < / form > < svg className = "graph" width = { FORCE.width } height = { FORCE.height } > < g > { links } < /g> < g > { nodes } < /g> < / svg > < /div> ); } } /////////////////////////////////////////////////////////// /////// Link component /////////////////////////////////////////////////////////// class Link extends React.Component { componentDidMount() { this.d3Link = d3.select(ReactDOM.findDOMNode(this)) .datum(this.props.data) .call(FORCE.enterLink); } componentDidUpdate() { this.d3Link.datum(this.props.data) .call(FORCE.updateLink); } render() { return ( < line className = 'link' / > ); } } /////////////////////////////////////////////////////////// /////// Node component /////////////////////////////////////////////////////////// class Node extends React.Component { componentDidMount() { this.d3Node = d3.select(ReactDOM.findDOMNode(this)) .datum(this.props.data) .call(FORCE.enterNode) } componentDidUpdate() { this.d3Node.datum(this.props.data) .call(FORCE.updateNode) } render() { return ( < g className = 'node' > < circle onClick = { this.props.addLink } /> < text > { this.props.data.name } < /text> < / g > ); } } ReactDOM.render( < App / > , document.querySelector('#root')) 
 .graph__container { display: grid; grid-template-columns: 1fr 1fr; } .graph { background-color: steelblue; } .form-addSystem { display: grid; grid-template-columns: min-content min-content; background-color: aliceblue; padding-bottom: 15px; margin-right: 10px; } .form-addSystem__header { grid-column: 1/-1; text-align: center; margin: 1rem; padding-bottom: 1rem; text-transform: uppercase; text-decoration: none; font-size: 1.2rem; color: steelblue; border-bottom: 1px dotted steelblue; font-family: cursive; } .form-addSystem__group { display: grid; margin: 0 1rem; align-content: center; } .form-addSystem__input, input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active { outline: none; border: none; border-bottom: 3px solid teal; padding: 1.5rem 2rem; border-radius: 3px; background-color: transparent; color: steelblue; transition: all .3s; font-family: cursive; transition: background-color 5000s ease-in-out 0s; } .form-addSystem__input:focus { outline: none; background-color: platinum; border-bottom: none; } .form-addSystem__input:focus:invalid { border-bottom: 3px solid steelblue; } .form-addSystem__input::-webkit-input-placeholder { color: steelblue; } .btnn { text-transform: uppercase; text-decoration: none; border-radius: 10rem; position: relative; font-size: 12px; height: 30px; align-self: center; background-color: cadetblue; border: none; color: aliceblue; transition: all .2s; } .btnn:hover { transform: translateY(-3px); box-shadow: 0 1rem 2rem rgba(0, 0, 0, .2) } .btnn:hover::after { transform: scaleX(1.4) scaleY(1.6); opacity: 0; } .btnn:active, .btnn:focus { transform: translateY(-1px); box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .2); outline: 0; } .form-addSystem__label { color: lightgray; font-size: 20px; font-family: cursive; font-weight: 700; margin-left: 1.5rem; margin-top: .7rem; display: block; transition: all .3s; } .form-addSystem__input:placeholder-shown+.form-addSystem__label { opacity: 0; visibility: hidden; transform: translateY(-4rem); } .form-addSystem__link { grid-column: 2/4; justify-self: center; align-self: center; text-transform: uppercase; text-decoration: none; font-size: 1.2rem; color: steelblue; } 
 <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> </script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script> <div id="root"></div> 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM