d3-funnel
Version:
A library for rendering SVG funnel charts using D3.js
1,351 lines (1,070 loc) • 208 kB
JavaScript
(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