@springernature/nn-charts
Version:
Visualization for DAS products
1,166 lines (1,013 loc) • 69.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var d3 = _interopRequireWildcard(require("d3"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/*
HierarchyChart class definition
*/
var HierarchyChart = exports["default"] = /*#__PURE__*/function () {
//
// HierarchyChart constructor definition
function HierarchyChart(data, enableCollaborationChart, language) {
var _this = this;
_classCallCheck(this, HierarchyChart);
//
// function call to implement chart responsiveness after window resizing
this.window = window.addEventListener("resize", function () {
_this.hierarchy_windowResize();
});
this.enableCollaborationChart = enableCollaborationChart; // referenced passed in refer to collaboration chart
/* store dimensions of browser window */
this.vis = {
width: document.body.clientWidth,
height: document.body.clientHeight
};
/* visualisation's data object */
/* event listener to check for user selecting chart reset button */
this.hierarchy = {
updateChart: document.getElementById("updateChart").addEventListener("click", function () {
d3.select("#updateChart").classed("disabled", true);
_this.submitNewHierarchy();
}),
/* selectedResearcherID: null, */ /* populates with researcher UID if a resercher has been selected by user */
onhierarchyPageLoad: false /* updates to true if page has now loaded */,
/* legendMargin: 50, */
minimumNumberChildrenAtTopicLevel: 0 /* threshold variable to determine if node label is displayed/visible based on number of children */,
minimumNumberChildrenAtIntermediateLevel: 5 /* threshold variable to determine if node label is displayed/visible based on number of children */,
minimumNumberChildrenAtResearcherLevel: 10 /* threshold variable to determine if node label is displayed/visible based on number of children */,
/* tooltipLineCounter: 0, */ /* counts number of lines used to display longer text tooltip label */
data: data /* ingested chart data */,
nodes: null /* initiate nodes variable */,
/* initiate grey-scale colour ramp for onload default displaying/styling of chart */
color: d3.scaleLinear().domain([-1, 5]).range(["#111111", "#999999"]).interpolate(d3.interpolateHcl),
/* default hierarchy definiton to use at onload */
hierarchyDefinition: ["country_array", "affiliation_array", "year"],
/* temp store car for potential new hierarchy user wants; allows on the fly error checking and warning if not allowed */
proposedHierarchyDefinition: null,
hierarchyLegendDefinition: ["Countries", "Institutes", "Last published in..."],
/* articles: [], */
/* authors: [], */
tooltipRectWidth: 350,
// width of tooltip
tooltipRectHeight: 135,
// height of tooltip
margin: 25,
// margin to apply
view: null,
// grab tehh current 'view' of the circle packing chart (which node?)
focus: null,
diameter: null,
// width/diameter of whole circle-packing chart.
node: null,
// var to contain a singular node
circle: null,
currentDepthLevel: 0,
// what is the current depth level selected by user in the chart?
nodeSelected: false,
transitionDuration: 1500
/* toolTipMargin: 15, */
};
} // end constructor
/*
name: init
description: function to initial building of chart
arguments: none
returns: none
calls: nestData
called from: textDemo.js (nn-charts-poc)
*/
return _createClass(HierarchyChart, [{
key: "init",
value: function init(name, id) {
// update proposed hierarchy def with current def
this.hierarchy.proposedHierarchyDefinition = this.hierarchy.hierarchyDefinition;
// globally store sub topic name and ID>
this.hierarchy.subTopicName = name;
this.hierarchy.subTopicID = id;
this.nestData();
return;
} // end function init()
/*
NAME: createNestingFunction
DESCRIPTION: recursive function to build required JSON object
ARGUMENTS TAKEN: propertyName - key name from JSON Object
ARGUMENTS RETURNED: d[propertyName]
CALLED FROM: nestData
CALLS: none
*/
}, {
key: "createNestingFunction",
value:
//end function attachLegend
/*
NAME: createNestingFunction
DESCRIPTION: recursive function to build required JSON object
ARGUMENTS TAKEN: propertyName - key name
ARGUMENTS RETURNED: d[propertyName]
CALLED FROM: nestData
CALLS: none
*/
function createNestingFunction(propertyName) {
var el = this;
return function (d) {
return d[propertyName];
};
} // end function createNestingFunction
/*
NAME: hierarchy_autocomplete
DESCRIPTION:
ARGUMENTS TAKEN:
ARGUMENTS RETURNED:
CALLED FROM:
CALLS:
URLs: https://www.w3schools.com/howto/howto_js_autocomplete.asp
*/
}, {
key: "submitNewHierarchy",
value:
// end function createNestingFunction
/*
NAME: submitNewHierarchy
DESCRIPTION: function called to zoom visual to selected circle
ARGUMENTS TAKEN: none
ARGUMENTS RETURNED: none
CALLED FROM: user clicking submit button in index.html
CALLS: nestData
*/
function submitNewHierarchy() {
var el = this;
// get values associated with level level0, level1, level2 selection lists
var level0 = document.getElementById("level0").value;
var level1 = document.getElementById("level1").value;
var level2 = document.getElementById("level2").value;
// get levels associated with level level0, level1, level2 selection lists
var level1Select = document.getElementById("level0");
var level2Select = document.getElementById("level1");
var level3Select = document.getElementById("level2");
// get text associated with level level0, level1, level2 selection lists
var level1Text = level1Select.options[level1Select.selectedIndex].text;
var level2Text = level2Select.options[level2Select.selectedIndex].text;
var level3Text = level3Select.options[level3Select.selectedIndex].text;
// update hierarchy defintion to rebuild new hierarchy visual.
el.hierarchy.hierarchyDefinition = [level0, level1, level2];
// update hierarchy defintion to rebuild LEGEND of new hierarchy.
el.hierarchy.hierarchyLegendDefinition = [level1Text, level2Text, level3Text];
// nullify text label strings in underlying value-added table
d3.selectAll(".hierarchyTableDataText").html("");
// document.getElementById("updateChart").disabled = true;
document.getElementById("reset").disabled = false;
d3.select("#reset").classed("disabled", false);
// update proposedHierarchyDefinition variable
el.hierarchy.proposedHierarchyDefinition = el.hierarchy.hierarchyDefinition.slice();
// call function to nest data for new hierarchy definition.
el.nestData();
return;
} // end function submitNewHierarchy
/*
NAME: nestData
DESCRIPTION: function called to nest ingested data to requsite JSON data strucutre
ARGUMENTS TAKEN: none
ARGUMENTS RETURNED: none
CALLED FROM:
chartDrawFunctionCall
submitNewHierarchy
CALLS:
drawCommunityHierarchyChart
createNestingFunction
rename
*/
}, {
key: "nestData",
value: function nestData() {
var el = this;
// added to hide all other visualisation .container DIVs.
d3.selectAll(".container.hierarchy-container").classed("hide", false);
d3.selectAll(".hierarchy--master--wrapper").classed("loading", false);
// key-value object to define required JSON object names.
var keys = {
key: "name",
values: "children"
};
/*
NAME: rename
DESCRIPTION: local function called to rename JSON object keys to desired names
ARGUMENTS TAKEN: value - key name
ARGUMENTS RETURNED: none
CALLED FROM: itself
CALLS: rename
*/
function rename(value) {
if (!value || _typeof(value) !== "object") {
return value;
}
if (Array.isArray(value)) {
return value.map(rename);
}
// construct data object
var dee = Object.fromEntries(Object.entries(value).map(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
k = _ref2[0],
v = _ref2[1];
return [keys[k] || k, rename(v)];
}));
// object passed in is a researcher with researcher ID ...
if (dee.researcher_id) {
//
// handling of use-case with no affilitated institutes
if (dee.affiliation_array.length == 0) {
dee.affiliation_array[0] = "No Affiliated Institutes";
}
//
// handling of use-case with no affilitated Countries
if (dee.country_array.length == 0) {
dee.country_array[0] = "No Affiliated Countries";
}
//
// handling of use-case with no Field fo Research
if (dee.F_L0 == null) {
dee.F_L0 = "No Affiliated Field of Research";
}
// construct and return JSON array element
return {
affiliation: dee.affiliation_array[0],
country: dee.country_array[0],
academic_age: dee.academic_age,
articles: dee.articles,
articles_3_years: dee.articles_3_years,
author: dee.last_name + ", " + dee.first_name,
authorID: dee.authorID,
children: [],
first_name: dee.first_name,
F_L0: dee.F_L0,
hindex: dee.hindex,
last_name: dee.last_name,
name: dee.last_name + ", " + dee.first_name,
firstName_lastName: dee.first_name + " " + dee.last_name,
pagerank: dee.pagerank,
researcher_id: dee.researcher_id,
size: dee.articles,
subtopic: dee.subtopic,
subtopic_id: dee.subtopic_id,
year: dee.year
};
} else if (dee.children) {
if (dee.name == "null") {
return {
name: "No Category Assigned",
children: dee.children
};
} else {
return {
name: dee.name,
children: dee.children
};
}
} else {
return dee;
}
} // end function rename
// filter data based on subtopic ID ...
el.hierarchy.filteredData = el.hierarchy.data.filter(function (d, i) {
return d.subtopic_id == el.hierarchy.subTopicID;
});
// initialised affilitation object and array ready for population via JSON object processing.
el.hierarchy.affiliationObject = {};
el.hierarchy.affiliationList = [];
// attach proxy author ID to each filtered author relevant to selected sub-topic.
el.hierarchy.filteredData.forEach(function (d, i) {
d.authorID = i; // author ID
d.fullNameBackward = d.last_name + ", " + d.first_name; // full researher name formatted, backward
d.fullNameForward = d.first_name + " " + d.last_name; // full researher name formatted, forward
//
var country_array = d.country_array; // locally store country array.
var countries = country_array.toString().split(",").join(" | "); // modify and format countries string from JSON
d.countries = countries;
//
var affiliation_array = d.affiliation_array; // locally store affiliations array.
var affiliations = affiliation_array.toString().split(",").join(" | "); // modify and format affiliations string from JSON
d.affiliation = affiliations;
//
var affiliation = d.affiliation; // locally store affilitions string/info
// handling of input if only one affilitation is attached to researcher.
if (d.grid_id_array.length == 1 && affiliation != null) {
var grid_id_array = d.grid_id_array.toString().replaceAll(".", "-").replaceAll(",", "-");
if (!el.hierarchy.affiliationObject.hasOwnProperty(grid_id_array)) {
el.hierarchy.affiliationObject[grid_id_array] = d.affiliation;
el.hierarchy.affiliationList.push([grid_id_array, d.affiliation, affiliation.substring(0, 35) + "..."]);
}
}
}); // end forEach...
/*
NAME: compareStrings
DESCRIPTION: local function called to logicall compare two strings. helps sorting of data
ARGUMENTS TAKEN:a - first string to compare
b - second string to compare
ARGUMENTS RETURNED: none
CALLED FROM: nestData
CALLS: none
*/
function compareStrings(a, b) {
// Assuming you want case-insensitive comparison
a = a.toLowerCase();
b = b.toLowerCase();
return a < b ? -1 : a > b ? 1 : 0;
}
// sort filteredData to build hierarchy chart in alphabetical order (may need to be changed based on agreed logic for displaying person names)
el.hierarchy.filteredData.sort(function (a, b) {
return compareStrings(a.fullNameBackward, b.fullNameBackward);
});
// https://stackoverflow.com/questions/22512853/d3-js-use-d3-nest-adding-keys-dynamically
el.hierarchy.levels = el.hierarchy.hierarchyDefinition.map(function (d, i) {
return d;
});
// generate D3 nest object based on data recursion
el.hierarchy.nest = d3.nest();
for (var i = 0; i < el.hierarchy.levels.length; i++) {
el.hierarchy.nest = el.hierarchy.nest.key(el.createNestingFunction(el.hierarchy.levels[i]));
} // end for ...
// construct updated structure for tree object
el.hierarchy.treeData = {
name: el.hierarchy.subTopicName,
children: rename(el.hierarchy.nest.entries(el.hierarchy.filteredData)) //compute the nest
};
// populte proxy article locator selection list (for testing purposes) with proxy ID data...
el.hierarchy.filteredData.forEach(function (d, i) {
// update author ID (different from Dimensions or other data source-generated ID)
d.authorID = +i;
d.author = d.last_name + ", " + d.first_name;
}); // end forEach
// call function to draw Community hierarchy chart ...
el.drawCommunityHierarchyChart(el.hierarchy.treeData);
return;
} // end function nestData
/*
NAME: findUserSelectionFunction
DESCRIPTION: function called to zoom visual to selected circle
ARGUMENTS TAKEN: none
ARGUMENTS RETURNED: none
CALLED FROM: user changing selection on find author/find article selction lists
CALLS:zoomToUserSelectedAuthorArticleNode
updateFunnelThumbnail
attachValueAddedResearcherInformation
attachTooltip
*/
}, {
key: "findUserSelectionFunction",
value: function findUserSelectionFunction(authorID, modifiedAuthorID) {
var el = this;
// turn off disabled state on Reset buttonm.
document.getElementById("reset").disabled = false;
d3.select("#reset").classed("disabled", false);
// de-style all chart node circles.
d3.selectAll(".hierarchyNode.node--leaf").classed("author-selector", false);
// update specific selection styling of selected node circle
d3.selectAll(".hierarchyNode.node--leaf.researcher_id-" + modifiedAuthorID).classed("author-selector", true);
// initialise local variables.
var selectedNode;
var nodeID;
var eye;
// find selected circle node and zoom to it.
// for each node in hierarchy
el.hierarchy.nodes.forEach(function (d, i) {
// if node has a researcher ID, i.e. it is a node at the lowest granular level of data ...
if (d.data.researcher_id) {
selectedNode = d; // isolate and store node data
eye = i; // isolate and store node index
nodeID = d.data.researcher_id.replaceAll(".", "-"); // node ID of iterated node ...
// modified/selected node is = to node ID currently being compared against ...
if (modifiedAuthorID == nodeID) {
// call function to zoom to slected node
el.zoomToUserSelectedAuthorArticleNode(selectedNode);
// call function to update funnel thumbnail; will be deprecated and removed in future design iterations
el.updateFunnelThumbnail(selectedNode);
// after delay, ...
var myTimeout = setTimeout(function myStopFunction() {
//
// add value added information to table below visual
el.attachValueAddedResearcherInformation(d, "circle-" + eye);
//
// attach tooltip to node;
el.attachTooltip(d);
}, el.hierarchy.transitionDuration);
} // end inner if ...
} // end outer if ...
}); // end forEach
//el.updateDisplayChartCloneMarker();
return;
} // end function findUserSelectionFunction
/*
NAME: wrap
DESCRIPTION: function called to wrap long D3 SVG labels on to multiple lines
ARGUMENTS TAKEN: text - long label text to wrap
width - width to wrap and constrain modified label to.
textClass -
ARGUMENTS RETURNED:
CALLED FROM:
CALLS:
*/
}, {
key: "wrap",
value: function wrap(text, width, textClass) {
var el = this;
// for each long label passed into function to modify and wrap ...
text.each(function () {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.25,
// ems
x = text.attr("x"),
y = text.attr("y"),
dy = 0,
//parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").classed(textClass, true).attr("x", x).attr("y", y).attr("dy", dy + "em");
// while still having words to consider in original long label ...
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").classed(textClass, true).attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
} // end if ...
} // end while ...
}); // end .each();
return;
} // end function wrap
/*
browser window screen widths and heights
https://andylangton.co.uk/blog/development/get-viewportwindow-size-width-and-height-javascript
http://ryanve.com/lab/dimensions/
If you are using jQuery, you can get the size of the window or the document using jQuery methods:
$(window).height(); // returns height of browser viewport
$(document).height(); // returns height of HTML document (same as pageHeight in screenshot)
$(window).width(); // returns width of browser viewport
$(document).width(); // returns width of HTML document (same as pageWidth in screenshot)
For screen size you can use the screen object in the following way:
screen.height;
screen.width;
*/
}, {
key: "alertSize",
value: function alertSize() {
var el = this;
var myWidth = 0;
var myHeight = 0;
if (typeof window.innerWidth == "number") {
//Non-IE
myWidth = window.innerWidth;
myHeight = window.innerHeight;
} else if (document.documentElement && (document.documentElement.clientWidth || document.documentElement.clientHeight)) {
//IE 6+ in 'standards compliant mode'
myWidth = document.documentElement.clientWidth;
myHeight = document.documentElement.clientHeight;
} else if (document.body && (document.body.clientWidth || document.body.clientHeight)) {
//IE 4 compatible
myWidth = document.body.clientWidth;
myHeight = document.body.clientHeight;
}
el.vis.width = myWidth;
el.vis.height = myHeight;
return;
} // end function alertSize
/*
NAME: zoomTo
DESCRIPTION: function called to zoom visual to selected circle
ARGUMENTS TAKEN: v - data
ARGUMENTS RETURNED: updated translate attrube values.
CALLED FROM:zoom
drawCommunityHierarchyChart
CALLS: n/a
*/
}, {
key: "zoomTo",
value: function zoomTo(v) {
var el = this;
// locally calculate and store new scale factor
var k = el.hierarchy.diameter / v[2];
el.hierarchy.view = v;
el.hierarchy.node.attr("transform", function (d) {
return "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")";
});
el.hierarchy.circle.attr("r", function (d) {
return d.r * k;
});
return;
} // end fucntion zoomTo
/*
NAME: zoomToUserSelectedAuthorArticleNode
DESCRIPTION: function called when user selects a new node to zoom
ARGUMENTS TAKEN: d - data
fid - identifier
ARGUMENTS RETURNED: none
CALLED FROM: findUserSelectionFunction
CALLS: n/a
*/
}, {
key: "zoomToUserSelectedAuthorArticleNode",
value: function zoomToUserSelectedAuthorArticleNode(d, fid) {
var el = this;
el.hierarchy.focus = d;
var transition = d3.transition().duration(el.hierarchy.transitionDuration).tween("zoom", function (d) {
var i = d3.interpolateZoom(el.hierarchy.view, [el.hierarchy.focus.x, el.hierarchy.focus.y, el.hierarchy.focus.r * 2 + 25]);
return function (t) {
var v = i(t);
var k = el.hierarchy.diameter / v[2];
el.hierarchy.view = v;
el.hierarchy.node.attr("transform", function (d) {
return "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")";
});
el.hierarchy.circle.attr("r", function (d) {
return d.r * k;
});
};
});
return;
} // end function zoomToUserSelectedAuthorArticleNode
/*
NAME: drawCommunityHierarchyChart
DESCRIPTION: function called to draw community hierarchy chart visual
ARGUMENTS TAKEN: root - base constructed data object
ARGUMENTS RETURNED: none
CALLED FROM:
CALLS: zoom
zoomToUserSelectedAuthorArticleNode
hierarchy_autocomplete
hierarchy_mouseover
hierarchy_mouseleave
updateFunnelThumbnail
updateDisplayChartCloneMarker
zoomTo
enableCollaborationChart
wrap
nestData
zoomTo
cloneChart
attachLegend
*/
}, {
key: "drawCommunityHierarchyChart",
value: function drawCommunityHierarchyChart(root) {
var el = this;
this.alertSize(); // function call to get current browser window dimensions
// remove old chart and topic name label
d3.selectAll(".chart").remove();
d3.selectAll(".topicNameBannerlabel").remove();
// select SVG panel on which to draw chart
var svg = d3.selectAll(".container.hierarchy-container").select("svg").attr("class", "pearl-svg Communities-Hierarchy").attr("id", "pearl-svg-Communities-Hierarchy").attr("width", "100%").attr("height", this.vis.height * 0.8);
// determine diameter of circle packing chart visual
el.hierarchy.diameter = d3.selectAll(".pearl-svg.Communities-Hierarchy").attr("height") - el.hierarchy.margin * 2;
// append g element to base SVG to contain new chart
var g = svg.append("g").attr("class", "chart").attr("transform", "translate(" + this.vis.width / 2 + "," + (this.vis.height / 2 - 75) + ")")
// append background rectangle to behind new chart to be drawn
.append("rect").attr("class", "main background-rect").attr("x", -el.hierarchy.diameter / 2).attr("y", -el.hierarchy.diameter / 2).attr("width", el.hierarchy.diameter).attr("height", el.hierarchy.diameter).classed("hide", true);
// append and update main topic name label to this visualisation
d3.selectAll(".topicNameBanner").append("label").attr("class", "topicNameBannerlabel").text("Community Hierarchy for " + el.hierarchy.subTopicName);
// call D3 circle packing function
var pack = d3.pack().size([el.hierarchy.diameter - el.hierarchy.margin * 2, el.hierarchy.diameter - el.hierarchy.margin * 2]).padding(2);
// call D3 hierarchy function to construct 'root'
root = d3.hierarchy(root).sum(function (d) {
return d.size;
}).sort(function (a, b) {
return b.value - a.value;
});
// if this is first time page/chart has been built...
if (el.hierarchy.onhierarchyPageLoad == false) {
el.hierarchy_autocomplete(document.getElementById("myHierarchyResearcherInput"), el.hierarchy.filteredData);
// update global page load variable.
el.hierarchy.onhierarchyPageLoad = true;
} // end if ...
// determine circle packing focus and nodes
el.hierarchy.focus = root;
el.hierarchy.nodes = pack(root).descendants();
// construct circle packing vis
el.hierarchy.circle = d3.selectAll(".chart").selectAll("circle").data(el.hierarchy.nodes).enter().append("circle").attr("id", function (d, i) {
return "circle-" + i;
}).attr("class", function (d) {
return d.parent ? d.children ? "hierarchyNode" : "researcher_id-" + d.data.researcher_id.replaceAll(".", "-") + " hierarchyNode node--leaf" : "hierarchyNode node--root";
}).style("fill", function (d) {
// d.depth == el.hierarchy.currentDepthLevel + 1 &&
// d.data.children.length >=
// el.hierarchy.minimumNumberChildrenAtIntermediateLevel
// ? d3.select(this).classed("inscope-node", true)
// : d3.select(this).classed("inscope-node", false);
return d.children ? el.hierarchy.color(d.depth) : null;
}).on("mouseover", function (d, i) {
if (el.hierarchy.nodeSelected == false) {
d3.event.stopPropagation();
el.hierarchy_mouseover(d, i, this);
}
}).on("mouseleave", function (d, i) {
// if no node has yet been selected
if (el.hierarchy.nodeSelected == false) {
d3.selectAll(".hierarchyTableDataText").html("");
d3.selectAll(".author-selector").classed("author-selector", false);
d3.selectAll(".chartClone-pointer-group").classed("hide", true);
} else {}
}).on("click", function (d, i) {
var clickedNode = d; // locally store node-specific data
//
// update funnel thumbnail map
el.updateFunnelThumbnail(clickedNode);
//
// update relevant variables.
el.hierarchy.selectedNodeParentDetail = d;
el.hierarchy.nodeSelected = true;
el.hierarchy.currentSelectedNodeID = d3.select(this).attr("id");
//
d3.selectAll(".chartClone-pointer-group").classed("hide", true);
// if user is zooming to node at a lower level ..
// if (d.depth != 0) {
// el.updateDisplayChartCloneMarker();
// } // end if ...
// store and wrap selected node long labels.
var selectedNodeLevelMarkerLabel = d3.selectAll(".selectedNodeLevelMarkerLabel");
el.wrap(selectedNodeLevelMarkerLabel, 200, "selectedNodeLevelMarkerLabel");
// if user has selected an intermediate level node.
if (el.hierarchy.focus !== d) {
//
// remove relevant chart annotations.
d3.selectAll(".selectedResearcherInformation").remove();
d3.selectAll(".clickHerePrompt").remove();
// call functions to zoom to selected node and load/display researcher collaboration chart.
el.zoom(d, this, "drawCommunityHierarchyChart");
el.enableCollaborationChart(false, d.data.label, d.data.researcher_id);
}
// end if ...
// otherwise you have selected a researcher ...
else {
console.log("you have clicked on an author/researcher");
// display new container for containing network collab chart
d3.selectAll(".container.Community-Network").classed("hide", false);
// old hard-coded use case to test against ...
// el.hierarchy.selectedResearcher = "Berton, M.";
// el.hierarchy.selectedResearcherID = "ur.012143560404.33";
// old dynaic-coded use case to test against ...
// el.selectedResearcher = d.data.label;
// el.selectedResearcherID = d.data.researcher_id;
// store var referencing the new container
var element = document.getElementById("collaborationNetwork-container");
// determine coordinates of bounding rectangle
var elementPosition = element.getBoundingClientRect().top;
var offsetPosition = elementPosition + window.pageYOffset;
// smoothly scroll down to new frame ...
window.scrollTo({
top: offsetPosition,
behavior: "smooth"
});
/* to enable collaboration chart */
el.enableCollaborationChart(true, d.data.label, d.data.researcher_id);
} // end else ..
});
// append text elements to circle packing node circles
var text = d3.selectAll(".chart").selectAll("text").data(el.hierarchy.nodes).enter().append("text").attr("class", function (d, i) {
if (d.parent != null) {
return "label hierarchyNodeLabels hierarchyNodeLabel-" + i;
} else {
return "label hierarchyNodeLabels root";
}
}).style("fill-opacity", function (d) {
return d.parent === root ? 1 : 0;
}).style("display", function (d, i) {
// if at top most level, and node has above min number of children.
if (d.depth == 0) {
return "none";
} else if (d.depth > 0 && d.data.children.length >= el.hierarchy.minimumNumberChildrenAtIntermediateLevel) {
return "inline";
} else {
return "none";
}
}).text(function (d) {
return d.data.name;
});
// select all circel node text labels for processing ...
el.hierarchy.node = d3.selectAll(".chart").selectAll("circle,text");
// handle user click interaction with chart
svg.on("click", function () {
el.zoom(root, "", "svg.on drawCommunityHierarchyChart");
});
// handle user click of reset button.
d3.select("#reset").on("click", function () {
document.querySelector(".myHierarchyResearcherInput").value = "";
//
// reinstate disabled state on reset and update chart buttons
document.getElementById("updateChart").disabled = true;
document.getElementById("reset").disabled = true;
//
// disable and make inactive reset and update chart form buttons
d3.select("#reset").classed("disabled", true);
d3.select("#updateChart").classed("disabled", true);
//
// nullfiy text strings
d3.selectAll(".hierarchyTableDataText").html("");
// remove all class node selection classes
d3.selectAll("circle").classed("author-selector", false).classed("article-selector", false);
// remove funnel thumbnail map level classnames
d3.selectAll(".levelMarkerLabels").classed("currentLevelLabel", false);
d3.selectAll(".levelMarkerLabels.depth-0").classed("currentLevelLabel", true);
// return to default styling of funnel map styling after reset
d3.selectAll(".levelMarkers").classed("currentLevelMarker", false);
d3.selectAll(".levelMarkers.depth-0").classed("currentLevelMarker", true);
// nullify/reset text strings and label content
d3.selectAll(".selectedNodeLevelMarkerLabel").text("");
d3.selectAll(".selectedNodeLevelMarkerLabel.depth-0").text(el.hierarchy.subTopicName);
// reset hierarchy definition
el.hierarchy.hierarchyDefinition = ["country_array", "affiliation_array", "year"];
// reset hierarchy legend definition
el.hierarchy.hierarchyLegendDefinition = ["Countries", "Institutes", "Last published in..."];
// hierarchy level defintion selction lists.
var element1 = document.getElementById("level0");
element1.value = el.hierarchy.hierarchyDefinition[0];
//
var element2 = document.getElementById("level1");
element2.value = el.hierarchy.hierarchyDefinition[1];
//
var element3 = document.getElementById("level2");
element3.value = el.hierarchy.hierarchyDefinition[2];
// call function to nest data for new hierarchy definition.
el.nestData();
return;
}); // end function called from 'reset' button
// call function to zoom to full extent
el.zoomTo([root.x, root.y, root.r * 2 + el.hierarchy.margin]);
// el.cloneChart();
// el.attachLegend();
// handle user change opf any of the level definition selection lists.
d3.selectAll(".hierarchyLevelSelection").on("change", function () {
// locally store intended new level.
var plannedNewLevel = this.value;
//determine id of this ...
var id = this.id.replaceAll("level", "");
// if new level IS currently used ...
if (el.hierarchy.proposedHierarchyDefinition.indexOf(plannedNewLevel) != -1) {
var element = document.getElementById(this.id);
element.value = el.hierarchy.hierarchyDefinition[id];
// present user with error handling warning to make change to selection.
d3.selectAll(".searchBanner").append("div").attr("class", "hierarchy-selector-error-tooltip").style("top", d3.selectAll(".controlsBanner").style("height")).append("label").attr("class", "hierarchy-selector-error-tooltip-label").text("Your selection is already used for another level. Please reselect.");
// hide error message after a short while.
var myTimeout = setTimeout(function () {
d3.selectAll(".hierarchy-selector-error-tooltip").classed("hide", true);
}, 5000);
return;
} else {}
// deteine full proposed new hierarchy definition ...
var proposedLevel0 = document.getElementById("level0").value;
var proposedLevel1 = document.getElementById("level1").value;
var proposedLevel2 = document.getElementById("level2").value;
// update proposed hierarchy defintion to taht will potentially be used to rebuild new hierarchy visual.
el.hierarchy.proposedHierarchyDefinition = [proposedLevel0, proposedLevel1, proposedLevel2];
// activate chart update button if all OK ...
document.getElementById("updateChart").disabled = false;
d3.select("#updateChart").classed("disabled", false);
return;
});
return;
} // end function drawCommunityHierarchyChart
/*
NAME: updateFunnelThumbnail
DESCRIPTION: function called to update the funnel thumbnail map (soon to be deprecated)
ARGUMENTS TAKEN: clickedNode - reference to node clicked by user on chart
ARGUMENTS RETURNED: none
CALLED FROM:findUserSelectionFunction
drawCommunityHierarchyChart
CALLS: recurse
*/
}, {
key: "updateFunnelThumbnail",
value: function updateFunnelThumbnail(clickedNode) {
//
var el = this;
var selectedHierarchyPath = [];
//
// recursive function
// https://www.javascripttutorial.net/javascript-recursive-function/
recurse(clickedNode);
/*
NAME: recurse
DESCRIPTION: function called to recurse through nested hierarchy data
ARGUMENTS TAKEN: level - information for level being considered
ARGUMENTS RETURNED: none
CALLED FROM:recurse
updateFunnelThumbnail
CALLS: recurse (itself)
*/
function recurse(level) {
if (level.parent == null) {
selectedHierarchyPath.push(level.data.name);
// stop calling itself
//...
} else {
selectedHierarchyPath.push(level.data.name);
recurse(level.parent);
}
} // end function recurse
// reverse array containing currently followed hierarchy path ...
selectedHierarchyPath.reverse();
// reset all funnel marker styling.
d3.selectAll(".levelMarkerLabels").classed("currentLevelLabel", false);
d3.selectAll(".levelMarkers").classed("currentLevelMarker", false);
d3.selectAll(".selectedNodeLevelMarkerLabel").text("");
// update marker label styling and class declarations for all levels passed through to current level...
selectedHierarchyPath.forEach(function (d, i) {
d3.selectAll(".levelMarkerLabels.depth-" + i).classed("currentLevelLabel", true);
// update marker styling and class declarations for all levels passed through to current level...
d3.selectAll(".levelMarkers.depth-" + i).classed("currentLevelMarker", true);
// update marker label styling and class declarations for all levels passed through to current level...
d3.selectAll(".selectedNodeLevelMarkerLabel.depth-" + i).text(function () {
if (i == selectedHierarchyPath.length - 1) {
var researcher = d;
var researcherReordered = researcher.split(", ");
researcherReordered[researcherReordered.length - 1] = " " + researcherReordered[researcherReordered.length - 1].substring(0, 1) + ". ";
return researcherReordered;
} else {
return d;
}
});
}); // end forEach loop ...
return;
} // end function
/*
NAME: updateDisplayChartCloneMarker
DESCRIPTION: function called to update line end coordinatres of clone map marker (soon to be)
ARGUMENTS TAKEN: none
ARGUMENTS RETURNED: none
CALLED FROM:findUserSelectionFunction
drawCommunityHierarchyChart
CALLS: none
*/
}, {
key: "updateDisplayChartCloneMarker",
value: function updateDisplayChartCloneMarker() {
var el = this;
//
// hide current marker group
d3.selectAll(".chartClone-pointer-group").classed("hide", false);
//
// determine new transtion values for line
var lineEndTranslate = d3.selectAll(".chartClone").select("#" + el.hierarchy.currentSelectedNodeID).attr("transform").replaceAll("translate(", "").replaceAll(")", "").split(",");
// update new end coordinates of line marker.
d3.selectAll(".chartClone-pointer-group-line").attr("x2", el.hierarchy.halvedCloneWidth / 2 - -lineEndTranslate[0]).attr("y2", el.hierarchy.halvedCloneWidth - -lineEndTranslate[1]);
return;
} // end function updateDisplayChartCloneMarker
/*
NAME: hierarchy_mouseover
DESCRIPTION: function called when user mouseovers a circle node
ARGUMENTS TAKEN: none
ARGUMENTS RETURNED: none
CALLED FROM: user changing selection on find author/find article selction lists
CALLS: attachTooltip
*/
}, {
key: "hierarchy_mouseover",
value: function hierarchy_mouseover(d, i, fid) {
var el = this;
// d3.selectAll(".label.hierarchyNodeLabels.hierarchyNodeLabel-" + i)
// .style("display", "inline")
// .style("fill-opacity", 1.0);
//
// if use is below topmost subtopic level
if (d.depth != 0) {
//
// determine new translate coordiantes
var lineEndTranslate = d3.select(fid).attr("transform").replaceAll("translate(", "").replaceAll(")", "").split(",");
// display clone map marker group
d3.selectAll(".chartClone-pointer-group").classed("hide", false);
// update marker line end coordinates
d3.selectAll(".chartClone-pointer-group-line").attr("x2", el.hierarchy.halvedCloneWidth / 2 - -lineEndTranslate[0]).attr("y2", el.hierarchy.halvedCloneWidth - -lineEndTranslate[1]);
} // end if ...
// update styling of clone map selected node
d3.selectAll(".chartClone").selectAll(".hierarchyNode").classed("author-selector", false);
d3.selectAll(".chartClone").select("#" + fid.id).classed("author-selector", true);
// if at top most level ...
if (d.height == 0) {
el.attachTooltip(d);
} // end if ...
else {}
return;
} // end function mouseover
/*
NAME: zoom
DESCRIPTION: function called to zoom visual to selected circle
ARGUMENTS TAKEN: d - data
fid - unique identifier of selected circle node
ARGUMENTS RETURNED: none
CALLED FROM: user click on circle node
CALLS:zoomTo
attachValueAddedResearcherInformation
attachTooltip
*/
}, {
key: "zoom",
value: function zoom(d, fid) {
var el = this;
if (!fid) {
return;
}
el.hierarchy.currentDepthLevel = d.depth;
el.hierarchy.currentParent = d.data.name;
// activate and enable chart reset button
document.getElementById("reset").disabled = false;
d3.select("#reset").classed("disabled", false);
el.hierarchy.focus = d;
// reset and update styling of all circle nodes by removing specific class names
d3.selectAll("circle").classed("author-selector", false).classed("article-selector", false);
// d3.selectAll(".hierarchyNode").classed("inscope-node", false);
// reset and update styling of all circle nodes
d3.selectAll(".chart").selectAll(".hierarchyNode").style("fill", function (d, i) {
return d.children ? el.hierarchy.color(d.depth) : null;
});
// update styling of selected circle node
d3.select(fid).classed("author-selector", true);
d3.selectAll(".chartClone").select("#" + fid.id).classed("author-selector", true);
// transition to newly selected circle node
var transition = d3.transition().duration(el.hierarchy.transitionDuration).tween("zoom", function (d) {
var i = d3.interpolateZoom(el.hierarchy.view, [el.hierarchy.focus.x, el.hierarchy.focus.y, el.hierarchy.focus.r * 2 + 25]);
return function (t) {
el.zoomTo(i(t));
};
});
// transition text of newly selected circle node
transition.selectAll(".chart").selectAll(".label").filter(function (d) {
return d.parent === el.hierarchy.focus || this.style.display === "inline";
}).style("display", function (d, i) {
// if at top most level, and node has above min number of children.
if (d.depth == 0) {
return "none";
} else if (d.depth > 0 && d.data.children.length >= el.hierarchy.minimumNumberChildrenAtIntermediateLevel) {
return "inline";
} else if (d.height == 0 && d.data.size >= el.hierarchy.minimumNumberChildrenAtResearcherLevel) {
return "inline";
} else {
return "none";
}
}).style("fill-opacity", function (d) {
return d.parent === el.hierarchy.focus ? 1 : 0;
}).on("start", function (d) {
if (d.parent === el.hierarchy.focus) this.style.display = "inline";
}).on("end", function (d) {
if (d.parent !== el.hierarchy.focus) this.style.display = "none";
});
// if user has zoomed to lowest level of chart (i.e. at teh researcher level)
if (d.depth == el.hierarchy.hierarchyDefinition.length + 1) {
var myTimeout = setTimeout(function myStopFunction() {
el.attachValueAddedResearcherInformation(d, fid.id);
el.attachTooltip(d);
}, el.hierarchy.transitionDuration);
} // end if ...
return;
} // end function zoom
/*
NAME: attachValueAddedResearcherInformation
DESCRIPTION: function called to construct and display tooltip
ARGUMENTS TAKEN: d - data object for selected node ..
id - if of selected node
ARGUMENTS RETURNED:none
CALLED FROM: findUserSelectionFunction
zoom
CALLS:displayTooltip
*/
}, {
key: "attachValueAddedResearcherInformation",
value: function attachValueAddedResearcherInformation(d, id) {
var el = this;
// remove any old tooltips from view ...
d3.selectAll(".clickHerePrompt").remove();
// construct tooltip text content
var data = ["Click on adjacent orange circle to view this researcher's collaboration network", "", d.data.last_name + ", " + d.data.first_name.substring(0, 1) + ". ", "Sub-topic: " + d.data.subtopic, "Field of Research: " + d.data.F_L0, "Affiliations: " + d.data.affiliation, "Countries: " + d.data.country, "H-index: " + d.data.hindex, "Academic age: " + d.data.academic_age + " years", "Last Published In: " + d.data.year, "Number of publications: " + d.data.articles, "Number of publications in last 3 years: " + d.data.articles_3_years];
// determine center coords of selected node
var centreCoords = d3.select("#" + id).attr("transform").replaceAll("translate(", "").replaceAll(")", "").split(",");
// determine radius of of selected node
var nodeRadius = Number(+d3.select("#" + id).attr("r") + 30);
// display tooltip a few milliseconds after chart transition
var myTimeout = setTimeout(displayTooltip, 5);
/*
NAME: displayTooltip
DESCRIPTION: function called to display tooltip
ARGUMENTS TAKEN:none
ARGUMENTS RETURNED:none
CALLED FROM: attachValueAddedResearcherInformation
CALLS:none
*/
function displayTooltip() {
//
// construct new tooltip base
var selectedResearcherInformation = d3.selectAll(".chart").append("g").attr("class", "selectedResearcherInformation").attr("id", "selectedResearcherInformation").attr("transform", "translate(" + Number(nodeRadius) + "," + 0 + ")").append("rect").attr("class", "selectedResearcherInformationBackground").attr("x", -10).attr("y", -20).attr("width", 375).attr("height", 210);
// ge