generate string path out of a click sequence - javascript

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>

Related

How to delete a certain item of an array?

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;
}

Is there any way to get unique random sub arrays form array, each random array shouldn't has same element? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I need to show team members on the website. The site should show four team member profiles at a time. Since the team members are more than four, it should display team members randomly. A team member image should not repeat unless all other items have been shown at least once.
I was stuck at pickRandomMembers. Here if (displayedArrayStore.length <= 4) has to be false for second call but is true. If someone finds a solution, help me. And while executing members are adding to the existing ones but members have to replace.
Thanks
I have generated an array with random elements then displayed member cards with this array the first time
Next, I have stored that unique array for comparing with the next generated random unique array, but I am failing at comparing.
I have generated an Array with random elements then displayed member cards
window.onload = function () {
let container = document.querySelector('.container');
const memsToDisplay = 4,
path = 'assets/images/',
/* we can get data form API call and store data as follows, intsead i am taking my own */
teamMembers = [
{
memId: 101,
avathar: 'avathar.png',
memName: 'Srinivas ',
activities: 'Learn something NEW',
},
{
memId: 102,
avathar: 'avathar.png',
memName: 'Kevin Powell',
activities: 'Teaches CSS',
},
{
memId: 103,
avathar: 'avathar.png',
memName: 'Kyle Simpson',
activities: 'Teaches JS',
},
{
memId: 104,
avathar: 'avathar.png',
memName: 'Brendan Hufford',
activities: 'Teaches SEO',
},
{
memId: 105,
avathar: 'avathar.png',
memName: 'Gary Simon',
activities: 'Teaches Designing',
},
{
memId: 106,
avathar: 'avathar.png',
memName: 'Zell Leiw',
activities: 'Teaches JS',
},
{
memId: 107,
avathar: 'avathar.png',
memName: 'DEV ED',
activities: 'Teaches WEB',
},
{
memId: 108,
avathar: 'avathar.png',
memName: 'Eddie',
activities: 'Teaches Math',
},
];
let displayedArrayStore = [],
finalRandomArray = [],
isMoreThanFourUsers = false;
/* Team member Card Making */
function makingMemberCard(memsArrayUniuqe) {
console.log(memsArrayUniuqe);
memsArrayUniuqe.map((member, index) => {
const card = document.createElement('div');
const image = document.createElement('img');
const name = document.createElement('p');
const desc = document.createElement('p');
/* Assigning class names */
card.classList.add('memberCard');
image.classList.add('avathar');
name.classList.add('memName');
desc.classList.add('activity');
/* Assigning data to it */
image.setAttribute(
'src',
`${path.concat(teamMembers[member].avathar)}`
);
name.innerText = ` ${teamMembers[member].memId} - ${teamMembers[member].memName}`;
desc.innerText = teamMembers[member].activities;
/* Appending to parent elements*/
container.append(card);
card.append(image);
card.append(name);
card.append(desc);
});
}
/* Removing Data with clearInterval */
function removingMembers() {
let container = document.querySelector('.container');
container.remove();
}
/* Generating Random Numbers */
function generatingRandomNumbers(members, memsToDisplay) {
console.log('generating');
let randomCheckArray;
if (members.length > memsToDisplay) {
randomCheckArray = new Set();
while (randomCheckArray.size !== 4) {
randomCheckArray.add(Math.floor(Math.random() * members.length));
}
finalRandomArray = Array.from(randomCheckArray);
return finalRandomArray;
} else {
finalRandomArray = members;
}
randomCheckArray = null;
}
function pickRandomMembers(members, memsToDisplay = 4) {
let teamMembersCopy = JSON.stringify(members);
let randomNumberArray = generatingRandomNumbers(
members,
memsToDisplay
);
if (members.length < 4) {
makingMemberCard(members);
cosnole.log('line 220');
} else {
if (displayedArrayStore.length <= 4) {
console.log('if pick');
//displayedArrayStore = [...randomNumberArray];
console.log(displayedArrayStore);
setInterval(
() =>
makingMemberCard(
generatingRandomNumbers(members, memsToDisplay)
),
5000
);
//makingMemberCard(generatingRandomNumbers(members, memsToDisplay));
//generatingRandomNumbers(members, memsToDisplay);
console.log('if after');
} else {
console.log('else pick');
displayedArrayStore = [...randomNumberArray];
for (let i = 0; i < randomNumberArray.length; i++) {
if (displayedArrayStore.indexOf(randomNumberArray[i]) === -1) {
setInterval(makingMemberCard(randomNumberArray), 5000);
//clearInterval(removingMembers(), 5000);
} else {
generatingRandomNumbers(members, memsToDisplay);
}
}
}
}
}
pickRandomMembers(teamMembers, memsToDisplay);
};
#import url('https://fonts.googleapis.com/css2?family=Roboto:wght#400;700&display=swap');
:root {
--primary-clr: rgba(255, 255, 255, 0.8);
--secondary-clr: rgba(0, 0, 0, 0.8);
--fontFamily: 'roboto', sans-serif;
--fontSize: 16px;
}
*,
::after,
::before {
box-sizing: border-box;
}
body {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
overflow-x: hidden;
background-color: var(--primary-clr) !important;
}
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
height: auto;
padding: 80px;
gap: 20px;
}
.memberCard {
flex: 1 1 20%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: var(--secondary-clr);
color: var(--primary-clr);
font-family: var(--fontFamily);
padding: 20px;
border-radius: 1em;
}
.avathar {
margin: 20px 0;
}
.memName {
font-size: calc(var(--fontSize) + 2);
margin: 10px 0;
font-weight: bold;
}
.activity {
font-size: var(--font-size);
margin: 10px 0;
}
#media only screen and (min-width: 768px) and (max-width: 1023px) {
.container {
flex-direction: row;
flex-wrap: wrap;
padding: 20px;
gap: 20px;
}
.memberCard {
flex: 1 1 48%;
}
}
#media (max-width: 767px) {
.container {
flex-direction: column;
padding: 20px;
}
.memberCard {
flex: none;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Site</title>
<link href="style.css" type="text/css" rel="stylesheet" />
<script src="script.js" type="text/javascript"></script>
</head>
<body>
<div class="container"></div>
</body>
</html>
You can create a random-selector function like this:
function getRandom(arr, x) {
const copy = arr.slice(0);
const selected = [];
for (let i = 0; i < x; i++) {
const randomIndex = Math.floor(Math.random() * copy.length);
const item = copy.splice(randomIndex, 1)[0];
selected.push(item);
}
return selected;
}
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
test.onclick = () => {
const selected = getRandom(items, 4);
console.log(selected.join(' '));
}
<button id=test>Test</button>
Having a function like this, you can do whatever you want with the selected array.
Also, give a look at the docs of Array.splice and Array.slice.

moving the content at the end of a table row

I'm having this issues with the below table in which I want to set the 'view book' link and reserved to be the last in the row, but apparently the x = TABLE_ROW.insertCell(-1) is not working.
I just want to have the link/span at the end of the table row. Shouldn't the -1 argument be enough for this?
Can you please help me identifying what I'm doing wrong?
var data = {
"headings": {
"propBook": "Book",
"propAuthor": "Author",
"propYear": "Year",
},
"items": [{
"fields": {
"propBook": "The Great Gatsby",
"propAuthor": "F Scott Fitzgerald",
"propYear": "1925",
},
"button": {
"name": "View book",
"propURL": "https://google.com"
}
},
{
"fields": {
"propBook": "The Grapes of Wrath",
"propAuthor": "John Steinbeck",
"propYear": "1939",
},
"button": {
"name": "View book",
"propURL": ""
}
},
{
"fields": {
"propBook": "A Wild Sheep Chase",
"propAuthor": "Haruki Murakami",
"propYear": "1982",
},
"button": {
"name": "View book",
"propURL": "https://google.com"
}
}
]
}
const HEADINGS = data.headings;
const ITEMS = data.items;
const TABLE_WRAPPER = document.querySelector('.book-component .table-wrapper');
const TABLE = document.createElement('table');
TABLE.setAttribute('class', 'pagination');
TABLE_WRAPPER.appendChild(TABLE);
for (const field in data) {
const TABLE_ROW = document.createElement('tr');
TABLE_ROW.setAttribute('id', 'myRow');
if (field == 'headings') {
for (const child in HEADINGS) {
const HEADER_CELL = document.createElement('th');
TABLE_ROW.appendChild(HEADER_CELL);
HEADER_CELL.setAttribute('class', 'sort-cta');
HEADER_CELL.innerText = HEADINGS[child];
TABLE.appendChild(TABLE_ROW);
}
} else if (field == 'items') {
for (const child in ITEMS) {
const TABLE_ROW = document.createElement('tr');
let item = ITEMS[child].fields;
let btn = ITEMS[child].button;
if (btn.propURL !== '') {
let link = document.createElement('a');
link.setAttribute('href', btn.propURL);
link.innerHTML = btn.name;
x = TABLE_ROW.insertCell(-1);
x.appendChild(link);
} else {
let link = document.createElement('span');
link.innerHTML = 'Reserved';
x = TABLE_ROW.insertCell(-1);
x.appendChild(link);
}
for (const row in item) {
const TABLE_DATA = document.createElement('td');
TABLE_ROW.appendChild(TABLE_DATA);
TABLE_DATA.innerText = item[row];
TABLE.appendChild(TABLE_ROW);
}
}
}
}
tr.inactive {
display: none;
}
.table-wrapper {
display: flex;
flex-direction: column-reverse;
}
.pager {
display: flex;
justify-content: center;
padding: 0;
margin-top: 10px;
font-weight: 800;
}
.pager-item.selected {
outline: none;
border-color: #0077cc;
background: #0077cc;
color: #fff;
cursor: default;
}
<div class="book-component">
<div class="table-wrapper">
</div>
</div>
You just should inset cell data first. Then insert the link cell at the end of a table row.
http://jsfiddle.net/u1bvq376/
Here is a sample. Hope to help, my friend :))
for (const field in data) {
const TABLE_ROW = document.createElement('tr');
TABLE_ROW.setAttribute('id', 'myRow');
if (field == 'headings') {
for (const child in HEADINGS) {
const HEADER_CELL = document.createElement('th');
TABLE_ROW.appendChild(HEADER_CELL);
HEADER_CELL.setAttribute('class', 'sort-cta');
HEADER_CELL.innerText = HEADINGS[child];
TABLE.appendChild(TABLE_ROW);
}
} else if (field == 'items') {
for (const child in ITEMS) {
const TABLE_ROW = document.createElement('tr');
let item = ITEMS[child].fields;
let btn = ITEMS[child].button;
// insert the cell data first
for (const row in item) {
const TABLE_DATA = document.createElement('td');
TABLE_ROW.appendChild(TABLE_DATA);
TABLE_DATA.innerText = item[row];
TABLE.appendChild(TABLE_ROW);
}
// then insert the link
if (btn.propURL !== '') {
let link = document.createElement('a');
link.setAttribute('href', btn.propURL);
link.innerHTML = btn.name;
x = TABLE_ROW.insertCell(-1);
x.appendChild(link);
} else {
let link = document.createElement('span');
link.innerHTML = 'Reserved';
x = TABLE_ROW.insertCell(-1);
x.appendChild(link);
}
}
}
}

How to show nested JSON value in table header & row

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>

How to create a d3 force layout graph using React

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>

Categories