@springernature/nn-charts
Version:
Visualization for DAS products
841 lines (744 loc) • 40 kB
JavaScript
"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);
// });
}
}]);
}();