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.13 to 0.7.0

.eslintrc

48

CHANGELOG.md

@@ -0,1 +1,49 @@

## v0.7.0 (October 4, 2015)
D3Funnel v0.7 is a **backwards-incompatible** release that resolves some
outstanding bugs, standardizes several option names and formats, and introduces
a few new features.
No new features will be added to the v0.6 series, but minor patches will be
available for a few months.
### Behavior Changes
* [#29]: Dynamic block heights are no longer determined by their weighted area, but by their weighted height
* Heights determined by weighted area: http://jsfiddle.net/zq4L82kv/2/ (legacy v0.6.x)
* Heights determined by weighted height: http://jsfiddle.net/bawv6m0j/1/ (v0.7+)
### New Features
* [#9]: Block can now have their color scale specified in addition to data points
* [#34]: Default options are now statically available and overridable
### Bug Fixes
* [#25]: Fix issues with `isInverted` and `dynamicArea` producing odd pyramids
* [#32]: Fix issue where pinched blocks were not having the same width as `bottomWidth`
### Upgrading from v0.6.x
Several options have been renamed for standardization. Please refer to the table
below for the new equivalent option:
| Old option | New option | Notes |
| -------------- | --------------------- | --------------- |
| `animation` | `chart.animate` | |
| `bottomPinch` | `chart.bottomPinch` | |
| `bottomWidth` | `chart.bottomWidth` | |
| `curveHeight | `chart.curve.height` | |
| `dynamicArea` | `block.dynamicHeight` | See change #29. |
| `fillType` | `block.fill.type` | |
| `height` | `chart.height` | |
| `hoverEffects` | `block.hightlight` | |
| `isCurved` | `chart.curve.enabled` | |
| `isInverted` | `chart.inverted` | |
| `onItemClick` | `events.click.block` | |
| `minHeight` | `block.minHeight` | |
| `width` | `chart.width` | |
In addition, please refer to change #29.
## v0.6.13 (October 2, 2015)

@@ -2,0 +50,0 @@

617

dist/d3-funnel.js

@@ -12,3 +12,3 @@

/* global d3, LabelFormatter, Navigator, Utils */
/* global d3, Colorizer, LabelFormatter, Navigator, Utils */
/* exported D3Funnel */

@@ -23,8 +23,45 @@

var D3Funnel = (function () {
_createClass(D3Funnel, null, [{
key: 'defaults',
value: {
chart: {
width: 350,
height: 400,
bottomWidth: 1 / 3,
bottomPinch: 0,
inverted: false,
animate: false,
curve: {
enabled: false,
height: 20
}
},
block: {
dynamicHeight: false,
fill: {
scale: d3.scale.category10().domain(d3.range(0, 10)),
type: 'solid'
},
minHeight: false,
highlight: false
},
label: {
fontSize: '14px',
fill: '#fff',
format: '{l}: {f}'
},
events: {
click: {
block: null
}
}
},
/**
* @param {string} selector A selector for the container element.
*
* @return {void}
*/
/**
* @param {string} selector A selector for the container element.
*
* @return {void}
*/
enumerable: true
}]);

@@ -36,23 +73,3 @@ function D3Funnel(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.colorizer = new Colorizer();

@@ -64,3 +81,4 @@ this.labelFormatter = new LabelFormatter();

/* exported LabelFormatter */
/* exported Colorizer */
/* jshint bitwise: false */

@@ -117,4 +135,2 @@ /**

this._setData(data);
var settings = this._getSettings(options);

@@ -126,16 +142,25 @@

// Set color scales
this.colorizer.setLabelFill(settings.label.fill);
this.colorizer.setScale(settings.block.fill.scale);
// 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;
this.width = settings.chart.width;
this.height = settings.chart.height;
this.bottomWidth = settings.chart.width * settings.chart.bottomWidth;
this.bottomPinch = settings.chart.bottomPinch;
this.isInverted = settings.chart.inverted;
this.isCurved = settings.chart.curve.enabled;
this.curveHeight = settings.chart.curve.height;
this.fillType = settings.block.fill.type;
this.hoverEffects = settings.block.highlight;
this.dynamicHeight = settings.block.dynamicHeight;
this.minHeight = settings.block.minHeight;
this.animation = settings.chart.animate;
// Support for events
this.onBlockClick = settings.events.click.block;
this._setBlocks(data);
// Calculate the bottom left x position

@@ -149,5 +174,2 @@ this.bottomLeftX = (this.width - this.bottomWidth) / 2;

this.dy = this._getDy();
// Support for events
this.onItemClick = settings.onItemClick;
}

@@ -169,2 +191,34 @@

/**
* @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({}, D3Funnel.defaults);
settings.chart.width = parseInt(d3.select(this.selector).style('width'), 10);
settings.chart.height = parseInt(d3.select(this.selector).style('height'), 10);
// 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.chart.width <= 0) {
settings.chart.width = D3Funnel.defaults.chart.width;
}
if (settings.chart.height <= 0) {
settings.chart.height = D3Funnel.defaults.chart.height;
}
return settings;
}
/**
* Register the raw data into a standard block format and pre-calculate
* some values.
*
* @param {Array} data

@@ -175,63 +229,87 @@ *

}, {
key: '_setData',
value: function _setData(data) {
this.data = data;
key: '_setBlocks',
value: function _setBlocks(data) {
var totalCount = this._getTotalCount(data);
this._setColors();
this.blocks = this._standardizeData(data, totalCount);
}
/**
* Set the colors for each block.
* Return the total count of all blocks.
*
* @return {void}
* @return {Number}
*/
}, {
key: '_setColors',
value: function _setColors() {
key: '_getTotalCount',
value: function _getTotalCount(data) {
var _this = this;
var colorScale = d3.scale.category10();
var hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
var total = 0;
// 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);
}
data.forEach(function (block) {
total += _this._getRawBlockCount(block);
});
return total;
}
/**
* @param {Object} options
* Convert the raw data into a standardized format.
*
* @returns {Object}
* @param {Array} data
* @param {Number} totalCount
*
* @return {Array}
*/
}, {
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);
key: '_standardizeData',
value: function _standardizeData(data, totalCount) {
var _this2 = this;
// Overwrite default settings with user options
settings = Utils.extend(settings, options);
var standardized = [];
// 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;
}
var count = undefined;
var ratio = undefined;
var label = undefined;
return settings;
data.forEach(function (block, index) {
count = _this2._getRawBlockCount(block);
ratio = count / totalCount;
label = block[0];
standardized.push({
index: index,
value: count,
ratio: ratio,
height: _this2.height * ratio,
formatted: _this2.labelFormatter.format(label, count),
fill: _this2.colorizer.getBlockFill(block, index),
label: {
raw: label,
formatted: _this2.labelFormatter.format(label, count),
color: _this2.colorizer.getLabelFill(block, index)
}
});
});
return standardized;
}
/**
* Given a raw data block, return its count.
*
* @param {Array} block
*
* @return {Number}
*/
}, {
key: '_getRawBlockCount',
value: function _getRawBlockCount(block) {
return Array.isArray(block[1]) ? block[1][0] : block[1];
}
/**
* @return {Number}
*/
}, {
key: '_getDx',

@@ -241,6 +319,6 @@ value: function _getDx() {

if (this.bottomPinch > 0) {
return this.bottomLeftX / (this.data.length - this.bottomPinch);
return this.bottomLeftX / (this.blocks.length - this.bottomPinch);
}
return this.bottomLeftX / this.data.length;
return this.bottomLeftX / this.blocks.length;
}

@@ -256,6 +334,6 @@

if (this.isCurved) {
return (this.height - this.curveHeight) / this.data.length;
return (this.height - this.curveHeight) / this.blocks.length;
}
return this.height / this.data.length;
return this.height / this.blocks.length;
}

@@ -299,3 +377,3 @@

value: function _makePaths() {
var _this2 = this;
var _this3 = this;

@@ -331,8 +409,4 @@ var paths = [];

var topBase = this.width;
var bottomBase = 0;
var totalHeight = this.height;
var totalArea = this.height * (this.width + this.bottomWidth) / 2;
var slope = 2 * this.height / (this.width - this.bottomWidth);
// This is greedy in that the block will have a guaranteed height

@@ -342,55 +416,84 @@ // and the remaining is shared among the ratio, instead of being

if (this.minHeight !== false) {
totalArea = (this.height - this.minHeight * this.data.length) * (this.width + this.bottomWidth) / 2;
totalHeight = this.height - this.minHeight * this.blocks.length;
}
var totalCount = 0;
var count = 0;
var slopeHeight = this.height;
// Harvest total count
this.data.forEach(function (block) {
totalCount += Array.isArray(block[1]) ? block[1][0] : block[1];
// Correct slope height if there are blocks being pinched (and thus
// requiring a sharper curve)
this.blocks.forEach(function (block, i) {
if (_this3.bottomPinch > 0) {
if (_this3.isInverted) {
if (i < _this3.bottomPinch) {
slopeHeight -= block.height;
}
} else if (i >= _this3.blocks.length - _this3.bottomPinch) {
slopeHeight -= block.height;
}
}
});
// The slope will determine the where the x points on each block
// iteration
var slope = 2 * slopeHeight / (this.width - this.bottomWidth);
// 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];
this.blocks.forEach(function (block, i) {
// Make heights proportional to block weight
if (_this3.dynamicHeight) {
// Slice off the height proportional to this block
dy = totalHeight * block.ratio;
// Calculate dynamic shapes based on area
if (_this2.dynamicArea) {
var ratio = count / totalCount;
var area = ratio * totalArea;
// Add greedy minimum height
if (_this3.minHeight !== false) {
dy += _this3.minHeight;
}
if (_this2.minHeight !== false) {
area += _this2.minHeight * (_this2.width + _this2.bottomWidth) / 2;
// Account for any curvature
if (_this3.isCurved) {
dy = dy - _this3.curveHeight / _this3.blocks.length;
}
bottomBase = Math.sqrt((slope * topBase * topBase - 4 * area) / slope);
// Given: y = mx + b
// Given: b = 0 (when funnel), b = this.height (when pyramid)
// For funnel, x_i = y_i / slope
nextLeftX = (prevHeight + dy) / slope;
// Prevent bottm points from becomming NaN
if (_this2.bottomWidth === 0 && i === _this2.data.length - 1) {
bottomBase = 0;
// For pyramid, x_i = y_i - this.height / -slope
if (_this3.isInverted) {
nextLeftX = (prevHeight + dy - _this3.height) / (-1 * slope);
}
// Prevent NaN slope
if (_this2.bottomWidth === _this2.width) {
bottomBase = topBase;
// If bottomWidth is 0, adjust last x position (to circumvent
// errors associated with rounding)
if (_this3.bottomWidth === 0 && i === _this3.blocks.length - 1) {
// For funnel, last position is the center
nextLeftX = _this3.width / 2;
// For pyramid, last position is the origin
if (_this3.isInverted) {
nextLeftX = 0;
}
}
dx = topBase / 2 - bottomBase / 2;
dy = area * 2 / (topBase + bottomBase);
// If bottomWidth is same as width, stop x velocity
if (_this3.bottomWidth === _this3.width) {
nextLeftX = prevLeftX;
}
if (_this2.isCurved) {
dy = dy - _this2.curveHeight / _this2.data.length;
// Calculate the shift necessary for both x points
dx = nextLeftX - prevLeftX;
if (_this3.isInverted) {
dx = prevLeftX - nextLeftX;
}
topBase = bottomBase;
}
// Stop velocity for pinched blocks
if (_this2.bottomPinch > 0) {
if (_this3.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) {
if (!_this3.isInverted) {
if (i >= _this3.blocks.length - _this3.bottomPinch) {
dx = 0;

@@ -403,9 +506,9 @@ }

// static area's (prevents zero velocity if isInverted
// and bottomPinch are non trivial and dynamicArea is
// and bottomPinch are non trivial and dynamicHeight is
// false)
if (!_this2.dynamicArea) {
dx = _this2.dx;
if (!_this3.dynamicHeight) {
dx = _this3.dx;
}
dx = i < _this2.bottomPinch ? 0 : dx;
dx = i < _this3.bottomPinch ? 0 : dx;
}

@@ -420,3 +523,3 @@ }

// Expand outward if inverted
if (_this2.isInverted) {
if (_this3.isInverted) {
nextLeftX = prevLeftX - dx;

@@ -427,10 +530,10 @@ nextRightX = prevRightX + dx;

// Plot curved lines
if (_this2.isCurved) {
if (_this3.isCurved) {
paths.push([
// Top Bezier curve
[prevLeftX, prevHeight, 'M'], [middle, prevHeight + (_this2.curveHeight - 10), 'Q'], [prevRightX, prevHeight, ''],
[prevLeftX, prevHeight, 'M'], [middle, prevHeight + (_this3.curveHeight - 10), 'Q'], [prevRightX, prevHeight, ''],
// Right line
[nextRightX, nextHeight, 'L'],
// Bottom Bezier curve
[nextRightX, nextHeight, 'M'], [middle, nextHeight + _this2.curveHeight, 'Q'], [nextLeftX, nextHeight, ''],
[nextRightX, nextHeight, 'M'], [middle, nextHeight + _this3.curveHeight, 'Q'], [nextLeftX, nextHeight, ''],
// Left line

@@ -475,5 +578,5 @@ [prevLeftX, prevHeight, 'L']]);

// Create a gradient for each block
this.data.forEach(function (block, index) {
var color = block[2];
var shade = Utils.shadeColor(color, -0.25);
this.blocks.forEach(function (block, index) {
var color = block.fill;
var shade = Colorizer.shade(color, -0.25);

@@ -525,3 +628,3 @@ // Create linear gradient

// Draw top oval
svg.append('path').attr('fill', Utils.shadeColor(this.data[0][2], -0.4)).attr('d', path);
svg.append('path').attr('fill', Colorizer.shade(this.blocks[0].fill, -0.4)).attr('d', path);
}

@@ -539,5 +642,5 @@

value: function _drawBlock(index) {
var _this3 = this;
var _this4 = this;
if (index === this.data.length) {
if (index === this.blocks.length) {
return;

@@ -551,11 +654,11 @@ }

var path = this._getBlockPath(group, index);
path.data(this._getBlockData(index));
path.data(this._getD3Data(index));
// 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);
path.transition().duration(this.animation).ease('linear').attr('fill', this._getFillColor(index)).attr('d', this._getPathDefinition(index)).each('end', function () {
_this4._drawBlock(index + 1);
});
} else {
path.attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index));
path.attr('fill', this._getFillColor(index)).attr('d', this._getPathDefinition(index));
this._drawBlock(index + 1);

@@ -570,4 +673,4 @@ }

// ItemClick event
if (this.onItemClick !== null) {
path.on('click', this.onItemClick);
if (this.onBlockClick !== null) {
path.on('click', this.onBlockClick);
}

@@ -621,7 +724,7 @@

// 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)
if (this.fillType === 'solid' && index > 0) {
beforeFill = this._getFillColor(index - 1);
// Otherwise use current background
} else {
beforeFill = this._getColor(index);
beforeFill = this._getFillColor(index);
}

@@ -633,2 +736,4 @@

/**
* Return d3 formatted data for the given block.
*
* @param {int} index

@@ -639,19 +744,9 @@ *

}, {
key: '_getBlockData',
value: function _getBlockData(index) {
var label = this.data[index][0];
var 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)
}];
key: '_getD3Data',
value: function _getD3Data(index) {
return [this.blocks[index]];
}
/**
* Return the color for the given index.
* Return the block fill color for the given index.
*

@@ -663,9 +758,9 @@ * @param {int} index

}, {
key: '_getColor',
value: function _getColor(index) {
key: '_getFillColor',
value: function _getFillColor(index) {
if (this.fillType === 'solid') {
return this.data[index][2];
} else {
return 'url(#gradient-' + index + ')';
return this.blocks[index].fill;
}
return 'url(#gradient-' + index + ')';
}

@@ -698,3 +793,3 @@

value: function _onMouseOver(data) {
d3.select(this).attr('fill', Utils.shadeColor(data.baseColor, -0.2));
d3.select(this).attr('fill', Colorizer.shade(data.fill, -0.2));
}

@@ -724,4 +819,4 @@

var label = this._getBlockData(index)[0].formatted;
var fill = this.data[index][3] || this.label.fill;
var text = this.blocks[index].label.formatted;
var fill = this.blocks[index].label.color;

@@ -731,3 +826,3 @@ var x = this.width / 2; // Center the text

group.append('text').text(label).attr({
group.append('text').text(text).attr({
'x': x,

@@ -754,3 +849,3 @@ 'y': y,

if (this.isCurved) {
return (paths[2][1] + paths[3][1]) / 2 + this.curveHeight / this.data.length;
return (paths[2][1] + paths[3][1]) / 2 + this.curveHeight / this.blocks.length;
}

@@ -765,2 +860,126 @@

var Colorizer = (function () {
function Colorizer() {
_classCallCheck(this, Colorizer);
this.hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
this.labelFill = null;
this.scale = null;
}
/* exported LabelFormatter */
/**
* @param {string} fill
*
* @return {void}
*/
_createClass(Colorizer, [{
key: 'setLabelFill',
value: function setLabelFill(fill) {
this.labelFill = fill;
}
/**
* @param {function|Array} scale
*
* @return {void}
*/
}, {
key: 'setScale',
value: function setScale(scale) {
this.scale = scale;
}
/**
* Given a raw data block, return an appropriate color for the block.
*
* @param {Array} block
* @param {Number} index
*
* @return {string}
*/
}, {
key: 'getBlockFill',
value: function getBlockFill(block, index) {
// Use the block's color, if set and valid
if (block.length > 2 && this.hexExpression.test(block[2])) {
return block[2];
}
if (Array.isArray(this.scale)) {
return this.scale[index];
}
return this.scale(index);
}
/**
* Given a raw data block, return an appropriate label color.
*
* @param {Array} block
*
* @return {string}
*/
}, {
key: 'getLabelFill',
value: function getLabelFill(block) {
// Use the label's color, if set and valid
if (block.length > 3 && this.hexExpression.test(block[3])) {
return block[3];
}
return this.labelFill;
}
/**
* 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: 'shade',
value: function shade(color, _shade) {
var hex = color.slice(1);
if (hex.length === 3) {
hex = Colorizer.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 Colorizer;
})();
var LabelFormatter = (function () {

@@ -816,5 +1035,5 @@

return this.formatter(label, value[0], value[1]);
} else {
return this.formatter(label, value, null);
}
return this.formatter(label, value, null);
}

@@ -840,9 +1059,11 @@

var formatted = fValue;
// Attempt to use supplied formatted value
// Otherwise, use the default
if (fValue === null) {
fValue = this.getDefaultFormattedValue(value);
formatted = this.getDefaultFormattedValue(value);
}
return this.expression.split('{l}').join(label).split('{v}').join(value).split('{f}').join(fValue);
return this.expression.split('{l}').join(label).split('{v}').join(value).split('{f}').join(formatted);
}

@@ -871,3 +1092,2 @@

/* exported Utils */
/* jshint bitwise: false */

@@ -923,4 +1143,8 @@ /**

if (b.hasOwnProperty(prop)) {
if (typeof a[prop] === 'object' && typeof b[prop] === 'object') {
a[prop] = Utils.extend(a[prop], b[prop]);
if (typeof b[prop] === 'object' && !Array.isArray(b[prop])) {
if (typeof a[prop] === 'object' && !Array.isArray(a[prop])) {
a[prop] = Utils.extend(a[prop], b[prop]);
} else {
a[prop] = Utils.extend({}, b[prop]);
}
} else {

@@ -934,45 +1158,2 @@ a[prop] = b[prop];

}
/**
* 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];
}
}]);

@@ -979,0 +1160,0 @@

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

/*! d3-funnel - v0.6.13 | 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});
/*! d3-funnel - v0.7.0 | 2015 */
!function(t,e){"function"==typeof define&&define.amd?define(["d3"],e):"object"==typeof exports?module.exports=e(require("d3")):t.D3Funnel=e(t.d3)}(this,function(t){"use strict";function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=function(){function t(t,e){for(var i=0;i<e.length;i++){var n=e[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,i,n){return i&&t(e.prototype,i),n&&t(e,n),e}}(),n=function(){function n(t){e(this,n),this.selector=t,this.colorizer=new h,this.labelFormatter=new o,this.navigator=new a}return i(n,null,[{key:"defaults",value:{chart:{width:350,height:400,bottomWidth:1/3,bottomPinch:0,inverted:!1,animate:!1,curve:{enabled:!1,height:20}},block:{dynamicHeight:!1,fill:{scale:t.scale.category10().domain(t.range(0,10)),type:"solid"},minHeight:!1,highlight:!1},label:{fontSize:"14px",fill:"#fff",format:"{l}: {f}"},events:{click:{block:null}}},enumerable:!0}]),i(n,[{key:"destroy",value:function(){t.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._draw()}},{key:"_initialize",value:function(t,e){this._validateData(t);var i=this._getSettings(e);this.label=i.label,this.labelFormatter.setFormat(this.label.format),this.colorizer.setLabelFill(i.label.fill),this.colorizer.setScale(i.block.fill.scale),this.width=i.chart.width,this.height=i.chart.height,this.bottomWidth=i.chart.width*i.chart.bottomWidth,this.bottomPinch=i.chart.bottomPinch,this.isInverted=i.chart.inverted,this.isCurved=i.chart.curve.enabled,this.curveHeight=i.chart.curve.height,this.fillType=i.block.fill.type,this.hoverEffects=i.block.highlight,this.dynamicHeight=i.block.dynamicHeight,this.minHeight=i.block.minHeight,this.animation=i.chart.animate,this.onBlockClick=i.events.click.block,this._setBlocks(t),this.bottomLeftX=(this.width-this.bottomWidth)/2,this.dx=this._getDx(),this.dy=this._getDy()}},{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:"_getSettings",value:function(e){var i=l.extend({},n.defaults);return i.chart.width=parseInt(t.select(this.selector).style("width"),10),i.chart.height=parseInt(t.select(this.selector).style("height"),10),i=l.extend(i,e),i.chart.width<=0&&(i.chart.width=n.defaults.chart.width),i.chart.height<=0&&(i.chart.height=n.defaults.chart.height),i}},{key:"_setBlocks",value:function(t){var e=this._getTotalCount(t);this.blocks=this._standardizeData(t,e)}},{key:"_getTotalCount",value:function(t){var e=this,i=0;return t.forEach(function(t){i+=e._getRawBlockCount(t)}),i}},{key:"_standardizeData",value:function(t,e){var i=this,n=[],h=void 0,o=void 0,a=void 0;return t.forEach(function(t,l){h=i._getRawBlockCount(t),o=h/e,a=t[0],n.push({index:l,value:h,ratio:o,height:i.height*o,formatted:i.labelFormatter.format(a,h),fill:i.colorizer.getBlockFill(t,l),label:{raw:a,formatted:i.labelFormatter.format(a,h),color:i.colorizer.getLabelFill(t,l)}})}),n}},{key:"_getRawBlockCount",value:function(t){return Array.isArray(t[1])?t[1][0]:t[1]}},{key:"_getDx",value:function(){return this.bottomPinch>0?this.bottomLeftX/(this.blocks.length-this.bottomPinch):this.bottomLeftX/this.blocks.length}},{key:"_getDy",value:function(){return this.isCurved?(this.height-this.curveHeight)/this.blocks.length:this.height/this.blocks.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,e=[],i=this.dx,n=this.dy,h=0,o=this.width,a=0;this.isInverted&&(h=this.bottomLeftX,o=this.width-this.bottomLeftX);var l=0,s=0,r=0,c=this.width/2;this.isCurved&&(a=10);var u=this.height;this.minHeight!==!1&&(u=this.height-this.minHeight*this.blocks.length);var d=this.height;this.blocks.forEach(function(e,i){t.bottomPinch>0&&(t.isInverted?i<t.bottomPinch&&(d-=e.height):i>=t.blocks.length-t.bottomPinch&&(d-=e.height))});var f=2*d/(this.width-this.bottomWidth);return this.blocks.forEach(function(d,v){t.dynamicHeight&&(n=u*d.ratio,t.minHeight!==!1&&(n+=t.minHeight),t.isCurved&&(n-=t.curveHeight/t.blocks.length),l=(a+n)/f,t.isInverted&&(l=(a+n-t.height)/(-1*f)),0===t.bottomWidth&&v===t.blocks.length-1&&(l=t.width/2,t.isInverted&&(l=0)),t.bottomWidth===t.width&&(l=h),i=l-h,t.isInverted&&(i=h-l)),t.bottomPinch>0&&(t.isInverted?(t.dynamicHeight||(i=t.dx),i=v<t.bottomPinch?0:i):v>=t.blocks.length-t.bottomPinch&&(i=0)),l=h+i,s=o-i,r=a+n,t.isInverted&&(l=h-i,s=o+i),t.isCurved?e.push([[h,a,"M"],[c,a+(t.curveHeight-10),"Q"],[o,a,""],[s,r,"L"],[s,r,"M"],[c,r+t.curveHeight,"Q"],[l,r,""],[h,a,"L"]]):e.push([[h,a,"M"],[o,a,"L"],[s,r,"L"],[l,r,"L"],[h,a,"L"]]),h=l,o=s,a=r}),e}},{key:"_defineColorGradients",value:function(t){var e=t.append("defs");this.blocks.forEach(function(t,i){var n=t.fill,o=h.shade(n,-.25),a=e.append("linearGradient").attr({id:"gradient-"+i}),l=[[0,o],[40,n],[60,n],[100,o]];l.forEach(function(t){a.append("stop").attr({offset:t[0]+"%",style:"stop-color:"+t[1]})})})}},{key:"_drawTopOval",value:function(t,e){var i=0,n=this.width,o=this.width/2;this.isInverted&&(i=this.bottomLeftX,n=this.width-this.bottomLeftX);var a=e[0],l=a[1][1]+this.curveHeight-10,s=this.navigator.plot([["M",i,a[0][1]],["Q",o,l],[" ",n,a[2][1]],["M",n,10],["Q",o,0],[" ",i,10]]);t.append("path").attr("fill",h.shade(this.blocks[0].fill,-.4)).attr("d",s)}},{key:"_drawBlock",value:function(t){var e=this;if(t!==this.blocks.length){var i=this.svg.append("g"),n=this._getBlockPath(i,t);n.data(this._getD3Data(t)),this.animation!==!1?n.transition().duration(this.animation).ease("linear").attr("fill",this._getFillColor(t)).attr("d",this._getPathDefinition(t)).each("end",function(){e._drawBlock(t+1)}):(n.attr("fill",this._getFillColor(t)).attr("d",this._getPathDefinition(t)),this._drawBlock(t+1)),this.hoverEffects&&n.on("mouseover",this._onMouseOver).on("mouseout",this._onMouseOut),null!==this.onBlockClick&&n.on("click",this.onBlockClick),this._addBlockLabel(i,t)}}},{key:"_getBlockPath",value:function(t,e){var i=t.append("path");return this.animation!==!1&&this._addBeforeTransition(i,e),i}},{key:"_addBeforeTransition",value:function(t,e){var i=this.blockPaths[e],n="",h="";n=this.isCurved?this.navigator.plot([["M",i[0][0],i[0][1]],["Q",i[1][0],i[1][1]],[" ",i[2][0],i[2][1]],["L",i[2][0],i[2][1]],["M",i[2][0],i[2][1]],["Q",i[1][0],i[1][1]],[" ",i[0][0],i[0][1]]]):this.navigator.plot([["M",i[0][0],i[0][1]],["L",i[1][0],i[1][1]],["L",i[1][0],i[1][1]],["L",i[0][0],i[0][1]]]),h="solid"===this.fillType&&e>0?this._getFillColor(e-1):this._getFillColor(e),t.attr("d",n).attr("fill",h)}},{key:"_getD3Data",value:function(t){return[this.blocks[t]]}},{key:"_getFillColor",value:function(t){return"solid"===this.fillType?this.blocks[t].fill:"url(#gradient-"+t+")"}},{key:"_getPathDefinition",value:function(t){var e=[];return this.blockPaths[t].forEach(function(t){e.push([t[2],t[0],t[1]])}),this.navigator.plot(e)}},{key:"_onMouseOver",value:function(e){t.select(this).attr("fill",h.shade(e.fill,-.2))}},{key:"_onMouseOut",value:function(e){t.select(this).attr("fill",e.fill)}},{key:"_addBlockLabel",value:function(t,e){var i=this.blockPaths[e],n=this.blocks[e].label.formatted,h=this.blocks[e].label.color,o=this.width/2,a=this._getTextY(i);t.append("text").text(n).attr({x:o,y:a,"text-anchor":"middle","dominant-baseline":"middle",fill:h,"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.blocks.length:(t[1][1]+t[2][1])/2}}]),n}(),h=function(){function t(){e(this,t),this.hexExpression=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i,this.labelFill=null,this.scale=null}return i(t,[{key:"setLabelFill",value:function(t){this.labelFill=t}},{key:"setScale",value:function(t){this.scale=t}},{key:"getBlockFill",value:function(t,e){return t.length>2&&this.hexExpression.test(t[2])?t[2]:Array.isArray(this.scale)?this.scale[e]:this.scale(e)}},{key:"getLabelFill",value:function(t){return t.length>3&&this.hexExpression.test(t[3])?t[3]:this.labelFill}}],[{key:"shade",value:function(e,i){var n=e.slice(1);3===n.length&&(n=t.expandHex(n));var h=parseInt(n,16),o=0>i?0:255,a=0>i?-1*i:i,l=h>>16,s=h>>8&255,r=255&h,c=16777216+65536*(Math.round((o-l)*a)+l)+256*(Math.round((o-s)*a)+s)+(Math.round((o-r)*a)+r);return"#"+c.toString(16).slice(1)}},{key:"expandHex",value:function(t){return t[0]+t[0]+t[1]+t[1]+t[2]+t[2]}}]),t}(),o=function(){function t(){e(this,t),this.expression=null}return i(t,[{key:"setFormat",value:function(t){"function"==typeof t?this.formatter=t:(this.expression=t,this.formatter=this.stringFormatter)}},{key:"format",value:function(t,e){return Array.isArray(e)?this.formatter(t,e[0],e[1]):this.formatter(t,e,null)}},{key:"stringFormatter",value:function(t,e){var i=arguments.length<=2||void 0===arguments[2]?null:arguments[2],n=i;return null===i&&(n=this.getDefaultFormattedValue(e)),this.expression.split("{l}").join(t).split("{v}").join(e).split("{f}").join(n)}},{key:"getDefaultFormattedValue",value:function(t){return t.toLocaleString()}}]),t}(),a=function(){function t(){e(this,t)}return i(t,[{key:"plot",value:function(t){var e="";return t.forEach(function(t){e+=t[0]+t[1]+","+t[2]+" "}),e.replace(/ +/g," ").trim()}}]),t}(),l=function(){function t(){e(this,t)}return i(t,null,[{key:"extend",value:function(e,i){var n=void 0;for(n in i)i.hasOwnProperty(n)&&("object"!=typeof i[n]||Array.isArray(i[n])?e[n]=i[n]:"object"!=typeof e[n]||Array.isArray(e[n])?e[n]=t.extend({},i[n]):e[n]=t.extend(e[n],i[n]));return e}}]),t}();return n});
var gulp = require('gulp');
var umd = require('gulp-wrap-umd');
var concat = require('gulp-concat');
var jshint = require('gulp-jshint');
var jscs = require('gulp-jscs');
var eslint = require('gulp-eslint');
var babel = require('gulp-babel');

@@ -17,2 +16,3 @@ var mocha = require('gulp-mocha-phantomjs');

'./src/d3-funnel/d3-funnel.js',
'./src/d3-funnel/colorizer.js',
'./src/d3-funnel/label-formatter.js',

@@ -34,8 +34,5 @@ './src/d3-funnel/navigator.js',

return gulp.src(src)
.pipe(jshint())
.pipe(jshint.reporter('default'))
.pipe(jshint.reporter('fail'))
.pipe(jscs({
configPath: './.jscsrc',
}));
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failOnError());
});

@@ -46,3 +43,5 @@

.pipe(concat('d3-funnel.js'))
.pipe(babel())
.pipe(babel({
stage: 0,
}))
.pipe(umd(umdOptions))

@@ -49,0 +48,0 @@ .pipe(gulp.dest('./compiled/'));

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

@@ -14,10 +14,12 @@ "author": "Jake Zatecky",

"devDependencies": {
"babel-eslint": "^4.1.3",
"chai": "^3.2.0",
"chai-spies": "^0.7.0",
"eslint": "^1.6.0",
"eslint-config-airbnb": "^0.1.0",
"gulp": "^3.9.0",
"gulp-babel": "^5.2.1",
"gulp-concat": "^2.6.0",
"gulp-eslint": "^1.0.0",
"gulp-header": "^1.7.1",
"gulp-jscs": "^2.0.0",
"gulp-jshint": "^1.11.2",
"gulp-mocha-phantomjs": "^0.10.1",

@@ -27,2 +29,3 @@ "gulp-rename": "^1.2.2",

"gulp-wrap-umd": "^0.2.1",
"lodash": "^3.10.1",
"mocha": "^2.3.2"

@@ -29,0 +32,0 @@ },

@@ -58,20 +58,21 @@ # D3 Funnel

| 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` |
| Option | Description | Type | Default |
| --------------------- | --------------------------------------------------------------------------- | -------------- | ----------------------- |
| `chart.width` | The pixel width of the chart. | int | Container's width |
| `chart.height` | The pixel height of the chart. | int | Container's height |
| `chart.bottomWidth` | The percent of total width the bottom should be. | float | `1 / 3` |
| `chart.bottomPinch` | How many blocks to pinch on the bottom to create a "neck". | int | `0` |
| `chart.inverted` | Whether the funnel is inverted (like a pyramid). | bool | `false` |
| `chart.animate` | The load animation speed in milliseconds. | int/bool | `false` |
| `chart.curve.enabled` | Whether the funnel is curved. | bool | `false` |
| `chart.curve.height` | The curvature amount. | int | `20` |
| `block.dynamicHeight` | Whether the block heights are proportional to its weight. | bool | `false` |
| `block.fill.scale` | The block background color scale. Expects an index and returns a color. | function/array | `d3.scale.category10()` |
| `block.fill.type` | Either `'solid'` or `'gradient'`. | string | `'solid'` |
| `block.minHeight` | The minimum pixel height of a block. | int/bool | `false` |
| `block.highlight` | Whether the blocks are highlighted on hover. | 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}'` |
| `events.click.block` | Callback for when a block is clicked. | function | `null` |

@@ -89,2 +90,27 @@ ### Label Format

### Overriding Defaults
You may wish to override the default chart options. For example, you may wish
for every funnel to have proportional heights. To do this, simplify modify the
`D3Funnel.defaults` property:
``` javascript
D3Funnel.defaults.block.dynamicHeight = true;
```
Should you wish to override multiple properties at a time, you may consider
using [lodash's][lodash-merge] `_.merge` or [jQuery's][jquery-extend] `$.extend`:
``` javascript
D3Funnel.defaults = _.merge(D3Funnel.defaults, {
chart: {
dynamicHeight: true,
animate: 200,
},
label: {
format: '{l}: ${f}',
},
});
```
## API

@@ -100,3 +126,4 @@

You can specify overriding colors for any data point (hex only):
You can specify colors to override `block.fill.scale` and `label.fill` for any
data point (hex only):

@@ -131,1 +158,3 @@ ``` javascript

[examples]: http://jakezatecky.github.io/d3-funnel/
[jQuery-extend]: https://api.jquery.com/jquery.extend/
[lodash-merge]: https://lodash.com/docs#merge

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

/* global d3, LabelFormatter, Navigator, Utils */
/* global d3, Colorizer, LabelFormatter, Navigator, Utils */
/* exported D3Funnel */

@@ -6,2 +6,36 @@

static defaults = {
chart: {
width: 350,
height: 400,
bottomWidth: 1 / 3,
bottomPinch: 0,
inverted: false,
animate: false,
curve: {
enabled: false,
height: 20,
},
},
block: {
dynamicHeight: false,
fill: {
scale: d3.scale.category10().domain(d3.range(0, 10)),
type: 'solid',
},
minHeight: false,
highlight: false,
},
label: {
fontSize: '14px',
fill: '#fff',
format: '{l}: {f}',
},
events: {
click: {
block: null,
},
},
};
/**

@@ -15,23 +49,3 @@ * @param {string} selector A selector for the container element.

// 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.colorizer = new Colorizer();

@@ -84,4 +98,2 @@ this.labelFormatter = new LabelFormatter();

this._setData(data);
let settings = this._getSettings(options);

@@ -93,16 +105,25 @@

// Set color scales
this.colorizer.setLabelFill(settings.label.fill);
this.colorizer.setScale(settings.block.fill.scale);
// 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;
this.width = settings.chart.width;
this.height = settings.chart.height;
this.bottomWidth = settings.chart.width * settings.chart.bottomWidth;
this.bottomPinch = settings.chart.bottomPinch;
this.isInverted = settings.chart.inverted;
this.isCurved = settings.chart.curve.enabled;
this.curveHeight = settings.chart.curve.height;
this.fillType = settings.block.fill.type;
this.hoverEffects = settings.block.highlight;
this.dynamicHeight = settings.block.dynamicHeight;
this.minHeight = settings.block.minHeight;
this.animation = settings.chart.animate;
// Support for events
this.onBlockClick = settings.events.click.block;
this._setBlocks(data);
// Calculate the bottom left x position

@@ -116,5 +137,2 @@ this.bottomLeftX = (this.width - this.bottomWidth) / 2;

this.dy = this._getDy();
// Support for events
this.onItemClick = settings.onItemClick;
}

@@ -137,2 +155,32 @@

/**
* @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({}, D3Funnel.defaults);
settings.chart.width = parseInt(d3.select(this.selector).style('width'), 10);
settings.chart.height = parseInt(d3.select(this.selector).style('height'), 10);
// 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.chart.width <= 0) {
settings.chart.width = D3Funnel.defaults.chart.width;
}
if (settings.chart.height <= 0) {
settings.chart.height = D3Funnel.defaults.chart.height;
}
return settings;
}
/**
* Register the raw data into a standard block format and pre-calculate
* some values.
*
* @param {Array} data

@@ -142,62 +190,82 @@ *

*/
_setData(data) {
this.data = data;
_setBlocks(data) {
let totalCount = this._getTotalCount(data);
this._setColors();
this.blocks = this._standardizeData(data, totalCount);
}
/**
* Set the colors for each block.
* Return the total count of all blocks.
*
* @return {void}
* @return {Number}
*/
_setColors() {
let colorScale = d3.scale.category10();
let hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
_getTotalCount(data) {
let total = 0;
// 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);
}
data.forEach((block) => {
total += this._getRawBlockCount(block);
});
return total;
}
/**
* @param {Object} options
* Convert the raw data into a standardized format.
*
* @returns {Object}
* @param {Array} data
* @param {Number} totalCount
*
* @return {Array}
*/
_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);
_standardizeData(data, totalCount) {
let standardized = [];
// Overwrite default settings with user options
settings = Utils.extend(settings, options);
let count;
let ratio;
let label;
// 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;
}
data.forEach((block, index) => {
count = this._getRawBlockCount(block);
ratio = count / totalCount;
label = block[0];
return settings;
standardized.push({
index: index,
value: count,
ratio: ratio,
height: this.height * ratio,
formatted: this.labelFormatter.format(label, count),
fill: this.colorizer.getBlockFill(block, index),
label: {
raw: label,
formatted: this.labelFormatter.format(label, count),
color: this.colorizer.getLabelFill(block, index),
},
});
});
return standardized;
}
/**
* Given a raw data block, return its count.
*
* @param {Array} block
*
* @return {Number}
*/
_getRawBlockCount(block) {
return Array.isArray(block[1]) ? block[1][0] : block[1];
}
/**
* @return {Number}
*/
_getDx() {
// Will be sharper if there is a pinch
if (this.bottomPinch > 0) {
return this.bottomLeftX / (this.data.length - this.bottomPinch);
return this.bottomLeftX / (this.blocks.length - this.bottomPinch);
}
return this.bottomLeftX / this.data.length;
return this.bottomLeftX / this.blocks.length;
}

@@ -211,6 +279,6 @@

if (this.isCurved) {
return (this.height - this.curveHeight) / this.data.length;
return (this.height - this.curveHeight) / this.blocks.length;
}
return this.height / this.data.length;
return this.height / this.blocks.length;
}

@@ -282,8 +350,4 @@

let topBase = this.width;
let bottomBase = 0;
let totalHeight = this.height;
let totalArea = this.height * (this.width + this.bottomWidth) / 2;
let slope = 2 * this.height / (this.width - this.bottomWidth);
// This is greedy in that the block will have a guaranteed height

@@ -293,47 +357,76 @@ // and the remaining is shared among the ratio, instead of being

if (this.minHeight !== false) {
totalArea = (this.height - this.minHeight * this.data.length) * (this.width + this.bottomWidth) / 2;
totalHeight = this.height - this.minHeight * this.blocks.length;
}
let totalCount = 0;
let count = 0;
let slopeHeight = this.height;
// Harvest total count
this.data.forEach((block) => {
totalCount += Array.isArray(block[1]) ? block[1][0] : block[1];
// Correct slope height if there are blocks being pinched (and thus
// requiring a sharper curve)
this.blocks.forEach((block, i) => {
if (this.bottomPinch > 0) {
if (this.isInverted) {
if (i < this.bottomPinch) {
slopeHeight -= block.height;
}
} else if (i >= this.blocks.length - this.bottomPinch) {
slopeHeight -= block.height;
}
}
});
// The slope will determine the where the x points on each block
// iteration
let slope = 2 * slopeHeight / (this.width - this.bottomWidth);
// 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];
this.blocks.forEach((block, i) => {
// Make heights proportional to block weight
if (this.dynamicHeight) {
// Slice off the height proportional to this block
dy = totalHeight * block.ratio;
// Calculate dynamic shapes based on area
if (this.dynamicArea) {
let ratio = count / totalCount;
let area = ratio * totalArea;
// Add greedy minimum height
if (this.minHeight !== false) {
area += this.minHeight * (this.width + this.bottomWidth) / 2;
dy += this.minHeight;
}
bottomBase = Math.sqrt((slope * topBase * topBase - (4 * area)) / slope);
// Account for any curvature
if (this.isCurved) {
dy = dy - (this.curveHeight / this.blocks.length);
}
// Prevent bottm points from becomming NaN
if (this.bottomWidth === 0 && i === this.data.length - 1) {
bottomBase = 0;
// Given: y = mx + b
// Given: b = 0 (when funnel), b = this.height (when pyramid)
// For funnel, x_i = y_i / slope
nextLeftX = (prevHeight + dy) / slope;
// For pyramid, x_i = y_i - this.height / -slope
if (this.isInverted) {
nextLeftX = (prevHeight + dy - this.height) / (-1 * slope);
}
// Prevent NaN slope
// If bottomWidth is 0, adjust last x position (to circumvent
// errors associated with rounding)
if (this.bottomWidth === 0 && i === this.blocks.length - 1) {
// For funnel, last position is the center
nextLeftX = this.width / 2;
// For pyramid, last position is the origin
if (this.isInverted) {
nextLeftX = 0;
}
}
// If bottomWidth is same as width, stop x velocity
if (this.bottomWidth === this.width) {
bottomBase = topBase;
nextLeftX = prevLeftX;
}
dx = (topBase / 2) - (bottomBase / 2);
dy = (area * 2) / (topBase + bottomBase);
// Calculate the shift necessary for both x points
dx = nextLeftX - prevLeftX;
if (this.isCurved) {
dy = dy - (this.curveHeight / this.data.length);
if (this.isInverted) {
dx = prevLeftX - nextLeftX;
}
topBase = bottomBase;
}

@@ -346,3 +439,3 @@

if (!this.isInverted) {
if (i >= this.data.length - this.bottomPinch) {
if (i >= this.blocks.length - this.bottomPinch) {
dx = 0;

@@ -355,5 +448,5 @@ }

// static area's (prevents zero velocity if isInverted
// and bottomPinch are non trivial and dynamicArea is
// and bottomPinch are non trivial and dynamicHeight is
// false)
if (!this.dynamicArea) {
if (!this.dynamicHeight) {
dx = this.dx;

@@ -429,5 +522,5 @@ }

// Create a gradient for each block
this.data.forEach((block, index) => {
let color = block[2];
let shade = Utils.shadeColor(color, -0.25);
this.blocks.forEach((block, index) => {
let color = block.fill;
let shade = Colorizer.shade(color, -0.25);

@@ -491,3 +584,3 @@ // Create linear gradient

svg.append('path')
.attr('fill', Utils.shadeColor(this.data[0][2], -0.4))
.attr('fill', Colorizer.shade(this.blocks[0].fill, -0.4))
.attr('d', path);

@@ -504,3 +597,3 @@ }

_drawBlock(index) {
if (index === this.data.length) {
if (index === this.blocks.length) {
return;

@@ -514,3 +607,3 @@ }

let path = this._getBlockPath(group, index);
path.data(this._getBlockData(index));
path.data(this._getD3Data(index));

@@ -522,3 +615,3 @@ // Add animation components

.ease('linear')
.attr('fill', this._getColor(index))
.attr('fill', this._getFillColor(index))
.attr('d', this._getPathDefinition(index))

@@ -529,3 +622,3 @@ .each('end', () => {

} else {
path.attr('fill', this._getColor(index))
path.attr('fill', this._getFillColor(index))
.attr('d', this._getPathDefinition(index));

@@ -542,4 +635,4 @@ this._drawBlock(index + 1);

// ItemClick event
if (this.onItemClick !== null) {
path.on('click', this.onItemClick);
if (this.onBlockClick !== null) {
path.on('click', this.onBlockClick);
}

@@ -602,7 +695,7 @@

// 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)
if (this.fillType === 'solid' && index > 0) {
beforeFill = this._getFillColor(index - 1);
// Otherwise use current background
} else {
beforeFill = this._getColor(index);
beforeFill = this._getFillColor(index);
}

@@ -615,2 +708,4 @@

/**
* Return d3 formatted data for the given block.
*
* @param {int} index

@@ -620,18 +715,8 @@ *

*/
_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),
}];
_getD3Data(index) {
return [this.blocks[index]];
}
/**
* Return the color for the given index.
* Return the block fill color for the given index.
*

@@ -642,8 +727,8 @@ * @param {int} index

*/
_getColor(index) {
_getFillColor(index) {
if (this.fillType === 'solid') {
return this.data[index][2];
} else {
return 'url(#gradient-' + index + ')';
return this.blocks[index].fill;
}
return 'url(#gradient-' + index + ')';
}

@@ -672,3 +757,3 @@

_onMouseOver(data) {
d3.select(this).attr('fill', Utils.shadeColor(data.baseColor, -0.2));
d3.select(this).attr('fill', Colorizer.shade(data.fill, -0.2));
}

@@ -694,4 +779,4 @@

let label = this._getBlockData(index)[0].formatted;
let fill = this.data[index][3] || this.label.fill;
let text = this.blocks[index].label.formatted;
let fill = this.blocks[index].label.color;

@@ -702,3 +787,3 @@ let x = this.width / 2; // Center the text

group.append('text')
.text(label)
.text(text)
.attr({

@@ -725,3 +810,3 @@ 'x': x,

if (this.isCurved) {
return (paths[2][1] + paths[3][1]) / 2 + (this.curveHeight / this.data.length);
return (paths[2][1] + paths[3][1]) / 2 + (this.curveHeight / this.blocks.length);
}

@@ -728,0 +813,0 @@

@@ -43,5 +43,5 @@ /* exported LabelFormatter */

return this.formatter(label, value[0], value[1]);
} else {
return this.formatter(label, value, null);
}
return this.formatter(label, value, null);
}

@@ -63,6 +63,8 @@

stringFormatter(label, value, fValue = null) {
let formatted = fValue;
// Attempt to use supplied formatted value
// Otherwise, use the default
if (fValue === null) {
fValue = this.getDefaultFormattedValue(value);
formatted = this.getDefaultFormattedValue(value);
}

@@ -73,3 +75,3 @@

.split('{v}').join(value)
.split('{f}').join(fValue);
.split('{f}').join(formatted);
}

@@ -76,0 +78,0 @@

@@ -13,3 +13,3 @@ /* exported Navigator */

plot(commands) {
var path = '';
let path = '';

@@ -16,0 +16,0 @@ commands.forEach((command) => {

/* exported Utils */
/* jshint bitwise: false */

@@ -22,4 +21,8 @@ /**

if (b.hasOwnProperty(prop)) {
if (typeof a[prop] === 'object' && typeof b[prop] === 'object') {
a[prop] = Utils.extend(a[prop], b[prop]);
if (typeof b[prop] === 'object' && !Array.isArray(b[prop])) {
if (typeof a[prop] === 'object' && !Array.isArray(a[prop])) {
a[prop] = Utils.extend(a[prop], b[prop]);
} else {
a[prop] = Utils.extend({}, b[prop]);
}
} else {

@@ -34,44 +37,2 @@ a[prop] = b[prop];

/**
* 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}
*/
static shadeColor(color, shade) {
let hex = color.slice(1);
if (hex.length === 3) {
hex = Utils.expandHex(hex);
}
let f = parseInt(hex, 16);
let t = shade < 0 ? 0 : 255;
let p = shade < 0 ? shade * -1 : shade;
let R = f >> 16;
let G = f >> 8 & 0x00FF;
let B = f & 0x0000FF;
let 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}
*/
static expandHex(hex) {
return hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
}
/* global d3, assert, chai, D3Funnel */
function getFunnel() {
return new D3Funnel('#funnel');
}
function getSvg() {
return d3.select('#funnel').selectAll('svg');
}
function getBasicData() {
return [['Node', 1000]];
}
function getPathHeight(path) {
var commands = path.attr('d').split(' ');
return getCommandHeight(commands[2]) - getCommandHeight(commands[0]);
}
function getCommandHeight(command) {
return parseFloat(command.split(',')[1]);
}
var defaults = _.clone(D3Funnel.defaults, true);
describe('D3Funnel', function () {
var getFunnel, getSvg, getBasicData, getPathHeight, getCommandHeight;
beforeEach(function (done) {
getFunnel = function () {
return new D3Funnel('#funnel');
};
getSvg = function () {
return d3.select('#funnel').selectAll('svg');
};
getBasicData = function () {
return [['Node', 1000]];
};
getPathHeight = function (path) {
var commands = path.attr('d').split(' ');
d3.select('#funnel').attr('style', null);
return getCommandHeight(commands[2]) - getCommandHeight(commands[0]);
};
getCommandHeight = function (command) {
return parseFloat(command.split(',')[1]);
};
D3Funnel.defaults = _.clone(defaults, true);

@@ -80,3 +88,3 @@ done();

colorScale = d3.scale.category10();
colorScale = d3.scale.category10().domain(d3.range(0, 10));

@@ -120,7 +128,27 @@ assert.equal('#111', d3.select(paths[0]).attr('fill'));

describe('defaults', function () {
it('should affect all default options', function () {
D3Funnel.defaults.label.fill = '#777';
getFunnel().draw(getBasicData(), {});
assert.isTrue(d3.select('#funnel text').attr('fill').indexOf('#777') > -1);
});
});
describe('options', function () {
describe('width', function () {
describe('chart.width', function () {
it ('should default to the container\'s width', function () {
d3.select('#funnel').style('width', '250px');
getFunnel().draw(getBasicData(), {});
assert.equal(250, getSvg().node().getBBox().width);
});
it('should set the funnel\'s width to the specified amount', function () {
getFunnel().draw(getBasicData(), {
width: 200,
chart: {
width: 200,
},
});

@@ -132,6 +160,16 @@

describe('height', function () {
describe('chart.height', function () {
it ('should default to the container\'s height', function () {
d3.select('#funnel').style('height', '250px');
getFunnel().draw(getBasicData(), {});
assert.equal(250, getSvg().node().getBBox().height);
});
it('should set the funnel\'s height to the specified amount', function () {
getFunnel().draw(getBasicData(), {
height: 200,
chart: {
height: 200,
},
});

@@ -143,6 +181,10 @@

describe('isCurved', function () {
describe('chart.curve.enabled', function () {
it('should create an additional path on top of the trapezoids', function () {
getFunnel().draw(getBasicData(), {
isCurved: true,
chart: {
curve: {
enabled: true,
},
},
});

@@ -155,3 +197,7 @@

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

@@ -169,46 +215,3 @@

describe('fillType', function () {
it('should create gradients when set to \'gradient\'', function () {
getFunnel().draw(getBasicData(), {
fillType: 'gradient',
});
// Cannot try to re-select the camelCased linearGradient element
// due to a Webkit bug in the current PhantomJS; workaround is
// to select the known ID of the linearGradient element
// https://bugs.webkit.org/show_bug.cgi?id=83438
assert.equal(1, d3.selectAll('#funnel defs #gradient-0')[0].length);
assert.equal('url(#gradient-0)', d3.select('#funnel path').attr('fill'));
});
it('should use solid fill when not set to \'gradient\'', function () {
getFunnel().draw(getBasicData(), {});
// Check for valid hex string
assert.isTrue(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(
d3.select('#funnel path').attr('fill')
));
});
});
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 () {
describe('block.dynamicHeight', function () {
it('should use equal heights when false', function () {

@@ -221,3 +224,5 @@ var paths;

], {
height: 300,
chart: {
height: 300,
},
});

@@ -238,4 +243,8 @@

], {
height: 300,
dynamicArea: true,
chart: {
height: 300,
},
block: {
dynamicHeight: true,
},
});

@@ -245,4 +254,4 @@

assert.equal(72, parseInt(getPathHeight(d3.select(paths[0])), 10));
assert.equal(227, parseInt(getPathHeight(d3.select(paths[1])), 10));
assert.equal(100, parseInt(getPathHeight(d3.select(paths[0])), 10));
assert.equal(200, parseInt(getPathHeight(d3.select(paths[1])), 10));
});

@@ -260,5 +269,9 @@

], {
height: 300,
dynamicArea: true,
bottomWidth: 0,
chart: {
height: 300,
bottomWidth: 0,
},
block: {
dynamicHeight: true,
},
});

@@ -278,5 +291,9 @@

], {
height: 300,
dynamicArea: true,
bottomWidth: 1,
chart: {
height: 300,
bottomWidth: 1,
},
block: {
dynamicHeight: true,
},
});

@@ -289,2 +306,141 @@

describe('block.fill.scale', function () {
it('should use a function\'s return value', function () {
getFunnel().draw([
['A', 1],
['B', 2],
], {
block: {
fill: {
scale: function (index) {
if (index === 0) {
return '#111';
}
return '#222';
},
},
},
});
paths = getSvg().selectAll('path')[0];
assert.equal('#111', d3.select(paths[0]).attr('fill'));
assert.equal('#222', d3.select(paths[1]).attr('fill'));
});
it('should use an array\'s return value', function () {
getFunnel().draw([
['A', 1],
['B', 2],
], {
block: {
fill: {
scale: ['#111', '#222'],
},
},
});
paths = getSvg().selectAll('path')[0];
assert.equal('#111', d3.select(paths[0]).attr('fill'));
assert.equal('#222', d3.select(paths[1]).attr('fill'));
});
});
describe('block.fill.type', function () {
it('should create gradients when set to \'gradient\'', function () {
getFunnel().draw(getBasicData(), {
block: {
fill: {
type: 'gradient',
},
},
});
// Cannot try to re-select the camelCased linearGradient element
// due to a Webkit bug in the current PhantomJS; workaround is
// to select the known ID of the linearGradient element
// https://bugs.webkit.org/show_bug.cgi?id=83438
assert.equal(1, d3.selectAll('#funnel defs #gradient-0')[0].length);
assert.equal('url(#gradient-0)', d3.select('#funnel path').attr('fill'));
});
it('should use solid fill when not set to \'gradient\'', function () {
getFunnel().draw(getBasicData(), {});
// Check for valid hex string
assert.isTrue(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(
d3.select('#funnel path').attr('fill')
));
});
});
describe('block.minHeight', function () {
it('should give each block the minimum height specified', function () {
var paths;
getFunnel().draw([
['A', 299],
['B', 1],
], {
chart: {
height: 300,
},
block: {
dynamicHeight: true,
minHeight: 10,
},
});
paths = d3.selectAll('#funnel path')[0];
assert.isAbove(parseFloat(getPathHeight(d3.select(paths[0]))), 10);
assert.isAbove(parseFloat(getPathHeight(d3.select(paths[1]))), 10);
});
it('should decrease the height of blocks above the minimum', function () {
var paths;
getFunnel().draw([
['A', 299],
['B', 1],
], {
chart: {
height: 300,
},
block: {
dynamicHeight: true,
minHeight: 10,
},
});
paths = d3.selectAll('#funnel path')[0];
assert.isBelow(parseFloat(getPathHeight(d3.select(paths[0]))), 290);
});
});
describe('block.highlight', 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'],
], {
block: {
highlight: true,
},
});
d3.select('#funnel path').node().dispatchEvent(event);
// #fff * -1/5 => #cccccc
assert.equal('#cccccc', d3.select('#funnel path').attr('fill'));
});
});
describe('label.fontSize', function () {

@@ -319,3 +475,3 @@ it('should set the label\'s font size to the specified amount', function () {

format: '{l} {v} {f}',
}
},
});

@@ -342,3 +498,3 @@

describe('onItemClick', function () {
describe('events.click.block', function () {
it('should invoke the callback function with the correct data', function () {

@@ -351,8 +507,12 @@ var event = document.createEvent('CustomEvent');

getFunnel().draw(getBasicData(), {
onItemClick: function (d, i) {
spy({
index: d.index,
label: d.label,
value: d.value,
}, i);
events: {
click: {
block: function (d, i) {
spy({
index: d.index,
label: d.label.raw,
value: d.value,
}, i);
},
},
},

@@ -359,0 +519,0 @@ });

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

* Make test case for container's default dimensions
* Document the block object available on click (and hover)
* Document ability to set block color scale
* Standardize option names
* Fix bug with hover overriding gradient
* Add tests for EACH option
* Allow user to specify defaults

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