UNPKG

@springernature/nn-charts

Version:
841 lines (744 loc) 40 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var d3 = _interopRequireWildcard(require("d3")); var _config = require("./config"); var utils = _interopRequireWildcard(require("./utils")); var _mergeConfig = require("../../utils/merge-config"); 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 _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _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 ScatterBubbleChart = exports["default"] = /*#__PURE__*/function () { //names to be changed function ScatterBubbleChart(data, legendsData, element, configOptions, loadingStartedCb, loadingEndedCb, clickNode, hoverNode, hoverOutNode) { _classCallCheck(this, ScatterBubbleChart); this.cf = Object.keys(configOptions).length ? (0, _mergeConfig.mergeConfigs)(_config.cf, configOptions) : _config.cf; this.loadingStartedCallback = loadingStartedCb; this.loadingEndedCallback = loadingEndedCb; //names to be changed this.clickNodeCallback = clickNode; this.hoverNodeCallback = hoverNode; this.hoverNodeOutCallback = hoverOutNode; this.srcData = data; this.continentsLegendsData = legendsData; // Validate that we have enough colors for all continents if (this.continentsLegendsData.length > this.cf.legend.categoryLegend.legendCategoryColours.length) { alert("Error: Not enough colors defined (".concat(this.cf.legend.categoryLegend.legendCategoryColours.length, ") for all continents (").concat(this.continentsLegendsData.length, ").")); // Only use continents that have corresponding colors to maintain visual integrity this.continentsLegendsData = this.continentsLegendsData.slice(0, this.cf.legend.categoryLegend.legendCategoryColours.length); } // Create a COPY of the colors array instead of modifying the original this.legendCategoryColours = this.cf.legend.categoryLegend.legendCategoryColours.slice(0, this.continentsLegendsData.length); this.config = { isLayerSelected: false, // boolean var to trigger if a category layer is selected or not isPreviousLayerSelected: false, // boolean var to trigger if a category layer is selected or not stickyActiveLabel: null /* acitvated if user has clicked on data rectangle to freeze layer highlighting */, selectedLayer: null /* has a data layer/horizon been selected? if so, this will not be blank/null */, data: data ? data : [] }; this.element = element; } return _createClass(ScatterBubbleChart, [{ key: "init", value: function init() { var _this = this; console.log("chart initiated"); // update broswer tab title from config. //d3.selectAll(".chart__TabTitle").text(this.cf.chart__TabTitle); // set the dimensions of the graph utils.alertSize(); // get current browser window dimensions // calculate/determine container width dimension this.cf.containerWidth = d3.selectAll(".content-panel.visualisation-panel").style("width").replaceAll("px", ""); // calculate chart dimensions this.cf.chartHeight = utils.setChartHeight(this.cf); this.cf.chartWidth = utils.setChartWidth(this.cf, "margins"); this.cf.chartLegendWidth = this.cf.chartWidth / 3; // setting boolean value for .delaunayBoundaryLayer based on config.js value for .voronoiBoundaryLayer // assumes both variables CANNOT be true this.cf.delaunayBoundaryLayer = this.cf.voronoiBoundaryLayer !== true; // re-storing src data as separate data obj. this.cf.data = this.srcData; this.cf.circleClickCallback = this.clickNodeCallback; this.cf.hoverNodeCallback = this.hoverNodeCallback; this.cf.hoverNodeOutCallback = this.hoverNodeOutCallback; // call method to sort data in correct order (descending) // add "ABS" to use the absolute value to determine data order (good for bubbles that show negative and positive values together) this.cf.sortedData = utils.sortData(this.cf.data, "ABSdescending", this.cf.circleRadiusAttributeField // TEST SORTING on "orgFieldCitationRatio" ); // format indentifier field for each nodes var _iterator = _createForOfIteratorHelper(this.cf.sortedData), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var node = _step.value; node.nodeIdentierFormatted = node[this.cf.dataObjectIdentifier].replaceAll(".", "_"); } // cycle through data, adding additonal attribute information // Needed for prototype. Likely not needed for 'Live' } catch (err) { _iterator.e(err); } finally { _iterator.f(); } this.cf.sortedData.forEach(function (d, i) { d.entityIdentifier = "id".concat(i); // create a proxy unique ID d.value = d[_this.cf.dataObjectIdentifier]; d.id = d[_this.cf.dataObjectIdentifier]; // create a second proxy unique ID d.text = d.institutionName; // store top-most N of sorted data into seperate array to plot (N=50; set in config). d.selected = i < _this.cf.displayTopNEntities; }); // sort filteredData to build hierarchy chart in alphabetical order // (may need to be changed based on agreed logic for displaying person names) var alphabeticallySortedEntityData = this.cf.data.sort(function (a, b) { return utils.compareStrings(a[_this.cf.entitySelectionListSortOrder], b[_this.cf.entitySelectionListSortOrder]); }); // // OUT OF SCOPE FOR v1.0 // // Initialize the Multi Select for Continent Selection // Proxy multi selection list needed only for prototype // NOT NEEDED IN PRODUCTIONISED SOLUTION. REPLACED WITH REACT EQUIVALENT /* new MultiSelect("#dynamicContinentList", { data: this.cf.proxyContinentLU, placeholder: "Continents", search: false, selectAll: false, listAll: false, onSelect: function (value, text, element) { const index = this.cf.selectedContinents.indexOf(value); if (index == -1) { this.cf.selectedContinents.push(value); } }, onUnselect: function (value, text, element) { const index = this.cf.selectedContinents.indexOf(value); if (index != -1) { this.cf.selectedContinents.splice(index, 1); } }, }); */ // set eventlistener for clicking 'submit selection button // Proxy needed only for prototype. NOT NEEDED IN PRODUCTIONIED VERSION d3.selectAll(".submitSelections").on("click", function () { utils.submit_selections(_this.cf.selectedContinents, _this.cf.selectedCountries, _this.cf, _this.cf.baseGroupForDataElements, xBottom, yLeft, createdColourPalette, _this.cf.dataPointBoundaries, _this.cf.fieldTitles, _this.cf.data); }); // // OUT OF SCOPE FOR v1.0 // // Initialize the Multi Select for Entity Selection // Proxy multi selection list needed only for prototype // NOT NEEDED IN PRODUCTIONISED SOLUTION. REPLACED WITH REACT EQUIVALENT // // MAY NEED TO GRAB SOME OF CODE LOGIC IMPLEMENTED VIA 'onSelect' option // // new MultiSelect("#dynamicEntityList", { // data: alphabeticallySortedEntityData, // placeholder: "Select entities to compare", // search: false, // selectAll: false, // listAll: false, // max: this.cf.maxNSelectedEntities, // Maximum number of items that can be selected // onSelect: function (value, text, element) { // // // console.log("dynamicEntityList onSelect:", value, text, element); // // // // appended selected entity "D" datum object onto array of selected entities. // this.cf.selectedEntityData.push( // this.cf.data.filter(function (d, i) { // return d[this.cf.dataObjectIdentifier] == value; // })[0] // ); // // append selected entity data circles for updated array of selected entities. // const appendedDataCircles = appendDataCircles( // cf, // this.cf.baseGroupForDataElements.classNames.join("."), // this.cf.selectedEntityData, // xBottom, // yLeft, // createdColourPalette // ); // // append selected entity data objects. // const appendedDataLabels = appendDataLabels( // cf, // svg, // this.cf.selectedEntityData, // xBottom, // yLeft, // createdColourPalette // /* this.cf.currentTransform */ // ); // // delete current bounding box definition for 'selected entities' befor redefining new bounding attributes from new data. // this.cf.dataPointBoundaries = deleteJSONArrayElement( // this.cf.dataPointBoundaries, // "type", // "selectedEntities" // ); // // get new boundary attribution for new set of 'selected entities' // const newDataPointBoundaries = getDataPointBoundaries( // this.cf.dataPointBoundaries, // this.cf.selectedEntityData, // this.cf.fieldTitles.x, // this.cf.fieldTitles.y, // "selectedEntities" // ); // // update and dsiplay new bounding rects // const appendedDataBoundingRects = appendDataBoundingRects( // this.cf.dataPointBoundaries, // this.cf.baseGroupForDataElements.classNames.join("."), // xBottom, // yLeft // ); // // update delaunary for new entity selection set // // remove old, and append new dealauany interaction layer // const updatedDelaunayLayer = addDelaunayInteractionLayer( // cf, // xBottom, // yLeft, // this.cf.selectedEntityData, // svg // ); // // call and update chart zoom translation (needed to prevent chart panning from previous state) // svg.call(zoom.transform, this.cf.currentTransform); // }, // onUnselect: function (value, text, element) { // // // console.log("dynamicEntityList onUnselect:", value, text, element); // // // // delete entity data datum from array of selected entities based on value deselected // this.cf.selectedEntityData = deleteJSONArrayElement( // this.cf.selectedEntityData, // this.cf.dataObjectIdentifier, // value // ); // // remove all entity node data circles and node data labels from chart view. // d3.selectAll(".nodeContent.node__circle.node").remove(); // d3.selectAll(".nodeContentElement.nodeLabels").remove(); // // append selected entity data objects. // const appendedDataCircles = appendDataCircles( // cf, // this.cf.baseGroupForDataElements.classNames.join("."), // this.cf.selectedEntityData, // xBottom, // yLeft, // createdColourPalette // ); // // append selected entity data objects. // const appendedDataLabels = appendDataLabels( // cf, // svg, // this.cf.selectedEntityData, // xBottom, // yLeft, // createdColourPalette // ); // // delete current bounding box definition for 'selected entities' befor redefining new bounding attributes from new data. // this.cf.dataPointBoundaries = deleteJSONArrayElement( // this.cf.dataPointBoundaries, // "type", // "selectedEntities" // ); // // get new boundary attribution for new set of 'selected entities' // getDataPointBoundaries( // this.cf.dataPointBoundaries, // this.cf.selectedEntityData, // this.cf.fieldTitles.x, // this.cf.fieldTitles.y, // "selectedEntities" // ); // // update and dsiplay new bounding rects // const appendedDataBoundingRects = appendDataBoundingRects( // this.cf.dataPointBoundaries, // this.cf.baseGroupForDataElements.classNames.join("."), // xBottom, // yLeft // ); // // update delaunary for new entity selection set // const updatedDelaunayLayer = addDelaunayInteractionLayer( // cf, // xBottom, // yLeft, // this.cf.selectedEntityData, // svg // ); // // call and update chart zoom translation (needed to prevent chart panning from previous state) // svg.call(zoom.transform, this.cf.currentTransform); // }, // }); // // OUT OF SCOPE FOR v1.0 // // populate proxy selection list to allow user to switch axes set used on chart /* d3.select("#axisSwitchList") .selectAll(".axisSwitchList-option") .data(this.cf.axisSwitchSets) .enter() .append("option") .attr("class", "axisSwitchList-option") .attr("data-value", function (d, i) { return d.value; }) .attr("value", function (d, i) { return d.value; }) .text(function (d, i) { return d.text; }); */ // add eventlistener for axis switch selection list // Proxy selection list needed only for prototype // NOT NEEDED IN PRODUCTIONISED SOLUTION. REPLACED WITH REACT EQUIVALENT // // MAY NEED TO GRAB SOME OF CODE LOGIC IMPLEMENTED VIA 'onSelect' option // // // OUT OF SCOPE FOR v1.0 // // document // .getElementById("axisSwitchList") // .addEventListener("change", function () { // let value = this.value; // this.cf.quadrantLabels.axisSet = value; // // update field defintions to change attribute definitions to use of chart zxes // this.cf.fieldTitles.x = this.cf.axisSwitchSets[value].x; // this.cf.fieldTitles.y = this.cf.axisSwitchSets[value].y; // // delete current bounding box definition for 'all entities' befor redefining new bounding attributes from new data. // this.cf.dataPointBoundaries = deleteJSONArrayElement( // this.cf.dataPointBoundaries, // "type", // "allEntities" // ); // // this needs to be replace with JSON array passed through API // // Akarsh said he could/would provide it wjen productionized // getDataPointBoundaries( // this.cf.dataPointBoundaries, // this.cf.data, // this.cf.fieldTitles.x, // this.cf.fieldTitles.y, // "allEntities" // ); // // delete current bounding box definition for 'selected entities' befor redefining new bounding attributes from new data. // this.cf.dataPointBoundaries = deleteJSONArrayElement( // this.cf.dataPointBoundaries, // "type", // "selectedEntities" // ); // // this needs to be replace with JSON array passed through API // // Akarsh said he could/would provide it wjen productionized // getDataPointBoundaries( // this.cf.dataPointBoundaries, // this.cf.selectedEntityData, // this.cf.fieldTitles.x, // this.cf.fieldTitles.y, // "selectedEntities" // ); // // call method to transitio chart to new state // // update position/dimensions/state of chart axis domains, chart titles, entity data circles, bounding rectangles // const transitionedChart = transitionChart( // this.cf.fieldTitles, // xBottom, // yLeft, // this.cf.data, // this.cf.chartTransitionEaseMethod, // this.cf.chartTransitionDuration, // this.cf.axis, // this.cf.chartType, // this.cf.axisTitlesLU // ); // }); // end change selection // // OUT OF SCOPE FOR v1.0 // // populate proxy selection list to allow user to change attribute for scaling node radius by /* d3.select("#nodeScalingAttributeList") .selectAll(".nodeScalingAttributeList-option") .data(this.cf.nodeScalingAttributes) .enter() .append("option") .attr("class", "nodeScalingAttributeList-option") .attr("data-value", function (d, i) { return d.value; }) .attr("value", function (d, i) { return d.value; }) .text(function (d, i) { return d.text; }); */ // // OUT OF SCOPE FOR v1.0 // // add eventlistener for scaling node radius selection list /* document .getElementById("nodeScalingAttributeList") .addEventListener("change", function () { let value = this.value; // update stored global value for scaling attributw this.cf.circleRadiusAttributeField = this.cf.nodeScalingAttributes[value].r; // determine maximum value of required scaling attribute (defined in config.js) based on selected data field this.cf.maxScalingValue = determineMaximumValueOfRequiredScalingAttribute(this.cf); // create circle radius scaling power law this.cf.CircleRadiusScale = createCircleRadiusScalingLaw(this.cf); // transition radius only of entity data circles (x- y- coordinates do not change) d3.selectAll(".nodeContent.node__circle.node") .transition() .duration(this.cf.chartTransitionDuration) .ease(this.cf.chartTransitionEaseMethod) .attr("r", function (d, i) { return this.cf.CircleRadiusScale(d[this.cf.circleRadiusAttributeField]); }); // transition all data circle node labels d3.selectAll(".nodeContentElement.nodeLabels") .transition() .duration(this.cf.chartTransitionDuration) .ease(this.cf.chartTransitionEaseMethod) .attr("x", function (d, i) { d.plottedX = xBottom(d[this.cf.fieldTitles.x]); let nodeRadius = this.cf.CircleRadiusScale(d[this.cf.circleRadiusAttributeField]); return ( this.cf.currentTransform.x + xBottom(d[this.cf.fieldTitles.x]) * this.cf.currentTransform.k + nodeRadius ); }) .attr("y", function (d) { d.plottedY = yLeft(d[this.cf.fieldTitles.y]); let nodeRadius = this.cf.CircleRadiusScale(d[this.cf.circleRadiusAttributeField]); return ( this.cf.currentTransform.y + yLeft(d[this.cf.fieldTitles.y]) * this.cf.currentTransform.k - nodeRadius ); }); return; }); */ // // // // // NOW BUILD CHART // // // // // this needs to be replace with JSON array passed through API // Akarsh said he could/would provide it wjen productionized utils.getDataPointBoundaries(this.cf.dataPointBoundaries, this.cf.data, this.cf.fieldTitles.x, this.cf.fieldTitles.y, "allEntities"); // populate array with default N top entities. this.cf.selectedEntityData = utils.getDefaultEntityData(this.cf.sortedData, this.cf.displayTopNEntities); // this needs to be replace with JSON array passed through API //Akarsh said he could/would provide it wjen productionized utils.getDataPointBoundaries(this.cf.dataPointBoundaries, this.cf.selectedEntityData, this.cf.fieldTitles.x, this.cf.fieldTitles.y, "selectedEntities"); // determine data fields from ingested data this.cf.inputFieldsFromData = utils.getDataFieldColumns(this.srcData); // call method to determine maximum value of required scaling attribute (defined in config.js) based on selected data field this.cf.maxScalingValue = utils.determineMaximumValueOfRequiredScalingAttribute(this.cf); // call method to create circle radius scaling power law this.cf.CircleRadiusScale = utils.createCircleRadiusScalingLaw(this.cf); // call method to create JSON of categoriesed data to allow group styling this.cf.createdCategorisedData = utils.createCategorisedData(this.cf.sortedData, this.cf.CategoryField); // generate chart data specific colour palette var createdColourPalette = utils.createColourPalette(this.continentsLegendsData, this.legendCategoryColours); // // OUT OF SCOPE FOR v1.0 // // add eventlistener for user selection to draw own zoom area using brush selection and appending // Needs migration over if fucntinmality is productionized // document.querySelector("#brush").addEventListener("change", function () { // // // // get current check state of button // this.cf.defineOwnZoomAreaCheckBoxState = this.checked; // // // // modfiy text on button // d3.selectAll(".defineYourOwnZoomArea").text(function () { // return this.cf.defineOwnZoomAreaCheckBoxState == true // ? "Allow Chart Pan/Zoom" // : "Define Your Own Zoom Area"; // }); // // call different method based on if check state is true or false. // this.cf.defineOwnZoomAreaCheckBoxState == true // ? start_brush_tool() /* Checkbox is checked */ // : end_brush_tool(); /* Checkbox is not checked */ // // call and update chart zoom translation (needed to prevent chart panning from previous state) // svg.call(zoom.transform, this.cf.currentTransform); // }); // add event listerner to ESC key to provide means to user to escape out of defining their own zoom area. document.body.addEventListener("keydown", function (e) { // if (e.key === "Escape") { svg.selectAll("g.brush").remove(); _this.cf.defineOwnZoomAreaCheckBoxState = false; d3.selectAll(".defineYourOwnZoomArea").text("Define Your Own Zoom Area"); document.querySelector("#brush").checked = false; } }); // Set the zoom and Pan features: // how much you can zoom, on which part, // and what to do when there is a zoom var zoom = d3.zoom().scaleExtent([1, 10]) // Infinite Pan // .translateExtent([ // [-Infinity, -Infinity], // [Infinity, Infinity], // ]) // // limited Pan Left and Down .translateExtent([[-this.cf.translateExtentBuffer, Number.NEGATIVE_INFINITY], [this.cf.containerWidth + this.cf.translateExtentBuffer, this.cf.containerHeight + this.cf.translateExtentBuffer / 2]]); // // OUT OF SCOPE FOR v1.0 // // .on("zoom", function () { // zoomhandler(); // }); // append main SVG panel to base container DIV for chart var svg = d3.selectAll(".content-panel.visualisation-panel").append("svg").attr("class", this.cf.svg__chart__container).attr("id", "svg__chart__container").attr("role", "presentation").attr("transform", "translate(".concat(this.cf.svg__chart__container_position.x, ",").concat(this.cf.svg__chart__container_position.y, ")")).attr("width", this.cf.containerWidth - this.cf.chartMargin.left - this.cf.chartMargin.right).attr("height", this.cf.chartHeight); // append seconday SVG panel to base container DIV for chart legend elements var legendsvg = d3.selectAll(".content-panel.visualisation-legend-panel").append("div").attr("class", "base-legend-panel svg__chart__legend__container row").attr("id", "svg__chart__legend__container"); // determine y-axis minimum and maximum values from data // https://stackoverflow.com/questions/11488194/how-to-use-d3-min-and-d3-max-within-a-d3-json-command var YAxisMin = utils.getMinimumDataValueFromKey(this.cf.data, this.cf.fieldTitles.y); var YAxisMax = utils.getMaximumDataValueFromKey(this.cf.data, this.cf.fieldTitles.y); // determine x-axis minimum and maximum values from data // https://stackoverflow.com/questions/11488194/how-to-use-d3-min-and-d3-max-within-a-d3-json-command var XAxisMin = utils.getMinimumDataValueFromKey(this.cf.data, this.cf.fieldTitles.x); var XAxisMax = utils.getMaximumDataValueFromKey(this.cf.data, this.cf.fieldTitles.x); // define domain and range of y axis var yLeft = utils.createLinearYAxis([0, Math.ceil(YAxisMax / this.cf.axis.y.axisRounding) * this.cf.axis.y.axisRounding], [this.cf.chartHeight - this.cf.chartMargin.bottom - this.cf.chartMargin.top, this.cf.chartMargin.top]); // define domain and range of y axis // define new linear x-axis var xBottom = utils.createLinearXAxis([Math.floor(XAxisMin / this.cf.axis.x.axisRounding) * this.cf.axis.x.axisRounding, Math.ceil(XAxisMax / this.cf.axis.x.axisRounding) * this.cf.axis.x.axisRounding], [this.cf.chartMargin.left, this.cf.chartWidth - this.cf.chartMargin.right]); // position new y-axis var yAxis = utils.positionAxis(yLeft, this.cf.axis.y, this.cf.chartType); // position new x-axis var xAxis = utils.positionAxis(xBottom, this.cf.axis.x, this.cf.chartType); // append new y-axis var gY = utils.appendAxis(yAxis, this.cf.axis.y, this.cf.svg__chart__container, 0 /* y */, this.cf.chartMargin.left /* x */); // append new x-axis var gX = utils.appendAxis(xAxis, this.cf.axis.x, this.cf.svg__chart__container, this.cf.chartHeight - this.cf.chartMargin.bottom - this.cf.chartMargin.top /* y */, 0 /* x */); // store axis domains as seperate variables to allow access elsewhere in code. var x0 = xBottom.domain(); this.cf.currentxAxisDomain = x0; var y0 = yLeft.domain(); this.cf.currentyAxisDomain = y0; // modify axis tick classname declarations too allow appending of chart width grid lines var yAxisTickClasses = utils.modifyAxisTickClasses(this.cf.axis, "y", this.cf.axis.y.axisTickFrequency, this.cf.chartType); // modify axis tick classname declarations too allow appending of chart height grid lines var xAxisTickClasses = utils.modifyAxisTickClasses(this.cf.axis, "x", this.cf.axis.x.axisTickFrequency, this.cf.chartType); // append vertical gridlines to x axis var appendedXAxisGridLines = utils.appendXAxisGridLines(-(d3.max(yLeft.range()) - d3.min(yLeft.range())), this.cf.axis.x.gridLines, this.cf.axis.x.classNames); // // append y-axis title var appendedYAxisTitle = utils.appendAxisTitle(this.cf.svg__chart__container, d3.max(yLeft.range()), 40, this.cf.axis, "y", this.cf.axisTitlesLU, this.cf.fieldTitles.y, "Left"); // // append x-axis title var appendedXAxisTitle = utils.appendAxisTitle(this.cf.axis.x.classNames.join("."), d3.max(xBottom.range()), 50, this.cf.axis, "x", this.cf.axisTitlesLU, this.cf.fieldTitles.x, ""); // append full chart-width gridlines to y-axis. var appendedYAxisGridLines = utils.appendYAxisGridLines(xBottom.range()[1] - xBottom.range()[0], this.cf.axis.y.gridLines, this.cf.axis.y.classNames); // append new dealauany interaction layer var updatedDelaunayLayer = utils.addDelaunayInteractionLayer(this.cf, xBottom, yLeft, this.cf.selectedEntityData, svg); // append new g element to attach all chart space content to (rects, data circles, reference lines) var group__chart__content = svg.append("g").attr("class", "group__chart__content").attr("clip-path", "url(#clip)"); // Add a clipPath: everything out of this area won't be drawn. var clip = svg var clip = svg.append("defs").append("SVG:clipPath").attr("id", "clip").append("SVG:rect").attr("id", "clipRect").attr("x", this.cf.chartMargin.left).attr("y", this.cf.chartMargin.top).attr("width", d3.max(xBottom.range()) - this.cf.chartMargin.left).attr("height", d3.max(yLeft.range()) - d3.min(yLeft.range())); // append passive frame rectangle to chart space to help delineate chart space from surrounding var appendedChartFrame = svg.append("rect").attr("class", "svg__rect__chart__frame").attr("x", this.cf.chartMargin.left).attr("y", this.cf.chartMargin.top).attr("width", d3.max(xBottom.range()) - this.cf.chartMargin.left).attr("height", d3.max(yLeft.range()) - d3.min(yLeft.range())); // append proxy thick black line to x and y axis at x/y=0 // Note this has to be positioning at where x-axis scale is x/y=0, // MUST BE ADDED AFTER ANY DATA OBJECTS THAT TRANSGRESS THE X/Y=0 lines // SO IT DOES NOT GET ASKED BY DATA OBJECTS var appendedXAxisThickLine = utils.appendAxisThickLine(this.cf.baseGroupForDataElements.classNames.join("."), yLeft, "x", xBottom); var appendedYAxisThickLine = utils.appendAxisThickLine(this.cf.baseGroupForDataElements.classNames.join("."), yLeft, "y", xBottom); // determine max data value for y-axis attr. var maxYAxis = utils.getMaximumDataValueFromKey(this.cf.selectedEntityData, "orgTotalCitations"); // determine max data value for x-axis attr. var maxXAxis = utils.getMaximumDataValueFromKey(this.cf.selectedEntityData, "articlesCount"); // store locally current D3/zoom transform (i.e. 0,0,1) this.cf.currentTransform = d3.zoomIdentity; // // OUT OF SCOPE FOR v1.0 // // append bounding rects /* const appendedDataBoundingRects = appendDataBoundingRects( this.cf.dataPointBoundaries, this.cf.baseGroupForDataElements.classNames.join("."), xBottom, yLeft ); */ // // OUT OF SCOPE FOR v1.0 // // append Y axis reference line ... /* const appendedYAxisReferenceLines = appendAxisReferenceLines( this.cf.dataPointBoundaries, this.cf.baseGroupForDataElements.classNames.join("."), xBottom, yLeft, "y", this.cf.axis.y.referenceLines ); */ // // OUT OF SCOPE FOR v1.0 // // append X axis reference line ... /* const appendedXAxisReferenceLines = appendAxisReferenceLines( this.cf.dataPointBoundaries, this.cf.baseGroupForDataElements.classNames.join("."), xBottom, yLeft, "x", this.cf.axis.x.referenceLines ); */ // // OUT OF SCOPE FOR v1.0 // // append quadrant description labels to chart. /* const appendedQuadrantLabels = appendQuadrantLabels( this.cf.baseGroupForDataElements.classNames.join("."), this.cf.quadrantLabels.labels[this.cf.quadrantLabels.axisSet], xBottom, yLeft, this.cf.dataPointBoundaries.filter(function (d, i) { return d.type == "allEntities"; })[0], this.cf.quadrantLabels.verticalOffset ); */ // append selected entity data objects. var appendedDataCircles = utils.appendDataCircles(this.cf, svg, this.cf.selectedEntityData, xBottom, yLeft, createdColourPalette, this.cf.currentTransform); // append selected entity data objects. var appendedDataLabels = utils.appendDataLabels(this.cf, svg, this.cf.selectedEntityData, xBottom, yLeft, createdColourPalette, this.cf.currentTransform); // store full axis defintions as seperate variables to allow access elsewhere in code. // append zoom defintion to base SVG var SVG = svg.call(zoom); // append mock zoom button // nonly needed for prototype var appendedMockZoomControlButtons = utils.appendMockZoomControlButtons("base-legend-panel", this.cf.chartMargin.left, Number(this.cf.legend.gapToLegends.y), this.cf.chartMargin); if (this.cf.legend.categoryLegend.hasLegend) { var appendedCategoryLegend = utils.appendCategoryLegend( // base SVG panel "base-legend-panel", // text label info { text: this.cf.legend.categoryLegend.text.text, x: this.cf.legend.categoryLegend.text.x, y: this.cf.legend.categoryLegend.text.y, categoryVerticalSpacing: this.cf.legend.categoryLegend.text.categoryVerticalSpacing }, // category information createdColourPalette); } if (this.cf.legend.scaleLegend.hasLegend) { // new for dynamic scale legend // const stepPercentage = 1 / Number(this.cf.legend.scaleLegend.numberLegendSteps); // // for (let i = 0; i < this.cf.legend.scaleLegend.numberLegendSteps; i++) { // // const roundedValue = Number( // (this.cf.maxScalingValue * (1 - i * stepPercentage)).toFixed( // this.cf.legend.scaleLegend.numDecimalPlaces // ) // ); // let roundedUpValue = null; // const roundingLookup = { // 1: 5, // 2: 10, // 3: 25, // 4: 100, // }; // const len = roundedValue.toString().length; // if (roundingLookup[len]) { // roundedUpValue = Math.ceil(roundedValue / roundingLookup[len]) * roundingLookup[len]; // } // // this.cf.legend.scaleLegend.scaleLegendSizes.push(Number(roundedUpValue)); // } // storing only the unique legend sizes by removing duplicate values // this.cf.legend.scaleLegend.scaleLegendSizes = [...new Set(this.cf.legend.scaleLegend.scaleLegendSizes)] this.cf.legend.scaleLegend.scaleLegendSizes = utils.generateConsistentLegendSizes(this.cf.maxScalingValue, this.cf.legend.scaleLegend.numberLegendSteps); console.log('got legend sizes => ', this.cf.legend.scaleLegend.scaleLegendSizes); // var appendedScaleLegend = utils.appendScaleLegendHTML( // base SVG panel "base-legend-panel", // group element positioning // { // x: Number( // this.cf.chartMargin.left + // 3 * this.cf.legend.gapToLegends.x + // // utils.getClientBoundingRectForElements("zoom-controls-group").width + // // utils.getClientBoundingRectForElements("legend-label-group").width + // utils.getClientBoundingRectForElements("category-legend-group").width // ), // y: Number(this.cf.legend.gapToLegends.y), // }, // background rect info // { // paddingLeft: this.cf.legend.scaleLegend.background.paddingLeft, // paddingRight: this.cf.legend.scaleLegend.background.paddingRight, // paddingTop: this.cf.legend.scaleLegend.background.paddingTop, // paddingBottom: this.cf.legend.scaleLegend.background.paddingBottom, // }, { // sizes: this.cf.legend.scaleLegend.scaleLegendSizes, radiusScale: this.cf.CircleRadiusScale, lineLength: this.cf.legend.scaleLegend.lineLength, label: this.cf.legend.scaleLegend.label }); } // add event listenres for display/hide boundaing rectangles. d3.selectAll(".display_bounding_rects").on("click", function () { this.value = this.value === "true" ? "false" : "true"; var btnValue = this.value; d3.selectAll(".framing__context_element").style("visibility", function () { return btnValue === "true" ? "visible" : "hidden"; }); d3.select(this).text(function () { // biome-ignore lint/suspicious/noDoubleEquals: <explanation> return this.value == "true" ? "Hide Bounding Rectangles" : "Show Bounding Rectangles"; }); }); // // window resize event listener this.cf.window = window.addEventListener("resize", function () { utils.scatterBubbleOnWindowResize(_this.cf, xBottom, yLeft, _this.cf.selectedEntityData, gX, gY, svg); }); // // zoom-in event listener // d3.selectAll(".zoom-in").on("click", function () { // zoom.scaleBy(svg.transition().duration(50), 1.25); // }); // // // // zoom-out event listener // d3.selectAll(".zoom-out").on("click", function () { // zoom.scaleBy(svg.transition().duration(50), 1 / 1.25); // }); // // // // zoom-reset chart event listener // d3.selectAll(".zoom-reset").on("click", function () { // svg // .transition() // .duration(this.cf.chartTransitionDuration) // .call(zoom.transform, d3.zoomIdentity); // }); } }]); }();