UNPKG

d3-funnel

Version:

A library for rendering SVG funnel charts using D3.js

1,351 lines (1,070 loc) 208 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["D3Funnel"] = factory(); else root["D3Funnel"] = factory(); })(self, function() { return /******/ (function() { // webpackBootstrap /******/ var __webpack_modules__ = ([ /* 0 */ /***/ (function(module, __unused_webpack_exports, __webpack_require__) { // Export default to provide support for non-ES6 solutions module.exports = __webpack_require__(1)["default"]; /***/ }), /* 1 */ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var d3_ease__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(99); /* harmony import */ var d3_array__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(105); /* harmony import */ var d3_scale__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(100); /* harmony import */ var d3_scale_chromatic__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(103); /* harmony import */ var d3_selection__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(96); /* harmony import */ var d3_transition__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var nanoid__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(97); /* harmony import */ var _Colorizer__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(92); /* harmony import */ var _Formatter__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(93); /* harmony import */ var _Navigator__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(94); /* harmony import */ var _Utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(95); function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _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 _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var D3Funnel = /*#__PURE__*/function () { /** * @param {string|HTMLElement} selector A selector for the container element. * * @return {void} */ function D3Funnel(selector) { _classCallCheck(this, D3Funnel); this.container = (0,d3_selection__WEBPACK_IMPORTED_MODULE_5__["default"])(selector).node(); this.colorizer = new _Colorizer__WEBPACK_IMPORTED_MODULE_1__["default"](); this.formatter = new _Formatter__WEBPACK_IMPORTED_MODULE_2__["default"](); this.navigator = new _Navigator__WEBPACK_IMPORTED_MODULE_3__["default"](); this.id = null; // Bind event handlers this.onMouseOver = this.onMouseOver.bind(this); this.onMouseOut = this.onMouseOut.bind(this); } /** * Remove the funnel and its events from the DOM. * * @return {void} */ _createClass(D3Funnel, [{ key: "destroy", value: function destroy() { var container = (0,d3_selection__WEBPACK_IMPORTED_MODULE_5__["default"])(this.container); // D3's remove method appears to be sufficient for removing the events container.selectAll('svg').remove(); // Remove other elements from container container.selectAll('*').remove(); // Remove inner text from container container.text(''); } /** * Draw the chart inside the container with the data and configuration * specified. This will remove any previous SVG elements in the container * and draw a new funnel chart on top of it. * * @param {Array} data A list of rows containing a category, a count, * and optionally a color (in hex). * @param {Object} options An optional configuration object to override * defaults. See the docs. * * @return {void} */ }, { key: "draw", value: function draw(data) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; this.destroy(); this.initialize(data, options); this.drawOntoDom(); } /** * Initialize and calculate important variables for drawing the chart. * * @param {Array} data * @param {Object} options * * @return {void} */ }, { key: "initialize", value: function initialize(data, options) { this.validateData(data); var settings = this.getSettings(options); this.id = "d3-funnel-".concat((0,nanoid__WEBPACK_IMPORTED_MODULE_6__.nanoid)()); // Set labels this.labelFormatter = this.formatter.getFormatter(settings.label.format); this.tooltipFormatter = this.formatter.getFormatter(settings.tooltip.format); // Set color scales this.colorizer.setInstanceId(this.id); this.colorizer.setLabelFill(settings.label.fill); this.colorizer.setScale(settings.block.fill.scale); // Initialize funnel chart settings this.settings = { width: settings.chart.width, height: settings.chart.height, bottomWidth: settings.chart.width * settings.chart.bottomWidth, bottomPinch: settings.chart.bottomPinch, isInverted: settings.chart.inverted, isCurved: settings.chart.curve.enabled, curveHeight: settings.chart.curve.height, curveShade: settings.chart.curve.shade, addValueOverlay: settings.block.barOverlay, animation: settings.chart.animate, totalCount: settings.chart.totalCount, fillType: settings.block.fill.type, hoverEffects: settings.block.highlight, dynamicHeight: settings.block.dynamicHeight, dynamicSlope: settings.block.dynamicSlope, minHeight: settings.block.minHeight, label: settings.label, tooltip: settings.tooltip, onBlockClick: settings.events.click.block }; this.setBlocks(data); } /** * @param {Array} data * * @return void */ }, { key: "validateData", value: function validateData(data) { if (Array.isArray(data) === false) { throw new Error('Data must be an array.'); } if (data.length === 0) { throw new Error('Data array must contain at least one element.'); } if (_typeof(data[0]) !== 'object') { throw new Error('Data array elements must be an object.'); } if (Array.isArray(data[0]) && data[0].length < 2 || Array.isArray(data[0]) === false && (data[0].label === undefined || data[0].value === undefined)) { throw new Error('Data array elements must contain a label and value.'); } } /** * @param {Object} options * * @return {Object} */ }, { key: "getSettings", value: function getSettings(options) { var containerDimensions = this.getContainerDimensions(); var defaults = this.getDefaultSettings(containerDimensions); // Prepare the configuration settings based on the defaults var settings = _Utils__WEBPACK_IMPORTED_MODULE_4__["default"].extend({}, defaults); // Override default settings with user options settings = _Utils__WEBPACK_IMPORTED_MODULE_4__["default"].extend(settings, options); // Account for any percentage-based dimensions settings.chart = _objectSpread(_objectSpread({}, settings.chart), this.castDimensions(settings, containerDimensions)); return settings; } /** * Return default settings. * * @param {Object} containerDimensions * * @return {Object} */ }, { key: "getDefaultSettings", value: function getDefaultSettings(containerDimensions) { var settings = D3Funnel.defaults; // Set the default width and height based on the container settings.chart = _objectSpread(_objectSpread({}, settings.chart), containerDimensions); return settings; } /** * Get the width/height dimensions of the container. * * @return {{width: Number, height: Number}} */ }, { key: "getContainerDimensions", value: function getContainerDimensions() { var dimensions = { width: parseFloat((0,d3_selection__WEBPACK_IMPORTED_MODULE_5__["default"])(this.container).style('width')), height: parseFloat((0,d3_selection__WEBPACK_IMPORTED_MODULE_5__["default"])(this.container).style('height')) }; // Remove container dimensions that resolve to zero ['width', 'height'].forEach(function (direction) { if (dimensions[direction] === 0) { delete dimensions[direction]; } }); return dimensions; } /** * Cast dimensions into tangible or meaningful numbers. * * @param {Object} chart * @param {Object} containerDimensions * * @return {{width: Number, height: Number}} */ }, { key: "castDimensions", value: function castDimensions(_ref, containerDimensions) { var chart = _ref.chart; var dimensions = {}; Object.keys(containerDimensions).forEach(function (direction) { var chartDimension = chart[direction]; var containerDimension = containerDimensions[direction]; if (/%$/.test(String(chartDimension))) { // Convert string into a percentage of the container dimensions[direction] = parseFloat(chartDimension) / 100 * containerDimension; } else if (chartDimension <= 0) { // If case of non-positive number, set to a usable number dimensions[direction] = D3Funnel.defaults.chart[direction]; } else { dimensions[direction] = chartDimension; } }); return dimensions; } /** * Register the raw data into a standard block format and pre-calculate * some values. * * @param {Array} data * * @return void */ }, { key: "setBlocks", value: function setBlocks(data) { var totalCount = this.getTotalCount(data); this.blocks = this.standardizeData(data, totalCount); } /** * Return the total count of all blocks. * * @param {Array} data * * @return {Number} */ }, { key: "getTotalCount", value: function getTotalCount(data) { if (this.settings.totalCount !== null) { return this.settings.totalCount || 0; } return data.reduce(function (a, b) { return a + _Utils__WEBPACK_IMPORTED_MODULE_4__["default"].getRawBlockCount(b); }, 0); } /** * Convert the raw data into a standardized format. * * @param {Array} data * @param {Number} totalCount * * @return {Array} */ }, { key: "standardizeData", value: function standardizeData(data, totalCount) { var _this = this; return data.map(function (rawBlock, index) { var block = Array.isArray(rawBlock) ? _Utils__WEBPACK_IMPORTED_MODULE_4__["default"].convertLegacyBlock(rawBlock) : rawBlock; var ratio = totalCount > 0 ? block.value / totalCount || 0 : 1 / data.length; return { index: index, ratio: ratio, value: block.value, height: _this.settings.height * ratio, fill: _this.colorizer.getBlockFill(block.backgroundColor, index, _this.settings.fillType), label: { enabled: !block.hideLabel, raw: block.label, formatted: _this.formatter.format(block, _this.labelFormatter), color: _this.colorizer.getLabelColor(block.labelColor) }, tooltip: { enabled: block.enabled, formatted: _this.formatter.format(block, _this.tooltipFormatter) } }; }); } /** * Draw the chart onto the DOM. * * @return {void} */ }, { key: "drawOntoDom", value: function drawOntoDom() { // Add the SVG this.svg = (0,d3_selection__WEBPACK_IMPORTED_MODULE_5__["default"])(this.container).append('svg').attr('id', this.id).attr('width', this.settings.width).attr('height', this.settings.height); var _this$makePaths = this.makePaths(); var _this$makePaths2 = _slicedToArray(_this$makePaths, 2); this.blockPaths = _this$makePaths2[0]; this.overlayPaths = _this$makePaths2[1]; // Define color gradients if (this.settings.fillType === 'gradient') { this.defineColorGradients(this.svg); } // Add top oval if curved if (this.settings.isCurved) { this.drawTopOval(this.svg, this.blockPaths); } // Add each block this.drawBlock(0); } /** * Create the paths to be used to define the discrete funnel blocks and * returns the results in an array. * * @return {Array, Array} */ }, { key: "makePaths", value: function makePaths() { var _this2 = this; // Calculate the important fixed positions var bottomLeftX = (this.settings.width - this.settings.bottomWidth) / 2; var centerX = this.settings.width / 2; var paths = []; var overlayPaths = []; // Calculate change in x, y direction this.dx = this.getDx(bottomLeftX); this.dy = this.getDy(); // Initialize velocity var dx = this.dx, dy = this.dy; // Initialize starting positions var prevLeftX = 0; var prevRightX = this.settings.width; var prevHeight = 0; // Start from the bottom for inverted if (this.settings.isInverted) { prevLeftX = bottomLeftX; prevRightX = this.settings.width - bottomLeftX; } // Initialize next positions var nextLeftX = 0; var nextRightX = 0; var nextHeight = 0; // Move down if there is an initial curve if (this.settings.isCurved) { prevHeight = this.settings.curveHeight / 2; } var totalHeight = this.settings.height; // This is greedy in that the block will have a guaranteed height // and the remaining is shared among the ratio, instead of being // shared according to the remaining minus the guaranteed if (this.settings.minHeight !== 0) { totalHeight = this.settings.height - this.settings.minHeight * this.blocks.length; } var slopeHeight = this.settings.height; // Correct slope height if there are blocks being pinched (and thus // requiring a sharper curve) if (this.settings.bottomPinch > 0) { this.blocks.forEach(function (block, i) { var height = totalHeight * block.ratio; // Add greedy minimum height if (_this2.settings.minHeight !== 0) { height += _this2.settings.minHeight; } // Account for any curvature if (_this2.settings.isCurved) { height += _this2.settings.curveHeight / _this2.blocks.length; } if (_this2.settings.isInverted) { if (i < _this2.settings.bottomPinch) { slopeHeight -= height; } } else if (i >= _this2.blocks.length - _this2.settings.bottomPinch) { slopeHeight -= height; } }); } // The slope will determine the x points on each block iteration // Given: slope = (y1 - y2) / (x1 - x2) // (x1, y1) = (bottomLeftX, height) // (x2, y2) = (0, 0) var slope = slopeHeight / bottomLeftX; // Create the path definition for each funnel block // Remember to loop back to the beginning point for a closed path this.blocks.forEach(function (block, i) { // Make heights proportional to block weight if (_this2.settings.dynamicHeight) { // Slice off the height proportional to this block dy = totalHeight * block.ratio; // Add greedy minimum height if (_this2.settings.minHeight !== 0) { dy += _this2.settings.minHeight; } // Account for any curvature if (_this2.settings.isCurved) { dy -= _this2.settings.curveHeight / _this2.blocks.length; } // Given: y = mx + b // Given: b = 0 (when funnel), b = this.settings.height (when pyramid) // For funnel, x_i = y_i / slope nextLeftX = (prevHeight + dy) / slope; // For pyramid, x_i = y_i - this.settings.height / -slope if (_this2.settings.isInverted) { nextLeftX = (prevHeight + dy - _this2.settings.height) / (-1 * slope); } // If bottomWidth is 0, adjust last x position (to circumvent // errors associated with rounding) if (_this2.settings.bottomWidth === 0 && i === _this2.blocks.length - 1) { // For funnel, last position is the center nextLeftX = _this2.settings.width / 2; // For pyramid, last position is the origin if (_this2.settings.isInverted) { nextLeftX = 0; } } // If bottomWidth is same as width, stop x velocity if (_this2.settings.bottomWidth === _this2.settings.width) { nextLeftX = prevLeftX; } // Prevent NaN or Infinite values (caused by zero heights) if (Number.isNaN(nextLeftX) || !Number.isFinite(nextLeftX)) { nextLeftX = 0; } // Calculate the shift necessary for both x points dx = nextLeftX - prevLeftX; if (_this2.settings.isInverted) { dx = prevLeftX - nextLeftX; } } // Make slope width proportional to change in block value if (_this2.settings.dynamicSlope && !_this2.settings.isInverted) { var nextBlockValue = _this2.blocks[i + 1] ? _this2.blocks[i + 1].value : block.value; var widthRatio = nextBlockValue / block.value; dx = (1 - widthRatio) * (centerX - prevLeftX); } // Stop velocity for pinched blocks if (_this2.settings.bottomPinch > 0) { // Check if we've reached the bottom of the pinch // If so, stop changing on x if (!_this2.settings.isInverted) { if (i >= _this2.blocks.length - _this2.settings.bottomPinch) { dx = 0; } // Pinch at the first blocks relating to the bottom pinch // Revert back to normal velocity after pinch } else { // Revert velocity back to the initial if we are using // static heights (prevents zero velocity if isInverted // and bottomPinch are non trivial and dynamicHeight is // false) if (!_this2.settings.dynamicHeight) { dx = _this2.dx; } dx = i < _this2.settings.bottomPinch ? 0 : dx; } } // Calculate the position of next block nextLeftX = prevLeftX + dx; nextRightX = prevRightX - dx; nextHeight = prevHeight + dy; _this2.blocks[i].height = dy; // Expand outward if inverted if (_this2.settings.isInverted) { nextLeftX = prevLeftX - dx; nextRightX = prevRightX + dx; } var dimensions = { centerX: centerX, prevLeftX: prevLeftX, prevRightX: prevRightX, prevHeight: prevHeight, nextLeftX: nextLeftX, nextRightX: nextRightX, nextHeight: nextHeight, curveHeight: _this2.settings.curveHeight, ratio: block.ratio }; if (_this2.settings.isCurved) { paths = [].concat(_toConsumableArray(paths), [_this2.navigator.makeCurvedPaths(dimensions)]); if (_this2.settings.addValueOverlay) { overlayPaths = [].concat(_toConsumableArray(overlayPaths), [_this2.navigator.makeCurvedPaths(dimensions, true)]); } } else { paths = [].concat(_toConsumableArray(paths), [_this2.navigator.makeStraightPaths(dimensions)]); if (_this2.settings.addValueOverlay) { overlayPaths = [].concat(_toConsumableArray(overlayPaths), [_this2.navigator.makeStraightPaths(dimensions, true)]); } } // Set the next block's previous position prevLeftX = nextLeftX; prevRightX = nextRightX; prevHeight = nextHeight; }); return [paths, overlayPaths]; } /** * @param {Number} bottomLeftX * * @return {Number} */ }, { key: "getDx", value: function getDx(bottomLeftX) { // Will be sharper if there is a pinch if (this.settings.bottomPinch > 0) { return bottomLeftX / (this.blocks.length - this.settings.bottomPinch); } return bottomLeftX / this.blocks.length; } /** * @return {Number} */ }, { key: "getDy", value: function getDy() { // Curved chart needs reserved pixels to account for curvature if (this.settings.isCurved) { return (this.settings.height - this.settings.curveHeight) / this.blocks.length; } return this.settings.height / this.blocks.length; } /** * Define the linear color gradients. * * @param {Object} svg * * @return {void} */ }, { key: "defineColorGradients", value: function defineColorGradients(svg) { var _this3 = this; var defs = svg.append('defs'); // Create a gradient for each block this.blocks.forEach(function (block, index) { var color = block.fill.raw; var shade = _this3.colorizer.shade(color, -0.2); // Create linear gradient var gradient = defs.append('linearGradient').attr('id', _this3.colorizer.getGradientId(index)); // Define the gradient stops var stops = [[0, shade], [40, color], [60, color], [100, shade]]; // Add the gradient stops stops.forEach(function (stop) { gradient.append('stop').attr('offset', "".concat(stop[0], "%")).attr('style', "stop-color: ".concat(stop[1])); }); }); } /** * Draw the top oval of a curved funnel. * * @param {Object} svg * @param {Array} blockPaths * * @return {void} */ }, { key: "drawTopOval", value: function drawTopOval(svg, blockPaths) { var centerX = this.settings.width / 2; // Create path from top-most block var paths = blockPaths[0]; var topCurve = paths[1][1] + this.settings.curveHeight / 2; var path = this.navigator.plot([['M', paths[0][0], paths[0][1]], ['Q', centerX, topCurve], [' ', paths[2][0], paths[2][1]], ['M', paths[2][0], this.settings.curveHeight / 2], ['Q', centerX, 0], [' ', paths[0][0], this.settings.curveHeight / 2]]); // Draw top oval svg.append('path').attr('fill', this.colorizer.shade(this.blocks[0].fill.raw, this.settings.curveShade)).attr('d', path); } /** * Draw the next block in the iteration. * * @param {int} index * * @return {void} */ }, { key: "drawBlock", value: function drawBlock(index) { var _this4 = this; if (index === this.blocks.length) { return; } // Create a group just for this block var group = this.svg.append('g'); var block = this.blocks[index]; // Fetch path element var path = this.getBlockPath(group, index); // Attach data to the element this.attachData(path, block); var overlayPath = null; var pathColor = block.fill.actual; if (this.settings.addValueOverlay) { overlayPath = this.getOverlayPath(group, index); this.attachData(overlayPath, block); // Add data attribute to distinguish between paths path.node().setAttribute('pathType', 'background'); overlayPath.node().setAttribute('pathType', 'foreground'); // Default path becomes background of lighter shade pathColor = this.colorizer.shade(block.fill.raw, 0.3); } // Add animation components if (this.settings.animation !== 0) { path.transition().duration(this.settings.animation).ease(d3_ease__WEBPACK_IMPORTED_MODULE_7__.linear).attr('fill', pathColor).attr('d', this.getPathDefinition(index)).on('end', function () { _this4.drawBlock(index + 1); }); } else { path.attr('fill', pathColor).attr('d', this.getPathDefinition(index)); this.drawBlock(index + 1); } // Add path overlay if (this.settings.addValueOverlay) { path.attr('stroke', this.blocks[index].fill.raw); if (this.settings.animation !== 0) { overlayPath.transition().duration(this.settings.animation).ease(d3_ease__WEBPACK_IMPORTED_MODULE_7__.linear).attr('fill', block.fill.actual).attr('d', this.getOverlayPathDefinition(index)); } else { overlayPath.attr('fill', block.fill.actual).attr('d', this.getOverlayPathDefinition(index)); } } // Add the hover events if (this.settings.hoverEffects) { [path, overlayPath].forEach(function (target) { if (!target) { return; } target.on('mouseover', _this4.onMouseOver).on('mouseout', _this4.onMouseOut); }); } // Add block click event if (this.settings.onBlockClick !== null) { [path, overlayPath].forEach(function (target) { if (!target) { return; } target.style('cursor', 'pointer').on('click', _this4.settings.onBlockClick); }); } // Add tooltips if (this.settings.tooltip.enabled) { [path, overlayPath].forEach(function (target) { if (!target) { return; } target.node().addEventListener('mouseout', function () { if (_this4.tooltip) { _this4.container.removeChild(_this4.tooltip); _this4.tooltip = null; } }); target.node().addEventListener('mousemove', function (e) { if (!_this4.tooltip) { _this4.tooltip = document.createElement('div'); _this4.tooltip.setAttribute('class', 'd3-funnel-tooltip'); _this4.container.appendChild(_this4.tooltip); } _this4.tooltip.innerText = block.tooltip.formatted; var width = _this4.tooltip.offsetWidth; var height = _this4.tooltip.offsetHeight; var rect = _this4.container.getBoundingClientRect(); var heightOffset = height + 5; var containerY = rect.y + window.scrollY; var isAbove = e.pageY - heightOffset < containerY; var top = isAbove ? e.pageY + 5 : e.pageY - heightOffset; var styles = ['display: inline-block', 'position: absolute', "left: ".concat(e.pageX - width / 2, "px"), "top: ".concat(top, "px"), "border: 1px solid ".concat(block.fill.raw), 'background: rgb(255,255,255,0.75)', 'padding: 5px 15px', 'color: #000', 'font-size: 14px', 'font-weight: bold', 'text-align: center', 'cursor: default', 'pointer-events: none']; _this4.tooltip.setAttribute('style', styles.join(';')); }); }); } if (this.settings.label.enabled && block.label.enabled) { this.addBlockLabel(group, index); } } /** * @param {Object} group * @param {int} index * * @return {Object} */ }, { key: "getBlockPath", value: function getBlockPath(group, index) { var path = group.append('path'); if (this.settings.animation !== 0) { this.addBeforeTransition(path, index, false); } return path; } /** * @param {Object} group * @param {int} index * * @return {Object} */ }, { key: "getOverlayPath", value: function getOverlayPath(group, index) { var path = group.append('path'); if (this.settings.animation !== 0) { this.addBeforeTransition(path, index, true); } return path; } /** * Set the attributes of a path element before its animation. * * @param {Object} path * @param {int} index * @param {boolean} isOverlay * * @return {void} */ }, { key: "addBeforeTransition", value: function addBeforeTransition(path, index, isOverlay) { var paths = isOverlay ? this.overlayPaths[index] : this.blockPaths[index]; var beforePath = ''; var beforeFill = ''; // Construct the top of the trapezoid and leave the other elements // hovering around to expand downward on animation if (!this.settings.isCurved) { beforePath = this.navigator.plot([['M', paths[0][0], paths[0][1]], ['L', paths[1][0], paths[1][1]], ['L', paths[1][0], paths[1][1]], ['L', paths[0][0], paths[0][1]]]); } else { beforePath = this.navigator.plot([['M', paths[0][0], paths[0][1]], ['Q', paths[1][0], paths[1][1]], [' ', paths[2][0], paths[2][1]], ['L', paths[2][0], paths[2][1]], ['M', paths[2][0], paths[2][1]], ['Q', paths[1][0], paths[1][1]], [' ', paths[0][0], paths[0][1]]]); } // Use previous fill color, if available if (this.settings.fillType === 'solid' && index > 0) { beforeFill = this.blocks[index - 1].fill.actual; // Otherwise use current background } else { beforeFill = this.blocks[index].fill.actual; } path.attr('d', beforePath).attr('fill', beforeFill); } /** * Attach data to the target element. Also attach the current node to the * data object. * * @param {Object} element * @param {Object} data * * @return {void} */ }, { key: "attachData", value: function attachData(element, data) { var nodeData = _objectSpread(_objectSpread({}, data), {}, { node: element.node() }); element.data([nodeData]); } /** * @param {int} index * * @return {string} */ }, { key: "getPathDefinition", value: function getPathDefinition(index) { var commands = []; this.blockPaths[index].forEach(function (command) { commands.push([command[2], command[0], command[1]]); }); return this.navigator.plot(commands); } /** * @param {int} index * * @return {string} */ }, { key: "getOverlayPathDefinition", value: function getOverlayPathDefinition(index) { var commands = []; this.overlayPaths[index].forEach(function (command) { commands.push([command[2], command[0], command[1]]); }); return this.navigator.plot(commands); } /** * @param {Object} event * @param {Object} data * * @return {void} */ }, { key: "onMouseOver", value: function onMouseOver(event, data) { var _this5 = this; var children = event.target.parentElement.childNodes; // Highlight all paths within one block _toConsumableArray(children).forEach(function (node) { if (node.nodeName.toLowerCase() === 'path') { var type = node.getAttribute('pathType') || ''; if (type === 'foreground') { (0,d3_selection__WEBPACK_IMPORTED_MODULE_5__["default"])(node).attr('fill', _this5.colorizer.shade(data.fill.raw, -0.5)); } else { (0,d3_selection__WEBPACK_IMPORTED_MODULE_5__["default"])(node).attr('fill', _this5.colorizer.shade(data.fill.raw, -0.2)); } } }); } /** * @param {Object} event * @param {Object} data * * @return {void} */ }, { key: "onMouseOut", value: function onMouseOut(event, data) { var _this6 = this; var children = event.target.parentElement.childNodes; // Restore original color for all paths of a block _toConsumableArray(children).forEach(function (node) { if (node.nodeName.toLowerCase() === 'path') { var type = node.getAttribute('pathType') || ''; if (type === 'background') { var backgroundColor = _this6.colorizer.shade(data.fill.raw, 0.3); (0,d3_selection__WEBPACK_IMPORTED_MODULE_5__["default"])(node).attr('fill', backgroundColor); } else { (0,d3_selection__WEBPACK_IMPORTED_MODULE_5__["default"])(node).attr('fill', data.fill.actual); } } }); } /** * @param {Object} group * @param {int} index * * @return {void} */ }, { key: "addBlockLabel", value: function addBlockLabel(group, index) { var paths = this.blockPaths[index]; var formattedLabel = this.blocks[index].label.formatted; var fill = this.blocks[index].label.color; // Center the text var x = this.settings.width / 2; var y = this.getTextY(paths); var text = group.append('text').attr('x', x).attr('y', y).attr('fill', fill).attr('font-size', this.settings.label.fontSize).attr('text-anchor', 'middle').attr('dominant-baseline', 'middle').attr('pointer-events', 'none'); // Add font-family, if exists if (this.settings.label.fontFamily !== null) { text.attr('font-family', this.settings.label.fontFamily); } this.addLabelLines(text, formattedLabel, x); } /** * Add <tspan> elements for each line of the formatted label. * * @param {Object} text * @param {String} formattedLabel * @param {Number} x * * @return {void} */ }, { key: "addLabelLines", value: function addLabelLines(text, formattedLabel, x) { var lines = formattedLabel.split('\n'); var lineHeight = 20; // dy will signify the change from the initial height y // We need to initially start the first line at the very top, factoring // in the other number of lines var initialDy = -1 * lineHeight * (lines.length - 1) / 2; lines.forEach(function (line, i) { var dy = i === 0 ? initialDy : lineHeight; text.append('tspan').attr('x', x).attr('dy', dy).text(line); }); } /** * Returns the y position of the given label's text. This is determined by * taking the mean of the bases. * * @param {Array} paths * * @return {Number} */ }, { key: "getTextY", value: function getTextY(paths) { var _this$settings = this.settings, isCurved = _this$settings.isCurved, curveHeight = _this$settings.curveHeight; if (isCurved) { return (paths[2][1] + paths[3][1]) / 2 + 1.5 * curveHeight / this.blocks.length; } return (paths[1][1] + paths[2][1]) / 2; } }]); return D3Funnel; }(); _defineProperty(D3Funnel, "defaults", { chart: { width: 350, height: 400, bottomWidth: 1 / 3, bottomPinch: 0, inverted: false, horizontal: false, animate: 0, curve: { enabled: false, height: 20, shade: -0.4 }, totalCount: null }, block: { dynamicHeight: false, dynamicSlope: false, barOverlay: false, fill: { scale: (0,d3_scale__WEBPACK_IMPORTED_MODULE_8__["default"])(d3_scale_chromatic__WEBPACK_IMPORTED_MODULE_9__["default"]).domain((0,d3_array__WEBPACK_IMPORTED_MODULE_10__["default"])(0, 10)), type: 'solid' }, minHeight: 0, highlight: false }, label: { enabled: true, fontFamily: null, fontSize: '14px', fill: '#fff', format: '{l}: {f}' }, tooltip: { enabled: false, format: '{l}: {f}' }, events: { click: { block: null } } }); /* harmony default export */ __webpack_exports__["default"] = (D3Funnel); /***/ }), /* 2 */ /***/ (function(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "transition": function() { return /* reexport safe */ _transition_index_js__WEBPACK_IMPORTED_MODULE_1__["default"]; }, /* harmony export */ "active": function() { return /* reexport safe */ _active_js__WEBPACK_IMPORTED_MODULE_2__["default"]; }, /* harmony export */ "interrupt": function() { return /* reexport safe */ _interrupt_js__WEBPACK_IMPORTED_MODULE_3__["default"]; } /* harmony export */ }); /* harmony import */ var _selection_index_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); /* harmony import */ var _transition_index_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(57); /* harmony import */ var _active_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(91); /* harmony import */ var _interrupt_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(50); /***/ }), /* 3 */ /***/ (function(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var d3_selection__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); /* harmony import */ var _interrupt_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(49); /* harmony import */ var _transition_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55); d3_selection__WEBPACK_IMPORTED_MODULE_0__["default"].prototype.interrupt = _interrupt_js__WEBPACK_IMPORTED_MODULE_1__["default"]; d3_selection__WEBPACK_IMPORTED_MODULE_0__["default"].prototype.transition = _transition_js__WEBPACK_IMPORTED_MODULE_2__["default"]; /***/ }), /* 4 */ /***/ (function(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "root": function() { return /* binding */ root; }, /* harmony export */ "Selection": function() { return /* binding */ Selection; } /* harmony export */ }); /* harmony import */ var _select_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5); /* harmony import */ var _selectAll_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7); /* harmony import */ var _selectChild_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10); /* harmony import */ var _selectChildren_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(12); /* harmony import */ var _filter_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(13); /* harmony import */ var _data_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(14); /* harmony import */ var _enter_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(15); /* harmony import */ var _exit_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(18); /* harmony import */ var _join_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(19); /* harmony import */ var _merge_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(20); /* harmony import */ var _order_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(21); /* harmony import */ var _sort_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(22); /* harmony import */ var _call_js__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(23); /* harmony import */ var _nodes_js__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(24); /* harmony import */ var _node_js__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(25); /* harmony import */ var _size_js__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(26); /* harmony import */ var _empty_js__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(27); /* harmony import */ var _each_js__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(28); /* harmony import */ var _attr_js__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(29); /* harmony import */ var _style_js__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(32); /* harmony import */ var _property_js__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(34); /* harmony import */ var _classed_js__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(35); /* harmony import */ var _text_js__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(36); /* harmony import */ var _html_js__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(37); /* harmony import */ var _raise_js__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(38); /* harmony import */ var _lower_js__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(39); /* harmony import */ var _append_js__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(40); /* harmony import */ var _insert_js__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(42); /* harmony import */ var _remove_js__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(43); /* harmony import */ var _clone_js__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(44); /* harmony import */ var _datum_js__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(45); /* harmony import */ var _on_js__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(46); /* harmony import */ var _dispatch_js__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(47); /* harmony import */ var _iterator_js__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(48); var root = [null]; function Selection(groups, parents) { this._groups = groups; this._parents = parents; } function selection() { return new Selection([[document.documentElement]], root); } function selection_selection() { return this; } Selection.prototype = selection.prototype = { constructor: Selection, select: _select_js__WEBPACK_IMPORTED_MODULE_0__["default"], selectAll: _selectAll_js__WEBPACK_IMPORTED_MODULE_1__["default"], selectChild: _selectChild_js__WEBPACK_IMPORTED_MODULE_2__["default"], selectChildren: _selectChildren_js__WEBPACK_IMPORTED_MODULE_3__["default"], filter: _filter_js__WEBPACK_IMPORTED_MODULE_4__["default"], data: _data_js__WEBPACK_IMPORTED_MODULE_5__["default"], enter: _enter_js__WEBPACK_IMPORTED_MODULE_6__["default"], exit: _exit_js__WEBPACK_IMPORTED_MODULE_7__["default"], join: _join_js__WEBPACK_IMPORTED_MODULE_8__["default"], merge: _merge_js__WEBPACK_IMPORTED_MODULE_9__["default"], selection: selection_selection, order: _order_js__WEBPACK_IMPORTED_MODULE_10__["default"], sort: _sort_js__WEBPACK_IMPORTED_MODULE_11__["default"], call: _call_js__WEBPACK_IMPORTED_MODULE_12__["default"], nodes: _nodes_js__WEBPACK_IMPORTED_MODULE_13__["default"], node: _node_js__WEBPACK_IMPORTED_MODULE_14__["default"], size: _size_js__WEBPACK_IMPORTED_MODULE_15__["default"], empty: _empty_js__WEBPACK_IMPORTED_MODULE_16__["default"], each: _each_js__WEBPACK_IMPORTED_MODULE_17__["default"], attr: _attr_js__WEBPACK_IMPORTED_MODULE_18__["default"], style: _style_js__WEBPACK_IMPORTED_MODULE_19__["default"], property: _property_js__WEBPACK_IMPORTED_MODULE_20__["default"], classed: _classed_js__WEBPACK_IMPORTED_MODULE_21__["default"], text: _text_js__WEBPACK_IMPORTED_MODULE_22__["default"], html: _html_js__WEBPACK_IMPORTED_MODULE_23__["default"], raise: _raise_js__WEBPACK_IMPORTED_MODULE_24__["default"], lower: _lower_js__WEBPACK_IMPORTED_MODULE_25__["default"], append: _append_js__WEBPACK_IMPORTED_MODULE_26__["default"], insert: _insert_js__WEBPACK_IMPORTED_MODULE_27__["default"], remove: _remove_js__WEBPACK_IMPORTED_MODULE_28__["default"], clone: _clone_js__WEBPACK_IMPORTED_MODULE_29__["default"], datum: _datum_js__WEBPACK_IMPORTED_MODULE_30__["default"], on: _on_js__WEBPACK_IMPORTED_MODULE_31__["default"], dispatch: _dispatch_js__WEBPACK_IMPORTED_MODULE_32__["default"], [Symbol.iterator]: _iterator_js__WEBPACK_IMPORTED_MODULE_33__["default"] }; /* harmony default export */ __webpack_exports__["default"] = (selection); /***/ }), /* 5 */ /***/ (function(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": function() { return /* export default binding */ __WEBPACK_DEFAULT_EXPORT__; } /* harmony export */ }); /* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); /* harmony import */ var _selector_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony default export */ function __WEBPACK_DEFAULT_EXPORT__(select) { if (typeof select !== "function") select = (0,_selector_js__WEBPACK_IMPORTED_MODULE_0__["default"])(select); for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) { for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) { if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) { if ("__data__" in node) subnode.__data__ = node.__data__; subgroup[i] = subnode; } } } return new _index_js__WEBPACK_IMPORTED_MODULE_1__.Selection(subgroups, this._parents); } /***/ }), /* 6 */ /***/ (function(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": function() { return /* export default binding */ __WEBPACK_DEFAULT_EXPORT__; } /* harmony export */ }); function none() {} /* harmony default export */ function __WEBPACK_DEFAULT_EXPORT__(selector) { return selector == null ? none : function() { return this.querySelector(selector); }; } /***/ }), /* 7 */ /***/ (function(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": function() { return /* export default binding */ __WEBPACK_DEFAULT_EXPORT__; } /* harmony export */ }); /* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var _array_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8); /* harmony import */ var _selectorAll_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9); function arrayAll(select) { return function() { return (0,_array_js__WEBPACK_IMPORTED_MODULE_0__["default"])(select.apply(this, arguments)); }; } /* harmony default export */ function __WEBPACK_DEFAULT_EXPORT__(select) { if (typeof select === "function