@eo4geo/curr-viz
Version:
EO4GEO-curr-viz is an script to visualize curricula in a tree layout
615 lines (529 loc) • 16.6 kB
JavaScript
var d3 = require("d3");
var currentSelectedD = null;
var i = 0,
duration = 750,
root,
treemap,
svg,
arc,
pie,
width,
height;
var outerRadius = 10;
var innerRadius = 0;
var proportionsCode = {
GI: 0,
IP: 1,
CF: 2,
CV: 3,
DA: 4,
DM: 5,
DN: 6,
PS: 7,
GD: 8,
GS: 9,
AM: 10,
MD: 11,
OI: 12,
GC: 13,
PP: 14,
SD: 15,
SH: 16,
TA: 17,
WB: 18,
no: 19
}
var codeColors = [
"#40e0d0",
"#1f77b4",
"#aec7e8",
"#ff7f0e",
"#ffbb78",
"#2ca02c",
"#98df8a",
"#d62728",
"#cc5b59",
"#9467bd",
"#8c564b",
"#8c564b",
"#c49c94",
"#e377c2",
"#f7b6d2",
"#7f7f7f",
"#c7c7c7",
"#bcbd22",
"#07561e",
"#17becf"
]
var colorForNode = "#75DCCD";
var colorForSelectedNode = "#096B5D";
//displays tree graph
exports.displayCurricula = function (tag = 'tree', treeData = treeData, initialWidth = 500, initialHeight = 650) {
currentSelectedD = null;
// Set the dimensions and margins of the diagram
var margin = {
top: 50,
right: 15,
bottom: 30,
left: 5
};
width = initialWidth - margin.left - margin.right,
height = initialHeight - margin.top - margin.bottom;
d3.select("#" + tag).select("svg").remove();
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
svg = d3.select("#" + tag).append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.attr("id", "svgGraphTree")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// declares a tree layout and assigns the size
treemap = d3.tree().size([width, height]);
arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
pie = d3.pie();
if (treeData != null) {
if (treeData.children && treeData.children.length != 0) {
// tree is not already created
if (!treeData.children[0].data) {
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function (d) {
return d.children;
});
root.x0 = width / 2;
root.y0 = 180 * root.data.depth;
// root.y0 = 0;
}
} else { // new SP
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function (d) {
return d.children;
});
root.x0 = width / 2;
root.y0 = 180 * root.data.depth;
// root.y0 = 0;
}
currentSelectedD = root;
exports.update(root);
}
},
exports.update = function (source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function (d) {
d.y = d.data.depth * 180
});
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function (d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("id", function (d) {
return "node" + d.id; // d.data.name;
})
.attr("name", function (d) {
return "node" + d.id;
})
.attr("transform", function (d) {
if (source && source.x0) {
console.log("translate(" + source.x0 + "," + source.y0 + ")");
return "translate(" + source.x0 + "," + source.y0 + ")";
} else {
console.log("translate(" + 200 + "," + 0 + ")");
return "translate(200,0)";
}
})
.on('click', clickNodeTree)
.style("fill", function (d) {
if (currentSelectedD.id == d.id)
return colorForSelectedNode;
else
return colorForNode;
})
.style("fill-opacity", 0.8)
.on("mouseover", function (d) {
d3.select(this).select('text')
.text(function (d) {
return d.data.name;
})
.style("fill-opacity", 1)
.attr("y", function (d) {
if (d.id % 2 == 0) {
return 42;
} else {
return -30;
}
})
}).on("mouseout", function (d) {
d3.select(this).select('text')
.text(function (d) {
if (d.data && d.data.name && d.data.name.length > 25) {
return d.data.name.substring(0, 15) + "...";
} else {
return d.data.name;
}
})
.style("fill-opacity", 0.7)
.attr("y", function (d) {
if (d.id % 2 == 0) {
return 30;
} else {
return -18;
}
})
});
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style("stroke-linecap", "round")
.style("stroke-width", "1px")
.style("stroke", "steelblue")
.style("fill", function (d) {
if (currentSelectedD.id == d.id)
return colorForSelectedNode;
else
return colorForNode;
})
.style("fill-opacity", 0.8);
// Add labels for the nodes
nodeEnter.append('text')
.attr("y", function (d) {
if (d.id % 2 == 0) {
// return d.depth < 4 ? -18 : 18;
return 30;
} else {
return -18;
}
}) //if it's last level, text goes below node, else above node
.attr("dy", -1)
.attr("x", function (d) {
return 0; // return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(function (d) {
return d.data.name;
})
.style("fill-opacity", 0.5);
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr("r", function (d) {
if (currentSelectedD.id == d.id)
return 15;
else
return 10;
})
.style("fill", function (d) { //change Node color depending on type
if (currentSelectedD.id == d.id)
return colorForSelectedNode;
else
return colorForNode;
})
.style("fill-opacity", 0.8)
.attr('cursor', 'pointer');
nodeUpdate.select("text")
.text(function (d) {
var abbr = false;
if (d.data && d.data.name && d.data.name.length > 30) {
return d.data.name.substring(0, 20) + "...";
} else {
return d.data.name;
}
}).attr("dy", function (d) {
if (d.children) {
return -1 - d.children.length * 1.5;
} else {
return -1;
}
})
.style("fill-opacity", 0.7);
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function (d) {
return d.id;
});
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.style("stroke-width", function (d) {
return d.data.ects > 0 ? 15 + d.data.ects * 3 : 15;
})
.style("stroke", colorForNode)
.style("fill", "none")
.style("stroke-linecap", "round")
.style("stroke-opacity", "0.2")
.attr('d', function (d) {
var o = {
x: source.x0,
y: source.y0
}
return diagonal(o, o)
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function (d) {
return diagonal(d, d.parent)
});
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function (d) {
var o = {
x: source.x,
y: source.y
}
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
calculateProportionsForPie(root);
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = `M ${s.x} ${s.y}
C ${(s.x + d.x) / 2} ${s.y},
${(s.x + d.x) / 2} ${d.y},
${d.x} ${d.y}`
return path
}
// Toggle children on click.
function toggleNodes(d) {
if ((d.children && d.children.length > 0) || (d._children && d._children.length > 0)) {
if (d.children) {
d.isClosed = true;
d._children = d.children ? d.children : d.data.children ? d.data.children : null;
d.children = null;
} else {
d.isClosed = false;
d.children = d._children ? d._children : d.data._children ? d.data._children : null;
d._children = null;
}
}
}
function clickNodeTree(d) {
//if clicked twice, hide or show children
if (currentSelectedD.id == d.id) { //already selected
toggleNodes(d);
} else { // new selection
currentSelectedD = d;
}
exports.update(d);
// redraw pies when parent closed
drawPieForChildren(currentSelectedD);
}
function drawPieForChildren(node) {
if (node.children && node.children.length > 0) {
for (var j = 0; j < node.children.length; j++) {
drawPieForChildren(node.children[j]);
drawPie(node.children[j]);
}
}
}
function drawPie(pieNode) {
var colorPalette = d3.scaleOrdinal(d3.schemeCategory10);
if (pieNode && pieNode.data.proportions && pieNode.data.proportions.length > 0) {
//Pie chart
d3.select("#node" + pieNode.id).selectAll("g.arc").remove();
var nodeUpdate = d3.select("#node" + pieNode.id);
arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(pieNode.data.r ? pieNode.data.r : 10);
var arcs = nodeUpdate
.append("g")
.attr("class", "arc")
.selectAll("g.arc")
.data(function (d) {
return pie(pieNode.data.proportions);
})
.enter();
arcs.append("path")
.attr("class", "pie")
.attr("fill", function (d, i) {
return codeColors[i];
})
.attr("d", arc);
}
}
function calculateProportionsForPie(nodeDrawingPie) {
//Pie chart
d3.select("#node" + nodeDrawingPie.id).selectAll("g.arc").remove();
var conceptId;
nodeDrawingPie.data.proportions = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
if (nodeDrawingPie.data.concepts && nodeDrawingPie.data.concepts.length > 0) {
for (var i = 0; i < nodeDrawingPie.data.concepts.length; i++) {
conceptId = nodeDrawingPie.data.concepts[i].substring(1, 3);
nodeDrawingPie.data.proportions[proportionsCode[conceptId]]++;
}
}
// recursively calculate proportions for child nodes
if (nodeDrawingPie.children && nodeDrawingPie.children.length > 0) {
for (var j = 0; j < nodeDrawingPie.children.length; j++) {
calculateProportionsForPie(nodeDrawingPie.children[j]);
}
}
drawPie(nodeDrawingPie)
}
},
exports.addNewNodeWithDepth = function (nameNew, depth) {
// If node children are closed copy them to prevent losing children
if (currentSelectedD && currentSelectedD.children == null) {
currentSelectedD.children = currentSelectedD._children;
}
var newNode = {
name: nameNew,
longName: nameNew,
proportions: [],
depth: depth,
id: 0,
};
//Creates a Node from newNode object using d3.hierarchy(.)
var newNode = d3.hierarchy(newNode);
//later added some properties to Node like child,parent,depth
newNode.depth = depth;
newNode.height = 4 - depth;
if (currentSelectedD) {
newNode.parent = currentSelectedD;
//Selected is a node, to which we are adding the new node as a child
//If no child array, create an empty array
if (currentSelectedD && !currentSelectedD.children) {
currentSelectedD.children = [];
}
//Push it to parent.children array
currentSelectedD.children.push(newNode);
//Update tree
exports.update(currentSelectedD);
} else {
root = d3.hierarchy(newNode, function (d) {
return d.children;
});
currentSelectedD = root;
exports.update(root);
}
},
exports.addNewNode = function (nameNew) {
// If node children are closed copy them to prevent losing children
if (currentSelectedD.children == null) {
currentSelectedD.children = currentSelectedD._children;
}
var newNode = {
name: nameNew,
longName: nameNew,
proportions: []
};
//Creates a Node from newNode object using d3.hierarchy(.)
var newNode = d3.hierarchy(newNode);
//later added some properties to Node like child,parent,depth
newNode.depth = currentSelectedD.depth + 1;
newNode.height = currentSelectedD.height - 1;
newNode.parent = currentSelectedD;
// newNode.id = Date.now();
//Selected is a node, to which we are adding the new node as a child
//If no child array, create an empty array
if (!currentSelectedD.children) {
currentSelectedD.children = [];
}
//Push it to parent.children array
currentSelectedD.children.push(newNode);
//Update tree
exports.update(currentSelectedD);
},
exports.addExistingNode = function (node) {
// If node children are closed copy them to prevent losing children
if (currentSelectedD.children == null) {
currentSelectedD.children = currentSelectedD._children;
}
node.proportions = [];
node.longName = node.name;
//Creates a Node from newNode object using d3.hierarchy(.)
var newNode = d3.hierarchy(node);
//later added some properties to Node like child,parent,depth
newNode.depth = currentSelectedD.depth + 1;
newNode.height = currentSelectedD.height - 1;
newNode.parent = currentSelectedD;
// newNode.id = Date.now();
//Selected is a node, to which we are adding the new node as a child
//If no child array, create an empty array
if (!currentSelectedD.children) {
currentSelectedD.children = [];
}
//add node children to this newNode
if (newNode.children && newNode.children.length > 0) {
newNode.children.forEach(c => {
c.depth = currentSelectedD.depth + 2;
if (c.children && c.children.length > 0) {
c.children.forEach(gs => {
gs.depth = currentSelectedD.depth + 3;
})
}
})
}
//Push it to parent.children array
currentSelectedD.children.push(newNode);
//Update tree
exports.update(currentSelectedD);
},
exports.removeSelectedNode = function () {
var temp = currentSelectedD.parent;
if (temp) {
for (var i = 0; i < temp.children.length; i++) {
if (temp.children[i] == currentSelectedD) {
temp.children.splice(i, 1);
}
}
if (temp.children.length == 0) {
temp.children = null;
}
currentSelectedD = temp;
}
//Update tree
exports.update(currentSelectedD);
},
exports.updateNode = function (node) {
currentSelectedD.data = node;
// currentSelectedD.data.longName = txt;
//Update tree
exports.update(currentSelectedD);
},
exports.getCurrentNode = function () {
return currentSelectedD;
}