This application is a visualization tool which helps the users to render a tree based on the data feed. It idenitifies the hierarchy in the data based on few parameters like 'parent' and 'dept' etc.
https://objective-minsky-1384e5.netlify.app/
- 🌈 Materialize CSS
- 🟨 Javascript (ES6+)
- 🖋️ D3.JS
- 🗃️ Firebase (Firestore)
Add the following CDN at the end of the body tag in the index.html
<script src="https://d3js.org/d3.v5.js"></script>
Get the below code from console.firebase.google.com and check out, Adding this project to the web app
<script src="https://www.gstatic.com/firebasejs/7.19.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.19.0/firebase-firestore.js"></script>
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "AIzaSyDQQN1E2V8K8wFqjHKiSjimAHvjagZst0k",
authDomain: "d3-firebase-starter.firebaseapp.com",
databaseURL: "https://d3-firebase-starter.firebaseio.com",
projectId: "d3-firebase-starter",
storageBucket: "d3-firebase-starter.appspot.com",
messagingSenderId: "1020387918909",
appId: "1:1020387918909:web:508221bf07b68bc2079654",
measurementId: "G-XJ376VQ9BE",
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
</script>
When the add button is clicked, a modal is opened with the form to submit the details to firestore
const modal = document.querySelector(".modal");
M.Modal.init(modal);
Making the application interactive and managing the data back and forth.
These are the DOM elements through which the data is send to the back-end. In our form there are 3 fields
- Name (Name identifier)
- Parent (Parent identifier)
- Dept (Department corresponding to)
const form = document.querySelector("form");
const name = document.querySelector("#name");
const parent = document.querySelector("#parent");
const dept = document.querySelector("#dept");
When the user submits the form, prevent few default actions and send the data to the firestore
form.addEventListener("submit", (e) => {
// Prevent default
e.preventDefault();
// Form Validations
if (name.value && dept.value) {
// Store the data in the firestore
db.collection("employees").add({
name: name.value,
parent: parent.value,
dept: dept.value,
});
let instanceOfModal = M.Modal.getInstance(modal);
instanceOfModal.close();
// Reset the form
form.reset();
}
});
Handling the form when no data is entered by adding the key event listeners to the corresponding fields and showing the error messages
Name field
name.addEventListener("keyup", (e) => {
if (name.value.length > 0) {
document.querySelector("#name + .error").textContent = "";
} else {
document.querySelector("#name + .error").textContent =
"Please enter the name";
}
});
Dept field
dept.addEventListener("keyup", (e) => {
if (dept.value.length > 0) {
document.querySelector("#dept + .error").textContent = "";
} else {
document.querySelector("#dept + .error").textContent =
"Please enter the department";
}
});
We can use onSnapshot() method of the firestore. This method accepts a call-back as an argument with res as its parameter.
- Apply this method on our collection where the data is stored
- There are 3 cases of data alteration in firestore
- Added: When a new document is added to the collection.
- Modified: When an existing document properties are altered or new properties are added to an existing document.
- Deleted: When an existing document is deleted.
db.collection("employees").onSnapshot((res) => {
res.docChanges().forEach((change) => {
const doc = { ...change.doc.data(), id: change.doc.id };
switch (change.type) {
case "added":
data.push(doc);
break;
case "modified":
const index = data.findIndex((item) => item.id == doc.id);
data[index] = doc;
break;
case "removed":
data = data.filter((item) => item.id !== doc.id);
break;
default:
break;
}
});
update(data);
});
The dimensions of the tree diagram and the SVG canvas
const dims = { height: 500, width: 1100 };
const svg = d3
.select(".canvas")
.append("svg")
.attr("width", dims.width + 100)
.attr("height", dim.height + 100);
Append the graph group to the SVG and transform i.e. move the graph 50 in the each direction so that it has some room to breathe
const graph = svg.append("g").attr("transform", "translate(50, 50)");
Create the graph and update the visualizations
We need to segregate the data based on the identifiers and parents, stratify is used for this purpose
const stratify = d3
.stratify()
.id((d) => d.name)
.parentId((d) => d.parent);
Update function to re-render the visualizations
const update = (data) => {
// Get updated root node data
const rootNode = stratify(data);
...
...
};
Using the tree generator to generate the tree diagram based on the dimensions
Creating the tree generator
const tree = d3.tree().size([dims.width, dims.height]);
Passing the data
const treeData = tree(rootNode);
Till this point nothing is being added to the DOM to render the tree, firstly joining the data to get the enter and exit selections. We use the descendants() method to convert the Node object to an array which is the only acceptable format of data()
const nodes = graph.selectAll(".node").data(treeData.descendants());
Transform the nodes based on the x and y positions generated by the tree generator
Appending the groups
const enterNodes = nodes
.enter()
.append("g")
.attr("class", "node")
.attr("transform", (d) => `translate(${d.x}, ${d.y})`);
Appending the rects to enter nodes
Here we determine the width of each node based on length of the name
enterNodes
.append("rect")
.attr("fill", "#aaa")
.attr("stroke", "#555")
.attr("stroke-width", 2)
.attr("height", 50)
.attr("width", (d) => d.data.name.length * 20);
Appending the text elements to enter nodes
enterNodes
.append("text")
.attr("text-anchor", "middle")
.attr("fill", "#fff")
.text((d) => d.data.name);
To fulfill the purpose of a tree, it needs to have some connections links from one node to another
Joining the data to the links
We use links() method to convert the data to the form which generates the paths of drawing the links
const links = graph.selectAll(".link").data(treeData.links());
Appending the paths
Above joined data add the x and y* co-ordinates required for the path elements drawn through d3.linkVertical()
links
.enter()
.append("path")
.attr("class", "link")
.attr("fill", "none")
.attr("stroke", "#aaa")
.attr("stroke-width", 2)
.attr(
"d",
d3
.linkVertical()
.x((d) => d.x)
.y((d) => d.y)
);
Current rects positions needs to translated based on the text (name). Move half-way top and move half way left based on the text length
.attr("transform", (d) => {
let x = d.data.name.length * 10;
return `translate(-${x}, -25)`;
})
Once the new nodes are added, the current paths needs to re-calculated and drawn coercively. For this app, re-calculating all the positions is bit tricky hence we can remove the rects and links every time the update() function is triggered. This makes the re-draw and no complex issues are raised
graph.selectAll(".node").remove();
graph.selectAll(".link").remove();
We can color the nodes based on departments. Using an ordinal scale, we can pass a range of colors with domain of depts and fill the rects based on the departments.
Creating the ordinal scale
We can use a range of values or any scheme set for random colors
const color = d3.scaleOrdinal(d3["schemeSet2"]);
Passing the domains on update
Depts needs to be based as the domain of the ordinal scale
color.domain(data.map((item) => item.dept));
Using the ordinal scale
Changing the color of rects by passing them through ordinal scale and giving the particular dept name
.attr("fill", (d) => color(d.data.dept))
- Twitter : @malsaslam97
- Github: @AssSam7
- LinkedIn: Aslam Mohammed
Give a ⭐️ if you feel this application has some credibility