繁体   English   中英

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

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

我想使用ReactJS创建一个d3力布局图

我使用React + d3创建了其他图形,如饼图,折线图,直方图。 现在我想知道如何构建像d3力布局这样涉及物理和用户交互的svg图形。

以下是我想要构建的示例http://bl.ocks.org/mbostock/4062045

由于D3和React在过去三年里没有减少流行度,我认为一个更具体的答案可能会帮助那些想要在React中制作D3力量布局的人。

创建D3图可以与任何其他D3图完全相同。 但您也可以使用React替换D3的输入,更新和退出功能。 所以React负责渲染线条,圆圈和svg。

当用户应该能够与图表进行大量交互时,这可能会有所帮助。 用户可以在图形的节点和链接中添加,删除,编辑和执行大量其他操作。

以下示例中有3个组件。 App组件保存应用程序的状态。 特别是具有节点和链接数据的2个标准数组应该传递给D3的d3.forceSimulation函数。

然后是链接的一个组件和节点的一个组件。 您可以使用React通过直线和圆圈执行任何操作。 例如,您可以使用React的onClick

函数enterNode(selection)enterLink(selection)渲染线条和圆圈。 这些函数在Node和Link组件中调用。 这些组件在将节点'和链接'数据传递给这些输入函数之前将其作为prop。

函数updateNode(selection)updateLink(selection)更新节点和链接的位置。 它们是从D3的tick函数调用的。

我使用了来自Shirley WuReact + D3力布局示例中的这些函数。

只能在下面的示例中添加节点。 但我希望它展示了如何使用React使力布局更具交互性。

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有一篇很棒的博客文章: http//formidable.com/blog/2015/05/21/react-d3-layouts/ 还有一个工作jsbin http://jsbin.com/fanofa/14/embed?js,output 有一个b.locks.org帐户,JMStewart,他有一个有趣的实现,它在d3代码中包含了React: http ://bl.ocks.org/JMStewart/f0dc27409658ab04d1c8。

在React中实施强制布局的每个人都注意到轻微的性能损失。 对于复杂的图表(超过100个节点),这变得更加严重。

注意:对于施加力的反应运动存在一个未解决的问题 (否则这将是一个很好的反应解决方案),但它已经沉默。

**这不是回答,但是STACKOVERFLOW没有为我添加评论的设施。 **

我的问题是对文森特来说。 代码完美编译,但是当我运行它时,背景会以蓝色绘制,但图形实际上会在左上角呈现4个点。 这一切都得到了解决。 我尝试了可能的方法,但似乎总是在左上角只有4个点得到相同的结果。 我的电子邮件ID是RVELUTHATTIL@YAHOO.COM。 如果您有这个问题可以让我知道,我将不胜感激

 /////////////////////////////////////////////////////////// /////// 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