UNPKG

@springernature/nn-charts

Version:
932 lines (894 loc) 46.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var d3 = _interopRequireWildcard(require("d3")); var _treeChartHelpers = require("../../utils/tree-chart-helpers"); var _shapeGeneration = require("../../utils/shape-generation"); var _common = require("../../utils/common"); var _tooltips = require("../../utils/tooltips"); var _zoomControls = require("../../utils/zoom-controls"); 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 _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread 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 _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } 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 ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } 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); } var DEFAULT_CONFIG = { svgBasePanelClassName: "knowledge-graph-base-panel", svgBasePanelWidth: "100%", svgBasePanelHeight: 700, marginOffsets: { top: 50, right: 50, bottom: 50, left: 50 }, graphLayout: { circleRadiusScaleDomain: [0, 10], circleRadiusScaleRange: [0, 20], chargeStrength: -6000, chargeDistanceMax: 2400, forceLinkStrength: 1, linkDistance: 150, linkStrength: 1, strokeFill: "#a0a0a0", strokeOpacity: 0.15, strokeWidth: 1.3 }, zoomControls: { isVisible: true, position: "bottom", // top, bottom placement: "right", // left , right, center icons: ["zoom-in", "zoom-out", "zoom-centre"], chartZoomMinimum: 1, chartZoomMaximum: 10, zoomLabelDisplayThreshold: 1.5, zoomScaleFactor: 1.25 }, legend: { isVisible: true, position: "bottom", // top, bottom placement: "right", // left , right, center legendItems: [] // array of objects where object should have name (string), color (hex code string) and swatchSymbol ('circle' | 'line') }, tooltip: { isVisible: true, tooltipContainerClass: "tooltip-container", headerTemplate: null, bodyTemplate: null, footerTemplate: null }, nodes: { nodeIdentifierKey: "nodeId", // unique identifer of each node object selectedNodeIdentifierValue: "", // value to be passed by consumer of this chart nodesBeScaledByKey: "articles", // key from node object to be used for scaling nodes nodeHighlightShadowColor: "#FF824A52", nodesBeHighlightedByKey: "", // country - used for highlighting nodes based on key passed by consumer selectedNodeHighlightByKeyValue: [] // value of nodesBeHighlightedByKey for the selected node }, links: { startNodeIdentifier: "startNodeIdentifier", endNodeIdentifier: "endNodeIdentifier" }, label: { nodes: [], links: [], displayNodeLabels: false } }; var KnowledgeGraphChart = exports["default"] = /*#__PURE__*/function () { function KnowledgeGraphChart(element, config, loadingStartedCb, loadingEndedCb, onErrorCb, onTooltipFooterBtnClick) { _classCallCheck(this, KnowledgeGraphChart); this.config = (0, _common.updateNestedProperties)(DEFAULT_CONFIG, config); this.chartParentElement = element; this.loadingStartedCallback = loadingStartedCb; this.loadingEndedCallback = loadingEndedCb; this.onErrorCallback = onErrorCb; this.onTooltipFooterBtnClickCallback = onTooltipFooterBtnClick; this.circleRadiusScale = d3.scalePow().exponent(0.5); // window.addEventListener("resize", () => this.resizeSvgContent()); } return _createClass(KnowledgeGraphChart, [{ key: "render", value: function render(_ref) { var _this = this; var nodes = _ref.nodes, links = _ref.links; try { if (!nodes || !links) return alert("Chart cannot be built as data is not available"); if (this.loadingStartedCallback) this.loadingStartedCallback(); (0, _common.clearSvgBasePanel)(this.config.svgBasePanelClassName); // initialize nodes and edges this.nodes = nodes; this.links = links; this.svgBasePanel = (0, _shapeGeneration.createBaseSVGPanel)(this.chartParentElement, this.config.svgBasePanelClassName, this.config.svgBasePanelWidth, this.config.svgBasePanelHeight); this.circleRadiusScale.domain(this.config.graphLayout.circleRadiusScaleDomain).range(this.config.graphLayout.circleRadiusScaleRange); // sort node by name this.nodes = (0, _treeChartHelpers.sortByKey)(this.nodes, "name"); // format nodeId field for each object of the nodes this.nodes.forEach(function (node) { node.nodeIdentierFormatted = node[_this.config.nodes.nodeIdentifierKey].replaceAll(".", "_"); }); var maximumDataRange = this.findMaximumValueFromList(this.nodes, this.config.nodes.nodesBeScaledByKey); this.updateCircleRadiusRangeDefinition(maximumDataRange); this.addditionalNodes = []; // filter links this.filteredLinks = this.links.filter(function (d) { // add formatted Id attributes to ease processing ... d.startNodeIdFormatted = d[_this.config.links.startNodeIdentifier].replaceAll(".", "_"); d.endNodeIdFormatted = d[_this.config.links.endNodeIdentifier].replaceAll(".", "_"); _this.addditionalNodes.push(d[_this.config.links.endNodeIdentifier]); return d[_this.config.links.startNodeIdentifier]; }); this.filteredNodes = this.nodes.filter(function (d) { return d[_this.config.nodes.nodeIdentifierKey] === _this.config.nodes.selectedNodeIdentifierValue || _this.addditionalNodes.indexOf(d[_this.config.nodes.nodeIdentifierKey]) !== -1; }); this.filteredNodes.forEach(function (d, i) { d.id = d[_this.config.nodes.nodeIdentifierKey]; // same object is pushed twice - courtesy from prototype code written by James Bayliss _this.config.label.nodes.push({ node: d }); _this.config.label.nodes.push({ node: d }); _this.config.label.links.push({ source: i * 2, target: i * 2 + 1 }); }); this.filteredLinks.forEach(function (d) { d.source = d[_this.config.links.startNodeIdentifier]; d.target = d[_this.config.links.endNodeIdentifier]; }); // avoid labels overlapping node circles d3.forceSimulation(this.config.label.nodes).force("charge", d3.forceManyBody().strength(-50)).force("link", d3.forceLink(this.config.label.links).distance(0).strength(2)).stop(); this.graphLayout = this.addSimulationToGraphLayout(); // store the indices of source and target links this.adjlist = []; this.filteredLinks.forEach(function (d) { _this.adjlist[d.source.index + "-" + d.target.index] = true; _this.adjlist[d.target.index + "-" + d.source.index] = true; }); this.svgBasePanel = this.resizeSvgBasePanel(); this.chartZoom = this.getZoomBehaviour(this.config.zoomControls.chartZoomMinimum, this.config.zoomControls.chartZoomMaximum); var _this$getSvgPanelCent = this.getSvgPanelCenterTranslateValues(), initialOriginX = _this$getSvgPanelCent.initialOriginX, initialOriginY = _this$getSvgPanelCent.initialOriginY; this.initialOriginX = initialOriginX; this.initialOriginY = initialOriginY; this.initialZoomTransform = d3.zoomIdentity.translate(this.initialOriginX, this.initialOriginY).scale(1); this.chartZoom.transform(this.svgBasePanel, this.initialZoomTransform); this.baseGroup = this.appendBaseGroup(); this.linksGroupElement = this.appendEdgesToBaseGroup(); this.nodesGroupElement = this.appendNodesToBaseGroup(); this.nodeCirclesElement = this.appendCircleToBaseNodeGroup(); this.appendLabelToBaseNodeGroup(); // add zoom handler this.chartZoom.on("zoom", function () { return _this.zoomHandler(); }); this.svgBasePanel.call(this.chartZoom).on("dblclick.zoom", null); // add drag support for base node group var dragBehavior = this.getDragBehaviour(); // apply the drag behavior to the node group elements this.nodesGroupElement.call(dragBehavior); // construct tooltip if (this.config.tooltip.isVisible) { var customTemplate = { header: this.config.tooltip.headerTemplate, body: this.config.tooltip.bodyTemplate, footer: this.config.tooltip.footerTemplate }; var tooltipTemplate = this.constructTooltip(undefined, false, customTemplate); var svgContainer = this.getSvgContainerElement(); (0, _common.appendElementToTarget)(tooltipTemplate, svgContainer); this.registerSvgContainerListeners(svgContainer); this.hideTooltip(); } // construct legends if (this.config.legend.isVisible) this.constructLegends(); // construct zoom controls var zoomControls = _objectSpread(_objectSpread({}, this.config.zoomControls), {}, { onZoomControlsContainerCreated: function onZoomControlsContainerCreated(element) { return element.style.top = "2rem"; } }); if (this.config.zoomControls.isVisible) (0, _zoomControls.constructZoomControls)(this.config.svgBasePanelClassName, zoomControls); // add zoom control click handlers this.registerZoomControlListeners(); } catch (error) { console.error("Error in KnowledgeGraphChart.render function: ", error); if (this.onErrorCallback) this.onErrorCallback(error); } if (this.loadingEndedCallback) this.loadingEndedCallback(); } }, { key: "getSvgBasePanelDimensions", value: function getSvgBasePanelDimensions() { var _this$svgBasePanel$no = this.svgBasePanel.node(), svgBasePanelWidth = _this$svgBasePanel$no.clientWidth, svgBasePanelHeight = _this$svgBasePanel$no.clientHeight; return { svgBasePanelWidth: parseFloat(svgBasePanelWidth), svgBasePanelHeight: parseFloat(svgBasePanelHeight) }; } }, { key: "registerSvgContainerListeners", value: function registerSvgContainerListeners(svgContainer) { var _this2 = this; d3.select(svgContainer).on("click", function () { var target = d3.event.target; if (!target.classList.contains("node") && !target.classList.contains("link")) _this2.hideTooltip(); }); } }, { key: "registerZoomControlListeners", value: function registerZoomControlListeners() { var _this3 = this; d3.selectAll(".zoom-in").on("click", function () { return _this3.chartZoom.scaleBy(_this3.svgBasePanel.transition().duration(50), _this3.config.zoomControls.zoomScaleFactor); }); d3.selectAll(".zoom-out").on("click", function () { return _this3.chartZoom.scaleBy(_this3.svgBasePanel.transition().duration(50), 1 / _this3.config.zoomControls.zoomScaleFactor); }); d3.selectAll(".zoom-centre").on("click", function () { var newTransform = d3.zoomIdentity.translate(_this3.initialOriginX, _this3.initialOriginY) // .scale(hRatio < wRatio ? hRatio : wRatio); .scale(1); _this3.chartZoom.transform(_this3.svgBasePanel, newTransform); }); } }, { key: "resizeSvgContent", value: function resizeSvgContent() { var _this$getSvgBasePanel = this.getSvgBasePanelDimensions(), svgBasePanelWidth = _this$getSvgBasePanel.svgBasePanelWidth, svgBasePanelHeight = _this$getSvgBasePanel.svgBasePanelHeight; var baseGroup = d3.select(".base-group"); // Get the initial bounding box of the base group var baseGroupBBox = baseGroup.node().getBBox(); var initialWidth = baseGroupBBox.width, initialHeight = baseGroupBBox.height; var aspectRatio = initialWidth / initialHeight; var newWidth = svgBasePanelWidth; var newHeight = svgBasePanelHeight; // Calculate the new dimensions while maintaining the aspect ratio if (svgBasePanelWidth / svgBasePanelHeight > aspectRatio) { newWidth = svgBasePanelHeight * aspectRatio; } else { newHeight = svgBasePanelWidth / aspectRatio; } // Calculate scale based on the new dimensions var scale = newWidth / initialWidth; // Calculate scale based on the smaller dimension to maintain aspect ratio // const scale = Math.min( // svgBasePanelWidth / initialWidth, // svgBasePanelHeight / initialHeight // ); // Calculate the translation to center the content var translateX = (svgBasePanelWidth - newWidth) / 2; var translateY = (svgBasePanelHeight - newHeight) / 2; // Calculate the translation to center the content // const translateX = (svgBasePanelWidth - initialWidth * scale) / 2; // const translateY = (svgBasePanelHeight - initialHeight * scale) / 2; // Apply the scale and translation to the base group baseGroup.attr("transform", "translate(".concat(translateX, ", ").concat(translateY, ") scale(").concat(scale, ")")); } }, { key: "resizeSvgBasePanel", value: function resizeSvgBasePanel() { return d3.selectAll(".".concat(this.config.svgBasePanelClassName)).attr("width", this.config.svgBasePanelWidth).attr("height", this.config.svgBasePanelHeight).on("click", function () { return d3.event.preventDefault(); }).on("dblclick", function () { return d3.event.preventDefault(); }); } }, { key: "appendBaseGroup", value: function appendBaseGroup() { return this.svgBasePanel.append("g").attr("class", "base-group").attr("transform", "translate(".concat(this.initialOriginX, ",").concat(this.initialOriginY, ") scale(", 1, ")")); } }, { key: "appendEdgesToBaseGroup", value: function appendEdgesToBaseGroup() { var _this4 = this; return this.baseGroup.append("g").attr("class", "links").selectAll("line").data(this.filteredLinks).enter().append("line").attr("class", function (d) { return "link source-".concat(d.startNodeIdFormatted, " target-").concat(d.endNodeIdFormatted); }).attr("stroke", this.config.graphLayout.strokeFill).style("stroke-width", this.config.graphLayout.strokeWidth).style("stroke-opacity", this.config.graphLayout.strokeOpacity).on("mouseover", function (d) { var _this4$clickedLink; // Check if this is not the clicked link if (d3.event.target !== ((_this4$clickedLink = _this4.clickedLink) === null || _this4$clickedLink === void 0 ? void 0 : _this4$clickedLink.element) && !_this4.isLinkConnectedToClickedNode(d)) { _this4.highlightEdge(d3.event.target, d.startNodeIdFormatted, d.endNodeIdFormatted); } }).on("mouseout", function (d) { var _this4$clickedLink2; // Check if this is not the clicked link if (d3.event.target !== ((_this4$clickedLink2 = _this4.clickedLink) === null || _this4$clickedLink2 === void 0 ? void 0 : _this4$clickedLink2.element) && !_this4.isLinkConnectedToClickedNode(d)) { _this4.dehighlightEdge(d3.event.target, d.startNodeIdFormatted, d.endNodeIdFormatted); } }).on("click", function (d) { // remove highlighting from all nodes and edges _this4.removeHighlightingFromKeyNodesEdges(); var link = d3.event.target; // Set the clicked link as the selected one _this4.clickedLink = { element: link, data: d }; // Highlight the edge and connected nodes _this4.highlightEdge(link, d.startNodeIdFormatted, d.endNodeIdFormatted); _this4.highlightEdgeLinkedNodes(d.startNodeIdFormatted, d.endNodeIdFormatted); // Clear any previously clicked node _this4.clickedNode = null; // Show tooltip if (_this4.config.tooltip.isVisible) _this4.showTooltip(link, d, false); }); } }, { key: "appendNodesToBaseGroup", value: function appendNodesToBaseGroup() { return this.baseGroup.append("g").attr("class", "nodes").selectAll("g").data(this.filteredNodes).enter().append("g").attr("class", function (d, i) { return "base-node-group base-node-group-".concat(d.nodeIdentierFormatted, " base-node-group-").concat(i); }); } }, { key: "appendCircleToBaseNodeGroup", value: function appendCircleToBaseNodeGroup() { var _this5 = this; return d3.selectAll(".base-node-group").append("circle").attr("r", function (d) { return d[_this5.config.nodes.nodesBeScaledByKey] ? _this5.circleRadiusScale(d[_this5.config.nodes.nodesBeScaledByKey]) : 5; }).attr("class", function (d, i) { var isRootNode = d[_this5.config.nodes.nodeIdentifierKey] === _this5.config.nodes.selectedNodeIdentifierValue; var highlightClass = _this5.shouldHighlightNode(d, _this5.config.nodes.nodesBeHighlightedByKey, _this5.config.nodes.selectedNodeHighlightByKeyValue) ? "node--highlighted-".concat(_this5.config.nodes.nodesBeHighlightedByKey) : ""; return "nodeContentElement node node-".concat(i, " ").concat(isRootNode ? "node--root" : "", " ").concat(highlightClass); }) // add mouse interaction event handlers for node circles .on("mouseover", function (d) { var _this5$clickedNode; var isRootNode = d[_this5.config.nodes.nodeIdentifierKey] === _this5.config.nodes.selectedNodeIdentifierValue; _this5.toggleHoveredNodeBorder(true, isRootNode); if (((_this5$clickedNode = _this5.clickedNode) === null || _this5$clickedNode === void 0 || (_this5$clickedNode = _this5$clickedNode.data) === null || _this5$clickedNode === void 0 ? void 0 : _this5$clickedNode.nodeIdentierFormatted) !== d.nodeIdentierFormatted) _this5.toggleHoveredNodeLabel(d, true); }).on("mouseout", function (d) { var _this5$clickedNode2; var isRootNode = d[_this5.config.nodes.nodeIdentifierKey] === _this5.config.nodes.selectedNodeIdentifierValue; _this5.toggleHoveredNodeBorder(false, isRootNode); if (((_this5$clickedNode2 = _this5.clickedNode) === null || _this5$clickedNode2 === void 0 || (_this5$clickedNode2 = _this5$clickedNode2.data) === null || _this5$clickedNode2 === void 0 ? void 0 : _this5$clickedNode2.nodeIdentierFormatted) !== d.nodeIdentierFormatted) _this5.toggleHoveredNodeLabel(d, false); }).on("click", function (d) { // remove highlighting from all nodes and edges (for the node which was previously clicked) _this5.removeHighlightingFromKeyNodesEdges(); // add highlighting for clicked node and its connecting edges & nodes _this5.clickedNode = { element: d3.event.target, data: d }; _this5.highlightClickedNode(_this5.clickedNode.element); _this5.highlightClickedNodesEdges(_this5.clickedNode.element); if (_this5.config.tooltip.isVisible) { _this5.hideTooltip(); // hide old tooltip _this5.showTooltip(_this5.clickedNode.element, d); // show tooltip with updated content } }); } }, { key: "appendLabelToBaseNodeGroup", value: function appendLabelToBaseNodeGroup() { var _this6 = this; return d3.selectAll(".base-node-group").append("text").attr("class", function (d, i) { return "d-none nodeContentElement nodeLabels node-label-".concat(i, " node-label-").concat(d.nodeIdentierFormatted); }).attr("x", function (d) { return d[_this6.config.nodes.nodesBeScaledByKey] ? _this6.circleRadiusScale(d[_this6.config.nodes.nodesBeScaledByKey]) + 2.5 : 10; }).attr("y", function (d) { return d[_this6.config.nodes.nodesBeScaledByKey] ? _this6.circleRadiusScale(d[_this6.config.nodes.nodesBeScaledByKey]) + 2.5 : 10; }).style("font-size", "14px").style("alignment-baseline", "central").text(function (d) { return d.name; }); } }, { key: "shouldHighlightNode", value: function shouldHighlightNode(nodeData, nodesHighlightedByKey, selectedNodeHighlightByKeyValue) { if (!nodeData[nodesHighlightedByKey]) return false; return Array.isArray(nodeData[nodesHighlightedByKey]) && selectedNodeHighlightByKeyValue.length && nodeData[nodesHighlightedByKey].some(function (highlightedNodeKeyValue) { return selectedNodeHighlightByKeyValue.includes(highlightedNodeKeyValue); }); } }, { key: "getSvgContainerElement", value: function getSvgContainerElement() { var _document$getElements; var svgPanel = (_document$getElements = document.getElementsByClassName("".concat(this.config.svgBasePanelClassName))) === null || _document$getElements === void 0 ? void 0 : _document$getElements[0]; var svgContainer = svgPanel.parentElement; return svgContainer; } }, { key: "getTooltipElement", value: function getTooltipElement() { return document.querySelector(".".concat(this.config.tooltip.tooltipContainerClass)); } }, { key: "resetChartStyling", value: function resetChartStyling() { d3.selectAll("*").classed("semi-transparent", false); } }, { key: "removeTooltipFrame", value: function removeTooltipFrame() { if (this.config.tooltip.isVisible) { var tooltipFrame = this.getTooltipElement(); tooltipFrame === null || tooltipFrame === void 0 || tooltipFrame.remove(); } } }, { key: "resizeNodes", value: function resizeNodes(resizeNodesByKey) { var _this7 = this; var maximumDataRange = this.findMaximumValueFromList(this.nodes, resizeNodesByKey); this.updateCircleRadiusRangeDefinition(maximumDataRange); d3.selectAll(".base-node-group").select("circle").attr("r", function (d) { return d[resizeNodesByKey] ? _this7.circleRadiusScale(d[resizeNodesByKey]) : 5; }); d3.selectAll(".base-node-group").select("text").attr("x", function (d) { return d[resizeNodesByKey] ? _this7.circleRadiusScale(d[resizeNodesByKey]) + 2.5 : 10; }).attr("y", function (d) { return d[resizeNodesByKey] ? _this7.circleRadiusScale(d[resizeNodesByKey]) + 2.5 : 10; }); } }, { key: "highlightNodes", value: function highlightNodes(nodesHighlightedByKey, selectedNodeHighlightByKeyValue) { var _this8 = this; d3.selectAll(".base-node-group").select("circle").attr("class", function (d, i, nodes) { var currentClasses = Array.from(nodes[i].classList); var highlightClass = nodesHighlightedByKey ? "node--highlighted-".concat(nodesHighlightedByKey) : ""; if (!nodesHighlightedByKey) { var classToRemove = "node--highlighted-".concat(_this8.currentNodesHighlightedByKey); if (currentClasses.includes(classToRemove)) { nodes[i].classList.remove(classToRemove); } currentClasses = Array.from(nodes[i].classList); } else _this8.currentNodesHighlightedByKey = nodesHighlightedByKey; var shouldHighlight = nodesHighlightedByKey && _this8.shouldHighlightNode(d, nodesHighlightedByKey, selectedNodeHighlightByKeyValue); return [].concat(_toConsumableArray(currentClasses), [shouldHighlight ? highlightClass : null]).filter(Boolean).join(" "); }); } }, { key: "constructTooltip", value: function constructTooltip() { var _this9 = this; var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var useNodeTemplate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var customTemplate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; var _data$name = data.name, name = _data$name === void 0 ? "" : _data$name, _data$articles = data.articles, articles = _data$articles === void 0 ? 0 : _data$articles, _data$weight = data.weight, weight = _data$weight === void 0 ? 0 : _data$weight; var isRootNode = data ? data[this.config.nodes.nodeIdentifierKey] === this.config.nodes.selectedNodeIdentifierValue : false; var _ref2 = customTemplate || {}, headerTemplate = _ref2.header, footerTemplate = _ref2.footer; var header = { showHeader: true, headerTemplate: headerTemplate, title: useNodeTemplate ? name : "".concat(weight, " publication").concat(weight > 1 ? "s" : ""), subtitle: useNodeTemplate ? "".concat(articles, " publications") : "co-authored" }; var footer = { showFooter: useNodeTemplate && !isRootNode, footerTemplate: footerTemplate, btnText: "View Profile", btnClickHandler: function btnClickHandler() { _this9.removeTooltipFrame(); _this9.onTooltipFooterBtnClickCallback(data); } }; return (0, _tooltips.getTooltipTemplate)(this.config.tooltip.tooltipContainerClass, header, footer); } }, { key: "showTooltip", value: function showTooltip(clickedElement, datum) { var useNodeTemplate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; this.updateTooltipContent(datum, useNodeTemplate); var tooltipFrame = this.getTooltipElement(); // Tooltip placement logic var nodeRect = clickedElement.getBoundingClientRect(); var svgPanel = this.svgBasePanel.node().getBoundingClientRect(); // Assuming `this.svgPanelElement` is a reference to the SVG panel // Calculate the position relative to the SVG panel var top = nodeRect.top - svgPanel.top - tooltipFrame.offsetHeight / 2; var left = nodeRect.left - svgPanel.left + nodeRect.width + 10; // Position to the right of the node // Check and adjust for right boundary of SVG panel if (left + tooltipFrame.offsetWidth > svgPanel.width) { left = nodeRect.left - svgPanel.left - tooltipFrame.offsetWidth - 10; // Position to the left of the node } // Check and adjust for top/bottom boundaries of SVG panel if (top < 0) { top = 0; // Align to top of SVG panel if too high } else if (top + tooltipFrame.offsetHeight > svgPanel.height) { top = svgPanel.height - tooltipFrame.offsetHeight; // Align to bottom of SVG panel if too low } tooltipFrame.style.top = "".concat(top + 200, "px"); // Positioned 200px with respect to the top of the node tooltipFrame.style.left = "".concat(left + 30, "px"); // Positioned 30px to the right of the node } }, { key: "hideTooltip", value: function hideTooltip() { var tooltipFrame = this.getTooltipElement(); if (tooltipFrame) tooltipFrame.classList.add("d-none"); } }, { key: "updateTooltipContent", value: function updateTooltipContent(datum) { var useNodeTemplate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var tooltipFrame = this.getTooltipElement(); if (!tooltipFrame) { console.error("Tooltip container not found."); return; } var tooltipTemplate = this.constructTooltip(datum, useNodeTemplate); tooltipFrame.replaceWith(tooltipTemplate); } }, { key: "addSimulationToGraphLayout", value: function addSimulationToGraphLayout() { var _this10 = this; return d3.forceSimulation(this.filteredNodes).force("charge", d3.forceManyBody().strength(this.config.graphLayout.chargeStrength).distanceMax(this.config.graphLayout.chargeDistanceMax)).force("center", d3.forceCenter(1, 1)).force("x", d3.forceX(1).strength(1)).force("y", d3.forceY(1).strength(1)).force("link", d3.forceLink(this.filteredLinks).id(function (d) { return d[_this10.config.nodes.nodeIdentifierKey]; }).distance(this.config.graphLayout.linkDistance).strength(this.config.graphLayout.linkStrength)).force("collide", d3.forceCollide().radius(function (d) { return d[_this10.config.nodes.nodesBeScaledByKey] ? _this10.circleRadiusScale(d[_this10.config.nodes.nodesBeScaledByKey]) + 15 : 5; }).iterations(3)).on("tick", function () { return _this10.ticked(); }); } }, { key: "updateNodePositionAttributes", value: function updateNodePositionAttributes(node) { var _this11 = this; node.attr("transform", function (d) { return "translate(".concat(_this11.fixna(d.x), ",").concat(_this11.fixna(d.y), ")"); }); } }, { key: "fixna", value: function fixna(x) { return isFinite(x) ? x : 0; } }, { key: "ticked", value: function ticked() { this.updateNodePositionAttributes(this.nodesGroupElement); this.updateLinkPositionAttributes(this.linksGroupElement); } }, { key: "updateLinkPositionAttributes", value: function updateLinkPositionAttributes(link) { var _this12 = this; link.attr("x1", function (d) { return _this12.fixna(d.source.x); }).attr("y1", function (d) { return _this12.fixna(d.source.y); }).attr("x2", function (d) { return _this12.fixna(d.target.x); }).attr("y2", function (d) { return _this12.fixna(d.target.y); }); } }, { key: "areNodesNeighbouring", value: function areNodesNeighbouring(a, b) { return a === b || this.adjlist["".concat(a, "-").concat(b)]; } }, { key: "findMaximumValueFromList", value: function findMaximumValueFromList(list, key) { return d3.max(list, function (d) { return d[key]; }); } }, { key: "updateCircleRadiusRangeDefinition", value: function updateCircleRadiusRangeDefinition(maxDataValue) { return this.circleRadiusScale.domain([0, maxDataValue]); } }, { key: "getZoomBehaviour", value: function getZoomBehaviour(minZoomScale, maxZoomScale) { return d3.zoom().scaleExtent([minZoomScale, maxZoomScale]); } }, { key: "getDragBehaviour", value: function getDragBehaviour() { var _this13 = this; return d3.drag().on("start", function (d) { return _this13.dragStarted(d); }).on("drag", function (d) { return _this13.dragged(d); }).on("end", function (d) { return _this13.dragEnded(d); }); } }, { key: "getSvgPanelCenterTranslateValues", value: function getSvgPanelCenterTranslateValues() { var _this$getSvgBasePanel2 = this.getSvgBasePanelDimensions(), svgBasePanelWidth = _this$getSvgBasePanel2.svgBasePanelWidth, svgBasePanelHeight = _this$getSvgBasePanel2.svgBasePanelHeight; var initialOriginX = svgBasePanelWidth / 2; var initialOriginY = svgBasePanelHeight / 2; return { initialOriginX: initialOriginX, initialOriginY: initialOriginY }; } }, { key: "dragStarted", value: function dragStarted(d) { d3.event.sourceEvent.stopPropagation(); if (!d3.event.active) this.graphLayout.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } }, { key: "dragged", value: function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } }, { key: "dragEnded", value: function dragEnded(d) { if (!d3.event.active) this.graphLayout.alphaTarget(0); d.fx = null; d.fy = null; } }, { key: "zoomHandler", value: function zoomHandler() { // determine current transform ... and store in config var. var _d3$event$transform = d3.event.transform, x = _d3$event$transform.x, y = _d3$event$transform.y, k = _d3$event$transform.k; var baseGroup = d3.select(".base-group"); // Get the SVG base panel width var _this$getSvgBasePanel3 = this.getSvgBasePanelDimensions(), svgBasePanelWidth = _this$getSvgBasePanel3.svgBasePanelWidth, svgBasePanelHeight = _this$getSvgBasePanel3.svgBasePanelHeight; // Get the bounding box of the base group var baseGroupBBox = baseGroup.node().getBBox(); var baseGroupWidth = baseGroupBBox.width, baseGroupHeight = baseGroupBBox.height; // Calculate the horizontal & vertical center translation var translateX = (svgBasePanelWidth - baseGroupWidth) / 2 - baseGroupBBox.x; var translateY = (svgBasePanelHeight - baseGroupHeight) / 2 - baseGroupBBox.y; // correctly scale SVG base group during user zoom in/out d3.select(".base-group").attr("transform", "translate(".concat(x, ",").concat(y, ") scale(").concat(k, ")")); //.attr("transform", `translate(${translateX},${translateY}) scale(${k})`); // correctly scale SVG Circles during user zoom in/out d3.selectAll(".nodeContentElement").attr("transform", "scale(".concat(1 / k, ")")); // correctly scale SVG lines during user zoom in/out d3.selectAll(".link").style("stroke-width", this.config.graphLayout.strokeWidth * (1 / k)); //if user has zoomed in beyond a certain threshold point, defined in config JSON, display all chart node labels .. d3.selectAll(".nodeLabels").classed("d-none", k > this.config.zoomControls.zoomLabelDisplayThreshold ? false : true); } }, { key: "isLinkConnectedToClickedNode", value: function isLinkConnectedToClickedNode(linkData) { var _this$clickedNode; if (!((_this$clickedNode = this.clickedNode) !== null && _this$clickedNode !== void 0 && _this$clickedNode.data)) return false; // Directly access the nodeIdFormatted of clickedNode var clickedNodeId = this.clickedNode.data.nodeIdentierFormatted; return clickedNodeId === linkData.startNodeIdFormatted || clickedNodeId === linkData.endNodeIdFormatted; } /***** START : NODE HIGHLIGHTING functions ***/ }, { key: "toggleHoveredNodeBorder", value: function toggleHoveredNodeBorder(isNodeHovered, isRootNode) { var selectedNode = d3.select(d3.event.target).node(); if (!selectedNode) return; if (isRootNode) { return d3.select(selectedNode).classed("node--root-hovered", isNodeHovered); } d3.select(selectedNode).classed("node--hovered", isNodeHovered); } }, { key: "highlightConnectedNodes", value: function highlightConnectedNodes(clickedNodeIndex) { var _this14 = this; this.nodeCirclesElement.classed("node--adjacent-highlighted", function (d) { var isRootNode = d[_this14.config.nodes.nodeIdentifierKey] === _this14.config.nodes.selectedNodeIdentifierValue; return !isRootNode && _this14.areNodesNeighbouring(clickedNodeIndex, d.index) ? true : false; }); } }, { key: "highlightClickedNode", value: function highlightClickedNode(node) { d3.select(node).classed("node--highlighted", true); } }, { key: "highlightEdgeLinkedNodes", value: function highlightEdgeLinkedNodes(source, target) { d3.selectAll(".base-node-group").style("opacity", 0.1).filter(".base-node-group-".concat(source, ", .base-node-group-").concat(target)).style("opacity", 1).selectAll(".node").classed("node--adjacent-highlighted", true); } /***** END : NODE HIGHLIGHTING functions ***/ /***** START : LINK HIGHLIGHTING functions ***/ }, { key: "highlightEdge", value: function highlightEdge(hoveredLinkElmt, source, target) { // d3.selectAll(".link").style("opacity", 0.1); d3.select(hoveredLinkElmt).style("opacity", 1).style("stroke-opacity", 1).classed("link--hovered", true); this.displayEdgeLinkedNodeLabels(source, target); } }, { key: "dehighlightEdge", value: function dehighlightEdge(hoveredLinkElmt, source, target) { d3.select(hoveredLinkElmt).style("stroke-opacity", this.config.graphLayout.strokeOpacity).classed("link--hovered", false); // reset opacity for link and base-node-group d3.selectAll(".link").style("opacity", 1); d3.selectAll(".base-node-group").style("opacity", 1); // hide labels d3.selectAll(".nodeLabels").classed("d-none", true); } }, { key: "removeHighlightingFromEdge", value: function removeHighlightingFromEdge(selectedLineElmt, source, target) { var _this$clickedLink; // remove highlighting from selected link d3.select(selectedLineElmt).style("stroke-opacity", this.config.graphLayout.strokeOpacity).classed("link--hovered", false); d3.selectAll(".link").style("opacity", 1); d3.selectAll(".base-node-group").style("opacity", 1); // hide labels d3.selectAll(".nodeLabels").classed("d-none", true); // remove node highlighting d3.selectAll(".node").classed("node--highlighted", false); // Reapply highlighting for nodes connected to the clicked link if (!this.clickedNode && ((_this$clickedLink = this.clickedLink) === null || _this$clickedLink === void 0 ? void 0 : _this$clickedLink.element) === selectedLineElmt) { d3.select(".base-node-group-".concat(source)).classed("node--highlighted", true); d3.select(".base-node-group-".concat(target)).classed("node--highlighted", true); } this.resetChartStyling(); this.displayEdgeLinkedNodeLabels(null, null); } }, { key: "highlightClickedNodesEdges", value: function highlightClickedNodesEdges(selectedElement) { var _this15 = this; var index = d3.select(selectedElement).datum().index; this.toggleNodeLabelsDisplay(false); // show or hide nodes based on whether they are adjacent or connected to the clicked node this.nodesGroupElement.style("opacity", function (d) { return _this15.areNodesNeighbouring(index, d.index) ? 1 : 0.1; }); // highlight connected/neighbour nodes this.highlightConnectedNodes(index); // highlight connected edges this.highlightConnectedEdges(index); } }, { key: "highlightConnectedEdges", value: function highlightConnectedEdges(clickedNodeIndex) { this.linksGroupElement.style("opacity", function (d) { return d.source.index === clickedNodeIndex || d.target.index === clickedNodeIndex ? 1 : 0.1; }).style("stroke-opacity", function (d) { return d.source.index === clickedNodeIndex || d.target.index === clickedNodeIndex ? 1 : 0.1; }).classed("link--highlighted", function (d) { return d.source.index === clickedNodeIndex || d.target.index === clickedNodeIndex ? true : false; }); } /***** END : NODE HIGHLIGHTING functions ***/ /***** START : COMMON TO BOTH NODE AND LINK HIGHLIGHTING ****/ }, { key: "removeHighlightingFromKeyNodesEdges", value: function removeHighlightingFromKeyNodesEdges() { this.nodesGroupElement.style("opacity", 1); // remove node circle highlighting this.nodeCirclesElement.classed("node--highlighted", false); // remove node circle highlighting from adjacent nodes this.nodeCirclesElement.classed("node--adjacent-highlighted", false); // remove link highlighting and link hovered state this.linksGroupElement.style("opacity", 1).style("stroke-opacity", this.config.graphLayout.strokeOpacity).classed("link--highlighted", false).classed("link--hovered", false); // hide node labels d3.selectAll(".nodeLabels").classed("d-none", true); } /***** END : COMMON TO BOTH NODE AND LINK HIGHLIGHTING ****/ }, { key: "toggleNodeLabelsDisplay", value: function toggleNodeLabelsDisplay(displayNodeLabels) { var nodeLabels = d3.selectAll(".nodeLabels"); nodeLabels.classed("d-none", !displayNodeLabels ? false : !nodeLabels.classed("d-none")); } }, { key: "toggleHoveredNodeLabel", value: function toggleHoveredNodeLabel(d, isHovered) { d3.selectAll(".node-label-".concat(d.nodeIdentierFormatted)).classed("d-none", !isHovered); } }, { key: "displayEdgeLinkedNodeLabels", value: function displayEdgeLinkedNodeLabels(sourceNode, targetNode) { // if coming from edge interation, display labels for source and target nodes if (sourceNode) { d3.selectAll(".node-label-".concat(sourceNode)).classed("d-none", false); d3.selectAll(".node-label-".concat(targetNode)).classed("d-none", false); return; } d3.selectAll(".nodeLabels").classed("d-none", true); } }, { key: "constructLegends", value: function constructLegends() { var _this16 = this; var basePanelElement = document.querySelector(".".concat(this.config.svgBasePanelClassName)); // create legend container var legendContainerElement = document.createElement("div"); legendContainerElement.classList.add("legend-container"); // create legend header var legendHeaderElemt = document.createElement("p"); legendHeaderElemt.textContent = "legend"; legendHeaderElemt.classList.add("legend-header--uppercase"); // Create the legend body var legendBodyElement = document.createElement("div"); legendBodyElement.classList.add("legend-body"); if (this.config.legend.legendItems.length) { // Create the unordered list var ulElement = document.createElement("ul"); // Create list items and append to ul this.config.legend.legendItems.forEach(function (legend) { var liElement = document.createElement("li"); liElement.classList.add("legend-item"); var swatchElement = document.createElement("span"); _this16.applyStylesToSwatchElement(legend, swatchElement); liElement.appendChild(swatchElement); liElement.appendChild(document.createTextNode(legend.name)); ulElement.appendChild(liElement); }); // Append unordered list to legend body legendBodyElement.appendChild(ulElement); } // Append legend header and body to container legendContainerElement.appendChild(legendHeaderElemt); legendContainerElement.appendChild(legendBodyElement); if (this.config.legend.position === "top") { legendContainerElement.style.bottom = "0"; legendContainerElement.style.top = "1rem"; } this.config.legend.position === "bottom" ? basePanelElement.after(legendContainerElement) : basePanelElement.before(legendContainerElement); } }, { key: "applyStylesToSwatchElement", value: function applyStylesToSwatchElement(legend, swatchElement) { swatchElement.classList.add("swatch"); if (legend.swatchSymbol === "circle") { swatchElement.style.height = "1rem"; swatchElement.style.backgroundColor = (legend === null || legend === void 0 ? void 0 : legend.color) || ""; swatchElement.style.borderColor = legend.borderColor ? legend.borderColor : ""; swatchElement.style.borderRadius = "50%"; } if (legend.swatchSymbol === "line") { swatchElement.style.borderWidth = legend.borderWidth; swatchElement.style.borderColor = legend.borderColor; } } }]); }();