I would like to create a d3 force layout graph using ReactJS.
I've created other graphs using React + d3 such as pie charts, line graphs, histograms. Now I wonder how to build a svg graphic like the d3 force layout which involves physics and user interaction.
Here is an example of what I want to build 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.
Creating a D3 graph can be exactly the same as for any other D3 graph. But you can also use React to replace D3's enter, update and exit functions. So React takes care of rendering the lines, circles and 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. The App component holds the app's state. In particular the 2 standard arrays with node and link data that should be passed to D3's d3.forceSimulation function.
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. You could use React's onClick, for example.
The functions enterNode(selection) and enterLink(selection) render the lines and circles. These functions are called from within the Node and Link components. These components take the nodes' and links' data as prop before they pass it to these enter functions.
The functions updateNode(selection) and updateLink(selection) update the nodes' and links' positions. They are called from D3's tick function.
I used these functions from a React + D3 force layout example from Shirley Wu.
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.
Codepen live example
///////////////////////////////////////////////////////////
/////// 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(" + d.x + "," + d.y + ")")
.attr("cx", function(d) {
return d.x = Math.max(30, Math.min(width - 30, d.x));
})
.attr("cy", function(d) {
return d.y = Math.max(30, Math.min(height - 30, d.y));
})
},
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 = d.x;
d.fy = d.y
},
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/. There is also a working 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.
Everyone who implements force-layouts in React notices a minor performance loss. For complex charts (beyond 100 nodes) this becomes much more severe.
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. **
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. 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. My email id is 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(" + d.x + "," + d.y + ")")
.attr("cx", function(d) {
return d.x = Math.max(30, Math.min(width - 30, d.x));
})
.attr("cy", function(d) {
return d.y = Math.max(30, Math.min(height - 30, d.y));
})
},
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 = d.x;
d.fy = d.y
},
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>
Related
There are 5 items in a array (which consists objects inside) and it is represented visually by creating some elements in the document as per the array length.
Like this:
let array = [
{
"value": "Hello"
},
{
"value": "World"
},
{
"value": "You Can"
},
{
"value": " NOT"
},
{
"value": "<h1>Remove ME!</h1>"
}
]
// creating it visually by creating elements.
for(let i = 0; i < array.length; i++) {
const div = document.createElement("div");
div.classList.add("DIV");
div.innerHTML = `
<span class="Text">${array[i].value}</span>
<span class="Remove">( Remove )</span>
`
document.body.appendChild(div);
}
document.querySelectorAll(".Remove").forEach(elem => {
elem.addEventListener("click", () => {
remove();
})
})
function remove() {
// Now how to remove the specific item from the array?
}
// I hope this is fully understandable.
/* CSS IS NOT VERY IMPORTANT */
.DIV {
padding: 10px;
margin: 10px;
background: purple;
color: #fff;
border-radius: 10px;
display: flex;
justify-content: space-between;
}
.Remove {
cursor: pointer;
height: fit-content;
}
Now I want to remove the 4th element of the array that has the value of "NOT" by clicking the 4th remove button but how can I do that.
( Your code should work in all the elements. )
Any help will be highly appreciated. Thank You.
In simple terms, add an id or some kind of identifier and use it that way.
let array = [
{
"value": "Hello"
},
{
"value": "World"
},
{
"value": "You Can"
},
{
"value": " NOT"
},
{
"value": "<h1>Remove ME!</h1>"
}
]
// creating it visually by creating elements.
for (let i = 0; i < array.length; i++) {
const div = document.createElement("div");
div.classList.add("DIV");
div.innerHTML = `
<span class="Text">${array[i].value}</span>
<span class="Remove" data-id="${"elem-" + i}">( Remove )</span>
`;
document.body.appendChild(div);
}
document.querySelectorAll(".Remove").forEach(elem => {
elem.addEventListener("click", () => {
remove(elem.dataset.id);
})
})
function remove(id) {
// Now how to remove the specific item from the array?
console.log(id);
}
// I hope this is fully understandable.
/* CSS IS NOT VERY IMPORTANT */
.DIV {
padding: 10px;
margin: 10px;
background: purple;
color: #fff;
border-radius: 10px;
display: flex;
justify-content: space-between;
}
.Remove {
cursor: pointer;
height: fit-content;
}
The above code will get the dataset now. Using this, remove the element of that id and rerender the view.
let array = [
{
"value": "Hello"
},
{
"value": "World"
},
{
"value": "You Can"
},
{
"value": " NOT"
},
{
"value": "<h1>Remove ME!</h1>"
}
]
function renderElement(array) {
// creating it visually by creating elements.
for (let i = 0; i < array.length; i++) {
const div = document.createElement("div");
div.classList.add("DIV");
div.innerHTML = `
<span class="Text">${array[i].value}</span>
<span class="Remove" data-id="${"elem-" + i}">( Remove )</span>
`;
document.body.appendChild(div);
}
document.querySelectorAll(".Remove").forEach(elem => {
elem.addEventListener("click", () => {
remove(elem.dataset.id);
})
});
}
renderElement(array);
function remove(id) {
// Now how to remove the specific item from the array?
// Remove the `elem-` from id.
array.splice(+id.replace("elem-", ""), 1);
document.querySelectorAll("body > .DIV").forEach(elem => {
elem.remove();
});
renderElement(array);
}
// I hope this is fully understandable.
/* CSS IS NOT VERY IMPORTANT */
.DIV {
padding: 10px;
margin: 10px;
background: purple;
color: #fff;
border-radius: 10px;
display: flex;
justify-content: space-between;
}
.Remove {
cursor: pointer;
height: fit-content;
}
I generate nested div elements based on an object structure. With a click on the parent you can toggle the children.
Now I want to generate a path, separated with slashes, of the click sequence and the "selected" elements. When the user clicks on read -> news -> sport the string path should be "read/news/sport". When the user now clicks on read -> books the path should be now "read/books"
Here is my current version: https://codepen.io/iamrbn/pen/yEqPjG
let path = "";
let object = {
"design": {
"inspiration": {},
"news": {}
},
"read": {
"news": {
"sport": {}
},
"books": {}
},
"code": {}
}
let categoryContainer = document.querySelector(".categoryContainer")
function categoryTree(obj, parent, start = true) {
for (var key in obj) {
let div = document.createElement("div");
div.textContent = key;
div.classList.add("category");
if (parent.children) parent.className += " bold";
if (!start) div.className = "normal hide category";
div.addEventListener('click', function(e) {
e.stopPropagation()
this.classList.toggle('active');
Array.from(div.children).forEach(child => {
child.classList.toggle('hide');
})
})
categoryTree(obj[key], div, false)
parent.appendChild(div)
}
}
categoryTree(object, categoryContainer)
.category {
color: black;
display: block;
line-height: 40px;
background-color: RGBA(83, 86, 90, 0.2);
margin: 8px;
}
.category .category {
display: inline-block;
margin: 0 8px;
padding: 0 8px;
}
.category.hide {display: none;}
.category.normal {font-weight: normal;}
.category.bold {font-weight: bold;}
.category.active {color: red;}
<div class="categoryContainer"></div>
Here's one approach. Your existing code is unmodified except for adding a call to the new getParents() function, which works by crawling up the DOM tree recursively to generate the "path" to the clicked node:
let path = "";
let object = {
"design": {
"inspiration": {},
"news": {}
},
"read": {
"news": {
"sport": {}
},
"books": {}
},
"code": {}
}
let categoryContainer = document.querySelector(".categoryContainer")
function categoryTree(obj, parent, start = true) {
for (var key in obj) {
let div = document.createElement("div");
div.textContent = key;
div.classList.add("category");
if (parent.children) parent.className += " bold";
if (!start) div.className = "normal hide category";
div.addEventListener('click', function(e) {
e.stopPropagation()
this.classList.toggle('active');
Array.from(div.children).forEach(child => {
child.classList.toggle('hide');
})
var thePath = getParents(e.target); // <--- new
console.log(thePath)
})
categoryTree(obj[key], div, false)
parent.appendChild(div)
}
}
function getParents(node, path) {
// Cheat a bit here: we know the textnode we want is the first child node, so we don't have to iterate through all children and check their nodeType:
let thisName = node.childNodes[0].textContent;
path = path ? (thisName + "/" + path) : thisName ;
// iterate to parent unless we're at the container:
if (node.parentNode.className.split(/\s+/).indexOf("categoryContainer") !== -1) {
return path;
} else {
return getParents(node.parentNode, path);
}
}
categoryTree(object, categoryContainer)
.category {
color: black;
display: block;
line-height: 40px;
background-color: RGBA(83, 86, 90, 0.2);
margin: 8px;
}
.category .category {
display: inline-block;
margin: 0 8px;
padding: 0 8px;
}
.category.hide {
display: none;
}
.category.normal {
font-weight: normal;
}
.category.bold {
font-weight: bold;
}
.category.active {
color: red;
}
<div class="categoryContainer"></div>
I am trying to render multiple D3 force layouts on a page. I managed to initially render the layouts, but only the last graphs' nodes can be dragged a few seconds after render.
I had the same problem a while ago. The problem came up because d3.drag() and .tick() weren't pointing to the right d3.forceSimulation. They were pointing to another d3.forceSimulation I mistakenly declared in the global namespace.
This time around I have multiple d3.forceSimulation again, but that's because I do want to render multiple force layouts.
I tried to map over each force layout's dataset and call d3.forceSimulation and tick() with each set.
Now, should tick() be called only once for all the data? Or for each layout seperately? It seems as if tick keeps working for the last graph only. So how can tick be set for all force.simulation?
A live example can be found be here
///////////////////////////////////////////////////////////
/////// 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 / 5).y(nsp.height / 2))
.force("collide", d3.forceCollide([5]).iterations([5]));
},
enterNode = (selection) => {
var circle = selection.select('circle')
.attr("r", 25)
.style("fill", '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(" + d.x + "," + d.y + ")")
.attr("cx", function(d) {
return d.x = Math.max(30, Math.min(width - 30, d.x));
})
.attr("cy", function(d) {
return d.y = Math.max(30, Math.min(height - 30, d.y));
})
},
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 = d.x;
d.fy = d.y
},
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 = {
data: [{
name: "one",
id: 65,
nodes: [{
"name": "fruit",
"id": 0
},
{
"name": "apple",
"id": 1
},
{
"name": "orange",
"id": 2
},
{
"name": "banana",
"id": 3
}
],
links: [{
"source": 0,
"target": 1,
"lineID": 1
},
{
"source": 0,
"target": 2,
"lineID": 2
},
{
"source": 3,
"target": 0,
"lineID": 3
}
]
},
{
name: "two",
id: 66,
nodes: [{
"name": "Me",
"id": 0
},
{
"name": "Jim",
"id": 1
},
{
"name": "Bob",
"id": 2
},
{
"name": "Jen",
"id": 3
}
],
links: [{
"source": 0,
"target": 1,
"lineID": 1
},
{
"source": 0,
"target": 2,
"lineID": 2
},
{
"source": 1,
"target": 2,
"lineID": 3
},
{
"source": 2,
"target": 3,
"lineID": 4
},
]
}
]
}
}
componentDidMount() {
const data = this.state.data;
data.map(({
nodes,
links
}) => (
FORCE.initForce(nodes, links)
));
FORCE.tick(this)
FORCE.drag()
}
componentDidUpdate(prevProps, prevState) {
if (prevState.nodes !== this.state.nodes || prevState.links !== this.state.links) {
const data = this.state.data;
data.map(({
nodes,
links
}) => (
FORCE.initForce(nodes, links)
));
FORCE.tick(this)
FORCE.drag()
}
}
render() {
const data = this.state.data;
return (
<
div className = "result__container" >
<
h5 className = "result__header" > Data < /h5> {
data.map(({
name,
id,
nodes,
links
}) => ( <
div className = "result__box"
key = {
id
}
value = {
name
} >
<
h5 className = "result__name" > {
name
} < /h5> <
div className = {
"container__graph"
} >
<
svg className = "graph"
width = {
FORCE.width
}
height = {
FORCE.height
} >
<
g > {
links.map((link) => {
return ( <
Link key = {
link.lineID
}
data = {
link
}
/>);
})
} <
/g> <
g > {
nodes.map((node) => {
return ( <
Node data = {
node
}
label = {
node.label
}
key = {
node.id
}
/>);
})
} <
/g> < /
svg > <
/div> < /
div >
))
} <
/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'))
.container__graph {
background-color: lightsteelblue;
}
.result__header {
background-color: aliceblue;
text-align: center;
color: cadetblue;
text-transform: uppercase;
font-family: cursive;
}
.result__name {
background-color: bisque;
text-align: center;
text-transform: uppercase;
color: chocolate;
font-family: cursive;
margin-bottom: 10px;
padding: 6px;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></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>
For a quick answer to your question: you do need to use each individual force.on('tick',...) for each simulation, because the tick event doesn't fire after a period of inactivity. But your program is actually running into another problem that causes the other graphs to freeze.
The Problem:
The main issue is that you're creating multiple different force simulations, but attaching the same handler to all of them:
data.map(({
nodes,
links
}) => (
FORCE.initForce(nodes, links) // initializes multiple force simulations
));
FORCE.tick(this)
FORCE.drag() // attaches only one handler to them all
When you look at the code, you can see that you're reassigning nsp.force for each graph so it's only referring to the last one:
initForce = (nodes, links) => {
nsp.force = d3.forceSimulation(nodes)
...
},
This becomes a problem with your drag handler, which changes the alphaTarget, which more or less tells nsp.force (which now refers only to the last graph) that it needs to continuing simulating and to restart the simulation:
dragStarted = (d) => {
if (!d3.event.active) nsp.force.alphaTarget(0.3).restart();
// ...
},
dragEnded = (d) => {
if (!d3.event.active) nsp.force.alphaTarget(0);
// ...
},
So the reason why every graph but the last stops moving after a few seconds is because those simulation think they are complete because they aren't rewoken with a drag event.
A Solution:
As I'm sure you've encountered, it's tricky managing multiple force simulations as one big object. Since they seem to be independent, it makes more sense to create a separate instance for the force simulation for each graph. In the following code snippet, I've changed the object FORCE, to be constructor Force, that creates an instance of a force simulation, which we'll create multiple times, one for each graph.
I've also made ForceGraph, a React component to hold the graph and SVG, to separate out elements for each graph, and makes it easer for that force simulation to act on.
Here are the results:
///////////////////////////////////////////////////////////
/////// Functions and variables
///////////////////////////////////////////////////////////
function Force() {
var width = 1080,
height = 250,
enterNode = (selection) => {
var circle = selection.select('circle')
.attr("r", 25)
.style("fill", '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(" + d.x + "," + d.y + ")")
.attr("cx", function(d) { return d.x = Math.max(30, Math.min(width - 30, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(30, Math.min(height - 30, d.y)); })
},
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);
},
color = d3.scaleOrdinal(d3.schemeCategory10),
nsp = {},
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]));
},
dragStarted = (d) => {
if (!d3.event.active) nsp.force.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y
},
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 = (node) => {
var d3Graph = d3.select(ReactDOM.findDOMNode(node)).select('svg');
d3Graph.selectAll('g.node')
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragging)
.on("end", dragEnded));
},
tick = (node) => {
var d3Graph = d3.select(ReactDOM.findDOMNode(node)).select('svg');
nsp.force.on('tick', () => {
d3Graph.call(updateGraph)
});
};
nsp.enterNode = enterNode;
nsp.updateNode = updateNode;
nsp.enterLink = enterLink;
nsp.updateLink = updateLink;
nsp.width = width;
nsp.height = height;
nsp.initForce = initForce;
nsp.dragStarted = dragStarted;
nsp.dragging = dragging;
nsp.dragEnded = dragEnded;
nsp.drag = drag;
nsp.tick = tick;
return nsp
}
class ForceGraph extends React.Component {
constructor(props) {
super(props);
this.data = props.data;
this.force = Force();
}
initGraph() {
const data = this.data;
this.force.initForce(data.nodes, data.links)
this.force.tick(this)
this.force.drag(this)
}
componentDidMount() {
this.initGraph();
}
componentDidUpdate(prevProps, prevState) {
// TBD
}
render() {
const {name, id, nodes, links} = this.data;
const force = this.force;
return (
<div className="result__box" key={id} value={name}>
<h5 className="result__name">{name}</h5>
<div className={"container__graph"}>
<svg className="graph" width={force.width} height={force.height}>
<g>
{links.map((link) => {
return (
<Link
key={link.lineID}
data={link}
force={force}
/>);
})}
</g>
<g>
{nodes.map((node) => {
return (
<Node
data={node}
label={node.label}
force={force}
key={node.id}
/>);
})}
</g>
</svg>
</div>
</div>
);
}
};
////////////////////////////////////////////////////////////////////////////
/////// class App is the parent component of Link and Node
////////////////////////////////////////////////////////////////////////////
class App extends React.Component {
constructor(props){
super(props)
this.state = {
data: [{
name: "one",
id: 65,
nodes: [{
"name": "fruit",
"id": 0
},
{
"name": "apple",
"id": 1
},
{
"name": "orange",
"id": 2
},
{
"name": "banana",
"id": 3
}
],
links: [{
"source": 0,
"target": 1,
"lineID": 1
},
{
"source": 0,
"target": 2,
"lineID": 2
},
{
"source": 3,
"target": 0,
"lineID": 3
}
]
},
{
name: "two",
id: 66,
nodes: [{
"name": "Me",
"id": 0
},
{
"name": "Jim",
"id": 1
},
{
"name": "Bob",
"id": 2
},
{
"name": "Jen",
"id": 3
}
],
links: [{
"source": 0,
"target": 1,
"lineID": 1
},
{
"source": 0,
"target": 2,
"lineID": 2
},
{
"source": 1,
"target": 2,
"lineID": 3
},
{
"source": 2,
"target": 3,
"lineID": 4
},
]
}
]
}
}
render() {
const data = this.state.data;
return (
<div className="result__container">
<h5 className="result__header">Data</h5>
{data.map((graphData) => (<ForceGraph data={graphData} key={graphData.id} />))}
</div>
)
}
}
///////////////////////////////////////////////////////////
/////// Link component
///////////////////////////////////////////////////////////
class Link extends React.Component {
componentDidMount() {
this.d3Link = d3.select(ReactDOM.findDOMNode(this))
.datum(this.props.data)
.call(this.props.force.enterLink);
}
componentDidUpdate() {
this.d3Link.datum(this.props.data)
.call(this.props.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(this.props.force.enterNode)
}
componentDidUpdate() {
this.d3Node.datum(this.props.data)
.call(this.props.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'))
.container__graph {
background-color: lightsteelblue;
}
.result__header {
background-color: aliceblue;
text-align: center;
color: cadetblue;
text-transform: uppercase;
font-family: cursive;
}
.result__name {
background-color: bisque;
text-align: center;
text-transform: uppercase;
color: chocolate;
font-family: cursive;
margin-bottom: 10px;
padding: 6px;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></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>
Update: It looks like if you try to update the data for the charts, it runs into some snags. I tinkered with it a little bit and came up with this fiddle. It's not perfect, but it's a start: https://jsfiddle.net/4pyzL0tq/
Here i am try to update json data and its D3 wheel view but i Don't know why its not update the D3 sunbrust and update the data
This is my code
// in html body
//css code
svg{
width: 100% !important;
background:#ffffff;
}
.slice {
cursor: pointer;
}
.slice .main-arc {
stroke: #fff;
stroke-width: 1px;
}
.slice .hidden-arc {
fill: none;
}
.slice text {
pointer-events: none;
dominant-baseline: middle;
text-anchor: middle;
}
#tooltip { background-color: white;
padding: 3px 5px;
border: 1px solid black;
text-align: center;}
div.tooltip {
position: absolute;
text-align: left;
width: auto;
height: auto;
padding: 8px;
font: 12px sans-serif;
background: #0a2538;
border: #0a2538 1px solid;
border-radius: 0px;
pointer-events: none;
color: white;
}
//D3 Script code for https://d3js.org/d3.v4.min.js
const initItems ={
"name": "Core root",
"id":3,
"description":"Lorem ipsum dolor sit amet",
"children": [
{
"name": "VM.HSN.5.A",
"children": [
{
"name": "eiusmod",
"children": [
{"name": "G.8.1", "id": 5},
{"name": "G.8.2", "id": 4}
]
},
{
"name": "F.8.0",
"children": [
{"name": "F.8.4", "id": 1},
{"name": "F.8.5", "id":1}
]
},
{
"name": "EE.8.5-CLX",
"children": [
{"name": "EE.8.5-CLX2", "id": 1}
]
}
]
},
{
"name": "NS.8.2-CLX",
"children": [
{"name": "NS.8.2-CLX4", "id": 1},
{"name": "NS.8.2-CLX5", "id": 1},
{
"name": "NS.8.0",
"children": [
{"name": "NS.8.2", "id": 1},
{"name": "NS.8.3", "id": 1}
]
},
{"name": "NS.8.1-CLX1", "id":1},
{"name": "NS.8.1-CLX2", "id": 1},
{"name": "NS.8.1-CLX3", "id":1},
{"name": "NS.8.1-CLX4", "id": 1},
{"name": "NS.8.1-CLX5", "id": 1}
]
}
]
}
var newItems = {
"name": "Second Root",
"children": [{
"name": "A2",
"children": [{
"name": "B4",
"size": 40
}, {
"name": "B5",
"size": 30
}, {
"name": "B6",
"size": 10
}]
}, {
"name": "A3",
"children": [{
"name": "B7",
"size": 50
}, {
"name": "B8",
"size": 15
}
]
}]
}
const width = window.innerWidth,
height = window.innerHeight,
maxRadius = (Math.min(width, height) / 2) - 20;
const formatNumber = d3.format(',d');
const x = d3.scaleLinear()
.range([0, 2 * Math.PI])
.clamp(true);
const y = d3.scaleSqrt()
.range([maxRadius*.1, maxRadius]);
const color = d3.scaleOrdinal(d3.schemeCategory20);
const partition = d3.partition();
const arc = d3.arc()
.startAngle(d => x(d.x0))
.endAngle(d => x(d.x1))
.innerRadius(d => Math.max(0, y(d.y0)))
.outerRadius(d => Math.max(0, y(d.y1)));
const middleArcLine = d => {
const halfPi = Math.PI/2;
const angles = [x(d.x0) - halfPi, x(d.x1) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) { angles.reverse(); }
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
return path.toString();
};
const textFits = d => {
const CHAR_SPACE = 6;
const deltaAngle = x(d.x1) - x(d.x0);
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const perimeter = r * deltaAngle;
return d.data.name.length * CHAR_SPACE < perimeter;
};
const svg = d3.select('#vdata').append('svg')
.style('width', '100vw')
.style('height', '100vh')
.attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
.on('click', () => focusOn()); // Reset zoom on canvas click
var updateChart = function (items) {
// var root = items;
console.log(items);
//d3.json('dummy4.json', (error, root) => {
///if (error) throw error;
//start custom code
var root = d3.hierarchy(items, function(d) { return d.children })
.sum( function(d) {
if(d.children) {
return 0
} else {
return 2
}
});
//end custom code
//root = d3.hierarchy(root);
//var ad =root.sum(d => d.depth);
//root.sum(d => d.id);
const slice = svg.selectAll('g.slice')
.data(partition(root).descendants());
slice.exit().remove();
const newSlice = slice.enter()
.append('g').attr('class', 'slice')
.on('click', d => {
console.log(d.data.name);
d3.event.stopPropagation();
focusOn(d);
});
newSlice.append('title')
//.text(d => d.data.name + '\n' + d.data.id);
//.on("mouseover", mouseover);
newSlice.append('path')
.attr('class', 'main-arc')
.style('fill',colour)
//.style('fill', d => color((d.children ? d : d.parent).data.name))
//console.log(data.id)
.on("mouseover", mouseover)
.on("mouseout", mouseOutArc)
.attr('d', arc);
newSlice.append('path')
.attr('class', 'hidden-arc')
.attr('id', (_, i) => `hiddenArc${i}`)
.attr('d', middleArcLine);
const text = newSlice.append('text')
.attr('display', d => textFits(d) ? null : 'none');
// Add white contour
text.append('textPath')
.attr('startOffset','50%')
.attr('xlink:href', (_, i) => `#hiddenArc${i}` )
.text(function(d) {
return d.data.name;
})
.style('fill', 'none')
.style('stroke', '#fff')
.style('stroke-width', 5)
.style('stroke-linejoin', 'round');
text.append('textPath')
.attr('startOffset','50%')
.attr('xlink:href', (_, i) => `#hiddenArc${i}` )
.text(function(d) {
return d.data.name;
});
//});
}
// tooltip
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
function colour(d) {
//if (d.id==5) {
var colours;
console.log(d.data.id);
if(d.data.id==5)
{
// There is a maximum of two children!
colours ='#b00';
}else if(d.data.name=='Apps'){
colours='#cc0001';
}
else{
colours=color((d.children ? d : d.parent).data.name)
} // L*a*b* might be better here...
// return d3.hsl((a.h + b.h) / 2, a.s * 1.2, a.l / 1.2);
//}
return colours;
}
function mouseover(d) {
d3.select(this).style("cursor", "pointer")
var descript;
if(d.data.description!=null)
{
descript= "<br/><br/>"+d.data.description;
}else{
descript='';
}
tooltip.html(d.data.name + descript)
.style("opacity", 0.8)
.style("left", (d3.event.pageX) + 0 + "px")
.style("top", (d3.event.pageY) - 0 + "px");
}
function mouseOutArc(){
d3.select(this).style("cursor", "default")
tooltip.style("opacity", 0);
}
function focusOn(d = { x0: 0, x1: 1, y0: 0, y1: 1 }) {
// Reset to top-level if no data point specified
const transition = svg.transition()
.duration(750)
.tween('scale', () => {
const xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]);
return t => { x.domain(xd(t)); y.domain(yd(t)); };
});
transition.selectAll('path.main-arc')
.attrTween('d', d => () => arc(d));
transition.selectAll('path.hidden-arc')
.attrTween('d', d => () => middleArcLine(d));
transition.selectAll('text')
.attrTween('display', d => () => textFits(d) ? null : 'none');
moveStackToFront(d);
//
function moveStackToFront(elD) {
svg.selectAll('.slice').filter(d => d === elD)
.each(function(d) {
this.parentNode.appendChild(this);
if (d.parent) { moveStackToFront(d.parent); }
})
}
}
updateChart(initItems);
I am trying to update json data and regenerate the D3 sunburst wheel but it not update.
On developer console it show the json data updated but it not update the text in wheel and not properly regenerate the D3 sunburst wheel.
Please help me out.!
Thanks in advance.
I used this code and it works for me great..!
setTimeout(function () {d3.selectAll("svg > *").remove(); updateChart(newItems); }, 2000);
I would like to show the nested JSON values in table header and row as per screenshot.
However, my current code show me as per below.
How can I get the result of the first screenshot?
Here is my code:
var tbl_ss564_ib_jsonData = [
{
"S_No": "1",
"SS564 Metric": "Power Usage Effectiveness(PUE)",
"Baseline": "2.2*",
"DC": [
{"A": "2.4"},
{"B": "2.61"},
{"C": "2.46"},
{"D": "2.25"},
{"E": "2.11"},
{"F": "3.75"},
{"G": "2.08"},
{"H": "2.17"},
{"I": "1.85"},
{"J": "2.57"},
{"K": "2.42"}
]
}
]
var sortAscending = true;
var tbl_ss564_ib = d3.select('#ss564_ib_page_wrap').append('table');
var title_ss564_ib = d3.keys(tbl_ss564_ib_jsonData[0]);
var header_ss564_ib = tbl_ss564_ib.append('thead').append('tr')
.selectAll('th')
.data(title_ss564_ib).enter()
.append('th')
.text(function (d) {
return d;
})
.on('click', function (d) {
header_ss564_ib.attr('class', 'header');
if (sortAscending) {
rows.sort(function(a, b) { return b[d] < a[d]; });
sortAscending = false;
this.className = 'aes';
} else {
rows.sort(function(a, b) { return b[d] > a[d]; });
sortAscending = true;
this.className = 'des';
}
});
var rows = tbl_ss564_ib.append('tbody').selectAll('tr')
.data(tbl_ss564_ib_jsonData).enter()
.append('tr');
rows.selectAll('td')
.data(function (d) {
return title_ss564_ib.map(function (k) {
return { 'value': d[k], 'name': k};
});
}).enter()
.append('td')
.attr('data-th', function (d) {
return d.name;
})
.text(function (d) {
return d.value;
});
* {
margin: 0;
padding: 0;
}
#ss564_ib_page_wrap body {
font: 14px/1.4 Georgia, Serif;
}
#ss564_ib_page_wrap {
margin: 20px;
}
p {
margin: 20px 0;
}
/*
Generic Styling, for Desktops/Laptops
*/
#ss564_ib_page_wrap table {
width: 100%;
border-collapse: collapse;
}
/* Zebra striping */
#ss564_ib_page_wrap tr:nth-of-type(odd) {
background: #eee;
}
#ss564_ib_page_wrap th {
background: Teal;
font-weight: bold;
cursor: s-resize;
background-repeat: no-repeat;
background-position: 3% center;
}
#ss564_ib_page_wrap td, th {
padding: 6px;
border: 1px solid #ccc;
text-align: left;
}
#ss564_ib_page_wrap th.des:after {
content: "\21E9";
}
#ss564_ib_page_wrap th.aes:after {
content: "\21E7";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="ss564_ib_page_wrap"> </div>
Without changing your D3 code, the easiest solution is just manipulating your data array beforehand:
tbl_ss564_ib_jsonData[0].DC.forEach(function(d, i) {
for (var key in d) {
tbl_ss564_ib_jsonData[0][key] = d[key];
}
});
delete tbl_ss564_ib_jsonData[0].DC;
Here is your code with that change:
var tbl_ss564_ib_jsonData = [{
"S_No": "1",
"SS564 Metric": "Power Usage Effectiveness(PUE)",
"Baseline": "2.2*",
"DC": [{
"A": "2.4"
},
{
"B": "2.61"
},
{
"C": "2.46"
},
{
"D": "2.25"
},
{
"E": "2.11"
},
{
"F": "3.75"
},
{
"G": "2.08"
},
{
"H": "2.17"
},
{
"I": "1.85"
},
{
"J": "2.57"
},
{
"K": "2.42"
}
]
}];
tbl_ss564_ib_jsonData[0].DC.forEach(function(d, i) {
for (var key in d) {
tbl_ss564_ib_jsonData[0][key] = d[key];
}
});
delete tbl_ss564_ib_jsonData[0].DC;
var sortAscending = true;
var tbl_ss564_ib = d3.select('#ss564_ib_page_wrap').append('table');
var title_ss564_ib = d3.keys(tbl_ss564_ib_jsonData[0]);
var header_ss564_ib = tbl_ss564_ib.append('thead').append('tr')
.selectAll('th')
.data(title_ss564_ib).enter()
.append('th')
.text(function(d) {
return d;
})
.on('click', function(d) {
header_ss564_ib.attr('class', 'header');
if (sortAscending) {
rows.sort(function(a, b) {
return b[d] < a[d];
});
sortAscending = false;
this.className = 'aes';
} else {
rows.sort(function(a, b) {
return b[d] > a[d];
});
sortAscending = true;
this.className = 'des';
}
});
var rows = tbl_ss564_ib.append('tbody').selectAll('tr')
.data(tbl_ss564_ib_jsonData).enter()
.append('tr');
rows.selectAll('td')
.data(function(d) {
return title_ss564_ib.map(function(k) {
return {
'value': d[k],
'name': k
};
});
}).enter()
.append('td')
.attr('data-th', function(d) {
return d.name;
})
.text(function(d) {
return d.value;
});
* {
margin: 0;
padding: 0;
}
#ss564_ib_page_wrap body {
font: 14px/1.4 Georgia, Serif;
}
#ss564_ib_page_wrap {
margin: 20px;
}
p {
margin: 20px 0;
}
/*
Generic Styling, for Desktops/Laptops
*/
#ss564_ib_page_wrap table {
width: 100%;
border-collapse: collapse;
}
/* Zebra striping */
#ss564_ib_page_wrap tr:nth-of-type(odd) {
background: #eee;
}
#ss564_ib_page_wrap th {
background: Teal;
font-weight: bold;
cursor: s-resize;
background-repeat: no-repeat;
background-position: 3% center;
}
#ss564_ib_page_wrap td,
th {
padding: 6px;
border: 1px solid #ccc;
text-align: left;
}
#ss564_ib_page_wrap th.des:after {
content: "\21E9";
}
#ss564_ib_page_wrap th.aes:after {
content: "\21E7";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="ss564_ib_page_wrap"> </div>