UNPKG

@springernature/nn-charts

Version:
1,106 lines (994 loc) 99.1 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 _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); } /* CollapsibleTreeChart class definition */ var CollapsibleTreeChart = exports["default"] = /*#__PURE__*/function () { function CollapsibleTreeChart(chartData, title, lineChart, getSubtopicDetails, enableHierarchyChart) { var _this = this; var language = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null; var initiated = arguments.length > 6 ? arguments[6] : undefined; var loaded = arguments.length > 7 ? arguments[7] : undefined; _classCallCheck(this, CollapsibleTreeChart); this.initiated = initiated; this.loaded = loaded; // // // function call to implement topic tree chart responsiveness after window resizing this.window = window.addEventListener("resize", function () { _this.topicModellingTopicTree_windowResize(); }); // function call to implement bar chart responsiveness after window resizing this.window = window.addEventListener("resize", function () { _this.topicModellingBarChart_windowResize(); }); /* arguments needed for hierarchy chart */ this.enableHierarchyChart = enableHierarchyChart; // this.currentXAxisDomain = []; // get/set dimensions of browser area. this.vis = { width: document.body.clientWidth, height: document.body.clientHeight }; // line chart is requried to be built also, (it should be, given its currenty part of Topic Explorer) if (lineChart) { this.lineChartData = lineChart.data; // reference to data this.x = lineChart.x; // reference to chart x-axis on upper focus chart this.y = lineChart.y; // reference to chart y-axis on upper focus chart this.x2 = lineChart.x2; // reference to chart x-axis on lowwer context chart this.y2 = lineChart.y2; // reference to chart y-axis on lowwer context chart this.fullTimeDomain = lineChart.fullTimeDomain; // full time doamin of lower x-axis this.width = lineChart.width; // focus and context chart width this.height = lineChart.height; // focus chart height this.height2 = lineChart.height2; // context chart height this.focusLine = lineChart.focusLine; // line path generator to build data line on focus chart this.contextLine = lineChart.contextLine; // line path generator to build data line on context chart this.margin = lineChart.margin; // margin construct this.focus = lineChart.focus; // reference to upper chart /* this.constructedLineData = lineChart.constructedLineData; */ this.constructedLineData2 = lineChart.constructedLineData2; // JSON obj containing all line data, in correct construct for display this.currentDataBeingDisplayed = lineChart.currentDataBeingDisplayed; this.nestedData = lineChart.nestedData; // ingested data nested this.LegendArrayPrefix = lineChart.LegendArrayPrefix; // legend content this.addLineMouseOverFunctionality = lineChart.addLineMouseOverFunctionality; // ref to function to implement ouseover ability this.setYdomains = lineChart.setYdomains; // NEEDED? this.set_FocusYdomain = lineChart.set_FocusYdomain; // ref to func to transtion and update y axis on focus chart after line add/remove this.set_ContextYdomain = lineChart.set_ContextYdomain; // ref to func to transtion and update y axis on context chart after line add/remove this.yAxisDomainMaxRounding = lineChart.yAxisDomainMaxRounding; // rounding values for y-axis /* this.lineChartViewType = lineChart.lineChartViewType; */ this.focus_lineChartViewType = lineChart.focus_lineChartViewType; // what line type is being viewed on focus chart (cumulative or point in time)? this.context_lineChartViewType = lineChart.context_lineChartViewType; // what line type is being viewed on context chart (cumulative or point in time)? } else { this.lineChartData = {}; this.x = {}; this.y = {}; this.x2 = {}; this.y2 = {}; this.fullTimeDomain = {}; this.width = 0; this.height = 0; this.height2 = 0; this.focusLine = {}; this.contextLine = {}; this.margin = {}; this.focus = {}; this.constructedLineData2 = {}; this.nestedData = {}; this.LegendArrayPrefix = {}; this.addLineMouseOverFunctionality = {}; this.yAxisDomainMaxRounding = {}; this.focus_lineChartViewType = {}; this.context_lineChartViewType = {}; } this.data = chartData; this.hierarchyData = []; // researcher data for hierarchy chart... this.title = title; this.root = {}; this.smallMultiplesChartxDomainMaximum = 0; this.yAxis = {}; this.context = lineChart.context; this.legend = lineChart.legend; this.xAxis = {}; this.chartHeight = 650; /* this.colouredNodes = false; */ this.scaledNodes = false; this.expandaTreeToFullExtent = false; this.smallMultiplesChart = { axisTitle: { x: language ? language.xAxisTitle : "Concept page rank per sub-topic", y: language ? language.yAxisTitle : "Relevant Concepts" } /* chart axis titles definitions */ }; this.collapsibleTreeData = {}; this.chunkingResult = []; this.collapsibleTreeNodeData = {}; this.smallMultiplesChartxDomainMaximum = 0; this.smallMultipleSortVariable = "page_rank"; this.coloursUsed = []; this.coloursAvailable = []; this.lineCounter = 0; this.topX = 10; /* this.yAxisDomainMaxRounding = 10; */ this.perChunk = 10; this.subTopicLength = 10; this.getSubtopicDetails = getSubtopicDetails; this.selectedSubTopics = {}; this.smallMultipleChartSliceSize = 10; this.tree = {}; this.treemap = null; this.button_width = 40; this.button_height = 14; this.tooltipRectWidth = 275; this.tooltipRectHeight = 100; this.svg = {}; this.colours = [/* "#E25F2B" */ /* NATURE ORANGE - RETIREVED FROM 'defaultLineColour' BELOW */ "#D9035C" /* PINK */, "#694594" /* PURPLE */, "#266DB5" /* BLUE */, "#1E98A3" /* TURQUOISE */, /* "#8B949F", */ /* REMOVED AS ITS TOO CLOSE TO DEFAUTL GREY FILL FOR NODE CIRCLES ; NO DECERNIBLE DIFFERENCE */ "#DDB70C" /* YELLOW */, "#7DB72D" /* APPLE GREEN */, "#2F965E" /* FOREST GREEN */]; this.coloursAvailable = [/* "#E25F2B" */ /* NATURE ORANGE - RETIREVED FROM 'defaultLineColour' BELOW */ "#D9035C" /* PINK */, "#694594" /* PURPLE */, "#266DB5" /* BLUE */, "#1E98A3" /* TURQUOISE */, /* "#8B949F", */ /* REMOVED AS ITS TOO CLOSE TO DEFAUTL GREY FILL FOR NODE CIRCLES ; NO DECERNIBLE DIFFERENCE */ "#DDB70C" /* YELLOW */, "#7DB72D" /* APPLE GREEN */, "#2F965E" /* FOREST GREEN */]; // // NEWLY ADDED BY JAMES ... From 10/08/2022 ... // this.formatDateLabelling = d3.timeFormat("%B %e, %Y"); // get from programmable time to label format time this.formatDateLabelling = d3.timeFormat("%B %Y"); // get from programmable time to label format time this.focus = ""; this.context = ""; this.collapsibleLegendThumbnailTreeData = {}; this.duration = 750; this.barChartDrawn = false; this.topicTreeChartRebuildCalledFrom = "load"; this.LegendArray = []; this.selectedSubTopicNode = ""; this.defaultLineColour = "#E25F2B"; this.defaultBarColour = "#CCCCCC"; this.conceptsColoursUsed = []; this.selectedConcepts = []; this.conceptSelectionCount = 0; this.conceptSelectionLimit = 3; this.conceptColours = ["#006699", "#09A4B0", "#094CB0"]; this.conceptColoursAvailable = ["#006699", "#09A4B0", "#094CB0"]; this.onLoad = true; this.thumbnailLegendValues = [{ id: "3", label: "Sub Topic 1", name: "Sub Topic 1", depth: 0, parent_id: "root", size: 494, modularity: 0.3042178531, concepts: [], relevance: [], page_rank: [], children: [{ id: "12", label: "Sub Topic 4", children: [], name: "Sub Topic 4", depth: 1, parent_id: "3", size: 201, modularity: 0.3109489056, concepts: [], relevance: [], page_rank: [] }] }, { id: "3", label: "Sub Topic 2", name: "Sub Topic 2", depth: 0, parent_id: "root", size: 494, modularity: 0.3042178531, concepts: [], relevance: [], page_rank: [], children: [{ id: "12", label: "Sub Topic 5", children: [], name: "Sub Topic 5", depth: 1, parent_id: "3", size: 201, modularity: 0.3109489056, concepts: [], relevance: [], page_rank: [] }, { id: "13", label: "Sub Topic 6", children: [], name: "Sub Topic 6", depth: 1, parent_id: "3", size: 165, modularity: 0.3109489056, concepts: [], relevance: [], page_rank: [] }, { id: "15", label: "Sub Topic 7", children: [], name: "Sub Topic 7", depth: 1, parent_id: "3", size: 160, modularity: 0.3109489056, concepts: [], relevance: [], page_rank: [] }] }]; } /* name: init description: function to initial building of chart arguments: none returns: none calls: drawCollapsibleTreeChart called from: textDemo.js (nn-charts-poc) */ return _createClass(CollapsibleTreeChart, [{ key: "init", value: function init() { this.drawCollapsibleTreeChart(); this.loaded(); return; } // end function init(); /* name: addOneTimeContent description: function to initial building of chart arguments: none returns: none calls: getSmallMultiplesChartDimensions called from: textDemo.js (nn-charts-poc) */ }, { key: "addOneTimeContent", value: function addOneTimeContent() { var el = this; // remove concepts bar chart gorup element from view d3.selectAll(".small-multiples-svg-group").remove(); // remove main chart title from view d3.selectAll(".staticTextLabel").remove(); // call function to get dimensions of container SVG panel for bar chart creation el.getSmallMultiplesChartDimensions(); // update dimensions of bar chart containing panel d3.selectAll(".small-multiples-svg").attr("width", "100%").attr("height", el.smallMultiplesChart.height + el.smallMultiplesChart.margin.top + el.smallMultiplesChart.margin.bottom) // append group element to contain bar chart .append("g").attr("class", "small-multiples-svg-group").attr("transform", "translate(" + Number(el.smallMultiplesChart.margin.left) + "," + el.smallMultiplesChart.margin.top + ")"); return; } // end function addOneTimeContent /* name: drawCollapsibleTreeChart description: function to initial building of chart arguments: none returns: none calls:redrawLegend recurse updateLineAddPrompt called from: init */ }, { key: "drawCollapsibleTreeChart", value: function drawCollapsibleTreeChart() { var el = this; el.initiated(); // display legend panel depending on which chart view is displayed // if (el.colouredNodes == true) { // d3.selectAll(".legend-type").classed("hide", true); // d3.selectAll(".coloured-legend").classed("hide", false); // } // end if ... if (el.scaledNodes == true) { d3.selectAll(".legend-type").classed("hide", true); d3.selectAll(".scaled-legend").classed("hide", false); } if (el.scaledNodes == false /* && el.colouredNodes == false */) { d3.selectAll(".legend-type").classed("hide", true); d3.selectAll(".default-legend").classed("hide", false); } // x axis definition for topic modelling line chart upper focus chart var x = d3.scaleTime().range([0, el.width]).domain(d3.extent(el.lineChartData, function (d) { return d.formattedDate; })); // x axis definition for topic modelling line chart lower context chart var x2 = d3.scaleTime().range([0, el.width]).domain(d3.extent(el.lineChartData, function (d) { return d.formattedDate; })); // y axis definition for topic modelling line chart upper focus chart var y = d3.scaleLinear().range([el.height, 0]).domain([0, Math.ceil(d3.max(el.lineChartData, function (d) { return d.articles; }) / el.yAxisDomainMaxRounding) * el.yAxisDomainMaxRounding]); // y axis definition for topic modelling line chart lower context chart var y2 = d3.scaleLinear().range([el.height2, 0]).domain([0, Math.ceil(d3.max(el.lineChartData, function (d) { return d.articles; }) / el.yAxisDomainMaxRounding) * el.yAxisDomainMaxRounding]); // line generator function to add the data line to focus line chart el.focusLine2 = d3.line().curve(d3.curveLinear).x(function (d) { return x(d.formattedDate); }).y(function (d) { return y(d.articles); }); // line generator function to add the data line to context line chart el.contextLine2 = d3.line().curve(d3.curveLinear).x(function (d) { return x2(d.formattedDate); }).y(function (d) { return y2(d.articles); }); // initiate JSON array containing line chart legend content el.LegendArrayPrefix = [{ subtopic: el.title + " (total)", topicID: "topicID-0", colour: el.defaultLineColour, index: -1 }]; // call legend to initially draw legend at point of chart/page load el.redrawLegend(); // move legend to front. d3.selectAll(".pearl-svg-legend").moveToFront(); // hide all visualisation .container DIVs. d3.selectAll(".container").classed("hide", true); // show only visualisation .container DIV for topic modelling and line charts d3.selectAll(".container.topicModelling").classed("hide", false); d3.selectAll(".container.line-container").classed("hide", false); // this.tree = {}; // initially construct JSON obj to build topic tree this.collapsibleTreeData = { name: el.title, children: el.data }; // determine minimum value of researcher attribute to scale by ... // use to help define domain range of circle sizing scale el.minResearcherAttributeValue = d3.min(el.data, function (d) { return d.size; }); // determine maximum value of researcher attribute to scale by ... // use to help define domain range of circle sizing scale el.maxResearcherAttributeValue = d3.max(el.data, function (d) { return d.size; }); var layer = el.collapsibleTreeData.children; // call recurse function to construct nested topic tree obj el.recurse(layer); // Set the dimensions and margins of the diagram // locally set window dimensions var margin = { top: 25, right: 25, bottom: 25, left: 100 }; var w = window.innerWidth; var h = window.innerHeight; var height = el.chartHeight - margin.top - margin.bottom; // update dimensions of topic tree svg container panel this.svg = d3.selectAll(".collapsible-tree-svg").attr("width", width).attr("height", height + margin.top + margin.bottom).append("g").attr("class", "collapsible-tree-group").attr("transform", "translate(" + 0 + "," + 0 + ")").classed("hide", true); // calculate width of container div for topic tree var width = d3.selectAll(".Collapsible-tree--wrapper").node().getBoundingClientRect().width - el.margin.left - el.margin.right; // change the width logic // append dynamic text string to prompt number of lines left possible to plot d3.selectAll(".collapsible-tree-svg").append("text").attr("class", "line-counter-label").attr("x", d3.selectAll(".Collapsible-tree--wrapper").node().getBoundingClientRect().width / 2).attr("y", height + margin.bottom); // call function to update line prompt for topic tree, stating number of data lines user can still add to line chart. el.updateLineAddPrompt(); // declares a tree layout and assigns the size el.treemap = d3.tree().size([height, width]); // Assigns parent, children, height, depth el.root = d3.hierarchy(el.collapsibleTreeData, function (d) { return d.children; }); // logical check to determine if sorting of tree branches is needed on number of sub-topics for each tree if (el.root.height > 1) { // sort in descending fashion tree branches based on number of children ... el.root.children.sort(function (x, y) { return el.scaledNodes == false && x.height > 0 ? d3.descending(x.data.children.length, y.data.children.length) : d3.descending(x.data.size, y.data.size); }); // end sort ... } // end logical check on tree depth // calculate step size between vertical horizons of sup topoc nodes. el.treeHorizonStep = width / Number(el.root.height); // position coordiantes for root node within chart space el.root.x0 = height / 2; el.root.y0 = 0; // Collapse after the second level el.root.children.forEach(function (datx) { el.collapseTreeBranch(datx); }); // update base panel to full width d3.selectAll(".collapsible-tree-svg").attr("width", "100%"); // if we want topic tree to be fully expanded at point of on load ... if (el.expandaTreeToFullExtent == true) { // expand initial tree to full extent on all branches. // http://jsfiddle.net/z9tmgpwd/ el.expandAll(); } // if we want topic tree to be fully expanded only to first subtopic layer at point of on load ... else { el.update(el.root, "load"); } } // end function drawCollapsibleTreeChart /* name: expand description: function to expand topic tree branches http://jsfiddle.net/z9tmgpwd/ arguments: d - data for branch node returns: none calls: expand (itself) called from: itself */ }, { key: "expand", value: function expand(d) { var el = this; var children = d.children ? d.children : d._children; if (d._children) { d.children = d._children; d._children = null; } if (children) { children.forEach(function (d, i) { el.expand(d); }); } } // end function expand /* name: expandAll description: function to expand ALL topic tree branches arguments: none returns: none calls:expand update called from: drawCollapsibleTreeChart */ }, { key: "expandAll", value: function expandAll() { var el = this; el.expand(el.root); el.update(el.root, "expandAll"); return; } // end function expandAll /* name: collapseAll description: function to collapse ALL topic tree branches arguments: none returns: none calls:collapse update called from: ??? */ }, { key: "collapseAll", value: function collapseAll() { var el = this; el.root.children.forEach(el.collapse); el.collapse(el.root); el.update(el.root, "collapseAll"); return; } // end function collapseAll /* name: collapse description: function to collapse some topic tree branches arguments: d - data for node returns: none calls:collapse (itself) called from: collapse (itself) */ }, { key: "collapse", value: function collapse(d) { var el = this; if (d.children) { d._children = d.children; d._children.forEach(el.collapse); d.children = null; } return; } // end function collapse /* name: recurse description: function to construct nested JSON arrray to build topic tree arguments: layer - data layer to consider returns: calls: called from: recurse */ }, { key: "recurse", value: function recurse(layer) { var el = this; if (layer.length == 0) { // stop calling itself //... } else { layer.forEach(function (d, i) { if (d.children && d.children.length > 0) { // sort children array selected sort variable var childrenArray = d.children; childrenArray.sort(function (x, y) { return d3.descending(x.size, y.size); }); // var layer = d; layer.zippedData = d3.zip(layer["concepts"], layer["page_rank"], layer["relevance"]); layer.JSONMappedZippedDataForSMBarChart = layer.zippedData.map(function (d, i) { return { concept: d[0], page_rank: d[1], relevance: d[2] }; }); el.collapsibleTreeNodeData[layer.label] = layer; layer.children.forEach(function (d, i) { if (d.children && d.children.length > 0) { el.recurse(d); } else { d.zippedData = d3.zip(d["concepts"], d["page_rank"], d["relevance"]); d.JSONMappedZippedDataForSMBarChart = d.zippedData.map(function (d, i) { return { concept: d[0], page_rank: d[1], relevance: d[2] }; }); el.collapsibleTreeNodeData[d.label] = d; } }); } else { d.zippedData = d3.zip(d["concepts"], d["page_rank"], d["relevance"]); d.JSONMappedZippedDataForSMBarChart = d.zippedData.map(function (d, i) { return { concept: d[0], page_rank: d[1], relevance: d[2] }; }); el.collapsibleTreeNodeData[d.label] = d; } }); } // end else ... return; } // end function recurse /* name: update description: function to update displayed topic tree arguments:source treeLoadedFrom returns: calls:clickNode redrawLegend lineAddRemove el.updateLineAddPrompt called from: collapseAll expandAll ...windowResize */ }, { key: "update", value: function update(source, treeLoadedFrom) { // var el = this; // // Assigns the x and y position for the nodes el.treeData = el.treemap(el.root); // // Compute the new tree layout. var nodes = el.treeData.descendants(); var links = el.treeData.descendants().slice(1); // Normalize for fixed-depth. nodes.forEach(function (d) { d.y = d.depth * el.treeHorizonStep; }); // ****************** Nodes section *************************** // Update the nodes... var node = el.svg.selectAll("g.node").data(nodes, function (d) { return d.data.id || (d.id = 0); }); // Enter any new nodes at the parent's previous position. var nodeEnter = node.enter().append("g").attr("class", function (d) { var topicID = d.data.id ? d.data.id : 0; var parentID = d.data.parent_id ? d.data.parent_id : 0; var str = "nodeGroup node node-" + parentID + "-" + topicID + " topicID-" + parentID + "-" + topicID; return str; }).attr("transform", "translate(" + el.treeHorizonStep + "," + source.x0 + ")"); // Add main circle for the nodes nodeEnter.append("circle").attr("class", function (d) { var topicID = d.data.id ? d.data.id : 0; var parentID = d.data.parent_id ? d.data.parent_id : 0; var str = "nodeTree node node-" + parentID + "-" + topicID + " topicID-" + parentID + "-" + topicID; return str; }).attr("r", 1e-6).on("click", function (d, i) { // var clickedNodeIndex = i; // // reset all variables relating to user concept selection on bar chart. // fixes bug relating to slecting 2nd and subseuqent sub topic nodes and bar chart highlighting not coming through. el.selectedConcepts = []; el.conceptSelectionCount = 0; el.conceptColoursAvailable = ["#006699", "#09A4B0", "#094CB0"]; el.conceptsColoursUsed = []; var state = "deselected"; el.barChartDrawn = true; if (d3.select(this).classed("selectedNode")) { // update class declarations of selected node d3.select(this).classed("selectedNode", false); // remove content d3.selectAll(".small-multiples-svg-group").remove(); d3.selectAll(".staticTextLabel.selectedSubTopicTitle").remove(); // update state of interacted node` state = "deselected"; } else { // update class declarations of selected node d3.selectAll(".selectedNode").classed("selectedNode", false); d3.select(this).classed("selectedNode", true); // update state of interacted node` state = "selected"; } // call function when node is clicked ... // currently builds bar chart el.clickNode(d, clickedNodeIndex, state); return; }); // append new smaller circle to allow user to add/remove data lines from line chart nodeEnter.append("circle").attr("class", function (d, i) { var circleClassIndex = i; var name = d.data.name ? d.data.name : d.data.label; var desired = name.replace(/[^\w\s]/gi, ""); var newName = desired.replaceAll(" ", "-"); var isSelected = name == el.title ? "line-selected" : ""; var topicID = d.data.id ? "topicID-" + d.data.id : "topicID-0"; return "nodeTree lineNode lineNode-" + circleClassIndex + " " + newName + " " + isSelected + " LINENODE " + topicID; }).attr("cx", 12.5).attr("cy", 12.5).attr("r", 7.5).style("display", function () { return /* el.colouredNodes == true || */el.scaledNodes == true ? "none" : "inline"; }).style("pointer-events", function (d) { var name = d.data.name ? d.data.name : d.data.label; return name == el.topic ? "none" : "auto"; }).style("fill", function (d) { var name = d.data.name ? d.data.name : d.data.label; return name == d.data.name ? "#e25f2b" : "#999999"; }).on("mouseover", function (d, i) { var circleMouseOverIndex = i; var numberSubTopicsAlreadySelected = Object.keys(el.selectedSubTopics).length; var mainTopicState = d3.selectAll(".nodeTree.lineNode.lineNode-0").classed("line-selected"); var thisState = d3.selectAll(".nodeTree.lineNode.lineNode-" + i).classed("line-selected"); if (numberSubTopicsAlreadySelected == el.colours.length && thisState == false) { d3.selectAll(".nodeTree.lineNode.lineNode-" + /* i */circleMouseOverIndex).style("cursor", "no-drop"); return; } // determine topic ID var topicID = d.data.id ? "topicID-" + d.data.id : "topicID-0"; // mouse over data line add/remove node ... d3.selectAll(".nodeTree.lineNode.lineNode-" + /* i */circleMouseOverIndex).transition().duration(500).ease(d3.easeLinear).attr("r", 12.5).delay(50).transition().duration(500).ease(d3.easeLinear).attr("r", 7.5); // append group element for attaching topic tree specific tooltip to el.tooltip = d3.selectAll(".collapsible-tree-group").append("g").attr("class", "nodeTree-tooltip-group").attr("id", "nodeTree-tooltip-group").attr("transform", function () { return "translate(" + Number(d.y + 25) + "," + Number(d.x + 5) + ")"; }) // append rect for tooltip pointer .append("rect").attr("class", "tooltip-pointer").attr("x", 0).attr("y", 0).attr("width", 10).attr("height", 10); // append text to tooltip d3.selectAll(".nodeTree-tooltip-group").append("text").attr("class", "nodePromptLabel showLine").attr("x", 10).attr("y", 12.5).text(function () { if (numberSubTopicsAlreadySelected == 0 && topicID == "topicID-0" || numberSubTopicsAlreadySelected == 1 && mainTopicState == false && thisState == true) { d3.selectAll(".nodeTree.lineNode.lineNode-" + circleMouseOverIndex).style("cursor", "no-drop"); // console.log("CANNOT REMOVE THIS DATA LINE WITH NO OTHER SUB-TOPICS or MAIN TOPIC SELECTED"); // return "Select another [main or sub-]topic before deleting this line"; } else if (d3.selectAll(".nodeTree.lineNode.lineNode-" + circleMouseOverIndex).classed("line-selected")) { d3.selectAll(".nodeTree.lineNode.lineNode-" + circleMouseOverIndex).style("cursor", "pointer"); return "Click to remove data lines from line chart"; } // end if ... else { d3.selectAll(".nodeTree.lineNode.lineNode-" + circleMouseOverIndex).style("cursor", "pointer"); return "Click to add data lines to line chart"; } }); // append rect for tooltip background d3.selectAll(".nodeTree-tooltip-group").append("rect").attr("class", "tooltip-frame").attr("x", 0).attr("y", -2.5).attr("width", function () { // https://stackoverflow.com/questions/59377004/get-width-of-text-node-in-d3-inside-attrs-method d3.selectAll(".nodePromptLabel.showLine").moveToFront(); return d3.selectAll(".nodePromptLabel.showLine").node().getComputedTextLength() + 20; }).attr("height", 25); d3.selectAll(".nodeTree-tooltip-group").attr("transform", function () { var containerWidth = +d3.selectAll(".Collapsible-tree--wrapper").style("width").replaceAll("px", ""); // if interaction is on righthand side of container DIV ... if (d.y > containerWidth / 2) { d3.selectAll(".nodeTree-tooltip-group").selectAll(".tooltip-pointer").style("transform", "translate(" + Number(d3.selectAll(".nodeTree-tooltip-group").selectAll(".tooltip-frame").node().getBoundingClientRect().width) + "px,0px) rotate(45deg)"); // return "translate(" + Number(d.y - d3.selectAll(".nodePromptLabel.showLine").node().getComputedTextLength() - 20) + "," + Number(d.x + 5) + ")"; } // else if interaction is on lefthand side of container DIV ... else { return "translate(" + Number(d.y + 25) + "," + Number(d.x + 5) + ")"; } }); return; }).on("mouseout", function () { d3.selectAll(".nodeTree-tooltip-group").remove(); }).on("click", function (d, i) { var clickIndex = i; var numberSubTopicsAlreadySelected = Object.keys(el.selectedSubTopics).length; var mainTopicState = d3.selectAll(".nodeTree.lineNode.lineNode-0").classed("line-selected"); var thisState = d3.selectAll(".nodeTree.lineNode.lineNode-" + /* i */clickIndex).classed("line-selected"); // update relevant variables var topicID = d.data.id ? "topicID-" + d.data.id : "topicID-0"; var topicName = d.data.name ? d.data.name : d.data.label; var circle = d3.select(".LINENODE." + topicID); el.selectedSupTopicNode = topicID; // error checking/handling for unique use case of person wanting to remove main topic lines/dots // before any sub topics are displayed. if (numberSubTopicsAlreadySelected == 0 && topicID == "topicID-0" || numberSubTopicsAlreadySelected == 1 && mainTopicState == false && thisState == true) { d3.select(this).style("cursor", "no-drop"); console.log("CANNOT REMOVE THIS DATA LINE WITH NO OTHER SUB-TOPICS or MAIN TOPIC SELECTED"); return; } // end if ... // update cursor pointer style d3.select(this).style("cursor", "pointer"); // if topic selected in main topic, not subtopic if (topicID == "topicID-0") { if (circle.classed("line-selected")) { el.LegendArrayPrefix = []; circle.classed("line-selected", false).style("fill", "#999999"); } else { el.LegendArrayPrefix = [{ subtopic: el.title + " (total)", topicID: "topicID-0", colour: el.defaultLineColour, index: -1 }]; circle.classed("line-selected", true).style("fill", el.LegendArrayPrefix[0].colour); } // call function to update and redraw line chart legend el.redrawLegend(); // call function to handle adding/remiving the [sub]topics data lines el.lineAddRemove(topicID); } else { // if maximum number of lines has already been reached ... if (el.lineCounter == el.colours.length) { el.maxLinesReached = true; // if line is already selected and you want to remove its line from the chart if (circle.classed("line-selected")) { var circleColour = d3.select(this).style("fill"); circle.classed("line-selected", false).style("fill", "#999999"); delete el.selectedSubTopics[el.selectedSupTopicNode]; el.coloursAvailable.push(circleColour); var index = el.coloursUsed.indexOf(circleColour); el.coloursUsed.splice(index, 1); el.lineCounter--; // call function to handle adding/remiving the [sub]topics data lines el.lineAddRemove(topicID); // call function to update and redraw line chart legend el.redrawLegend(); // call fucntion to update line add prompt below topic tree el.updateLineAddPrompt(); } else { console.log("YOU HAD reached max number of lines (", el.lineCounter, ") YOU JUST CLICKED ON ANOTHER NEW GREY DOT BUT CANNOT ADD IT DUE TO ALREADY HITTING THE MAX."); d3.selectAll(".nodeTree.lineNode.lineNode-" + /* i */clickIndex).style("cursor", "no-drop"); } return; // else if maximum number of lines has NOT already been reached ... } else { // if line has already been selected ... if (circle.classed("line-selected")) { var componentToHex = function componentToHex(c) { var hex = c.toString(16); return hex.length == 1 ? "0" + hex : hex; }; var rgbToHex = function rgbToHex(r, g, b) { return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); }; // store converted colour delete el.selectedSubTopics[el.selectedSupTopicNode]; // get colour fill of selected circle. var colour = d3.select(this).style("fill").replaceAll("rgb(", "").replaceAll(")", "").split(", "); // change colour fill of selected circle to white circle.classed("line-selected", false).style("fill", "#999999"); var convertedColour = rgbToHex(+colour[0], +colour[1], +colour[2]); // modify colour arrays to reflect this circle selection. el.coloursAvailable.push(convertedColour); var index = el.coloursUsed.indexOf(convertedColour); el.coloursUsed.splice(index, 1); // decrement line counter by 1 el.lineCounter--; } // if line has NOT already been selected ... else { // modify colour arrays to reflect this circle selection. el.colourToUse = el.coloursAvailable[0]; el.coloursUsed.push(el.colourToUse); el.coloursAvailable = el.coloursAvailable.filter(function (i) { var filterIndex = i; return this.indexOf( /* i */filterIndex) < 0; }, el.coloursUsed); // update fill colour of selected circle. circle.classed("line-selected", true).style("fill", el.colourToUse); // construct and store new JSON element based on subtopic selected and added to line chart el.selectedSubTopics[el.selectedSupTopicNode] = { subtopic: topicName, topicID: topicID, colour: el.colourToUse, index: el.lineCounter }; // increment line counter by 1 el.lineCounter++; } // end else. // call function to handle adding/remiving the [sub]topics data lines el.lineAddRemove(topicID); // call function to update and redraw line chart legend el.redrawLegend(); // call function to update line add prompt below topic tree el.updateLineAddPrompt(); } return; } return; }); // Add Circle to allow user to click to Show Hierarchy Chart // Add labels to the nodes nodeEnter.append("text").attr("dy", ".35em").attr("class", function (d, i) { var nodeLabelIndex = i; return "node-text-label notWrapped original node-text-label-" + nodeLabelIndex; }).attr("x", -25).attr("y", 0).style("text-anchor", "end").text(function (d, i) { var nodeLabelTextIndex = i; var dataLabel = d.data.name ? d.data.name : d.data.label; return el.scaledNodes == true && nodeLabelTextIndex > 0 ? dataLabel + " (" + el.numberWithCommas(+d.data.size) + ")" : dataLabel; }); // // wrap sub-topic node labels if too long, and page has just loaded. if (el.onLoad == true) { el.selectLabelsToWrap(); el.onLoad = false; } // end if check .. // Add count labels to the nodes nodeEnter.append("text").attr("class", "node-text-count").attr("dy", ".35em").attr("x", 0).style("display", function () { return el.scaledNodes == true ? "none" : "inline"; }).text(function (d) { d.data.label ? d._children ? d3.select(this.parentNode).classed("hasChildren", true) : null : null; return d.children || d._children ? d.data.children.length : ""; }); // reposition entire tree based on width of tree (number of layers/horizons) starting from main topic node. d3.selectAll(".collapsible-tree-group").attr("transform", "translate(" + (Math.ceil(d3.selectAll(".node.node-0-0").node().getBoundingClientRect().width / 10) * 10 - 20) + "," + 0 + ")").classed("hide", false); // UPDATE var nodeUpdate = nodeEnter.merge(node); // modify transiton duration based on source if (treeLoadedFrom == "load") { el.duration = 750; } else if (treeLoadedFrom == "newBranch") { el.duration = 750; } else if (treeLoadedFrom == "windowResize") { el.duration = 0; } // Transition to the proper position for the node nodeUpdate.transition().duration(el.duration).attr("transform", function (d) { return "translate(" + d.y + "," + d.x + ")"; }); // construct circle chape scaling law // https://gist.github.com/guilhermesimoes/e6356aa90a16163a6f917f53600a2b4a el.circleRadius = d3.scalePow().exponent(0.5).domain([0, el.maxResearcherAttributeValue]).range([0, 100]); // initialise locally array to contain data values to scale legend circles by ... var circleRadii = []; // convert max value to string to allow dynamic array to be constructed for all values below max data value. var maxResearcherAttributeValueAsString = el.maxResearcherAttributeValue.toString(); var nextLevelDown = "1"; // loop through array for (var /* i */counter = 0; /* i */counter < maxResearcherAttributeValueAsString.length - 1; /* i */counter++) { // generate next level value to build legend/array nextLevelDown = nextLevelDown + "0"; // push onto array circleRadii.push(+nextLevelDown); } // end for loop // push final data value onto array to build dynamic legend. circleRadii.push(el.maxResearcherAttributeValue); // const circleRadiiTopics = [ // "Ecological Risk and Pollution Assessment", // "Ecological Restoration and Land Degradation", // "Carbon Sequestration and Climate Change Mitigation", // "Agricultural and Landscape Management Practices", // "Soil Properties and Organic Carbon", // "Land Use and Cover Change", // "Biodiversity Conservation and Monitoring", // "Ecosystem Services and Human Impact", // ]; // circleRadii = [1196, 1767, 3087, 3349, 7037, 7103, 7329, 11171]; // dynamically update height attr of legend-containing base SVG panel so legend is not truncated if (el.scaledNodes == true) { d3.selectAll(".topicModelling-legend-half").style("height", Number(10 + 2 * el.circleRadius(circleRadii[circleRadii.length - 1])) + "px"); // remove old legend d3.selectAll(".legend-scaled-circles-group").remove(); // selected legend attribute, and containing grop g element var scaledLegend = d3.selectAll(".scaled-legend-svg").append("g").attr("class", "legend-scaled-circles-group").attr("transform", "translate(" + 0 + "," + 0 + ")"); // append new circles onto legend to repesetn each of the data values to scale by scaledLegend.selectAll("legend-scaled-circles").data(circleRadii).enter().append("circle").attr("class", "legend-scaled-circles").attr("cx", function /* d, i */ () { return Number(5 * el.circleRadius(circleRadii[circleRadii.length - 1])); }).attr("cy", function /* d, i */ () { return Number(5 + el.circleRadius(circleRadii[circleRadii.length - 1])); }).attr("r", function (d /* , i */) { return el.circleRadius(d); }); // append new lines onto legend to represent each of the data values to scale by scaledLegend.selectAll("legend-scaled-markerLines").data(circleRadii).enter().append("line").attr("class", "legend-scaled-markerLines").attr("x1", 5 * el.circleRadius(circleRadii[circleRadii.length - 1])).attr("x2", function (d, i) { var factor = i % 2 == 0 ? -1 : 1; return 5 * el.circleRadius(circleRadii[circleRadii.length - 1]) + factor * 250; }).attr("y1", function (d, i) { return 5 + el.circleRadius(circleRadii[circleRadii.length - 1]) + el.circleRadius(circleRadii[i]); }).attr("y2", function (d, i) { return 5 + el.circleRadius(circleRadii[circleRadii.length - 1]) + el.circleRadius(circleRadii[i]); }); // append new line labels onto legend to represent each of the data values to scale by scaledLegend.selectAll("legend-scaled-markerLineLabels").data(circleRadii).enter().append("text").attr("class", "legend-scaled-markerLineLabels").attr("x", function (d, i) { var factor = i % 2 == 0 ? -1 : 1; return 5 * el.circleRadius(circleRadii[circleRadii.length - 1]) + factor * 250; }).attr("y", function (d, i) { return 5 + el.circleRadius(circleRadii[circleRadii.length - 1]) + el.circleRadius(circleRadii[i]); }).style("text-anchor", function (d, i) { return i % 2 == 0 ? "end" : "start"; }).text(function (d, i) { return i == circleRadii.length - 1 ? el.numberWithCommas(+d) + " (max number of articles in topic)" : el.numberWithCommas(+d); }); } // end if ... // construct colour scale to annotate cirles and legend with (likely going to be removed from future builds) el.circleColour = d3.scaleLinear().domain([0, el.maxResearcherAttributeValue * 0.2, el.maxResearcherAttributeValue * 0.4, el.maxResearcherAttributeValue * 0.6, el.maxResearcherAttributeValue * 0.8, el.maxResearcherAttributeValue]).range(["#7EC93C", "#4EBFB9", "#48B8F0", "#A0A7FA", "#C89AFC", "#F39ACE"]); // update colour legend explanatory labels d3.selectAll(".colour-ramp-rangeLabel-left").html("1 article"); d3.selectAll(".colour-ramp-rangeLabel-right").html(el.numberWithCommas(el.maxResearcherAttributeValue + " articles")); // Update the node attributes and style nodeUpdate.select("circle.node").attr("r", function (d, i) { return el.scaledNodes == true && i > 0 ? el.circleRadius(d.data.size) : 15; }).style("fill", function (d, i) { return; /* el.colouredNodes == true && i > 0 ? el.circleColour(d.data.size) : */ "#999999"; }).attr("cursor", "pointer").on("mouseover", function (d) { if (d.data.id) { if (el.scaledNodes == false) { // mouse over main node ... d3.select(this).transition().duration(500).ease(d3.easeLinear).attr("r", 17.5).delay(50).transition().duration(500).ease(d3.easeLinear).attr("r", 15); } // end if check el.tooltip = d3.selectAll(".collapsible-tree-group").append("g").attr("class", "nodeTree-tooltip-group").attr("id", "nodeTree-tooltip-group").attr("transform", function () { return "translate(" + Number(d.y + 20) + "," + Number(d.x - 5) + ")"; }).append("rect").attr("class", "tooltip-pointer").attr("x", 0).attr("y", 0).attr("width", 10).attr("height", 10); d3.selectAll(".nodeTree-tooltip-group").append("text").attr("class", "nodePromptLabel showConcepts").attr("x", 10).attr("y", 12.5).text(function () { // // NOTE: possibly needs to be improved a little further to truly account for all potential scenarios. // if (d3.selectAll(".topicID-" + d.data.parent_id + "-" + d.data.id)) { if (d.height > 0 && d.children) { return "Click to contract sub-topic and view its Top 10 related concepts"; } else if (d.height > 0 && d._children) { return "Click to expand sub-topic and view its Top 10 related concepts"; } else { return "Click to view sub-topic's Top 10 related concepts"; } } }); // append rectangle to act as tooltip background d3.selectAll(".nodeTree-tooltip-group").append("rect").attr("class", "tooltip-frame").attr("x", 0).attr("y", -2.5).attr("width", function () { d3.selectAll(".nodePromptLabel.showConcepts").moveToFront(); return d3.selectAll(".nodePromptLabel.showConcepts").node().getComputedTextLength() + 20; }).attr("height", 25); } // dynamically repositon tooltip based on if user interaction is on lefrt or right of chart area d3.selectAll(".nodeTree-tooltip-group").attr("transform", function () { var containerWidth = +d3.selectAll(".Collapsible-tree--wrapper").style("width").replaceAll("px", ""); // if interaction is on righthand side of container DIV ... if (d.y > containerWidth / 2) { d3.selectAll(".nodeTree-tooltip-group").selectAll(".tooltip-pointer").style("transform", "translate(" + Number(d3.selectAll(".nodeTree-tooltip-group").selectAll(".tooltip-frame").node().getBoundingClientRect().width) + "px,0px) rotate(45deg)"); // return "translate(" + Number(d.y - d3.selectAll(".nodePromptLabel.showConcepts").node().getComputedTextLength() - 40) + "," + Number(d.x - 5) + ")"; } // else if interaction is on lefthand side of container DI