UNPKG

@springernature/nn-charts

Version:
1,165 lines (1,013 loc) 69.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var d3 = _interopRequireWildcard(require("d3")); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; } 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) + ",