Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

d3-funnel

Package Overview
Dependencies
Maintainers
1
Versions
32
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

d3-funnel - npm Package Compare versions

Comparing version 0.6.11 to 0.6.12

CHANGELOG.md

1345

dist/d3-funnel.js

@@ -0,1 +1,15 @@

(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(["d3"], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('d3'));
} else {
root.D3Funnel = factory(root.d3);
}
}(this, function(d3) {
/* global d3, LabelFormatter, Navigator, Utils */
/* exported D3Funnel */
'use strict';

@@ -7,672 +21,929 @@

(function (global, d3) {
var D3Funnel = (function () {
/* global d3 */
/* jshint bitwise: false */
'use strict';
/**
* @param {string} selector A selector for the container element.
*
* @return {void}
*/
var D3Funnel = (function () {
function D3Funnel(selector) {
_classCallCheck(this, D3Funnel);
this.selector = selector;
// Default configuration values
this.defaults = {
width: 350,
height: 400,
bottomWidth: 1 / 3,
bottomPinch: 0,
isCurved: false,
curveHeight: 20,
fillType: 'solid',
isInverted: false,
hoverEffects: false,
dynamicArea: false,
minHeight: false,
animation: false,
label: {
fontSize: '14px',
fill: '#fff',
format: '{l}: {f}'
},
onItemClick: null
};
this.labelFormatter = new LabelFormatter();
this.navigator = new Navigator();
}
/* exported LabelFormatter */
/**
* Remove the funnel and its events from the DOM.
*
* @return {void}
*/
_createClass(D3Funnel, [{
key: 'destroy',
value: function destroy() {
// D3's remove method appears to be sufficient for removing the events
d3.select(this.selector).selectAll('svg').remove();
}
/**
* @param {string} selector A selector for the container element.
* 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];
function D3Funnel(selector) {
_classCallCheck(this, D3Funnel);
this.destroy();
this.selector = selector;
this._initialize(data, options);
// Default configuration values
this.defaults = {
width: 350,
height: 400,
bottomWidth: 1 / 3,
bottomPinch: 0,
isCurved: false,
curveHeight: 20,
fillType: 'solid',
isInverted: false,
hoverEffects: false,
dynamicArea: false,
minHeight: false,
animation: false,
label: {
fontSize: '14px',
fill: '#fff'
}
};
this._draw();
}
/**
* Check if the supplied value is an array.
* Initialize and calculate important variables for drawing the chart.
*
* @param {*} value
* @param {Array} data
* @param {Object} options
*
* @return {bool}
*/
/**
* Remove the funnel and its events from the DOM.
*
* @return {void}
*/
}, {
key: '_initialize',
value: function _initialize(data, options) {
this._validateData(data);
_createClass(D3Funnel, [{
key: 'destroy',
value: function destroy() {
// D3's remove method appears to be sufficient for removing the events
d3.select(this.selector).selectAll('svg').remove();
}
this._setData(data);
/**
* 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];
var settings = this._getSettings(options);
// Remove any previous drawings
this.destroy();
// Set labels
this.label = settings.label;
this.labelFormatter.setFormat(this.label.format);
// Initialize chart options
this._initialize(data, options);
// Initialize funnel chart settings
this.width = settings.width;
this.height = settings.height;
this.bottomWidth = settings.width * settings.bottomWidth;
this.bottomPinch = settings.bottomPinch;
this.isCurved = settings.isCurved;
this.curveHeight = settings.curveHeight;
this.fillType = settings.fillType;
this.isInverted = settings.isInverted;
this.hoverEffects = settings.hoverEffects;
this.dynamicArea = settings.dynamicArea;
this.minHeight = settings.minHeight;
this.animation = settings.animation;
// Add the SVG
this.svg = d3.select(this.selector).append('svg').attr('width', this.width).attr('height', this.height);
// Calculate the bottom left x position
this.bottomLeftX = (this.width - this.bottomWidth) / 2;
this.blockPaths = this._makePaths();
// Change in x direction
this.dx = this._getDx();
// Define color gradients
if (this.fillType === 'gradient') {
this._defineColorGradients(this.svg);
}
// Change in y direction
this.dy = this._getDy();
// Add top oval if curved
if (this.isCurved) {
this._drawTopOval(this.svg, this.blockPaths);
}
// Support for events
this.onItemClick = settings.onItemClick;
}
// Add each block
this._drawBlock(0);
/**
* @param {Array} data
*
* @return void
*/
}, {
key: '_validateData',
value: function _validateData(data) {
if (Array.isArray(data) === false || data.length === 0 || Array.isArray(data[0]) === false || data[0].length < 2) {
throw new Error('Funnel data is not valid.');
}
}
/**
* Initialize and calculate important variables for drawing the chart.
*
* @param {Array} data
* @param {Object} options
*
* @return {void}
*/
}, {
key: '_initialize',
value: function _initialize(data, options) {
if (!isArray(data) || data.length === 0 || !isArray(data[0]) || data[0].length < 2) {
throw new Error('Funnel data is not valid.');
}
/**
* @param {Array} data
*
* @return void
*/
}, {
key: '_setData',
value: function _setData(data) {
this.data = data;
this.data = data;
this._setColors();
}
// Counter
var i = undefined;
/**
* Set the colors for each block.
*
* @return {void}
*/
}, {
key: '_setColors',
value: function _setColors() {
var _this = this;
// Prepare the configuration settings based on the defaults
// Set the default width and height based on the container
var settings = extend({}, this.defaults);
settings.width = parseInt(d3.select(this.selector).style('width'), 10);
settings.height = parseInt(d3.select(this.selector).style('height'), 10);
var colorScale = d3.scale.category10();
var hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
// Overwrite default settings with user options
var keys = Object.keys(options);
for (i = 0; i < keys.length; i++) {
if (keys[i] !== 'label') {
settings[keys[i]] = options[keys[i]];
}
// Add a color for for each block without one
this.data.forEach(function (block, index) {
if (block.length < 3 || !hexExpression.test(block[2])) {
_this.data[index][2] = colorScale(index);
}
});
}
// Label settings
if (options.hasOwnProperty('label')) {
(function () {
var validLabelOptions = /fontSize|fill/;
/**
* @param {Object} options
*
* @returns {Object}
*/
}, {
key: '_getSettings',
value: function _getSettings(options) {
// Prepare the configuration settings based on the defaults
// Set the default width and height based on the container
var settings = Utils.extend({}, this.defaults);
settings.width = parseInt(d3.select(this.selector).style('width'), 10);
settings.height = parseInt(d3.select(this.selector).style('height'), 10);
Object.keys(options.label).forEach(function (labelOption) {
if (labelOption.match(validLabelOptions)) {
settings.label[labelOption] = options.label[labelOption];
}
});
})();
}
this.label = settings.label;
// Overwrite default settings with user options
settings = Utils.extend(settings, options);
// In the case that the width or height is not valid, set
// the width/height as its default hard-coded value
if (settings.width <= 0) {
settings.width = this.defaults.width;
}
if (settings.height <= 0) {
settings.height = this.defaults.height;
}
// In the case that the width or height is not valid, set
// the width/height as its default hard-coded value
if (settings.width <= 0) {
settings.width = this.defaults.width;
}
if (settings.height <= 0) {
settings.height = this.defaults.height;
}
// Initialize the colors for each block
var colorScale = d3.scale.category10();
for (i = 0; i < this.data.length; i++) {
var hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
return settings;
}
// If a color is not set for the record, add one
if (!('2' in this.data[i]) || !hexExpression.test(this.data[i][2])) {
this.data[i][2] = colorScale(i);
}
}
/**
* @return {Number}
*/
}, {
key: '_getDx',
value: function _getDx() {
// Will be sharper if there is a pinch
if (this.bottomPinch > 0) {
return this.bottomLeftX / (this.data.length - this.bottomPinch);
}
// Initialize funnel chart settings
this.width = settings.width;
this.height = settings.height;
this.bottomWidth = settings.width * settings.bottomWidth;
this.bottomPinch = settings.bottomPinch;
this.isCurved = settings.isCurved;
this.curveHeight = settings.curveHeight;
this.fillType = settings.fillType;
this.isInverted = settings.isInverted;
this.hoverEffects = settings.hoverEffects;
this.dynamicArea = settings.dynamicArea;
this.minHeight = settings.minHeight;
this.animation = settings.animation;
return this.bottomLeftX / this.data.length;
}
// Calculate the bottom left x position
this.bottomLeftX = (this.width - this.bottomWidth) / 2;
/**
* @return {Number}
*/
}, {
key: '_getDy',
value: function _getDy() {
// Curved chart needs reserved pixels to account for curvature
if (this.isCurved) {
return (this.height - this.curveHeight) / this.data.length;
}
// Change in x direction
// Will be sharper if there is a pinch
this.dx = this.bottomPinch > 0 ? this.bottomLeftX / (data.length - this.bottomPinch) : this.bottomLeftX / data.length;
// Change in y direction
// Curved chart needs reserved pixels to account for curvature
this.dy = this.isCurved ? (this.height - this.curveHeight) / data.length : this.height / data.length;
return this.height / this.data.length;
}
// Support for events
this.onItemClick = settings.onItemClick;
/**
* Draw the chart onto the DOM.
*
* @return {void}
*/
}, {
key: '_draw',
value: function _draw() {
// Add the SVG
this.svg = d3.select(this.selector).append('svg').attr('width', this.width).attr('height', this.height);
this.blockPaths = this._makePaths();
// Define color gradients
if (this.fillType === 'gradient') {
this._defineColorGradients(this.svg);
}
/**
* Create the paths to be used to define the discrete funnel blocks and
* returns the results in an array.
*
* @return {Array}
*/
}, {
key: '_makePaths',
value: function _makePaths() {
var paths = [];
// Add top oval if curved
if (this.isCurved) {
this._drawTopOval(this.svg, this.blockPaths);
}
// Initialize velocity
var dx = this.dx;
var dy = this.dy;
// Add each block
this._drawBlock(0);
}
// Initialize starting positions
var prevLeftX = 0;
var prevRightX = this.width;
var prevHeight = 0;
/**
* Create the paths to be used to define the discrete funnel blocks and
* returns the results in an array.
*
* @return {Array}
*/
}, {
key: '_makePaths',
value: function _makePaths() {
var _this2 = this;
// Start from the bottom for inverted
if (this.isInverted) {
prevLeftX = this.bottomLeftX;
prevRightX = this.width - this.bottomLeftX;
}
var paths = [];
// Initialize next positions
var nextLeftX = 0;
var nextRightX = 0;
var nextHeight = 0;
// Initialize velocity
var dx = this.dx;
var dy = this.dy;
var middle = this.width / 2;
// Initialize starting positions
var prevLeftX = 0;
var prevRightX = this.width;
var prevHeight = 0;
// Move down if there is an initial curve
if (this.isCurved) {
prevHeight = 10;
}
// Start from the bottom for inverted
if (this.isInverted) {
prevLeftX = this.bottomLeftX;
prevRightX = this.width - this.bottomLeftX;
}
var topBase = this.width;
var bottomBase = 0;
// Initialize next positions
var nextLeftX = 0;
var nextRightX = 0;
var nextHeight = 0;
var totalArea = this.height * (this.width + this.bottomWidth) / 2;
var slope = 2 * this.height / (this.width - this.bottomWidth);
var middle = this.width / 2;
// 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.minHeight !== false) {
var height = this.height - this.minHeight * this.data.length;
totalArea = height * (this.width + this.bottomWidth) / 2;
}
// Move down if there is an initial curve
if (this.isCurved) {
prevHeight = 10;
}
var totalCount = 0;
var count = 0;
var topBase = this.width;
var bottomBase = 0;
// Harvest total count
for (var i = 0; i < this.data.length; i++) {
totalCount += isArray(this.data[i][1]) ? this.data[i][1][0] : this.data[i][1];
}
var totalArea = this.height * (this.width + this.bottomWidth) / 2;
var slope = 2 * this.height / (this.width - this.bottomWidth);
// Create the path definition for each funnel block
// Remember to loop back to the beginning point for a closed path
for (var i = 0; i < this.data.length; i++) {
count = isArray(this.data[i][1]) ? this.data[i][1][0] : this.data[i][1];
// 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.minHeight !== false) {
totalArea = (this.height - this.minHeight * this.data.length) * (this.width + this.bottomWidth) / 2;
}
// Calculate dynamic shapes based on area
if (this.dynamicArea) {
var ratio = count / totalCount;
var area = ratio * totalArea;
var totalCount = 0;
var count = 0;
if (this.minHeight !== false) {
area += this.minHeight * (this.width + this.bottomWidth) / 2;
}
// Harvest total count
this.data.forEach(function (block) {
totalCount += Array.isArray(block[1]) ? block[1][0] : block[1];
});
bottomBase = Math.sqrt((slope * topBase * topBase - 4 * area) / slope);
dx = topBase / 2 - bottomBase / 2;
dy = area * 2 / (topBase + bottomBase);
// Create the path definition for each funnel block
// Remember to loop back to the beginning point for a closed path
this.data.forEach(function (block, i) {
count = Array.isArray(block[1]) ? block[0] : block[1];
if (this.isCurved) {
dy = dy - this.curveHeight / this.data.length;
}
// Calculate dynamic shapes based on area
if (_this2.dynamicArea) {
var ratio = count / totalCount;
var area = ratio * totalArea;
topBase = bottomBase;
if (_this2.minHeight !== false) {
area += _this2.minHeight * (_this2.width + _this2.bottomWidth) / 2;
}
// Stop velocity for pinched blocks
if (this.bottomPinch > 0) {
// Check if we've reached the bottom of the pinch
// If so, stop changing on x
if (!this.isInverted) {
if (i >= this.data.length - this.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 area's (prevents zero velocity if isInverted
// and bottomPinch are non trivial and dynamicArea is
// false)
if (!this.dynamicArea) {
dx = this.dx;
}
bottomBase = Math.sqrt((slope * topBase * topBase - 4 * area) / slope);
dx = i < this.bottomPinch ? 0 : dx;
}
// Prevent bottm points from becomming NaN
if (_this2.bottomWidth === 0 && i === _this2.data.length - 1) {
bottomBase = 0;
}
// Calculate the position of next block
nextLeftX = prevLeftX + dx;
nextRightX = prevRightX - dx;
nextHeight = prevHeight + dy;
// Prevent NaN slope
if (_this2.bottomWidth === _this2.width) {
bottomBase = topBase;
}
// Expand outward if inverted
if (this.isInverted) {
nextLeftX = prevLeftX - dx;
nextRightX = prevRightX + dx;
dx = topBase / 2 - bottomBase / 2;
dy = area * 2 / (topBase + bottomBase);
if (_this2.isCurved) {
dy = dy - _this2.curveHeight / _this2.data.length;
}
// Plot curved lines
if (this.isCurved) {
paths.push([
// Top Bezier curve
[prevLeftX, prevHeight, 'M'], [middle, prevHeight + (this.curveHeight - 10), 'Q'], [prevRightX, prevHeight, ''],
// Right line
[nextRightX, nextHeight, 'L'],
// Bottom Bezier curve
[nextRightX, nextHeight, 'M'], [middle, nextHeight + this.curveHeight, 'Q'], [nextLeftX, nextHeight, ''],
// Left line
[prevLeftX, prevHeight, 'L']]);
// Plot straight lines
topBase = bottomBase;
}
// Stop velocity for pinched blocks
if (_this2.bottomPinch > 0) {
// Check if we've reached the bottom of the pinch
// If so, stop changing on x
if (!_this2.isInverted) {
if (i >= _this2.data.length - _this2.bottomPinch) {
dx = 0;
}
// Pinch at the first blocks relating to the bottom pinch
// Revert back to normal velocity after pinch
} else {
paths.push([
// Start position
[prevLeftX, prevHeight, 'M'],
// Move to right
[prevRightX, prevHeight, 'L'],
// Move down
[nextRightX, nextHeight, 'L'],
// Move to left
[nextLeftX, nextHeight, 'L'],
// Wrap back to top
[prevLeftX, prevHeight, 'L']]);
// Revert velocity back to the initial if we are using
// static area's (prevents zero velocity if isInverted
// and bottomPinch are non trivial and dynamicArea is
// false)
if (!_this2.dynamicArea) {
dx = _this2.dx;
}
dx = i < _this2.bottomPinch ? 0 : dx;
}
}
// Set the next block's previous position
prevLeftX = nextLeftX;
prevRightX = nextRightX;
prevHeight = nextHeight;
// Calculate the position of next block
nextLeftX = prevLeftX + dx;
nextRightX = prevRightX - dx;
nextHeight = prevHeight + dy;
// Expand outward if inverted
if (_this2.isInverted) {
nextLeftX = prevLeftX - dx;
nextRightX = prevRightX + dx;
}
return paths;
}
// Plot curved lines
if (_this2.isCurved) {
paths.push([
// Top Bezier curve
[prevLeftX, prevHeight, 'M'], [middle, prevHeight + (_this2.curveHeight - 10), 'Q'], [prevRightX, prevHeight, ''],
// Right line
[nextRightX, nextHeight, 'L'],
// Bottom Bezier curve
[nextRightX, nextHeight, 'M'], [middle, nextHeight + _this2.curveHeight, 'Q'], [nextLeftX, nextHeight, ''],
// Left line
[prevLeftX, prevHeight, 'L']]);
// Plot straight lines
} else {
paths.push([
// Start position
[prevLeftX, prevHeight, 'M'],
// Move to right
[prevRightX, prevHeight, 'L'],
// Move down
[nextRightX, nextHeight, 'L'],
// Move to left
[nextLeftX, nextHeight, 'L'],
// Wrap back to top
[prevLeftX, prevHeight, 'L']]);
}
/**
* Define the linear color gradients.
*
* @param {Object} svg
*
* @return {void}
*/
}, {
key: '_defineColorGradients',
value: function _defineColorGradients(svg) {
var defs = svg.append('defs');
// Set the next block's previous position
prevLeftX = nextLeftX;
prevRightX = nextRightX;
prevHeight = nextHeight;
});
// Create a gradient for each block
for (var i = 0; i < this.data.length; i++) {
var color = this.data[i][2];
var shade = shadeColor(color, -0.25);
return paths;
}
// Create linear gradient
var gradient = defs.append('linearGradient').attr({
id: 'gradient-' + i
/**
* Define the linear color gradients.
*
* @param {Object} svg
*
* @return {void}
*/
}, {
key: '_defineColorGradients',
value: function _defineColorGradients(svg) {
var defs = svg.append('defs');
// Create a gradient for each block
this.data.forEach(function (block, index) {
var color = block[2];
var shade = Utils.shadeColor(color, -0.25);
// Create linear gradient
var gradient = defs.append('linearGradient').attr({
id: 'gradient-' + 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: stop[0] + '%',
style: 'stop-color:' + stop[1]
});
});
});
}
// Define the gradient stops
var stops = [[0, shade], [40, color], [60, color], [100, shade]];
/**
* Draw the top oval of a curved funnel.
*
* @param {Object} svg
* @param {Array} blockPaths
*
* @return {void}
*/
}, {
key: '_drawTopOval',
value: function _drawTopOval(svg, blockPaths) {
var leftX = 0;
var rightX = this.width;
var centerX = this.width / 2;
// Add the gradient stops
for (var j = 0; j < stops.length; j++) {
var _stop = stops[j];
gradient.append('stop').attr({
offset: _stop[0] + '%',
style: 'stop-color:' + _stop[1]
});
}
}
if (this.isInverted) {
leftX = this.bottomLeftX;
rightX = this.width - this.bottomLeftX;
}
/**
* Draw the top oval of a curved funnel.
*
* @param {Object} svg
* @param {Array} blockPaths
*
* @return {void}
*/
}, {
key: '_drawTopOval',
value: function _drawTopOval(svg, blockPaths) {
var leftX = 0;
var rightX = this.width;
var centerX = this.width / 2;
// Create path from top-most block
var paths = blockPaths[0];
var topCurve = paths[1][1] + this.curveHeight - 10;
if (this.isInverted) {
leftX = this.bottomLeftX;
rightX = this.width - this.bottomLeftX;
}
var path = this.navigator.plot([['M', leftX, paths[0][1]], ['Q', centerX, topCurve], [' ', rightX, paths[2][1]], ['M', rightX, 10], ['Q', centerX, 0], [' ', leftX, 10]]);
// Create path from top-most block
var paths = blockPaths[0];
var path = 'M' + leftX + ',' + paths[0][1] + ' Q' + centerX + ',' + (paths[1][1] + this.curveHeight - 10) + ' ' + rightX + ',' + paths[2][1] + ' M' + rightX + ',10' + ' Q' + centerX + ',0' + ' ' + leftX + ',10';
// Draw top oval
svg.append('path').attr('fill', Utils.shadeColor(this.data[0][2], -0.4)).attr('d', path);
}
// Draw top oval
svg.append('path').attr('fill', shadeColor(this.data[0][2], -0.4)).attr('d', path);
/**
* Draw the next block in the iteration.
*
* @param {int} index
*
* @return {void}
*/
}, {
key: '_drawBlock',
value: function _drawBlock(index) {
var _this3 = this;
if (index === this.data.length) {
return;
}
/**
* Draw the next block in the iteration.
*
* @param {int} index
*
* @return {void}
*/
}, {
key: '_drawBlock',
value: function _drawBlock(index) {
var _this = this;
// Create a group just for this block
var group = this.svg.append('g');
if (index === this.data.length) {
return;
}
// Fetch path element
var path = this._getBlockPath(group, index);
path.data(this._getBlockData(index));
// Create a group just for this block
var group = this.svg.append('g');
// Add animation components
if (this.animation !== false) {
path.transition().duration(this.animation).ease('linear').attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index)).each('end', function () {
_this3._drawBlock(index + 1);
});
} else {
path.attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index));
this._drawBlock(index + 1);
}
// Fetch path element
var path = this._getBlockPath(group, index);
path.data(this._getBlockData(index));
// Add the hover events
if (this.hoverEffects) {
path.on('mouseover', this._onMouseOver).on('mouseout', this._onMouseOut);
}
// Add animation components
if (this.animation !== false) {
path.transition().duration(this.animation).ease('linear').attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index)).each('end', function () {
_this._drawBlock(index + 1);
});
} else {
path.attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index));
this._drawBlock(index + 1);
}
// ItemClick event
if (this.onItemClick !== null) {
path.on('click', this.onItemClick);
}
// Add the hover events
if (this.hoverEffects) {
path.on('mouseover', this._onMouseOver).on('mouseout', this._onMouseOut);
}
this._addBlockLabel(group, index);
}
// ItemClick event
if (this.onItemClick) {
path.on('click', this.onItemClick);
}
/**
* @param {Object} group
* @param {int} index
*
* @return {Object}
*/
}, {
key: '_getBlockPath',
value: function _getBlockPath(group, index) {
var path = group.append('path');
this._addBlockLabel(group, index);
if (this.animation !== false) {
this._addBeforeTransition(path, index);
}
/**
* @param {Object} group
* @param {int} index
*
* @return {Object}
*/
}, {
key: '_getBlockPath',
value: function _getBlockPath(group, index) {
var path = group.append('path');
return path;
}
if (this.animation !== false) {
this._addBeforeTransition(path, index);
}
/**
* Set the attributes of a path element before its animation.
*
* @param {Object} path
* @param {int} index
*
* @return {void}
*/
}, {
key: '_addBeforeTransition',
value: function _addBeforeTransition(path, index) {
var paths = this.blockPaths[index];
return path;
var beforePath = '';
var beforeFill = '';
// Construct the top of the trapezoid and leave the other elements
// hovering around to expand downward on animation
if (!this.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]]]);
}
/**
* Set the attributes of a path element before its animation.
*
* @param {Object} path
* @param {int} index
*
* @return {void}
*/
}, {
key: '_addBeforeTransition',
value: function _addBeforeTransition(path, index) {
var paths = this.blockPaths[index];
// Use previous fill color, if available
if (this.fillType === 'solid') {
beforeFill = index > 0 ? this._getColor(index - 1) : this._getColor(index);
// Use current background if gradient (gradients do not transition)
} else {
beforeFill = this._getColor(index);
}
var beforePath = '';
var beforeFill = '';
path.attr('d', beforePath).attr('fill', beforeFill);
}
// Construct the top of the trapezoid and leave the other elements
// hovering around to expand downward on animation
if (!this.isCurved) {
beforePath = '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 = '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];
}
/**
* @param {int} index
*
* @return {Array}
*/
}, {
key: '_getBlockData',
value: function _getBlockData(index) {
var label = this.data[index][0];
var value = this.data[index][1];
// Use previous fill color, if available
if (this.fillType === 'solid') {
beforeFill = index > 0 ? this._getColor(index - 1) : this._getColor(index);
// Use current background if gradient (gradients do not transition)
} else {
beforeFill = this._getColor(index);
}
return [{
index: index,
label: label,
value: value,
formatted: this.labelFormatter.format(label, value),
baseColor: this.data[index][2],
fill: this._getColor(index)
}];
}
path.attr('d', beforePath).attr('fill', beforeFill);
/**
* Return the color for the given index.
*
* @param {int} index
*
* @return {string}
*/
}, {
key: '_getColor',
value: function _getColor(index) {
if (this.fillType === 'solid') {
return this.data[index][2];
} else {
return 'url(#gradient-' + index + ')';
}
}
/**
* @param {int} index
*
* @return {Array}
*/
}, {
key: '_getBlockData',
value: function _getBlockData(index) {
return [{
index: index,
label: this.data[index][0],
value: isArray(this.data[index][1]) ? this.data[index][1][0] : this.data[index][1],
formattedValue: isArray(this.data[index][1]) ? this.data[index][1][1] : this.data[index][1].toLocaleString(),
baseColor: this.data[index][2],
fill: this._getColor(index)
}];
}
/**
* @param {int} index
*
* @return {string}
*/
}, {
key: '_getPathDefinition',
value: function _getPathDefinition(index) {
var commands = [];
/**
* Return the color for the given index.
*
* @param {int} index
*
* @return {string}
*/
}, {
key: '_getColor',
value: function _getColor(index) {
if (this.fillType === 'solid') {
return this.data[index][2];
} else {
return 'url(#gradient-' + index + ')';
}
}
this.blockPaths[index].forEach(function (command) {
commands.push([command[2], command[0], command[1]]);
});
/**
* @param {int} index
*
* @return {string}
*/
}, {
key: '_getPathDefinition',
value: function _getPathDefinition(index) {
var pathStr = '';
var point = [];
var paths = this.blockPaths[index];
return this.navigator.plot(commands);
}
for (var j = 0; j < paths.length; j++) {
point = paths[j];
pathStr += point[2] + point[0] + ',' + point[1] + ' ';
}
/**
* @param {Object} data
*
* @return {void}
*/
}, {
key: '_onMouseOver',
value: function _onMouseOver(data) {
d3.select(this).attr('fill', Utils.shadeColor(data.baseColor, -0.2));
}
return pathStr;
}
/**
* @param {Object} data
*
* @return {void}
*/
}, {
key: '_onMouseOut',
value: function _onMouseOut(data) {
d3.select(this).attr('fill', data.fill);
}
/**
* @param {Object} data
*
* @return {void}
*/
}, {
key: '_onMouseOver',
value: function _onMouseOver(data) {
d3.select(this).attr('fill', shadeColor(data.baseColor, -0.2));
}
/**
* @param {Object} group
* @param {int} index
*
* @return {void}
*/
}, {
key: '_addBlockLabel',
value: function _addBlockLabel(group, index) {
var paths = this.blockPaths[index];
/**
* @param {Object} data
*
* @return {void}
*/
}, {
key: '_onMouseOut',
value: function _onMouseOut(data) {
d3.select(this).attr('fill', data.fill);
}
var label = this._getBlockData(index)[0].formatted;
var fill = this.data[index][3] || this.label.fill;
/**
* @param {Object} group
* @param {int} index
*
* @return {void}
*/
}, {
key: '_addBlockLabel',
value: function _addBlockLabel(group, index) {
var i = index;
var paths = this.blockPaths[index];
var blockData = this._getBlockData(index)[0];
var textStr = blockData.label + ': ' + blockData.formattedValue;
var textFill = this.data[i][3] || this.label.fill;
var x = this.width / 2; // Center the text
var y = this._getTextY(paths);
var textX = this.width / 2; // Center the text
var textY = !this.isCurved ? // Average height of bases
(paths[1][1] + paths[2][1]) / 2 : (paths[2][1] + paths[3][1]) / 2 + this.curveHeight / this.data.length;
group.append('text').text(label).attr({
'x': x,
'y': y,
'text-anchor': 'middle',
'dominant-baseline': 'middle',
'fill': fill,
'pointer-events': 'none'
}).style('font-size', this.label.fontSize);
}
group.append('text').text(textStr).attr({
'x': textX,
'y': textY,
'text-anchor': 'middle',
'dominant-baseline': 'middle',
'fill': textFill,
'pointer-events': 'none'
}).style('font-size', this.label.fontSize);
/**
* 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) {
if (this.isCurved) {
return (paths[2][1] + paths[3][1]) / 2 + this.curveHeight / this.data.length;
}
}]);
return D3Funnel;
})();
return (paths[1][1] + paths[2][1]) / 2;
}
}]);
function isArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
return D3Funnel;
})();
var LabelFormatter = (function () {
/**
* Initial the formatter.
*
* @return {void}
*/
function LabelFormatter() {
_classCallCheck(this, LabelFormatter);
this.expression = null;
}
/* exported Navigator */
/**
* Extends an object with the members of another.
* Register the format function.
*
* @param {Object} a The object to be extended.
* @param {Object} b The object to clone from.
* @param {string|function} format
*
* @return {Object}
* @return {void}
*/
function extend(a, b) {
var prop = undefined;
for (prop in b) {
if (b.hasOwnProperty(prop)) {
a[prop] = b[prop];
_createClass(LabelFormatter, [{
key: 'setFormat',
value: function setFormat(format) {
if (typeof format === 'function') {
this.formatter = format;
} else {
this.expression = format;
this.formatter = this.stringFormatter;
}
}
return a;
/**
* Format the given value according to the data point or the format.
*
* @param {string} label
* @param {number} value
*
* @return string
*/
}, {
key: 'format',
value: function format(label, value) {
// Try to use any formatted value specified through the data
// Otherwise, attempt to use the format function
if (Array.isArray(value)) {
return this.formatter(label, value[0], value[1]);
} else {
return this.formatter(label, value, null);
}
}
/**
* Format the string according to a simple expression.
*
* {l}: label
* {v}: raw value
* {f}: formatted value
*
* @param {string} label
* @param {number} value
* @param {*} fValue
*
* @return {string}
*/
}, {
key: 'stringFormatter',
value: function stringFormatter(label, value) {
var fValue = arguments.length <= 2 || arguments[2] === undefined ? null : arguments[2];
// Attempt to use supplied formatted value
// Otherwise, use the default
if (fValue === null) {
fValue = this.getDefaultFormattedValue(value);
}
return this.expression.split('{l}').join(label).split('{v}').join(value).split('{f}').join(fValue);
}
/**
* @param {number} value
*
* @return {string}
*/
}, {
key: 'getDefaultFormattedValue',
value: function getDefaultFormattedValue(value) {
return value.toLocaleString();
}
}]);
return LabelFormatter;
})();
var Navigator = (function () {
function Navigator() {
_classCallCheck(this, Navigator);
}
/* exported Utils */
/* jshint bitwise: false */
/**
* Shade a color to the given percentage.
*
* @param {string} color A hex color.
* @param {number} shade The shade adjustment. Can be positive or negative.
*
* @return {string}
* Simple utility class.
*/
function shadeColor(color, shade) {
var f = parseInt(color.slice(1), 16);
var t = shade < 0 ? 0 : 255;
var p = shade < 0 ? shade * -1 : shade;
var R = f >> 16,
G = f >> 8 & 0x00FF;
var B = f & 0x0000FF;
var converted = 0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G) * 0x100 + (Math.round((t - B) * p) + B);
_createClass(Navigator, [{
key: 'plot',
return '#' + converted.toString(16).slice(1);
/**
* Given a list of path commands, returns the compiled description.
*
* @param {Array} commands
*
* @returns {string}
*/
value: function plot(commands) {
var path = '';
commands.forEach(function (command) {
path += command[0] + command[1] + ',' + command[2] + ' ';
});
return path.replace(/ +/g, ' ').trim();
}
}]);
return Navigator;
})();
var Utils = (function () {
function Utils() {
_classCallCheck(this, Utils);
}
global.D3Funnel = D3Funnel;
})(window, d3);
_createClass(Utils, null, [{
key: 'extend',
/**
* Extends an object with the members of another.
*
* @param {Object} a The object to be extended.
* @param {Object} b The object to clone from.
*
* @return {Object}
*/
value: function extend(a, b) {
var prop = undefined;
for (prop in b) {
if (b.hasOwnProperty(prop)) {
if (typeof a[prop] === 'object' && typeof b[prop] === 'object') {
a[prop] = Utils.extend(a[prop], b[prop]);
} else {
a[prop] = b[prop];
}
}
}
return a;
}
/**
* Shade a color to the given percentage.
*
* @param {string} color A hex color.
* @param {number} shade The shade adjustment. Can be positive or negative.
*
* @return {string}
*/
}, {
key: 'shadeColor',
value: function shadeColor(color, shade) {
var hex = color.slice(1);
if (hex.length === 3) {
hex = Utils.expandHex(hex);
}
var f = parseInt(hex, 16);
var t = shade < 0 ? 0 : 255;
var p = shade < 0 ? shade * -1 : shade;
var R = f >> 16;
var G = f >> 8 & 0x00FF;
var B = f & 0x0000FF;
var converted = 0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G) * 0x100 + (Math.round((t - B) * p) + B);
return '#' + converted.toString(16).slice(1);
}
/**
* Expands a three character hex code to six characters.
*
* @param {string} hex
*
* @return {string}
*/
}, {
key: 'expandHex',
value: function expandHex(hex) {
return hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
}]);
return Utils;
})();
return D3Funnel;
}));

@@ -1,2 +0,2 @@

/*! d3-funnel - v0.6.11 | 2015 */
"use strict";function _classCallCheck(t,i){if(!(t instanceof i))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function t(t,i){for(var e=0;e<i.length;e++){var h=i[e];h.enumerable=h.enumerable||!1,h.configurable=!0,"value"in h&&(h.writable=!0),Object.defineProperty(t,h.key,h)}}return function(i,e,h){return e&&t(i.prototype,e),h&&t(i,h),i}}();!function(t,i){function e(t){return"[object Array]"===Object.prototype.toString.call(t)}function h(t,i){var e=void 0;for(e in i)i.hasOwnProperty(e)&&(t[e]=i[e]);return t}function a(t,i){var e=parseInt(t.slice(1),16),h=0>i?0:255,a=0>i?-1*i:i,s=e>>16,n=e>>8&255,o=255&e,r=16777216+65536*(Math.round((h-s)*a)+s)+256*(Math.round((h-n)*a)+n)+(Math.round((h-o)*a)+o);return"#"+r.toString(16).slice(1)}var s=function(){function t(i){_classCallCheck(this,t),this.selector=i,this.defaults={width:350,height:400,bottomWidth:1/3,bottomPinch:0,isCurved:!1,curveHeight:20,fillType:"solid",isInverted:!1,hoverEffects:!1,dynamicArea:!1,minHeight:!1,animation:!1,label:{fontSize:"14px",fill:"#fff"}}}return _createClass(t,[{key:"destroy",value:function(){i.select(this.selector).selectAll("svg").remove()}},{key:"draw",value:function(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];this.destroy(),this._initialize(t,e),this.svg=i.select(this.selector).append("svg").attr("width",this.width).attr("height",this.height),this.blockPaths=this._makePaths(),"gradient"===this.fillType&&this._defineColorGradients(this.svg),this.isCurved&&this._drawTopOval(this.svg,this.blockPaths),this._drawBlock(0)}},{key:"_initialize",value:function(t,a){if(!e(t)||0===t.length||!e(t[0])||t[0].length<2)throw new Error("Funnel data is not valid.");this.data=t;var s=void 0,n=h({},this.defaults);n.width=parseInt(i.select(this.selector).style("width"),10),n.height=parseInt(i.select(this.selector).style("height"),10);var o=Object.keys(a);for(s=0;s<o.length;s++)"label"!==o[s]&&(n[o[s]]=a[o[s]]);a.hasOwnProperty("label")&&!function(){var t=/fontSize|fill/;Object.keys(a.label).forEach(function(i){i.match(t)&&(n.label[i]=a.label[i])})}(),this.label=n.label,n.width<=0&&(n.width=this.defaults.width),n.height<=0&&(n.height=this.defaults.height);var r=i.scale.category10();for(s=0;s<this.data.length;s++){var l=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i;"2"in this.data[s]&&l.test(this.data[s][2])||(this.data[s][2]=r(s))}this.width=n.width,this.height=n.height,this.bottomWidth=n.width*n.bottomWidth,this.bottomPinch=n.bottomPinch,this.isCurved=n.isCurved,this.curveHeight=n.curveHeight,this.fillType=n.fillType,this.isInverted=n.isInverted,this.hoverEffects=n.hoverEffects,this.dynamicArea=n.dynamicArea,this.minHeight=n.minHeight,this.animation=n.animation,this.bottomLeftX=(this.width-this.bottomWidth)/2,this.dx=this.bottomPinch>0?this.bottomLeftX/(t.length-this.bottomPinch):this.bottomLeftX/t.length,this.dy=this.isCurved?(this.height-this.curveHeight)/t.length:this.height/t.length,this.onItemClick=n.onItemClick}},{key:"_makePaths",value:function(){var t=[],i=this.dx,h=this.dy,a=0,s=this.width,n=0;this.isInverted&&(a=this.bottomLeftX,s=this.width-this.bottomLeftX);var o=0,r=0,l=0,d=this.width/2;this.isCurved&&(n=10);var c=this.width,u=0,f=this.height*(this.width+this.bottomWidth)/2,v=2*this.height/(this.width-this.bottomWidth);if(this.minHeight!==!1){var g=this.height-this.minHeight*this.data.length;f=g*(this.width+this.bottomWidth)/2}for(var m=0,b=0,y=0;y<this.data.length;y++)m+=e(this.data[y][1])?this.data[y][1][0]:this.data[y][1];for(var y=0;y<this.data.length;y++){if(b=e(this.data[y][1])?this.data[y][1][0]:this.data[y][1],this.dynamicArea){var k=b/m,p=k*f;this.minHeight!==!1&&(p+=this.minHeight*(this.width+this.bottomWidth)/2),u=Math.sqrt((v*c*c-4*p)/v),i=c/2-u/2,h=2*p/(c+u),this.isCurved&&(h-=this.curveHeight/this.data.length),c=u}this.bottomPinch>0&&(this.isInverted?(this.dynamicArea||(i=this.dx),i=y<this.bottomPinch?0:i):y>=this.data.length-this.bottomPinch&&(i=0)),o=a+i,r=s-i,l=n+h,this.isInverted&&(o=a-i,r=s+i),t.push(this.isCurved?[[a,n,"M"],[d,n+(this.curveHeight-10),"Q"],[s,n,""],[r,l,"L"],[r,l,"M"],[d,l+this.curveHeight,"Q"],[o,l,""],[a,n,"L"]]:[[a,n,"M"],[s,n,"L"],[r,l,"L"],[o,l,"L"],[a,n,"L"]]),a=o,s=r,n=l}return t}},{key:"_defineColorGradients",value:function(t){for(var i=t.append("defs"),e=0;e<this.data.length;e++)for(var h=this.data[e][2],s=a(h,-.25),n=i.append("linearGradient").attr({id:"gradient-"+e}),o=[[0,s],[40,h],[60,h],[100,s]],r=0;r<o.length;r++){var l=o[r];n.append("stop").attr({offset:l[0]+"%",style:"stop-color:"+l[1]})}}},{key:"_drawTopOval",value:function(t,i){var e=0,h=this.width,s=this.width/2;this.isInverted&&(e=this.bottomLeftX,h=this.width-this.bottomLeftX);var n=i[0],o="M"+e+","+n[0][1]+" Q"+s+","+(n[1][1]+this.curveHeight-10)+" "+h+","+n[2][1]+" M"+h+",10 Q"+s+",0 "+e+",10";t.append("path").attr("fill",a(this.data[0][2],-.4)).attr("d",o)}},{key:"_drawBlock",value:function(t){var i=this;if(t!==this.data.length){var e=this.svg.append("g"),h=this._getBlockPath(e,t);h.data(this._getBlockData(t)),this.animation!==!1?h.transition().duration(this.animation).ease("linear").attr("fill",this._getColor(t)).attr("d",this._getPathDefinition(t)).each("end",function(){i._drawBlock(t+1)}):(h.attr("fill",this._getColor(t)).attr("d",this._getPathDefinition(t)),this._drawBlock(t+1)),this.hoverEffects&&h.on("mouseover",this._onMouseOver).on("mouseout",this._onMouseOut),this.onItemClick&&h.on("click",this.onItemClick),this._addBlockLabel(e,t)}}},{key:"_getBlockPath",value:function(t,i){var e=t.append("path");return this.animation!==!1&&this._addBeforeTransition(e,i),e}},{key:"_addBeforeTransition",value:function(t,i){var e=this.blockPaths[i],h="",a="";h=this.isCurved?"M"+e[0][0]+","+e[0][1]+" Q"+e[1][0]+","+e[1][1]+" "+e[2][0]+","+e[2][1]+" L"+e[2][0]+","+e[2][1]+" M"+e[2][0]+","+e[2][1]+" Q"+e[1][0]+","+e[1][1]+" "+e[0][0]+","+e[0][1]:"M"+e[0][0]+","+e[0][1]+" L"+e[1][0]+","+e[1][1]+" L"+e[1][0]+","+e[1][1]+" L"+e[0][0]+","+e[0][1],a=this._getColor("solid"===this.fillType?i>0?i-1:i:i),t.attr("d",h).attr("fill",a)}},{key:"_getBlockData",value:function(t){return[{index:t,label:this.data[t][0],value:e(this.data[t][1])?this.data[t][1][0]:this.data[t][1],formattedValue:e(this.data[t][1])?this.data[t][1][1]:this.data[t][1].toLocaleString(),baseColor:this.data[t][2],fill:this._getColor(t)}]}},{key:"_getColor",value:function(t){return"solid"===this.fillType?this.data[t][2]:"url(#gradient-"+t+")"}},{key:"_getPathDefinition",value:function(t){for(var i="",e=[],h=this.blockPaths[t],a=0;a<h.length;a++)e=h[a],i+=e[2]+e[0]+","+e[1]+" ";return i}},{key:"_onMouseOver",value:function(t){i.select(this).attr("fill",a(t.baseColor,-.2))}},{key:"_onMouseOut",value:function(t){i.select(this).attr("fill",t.fill)}},{key:"_addBlockLabel",value:function(t,i){var e=i,h=this.blockPaths[i],a=this._getBlockData(i)[0],s=a.label+": "+a.formattedValue,n=this.data[e][3]||this.label.fill,o=this.width/2,r=this.isCurved?(h[2][1]+h[3][1])/2+this.curveHeight/this.data.length:(h[1][1]+h[2][1])/2;t.append("text").text(s).attr({x:o,y:r,"text-anchor":"middle","dominant-baseline":"middle",fill:n,"pointer-events":"none"}).style("font-size",this.label.fontSize)}}]),t}();t.D3Funnel=s}(window,d3);
/*! d3-funnel - v0.6.12 | 2015 */
!function(t,i){"function"==typeof define&&define.amd?define(["d3"],i):"object"==typeof exports?module.exports=i(require("d3")):t.D3Funnel=i(t.d3)}(this,function(t){"use strict";function i(t,i){if(!(t instanceof i))throw new TypeError("Cannot call a class as a function")}var e=function(){function t(t,i){for(var e=0;e<i.length;e++){var a=i[e];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),Object.defineProperty(t,a.key,a)}}return function(i,e,a){return e&&t(i.prototype,e),a&&t(i,a),i}}(),a=function(){function a(t){i(this,a),this.selector=t,this.defaults={width:350,height:400,bottomWidth:1/3,bottomPinch:0,isCurved:!1,curveHeight:20,fillType:"solid",isInverted:!1,hoverEffects:!1,dynamicArea:!1,minHeight:!1,animation:!1,label:{fontSize:"14px",fill:"#fff",format:"{l}: {f}"},onItemClick:null},this.labelFormatter=new n,this.navigator=new h}return e(a,[{key:"destroy",value:function(){t.select(this.selector).selectAll("svg").remove()}},{key:"draw",value:function(t){var i=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];this.destroy(),this._initialize(t,i),this._draw()}},{key:"_initialize",value:function(t,i){this._validateData(t),this._setData(t);var e=this._getSettings(i);this.label=e.label,this.labelFormatter.setFormat(this.label.format),this.width=e.width,this.height=e.height,this.bottomWidth=e.width*e.bottomWidth,this.bottomPinch=e.bottomPinch,this.isCurved=e.isCurved,this.curveHeight=e.curveHeight,this.fillType=e.fillType,this.isInverted=e.isInverted,this.hoverEffects=e.hoverEffects,this.dynamicArea=e.dynamicArea,this.minHeight=e.minHeight,this.animation=e.animation,this.bottomLeftX=(this.width-this.bottomWidth)/2,this.dx=this._getDx(),this.dy=this._getDy(),this.onItemClick=e.onItemClick}},{key:"_validateData",value:function(t){if(Array.isArray(t)===!1||0===t.length||Array.isArray(t[0])===!1||t[0].length<2)throw new Error("Funnel data is not valid.")}},{key:"_setData",value:function(t){this.data=t,this._setColors()}},{key:"_setColors",value:function(){var i=this,e=t.scale.category10(),a=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i;this.data.forEach(function(t,n){(t.length<3||!a.test(t[2]))&&(i.data[n][2]=e(n))})}},{key:"_getSettings",value:function(i){var e=s.extend({},this.defaults);return e.width=parseInt(t.select(this.selector).style("width"),10),e.height=parseInt(t.select(this.selector).style("height"),10),e=s.extend(e,i),e.width<=0&&(e.width=this.defaults.width),e.height<=0&&(e.height=this.defaults.height),e}},{key:"_getDx",value:function(){return this.bottomPinch>0?this.bottomLeftX/(this.data.length-this.bottomPinch):this.bottomLeftX/this.data.length}},{key:"_getDy",value:function(){return this.isCurved?(this.height-this.curveHeight)/this.data.length:this.height/this.data.length}},{key:"_draw",value:function(){this.svg=t.select(this.selector).append("svg").attr("width",this.width).attr("height",this.height),this.blockPaths=this._makePaths(),"gradient"===this.fillType&&this._defineColorGradients(this.svg),this.isCurved&&this._drawTopOval(this.svg,this.blockPaths),this._drawBlock(0)}},{key:"_makePaths",value:function(){var t=this,i=[],e=this.dx,a=this.dy,n=0,h=this.width,s=0;this.isInverted&&(n=this.bottomLeftX,h=this.width-this.bottomLeftX);var o=0,r=0,l=0,u=this.width/2;this.isCurved&&(s=10);var d=this.width,f=0,c=this.height*(this.width+this.bottomWidth)/2,v=2*this.height/(this.width-this.bottomWidth);this.minHeight!==!1&&(c=(this.height-this.minHeight*this.data.length)*(this.width+this.bottomWidth)/2);var g=0,m=0;return this.data.forEach(function(t){g+=Array.isArray(t[1])?t[1][0]:t[1]}),this.data.forEach(function(y,p){if(m=Array.isArray(y[1])?y[0]:y[1],t.dynamicArea){var k=m/g,b=k*c;t.minHeight!==!1&&(b+=t.minHeight*(t.width+t.bottomWidth)/2),f=Math.sqrt((v*d*d-4*b)/v),0===t.bottomWidth&&p===t.data.length-1&&(f=0),t.bottomWidth===t.width&&(f=d),e=d/2-f/2,a=2*b/(d+f),t.isCurved&&(a-=t.curveHeight/t.data.length),d=f}t.bottomPinch>0&&(t.isInverted?(t.dynamicArea||(e=t.dx),e=p<t.bottomPinch?0:e):p>=t.data.length-t.bottomPinch&&(e=0)),o=n+e,r=h-e,l=s+a,t.isInverted&&(o=n-e,r=h+e),t.isCurved?i.push([[n,s,"M"],[u,s+(t.curveHeight-10),"Q"],[h,s,""],[r,l,"L"],[r,l,"M"],[u,l+t.curveHeight,"Q"],[o,l,""],[n,s,"L"]]):i.push([[n,s,"M"],[h,s,"L"],[r,l,"L"],[o,l,"L"],[n,s,"L"]]),n=o,h=r,s=l}),i}},{key:"_defineColorGradients",value:function(t){var i=t.append("defs");this.data.forEach(function(t,e){var a=t[2],n=s.shadeColor(a,-.25),h=i.append("linearGradient").attr({id:"gradient-"+e}),o=[[0,n],[40,a],[60,a],[100,n]];o.forEach(function(t){h.append("stop").attr({offset:t[0]+"%",style:"stop-color:"+t[1]})})})}},{key:"_drawTopOval",value:function(t,i){var e=0,a=this.width,n=this.width/2;this.isInverted&&(e=this.bottomLeftX,a=this.width-this.bottomLeftX);var h=i[0],o=h[1][1]+this.curveHeight-10,r=this.navigator.plot([["M",e,h[0][1]],["Q",n,o],[" ",a,h[2][1]],["M",a,10],["Q",n,0],[" ",e,10]]);t.append("path").attr("fill",s.shadeColor(this.data[0][2],-.4)).attr("d",r)}},{key:"_drawBlock",value:function(t){var i=this;if(t!==this.data.length){var e=this.svg.append("g"),a=this._getBlockPath(e,t);a.data(this._getBlockData(t)),this.animation!==!1?a.transition().duration(this.animation).ease("linear").attr("fill",this._getColor(t)).attr("d",this._getPathDefinition(t)).each("end",function(){i._drawBlock(t+1)}):(a.attr("fill",this._getColor(t)).attr("d",this._getPathDefinition(t)),this._drawBlock(t+1)),this.hoverEffects&&a.on("mouseover",this._onMouseOver).on("mouseout",this._onMouseOut),null!==this.onItemClick&&a.on("click",this.onItemClick),this._addBlockLabel(e,t)}}},{key:"_getBlockPath",value:function(t,i){var e=t.append("path");return this.animation!==!1&&this._addBeforeTransition(e,i),e}},{key:"_addBeforeTransition",value:function(t,i){var e=this.blockPaths[i],a="",n="";a=this.isCurved?this.navigator.plot([["M",e[0][0],e[0][1]],["Q",e[1][0],e[1][1]],[" ",e[2][0],e[2][1]],["L",e[2][0],e[2][1]],["M",e[2][0],e[2][1]],["Q",e[1][0],e[1][1]],[" ",e[0][0],e[0][1]]]):this.navigator.plot([["M",e[0][0],e[0][1]],["L",e[1][0],e[1][1]],["L",e[1][0],e[1][1]],["L",e[0][0],e[0][1]]]),n="solid"===this.fillType&&i>0?this._getColor(i-1):this._getColor(i),t.attr("d",a).attr("fill",n)}},{key:"_getBlockData",value:function(t){var i=this.data[t][0],e=this.data[t][1];return[{index:t,label:i,value:e,formatted:this.labelFormatter.format(i,e),baseColor:this.data[t][2],fill:this._getColor(t)}]}},{key:"_getColor",value:function(t){return"solid"===this.fillType?this.data[t][2]:"url(#gradient-"+t+")"}},{key:"_getPathDefinition",value:function(t){var i=[];return this.blockPaths[t].forEach(function(t){i.push([t[2],t[0],t[1]])}),this.navigator.plot(i)}},{key:"_onMouseOver",value:function(i){t.select(this).attr("fill",s.shadeColor(i.baseColor,-.2))}},{key:"_onMouseOut",value:function(i){t.select(this).attr("fill",i.fill)}},{key:"_addBlockLabel",value:function(t,i){var e=this.blockPaths[i],a=this._getBlockData(i)[0].formatted,n=this.data[i][3]||this.label.fill,h=this.width/2,s=this._getTextY(e);t.append("text").text(a).attr({x:h,y:s,"text-anchor":"middle","dominant-baseline":"middle",fill:n,"pointer-events":"none"}).style("font-size",this.label.fontSize)}},{key:"_getTextY",value:function(t){return this.isCurved?(t[2][1]+t[3][1])/2+this.curveHeight/this.data.length:(t[1][1]+t[2][1])/2}}]),a}(),n=function(){function t(){i(this,t),this.expression=null}return e(t,[{key:"setFormat",value:function(t){"function"==typeof t?this.formatter=t:(this.expression=t,this.formatter=this.stringFormatter)}},{key:"format",value:function(t,i){return Array.isArray(i)?this.formatter(t,i[0],i[1]):this.formatter(t,i,null)}},{key:"stringFormatter",value:function(t,i){var e=arguments.length<=2||void 0===arguments[2]?null:arguments[2];return null===e&&(e=this.getDefaultFormattedValue(i)),this.expression.split("{l}").join(t).split("{v}").join(i).split("{f}").join(e)}},{key:"getDefaultFormattedValue",value:function(t){return t.toLocaleString()}}]),t}(),h=function(){function t(){i(this,t)}return e(t,[{key:"plot",value:function(t){var i="";return t.forEach(function(t){i+=t[0]+t[1]+","+t[2]+" "}),i.replace(/ +/g," ").trim()}}]),t}(),s=function(){function t(){i(this,t)}return e(t,null,[{key:"extend",value:function(i,e){var a=void 0;for(a in e)e.hasOwnProperty(a)&&("object"==typeof i[a]&&"object"==typeof e[a]?i[a]=t.extend(i[a],e[a]):i[a]=e[a]);return i}},{key:"shadeColor",value:function(i,e){var a=i.slice(1);3===a.length&&(a=t.expandHex(a));var n=parseInt(a,16),h=0>e?0:255,s=0>e?-1*e:e,o=n>>16,r=n>>8&255,l=255&n,u=16777216+65536*(Math.round((h-o)*s)+o)+256*(Math.round((h-r)*s)+r)+(Math.round((h-l)*s)+l);return"#"+u.toString(16).slice(1)}},{key:"expandHex",value:function(t){return t[0]+t[0]+t[1]+t[1]+t[2]+t[2]}}]),t}();return a});
var gulp = require('gulp');
var umd = require('gulp-wrap-umd');
var concat = require('gulp-concat');
var jshint = require('gulp-jshint');

@@ -13,3 +15,17 @@ var jscs = require('gulp-jscs');

var src = ['./src/d3-funnel/d3-funnel.js'];
var src = [
'./src/d3-funnel/d3-funnel.js',
'./src/d3-funnel/label-formatter.js',
'./src/d3-funnel/navigator.js',
'./src/d3-funnel/utils.js',
];
var umdOptions = {
exports: 'D3Funnel',
namespace: 'D3Funnel',
deps: [{
name: 'd3',
globalName: 'd3',
paramName: 'd3',
}],
};

@@ -22,3 +38,3 @@ gulp.task('test-format', function () {

.pipe(jscs({
configPath: './.jscsrc'
configPath: './.jscsrc',
}));

@@ -29,3 +45,5 @@ });

return gulp.src(src)
.pipe(concat('d3-funnel.js'))
.pipe(babel())
.pipe(umd(umdOptions))
.pipe(gulp.dest('./compiled/'));

@@ -39,11 +57,13 @@ });

gulp.task('build', ['test-format', 'test-mocha'], function () {
gulp.task('test', ['test-format', 'test-mocha']);
gulp.task('build', ['test'], function () {
return gulp.src(['./compiled/d3-funnel.js'])
.pipe(gulp.dest('./dist/'))
.pipe(rename({
extname: '.min.js'}
))
extname: '.min.js',
}))
.pipe(uglify())
.pipe(header(banner, {
pkg: pkg
pkg: pkg,
}))

@@ -53,2 +73,6 @@ .pipe(gulp.dest('./dist/'));

gulp.task('watch', function () {
gulp.watch(src, ['build']);
});
gulp.task('default', ['build']);

@@ -0,0 +0,0 @@ The MIT License (MIT)

{
"name": "d3-funnel",
"version": "0.6.11",
"version": "0.6.12",
"description": "A library for rendering SVG funnel charts using D3.js",

@@ -15,11 +15,14 @@ "author": "Jake Zatecky",

"chai": "^3.2.0",
"chai-spies": "^0.7.0",
"gulp": "^3.9.0",
"gulp-babel": "^5.2.0",
"gulp-header": "^1.2.2",
"gulp-babel": "^5.2.1",
"gulp-concat": "^2.6.0",
"gulp-header": "^1.7.1",
"gulp-jscs": "^2.0.0",
"gulp-jshint": "^1.11.2",
"gulp-mocha-phantomjs": "^0.8.1",
"gulp-mocha-phantomjs": "^0.10.1",
"gulp-rename": "^1.2.2",
"gulp-uglify": "^1.2.0",
"mocha": "^2.2.5"
"gulp-uglify": "^1.4.1",
"gulp-wrap-umd": "^0.2.1",
"mocha": "^2.3.2"
},

@@ -26,0 +29,0 @@ "dependencies": {

# D3 Funnel
[![npm](https://img.shields.io/npm/v/d3-funnel.svg)](https://www.npmjs.com/package/d3-funnel)
[![Build Status](https://travis-ci.org/jakezatecky/d3-funnel.svg?branch=master)](https://travis-ci.org/jakezatecky/d3-funnel)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/jakezatecky/d3-funnel/master/LICENSE.txt)
[![npm](https://img.shields.io/npm/v/d3-funnel.svg?style=flat-square)](https://www.npmjs.com/package/d3-funnel)
[![Build Status](https://img.shields.io/travis/jakezatecky/d3-funnel/master.svg?style=flat-square)](https://travis-ci.org/jakezatecky/d3-funnel)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/jakezatecky/d3-funnel/master/LICENSE.txt)

@@ -34,3 +34,3 @@ **D3Funnel** is an extensible, open-source JavaScript library for rendering

['Perennials', 200],
['Roses', 50]
['Roses', 50],
];

@@ -46,20 +46,32 @@ var options = {};

| Option | Description | Type | Default |
| ---------------- | --------------------------------------------------------------------------- | -------- | ------------------- |
| `width` | The pixel width of the chart. | int | Container's width |
| `height` | The pixel height of the chart. | int | Container's height |
| `bottomWidth` | The percent of total width the bottom should be. | float | `1 / 3` |
| `bottomPinch` | How many blocks to pinch on the bottom to create a "neck". | int | `0` |
| `isCurved` | Whether the funnel is curved. | bool | `false` |
| `curveHeight` | The curvature amount (if `isCurved` is `true`). | int | `20` |
| `fillType` | Either `'solid'` or `'gradient'`. | string | `'solid'` |
| `isInverted` | Whether the funnel is inverted (like a pyramid). | bool | `false` |
| `hoverEffects` | Whether the funnel has effects on hover. | bool | `false` |
| `dynamicArea` | Whether block areas are calculated by counts (as opposed to static height). | bool | `false` |
| `minHeight` | The minimum pixel height of a block. | int/bool | `false` |
| `animation` | The load animation speed in milliseconds. | int/bool | `false` |
| `label.fontSize` | Any valid font size for the labels. | string | `'14px'` |
| `label.fill` | Any valid hex color for the label color | string | `'#fff'` |
| `onItemClick` | Event handler if one of the items is clicked. | function | `function(d, i) {}` |
| Option | Description | Type | Default |
| ---------------- | --------------------------------------------------------------------------- | -------- | ------------------ |
| `width` | The pixel width of the chart. | int | Container's width |
| `height` | The pixel height of the chart. | int | Container's height |
| `bottomWidth` | The percent of total width the bottom should be. | float | `1 / 3` |
| `bottomPinch` | How many blocks to pinch on the bottom to create a "neck". | int | `0` |
| `isCurved` | Whether the funnel is curved. | bool | `false` |
| `curveHeight` | The curvature amount (if `isCurved` is `true`). | int | `20` |
| `fillType` | Either `'solid'` or `'gradient'`. | string | `'solid'` |
| `isInverted` | Whether the funnel is inverted (like a pyramid). | bool | `false` |
| `hoverEffects` | Whether the funnel has effects on hover. | bool | `false` |
| `dynamicArea` | Whether block areas are calculated by counts (as opposed to static height). | bool | `false` |
| `minHeight` | The minimum pixel height of a block. | int/bool | `false` |
| `animation` | The load animation speed in milliseconds. | int/bool | `false` |
| `label.fontSize` | Any valid font size for the labels. | string | `'14px'` |
| `label.fill` | Any valid hex color for the label color | string | `'#fff'` |
| `label.format` | Either `function(label, value)` or a format string. See below. | mixed | `'{l}: {f}'` |
| `onItemClick` | Event handler if one of the items is clicked. | function | `null` |
### Label Format
The option `label.format` can either be a function or a string. The following
keys will be substituted by the string formatter:
| Key | Description |
| ------- | ---------------------------- |
| `'{l}'` | The block's supplied label. |
| `'{v}'` | The block's raw value. |
| `'{f}'` | The block's formatted value. |
## API

@@ -82,3 +94,3 @@

['Persimmon', 2500, '#ff634d' '#6f34fd'],
['Azure', 1500, '#007fff' '#07fff0']
['Azure', 1500, '#007fff' '#07fff0'],
// Background ---^ ^--- Label

@@ -96,3 +108,3 @@ ];

['Persimmon', [2500, 'USD 2,500']],
['Azure', [1500, 'USD 1,500']]
['Azure', [1500, 'USD 1,500']],
];

@@ -99,0 +111,0 @@ ```

@@ -1,701 +0,705 @@

((global, d3) => {
/* global d3, LabelFormatter, Navigator, Utils */
/* exported D3Funnel */
/* global d3 */
/* jshint bitwise: false */
'use strict';
class D3Funnel {
class D3Funnel
{
/**
* @param {string} selector A selector for the container element.
*
* @return {void}
*/
constructor(selector) {
this.selector = selector;
/**
* @param {string} selector A selector for the container element.
*
* @return {void}
*/
constructor(selector)
{
this.selector = selector;
// Default configuration values
this.defaults = {
width: 350,
height: 400,
bottomWidth: 1 / 3,
bottomPinch: 0,
isCurved: false,
curveHeight: 20,
fillType: 'solid',
isInverted: false,
hoverEffects: false,
dynamicArea: false,
minHeight: false,
animation: false,
label: {
fontSize: '14px',
fill: '#fff',
format: '{l}: {f}',
},
onItemClick: null,
};
// Default configuration values
this.defaults = {
width: 350,
height: 400,
bottomWidth: 1 / 3,
bottomPinch: 0,
isCurved: false,
curveHeight: 20,
fillType: 'solid',
isInverted: false,
hoverEffects: false,
dynamicArea: false,
minHeight: false,
animation: false,
label: {
fontSize: '14px',
fill: '#fff',
},
};
}
this.labelFormatter = new LabelFormatter();
/**
* Remove the funnel and its events from the DOM.
*
* @return {void}
*/
destroy()
{
// D3's remove method appears to be sufficient for removing the events
d3.select(this.selector).selectAll('svg').remove();
}
this.navigator = new Navigator();
}
/**
* 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}
*/
draw(data, options = {})
{
// Remove any previous drawings
this.destroy();
/**
* Remove the funnel and its events from the DOM.
*
* @return {void}
*/
destroy() {
// D3's remove method appears to be sufficient for removing the events
d3.select(this.selector).selectAll('svg').remove();
}
// Initialize chart options
this._initialize(data, options);
/**
* 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}
*/
draw(data, options = {}) {
this.destroy();
// Add the SVG
this.svg = d3.select(this.selector)
.append('svg')
.attr('width', this.width)
.attr('height', this.height);
this._initialize(data, options);
this.blockPaths = this._makePaths();
this._draw();
}
// Define color gradients
if (this.fillType === 'gradient') {
this._defineColorGradients(this.svg);
}
/**
* Initialize and calculate important variables for drawing the chart.
*
* @param {Array} data
* @param {Object} options
*
* @return {void}
*/
_initialize(data, options) {
this._validateData(data);
// Add top oval if curved
if (this.isCurved) {
this._drawTopOval(this.svg, this.blockPaths);
}
this._setData(data);
// Add each block
this._drawBlock(0);
}
let settings = this._getSettings(options);
/**
* Initialize and calculate important variables for drawing the chart.
*
* @param {Array} data
* @param {Object} options
*
* @return {void}
*/
_initialize(data, options)
{
if (!isArray(data) || data.length === 0 || !isArray(data[0]) || data[0].length < 2) {
throw new Error('Funnel data is not valid.');
}
// Set labels
this.label = settings.label;
this.labelFormatter.setFormat(this.label.format);
this.data = data;
// Initialize funnel chart settings
this.width = settings.width;
this.height = settings.height;
this.bottomWidth = settings.width * settings.bottomWidth;
this.bottomPinch = settings.bottomPinch;
this.isCurved = settings.isCurved;
this.curveHeight = settings.curveHeight;
this.fillType = settings.fillType;
this.isInverted = settings.isInverted;
this.hoverEffects = settings.hoverEffects;
this.dynamicArea = settings.dynamicArea;
this.minHeight = settings.minHeight;
this.animation = settings.animation;
// Counter
let i;
// Calculate the bottom left x position
this.bottomLeftX = (this.width - this.bottomWidth) / 2;
// Prepare the configuration settings based on the defaults
// Set the default width and height based on the container
let settings = extend({}, this.defaults);
settings.width = parseInt(d3.select(this.selector).style('width'), 10);
settings.height = parseInt(d3.select(this.selector).style('height'), 10);
// Change in x direction
this.dx = this._getDx();
// Overwrite default settings with user options
let keys = Object.keys(options);
for (i = 0; i < keys.length; i++) {
if (keys[i] !== 'label') {
settings[keys[i]] = options[keys[i]];
}
}
// Change in y direction
this.dy = this._getDy();
// Label settings
if (options.hasOwnProperty('label')) {
let validLabelOptions = /fontSize|fill/;
// Support for events
this.onItemClick = settings.onItemClick;
}
Object.keys(options.label).forEach((labelOption) => {
if (labelOption.match(validLabelOptions)) {
settings.label[labelOption] = options.label[labelOption];
}
});
}
this.label = settings.label;
/**
* @param {Array} data
*
* @return void
*/
_validateData(data) {
if (Array.isArray(data) === false ||
data.length === 0 ||
Array.isArray(data[0]) === false ||
data[0].length < 2) {
throw new Error('Funnel data is not valid.');
}
}
// In the case that the width or height is not valid, set
// the width/height as its default hard-coded value
if (settings.width <= 0) {
settings.width = this.defaults.width;
}
if (settings.height <= 0) {
settings.height = this.defaults.height;
}
/**
* @param {Array} data
*
* @return void
*/
_setData(data) {
this.data = data;
// Initialize the colors for each block
let colorScale = d3.scale.category10();
for (i = 0; i < this.data.length; i++) {
let hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
this._setColors();
}
// If a color is not set for the record, add one
if (!('2' in this.data[i]) || !hexExpression.test(this.data[i][2])) {
this.data[i][2] = colorScale(i);
}
/**
* Set the colors for each block.
*
* @return {void}
*/
_setColors() {
let colorScale = d3.scale.category10();
let hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
// Add a color for for each block without one
this.data.forEach((block, index) => {
if (block.length < 3 || !hexExpression.test(block[2])) {
this.data[index][2] = colorScale(index);
}
});
}
// Initialize funnel chart settings
this.width = settings.width;
this.height = settings.height;
this.bottomWidth = settings.width * settings.bottomWidth;
this.bottomPinch = settings.bottomPinch;
this.isCurved = settings.isCurved;
this.curveHeight = settings.curveHeight;
this.fillType = settings.fillType;
this.isInverted = settings.isInverted;
this.hoverEffects = settings.hoverEffects;
this.dynamicArea = settings.dynamicArea;
this.minHeight = settings.minHeight;
this.animation = settings.animation;
/**
* @param {Object} options
*
* @returns {Object}
*/
_getSettings(options) {
// Prepare the configuration settings based on the defaults
// Set the default width and height based on the container
let settings = Utils.extend({}, this.defaults);
settings.width = parseInt(d3.select(this.selector).style('width'), 10);
settings.height = parseInt(d3.select(this.selector).style('height'), 10);
// Calculate the bottom left x position
this.bottomLeftX = (this.width - this.bottomWidth) / 2;
// Overwrite default settings with user options
settings = Utils.extend(settings, options);
// Change in x direction
// Will be sharper if there is a pinch
this.dx = this.bottomPinch > 0 ?
this.bottomLeftX / (data.length - this.bottomPinch) :
this.bottomLeftX / data.length;
// Change in y direction
// Curved chart needs reserved pixels to account for curvature
this.dy = this.isCurved ?
(this.height - this.curveHeight) / data.length :
this.height / data.length;
// In the case that the width or height is not valid, set
// the width/height as its default hard-coded value
if (settings.width <= 0) {
settings.width = this.defaults.width;
}
if (settings.height <= 0) {
settings.height = this.defaults.height;
}
// Support for events
this.onItemClick = settings.onItemClick;
return settings;
}
/**
* @return {Number}
*/
_getDx() {
// Will be sharper if there is a pinch
if (this.bottomPinch > 0) {
return this.bottomLeftX / (this.data.length - this.bottomPinch);
}
/**
* Create the paths to be used to define the discrete funnel blocks and
* returns the results in an array.
*
* @return {Array}
*/
_makePaths()
{
let paths = [];
return this.bottomLeftX / this.data.length;
}
// Initialize velocity
let dx = this.dx;
let dy = this.dy;
/**
* @return {Number}
*/
_getDy() {
// Curved chart needs reserved pixels to account for curvature
if (this.isCurved) {
return (this.height - this.curveHeight) / this.data.length;
}
// Initialize starting positions
let prevLeftX = 0;
let prevRightX = this.width;
let prevHeight = 0;
return this.height / this.data.length;
}
// Start from the bottom for inverted
if (this.isInverted) {
prevLeftX = this.bottomLeftX;
prevRightX = this.width - this.bottomLeftX;
}
/**
* Draw the chart onto the DOM.
*
* @return {void}
*/
_draw() {
// Add the SVG
this.svg = d3.select(this.selector)
.append('svg')
.attr('width', this.width)
.attr('height', this.height);
// Initialize next positions
let nextLeftX = 0;
let nextRightX = 0;
let nextHeight = 0;
this.blockPaths = this._makePaths();
let middle = this.width / 2;
// Define color gradients
if (this.fillType === 'gradient') {
this._defineColorGradients(this.svg);
}
// Move down if there is an initial curve
if (this.isCurved) {
prevHeight = 10;
}
// Add top oval if curved
if (this.isCurved) {
this._drawTopOval(this.svg, this.blockPaths);
}
let topBase = this.width;
let bottomBase = 0;
// Add each block
this._drawBlock(0);
}
let totalArea = this.height * (this.width + this.bottomWidth) / 2;
let slope = 2 * this.height / (this.width - this.bottomWidth);
/**
* Create the paths to be used to define the discrete funnel blocks and
* returns the results in an array.
*
* @return {Array}
*/
_makePaths() {
let paths = [];
// 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.minHeight !== false) {
let height = (this.height - this.minHeight * this.data.length);
totalArea = height * (this.width + this.bottomWidth) / 2;
}
// Initialize velocity
let dx = this.dx;
let dy = this.dy;
let totalCount = 0;
let count = 0;
// Initialize starting positions
let prevLeftX = 0;
let prevRightX = this.width;
let prevHeight = 0;
// Harvest total count
for (let i = 0; i < this.data.length; i++) {
totalCount += isArray(this.data[i][1]) ? this.data[i][1][0] : this.data[i][1];
}
// Start from the bottom for inverted
if (this.isInverted) {
prevLeftX = this.bottomLeftX;
prevRightX = this.width - this.bottomLeftX;
}
// Create the path definition for each funnel block
// Remember to loop back to the beginning point for a closed path
for (let i = 0; i < this.data.length; i++) {
count = isArray(this.data[i][1]) ? this.data[i][1][0] : this.data[i][1];
// Initialize next positions
let nextLeftX = 0;
let nextRightX = 0;
let nextHeight = 0;
// Calculate dynamic shapes based on area
if (this.dynamicArea) {
let ratio = count / totalCount;
let area = ratio * totalArea;
let middle = this.width / 2;
if (this.minHeight !== false) {
area += this.minHeight * (this.width + this.bottomWidth) / 2;
}
// Move down if there is an initial curve
if (this.isCurved) {
prevHeight = 10;
}
bottomBase = Math.sqrt((slope * topBase * topBase - (4 * area)) / slope);
dx = (topBase / 2) - (bottomBase / 2);
dy = (area * 2) / (topBase + bottomBase);
let topBase = this.width;
let bottomBase = 0;
if (this.isCurved) {
dy = dy - (this.curveHeight / this.data.length);
}
let totalArea = this.height * (this.width + this.bottomWidth) / 2;
let slope = 2 * this.height / (this.width - this.bottomWidth);
topBase = bottomBase;
}
// 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.minHeight !== false) {
totalArea = (this.height - this.minHeight * this.data.length) * (this.width + this.bottomWidth) / 2;
}
// Stop velocity for pinched blocks
if (this.bottomPinch > 0) {
// Check if we've reached the bottom of the pinch
// If so, stop changing on x
if (!this.isInverted) {
if (i >= this.data.length - this.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 area's (prevents zero velocity if isInverted
// and bottomPinch are non trivial and dynamicArea is
// false)
if (!this.dynamicArea) {
dx = this.dx;
}
let totalCount = 0;
let count = 0;
dx = i < this.bottomPinch ? 0 : dx;
}
// Harvest total count
this.data.forEach((block) => {
totalCount += Array.isArray(block[1]) ? block[1][0] : block[1];
});
// Create the path definition for each funnel block
// Remember to loop back to the beginning point for a closed path
this.data.forEach((block, i) => {
count = Array.isArray(block[1]) ? block[0] : block[1];
// Calculate dynamic shapes based on area
if (this.dynamicArea) {
let ratio = count / totalCount;
let area = ratio * totalArea;
if (this.minHeight !== false) {
area += this.minHeight * (this.width + this.bottomWidth) / 2;
}
// Calculate the position of next block
nextLeftX = prevLeftX + dx;
nextRightX = prevRightX - dx;
nextHeight = prevHeight + dy;
bottomBase = Math.sqrt((slope * topBase * topBase - (4 * area)) / slope);
// Expand outward if inverted
if (this.isInverted) {
nextLeftX = prevLeftX - dx;
nextRightX = prevRightX + dx;
// Prevent bottm points from becomming NaN
if (this.bottomWidth === 0 && i === this.data.length - 1) {
bottomBase = 0;
}
// Plot curved lines
// Prevent NaN slope
if (this.bottomWidth === this.width) {
bottomBase = topBase;
}
dx = (topBase / 2) - (bottomBase / 2);
dy = (area * 2) / (topBase + bottomBase);
if (this.isCurved) {
paths.push([
// Top Bezier curve
[prevLeftX, prevHeight, 'M'],
[middle, prevHeight + (this.curveHeight - 10), 'Q'],
[prevRightX, prevHeight, ''],
// Right line
[nextRightX, nextHeight, 'L'],
// Bottom Bezier curve
[nextRightX, nextHeight, 'M'],
[middle, nextHeight + this.curveHeight, 'Q'],
[nextLeftX, nextHeight, ''],
// Left line
[prevLeftX, prevHeight, 'L'],
]);
// Plot straight lines
} else {
paths.push([
// Start position
[prevLeftX, prevHeight, 'M'],
// Move to right
[prevRightX, prevHeight, 'L'],
// Move down
[nextRightX, nextHeight, 'L'],
// Move to left
[nextLeftX, nextHeight, 'L'],
// Wrap back to top
[prevLeftX, prevHeight, 'L'],
]);
dy = dy - (this.curveHeight / this.data.length);
}
// Set the next block's previous position
prevLeftX = nextLeftX;
prevRightX = nextRightX;
prevHeight = nextHeight;
topBase = bottomBase;
}
return paths;
}
// Stop velocity for pinched blocks
if (this.bottomPinch > 0) {
// Check if we've reached the bottom of the pinch
// If so, stop changing on x
if (!this.isInverted) {
if (i >= this.data.length - this.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 area's (prevents zero velocity if isInverted
// and bottomPinch are non trivial and dynamicArea is
// false)
if (!this.dynamicArea) {
dx = this.dx;
}
/**
* Define the linear color gradients.
*
* @param {Object} svg
*
* @return {void}
*/
_defineColorGradients(svg)
{
let defs = svg.append('defs');
// Create a gradient for each block
for (let i = 0; i < this.data.length; i++) {
let color = this.data[i][2];
let shade = shadeColor(color, -0.25);
// Create linear gradient
let gradient = defs.append('linearGradient')
.attr({
id: 'gradient-' + i
});
// Define the gradient stops
let stops = [
[0, shade],
[40, color],
[60, color],
[100, shade],
];
// Add the gradient stops
for (let j = 0; j < stops.length; j++) {
let stop = stops[j];
gradient.append('stop').attr({
offset: stop[0] + '%',
style: 'stop-color:' + stop[1],
});
dx = i < this.bottomPinch ? 0 : dx;
}
}
}
/**
* Draw the top oval of a curved funnel.
*
* @param {Object} svg
* @param {Array} blockPaths
*
* @return {void}
*/
_drawTopOval(svg, blockPaths)
{
let leftX = 0;
let rightX = this.width;
let centerX = this.width / 2;
// Calculate the position of next block
nextLeftX = prevLeftX + dx;
nextRightX = prevRightX - dx;
nextHeight = prevHeight + dy;
// Expand outward if inverted
if (this.isInverted) {
leftX = this.bottomLeftX;
rightX = this.width - this.bottomLeftX;
nextLeftX = prevLeftX - dx;
nextRightX = prevRightX + dx;
}
// Create path from top-most block
let paths = blockPaths[0];
let path = 'M' + leftX + ',' + paths[0][1] +
' Q' + centerX + ',' + (paths[1][1] + this.curveHeight - 10) +
' ' + rightX + ',' + paths[2][1] +
' M' + rightX + ',10' +
' Q' + centerX + ',0' +
' ' + leftX + ',10';
// Plot curved lines
if (this.isCurved) {
paths.push([
// Top Bezier curve
[prevLeftX, prevHeight, 'M'],
[middle, prevHeight + (this.curveHeight - 10), 'Q'],
[prevRightX, prevHeight, ''],
// Right line
[nextRightX, nextHeight, 'L'],
// Bottom Bezier curve
[nextRightX, nextHeight, 'M'],
[middle, nextHeight + this.curveHeight, 'Q'],
[nextLeftX, nextHeight, ''],
// Left line
[prevLeftX, prevHeight, 'L'],
]);
// Plot straight lines
} else {
paths.push([
// Start position
[prevLeftX, prevHeight, 'M'],
// Move to right
[prevRightX, prevHeight, 'L'],
// Move down
[nextRightX, nextHeight, 'L'],
// Move to left
[nextLeftX, nextHeight, 'L'],
// Wrap back to top
[prevLeftX, prevHeight, 'L'],
]);
}
// Draw top oval
svg.append('path')
.attr('fill', shadeColor(this.data[0][2], -0.4))
.attr('d', path);
}
// Set the next block's previous position
prevLeftX = nextLeftX;
prevRightX = nextRightX;
prevHeight = nextHeight;
});
/**
* Draw the next block in the iteration.
*
* @param {int} index
*
* @return {void}
*/
_drawBlock(index)
{
if (index === this.data.length) {
return;
}
return paths;
}
// Create a group just for this block
let group = this.svg.append('g');
/**
* Define the linear color gradients.
*
* @param {Object} svg
*
* @return {void}
*/
_defineColorGradients(svg) {
let defs = svg.append('defs');
// Fetch path element
let path = this._getBlockPath(group, index);
path.data(this._getBlockData(index));
// Create a gradient for each block
this.data.forEach((block, index) => {
let color = block[2];
let shade = Utils.shadeColor(color, -0.25);
// Add animation components
if (this.animation !== false) {
path.transition()
.duration(this.animation)
.ease('linear')
.attr('fill', this._getColor(index))
.attr('d', this._getPathDefinition(index))
.each('end', () => {
this._drawBlock(index + 1);
});
} else {
path.attr('fill', this._getColor(index))
.attr('d', this._getPathDefinition(index));
this._drawBlock(index + 1);
}
// Create linear gradient
let gradient = defs.append('linearGradient')
.attr({
id: 'gradient-' + index,
});
// Add the hover events
if (this.hoverEffects) {
path.on('mouseover', this._onMouseOver)
.on('mouseout', this._onMouseOut);
}
// Define the gradient stops
let stops = [
[0, shade],
[40, color],
[60, color],
[100, shade],
];
// ItemClick event
if (this.onItemClick) {
path.on('click', this.onItemClick);
}
// Add the gradient stops
stops.forEach((stop) => {
gradient.append('stop').attr({
offset: stop[0] + '%',
style: 'stop-color:' + stop[1],
});
});
});
}
this._addBlockLabel(group, index);
/**
* Draw the top oval of a curved funnel.
*
* @param {Object} svg
* @param {Array} blockPaths
*
* @return {void}
*/
_drawTopOval(svg, blockPaths) {
let leftX = 0;
let rightX = this.width;
let centerX = this.width / 2;
if (this.isInverted) {
leftX = this.bottomLeftX;
rightX = this.width - this.bottomLeftX;
}
/**
* @param {Object} group
* @param {int} index
*
* @return {Object}
*/
_getBlockPath(group, index)
{
let path = group.append('path');
// Create path from top-most block
let paths = blockPaths[0];
let topCurve = paths[1][1] + this.curveHeight - 10;
if (this.animation !== false) {
this._addBeforeTransition(path, index);
}
let path = this.navigator.plot([
['M', leftX, paths[0][1]],
['Q', centerX, topCurve],
[' ', rightX, paths[2][1]],
['M', rightX, 10],
['Q', centerX, 0],
[' ', leftX, 10],
]);
return path;
// Draw top oval
svg.append('path')
.attr('fill', Utils.shadeColor(this.data[0][2], -0.4))
.attr('d', path);
}
/**
* Draw the next block in the iteration.
*
* @param {int} index
*
* @return {void}
*/
_drawBlock(index) {
if (index === this.data.length) {
return;
}
/**
* Set the attributes of a path element before its animation.
*
* @param {Object} path
* @param {int} index
*
* @return {void}
*/
_addBeforeTransition(path, index)
{
let paths = this.blockPaths[index];
// Create a group just for this block
let group = this.svg.append('g');
let beforePath = '';
let beforeFill = '';
// Fetch path element
let path = this._getBlockPath(group, index);
path.data(this._getBlockData(index));
// Construct the top of the trapezoid and leave the other elements
// hovering around to expand downward on animation
if (!this.isCurved) {
beforePath = '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 = '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.fillType === 'solid') {
beforeFill = index > 0 ? this._getColor(index - 1) : this._getColor(index);
// Use current background if gradient (gradients do not transition)
} else {
beforeFill = this._getColor(index);
}
path.attr('d', beforePath)
.attr('fill', beforeFill);
// Add animation components
if (this.animation !== false) {
path.transition()
.duration(this.animation)
.ease('linear')
.attr('fill', this._getColor(index))
.attr('d', this._getPathDefinition(index))
.each('end', () => {
this._drawBlock(index + 1);
});
} else {
path.attr('fill', this._getColor(index))
.attr('d', this._getPathDefinition(index));
this._drawBlock(index + 1);
}
/**
* @param {int} index
*
* @return {Array}
*/
_getBlockData(index)
{
return [{
index: index,
label: this.data[index][0],
value: isArray(this.data[index][1]) ?
this.data[index][1][0] :
this.data[index][1],
formattedValue: isArray(this.data[index][1]) ?
this.data[index][1][1] :
this.data[index][1].toLocaleString(),
baseColor: this.data[index][2],
fill: this._getColor(index),
}];
// Add the hover events
if (this.hoverEffects) {
path.on('mouseover', this._onMouseOver)
.on('mouseout', this._onMouseOut);
}
/**
* Return the color for the given index.
*
* @param {int} index
*
* @return {string}
*/
_getColor(index)
{
if (this.fillType === 'solid') {
return this.data[index][2];
} else {
return 'url(#gradient-' + index + ')';
}
// ItemClick event
if (this.onItemClick !== null) {
path.on('click', this.onItemClick);
}
/**
* @param {int} index
*
* @return {string}
*/
_getPathDefinition(index)
{
let pathStr = '';
let point = [];
let paths = this.blockPaths[index];
this._addBlockLabel(group, index);
}
for (let j = 0; j < paths.length; j++) {
point = paths[j];
pathStr += point[2] + point[0] + ',' + point[1] + ' ';
}
/**
* @param {Object} group
* @param {int} index
*
* @return {Object}
*/
_getBlockPath(group, index) {
let path = group.append('path');
return pathStr;
if (this.animation !== false) {
this._addBeforeTransition(path, index);
}
/**
* @param {Object} data
*
* @return {void}
*/
_onMouseOver(data)
{
d3.select(this).attr('fill', shadeColor(data.baseColor, -0.2));
}
return path;
}
/**
* @param {Object} data
*
* @return {void}
*/
_onMouseOut(data)
{
d3.select(this).attr('fill', data.fill);
}
/**
* Set the attributes of a path element before its animation.
*
* @param {Object} path
* @param {int} index
*
* @return {void}
*/
_addBeforeTransition(path, index) {
let paths = this.blockPaths[index];
/**
* @param {Object} group
* @param {int} index
*
* @return {void}
*/
_addBlockLabel(group, index)
{
let i = index;
let paths = this.blockPaths[index];
let blockData = this._getBlockData(index)[0];
let textStr = blockData.label + ': ' + blockData.formattedValue;
let textFill = this.data[i][3] || this.label.fill;
let beforePath = '';
let beforeFill = '';
let textX = this.width / 2; // Center the text
let textY = !this.isCurved ? // Average height of bases
(paths[1][1] + paths[2][1]) / 2 :
(paths[2][1] + paths[3][1]) / 2 + (this.curveHeight / this.data.length);
// Construct the top of the trapezoid and leave the other elements
// hovering around to expand downward on animation
if (!this.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]],
]);
}
group.append('text')
.text(textStr)
.attr({
'x': textX,
'y': textY,
'text-anchor': 'middle',
'dominant-baseline': 'middle',
'fill': textFill,
'pointer-events': 'none',
})
.style('font-size', this.label.fontSize);
// Use previous fill color, if available
if (this.fillType === 'solid') {
beforeFill = index > 0 ? this._getColor(index - 1) : this._getColor(index);
// Use current background if gradient (gradients do not transition)
} else {
beforeFill = this._getColor(index);
}
path.attr('d', beforePath)
.attr('fill', beforeFill);
}
/**
* Check if the supplied value is an array.
* @param {int} index
*
* @param {*} value
*
* @return {bool}
* @return {Array}
*/
function isArray(value)
{
return Object.prototype.toString.call(value) === '[object Array]';
_getBlockData(index) {
let label = this.data[index][0];
let value = this.data[index][1];
return [{
index: index,
label: label,
value: value,
formatted: this.labelFormatter.format(label, value),
baseColor: this.data[index][2],
fill: this._getColor(index),
}];
}
/**
* Extends an object with the members of another.
* Return the color for the given index.
*
* @param {Object} a The object to be extended.
* @param {Object} b The object to clone from.
* @param {int} index
*
* @return {Object}
* @return {string}
*/
function extend(a, b)
{
let prop;
for (prop in b) {
if (b.hasOwnProperty(prop)) {
a[prop] = b[prop];
}
_getColor(index) {
if (this.fillType === 'solid') {
return this.data[index][2];
} else {
return 'url(#gradient-' + index + ')';
}
return a;
}
/**
* Shade a color to the given percentage.
* @param {int} index
*
* @param {string} color A hex color.
* @param {number} shade The shade adjustment. Can be positive or negative.
*
* @return {string}
*/
function shadeColor(color, shade)
{
let f = parseInt(color.slice(1), 16);
let t = shade < 0 ? 0 : 255;
let p = shade < 0 ? shade * -1 : shade;
let R = f >> 16, G = f >> 8 & 0x00FF;
let B = f & 0x0000FF;
_getPathDefinition(index) {
let commands = [];
let converted = (0x1000000 + (Math.round((t - R) * p) + R) *
0x10000 + (Math.round((t - G) * p) + G) *
0x100 + (Math.round((t - B) * p) + B));
this.blockPaths[index].forEach((command) => {
commands.push([command[2], command[0], command[1]]);
});
return '#' + converted.toString(16).slice(1);
return this.navigator.plot(commands);
}
global.D3Funnel = D3Funnel;
/**
* @param {Object} data
*
* @return {void}
*/
_onMouseOver(data) {
d3.select(this).attr('fill', Utils.shadeColor(data.baseColor, -0.2));
}
})(window, d3);
/**
* @param {Object} data
*
* @return {void}
*/
_onMouseOut(data) {
d3.select(this).attr('fill', data.fill);
}
/**
* @param {Object} group
* @param {int} index
*
* @return {void}
*/
_addBlockLabel(group, index) {
let paths = this.blockPaths[index];
let label = this._getBlockData(index)[0].formatted;
let fill = this.data[index][3] || this.label.fill;
let x = this.width / 2; // Center the text
let y = this._getTextY(paths);
group.append('text')
.text(label)
.attr({
'x': x,
'y': y,
'text-anchor': 'middle',
'dominant-baseline': 'middle',
'fill': fill,
'pointer-events': 'none',
})
.style('font-size', this.label.fontSize);
}
/**
* 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}
*/
_getTextY(paths) {
if (this.isCurved) {
return (paths[2][1] + paths[3][1]) / 2 + (this.curveHeight / this.data.length);
}
return (paths[1][1] + paths[2][1]) / 2;
}
}

@@ -0,3 +1,5 @@

/* global d3, assert, chai, D3Funnel */
describe('D3Funnel', function () {
var getFunnel, getSvg, getLength, getBasicData;
var getFunnel, getSvg, getBasicData, getPathHeight, getCommandHeight;

@@ -11,8 +13,13 @@ beforeEach(function (done) {

};
getLength = function (selection) {
return selection[0].length;
getBasicData = function () {
return [['Node', 1000]];
};
getBasicData = function() {
return [['Node', 100]];
getPathHeight = function (path) {
var commands = path.attr('d').split(' ');
return getCommandHeight(commands[2]) - getCommandHeight(commands[0]);
};
getCommandHeight = function (command) {
return parseFloat(command.split(',')[1]);
};

@@ -30,6 +37,6 @@ done();

describe('draw', function () {
it('should draw simple chart', function () {
it('should draw a chart on the identified target', function () {
getFunnel().draw(getBasicData(), {});
assert.equal(1, getLength(getSvg()));
assert.equal(1, getSvg()[0].length);
});

@@ -40,3 +47,3 @@

assert.equal(1, getLength(getSvg()));
assert.equal(1, getSvg()[0].length);
});

@@ -51,2 +58,52 @@

});
it('should draw as many blocks as there are elements', function () {
getFunnel().draw([
['Node A', 1],
['Node B', 2],
['Node C', 3],
['Node D', 4],
]);
assert.equal(4, getSvg().selectAll('path')[0].length);
});
it('should use colors assigned to a data element', function () {
var paths;
var colorScale;
getFunnel().draw([
['A', 1, '#111'],
['B', 2, '#222'],
['C', 3],
['D', 4, '#444'],
]);
paths = getSvg().selectAll('path')[0];
colorScale = d3.scale.category10();
assert.equal('#111', d3.select(paths[0]).attr('fill'));
assert.equal('#222', d3.select(paths[1]).attr('fill'));
assert.equal(colorScale(2), d3.select(paths[2]).attr('fill'));
assert.equal('#444', d3.select(paths[3]).attr('fill'));
});
it('should use label colors assigned to a data element', function () {
var texts;
getFunnel().draw([
['A', 1, null, '#111'],
['B', 2, null, '#222'],
['C', 3],
['D', 4, null, '#444'],
]);
texts = getSvg().selectAll('text')[0];
assert.equal('#111', d3.select(texts[0]).attr('fill'));
assert.equal('#222', d3.select(texts[1]).attr('fill'));
assert.equal('#fff', d3.select(texts[2]).attr('fill'));
assert.equal('#444', d3.select(texts[3]).attr('fill'));
});
});

@@ -61,3 +118,3 @@

assert.equal(0, getLength(getSvg()));
assert.equal(0, getSvg()[0].length);
});

@@ -71,3 +128,3 @@ });

getFunnel().draw(getBasicData(), {
width: 200
width: 200,
});

@@ -82,3 +139,3 @@

getFunnel().draw(getBasicData(), {
height: 200
height: 200,
});

@@ -93,3 +150,3 @@

getFunnel().draw(getBasicData(), {
isCurved: true
isCurved: true,
});

@@ -102,3 +159,3 @@

getFunnel().draw(getBasicData(), {
isCurved: true
isCurved: true,
});

@@ -119,3 +176,3 @@

getFunnel().draw(getBasicData(), {
fillType: 'gradient'
fillType: 'gradient',
});

@@ -142,2 +199,91 @@

describe('hoverEffects', function () {
it('should change block color on hover', function () {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('mouseover', false, false, null);
getFunnel().draw([
['A', 1, '#fff'],
], {
hoverEffects: true,
});
d3.select('#funnel path').node().dispatchEvent(event);
// #fff * -1/5 => #cccccc
assert.equal('#cccccc', d3.select('#funnel path').attr('fill'));
});
});
describe('dynamicArea', function () {
it('should use equal heights when false', function () {
var paths;
getFunnel().draw([
['A', 1],
['B', 2],
], {
height: 300,
});
paths = d3.selectAll('#funnel path')[0];
assert.equal(150, getPathHeight(d3.select(paths[0])));
assert.equal(150, getPathHeight(d3.select(paths[1])));
});
it('should use proportional heights when true', function () {
var paths;
getFunnel().draw([
['A', 1],
['B', 2],
], {
height: 300,
dynamicArea: true,
});
paths = d3.selectAll('#funnel path')[0];
assert.equal(72, parseInt(getPathHeight(d3.select(paths[0])), 10));
assert.equal(227, parseInt(getPathHeight(d3.select(paths[1])), 10));
});
it('should not have NaN in the last path when bottomWidth is equal to 0%', function () {
var paths;
// A very specific cooked-up example that could trigger NaN
getFunnel().draw([
['A', 120],
['B', 40],
['C', 20],
['D', 15],
], {
height: 300,
dynamicArea: true,
bottomWidth: 0,
});
paths = d3.selectAll('#funnel path')[0];
assert.equal(-1, d3.select(paths[3]).attr('d').indexOf('NaN'))
});
it('should not error when bottomWidth is equal to 100%', function () {
var paths;
getFunnel().draw([
['A', 1],
['B', 2],
], {
height: 300,
dynamicArea: true,
bottomWidth: 1,
});
paths = d3.selectAll('#funnel path')[0];
});
});
describe('label.fontSize', function () {

@@ -147,4 +293,4 @@ it('should set the label\'s font size to the specified amount', function () {

label: {
fontSize: '16px'
}
fontSize: '16px',
},
});

@@ -160,4 +306,4 @@

label: {
fill: '#777'
}
fill: '#777',
},
});

@@ -168,3 +314,57 @@

});
describe('label.format', function () {
it('should parse a string template', function () {
getFunnel().draw(getBasicData(), {
label: {
format: '{l} {v} {f}',
}
});
// Node.js does not have localization, so toLocaleString() will
// leave the value untouched
// https://github.com/joyent/node/issues/4689
assert.equal('Node 1000 1000', d3.select('#funnel text').text());
});
it('should pass values to a supplied function', function () {
getFunnel().draw(getBasicData(), {
label: {
format: function (label, value, fValue) {
return label + '/' + value + '/' + fValue;
}
}
});
assert.equal('Node/1000/null', d3.select('#funnel text').text());
});
});
describe('onItemClick', function () {
it('should invoke the callback function with the correct data', function () {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('click', false, false, null);
var spy = chai.spy();
getFunnel().draw(getBasicData(), {
onItemClick: function (d, i) {
spy({
index: d.index,
label: d.label,
value: d.value,
}, i);
},
});
d3.select('#funnel path').node().dispatchEvent(event);
chai.expect(spy).to.have.been.called.once.with({
index: 0,
label: 'Node',
value: 1000,
}, 0);
});
});
});
});

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc