@springernature/nn-charts
Version:
Visualization for DAS products
1,106 lines (994 loc) • 99.1 kB
JavaScript
"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